diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..96824f8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+# PHP PSR-2 Coding Standards
+# http://www.php-fig.org/psr/psr-2/
+[*.php]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
\ No newline at end of file
diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml
new file mode 100644
index 0000000..53b8176
--- /dev/null
+++ b/.github/workflows/php-lint.yml
@@ -0,0 +1,64 @@
+name: PHP Code Linting
+
+on:
+ push:
+ branches:
+ - main
+ # Only run if PHP-related files changed.
+ paths:
+ - '.github/workflows/php-lint.yml'
+ - '**.php'
+ - 'phpcs.xml.dist'
+ - 'phpmd.xml'
+ - 'phpstan.neon.dist'
+ - 'composer.json'
+ - 'composer.lock'
+ pull_request:
+ branches:
+ - main
+ # Only run if PHP-related files changed.
+ paths:
+ - '.github/workflows/php-lint.yml'
+ - '**.php'
+ - 'phpcs.xml.dist'
+ - 'phpmd.xml'
+ - 'phpstan.neon.dist'
+ - 'composer.json'
+ - 'composer.lock'
+ types:
+ - opened
+ - reopened
+ - synchronize
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+ php-lint:
+ name: PHP
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.0'
+
+ - name: Validate Composer configuration
+ run: composer validate
+
+ - name: Install PHP dependencies
+ uses: ramsey/composer-install@83af392bf5f031813d25e6fe4cd626cdba9a2df6
+ with:
+ composer-options: '--prefer-dist --no-progress --no-interaction'
+
+ - name: PHP Lint
+ run: composer lint
+
+ - name: PHPStan
+ run: composer phpstan
+
+ - name: PHPMD
+ run: composer phpmd
diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml
new file mode 100644
index 0000000..a17907e
--- /dev/null
+++ b/.github/workflows/php-test.yml
@@ -0,0 +1,54 @@
+name: PHP Unit Testing
+
+on:
+ push:
+ branches:
+ - main
+ # Only run if PHP-related files changed.
+ paths:
+ - '.github/workflows/php-test.yml'
+ - '**.php'
+ - 'phpunit.xml.dist'
+ - 'composer.json'
+ - 'composer.lock'
+ pull_request:
+ branches:
+ - main
+ # Only run if PHP-related files changed.
+ paths:
+ - '.github/workflows/php-test.yml'
+ - '**.php'
+ - 'phpunit.xml.dist'
+ - 'composer.json'
+ - 'composer.lock'
+ types:
+ - opened
+ - reopened
+ - synchronize
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+ php-test:
+ name: PHP
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.0'
+
+ - name: Validate Composer configuration
+ run: composer validate
+
+ - name: Install PHP dependencies
+ uses: ramsey/composer-install@83af392bf5f031813d25e6fe4cd626cdba9a2df6
+ with:
+ composer-options: '--prefer-dist --no-progress --no-interaction'
+
+ - name: PHPUnit Test
+ run: composer test
diff --git a/.gitignore b/.gitignore
index 668ec4f..b4a9189 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
node_modules/
.DS_Store
-lib/
\ No newline at end of file
+lib/
+vendor/
+*.cache
\ No newline at end of file
diff --git a/README.md b/README.md
index 792d4d4..b47cf99 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,10 @@
Third Party Capital is a resource that consolidates best practices for loading popular third-parties in a single place.
+The project provides implementations that can be used in the following languages:
+* JavaScript (see [`src` directory](./src))
+* PHP (see [`inc` directory](./inc))
+
## Rationale
There is a large, cross-functional Chrome initiative that aims to improve third-party resource loading on the web. One part of this effort is to provide a default set of recommendations, or "components," to developers. These components will help developers sequence and fetch popular third-party resources at the right time to minimize their overall impact to page performance.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..3514ccd
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,45 @@
+{
+ "name": "googlechromelabs/third-party-capital",
+ "description": "This package is a collection of classes and utilities that can be used to efficiently load third-party libraries into your PHP application.",
+ "type": "library",
+ "license": "Apache-2.0",
+ "require": {
+ "php": ">=7"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpmd/phpmd": "^2.9",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9",
+ "slevomat/coding-standard": "^8.9"
+ },
+ "scripts": {
+ "lint": "phpcs --standard=phpcs.xml.dist",
+ "format": "phpcbf --standard=phpcs.xml.dist",
+ "test": "phpunit -c phpunit.xml.dist --verbose",
+ "phpmd": "phpmd . text phpmd.xml",
+ "phpstan": "phpstan analyse --memory-limit=2048M"
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true,
+ "phpstan/extension-installer": true
+ }
+ },
+ "archive": {
+ "exclude": ["/.husky", "/config", "/src", "!/*.json"]
+ },
+ "autoload": {
+ "psr-4": {
+ "GoogleChromeLabs\\ThirdPartyCapital\\": "inc/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "GoogleChromeLabs\\ThirdPartyCapital\\TestData\\": "tests/phpunit/testdata",
+ "GoogleChromeLabs\\ThirdPartyCapital\\TestUtils\\": "tests/phpunit/utils"
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..9078756
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,3237 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "16bd045d073c0751ad48aa5d331e13f0",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "composer/pcre",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9",
+ "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.3",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-10-11T07:11:09+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "ced299686f41dce890debac69273b47ffe98a40c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c",
+ "reference": "ced299686f41dce890debac69273b47ffe98a40c",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T21:32:43+00:00"
+ },
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/composer-installer.git",
+ "reference": "4be43904336affa5c2f70744a348312336afd0da"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da",
+ "reference": "4be43904336affa5c2f70744a348312336afd0da",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0"
+ },
+ "require-dev": {
+ "composer/composer": "*",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "franck.nijhof@dealerdirect.com",
+ "homepage": "http://www.frenck.nl",
+ "role": "Developer / IT Manager"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "homepage": "http://www.dealerdirect.com",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcbf",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/composer-installer/issues",
+ "source": "https://github.com/PHPCSStandards/composer-installer"
+ },
+ "time": "2023-01-05T11:28:13+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.30 || ^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-30T00:15:36+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-03-08T13:26:56+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999",
+ "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0"
+ },
+ "time": "2023-12-10T21:03:43+00:00"
+ },
+ {
+ "name": "pdepend/pdepend",
+ "version": "2.16.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pdepend/pdepend.git",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.7",
+ "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/polyfill-mbstring": "^1.19"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0|^1.2.3",
+ "gregwar/rst": "^1.0",
+ "squizlabs/php_codesniffer": "^2.0.0"
+ },
+ "bin": [
+ "src/bin/pdepend"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PDepend\\": "src/main/php/PDepend"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Official version of pdepend to be handled with Composer",
+ "keywords": [
+ "PHP Depend",
+ "PHP_Depend",
+ "dev",
+ "pdepend"
+ ],
+ "support": {
+ "issues": "https://github.com/pdepend/pdepend/issues",
+ "source": "https://github.com/pdepend/pdepend/tree/2.16.2"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-17T18:09:59+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.3"
+ },
+ "time": "2021-07-20T11:28:43+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpcompatibility/php-compatibility",
+ "version": "9.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCompatibility/PHPCompatibility.git",
+ "reference": "9fb324479acf6f39452e0655d2429cc0d3914243"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243",
+ "reference": "9fb324479acf6f39452e0655d2429cc0d3914243",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "squizlabs/php_codesniffer": "^2.3 || ^3.0.2"
+ },
+ "conflict": {
+ "squizlabs/php_codesniffer": "2.6.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "suggest": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.",
+ "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Wim Godden",
+ "homepage": "https://github.com/wimg",
+ "role": "lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors"
+ }
+ ],
+ "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.",
+ "homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
+ "keywords": [
+ "compatibility",
+ "phpcs",
+ "standards"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
+ "source": "https://github.com/PHPCompatibility/PHPCompatibility"
+ },
+ "time": "2019-12-27T09:44:58+00:00"
+ },
+ {
+ "name": "phpmd/phpmd",
+ "version": "2.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpmd/phpmd.git",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "shasum": ""
+ },
+ "require": {
+ "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0",
+ "ext-xml": "*",
+ "pdepend/pdepend": "^2.16.1",
+ "php": ">=5.3.9"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0 || ^1.3.2",
+ "ext-json": "*",
+ "ext-simplexml": "*",
+ "gregwar/rst": "^1.0",
+ "mikey179/vfsstream": "^1.6.8",
+ "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2"
+ },
+ "bin": [
+ "src/bin/phpmd"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "PHPMD\\": "src/main/php"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Manuel Pichler",
+ "email": "github@manuel-pichler.de",
+ "homepage": "https://github.com/manuelpichler",
+ "role": "Project Founder"
+ },
+ {
+ "name": "Marc Würth",
+ "email": "ravage@bluewin.ch",
+ "homepage": "https://github.com/ravage84",
+ "role": "Project Maintainer"
+ },
+ {
+ "name": "Other contributors",
+ "homepage": "https://github.com/phpmd/phpmd/graphs/contributors",
+ "role": "Contributors"
+ }
+ ],
+ "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
+ "homepage": "https://phpmd.org/",
+ "keywords": [
+ "dev",
+ "mess detection",
+ "mess detector",
+ "pdepend",
+ "phpmd",
+ "pmd"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/phpmd",
+ "issues": "https://github.com/phpmd/phpmd/issues",
+ "source": "https://github.com/phpmd/phpmd/tree/2.15.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-11T08:22:20+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "1.25.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240",
+ "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^4.15",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^1.5",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "phpstan/phpstan-strict-rules": "^1.0",
+ "phpunit/phpunit": "^9.5",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0"
+ },
+ "time": "2024-01-04T17:06:16+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "1.10.54",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "3e25f279dada0adc14ffd7bad09af2e2fc3523bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3e25f279dada0adc14ffd7bad09af2e2fc3523bb",
+ "reference": "3e25f279dada0adc14ffd7bad09af2e2fc3523bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-05T15:50:47+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.30",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089",
+ "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:47:57+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1",
+ "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1 || ^2",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.3",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.28",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.5",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^3.2",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-01T16:55:19+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
+ "reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/1.1.2"
+ },
+ "time": "2021-11-05T16:50:12+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:08:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
+ "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-05-07T05:35:17+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T06:03:37+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "bde739e7565280bda77be70044ac1047bc007e34"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34",
+ "reference": "bde739e7565280bda77be70044ac1047bc007e34",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-02T09:26:13+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:07:39+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "slevomat/coding-standard",
+ "version": "8.14.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slevomat/coding-standard.git",
+ "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926",
+ "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpdoc-parser": "^1.23.1",
+ "squizlabs/php_codesniffer": "^3.7.1"
+ },
+ "require-dev": {
+ "phing/phing": "2.17.4",
+ "php-parallel-lint/php-parallel-lint": "1.3.2",
+ "phpstan/phpstan": "1.10.37",
+ "phpstan/phpstan-deprecation-rules": "1.1.4",
+ "phpstan/phpstan-phpunit": "1.3.14",
+ "phpstan/phpstan-strict-rules": "1.5.1",
+ "phpunit/phpunit": "8.5.21|9.6.8|10.3.5"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SlevomatCodingStandard\\": "SlevomatCodingStandard/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
+ "keywords": [
+ "dev",
+ "phpcs"
+ ],
+ "support": {
+ "issues": "https://github.com/slevomat/coding-standard/issues",
+ "source": "https://github.com/slevomat/coding-standard/tree/8.14.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kukulich",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-10-08T07:28:08+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5805f7a4e4958dbb5e944ef1e6edae0a303765e7",
+ "reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/phpcs",
+ "bin/phpcbf"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-12-08T12:32:31+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v5.4.31",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "dd5ea39de228813aba0c23c3a4153da2a4cf3cd9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/dd5ea39de228813aba0c23c3a4153da2a4cf3cd9",
+ "reference": "dd5ea39de228813aba0c23c3a4153da2a4cf3cd9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/filesystem": "^4.4|^5.0|^6.0",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/polyfill-php81": "^1.22"
+ },
+ "conflict": {
+ "symfony/finder": "<4.4"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^4.4|^5.0|^6.0",
+ "symfony/finder": "^4.4|^5.0|^6.0",
+ "symfony/messenger": "^4.4|^5.0|^6.0",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/yaml": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/yaml": "To use the yaml reference dumper"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v5.4.31"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-11-09T08:22:43+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v5.4.34",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "75d568165a65fa7d8124869ec7c3a90424352e6c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/75d568165a65fa7d8124869ec7c3a90424352e6c",
+ "reference": "75d568165a65fa7d8124869ec7c3a90424352e6c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1.1",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/polyfill-php81": "^1.22",
+ "symfony/service-contracts": "^1.1.6|^2"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2",
+ "symfony/config": "<5.3",
+ "symfony/finder": "<4.4",
+ "symfony/proxy-manager-bridge": "<4.4",
+ "symfony/yaml": "<4.4.26"
+ },
+ "provide": {
+ "psr/container-implementation": "1.0",
+ "symfony/service-implementation": "1.0|2.0"
+ },
+ "require-dev": {
+ "symfony/config": "^5.3|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/yaml": "^4.4.26|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/config": "",
+ "symfony/expression-language": "For using expressions in service container configuration",
+ "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required",
+ "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them",
+ "symfony/yaml": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/v5.4.34"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-28T09:31:38+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v2.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+ "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-01-02T09:53:40+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v5.4.25",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/0ce3a62c9579a53358d3a7eb6b3dfb79789a6364",
+ "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v5.4.25"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-05-31T13:04:02+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.28.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+ "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.28-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-01-26T09:26:14+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.28.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "42292d99c55abe617799667f454222c54c60e229"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
+ "reference": "42292d99c55abe617799667f454222c54c60e229",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.28-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-07-28T09:04:16+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.28.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+ "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.28-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-01-26T09:26:14+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php81",
+ "version": "v1.28.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
+ "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.28-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php81\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-01-26T09:26:14+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v2.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
+ "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1",
+ "symfony/deprecation-contracts": "^2.1|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v2.5.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-30T19:17:29+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
+ "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2023-11-20T00:12:19+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
+}
diff --git a/inc/Contracts/Arrayable.php b/inc/Contracts/Arrayable.php
new file mode 100644
index 0000000..ea6818c
--- /dev/null
+++ b/inc/Contracts/Arrayable.php
@@ -0,0 +1,24 @@
+$field = isset($data[ $field ]) ? (string) $data[ $field ] : '';
+ }
+
+ $to3pScript = static function ($scriptData) {
+ return new ThirdPartyScriptData($scriptData);
+ };
+
+ $this->html = isset($data['html']) ? new ThirdPartyHtmlData($data['html']) : null;
+ $this->stylesheets = isset($data['stylesheets']) ? array_map('strval', $data['stylesheets']) : [];
+ $this->scripts = isset($data['scripts']) ? array_map($to3pScript, $data['scripts']) : [];
+ }
+
+ /**
+ * Gets the third party identifier.
+ *
+ * @return string Third party identifier.
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * Gets the third party description.
+ *
+ * @return string Third party description.
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ /**
+ * Gets the third party website.
+ *
+ * @return string Third party website, if provided.
+ */
+ public function getWebsite(): string
+ {
+ return $this->website;
+ }
+
+ /**
+ * Gets the HTML element needed for the third party.
+ *
+ * @return ThirdPartyHtmlData|null HTML element needed for the third party, or null.
+ */
+ public function getHtml()
+ {
+ return $this->html;
+ }
+
+ /**
+ * Gets the stylesheets needed for the third party.
+ *
+ * @return string[] Stylesheets needed for the third party.
+ */
+ public function getStylesheets(): array
+ {
+ return $this->stylesheets;
+ }
+
+ /**
+ * Gets the scripts needed for the third party.
+ *
+ * @return ThirdPartyScriptData[] Scripts needed for the third party.
+ */
+ public function getScripts(): array
+ {
+ return $this->scripts;
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ $data = [
+ 'id' => $this->id,
+ 'description' => $this->description,
+ ];
+ if ($this->website) {
+ $data['website'] = $this->website;
+ }
+ if ($this->html) {
+ $data['html'] = $this->html->toArray();
+ }
+ if ($this->stylesheets) {
+ $data['stylesheets'] = $this->stylesheets;
+ }
+ if ($this->scripts) {
+ $data['scripts'] = array_map(
+ static function (ThirdPartyScriptData $scriptData) {
+ return $scriptData->toArray();
+ },
+ $this->scripts
+ );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Creates a new instance from a JSON file with third party configuration data.
+ *
+ * @param string $file_path Absolute path to the JSON file.
+ * @return ThirdPartyData Third party data instance based on the JSON data.
+ */
+ public static function fromJsonFile(string $file_path): ThirdPartyData
+ {
+ $data = json_decode(file_get_contents($file_path), true);
+ return new self($data);
+ }
+}
diff --git a/inc/Data/ThirdPartyDataFormatter.php b/inc/Data/ThirdPartyDataFormatter.php
new file mode 100644
index 0000000..a2123fc
--- /dev/null
+++ b/inc/Data/ThirdPartyDataFormatter.php
@@ -0,0 +1,295 @@
+getHtml();
+ $scriptsData = $data->getScripts();
+
+ $allScriptParams = array_reduce(
+ $scriptsData,
+ static function ($acc, ThirdPartyScriptData $scriptData) {
+ foreach ($scriptData->getParams() as $param) {
+ $acc[] = $param;
+ }
+ return $acc;
+ },
+ []
+ );
+
+ $scriptUrlParamInputs = self::intersectArgs($args, $allScriptParams);
+
+ $htmlUrlParamInputs = [];
+ $htmlSlugParamInput = [];
+ if ($htmlData) {
+ if (isset($htmlData->getAttributes()['src'])
+ && $htmlData->getAttributes()['src'] instanceof ThirdPartySrcValue
+ ) {
+ $htmlUrlParamInputs = self::intersectArgs(
+ $args,
+ $htmlData->getAttributes()['src']->getParams()
+ );
+ $htmlSlugParamInput = self::intersectArgs(
+ $args,
+ [$htmlData->getAttributes()['src']->getSlugParam()]
+ );
+ }
+ }
+
+ $htmlAttrInputs = self::diffArgs(
+ $args,
+ array_keys(
+ array_merge(
+ $scriptUrlParamInputs,
+ $htmlUrlParamInputs,
+ $htmlSlugParamInput
+ )
+ )
+ );
+
+ $newData = $data->toArray();
+ if (isset($newData['html']) && $newData['html']) {
+ $newData['html'] = self::formatHtml(
+ $newData['html']['element'],
+ $newData['html']['attributes'],
+ $htmlAttrInputs,
+ $htmlUrlParamInputs,
+ $htmlSlugParamInput
+ );
+ }
+ if (isset($newData['scripts']) && $newData['scripts']) {
+ $newData['scripts'] = array_map(
+ static function ($scriptData) use ($scriptUrlParamInputs) {
+ if (isset($scriptData['url'])) {
+ $scriptData['url'] = self::formatUrl(
+ $scriptData['url'],
+ $scriptData['params'],
+ $scriptUrlParamInputs
+ );
+ } else {
+ $scriptData['code'] = self::formatCode(
+ $scriptData['code'],
+ $scriptUrlParamInputs
+ );
+ }
+ unset($scriptData['params']); // Params are irrelevant for formatted output.
+ return $scriptData;
+ },
+ $newData['scripts']
+ );
+ }
+
+ return new ThirdPartyOutput($newData);
+ }
+
+ /**
+ * Formats the given HTML arguments into an HTML string.
+ *
+ * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L55
+ *
+ * @param string $element Element tag name for the HTML element.
+ * @param array $attributes Attributes for the HTML element.
+ * @param array $htmlAttrArgs Input arguments for the HTML element attributes.
+ * @param array $urlQueryParamArgs Input arguments for the src attribute query parameters.
+ * @param array $slugParamArg Optional. Input argument for the src attribute slug query parameter.
+ * Default empty array.
+ * @return string HTML string.
+ */
+ public static function formatHtml(
+ string $element,
+ array $attributes,
+ array $htmlAttrArgs,
+ array $urlQueryParamArgs,
+ array $slugParamArg = []
+ ): string {
+ if (! $attributes) {
+ return "<{$element}>{$element}>";
+ }
+
+ if (isset($attributes['src']['url'])) {
+ $attributes['src'] = self::formatUrl(
+ $attributes['src']['url'],
+ $attributes['src']['params'] ?? [],
+ $urlQueryParamArgs,
+ $slugParamArg
+ );
+ }
+
+ // Overwrite default attributes with arguments as needed.
+ foreach ($htmlAttrArgs as $name => $value) {
+ $attributes[ $name ] = $value;
+ }
+
+ $htmlAttributes = new HtmlAttributes($attributes);
+ return "<{$element}{$htmlAttributes}>{$element}>";
+ }
+
+ /**
+ * Formats the given URL arguments into a URL string.
+ *
+ * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L28
+ *
+ * @param string $url Base URL.
+ * @param string[] $params Parameter names.
+ * @param array $args Input arguments for the src attribute query parameters.
+ * @param array $slugParamArg Optional. Input argument for the src attribute slug query parameter.
+ * Default empty array.
+ * @return string HTML string.
+ */
+ public static function formatUrl(string $url, array $params, array $args, array $slugParamArg = []): string
+ {
+ if ($slugParamArg) {
+ $slug = array_values($slugParamArg)[0];
+
+ $path = parse_url($url, PHP_URL_PATH);
+ if ($path) {
+ $trailingSlash = str_ends_with($path, '/') ? '/' : '';
+ $url = str_replace(
+ $path,
+ substr($path, 0, - strlen(basename($path) . $trailingSlash)) . $slug . $trailingSlash,
+ $url
+ );
+ } else {
+ $url = rtrim($url, '/') . '/' . $slug;
+ }
+ }
+
+ if ($params && $args) {
+ $queryArgs = self::intersectArgs($args, $params);
+ if ($queryArgs) {
+ $url = self::setUrlQueryArgs($url, $queryArgs);
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * Formats the given code arguments into a code string.
+ *
+ * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L48
+ *
+ * @param string $code Code string with placeholders for URL query parameters.
+ * @param array $args Input arguments for the src attribute query parameters.
+ * @return string HTML string.
+ */
+ public static function formatCode(string $code, array $args): string
+ {
+ return preg_replace_callback(
+ '/{{([^}]+)}}/',
+ static function ($matches) use ($args) {
+ if (isset($args[ $matches[1] ])) {
+ return $args[ $matches[1] ];
+ }
+ return '';
+ },
+ $code
+ );
+ }
+
+ /**
+ * Returns the subset of the given $args that refers to parameter within the given $params.
+ *
+ * @param array $args Input arguments.
+ * @param string[] $params Parameter names.
+ * @return array Intersection of $args based on $params.
+ */
+ private static function intersectArgs(array $args, array $params): array
+ {
+ return array_intersect_key($args, array_flip($params));
+ }
+
+ /**
+ * Returns the subset of the given $args that refers to parameter not within the given $params.
+ *
+ * @param array $args Input arguments.
+ * @param string[] $params Parameter names.
+ * @return array Diff of $args based on $params.
+ */
+ private static function diffArgs(array $args, array $params): array
+ {
+ return array_diff_key($args, array_flip($params));
+ }
+
+ /**
+ * Sets the given query $args on the given URL.
+ *
+ * @param string $url URL.
+ * @param array $args Input arguments for the URL query string.
+ * @return string URL including query arguments.
+ */
+ private static function setUrlQueryArgs(string $url, array $args): string
+ {
+ if (! $args) {
+ return $url;
+ }
+
+ $frag = strstr($url, '#');
+ if ($frag) {
+ $url = substr($url, 0, -strlen($frag));
+ } else {
+ $frag = '';
+ }
+
+ if (str_contains($url, '?')) {
+ list( $url, $query ) = explode('?', $url, 2);
+ $url .= '?';
+ } else {
+ $url .= '?';
+ $query = '';
+ }
+
+ parse_str($query, $qs);
+ $qs = self::urlencodeRecursive($qs);
+ foreach ($args as $key => $value) {
+ $qs[ $key ] = $value;
+ }
+
+ $query = http_build_query($qs);
+
+ return ( $query ? $url . $query : rtrim($url, '?') ) . $frag;
+ }
+
+ /**
+ * URL-encodes a value or a potentially nested array structure.
+ *
+ * @param mixed $value Scalar value or array to URL-encode.
+ * @return mixed URL-encoded result.
+ */
+ private static function urlencodeRecursive($value)
+ {
+ if (is_array($value)) {
+ foreach ($value as $index => $item) {
+ $value[ $index ] = self::urlencodeRecursive($item);
+ }
+ return $value;
+ }
+
+ return urlencode($value);
+ }
+}
diff --git a/inc/Data/ThirdPartyHtmlAttributes.php b/inc/Data/ThirdPartyHtmlAttributes.php
new file mode 100644
index 0000000..4722934
--- /dev/null
+++ b/inc/Data/ThirdPartyHtmlAttributes.php
@@ -0,0 +1,51 @@
+getUrl() . '"';
+ }
+
+ return parent::toAttrString($name, $value);
+ }
+}
diff --git a/inc/Data/ThirdPartyHtmlData.php b/inc/Data/ThirdPartyHtmlData.php
new file mode 100644
index 0000000..eccc116
--- /dev/null
+++ b/inc/Data/ThirdPartyHtmlData.php
@@ -0,0 +1,111 @@
+validateData($htmlData);
+ $this->setData($htmlData);
+ }
+
+ /**
+ * Gets the element tag name for the HTML element.
+ *
+ * @return string Element tag name for the HTML element.
+ */
+ public function getElement(): string
+ {
+ return $this->element;
+ }
+
+ /**
+ * Gets the attributes for the HTML element.
+ *
+ * @return ThirdPartyHtmlAttributes Attributes for the HTML element.
+ */
+ public function getAttributes(): ThirdPartyHtmlAttributes
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Validates the given HTML data.
+ *
+ * @param array $htmlData HTML data, e.g. from a third party JSON file.
+ *
+ * @throws InvalidThirdPartyDataException Thrown when provided HTML data is invalid.
+ */
+ private function validateData(array $htmlData)
+ {
+ if (!isset($htmlData['element'])) {
+ throw new InvalidThirdPartyDataException('Missing HTML element.');
+ }
+ if (!isset($htmlData['attributes'])) {
+ throw new InvalidThirdPartyDataException('Missing HTML attributes.');
+ }
+ if (!isset($htmlData['attributes']['src'])) {
+ throw new InvalidThirdPartyDataException('Missing HTML src attribute.');
+ }
+ }
+
+ /**
+ * Sets the given HTML data.
+ *
+ * @param array $htmlData HTML data, e.g. from a third party JSON file.
+ */
+ private function setData(array $htmlData)
+ {
+ $this->element = (string) $htmlData['element'];
+ $this->attributes = new ThirdPartyHtmlAttributes($htmlData['attributes']);
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ return [
+ 'element' => $this->element,
+ 'attributes' => $this->attributes->toArray(),
+ ];
+ }
+}
diff --git a/inc/Data/ThirdPartyOutput.php b/inc/Data/ThirdPartyOutput.php
new file mode 100644
index 0000000..554f8fe
--- /dev/null
+++ b/inc/Data/ThirdPartyOutput.php
@@ -0,0 +1,173 @@
+$field = isset($data[ $field ]) ? (string) $data[ $field ] : '';
+ }
+
+ $to3pScript = static function ($scriptData) {
+ return new ThirdPartyScriptOutput($scriptData);
+ };
+
+ $this->stylesheets = isset($data['stylesheets']) ? array_map('strval', $data['stylesheets']) : [];
+ $this->scripts = isset($data['scripts']) ? array_map($to3pScript, $data['scripts']) : [];
+ }
+
+ /**
+ * Gets the third party identifier.
+ *
+ * @return string Third party identifier.
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * Gets the third party description.
+ *
+ * @return string Third party description.
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ /**
+ * Gets the third party website.
+ *
+ * @return string Third party website, if provided.
+ */
+ public function getWebsite(): string
+ {
+ return $this->website;
+ }
+
+ /**
+ * Gets the HTML needed for the third party.
+ *
+ * @return string HTML needed for the third party.
+ */
+ public function getHtml(): string
+ {
+ return $this->html;
+ }
+
+ /**
+ * Gets the stylesheets needed for the third party.
+ *
+ * @return string[] Stylesheets needed for the third party.
+ */
+ public function getStylesheets(): array
+ {
+ return $this->stylesheets;
+ }
+
+ /**
+ * Gets the scripts needed for the third party.
+ *
+ * @return ThirdPartyScriptOutput[] Scripts needed for the third party.
+ */
+ public function getScripts(): array
+ {
+ return $this->scripts;
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ $data = [
+ 'id' => $this->id,
+ 'description' => $this->description,
+ ];
+ if ($this->website) {
+ $data['website'] = $this->website;
+ }
+ if ($this->html) {
+ $data['html'] = $this->html;
+ }
+ if ($this->stylesheets) {
+ $data['stylesheets'] = $this->stylesheets;
+ }
+ if ($this->scripts) {
+ $data['scripts'] = array_map(
+ static function (ThirdPartyScriptOutput $scriptData) {
+ return $scriptData->toArray();
+ },
+ $this->scripts
+ );
+ }
+
+ return $data;
+ }
+}
diff --git a/inc/Data/ThirdPartyScriptData.php b/inc/Data/ThirdPartyScriptData.php
new file mode 100644
index 0000000..2e1f994
--- /dev/null
+++ b/inc/Data/ThirdPartyScriptData.php
@@ -0,0 +1,283 @@
+validateData($scriptData);
+ $this->setData($scriptData);
+ }
+
+ /**
+ * Gets the strategy for including the script.
+ *
+ * @return string Strategy for including the script.
+ */
+ public function getStrategy(): string
+ {
+ return $this->strategy;
+ }
+
+ /**
+ * Gets the location where to include the script.
+ *
+ * @return string Location where to include the script.
+ */
+ public function getLocation(): string
+ {
+ return $this->location;
+ }
+
+ /**
+ * Gets the action how to include the script.
+ *
+ * @return string Action how to include the script.
+ */
+ public function getAction(): string
+ {
+ return $this->action;
+ }
+
+ /**
+ * Gets the script URL, if an external script.
+ *
+ * @return string Script URL, if an external script.
+ */
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ /**
+ * Gets the script code, if an inline script.
+ *
+ * @return string Script code, if an inline script.
+ */
+ public function getCode(): string
+ {
+ return $this->code;
+ }
+
+ /**
+ * Gets the script key, if provided.
+ *
+ * @return string Script key, if provided.
+ */
+ public function getKey(): string
+ {
+ return $this->key;
+ }
+
+ /**
+ * Gets the list of parameters for the script, if needed.
+ *
+ * @return string[] List of parameters for the script, if needed.
+ */
+ public function getParams(): array
+ {
+ return $this->params;
+ }
+
+ /**
+ * Determines whether the script is an external script.
+ *
+ * @return bool True if an external script, false if an inline script.
+ */
+ public function isExternal(): bool
+ {
+ return '' !== $this->url;
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ $data = [
+ 'strategy' => $this->strategy,
+ 'location' => $this->location,
+ 'action' => $this->action,
+ ];
+ if ($this->url) {
+ $data['url'] = $this->url;
+ } else {
+ $data['code'] = $this->code;
+ }
+ if ($this->key) {
+ $data['key'] = $this->key;
+ }
+ if ($this->params) {
+ $data['params'] = $this->params;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Validates the given script data.
+ *
+ * @param array $scriptData Script data, e.g. from a third party JSON file.
+ *
+ * @throws InvalidThirdPartyDataException Thrown when provided script data is invalid.
+ */
+ private function validateData(array $scriptData)
+ {
+ $enumFields = ['strategy', 'location', 'action'];
+ foreach ($enumFields as $enumField) {
+ if (!isset($scriptData[ $enumField ])) {
+ throw new InvalidThirdPartyDataException(
+ sprintf('Missing script %s.', $enumField)
+ );
+ }
+ $methodName = 'isValid' . ucfirst($enumField);
+ if (! call_user_func([$this, $methodName], $scriptData[ $enumField ])) {
+ throw new InvalidThirdPartyDataException(
+ sprintf('Invalid script %s.', $enumField)
+ );
+ }
+ }
+
+ if (!isset($scriptData['url']) && !isset($scriptData['code'])) {
+ throw new InvalidThirdPartyDataException(
+ 'Missing both script URL and script code, one of which must be provided.'
+ );
+ }
+ if (isset($scriptData['url']) && isset($scriptData['code'])) {
+ throw new InvalidThirdPartyDataException('Only one of script URL or script code must be provided.');
+ }
+ }
+
+ /**
+ * Checks whether the given strategy is valid.
+ *
+ * @param string $strategy Strategy to validate.
+ * @return bool True if strategy is valid, false otherwise.
+ */
+ private function isValidStrategy(string $strategy): bool
+ {
+ return self::STRATEGY_SERVER === $strategy
+ || self::STRATEGY_CLIENT === $strategy
+ || self::STRATEGY_IDLE === $strategy
+ || self::STRATEGY_WORKER === $strategy;
+ }
+
+ /**
+ * Checks whether the given location is valid.
+ *
+ * @param string $location Location to validate.
+ * @return bool True if location is valid, false otherwise.
+ */
+ private function isValidLocation(string $location): bool
+ {
+ return self::LOCATION_HEAD === $location || self::LOCATION_BODY === $location;
+ }
+
+ /**
+ * Checks whether the given action is valid.
+ *
+ * @param string $action Action to validate.
+ * @return bool True if action is valid, false otherwise.
+ */
+ private function isValidAction(string $action): bool
+ {
+ return self::ACTION_APPEND === $action || self::ACTION_PREPEND === $action;
+ }
+
+ /**
+ * Sets the given script data.
+ *
+ * @param array $scriptData Script data, e.g. from a third party JSON file.
+ */
+ private function setData(array $scriptData)
+ {
+ $strFields = ['strategy', 'location', 'action', 'url', 'code', 'key'];
+ foreach ($strFields as $field) {
+ $this->$field = isset($scriptData[ $field ]) ? (string) $scriptData[ $field ] : '';
+ }
+
+ $this->params = isset($scriptData['params']) ? array_map('strval', $scriptData['params']) : [];
+ }
+}
diff --git a/inc/Data/ThirdPartyScriptOutput.php b/inc/Data/ThirdPartyScriptOutput.php
new file mode 100644
index 0000000..223d0f7
--- /dev/null
+++ b/inc/Data/ThirdPartyScriptOutput.php
@@ -0,0 +1,168 @@
+$field = isset($scriptData[ $field ]) ? (string) $scriptData[ $field ] : '';
+ }
+ }
+
+ /**
+ * Gets the strategy for including the script.
+ *
+ * @return string Strategy for including the script.
+ */
+ public function getStrategy(): string
+ {
+ return $this->strategy;
+ }
+
+ /**
+ * Gets the location where to include the script.
+ *
+ * @return string Location where to include the script.
+ */
+ public function getLocation(): string
+ {
+ return $this->location;
+ }
+
+ /**
+ * Gets the action how to include the script.
+ *
+ * @return string Action how to include the script.
+ */
+ public function getAction(): string
+ {
+ return $this->action;
+ }
+
+ /**
+ * Gets the script URL, if an external script.
+ *
+ * @return string Script URL, if an external script.
+ */
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ /**
+ * Gets the script code, if an inline script.
+ *
+ * @return string Script code, if an inline script.
+ */
+ public function getCode(): string
+ {
+ return $this->code;
+ }
+
+ /**
+ * Gets the script key, if provided.
+ *
+ * @return string Script key, if provided.
+ */
+ public function getKey(): string
+ {
+ return $this->key;
+ }
+
+ /**
+ * Determines whether the script is an external script.
+ *
+ * @return bool True if an external script, false if an inline script.
+ */
+ public function isExternal(): bool
+ {
+ return '' !== $this->url;
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ $data = [
+ 'strategy' => $this->strategy,
+ 'location' => $this->location,
+ 'action' => $this->action,
+ ];
+ if ($this->url) {
+ $data['url'] = $this->url;
+ } else {
+ $data['code'] = $this->code;
+ }
+ if ($this->key) {
+ $data['key'] = $this->key;
+ }
+
+ return $data;
+ }
+}
diff --git a/inc/Data/ThirdPartySrcValue.php b/inc/Data/ThirdPartySrcValue.php
new file mode 100644
index 0000000..297d118
--- /dev/null
+++ b/inc/Data/ThirdPartySrcValue.php
@@ -0,0 +1,106 @@
+url = $srcData['url'];
+ $this->slugParam = isset($srcData['slugParam']) ? (string) $srcData['slugParam'] : '';
+ $this->params = isset($srcData['params']) ? array_map('strval', $srcData['params']) : [];
+ }
+
+ /**
+ * Gets the URL for the src value.
+ *
+ * @return string URL for the src value.
+ */
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ /**
+ * Gets the slug param for the src value, if needed.
+ *
+ * @return string Slug param for the src value, if needed.
+ */
+ public function getSlugParam(): string
+ {
+ return $this->slugParam;
+ }
+
+ /**
+ * Gets the list of parameters for the src value, if needed.
+ *
+ * @return string[] List of parameters for the src value, if needed.
+ */
+ public function getParams(): array
+ {
+ return $this->params;
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ $data = ['url' => $this->url];
+ if ($this->slugParam) {
+ $data['slugParam'] = $this->slugParam;
+ }
+ if ($this->params) {
+ $data['params'] = $this->params;
+ }
+ return $data;
+ }
+}
diff --git a/inc/Exception/InvalidThirdPartyDataException.php b/inc/Exception/InvalidThirdPartyDataException.php
new file mode 100644
index 0000000..e03d51f
--- /dev/null
+++ b/inc/Exception/InvalidThirdPartyDataException.php
@@ -0,0 +1,20 @@
+jsonFilePath = $this->getJsonFilePath();
+
+ $this->setArgs($args);
+ }
+
+ /**
+ * Gets the third party identifier.
+ *
+ * @return string Third party identifier.
+ */
+ public function getId(): string
+ {
+ $this->lazilyInitialize();
+
+ return $this->output->getId();
+ }
+
+ /**
+ * Sets input arguments for the integration.
+ *
+ * @param array $args Input arguments to set.
+ */
+ public function setArgs(array $args)
+ {
+ $this->args = $args;
+
+ // Reset third party output.
+ $this->output = null;
+ }
+
+ /**
+ * Gets the HTML output for the integration.
+ *
+ * Only relevant if the integration provides user-facing output.
+ *
+ * @return string HTML output, or empty string if not applicable.
+ */
+ public function getHtml(): string
+ {
+ $this->lazilyInitialize();
+
+ return $this->output->getHtml();
+ }
+
+ /**
+ * Gets the stylesheet URLs for the integration.
+ *
+ * Only relevant if the integration provides stylesheets to use.
+ *
+ * @return string[] List of stylesheet URLs, or empty array if not applicable.
+ */
+ public function getStylesheets(): array
+ {
+ $this->lazilyInitialize();
+
+ return $this->output->getStylesheets();
+ }
+
+ /**
+ * Gets the script definitions for the integration.
+ *
+ * Only relevant if the integration provides scripts to use.
+ *
+ * @return ThirdPartyScriptOutput[] List of script definition objects, or empty array if not applicable.
+ */
+ public function getScripts(): array
+ {
+ $this->lazilyInitialize();
+
+ return $this->output->getScripts();
+ }
+
+ /**
+ * Gets the path to the third party data JSON file.
+ *
+ * @return string Absolute path to the JSON file.
+ */
+ abstract protected function getJsonFilePath(): string;
+
+ /**
+ * Lazily initializes the data and output instances, only if they aren't initialized yet.
+ *
+ * The data instance is only initialized once as it is agnostic to the input arguments.
+ * The output instance needs to be reinitialized whenever the input arguments change.
+ */
+ private function lazilyInitialize()
+ {
+ if (! $this->data) {
+ $this->data = ThirdPartyData::fromJsonFile($this->jsonFilePath);
+ }
+
+ if (! $this->output) {
+ $this->output = ThirdPartyDataFormatter::formatData($this->data, $this->args);
+ }
+ }
+}
diff --git a/inc/ThirdParties/YouTubeEmbed.php b/inc/ThirdParties/YouTubeEmbed.php
new file mode 100644
index 0000000..8acefc7
--- /dev/null
+++ b/inc/ThirdParties/YouTubeEmbed.php
@@ -0,0 +1,29 @@
+ $value) {
+ $this->attr[ $name ] = $this->sanitizeAttr($name, $value);
+ }
+ }
+
+ /**
+ * Checks if the given attribute is set.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $name Attribute name.
+ * @return bool True if the attribute is set, false otherwise.
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($name)
+ {
+ return isset($this->attr[ $name ]);
+ }
+
+ /**
+ * Gets the value for the given attribute.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $name Attribute name.
+ * @return mixed Value for the given attribute.
+ *
+ * @throws NotFoundException Thrown if the attribute is not set.
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($name)
+ {
+ if (!isset($this->attr[ $name ])) {
+ throw new NotFoundException(
+ sprintf(
+ 'Attribute with name %s not set.',
+ $name
+ )
+ );
+ }
+
+ return $this->attr[ $name ];
+ }
+
+ /**
+ * Sets the given value for the given attribute.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $name Attribute name.
+ * @param mixed $value Attribute value.
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($name, $value)
+ {
+ // Not implemented, as the attributes are read-only.
+ }
+
+ /**
+ * Unsets the value for the given attribute.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $name Attribute name.
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($name)
+ {
+ // Not implemented, as the attributes are read-only.
+ }
+
+ /**
+ * Returns an iterator for the attributes.
+ *
+ * @since n.e.x.t
+ *
+ * @return Traversable Attributes iterator.
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->attr);
+ }
+
+ /**
+ * Returns an array representation of the data.
+ *
+ * @return array Associative array of data.
+ */
+ public function toArray(): array
+ {
+ return array_map(
+ static function ($value) {
+ if ($value instanceof Arrayable) {
+ return $value->toArray();
+ }
+ return $value;
+ },
+ $this->attr
+ );
+ }
+
+ /**
+ * Returns the HTML string of attributes to append to the HTML element tag name.
+ *
+ * @return string HTML attributes string.
+ */
+ public function __toString(): string
+ {
+ $output = '';
+ foreach ($this->attr as $name => $value) {
+ $output .= $this->toAttrString($name, $value);
+ }
+ return $output;
+ }
+
+ /**
+ * Returns the sanitized attribute value for the given attribute name and value.
+ *
+ * @param string $name Attribute name.
+ * @param mixed $value Attribute value.
+ * @return mixed Sanitized attribute value.
+ */
+ protected function sanitizeAttr(string $name, $value)
+ {
+ if (is_bool($value)) {
+ return $value;
+ }
+ return (string) $value;
+ }
+
+ /**
+ * Returns the attribute string for the given attribute name and value.
+ *
+ * @param string $name Attribute name.
+ * @param mixed $value Attribute value.
+ * @return string HTML attribute string (starts with a space), or empty string to skip.
+ */
+ protected function toAttrString(string $name, $value): string
+ {
+ if (is_bool($value)) {
+ return $value ? ' ' . $name : '';
+ }
+
+ return ' ' . $name . '="' . $value . '"';
+ }
+}
diff --git a/inc/Util/ThirdPartiesDir.php b/inc/Util/ThirdPartiesDir.php
new file mode 100644
index 0000000..20e306e
--- /dev/null
+++ b/inc/Util/ThirdPartiesDir.php
@@ -0,0 +1,30 @@
+
+
+ PHPCS rules for Third Party Capital.
+
+
+
+
+
+
+
+
+
+ ./inc
+ ./tests/phpunit
+
\ No newline at end of file
diff --git a/phpmd.xml b/phpmd.xml
new file mode 100644
index 0000000..bf23549
--- /dev/null
+++ b/phpmd.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ */node_modules/*
+ */tests/*
+ */vendor/*
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..2090fef
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,5 @@
+parameters:
+ level: 5
+ paths:
+ - inc/
+ treatPhpDocTypesAsCertain: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..0682a00
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,21 @@
+
+
+
+
+ inc
+
+
+
+
+ tests/phpunit/tests
+
+
+
\ No newline at end of file
diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php
new file mode 100644
index 0000000..6467fce
--- /dev/null
+++ b/tests/phpunit/bootstrap.php
@@ -0,0 +1,14 @@
+assertSame($expected, $output->toArray());
+ }
+
+ public function dataFormatData()
+ {
+ return [
+ 'minimum fields' => [
+ [
+ 'id' => 'a-useless-service',
+ 'description' => 'This service cannot do anything. Nobody would do that in production.',
+ ],
+ [],
+ [
+ 'id' => 'a-useless-service',
+ 'description' => 'This service cannot do anything. Nobody would do that in production.',
+ ],
+ ],
+ 'basic example' => [
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something.',
+ 'website' => 'https://my-service.com/',
+ 'html' => [
+ 'element' => 'iframe',
+ 'attributes' => [
+ 'src' => 'https://example.com/my-video/',
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ ],
+ 'stylesheets' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ ],
+ [],
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something.',
+ 'website' => 'https://my-service.com/',
+ 'html' => '',
+ 'stylesheets' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ ],
+ ],
+ 'with HTML params' => [
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something dynamic.',
+ 'website' => 'https://my-service.com/',
+ 'html' => [
+ 'element' => 'iframe',
+ 'attributes' => [
+ 'src' => [
+ 'url' => 'https://example.com/my-video/',
+ 'params' => ['v'],
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ ],
+ 'stylesheets' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ ],
+ [
+ 'v' => '12345',
+ 'loading' => 'lazy',
+ ],
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something dynamic.',
+ 'website' => 'https://my-service.com/',
+ 'html' => '',
+ 'stylesheets' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ ],
+ ],
+ 'with HTML slug param' => [
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something with a slug param.',
+ 'website' => 'https://my-service.com/',
+ 'html' => [
+ 'element' => 'iframe',
+ 'attributes' => [
+ 'src' => [
+ 'url' => 'https://example.com/design-pattern/blue/',
+ 'slugParam' => 'color',
+ 'params' => ['id'],
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ ],
+ 'stylesheets' => ['https://example.com/style.css'],
+ ],
+ [
+ 'id' => '481',
+ 'color' => 'green',
+ 'loading' => 'lazy',
+ 'allowfullscreen' => false,
+ ],
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something with a slug param.',
+ 'website' => 'https://my-service.com/',
+ 'html' => '',
+ 'stylesheets' => ['https://example.com/style.css'],
+ ],
+ ],
+ 'with script params' => [
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows loading an Analytics script.',
+ 'website' => 'https://my-service.com/',
+ 'scripts' => [
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'url' => 'https://example.com/analytics/',
+ 'key' => 'my-analytics',
+ 'params' => ['id', 'anonymizeIP', 'enhancedAttribution'],
+ ],
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'code' => 'exampleAnalytics.init()',
+ ],
+ ],
+ ],
+ [
+ 'id' => '987123',
+ 'anonymizeIP' => 1,
+ ],
+ [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows loading an Analytics script.',
+ 'website' => 'https://my-service.com/',
+ 'scripts' => [
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'url' => 'https://example.com/analytics/?id=987123&anonymizeIP=1',
+ 'key' => 'my-analytics',
+ ],
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'code' => 'exampleAnalytics.init()',
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ public function testFormatHtmlWithoutAttributes()
+ {
+ $html = ThirdPartyDataFormatter::formatHtml('video', [], [], []);
+ $this->assertSame('', $html);
+ }
+
+ public function testFormatHtmlWithAttributes()
+ {
+ $html = ThirdPartyDataFormatter::formatHtml(
+ 'video',
+ [
+ 'src' => 'https://example.com/custom-video.mpg',
+ 'width' => '1024',
+ 'height' => '768',
+ 'controls' => true,
+ 'muted' => false,
+ ],
+ [],
+ []
+ );
+ $this->assertSame(
+ '',
+ $html
+ );
+ }
+
+ public function testFormatHtmlWithAttributesIncludingSrcValue()
+ {
+ $html = ThirdPartyDataFormatter::formatHtml(
+ 'iframe',
+ [
+ 'src' => [
+ 'url' => 'https://logo-service.com/embed/unicolor',
+ 'slugParam' => 'kind',
+ 'params' => ['id', 'style', 'lang']
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ [],
+ [
+ 'id' => '987654',
+ 'lang' => 'de',
+ ],
+ ['kind' => 'duotone']
+ );
+ $this->assertSame(
+ '',
+ $html
+ );
+ }
+
+ public function testFormatHtmlWithAttributesIncludingSrcValueAndAttributeArgs()
+ {
+ $html = ThirdPartyDataFormatter::formatHtml(
+ 'iframe',
+ [
+ 'src' => [
+ 'url' => 'https://example.com/embed/',
+ 'params' => ['id']
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ [
+ 'loading' => 'lazy',
+ ],
+ [
+ 'id' => '23',
+ ]
+ );
+ $this->assertSame(
+ '',
+ $html
+ );
+ }
+
+ public function testFormatUrlWithNoParamsOrArgs()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl('https://example.com/embed/', [], []);
+ $this->assertSame('https://example.com/embed/', $url);
+ }
+
+ public function testFormatUrlWithParamsButNoArgs()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl(
+ 'https://example.com/embed/',
+ ['id', 'lang'],
+ []
+ );
+ $this->assertSame('https://example.com/embed/', $url);
+ }
+
+ public function testFormatUrlWithParamsAndArgs()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl(
+ 'https://example.com/embed/',
+ ['id', 'direction', 'lang', 'style'],
+ [
+ 'id' => '8642',
+ 'lang' => 'es',
+ ]
+ );
+ $this->assertSame('https://example.com/embed/?id=8642&lang=es', $url);
+ }
+
+ public function testFormatUrlWithSlugParamAndPathWithoutTrailingSlash()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl(
+ 'https://example.com/embed/static',
+ ['id'],
+ [],
+ ['mode' => 'interactive']
+ );
+ $this->assertSame('https://example.com/embed/interactive', $url);
+ }
+
+ public function testFormatUrlWithSlugParamAndPathWithTrailingSlash()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl(
+ 'https://example.com/embed/static/',
+ ['id'],
+ [],
+ ['mode' => 'interactive']
+ );
+ $this->assertSame('https://example.com/embed/interactive/', $url);
+ }
+
+ public function testFormatUrlWithSlugParamAndNoPath()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl(
+ 'https://example.com',
+ ['id'],
+ [],
+ ['mode' => 'interactive']
+ );
+ $this->assertSame('https://example.com/interactive', $url);
+ }
+
+ public function testFormatUrlWithQueryAndSlugParamAndParamsAndArgs()
+ {
+ $url = ThirdPartyDataFormatter::formatUrl(
+ 'https://example.com/embed/static?forcedParam=value',
+ ['id'],
+ ['id' => '12345'],
+ ['mode' => 'interactive']
+ );
+ $this->assertSame('https://example.com/embed/interactive?forcedParam=value&id=12345', $url);
+ }
+
+ public function testFormatCodeWithoutArgs()
+ {
+ $code = ThirdPartyDataFormatter::formatCode(
+ 'document.querySelector("{{selector}}").addEventListener(api.{{callback}});',
+ []
+ );
+ $this->assertSame(
+ 'document.querySelector("").addEventListener(api.);',
+ $code
+ );
+ }
+
+ public function testFormatCodeWithArgs()
+ {
+ $code = ThirdPartyDataFormatter::formatCode(
+ 'document.querySelector("{{selector}}").addEventListener(api.{{callback}});',
+ [
+ 'selector' => '.my-cta-button',
+ 'callback' => 'addToCart',
+ ]
+ );
+ $this->assertSame(
+ 'document.querySelector(".my-cta-button").addEventListener(api.addToCart);',
+ $code
+ );
+ }
+
+ public function testFormatCodeWithArgsIncorrectOrderAndTooMany()
+ {
+ $code = ThirdPartyDataFormatter::formatCode(
+ 'document.querySelector("{{selector}}").addEventListener(api.{{callback}});',
+ [
+ 'callback' => 'addToCart',
+ 'device' => 'phone',
+ 'selector' => '.my-cta-button',
+ ]
+ );
+ $this->assertSame(
+ 'document.querySelector(".my-cta-button").addEventListener(api.addToCart);',
+ $code
+ );
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartyDataTest.php b/tests/phpunit/tests/Data/ThirdPartyDataTest.php
new file mode 100644
index 0000000..ef35dbe
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartyDataTest.php
@@ -0,0 +1,120 @@
+runGetterTestCase(ThirdPartyData::class, $getMethod, $args, $expected);
+ }
+
+ public function dataGetMethods()
+ {
+ return $this->gettersToTestCases([
+ [
+ 'field' => 'id',
+ 'getter' => 'getId',
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'description',
+ 'getter' => 'getDescription',
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'website',
+ 'getter' => 'getWebsite',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'html',
+ 'getter' => 'getHtml',
+ 'value' => [
+ 'element' => 'iframe',
+ 'attributes' => [
+ 'src' => [
+ 'url' => 'https://example.com/my-video/',
+ 'params' => ['v'],
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ ],
+ 'default' => null,
+ 'required' => false,
+ ],
+ [
+ 'field' => 'stylesheets',
+ 'getter' => 'getStylesheets',
+ 'value' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ 'default' => [],
+ 'required' => false,
+ ],
+ [
+ 'field' => 'scripts',
+ 'getter' => 'getScripts',
+ 'value' => [
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'url' => 'https://example.com/',
+ 'key' => 'my-analytics',
+ 'params' => ['id'],
+ ],
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'code' => 'window.dataLayer=window.dataLayer',
+ ],
+ ],
+ 'default' => [],
+ 'required' => false,
+ ],
+ ], InvalidThirdPartyDataException::class);
+ }
+
+ public function testToArray()
+ {
+ $input = [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something.',
+ 'website' => 'https://my-service.com/',
+ 'html' => [
+ 'element' => 'iframe',
+ 'attributes' => [
+ 'src' => [
+ 'url' => 'https://example.com/my-video/',
+ 'params' => ['v'],
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ ],
+ 'stylesheets' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ ];
+ $data = new ThirdPartyData($input);
+ $this->assertSame($input, $data->toArray());
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartyHtmlAttributesTest.php b/tests/phpunit/tests/Data/ThirdPartyHtmlAttributesTest.php
new file mode 100644
index 0000000..fdb5ee3
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartyHtmlAttributesTest.php
@@ -0,0 +1,47 @@
+ [
+ 'url' => 'https://embed-test.com',
+ 'params' => ['id'],
+ ],
+ ]);
+ $this->assertInstanceOf(ThirdPartySrcValue::class, $attrs['src']);
+ $this->assertSame('https://embed-test.com', $attrs['src']->getUrl());
+ $this->assertSame(['id'], $attrs['src']->getParams());
+ }
+
+ public function testToStringWithSrcValue()
+ {
+ $attrs = new ThirdPartyHtmlAttributes([
+ 'id' => 'test-unique-id',
+ 'src' => [
+ 'url' => 'https://embed-test.com',
+ 'params' => ['id'],
+ ],
+ 'defer' => true,
+ ]);
+ $this->assertSame(
+ ' id="test-unique-id" src="https://embed-test.com" defer',
+ (string) $attrs
+ );
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartyHtmlDataTest.php b/tests/phpunit/tests/Data/ThirdPartyHtmlDataTest.php
new file mode 100644
index 0000000..71b401d
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartyHtmlDataTest.php
@@ -0,0 +1,62 @@
+runGetterTestCase(ThirdPartyHtmlData::class, $getMethod, $args, $expected);
+ }
+
+ public function dataGetMethods()
+ {
+ return $this->gettersToTestCases([
+ [
+ 'field' => 'element',
+ 'getter' => 'getElement',
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'attributes',
+ 'getter' => 'getAttributes',
+ 'value' => ['src' => 'https://example.com'],
+ 'default' => [],
+ 'required' => true,
+ ],
+ ], InvalidThirdPartyDataException::class);
+ }
+
+ public function testToArray()
+ {
+ $input = [
+ 'element' => 'iframe',
+ 'attributes' => [
+ 'src' => [
+ 'url' => 'https://example.com/my-video/',
+ 'params' => ['v'],
+ ],
+ 'width' => '1920',
+ 'height' => '1080',
+ ],
+ ];
+ $htmlData = new ThirdPartyHtmlData($input);
+ $this->assertSame($input, $htmlData->toArray());
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartyOutputTest.php b/tests/phpunit/tests/Data/ThirdPartyOutputTest.php
new file mode 100644
index 0000000..2d73cb9
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartyOutputTest.php
@@ -0,0 +1,97 @@
+runGetterTestCase(ThirdPartyOutput::class, $getMethod, $args, $expected);
+ }
+
+ public function dataGetMethods()
+ {
+ return $this->gettersToTestCases([
+ [
+ 'field' => 'id',
+ 'getter' => 'getId',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'description',
+ 'getter' => 'getDescription',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'website',
+ 'getter' => 'getWebsite',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'html',
+ 'getter' => 'getHtml',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'stylesheets',
+ 'getter' => 'getStylesheets',
+ 'value' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ 'default' => [],
+ 'required' => false,
+ ],
+ [
+ 'field' => 'scripts',
+ 'getter' => 'getScripts',
+ 'value' => [
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'url' => 'https://example.com/?id=12345789',
+ 'key' => 'my-analytics',
+ ],
+ [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'code' => 'window.dataLayer=window.dataLayer',
+ ],
+ ],
+ 'default' => [],
+ 'required' => false,
+ ],
+ ]);
+ }
+
+ public function testToArray()
+ {
+ $input = [
+ 'id' => 'my-service',
+ 'description' => 'A service that allows embedding something.',
+ 'website' => 'https://my-service.com/',
+ 'html' => '',
+ 'stylesheets' => ['https://example.com/style.css', 'https://example.com/style-2.css'],
+ ];
+ $output = new ThirdPartyOutput($input);
+ $this->assertSame($input, $output->toArray());
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartyScriptDataTest.php b/tests/phpunit/tests/Data/ThirdPartyScriptDataTest.php
new file mode 100644
index 0000000..bd6ee02
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartyScriptDataTest.php
@@ -0,0 +1,151 @@
+ ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ ];
+
+ /**
+ * @dataProvider dataGetMethods
+ */
+ public function testGetMethods(string $getMethod, array $args, $expected)
+ {
+ $this->runGetterTestCase(ThirdPartyScriptData::class, $getMethod, $args, $expected);
+ }
+
+ public function dataGetMethods()
+ {
+ return $this->gettersToTestCases([
+ [
+ 'field' => 'strategy',
+ 'getter' => 'getStrategy',
+ 'value' => ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'location',
+ 'getter' => 'getLocation',
+ 'value' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'action',
+ 'getter' => 'getAction',
+ 'value' => ThirdPartyScriptData::ACTION_APPEND,
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'url',
+ 'getter' => 'getUrl',
+ 'default' => '',
+ 'required' => true,
+ ], // Don't cover 'code' here as it can only be provided if 'url' is not provided. See separate test below.
+ [
+ 'field' => 'key',
+ 'getter' => 'getKey',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'params',
+ 'getter' => 'getParams',
+ 'default' => [],
+ 'required' => false,
+ ],
+ ], InvalidThirdPartyDataException::class);
+ }
+
+ public function testConstructorWithUrlAndNoCode()
+ {
+ $data = ['url' => 'https://example.com/'];
+ $scriptData = new ThirdPartyScriptData(array_merge($this->baseData, $data));
+
+ $this->assertSame($data['url'], $scriptData->getUrl());
+ }
+
+ public function testConstructorWithCodeAndNoUrl()
+ {
+ $data = ['code' => 'window.dataLayer=window.dataLayer'];
+ $scriptData = new ThirdPartyScriptData(array_merge($this->baseData, $data));
+
+ $this->assertSame($data['code'], $scriptData->getCode());
+ }
+
+ public function testConstructorWithNoUrlAndNoCode()
+ {
+ $this->expectException(InvalidThirdPartyDataException::class);
+
+ $scriptData = new ThirdPartyScriptData($this->baseData);
+ }
+
+ public function testConstructorWithUrlAndCode()
+ {
+ $this->expectException(InvalidThirdPartyDataException::class);
+
+ $data = [
+ 'url' => 'https://example.com/',
+ 'code' => 'window.dataLayer=window.dataLayer',
+ ];
+ $scriptData = new ThirdPartyScriptData(array_merge($this->baseData, $data));
+ }
+
+ /**
+ * @dataProvider dataIsExternal
+ */
+ public function testIsExternal(array $data, bool $expected)
+ {
+ $scriptData = new ThirdPartyScriptData(array_merge($this->baseData, $data));
+ if ($expected) {
+ $this->assertTrue($scriptData->isExternal());
+ } else {
+ $this->assertFalse($scriptData->isExternal());
+ }
+ }
+
+ public function dataIsExternal()
+ {
+ return [
+ 'with URL' => [
+ ['url' => 'https://example.com/'],
+ true,
+ ],
+ 'with code' => [
+ ['code' => 'window.dataLayer=window.dataLayer'],
+ false,
+ ],
+ ];
+ }
+
+ public function testToArray()
+ {
+ $input = array_merge(
+ $this->baseData,
+ [
+ 'url' => 'https://example.com/',
+ 'key' => 'my-analytics',
+ 'params' => ['id'],
+ ]
+ );
+ $scriptData = new ThirdPartyScriptData($input);
+ $this->assertSame($input, $scriptData->toArray());
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartyScriptOutputTest.php b/tests/phpunit/tests/Data/ThirdPartyScriptOutputTest.php
new file mode 100644
index 0000000..330f2b7
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartyScriptOutputTest.php
@@ -0,0 +1,81 @@
+runGetterTestCase(ThirdPartyScriptOutput::class, $getMethod, $args, $expected);
+ }
+
+ public function dataGetMethods()
+ {
+ return $this->gettersToTestCases([
+ [
+ 'field' => 'strategy',
+ 'getter' => 'getStrategy',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'location',
+ 'getter' => 'getLocation',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'action',
+ 'getter' => 'getAction',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'url',
+ 'getter' => 'getUrl',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'code',
+ 'getter' => 'getCode',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'key',
+ 'getter' => 'getKey',
+ 'default' => '',
+ 'required' => false,
+ ],
+ ]);
+ }
+
+ public function testToArray()
+ {
+ $input = [
+ 'strategy' => ThirdPartyScriptData::STRATEGY_CLIENT,
+ 'location' => ThirdPartyScriptData::LOCATION_HEAD,
+ 'action' => ThirdPartyScriptData::ACTION_APPEND,
+ 'url' => 'https://example.com/',
+ 'key' => 'my-analytics',
+ ];
+ $scriptOutput = new ThirdPartyScriptOutput($input);
+ $this->assertSame($input, $scriptOutput->toArray());
+ }
+}
diff --git a/tests/phpunit/tests/Data/ThirdPartySrcValueTest.php b/tests/phpunit/tests/Data/ThirdPartySrcValueTest.php
new file mode 100644
index 0000000..7f8b5d3
--- /dev/null
+++ b/tests/phpunit/tests/Data/ThirdPartySrcValueTest.php
@@ -0,0 +1,62 @@
+runGetterTestCase(ThirdPartySrcValue::class, $getMethod, $args, $expected);
+ }
+
+ public function dataGetMethods()
+ {
+ return $this->gettersToTestCases([
+ [
+ 'field' => 'url',
+ 'getter' => 'getUrl',
+ 'default' => '',
+ 'required' => true,
+ ],
+ [
+ 'field' => 'slugParam',
+ 'getter' => 'getSlugParam',
+ 'default' => '',
+ 'required' => false,
+ ],
+ [
+ 'field' => 'params',
+ 'getter' => 'getParams',
+ 'default' => [],
+ 'required' => false,
+ ],
+ ], InvalidThirdPartyDataException::class);
+ }
+
+ public function testToArray()
+ {
+ $input = [
+ 'url' => 'https://my-embed.com',
+ 'slugParam' => 'type',
+ 'params' => ['id', 'mode'],
+ ];
+ $srcValue = new ThirdPartySrcValue($input);
+ $this->assertSame($input, $srcValue->toArray());
+ }
+}
diff --git a/tests/phpunit/tests/Util/HtmlAttributesTest.php b/tests/phpunit/tests/Util/HtmlAttributesTest.php
new file mode 100644
index 0000000..54cc5cf
--- /dev/null
+++ b/tests/phpunit/tests/Util/HtmlAttributesTest.php
@@ -0,0 +1,145 @@
+ 'test-class']);
+ $this->assertTrue(isset($attrs['class']));
+ $this->assertFalse(isset($attrs['id']));
+ }
+
+ public function testOffsetExistsWithMissingAttr()
+ {
+ $attrs = new HtmlAttributes(['class' => 'test-class']);
+ $this->assertFalse(isset($attrs['id']));
+ }
+
+ public function testOffsetGetWithPresentStringAttr()
+ {
+ $attrs = new HtmlAttributes(['class' => 'demo-class']);
+ $this->assertSame('demo-class', $attrs['class']);
+ }
+
+ public function testOffsetGetWithPresentIntAttr()
+ {
+ $attrs = new HtmlAttributes(['min' => 3]);
+ $this->assertSame('3', $attrs['min']); // Sanitized into string.
+ }
+
+ public function testOffsetGetWithPresentBoolAttr()
+ {
+ $attrs = new HtmlAttributes(['defer' => true]);
+ $this->assertSame(true, $attrs['defer']);
+ }
+
+ public function testOffsetGetWithMissingAttr()
+ {
+ $this->expectException(NotFoundException::class);
+
+ $attrs = new HtmlAttributes(['class' => 'demo-class']);
+ $attrs['id'];
+ }
+
+ public function testOffsetSet()
+ {
+ $attrs = new HtmlAttributes(['class' => 'test-class']);
+ $this->assertSame('test-class', $attrs['class']);
+
+ // Class is read-only so setting shouldn't do anything.
+ $attrs['class'] = 'another-class';
+ $this->assertSame('test-class', $attrs['class']);
+ }
+
+ public function testOffsetUnset()
+ {
+ $attrs = new HtmlAttributes(['class' => 'demo-class']);
+ $this->assertTrue(isset($attrs['class']));
+
+ // Class is read-only so unsetting shouldn't do anything.
+ unset($attrs['class']);
+ $this->assertTrue(isset($attrs['class']));
+ }
+
+ public function testGetIterator()
+ {
+ $attrs = new HtmlAttributes([
+ 'id' => 'unique-id',
+ 'class' => 'test-class',
+ ]);
+ $output = '';
+ foreach ($attrs as $attr => $value) {
+ $output .= "{$attr}:{$value};";
+ }
+ $this->assertSame('id:unique-id;class:test-class;', $output);
+ }
+
+ public function testToArray()
+ {
+ $input = [
+ 'id' => 'unique-id',
+ 'class' => 'test-class',
+ ];
+ $attrs = new HtmlAttributes($input);
+ $this->assertSame($input, $attrs->toArray());
+ }
+
+ /**
+ * @dataProvider dataToString
+ */
+ public function testToString(array $input, string $expected)
+ {
+ $attrs = new HtmlAttributes($input);
+ $this->assertSame($expected, (string) $attrs);
+ }
+
+ public function dataToString()
+ {
+ return [
+ 'regular' => [
+ [
+ 'id' => 'unique-id',
+ 'class' => 'test-class',
+ ],
+ ' id="unique-id" class="test-class"',
+ ],
+ 'with bool enabled' => [
+ [
+ 'id' => 'random-id',
+ 'editable' => true,
+ ],
+ ' id="random-id" editable',
+ ],
+ 'with bool disabled' => [
+ [
+ 'id' => 'unique-id',
+ 'defer' => false,
+ ],
+ ' id="unique-id"',
+ ],
+ 'with bool mixed' => [
+ [
+ 'id' => 'unique-id',
+ 'defer' => false,
+ 'async' => true,
+ 'class' => 'demo-class',
+ ],
+ ' id="unique-id" async class="demo-class"',
+ ],
+ ];
+ }
+}
diff --git a/tests/phpunit/tests/Util/ThirdPartiesDirTest.php b/tests/phpunit/tests/Util/ThirdPartiesDirTest.php
new file mode 100644
index 0000000..ac2c9d0
--- /dev/null
+++ b/tests/phpunit/tests/Util/ThirdPartiesDirTest.php
@@ -0,0 +1,29 @@
+assertStringEndsWith('/src/third-parties/google-analytics/data.json', $absPath);
+ }
+
+ public function testGetFilePathWithLeadingSlash()
+ {
+ $absPath = ThirdPartiesDir::getFilePath('/google-analytics/data.json');
+ $this->assertStringEndsWith('/src/third-parties/google-analytics/data.json', $absPath);
+ }
+}
diff --git a/tests/phpunit/utils/TestCase.php b/tests/phpunit/utils/TestCase.php
new file mode 100644
index 0000000..32cb0cf
--- /dev/null
+++ b/tests/phpunit/utils/TestCase.php
@@ -0,0 +1,119 @@
+expectException($expected);
+
+ $instance = new $className($args);
+ call_user_func([$instance, $getMethod]);
+ return;
+ }
+
+ $instance = new $className($args);
+ $result = call_user_func([$instance, $getMethod]);
+ if ($result instanceof Arrayable) {
+ $result = $result->toArray();
+ } elseif (is_array($result)) {
+ $result = array_map(
+ static function ($entry) {
+ if ($entry instanceof Arrayable) {
+ return $entry->toArray();
+ }
+ return $entry;
+ },
+ $result
+ );
+ }
+ $this->assertSame($expected, $result);
+ }
+
+ protected function gettersToTestCases(array $getters, string $exceptionClass = null): array
+ {
+ if (!$exceptionClass) {
+ $exceptionClass = Exception::class;
+ }
+
+ $requiredFields = [];
+ foreach ($getters as $getter) {
+ if (!isset($getter['required']) || ! $getter['required']) {
+ continue;
+ }
+
+ if (!isset($getter['value'])) {
+ $type = isset($getter['default']) ? gettype($getter['default']) : 'string';
+ $value = $this->createValueOfType($type);
+ } else {
+ $value = $getter['value'];
+ }
+ $requiredFields[$getter['field']] = $value;
+ }
+
+ $testCases = [];
+ foreach ($getters as $getter) {
+ if (!isset($getter['value'])) {
+ $type = isset($getter['default']) ? gettype($getter['default']) : 'string';
+ $value = $this->createValueOfType($type);
+ } else {
+ $value = $getter['value'];
+ }
+ $args = $requiredFields;
+ $args[$getter['field']] = $value;
+
+ $testCases["{$getter['getter']} with value"] = [
+ $getter['getter'],
+ $args,
+ $value,
+ ];
+
+ if ((isset($getter['required']) && $getter['required']) || isset($getter['default'])) {
+ unset($args[$getter['field']]);
+
+ $testCases["{$getter['getter']} without value"] = [
+ $getter['getter'],
+ $args,
+ isset($getter['required']) && $getter['required'] ?
+ $exceptionClass :
+ $getter['default']
+ ];
+ }
+ }
+ return $testCases;
+ }
+
+ private function createValueOfType(string $type)
+ {
+ switch ($type) {
+ case 'bool':
+ case 'boolean':
+ return true;
+ case 'double':
+ case 'float':
+ return 5.9;
+ case 'int':
+ case 'integer':
+ return 23;
+ case 'array':
+ return ['id'];
+ }
+
+ // Default 'string'.
+ return 'something';
+ }
+}