diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 0000000..7a58683 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,5 @@ +{ + "$schema": "/phpactor.schema.json", + "language_server_phpstan.enabled": false, + "language_server_php_cs_fixer.enabled": false +} \ No newline at end of file diff --git a/composer.json b/composer.json index 650ad6d..765e3d3 100644 --- a/composer.json +++ b/composer.json @@ -9,19 +9,21 @@ "oauth2-keycloak" ], "require": { - "php": ">=8.2", + "php": ">=8.4", "stevenmaguire/oauth2-keycloak": "^5.1", - "symfony/routing": "^6.4 || ^7.2", - "symfony/security-bundle": "^6.4 || ^7.2", - "symfony/http-kernel": "^6.4 || ^7.2", - "symfony/framework-bundle": "^6.4 || ^7.2", - "symfony/serializer-pack": "^1.3" + "symfony/routing": "^8.0", + "symfony/security-bundle": "^8.0", + "symfony/http-kernel": "^8.0", + "symfony/framework-bundle": "^8.0", + "symfony/serializer": "^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75", - "phpunit/phpunit": "^11.2", + "friendsofphp/php-cs-fixer": "^3.93", + "phpunit/phpunit": "^12.5", "mockery/mockery": "^1.6", - "phpstan/phpstan": "^2.1" + "phpstan/phpstan": "^2.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-mockery": "^2.0" }, "autoload": { "psr-4": { @@ -45,5 +47,10 @@ "email": "maico.orazio@gmail.com", "homepage": "https://github.com/mainick" } - ] + ], + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } + } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..bbc2af1 --- /dev/null +++ b/composer.lock @@ -0,0 +1,7288 @@ +{ + "_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": "03982423378c2f25934529e9b4ec890e", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.6.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.9.0" + }, + "time": "2025-11-25T22:17:17+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "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/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "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/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "stevenmaguire/oauth2-keycloak", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/stevenmaguire/oauth2-keycloak.git", + "reference": "1b690b7377dfe7a23e1590373f37e12cf40a6d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/1b690b7377dfe7a23e1590373f37e12cf40a6d75", + "reference": "1b690b7377dfe7a23e1590373f37e12cf40a6d75", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "^6.0", + "league/oauth2-client": "^2.0", + "php": "~7.2 || ~8.0" + }, + "require-dev": { + "mockery/mockery": "~1.5.0", + "phpunit/phpunit": "~9.6.4", + "squizlabs/php_codesniffer": "~3.7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stevenmaguire\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steven Maguire", + "email": "stevenmaguire@gmail.com", + "homepage": "https://github.com/stevenmaguire" + } + ], + "description": "Keycloak OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "authorisation", + "authorization", + "client", + "keycloak", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/stevenmaguire/oauth2-keycloak/issues", + "source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/5.1.0" + }, + "time": "2023-10-24T06:10:44+00:00" + }, + { + "name": "symfony/cache", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "92e9960386c7e01f58198038c199d522959a843c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/92e9960386c7e01f58198038c199d522959a843c", + "reference": "92e9960386c7e01f58198038c199d522959a843c", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^7.4|^8.0" + }, + "conflict": { + "doctrine/dbal": "<4.3", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:18:07+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "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 caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.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": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:46:48+00:00" + }, + { + "name": "symfony/config", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "8f45af92f08f82902827a8b6f403aaf49d893539" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/8f45af92f08f82902827a8b6f403aaf49d893539", + "reference": "8f45af92f08f82902827a8b6f403aaf49d893539", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "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/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-13T13:06:50+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "40a6c455ade7e3bf25900d6b746d40cfa2573e26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/40a6c455ade7e3bf25900d6b746d40cfa2573e26", + "reference": "40a6c455ade7e3bf25900d6b746d40cfa2573e26", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^7.4|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "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/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:18:07+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "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/v3.6.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": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "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 tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "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 tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "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 dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.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": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "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/v8.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-01T09:13:36+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "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": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "e2f9469e7a802dd7c0d193792afc494d68177c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/e2f9469e7a802dd7c0d193792afc494d68177c54", + "reference": "e2f9469e7a802dd7c0d193792afc494d68177c54", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.4", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.4|^8.0.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<7.4", + "symfony/form": "<7.4", + "symfony/json-streamer": "<7.4", + "symfony/messenger": "<7.4", + "symfony/security-csrf": "<7.4", + "symfony/serializer": "<7.4", + "symfony/translation": "<7.4", + "symfony/webhook": "<7.4", + "symfony/workflow": "<7.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^7.4|^8.0", + "symfony/asset-mapper": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/dotenv": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/json-streamer": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/mailer": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/notifier": "^7.4|^8.0", + "symfony/object-mapper": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/scheduler": "^7.4|^8.0", + "symfony/security-bundle": "^7.4|^8.0", + "symfony/semaphore": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0", + "symfony/type-info": "^7.4.1|^8.0.1", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0", + "symfony/webhook": "^7.4|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "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 a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T09:06:10+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "e3422806e6f6760dbed0ddbc0a7fbfb6b5ce96bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e3422806e6f6760dbed0ddbc0a7fbfb6b5ce96bb", + "reference": "e3422806e6f6760dbed0ddbc0a7fbfb6b5ce96bb", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "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": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:18:07+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "20c1c5e41fc53928dbb670088f544f2d460d497d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/20c1c5e41fc53928dbb670088f544f2d460d497d", + "reference": "20c1c5e41fc53928dbb670088f544f2d460d497d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "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 a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-28T10:46:31+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "ca6af4e20357d58d50c818d676cf2e2dd5e53b02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/ca6af4e20357d58d50c818d676cf2e2dd5e53b02", + "reference": "ca6af4e20357d58d50c818d676cf2e2dd5e53b02", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-01T23:07:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "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 for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "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.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/property-access", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "a35a5ec85b605d0d1a9fd802cb44d87682c746fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/a35a5ec85b605d0d1a9fd802cb44d87682c746fd", + "reference": "a35a5ec85b605d0d1a9fd802cb44d87682c746fd", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/property-info": "^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "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 functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T09:27:50+00:00" + }, + { + "name": "symfony/property-info", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "9d987224b54758240e80a062c5e414431bbf84de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/9d987224b54758240e80a062c5e414431bbf84de", + "reference": "9d987224b54758240e80a062c5e414431bbf84de", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.4|^8.0.4" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:18:07+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "4a2bc08d1c35307239329f434d45c2bfe8241fa9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/4a2bc08d1c35307239329f434d45c2bfe8241fa9", + "reference": "4a2bc08d1c35307239329f434d45c2bfe8241fa9", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "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": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-12T12:37:40+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "c170650a00ba724be3455852747af600a2f042b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/c170650a00ba724be3455852747af600a2f042b4", + "reference": "c170650a00ba724be3455852747af600a2f042b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.4", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/password-hasher": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^7.4|^8.0", + "symfony/security-http": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/asset": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/ldap": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "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 a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-10T13:58:55+00:00" + }, + { + "name": "symfony/security-core", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "c62565de41a136535ffa79a4db0373a7173b4d02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/c62565de41a136535ffa79a4db0373a7173b4d02", + "reference": "c62565de41a136535ffa79a4db0373a7173b4d02", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/ldap": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "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": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "8be8bc615044c5911e6d15a5b0a80132068170c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/8be8bc615044c5911e6d15a5b0a80132068170c5", + "reference": "8be8bc615044c5911e6d15a5b0a80132068170c5", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/security-core": "^7.4|^8.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "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": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/security-http", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "02f37c050db6e997052916194086d1a0a8790b8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/02f37c050db6e997052916194086d1a0a8790b8f", + "reference": "02f37c050db6e997052916194086d1a0a8790b8f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/security-csrf": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "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": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/serializer", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "867a38a1927d23a503f7248aa182032c6ea42702" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/867a38a1927d23a503f7248aa182032c6ea42702", + "reference": "867a38a1927d23a503f7248aa182032c6ea42702", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-info": "<7.3" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "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": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T09:06:43+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "758b372d6882506821ed666032e43020c4f57194" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-12T12:37:40+00:00" + }, + { + "name": "symfony/type-info", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "106a2d3bbf0d4576b2f70e6ca866fa420956ed0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/106a2d3bbf0d4576b2f70e6ca866fa420956ed0d", + "reference": "106a2d3bbf0d4576b2f70e6ca866fa420956ed0d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-09T12:15:10+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/326e0406fc315eca57ef5740fa4a280b7a068c82", + "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-01T23:07:29+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + } + ], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "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.3.2" + }, + "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": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "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", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "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": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "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": "2024-05-06T16:37:16+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.93.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "50895a07cface1385082e4caa6a6786c4e033468" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/50895a07cface1385082e4caa6a6786c4e033468", + "reference": "50895a07cface1385082e4caa6a6786c4e033468", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.7", + "infection/infection": "^0.32", + "justinrainbow/json-schema": "^6.6", + "keradus/cli-executor": "^2.3", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.9", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", + "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-01-23T17:33:21+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "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", + "phpspec/prophecy": "^1.10", + "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.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-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/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "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.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+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": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.37", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49", + "reference": "28cd424c5ea984128c95cfa7ea658808e8954e49", + "shasum": "" + }, + "require": { + "php": "^7.4|^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" + } + ], + "time": "2026-01-24T08:21:55+00:00" + }, + { + "name": "phpstan/phpstan-mockery", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-mockery.git", + "reference": "89a949d0ac64298e88b7c7fa00caee565c198394" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-mockery/zipball/89a949d0ac64298e88b7c7fa00caee565c198394", + "reference": "89a949d0ac64298e88b7c7fa00caee565c198394", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "mockery/mockery": "^1.6.11", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan Mockery extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-mockery/issues", + "source": "https://github.com/phpstan/phpstan-mockery/tree/2.0.0" + }, + "time": "2024-10-14T03:18:12+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "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-main": "12.5.x-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/12.5.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:03:04+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.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", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/37ddb96c14bfee10304825edbb7e66d341ec6889", + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.2", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-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/12.5.8" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T06:12:29+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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 for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-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", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:28:48+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.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 functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-08-12T14:11:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.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": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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 types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/console", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b", + "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4|^8.0" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "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": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-13T13:06:50+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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 an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:55:31+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "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.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/process", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "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 a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T07:36:47+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "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/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/src/Annotation/Since.php b/src/Annotation/Since.php index 10e30ea..aa23d62 100644 --- a/src/Annotation/Since.php +++ b/src/Annotation/Since.php @@ -8,7 +8,7 @@ final readonly class Since { public function __construct( - public string $version + public string $version, ) { } } diff --git a/src/Annotation/Until.php b/src/Annotation/Until.php index dd58d37..69159a2 100644 --- a/src/Annotation/Until.php +++ b/src/Annotation/Until.php @@ -8,7 +8,7 @@ final readonly class Until { public function __construct( - public string $version + public string $version, ) { } } diff --git a/src/Controller/KeycloakController.php b/src/Controller/KeycloakController.php index 647fa6b..39342d5 100644 --- a/src/Controller/KeycloakController.php +++ b/src/Controller/KeycloakController.php @@ -16,7 +16,7 @@ final class KeycloakController extends AbstractController { public function __construct( private readonly LoggerInterface $keycloakClientLogger, - private readonly IamClientInterface $iamClient + private readonly IamClientInterface $iamClient, ) { } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 668ad3d..d3f37dd 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -13,52 +13,104 @@ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('mainick_keycloak_client'); $rootNode = $treeBuilder->getRootNode(); - $adminCliChildren = $rootNode->children()->arrayNode('admin_cli')->children(); + $adminCliChildren = $rootNode + ->children() + ->arrayNode('admin_cli') + ->children(); $rootNode ->children() - ->arrayNode('keycloak') - ->children() - ->booleanNode('verify_ssl')->isRequired()->defaultTrue()->end() - ->scalarNode('base_url')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('realm')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('client_id')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('client_secret')->defaultNull()->end() - ->scalarNode('redirect_uri')->defaultNull()->end() - ->scalarNode('encryption_algorithm')->defaultNull()->end() - ->scalarNode('encryption_key')->defaultNull()->end() - ->scalarNode('encryption_key_path')->defaultNull()->end() - ->scalarNode('encryption_key_passphrase')->defaultNull()->end() - ->scalarNode('version')->defaultNull()->end() - ->arrayNode('allowed_jwks_domains') - ->info('Whitelist of allowed domains for JWKS endpoint requests. If empty, only the base_url domain is allowed.') - ->scalarPrototype()->end() - ->end() - ->end() - ->validate() - ->ifTrue(function ($v) { - return empty($v['encryption_key']) && empty($v['encryption_key_path']); - }) - ->thenInvalid('At least one of "encryption_key" or "encryption_key_path" must be provided.') - ->end() - ->end() - ->arrayNode('security') - ->info('Enable this if you want to use the Keycloak security layer. This will protect your application with Keycloak.') - ->canBeEnabled() - ->children() - ->scalarNode('default_target_route_name')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('admin_cli') - ->info('Enable this if you want to use the admin-cli client to authenticate with Keycloak. This is useful if you want to use the Keycloak Admin REST API.') - ->canBeEnabled() - ->children() - ->scalarNode('realm')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('client_id')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('username')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('password')->isRequired()->cannotBeEmpty()->end() - ->end() - ->end() + ->arrayNode('keycloak') + ->children() + ->booleanNode('verify_ssl') + ->isRequired() + ->end() + ->scalarNode('base_url') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('realm') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('client_id') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('client_secret') + ->defaultNull() + ->end() + ->scalarNode('redirect_uri') + ->defaultNull() + ->end() + ->scalarNode('encryption_algorithm') + ->defaultNull() + ->end() + ->scalarNode('encryption_key') + ->defaultNull() + ->end() + ->scalarNode('encryption_key_path') + ->defaultNull() + ->end() + ->scalarNode('encryption_key_passphrase') + ->defaultNull() + ->end() + ->scalarNode('version') + ->defaultNull() + ->end() + ->arrayNode('allowed_jwks_domains') + ->info( + 'Whitelist of allowed domains for JWKS endpoint requests. If empty, only the base_url domain is allowed.', + ) + ->scalarPrototype() + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return empty($v['encryption_key']) + && empty($v['encryption_key_path']); + }) + ->thenInvalid( + 'At least one of "encryption_key" or "encryption_key_path" must be provided.', + ) + ->end() + ->end() + ->arrayNode('security') + ->info( + 'Enable this if you want to use the Keycloak security layer. This will protect your application with Keycloak.', + ) + ->canBeEnabled() + ->children() + ->scalarNode('default_target_route_name') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('admin_cli') + ->info( + 'Enable this if you want to use the admin-cli client to authenticate with Keycloak. This is useful if you want to use the Keycloak Admin REST API.', + ) + ->canBeEnabled() + ->children() + ->scalarNode('realm') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('client_id') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('username') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('password') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/src/EventSubscriber/ExceptionListener.php b/src/EventSubscriber/ExceptionListener.php index 152ec34..f49c282 100644 --- a/src/EventSubscriber/ExceptionListener.php +++ b/src/EventSubscriber/ExceptionListener.php @@ -12,7 +12,7 @@ final readonly class ExceptionListener { public function __construct( - private UrlGeneratorInterface $urlGenerator + private UrlGeneratorInterface $urlGenerator, ) { } diff --git a/src/EventSubscriber/LogoutAuthListener.php b/src/EventSubscriber/LogoutAuthListener.php index 947d19a..051ab2e 100644 --- a/src/EventSubscriber/LogoutAuthListener.php +++ b/src/EventSubscriber/LogoutAuthListener.php @@ -19,7 +19,7 @@ public function __construct( private UrlGeneratorInterface $urlGenerator, private TokenStorageInterface $tokenStorage, private IamClientInterface $iamClient, - private string $defaultTargetRouteName + private string $defaultTargetRouteName, ) { } diff --git a/src/EventSubscriber/TokenAuthListener.php b/src/EventSubscriber/TokenAuthListener.php index b944afd..b90fa9e 100644 --- a/src/EventSubscriber/TokenAuthListener.php +++ b/src/EventSubscriber/TokenAuthListener.php @@ -63,11 +63,13 @@ public function checkValidToken(RequestEvent $requestEvent): void $jwtToken = $request->headers->get('X-Auth-Token'); if (!$jwtToken) { $this->setUnauthorizedResponse($requestEvent, 'Token not found'); + return; } if (!$this->validateToken($jwtToken, $request)) { $this->setUnauthorizedResponse($requestEvent, 'Token not valid'); + return; } } @@ -78,8 +80,8 @@ private function shouldSkipRouteValidation(?string $route): bool return false; } - return in_array($route, self::EXCLUDED_ROUTES, true) || - !empty(array_filter(self::EXCLUDED_ROUTES_PREFIX, static fn (string $prefix): bool => str_starts_with($route, $prefix))); + return in_array($route, self::EXCLUDED_ROUTES, true) + || !empty(array_filter(self::EXCLUDED_ROUTES_PREFIX, static fn (string $prefix): bool => str_starts_with($route, $prefix))); } private function shouldSkipControllerValidation(mixed $controller): bool @@ -101,7 +103,7 @@ private function shouldSkipControllerValidation(mixed $controller): bool if (is_string($controller)) { // Check if "Controller::method" or "Controller:method" format $parts = preg_split('/:{1,2}/', $controller); - if (count($parts) === 2) { + if (2 === count($parts)) { $controllerClass = $parts[0]; $controllerMethod = $parts[1]; } @@ -118,6 +120,7 @@ private function shouldSkipControllerValidation(mixed $controller): bool try { $reflectionMethod = new \ReflectionMethod($controllerClass, $controllerMethod); + return !empty($reflectionMethod->getAttributes(ExcludeTokenValidationAttribute::class)); } catch (\ReflectionException) { @@ -134,6 +137,7 @@ private function validateToken(string $jwtToken, Request $request): bool $userInfo = $this->iamClient->userInfo($token); if ($userInfo) { $request->attributes->set('user', $userInfo); + return true; } diff --git a/src/Exception/KeycloakAuthenticationException.php b/src/Exception/KeycloakAuthenticationException.php index 274b8e4..65bd521 100644 --- a/src/Exception/KeycloakAuthenticationException.php +++ b/src/Exception/KeycloakAuthenticationException.php @@ -6,5 +6,4 @@ class KeycloakAuthenticationException extends \Exception { - } diff --git a/src/Exception/PropertyDoesNotExistException.php b/src/Exception/PropertyDoesNotExistException.php index 97d337d..0d74cac 100644 --- a/src/Exception/PropertyDoesNotExistException.php +++ b/src/Exception/PropertyDoesNotExistException.php @@ -6,5 +6,4 @@ class PropertyDoesNotExistException extends \Exception { - } diff --git a/src/Exception/TokenDecoderException.php b/src/Exception/TokenDecoderException.php index 419b657..3a91e7a 100644 --- a/src/Exception/TokenDecoderException.php +++ b/src/Exception/TokenDecoderException.php @@ -38,23 +38,32 @@ public static function forInvalidToken(\Exception $e): self return new self('Invalid token', $e); } - public static function forInvalidConfiguration(string $message, ?\Exception $e = null): self - { + public static function forInvalidConfiguration( + string $message, + ?\Exception $e = null, + ): self { return new self($message, $e ?? new \Exception($message)); } public static function forJwksError(string $message, \Exception $e): self { - return new self('JWKS error: ' . $message, $e); + return new self('JWKS error: '.$message, $e); } - public static function forDecodingError(string $message, \Exception $e): self - { - return new self('Token decoding error: ' . $message, $e); + public static function forDecodingError( + string $message, + \Exception $e, + ): self { + return new self('Token decoding error: '.$message, $e); } - public static function forSecurityViolation(string $message, ?\Exception $e = null): self - { - return new self('Security violation: ' . $message, $e ?? new \Exception($message)); + public static function forSecurityViolation( + string $message, + ?\Exception $e = null, + ): self { + return new self( + 'Security violation: '.$message, + $e ?? new \Exception($message), + ); } } diff --git a/src/Interface/AccessTokenInterface.php b/src/Interface/AccessTokenInterface.php index d6e5cf1..9023e08 100644 --- a/src/Interface/AccessTokenInterface.php +++ b/src/Interface/AccessTokenInterface.php @@ -47,11 +47,15 @@ public function hasExpired(): bool; /** * Returns additional vendor values stored in the token. + * + * @return array */ public function getValues(): array; /** * Sets additional vendor values stored in the token. + * + * @param array $values */ public function setValues(array $values): self; @@ -63,6 +67,8 @@ public function __toString(): string; /** * Returns an array of parameters to serialize when this is serialized with * json_encode(). + * + * @return array */ public function jsonSerialize(): array; } diff --git a/src/Interface/IamClientInterface.php b/src/Interface/IamClientInterface.php index f0f2c6c..55c2cad 100644 --- a/src/Interface/IamClientInterface.php +++ b/src/Interface/IamClientInterface.php @@ -9,15 +9,26 @@ interface IamClientInterface { - public function refreshToken(AccessTokenInterface $token): ?AccessTokenInterface; + public function refreshToken( + AccessTokenInterface $token, + ): ?AccessTokenInterface; - public function verifyToken(AccessTokenInterface $token): ?UserRepresentationDTO; + public function verifyToken( + AccessTokenInterface $token, + ): ?UserRepresentationDTO; - public function userInfo(AccessTokenInterface $token): ?UserRepresentationDTO; + public function userInfo( + AccessTokenInterface $token, + ): ?UserRepresentationDTO; + /** + * @return ?array + */ public function userInfoRaw(AccessTokenInterface $token): ?array; - public function fetchUserFromToken(AccessTokenInterface $token): ?KeycloakResourceOwner; + public function fetchUserFromToken( + AccessTokenInterface $token, + ): ?KeycloakResourceOwner; /** * @param array $options @@ -25,16 +36,22 @@ public function fetchUserFromToken(AccessTokenInterface $token): ?KeycloakResour public function getAuthorizationUrl(array $options = []): string; /** - * @param array $options + * @param array $options */ public function logoutUrl(array $options = []): string; /** * @param array $options */ - public function authorize(array $options, ?callable $redirectHandler = null): never; + public function authorize( + array $options, + ?callable $redirectHandler = null, + ): never; - public function authenticate(string $username, string $password): ?AccessTokenInterface; + public function authenticate( + string $username, + string $password, + ): ?AccessTokenInterface; public function getState(): string; @@ -48,31 +65,46 @@ public function hasAnyRole(AccessTokenInterface $token, array $roles): bool; /** * @param array $roles */ - public function hasAllRoles(AccessTokenInterface $token, array $roles): bool; + public function hasAllRoles( + AccessTokenInterface $token, + array $roles, + ): bool; public function hasRole(AccessTokenInterface $token, string $role): bool; /** * @param array $scopes */ - public function hasAnyScope(AccessTokenInterface $token, array $scopes): bool; + public function hasAnyScope( + AccessTokenInterface $token, + array $scopes, + ): bool; /** * @param array $scopes */ - public function hasAllScopes(AccessTokenInterface $token, array $scopes): bool; + public function hasAllScopes( + AccessTokenInterface $token, + array $scopes, + ): bool; public function hasScope(AccessTokenInterface $token, string $scope): bool; /** * @param array $groups */ - public function hasAnyGroup(AccessTokenInterface $token, array $groups): bool; + public function hasAnyGroup( + AccessTokenInterface $token, + array $groups, + ): bool; /** * @param array $groups */ - public function hasAllGroups(AccessTokenInterface $token, array $groups): bool; + public function hasAllGroups( + AccessTokenInterface $token, + array $groups, + ): bool; public function hasGroup(AccessTokenInterface $token, string $group): bool; } diff --git a/src/Interface/ResourceOwnerInterface.php b/src/Interface/ResourceOwnerInterface.php index 19773f2..11abc85 100644 --- a/src/Interface/ResourceOwnerInterface.php +++ b/src/Interface/ResourceOwnerInterface.php @@ -40,6 +40,8 @@ public function getLastName(): ?string; /** * Return all of the owner details available as an array. + * + * @return array */ public function toArray(): array; } diff --git a/src/Interface/TokenDecoderInterface.php b/src/Interface/TokenDecoderInterface.php index 1463f8b..4b4ac87 100644 --- a/src/Interface/TokenDecoderInterface.php +++ b/src/Interface/TokenDecoderInterface.php @@ -12,7 +12,6 @@ interface TokenDecoderInterface public function decode(string $token, string $key): array; /** - * @param string $realm * @param array $tokenDecoded */ public function validateToken(string $realm, array $tokenDecoded): void; diff --git a/src/Provider/KeycloakClient.php b/src/Provider/KeycloakClient.php index 55ebf31..b2142d0 100644 --- a/src/Provider/KeycloakClient.php +++ b/src/Provider/KeycloakClient.php @@ -49,7 +49,8 @@ public function __construct( } if ('' !== $this->encryption_key) { $this->keycloakProvider->setEncryptionKey($this->encryption_key); - } elseif ('' !== $this->encryption_key_path) { + } + elseif ('' !== $this->encryption_key_path) { $this->keycloakProvider->setEncryptionKeyPath($this->encryption_key_path); } } diff --git a/src/Representation/ClientRepresentation.php b/src/Representation/ClientRepresentation.php index 9b805bf..57f81ee 100644 --- a/src/Representation/ClientRepresentation.php +++ b/src/Representation/ClientRepresentation.php @@ -4,12 +4,17 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\DTO\ProtocolMapperRepresentationDTO; use Mainick\KeycloakClientBundle\Representation\Collection\ProtocolMapperCollection; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class ClientRepresentation extends Representation { + /** + * @param ?Map $attributes + * @param ?Map $registeredNodes + * @param ?Map $access + * @param ?Map $authenticationFlowBindingOverrides + */ public function __construct( public ?string $id = null, public ?string $clientId = null, @@ -50,7 +55,7 @@ public function __construct( /** @var string[]|null */ public ?array $optionalClientScopes = null, public ?Map $access = null, - public ?string $origin = null + public ?string $origin = null, ) { } } diff --git a/src/Representation/ClientScopeRepresentation.php b/src/Representation/ClientScopeRepresentation.php index 33502a8..a4f3b59 100644 --- a/src/Representation/ClientScopeRepresentation.php +++ b/src/Representation/ClientScopeRepresentation.php @@ -5,11 +5,13 @@ namespace Mainick\KeycloakClientBundle\Representation; use Mainick\KeycloakClientBundle\Representation\Collection\ProtocolMapperCollection; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class ClientScopeRepresentation extends Representation { + /** + * @param ?Map $attributes + */ public function __construct( public ?string $id = null, public ?string $name = null, diff --git a/src/Representation/Collection/ClientCollection.php b/src/Representation/Collection/ClientCollection.php index 5901d7c..8a6e634 100644 --- a/src/Representation/Collection/ClientCollection.php +++ b/src/Representation/Collection/ClientCollection.php @@ -11,9 +11,6 @@ */ class ClientCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return ClientRepresentation::class; diff --git a/src/Representation/Collection/ClientScopeCollection.php b/src/Representation/Collection/ClientScopeCollection.php index bb60d60..2cdbbee 100644 --- a/src/Representation/Collection/ClientScopeCollection.php +++ b/src/Representation/Collection/ClientScopeCollection.php @@ -11,9 +11,6 @@ */ class ClientScopeCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return ClientScopeRepresentation::class; diff --git a/src/Representation/Collection/Collection.php b/src/Representation/Collection/Collection.php index 5073ef6..7c68501 100644 --- a/src/Representation/Collection/Collection.php +++ b/src/Representation/Collection/Collection.php @@ -56,12 +56,7 @@ public function add(Representation $representation): void { $expectedClass = static::getRepresentationClass(); if (!$representation instanceof $expectedClass) { - throw new \InvalidArgumentException(sprintf( - '%s expects items to be %s representation, %s given', - (new \ReflectionClass(static::class))->getShortName(), - (new \ReflectionClass($expectedClass))->getShortName(), - (new \ReflectionClass($representation))->getShortName() - )); + throw new \InvalidArgumentException(sprintf('%s expects items to be %s representation, %s given', (new \ReflectionClass(static::class))->getShortName(), (new \ReflectionClass($expectedClass))->getShortName(), (new \ReflectionClass($representation))->getShortName())); } $this->items[] = $representation; diff --git a/src/Representation/Collection/CredentialCollection.php b/src/Representation/Collection/CredentialCollection.php index 664d598..e9c7176 100644 --- a/src/Representation/Collection/CredentialCollection.php +++ b/src/Representation/Collection/CredentialCollection.php @@ -11,9 +11,6 @@ */ class CredentialCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return CredentialRepresentation::class; diff --git a/src/Representation/Collection/GroupCollection.php b/src/Representation/Collection/GroupCollection.php index d032f5a..d2ed4f2 100644 --- a/src/Representation/Collection/GroupCollection.php +++ b/src/Representation/Collection/GroupCollection.php @@ -11,9 +11,6 @@ */ class GroupCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return GroupRepresentation::class; diff --git a/src/Representation/Collection/ProtocolMapperCollection.php b/src/Representation/Collection/ProtocolMapperCollection.php index a2d79ec..676f129 100644 --- a/src/Representation/Collection/ProtocolMapperCollection.php +++ b/src/Representation/Collection/ProtocolMapperCollection.php @@ -11,9 +11,6 @@ */ class ProtocolMapperCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return ProtocolMapperRepresentation::class; diff --git a/src/Representation/Collection/RealmCollection.php b/src/Representation/Collection/RealmCollection.php index 90221a8..4977b6b 100644 --- a/src/Representation/Collection/RealmCollection.php +++ b/src/Representation/Collection/RealmCollection.php @@ -11,9 +11,6 @@ */ class RealmCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return RealmRepresentation::class; diff --git a/src/Representation/Collection/RoleCollection.php b/src/Representation/Collection/RoleCollection.php index 0a9c2b1..8088c8d 100644 --- a/src/Representation/Collection/RoleCollection.php +++ b/src/Representation/Collection/RoleCollection.php @@ -11,9 +11,6 @@ */ class RoleCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return RoleRepresentation::class; diff --git a/src/Representation/Collection/UPAttributeCollection.php b/src/Representation/Collection/UPAttributeCollection.php index e0797a8..1016e70 100644 --- a/src/Representation/Collection/UPAttributeCollection.php +++ b/src/Representation/Collection/UPAttributeCollection.php @@ -11,9 +11,6 @@ */ class UPAttributeCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return UPAttribute::class; diff --git a/src/Representation/Collection/UPGroupCollection.php b/src/Representation/Collection/UPGroupCollection.php index e30378e..03fcb77 100644 --- a/src/Representation/Collection/UPGroupCollection.php +++ b/src/Representation/Collection/UPGroupCollection.php @@ -11,9 +11,6 @@ */ class UPGroupCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return UPGroup::class; diff --git a/src/Representation/Collection/UserCollection.php b/src/Representation/Collection/UserCollection.php index e282bdb..6b24e75 100644 --- a/src/Representation/Collection/UserCollection.php +++ b/src/Representation/Collection/UserCollection.php @@ -11,9 +11,6 @@ */ class UserCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return UserRepresentation::class; diff --git a/src/Representation/Collection/UserConsentCollection.php b/src/Representation/Collection/UserConsentCollection.php index 1943d42..c79a863 100644 --- a/src/Representation/Collection/UserConsentCollection.php +++ b/src/Representation/Collection/UserConsentCollection.php @@ -11,9 +11,6 @@ */ class UserConsentCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return UserConsentRepresentation::class; diff --git a/src/Representation/Collection/UserProfileAttributeGroupMetadataCollection.php b/src/Representation/Collection/UserProfileAttributeGroupMetadataCollection.php index 4ef907f..8b48310 100644 --- a/src/Representation/Collection/UserProfileAttributeGroupMetadataCollection.php +++ b/src/Representation/Collection/UserProfileAttributeGroupMetadataCollection.php @@ -11,9 +11,6 @@ */ class UserProfileAttributeGroupMetadataCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return UserProfileAttributeGroupMetadata::class; diff --git a/src/Representation/Collection/UserProfileAttributeMetadataCollection.php b/src/Representation/Collection/UserProfileAttributeMetadataCollection.php index d17f380..b99f215 100644 --- a/src/Representation/Collection/UserProfileAttributeMetadataCollection.php +++ b/src/Representation/Collection/UserProfileAttributeMetadataCollection.php @@ -11,9 +11,6 @@ */ class UserProfileAttributeMetadataCollection extends Collection { - /** - * @inheritDoc - */ public static function getRepresentationClass(): string { return UserProfileAttributeMetadata::class; diff --git a/src/Representation/Composites.php b/src/Representation/Composites.php index 9da7a63..cf7d52f 100644 --- a/src/Representation/Composites.php +++ b/src/Representation/Composites.php @@ -4,12 +4,17 @@ namespace Mainick\KeycloakClientBundle\Representation; +use Mainick\KeycloakClientBundle\Representation\Collection\RealmCollection; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class Composites extends Representation { + /** + * @param ?Map $client + * @param ?Map $application + */ public function __construct( - public ?RealCollection $realm = null, + public ?RealmCollection $realm = null, public ?Map $client = null, public ?Map $application = null, ) { diff --git a/src/Representation/CredentialRepresentation.php b/src/Representation/CredentialRepresentation.php index b52d90d..422179a 100644 --- a/src/Representation/CredentialRepresentation.php +++ b/src/Representation/CredentialRepresentation.php @@ -4,11 +4,13 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class CredentialRepresentation extends Representation { + /** + * @param ?Map $config + */ public function __construct( public ?string $id = null, public ?string $type = null, @@ -27,8 +29,7 @@ public function __construct( public ?string $algorithm = null, public ?int $digits = null, public ?int $period = null, - public ?Map $config = null - ) - { + public ?Map $config = null, + ) { } } diff --git a/src/Representation/GroupRepresentation.php b/src/Representation/GroupRepresentation.php index a5b60cf..7ed3ac9 100644 --- a/src/Representation/GroupRepresentation.php +++ b/src/Representation/GroupRepresentation.php @@ -6,26 +6,27 @@ use Mainick\KeycloakClientBundle\Annotation\Since; use Mainick\KeycloakClientBundle\Representation\Collection\GroupCollection; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class GroupRepresentation extends Representation { + /** + * @param ?Map $attributes + * @param ?Map $clientRoles + * @param ?Map $access + */ public function __construct( public ?string $id = null, public ?string $name = null, public ?string $path = null, - #[Since('23.0.0')] - public ?string $parentId = null, - #[Since('23.0.0')] - public ?int $subGroupCount = null, + #[Since('23.0.0')] public ?string $parentId = null, + #[Since('23.0.0')] public ?int $subGroupCount = null, public ?GroupCollection $subGroups = null, public ?Map $attributes = null, /** @var string[]|null */ public ?array $realmRoles = null, public ?Map $clientRoles = null, - public ?Map $access = null - ) - { + public ?Map $access = null, + ) { } } diff --git a/src/Representation/ProtocolMapperRepresentation.php b/src/Representation/ProtocolMapperRepresentation.php index d91aac6..271444e 100644 --- a/src/Representation/ProtocolMapperRepresentation.php +++ b/src/Representation/ProtocolMapperRepresentation.php @@ -8,6 +8,9 @@ final class ProtocolMapperRepresentation extends Representation { + /** + * @param ?Map $config + */ public function __construct( public ?string $id = null, public ?string $name = null, @@ -15,7 +18,7 @@ public function __construct( public ?string $protocolMapper = null, public ?bool $consentRequired = null, public ?string $consentText = null, - public ?Map $config = null + public ?Map $config = null, ) { } } diff --git a/src/Representation/RealmRepresentation.php b/src/Representation/RealmRepresentation.php index dcae9eb..3c251e1 100644 --- a/src/Representation/RealmRepresentation.php +++ b/src/Representation/RealmRepresentation.php @@ -14,6 +14,12 @@ final class RealmRepresentation extends Representation { + /** + * @param ?Map $smtpServer + * @param ?Map $clientScopeMappings + * @param ?Map $browserSecurityHeaders + * @param ?Map $attributes + */ public function __construct( public ?string $id = null, public ?string $realm = null, @@ -58,11 +64,9 @@ public function __construct( public ?bool $realmCacheEnabled = null, public ?bool $bruteForceProtected = null, public ?bool $permanentLockout = null, - #[Since('24.0.0')] - public ?int $maxTemporaryLockouts = null, + #[Since('24.0.0')] public ?int $maxTemporaryLockouts = null, public ?int $maxFailureWaitSeconds = null, - #[Since('24.0.0')] - public ?int $minimumQuickLoginWaitSeconds = null, + #[Since('24.0.0')] public ?int $minimumQuickLoginWaitSeconds = null, public ?int $waitIncrementSeconds = null, public ?int $quickLoginCheckMilliSeconds = null, public ?int $maxDeltaTimeSeconds = null, @@ -88,8 +92,7 @@ public function __construct( public ?int $otpPolicyDigits = null, public ?int $otpPolicyLookAheadWindow = null, public ?int $otpPolicyPeriod = null, - #[Since('20.0.0')] - public ?bool $otpPolicyCodeReusable = null, + #[Since('20.0.0')] public ?bool $otpPolicyCodeReusable = null, /** @var string[]|null */ public ?array $otpSupportedApplications = null, public ?string $webAuthnPolicyRpEntityName = null, @@ -105,8 +108,7 @@ public function __construct( /** @var string[]|null */ public ?array $webAuthnPolicyAcceptableAaguids = null, /** @var string[]|null */ - #[Since('23.0.0')] - public ?array $webAuthnPolicyExtraOrigins = null, + #[Since('23.0.0')] public ?array $webAuthnPolicyExtraOrigins = null, public ?string $webAuthnPolicyPasswordlessRpEntityName = null, /** @var string[]|null */ public ?array $webAuthnPolicyPasswordlessSignatureAlgorithms = null, @@ -120,13 +122,13 @@ public function __construct( /** @var string[]|null */ public ?array $webAuthnPolicyPasswordlessAcceptableAaguids = null, /** @var string[]|null */ - #[Since('23.0.0')] + #[Since('23.0.0'),] public ?array $webAuthnPolicyPasswordlessExtraOrigins = null, - //public ?ClientProfiles $clientProfiles = null, - //public ?ClientPolicies $clientPolicies = null, + // public ?ClientProfiles $clientProfiles = null, + // public ?ClientPolicies $clientPolicies = null, public ?UserCollection $users = null, public ?UserCollection $federatedUsers = null, - //public ?ScopeMappingCollection $scopeMappings = null, + // public ?ScopeMappingCollection $scopeMappings = null, public ?Map $clientScopeMappings = null, public ?ClientCollection $clients = null, public ?ClientScopeCollection $clientScopes = null, @@ -136,8 +138,8 @@ public function __construct( public ?array $defaultOptionalClientScopes = null, public ?Map $browserSecurityHeaders = null, public ?Map $smtpServer = null, - //public ?UserFederationProviderCollection $userFederationProviders = null, - //public ?UserFederationMapperCollection $userFederationMappers = null, + // public ?UserFederationProviderCollection $userFederationProviders = null, + // public ?UserFederationMapperCollection $userFederationMappers = null, public ?string $loginTheme = null, public ?string $accountTheme = null, public ?string $adminTheme = null, @@ -150,56 +152,48 @@ public function __construct( public ?array $enabledEventTypes = null, public ?bool $adminEventsEnabled = null, public ?bool $adminEventsDetailsEnabled = null, - //public ?IdentityProviderCollection $identityProviders = null, - //public ?IdentityProviderMapperCollection $identityProviderMappers = null, + // public ?IdentityProviderCollection $identityProviders = null, + // public ?IdentityProviderMapperCollection $identityProviderMappers = null, public ?ProtocolMapperCollection $protocolMappers = null, - //public ?MultivaluedHashMap $components = null, + // public ?MultivaluedHashMap $components = null, public ?bool $internationalizationEnabled = null, /** @var string[]|null */ public ?array $supportedLocales = null, public ?string $defaultLocale = null, - //public ?AuthenticationFlowCollection $authenticationFlows = null, - //public ?AuthenticatorConfigCollection $authenticatorConfig = null, - //public ?RequiredActionProviderCollection $requiredActions = null, + // public ?AuthenticationFlowCollection $authenticationFlows = null, + // public ?AuthenticatorConfigCollection $authenticatorConfig = null, + // public ?RequiredActionProviderCollection $requiredActions = null, public ?string $browserFlow = null, public ?string $registrationFlow = null, public ?string $directGrantFlow = null, public ?string $resetCredentialsFlow = null, public ?string $clientAuthenticationFlow = null, public ?string $dockerAuthenticationFlow = null, - #[Since('24.0.0')] - public ?string $firstBrokerLoginFlow = null, + #[Since('24.0.0')] public ?string $firstBrokerLoginFlow = null, public ?Map $attributes = null, public ?string $keycloakVersion = null, public ?bool $userManagedAccessAllowed = null, -// #[Since('25.0.0')] -// public ?bool $organizationsEnabled = null, -// #[Since('25.0.0')] -// public ?OrganizationCollection $organizations = null, - #[Since('25.0.0')] - public ?bool $verifiableCredentialsEnabled = null, - #[Since('25.0.0')] - public ?bool $adminPermissionsEnabled = null, - #[Since('25.0.0')] - public ?bool $social = null, - #[Since('25.0.0')] + // #[Since('25.0.0')] + // public ?bool $organizationsEnabled = null, + // #[Since('25.0.0')] + // public ?OrganizationCollection $organizations = null, + #[Since('25.0.0')] public ?bool $verifiableCredentialsEnabled = null, + #[Since('25.0.0')] public ?bool $adminPermissionsEnabled = null, + #[Since('25.0.0')] public ?bool $social = null, + #[Since('25.0.0'),] public ?bool $updateProfileOnInitialSocialLogin = null, /** @var string[]|null */ - #[Since('25.0.0')] - public ?array $socialProviders = null, - /** @var string[]|null */ - #[Since('25.0.0')] - public ?array $applicationScopeMappings = null, -// #[Since('25.0.0')] -// public ?ApplicationRepresentation $application = null, -// #[Since('25.0.0')] -// public ?OAuthClientRepresentation $oauthClients = null, -// #[Since('25.0.0')] -// public ?ClientTemplateRepresentation $clientTemplates = null, - #[Since('25.0.0')] - public ?int $oAuth2DeviceCodeLifespan = null, - #[Since('25.0.0')] - public ?int $oAuth2DevicePollingInterval = null, + #[Since('25.0.0')] public ?array $socialProviders = null, + /** @var string[]|null */ + #[Since('25.0.0')] public ?array $applicationScopeMappings = null, + // #[Since('25.0.0')] + // public ?ApplicationRepresentation $application = null, + // #[Since('25.0.0')] + // public ?OAuthClientRepresentation $oauthClients = null, + // #[Since('25.0.0')] + // public ?ClientTemplateRepresentation $clientTemplates = null, + #[Since('25.0.0')] public ?int $oAuth2DeviceCodeLifespan = null, + #[Since('25.0.0')] public ?int $oAuth2DevicePollingInterval = null, ) { } } diff --git a/src/Representation/Representation.php b/src/Representation/Representation.php index 69fa5ac..b7c8cfa 100644 --- a/src/Representation/Representation.php +++ b/src/Representation/Representation.php @@ -12,8 +12,8 @@ abstract class Representation implements \JsonSerializable abstract public function __construct(); /** - * @param array $properties - * @return static + * @param array $properties + * * @throws PropertyDoesNotExistException */ final public static function from(array $properties): static @@ -27,24 +27,30 @@ final public static function from(array $properties): static } /** - * @param string $json - * @return static * @throws PropertyDoesNotExistException */ public static function fromJson(string $json): static { - return static::from((new JsonEncoder())->decode($json, JsonEncoder::FORMAT)); + return static::from( + new JsonEncoder()->decode($json, JsonEncoder::FORMAT), + ); } + /** + * @return array + */ final public function jsonSerialize(): array { $serializable = []; - $reflectedClass = (new \ReflectionClass($this)); - $properties = $reflectedClass->getProperties(\ReflectionProperty::IS_PUBLIC); + $reflectedClass = new \ReflectionClass($this); + $properties = $reflectedClass->getProperties( + \ReflectionProperty::IS_PUBLIC, + ); foreach ($properties as $property) { - $serializable[$property->getName()] = ($property->getValue($this) instanceof \JsonSerializable) - ? $property->getValue($this)->jsonSerialize() - : $property->getValue($this); + $serializable[$property->getName()] = + $property->getValue($this) instanceof \JsonSerializable + ? $property->getValue($this)->jsonSerialize() + : $property->getValue($this); } return $serializable; diff --git a/src/Representation/RoleRepresentation.php b/src/Representation/RoleRepresentation.php index 6f96564..db0f2fa 100644 --- a/src/Representation/RoleRepresentation.php +++ b/src/Representation/RoleRepresentation.php @@ -8,6 +8,9 @@ final class RoleRepresentation extends Representation { + /** + * @param ?Map $attributes + */ public function __construct( public ?string $id = null, public ?string $name = null, diff --git a/src/Representation/RolesRepresentation.php b/src/Representation/RolesRepresentation.php index 4fb7781..af00343 100644 --- a/src/Representation/RolesRepresentation.php +++ b/src/Representation/RolesRepresentation.php @@ -9,6 +9,10 @@ final class RolesRepresentation extends Representation { + /** + * @param ?Map $client + * @param ?Map $application + */ public function __construct( public ?RealmCollection $realm = null, public ?Map $client = null, diff --git a/src/Representation/Type/Map.php b/src/Representation/Type/Map.php index 637b97f..3be0346 100644 --- a/src/Representation/Type/Map.php +++ b/src/Representation/Type/Map.php @@ -4,21 +4,18 @@ namespace Mainick\KeycloakClientBundle\Representation\Type; -use Traversable; - /** * @template T * - * @implements \JsonSerializable + * @implements \IteratorAggregate */ -class Map extends Type implements \Countable, \IteratorAggregate +class Map extends Type implements \Countable, \IteratorAggregate, \JsonSerializable { /** * @param array $data */ - public function __construct( - private array $data = [] - ) { + public function __construct(private array $data = []) + { } /** @@ -56,6 +53,11 @@ public function get(string $key): mixed return $this->data[$key]; } + /** + * @param T $value + * + * @return Map + */ public function with(string $key, mixed $value): self { $clone = clone $this; @@ -64,6 +66,9 @@ public function with(string $key, mixed $value): self return $clone; } + /** + * @return Map + */ public function without(string $key): self { $clone = clone $this; diff --git a/src/Representation/UPAttribute.php b/src/Representation/UPAttribute.php index 810a979..39b5bd7 100644 --- a/src/Representation/UPAttribute.php +++ b/src/Representation/UPAttribute.php @@ -4,11 +4,14 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class UPAttribute extends Representation { + /** + * @param ?Map $validations + * @param ?Map $annotations + */ public function __construct( public ?string $name = null, public ?string $displayName = null, @@ -19,6 +22,6 @@ public function __construct( public ?UPAttributeSelector $selector = null, public ?string $group = null, public ?bool $multivalued = null, - ){ + ) { } } diff --git a/src/Representation/UPAttributePermissions.php b/src/Representation/UPAttributePermissions.php index ca29d8c..c705a80 100644 --- a/src/Representation/UPAttributePermissions.php +++ b/src/Representation/UPAttributePermissions.php @@ -8,6 +8,10 @@ final class UPAttributePermissions extends Representation { + /** + * @param ?Map $view + * @param ?Map $edit + */ public function __construct( public ?Map $view = null, public ?Map $edit = null, diff --git a/src/Representation/UPAttributeRequired.php b/src/Representation/UPAttributeRequired.php index a3e88f9..638ab67 100644 --- a/src/Representation/UPAttributeRequired.php +++ b/src/Representation/UPAttributeRequired.php @@ -8,6 +8,10 @@ final class UPAttributeRequired extends Representation { + /** + * @param ?Map $roles + * @param ?Map $scopes + */ public function __construct( public ?Map $roles = null, public ?Map $scopes = null, diff --git a/src/Representation/UPAttributeSelector.php b/src/Representation/UPAttributeSelector.php index a67a9c1..1cbe5a0 100644 --- a/src/Representation/UPAttributeSelector.php +++ b/src/Representation/UPAttributeSelector.php @@ -8,8 +8,10 @@ final class UPAttributeSelector extends Representation { - public function __construct( - public ?Map $scopes = null, - ) { + /** + * @param ?Map $scopes + */ + public function __construct(public ?Map $scopes = null) + { } } diff --git a/src/Representation/UPConfig.php b/src/Representation/UPConfig.php index 7c66089..cbff228 100644 --- a/src/Representation/UPConfig.php +++ b/src/Representation/UPConfig.php @@ -6,7 +6,6 @@ use Mainick\KeycloakClientBundle\Representation\Collection\UPAttributeCollection; use Mainick\KeycloakClientBundle\Representation\Collection\UPGroupCollection; -use Mainick\KeycloakClientBundle\Representation\Representation; final class UPConfig extends Representation { diff --git a/src/Representation/UPGroup.php b/src/Representation/UPGroup.php index bf827a8..1c825cd 100644 --- a/src/Representation/UPGroup.php +++ b/src/Representation/UPGroup.php @@ -4,11 +4,13 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class UPGroup extends Representation { + /** + * @param ?Map $annotations + */ public function __construct( public ?string $name = null, public ?string $displayHeader = null, diff --git a/src/Representation/UserConsentRepresentation.php b/src/Representation/UserConsentRepresentation.php index 53d8357..492d9ad 100644 --- a/src/Representation/UserConsentRepresentation.php +++ b/src/Representation/UserConsentRepresentation.php @@ -4,8 +4,6 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; - class UserConsentRepresentation extends Representation { public function __construct( diff --git a/src/Representation/UserProfileAttributeGroupMetadata.php b/src/Representation/UserProfileAttributeGroupMetadata.php index 4f39ac0..9e74280 100644 --- a/src/Representation/UserProfileAttributeGroupMetadata.php +++ b/src/Representation/UserProfileAttributeGroupMetadata.php @@ -4,11 +4,13 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class UserProfileAttributeGroupMetadata extends Representation { + /** + * @param ?Map $annotations + */ public function __construct( public ?string $name = null, public ?string $displayHeader = null, diff --git a/src/Representation/UserProfileAttributeMetadata.php b/src/Representation/UserProfileAttributeMetadata.php index a53aab7..621e9b2 100644 --- a/src/Representation/UserProfileAttributeMetadata.php +++ b/src/Representation/UserProfileAttributeMetadata.php @@ -4,11 +4,14 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class UserProfileAttributeMetadata extends Representation { + /** + * @param ?Map $annotations + * @param ?Map $validators + */ public function __construct( public ?string $name = null, public ?string $displayName = null, diff --git a/src/Representation/UserProfileMetadata.php b/src/Representation/UserProfileMetadata.php index f99ce87..7e924b5 100644 --- a/src/Representation/UserProfileMetadata.php +++ b/src/Representation/UserProfileMetadata.php @@ -6,7 +6,6 @@ use Mainick\KeycloakClientBundle\Representation\Collection\UserProfileAttributeGroupMetadataCollection; use Mainick\KeycloakClientBundle\Representation\Collection\UserProfileAttributeMetadataCollection; -use Mainick\KeycloakClientBundle\Representation\Representation; final class UserProfileMetadata extends Representation { diff --git a/src/Representation/UserRepresentation.php b/src/Representation/UserRepresentation.php index 18e3db4..d5df7eb 100644 --- a/src/Representation/UserRepresentation.php +++ b/src/Representation/UserRepresentation.php @@ -10,6 +10,12 @@ final class UserRepresentation extends Representation { + /** + * @param ?Map $attributes + * @param ?Map $clientRoles + * @param ?Map $applicationRoles + * @param ?Map $access + */ public function __construct( public ?string $id = null, public ?string $username = null, @@ -31,14 +37,14 @@ public function __construct( public ?array $disableableCredentialTypes = null, /** @var string[]|null */ public ?array $requiredActions = null, - //public ?FederatedIdentityCollection $federatedIdentities = null, + // public ?FederatedIdentityCollection $federatedIdentities = null, /** @var string[]|null */ public ?array $realmRoles = null, public ?Map $clientRoles = null, public ?UserConsentCollection $clientConsents = null, public ?int $notBefore = null, public ?Map $applicationRoles = null, - //public ?SocialLinkCollection $socialLinks = null, + // public ?SocialLinkCollection $socialLinks = null, /** @var string[]|null */ public ?array $groups = null, public ?Map $access = null, diff --git a/src/Representation/UserSessionRepresentation.php b/src/Representation/UserSessionRepresentation.php index 28ae878..bbb6876 100644 --- a/src/Representation/UserSessionRepresentation.php +++ b/src/Representation/UserSessionRepresentation.php @@ -4,11 +4,13 @@ namespace Mainick\KeycloakClientBundle\Representation; -use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Representation\Type\Map; final class UserSessionRepresentation extends Representation { + /** + * @param ?Map $clients + */ public function __construct( public ?string $id = null, public ?string $username = null, @@ -19,7 +21,6 @@ public function __construct( public ?bool $rememberMe = null, public ?Map $clients = null, public ?bool $transientUser = null, - ) - { + ) { } } diff --git a/src/Security/Authenticator/KeycloakAuthenticator.php b/src/Security/Authenticator/KeycloakAuthenticator.php index a70a629..98fb83b 100644 --- a/src/Security/Authenticator/KeycloakAuthenticator.php +++ b/src/Security/Authenticator/KeycloakAuthenticator.php @@ -4,6 +4,7 @@ namespace Mainick\KeycloakClientBundle\Security\Authenticator; +use GuzzleHttp\Exception\ClientException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use Mainick\KeycloakClientBundle\DTO\KeycloakAuthorizationCodeEnum; use Mainick\KeycloakClientBundle\Interface\IamClientInterface; @@ -25,24 +26,31 @@ class KeycloakAuthenticator extends AbstractAuthenticator implements Interactive public function __construct( private readonly LoggerInterface $keycloakClientLogger, private readonly IamClientInterface $iamClient, - private readonly KeycloakUserProvider $userProvider + private readonly KeycloakUserProvider $userProvider, ) { } public function supports(Request $request): ?bool { - return 'mainick_keycloak_security_auth_connect_check' === $request->attributes->get('_route'); + return 'mainick_keycloak_security_auth_connect_check' === + $request->attributes->get('_route'); } public function authenticate(Request $request): Passport { - $queryState = $request->query->get(KeycloakAuthorizationCodeEnum::STATE_KEY); - $sessionState = $request->getSession()->get(KeycloakAuthorizationCodeEnum::STATE_SESSION_KEY); + $queryState = $request->query->get( + KeycloakAuthorizationCodeEnum::STATE_KEY, + ); + $sessionState = $request + ->getSession() + ->get(KeycloakAuthorizationCodeEnum::STATE_SESSION_KEY); if (null === $queryState || $queryState !== $sessionState) { throw new AuthenticationException(sprintf('query state (%s) is not the same as session state (%s)', $queryState ?? 'NULL', $sessionState ?? 'NULL')); } - $queryCode = $request->query->get(KeycloakAuthorizationCodeEnum::CODE_KEY); + $queryCode = $request->query->get( + KeycloakAuthorizationCodeEnum::CODE_KEY, + ); if (null === $queryCode) { throw new AuthenticationException('Authentication failed! Did you authorize our app?'); } @@ -53,14 +61,20 @@ public function authenticate(Request $request): Passport catch (IdentityProviderException $e) { throw new AuthenticationException(sprintf('Error authenticating code grant (%s)', $e->getMessage()), previous: $e); } + catch (ClientException $e) { + throw new AuthenticationException(sprintf('Bad status code returned by openID server (%s)', $e->getResponse()->getStatusCode()), previous: $e); + } catch (\Exception $e) { - throw new AuthenticationException(sprintf('Bad status code returned by openID server (%s)', $e->getStatusCode()), previous: $e); + throw new AuthenticationException(sprintf('Unexpected error occurred (%s)', $e->getMessage()), previous: $e); } if (!$accessToken || !$accessToken->getToken()) { - $this->keycloakClientLogger->error('KeycloakAuthenticator::authenticate', [ - 'error' => 'No access token provided', - ]); + $this->keycloakClientLogger->error( + 'KeycloakAuthenticator::authenticate', + [ + 'error' => 'No access token provided', + ], + ); throw new CustomUserMessageAuthenticationException('No access token provided'); } @@ -71,20 +85,30 @@ public function authenticate(Request $request): Passport throw new CustomUserMessageAuthenticationException('Refresh token not found'); } - return new SelfValidatingPassport(new UserBadge($accessToken->getToken(), fn () => $this->userProvider->loadUserByIdentifier($accessToken))); + return new SelfValidatingPassport( + new UserBadge( + $accessToken->getToken(), + fn () => $this->userProvider->loadUserByIdentifier($accessToken), + ), + ); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response - { + public function onAuthenticationSuccess( + Request $request, + TokenInterface $token, + string $firewallName, + ): ?Response { return null; } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response - { - $request->getSession()->getBag('flashes')->add( - 'error', - 'An authentication error occured', - ); + public function onAuthenticationFailure( + Request $request, + AuthenticationException $exception, + ): ?Response { + $errors = [ + 'error' => 'An authentication error occured', + ]; + $request->getSession()->getBag('flashes')->clear()->initialize($errors); // $message = strtr($exception->getMessageKey(), $exception->getMessageData()); return new Response('Authentication failed', Response::HTTP_FORBIDDEN); diff --git a/src/Security/EntryPoint/KeycloakAuthenticationEntryPoint.php b/src/Security/EntryPoint/KeycloakAuthenticationEntryPoint.php index c6695a4..21af6e0 100644 --- a/src/Security/EntryPoint/KeycloakAuthenticationEntryPoint.php +++ b/src/Security/EntryPoint/KeycloakAuthenticationEntryPoint.php @@ -22,38 +22,55 @@ public function __construct( ) { } - public function start(Request $request, ?AuthenticationException $authException = null): Response - { + public function start( + Request $request, + ?AuthenticationException $authException = null, + ): Response { // Handling AJAX requests if ($request->isXmlHttpRequest()) { return new JsonResponse( [ 'code' => Response::HTTP_UNAUTHORIZED, 'message' => 'Authentication Required', - 'login_url' => $this->urlGenerator->generate('mainick_keycloak_security_auth_connect'), + 'login_url' => $this->urlGenerator->generate( + 'mainick_keycloak_security_auth_connect', + ), ], - Response::HTTP_UNAUTHORIZED + Response::HTTP_UNAUTHORIZED, ); } if ($request->hasSession()) { - $request->getSession()->set(KeycloakAuthorizationCodeEnum::LOGIN_REFERRER, $request->getUri()); + $request + ->getSession() + ->set( + KeycloakAuthorizationCodeEnum::LOGIN_REFERRER, + $request->getUri(), + ); - $request->getSession()->getBag('flashes')->add( - 'info', - 'Please log in to access this page', - ); + $info = [ + 'info' => 'Please log in to access this page', + ]; + + $flashes = $request->getSession()->getBag('flashes'); + $flashes->clear(); + $flashes->initialize($info); } - $this->keycloakClientLogger?->info('KeycloakAuthenticationEntryPoint::start', [ - 'path' => $request->getPathInfo(), - 'error' => $authException?->getMessage(), - 'loginReferrer' => $request->getUri(), - ]); + $this->keycloakClientLogger?->info( + 'KeycloakAuthenticationEntryPoint::start', + [ + 'path' => $request->getPathInfo(), + 'error' => $authException?->getMessage(), + 'loginReferrer' => $request->getUri(), + ], + ); return new RedirectResponse( - $this->urlGenerator->generate('mainick_keycloak_security_auth_connect'), - Response::HTTP_TEMPORARY_REDIRECT + $this->urlGenerator->generate( + 'mainick_keycloak_security_auth_connect', + ), + Response::HTTP_TEMPORARY_REDIRECT, ); } } diff --git a/src/Security/User/KeycloakUserProvider.php b/src/Security/User/KeycloakUserProvider.php index fccaac9..ae03b86 100644 --- a/src/Security/User/KeycloakUserProvider.php +++ b/src/Security/User/KeycloakUserProvider.php @@ -14,11 +14,14 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +/** + * @implements UserProviderInterface + */ class KeycloakUserProvider implements UserProviderInterface { public function __construct( private readonly LoggerInterface $keycloakClientLogger, - private readonly IamClientInterface $iamClient + private readonly IamClientInterface $iamClient, ) { } @@ -30,10 +33,13 @@ public function refreshUser(UserInterface $user): UserInterface $accessToken = $user->getAccessToken(); if (!$accessToken) { - $this->keycloakClientLogger->error('KeycloakUserProvider::refreshUser', [ - 'message' => 'User does not have an access token.', - 'user_id' => $user->getUserIdentifier(), - ]); + $this->keycloakClientLogger->error( + 'KeycloakUserProvider::refreshUser', + [ + 'message' => 'User does not have an access token.', + 'user_id' => $user->getUserIdentifier(), + ], + ); throw new AuthenticationException('No valid access token available. Please login again.'); } @@ -48,11 +54,14 @@ public function refreshUser(UserInterface $user): UserInterface return $this->loadUserByIdentifier($accessToken); } catch (\Exception $e) { - $this->keycloakClientLogger->error('KeycloakUserProvider::refreshUser', [ - 'error' => $e->getMessage(), - 'message' => 'Failed to refresh user access token', - 'user_id' => $user->getUserIdentifier(), - ]); + $this->keycloakClientLogger->error( + 'KeycloakUserProvider::refreshUser', + [ + 'error' => $e->getMessage(), + 'message' => 'Failed to refresh user access token', + 'user_id' => $user->getUserIdentifier(), + ], + ); throw new AuthenticationException('Failed to refresh user session. Please login again.'); } @@ -63,7 +72,7 @@ public function supportsClass(string $class): bool return KeycloakResourceOwner::class === $class; } - public function loadUserByIdentifier($identifier): UserInterface + public function loadUserByIdentifier(mixed $identifier): UserInterface { if (!$identifier instanceof AccessTokenInterface) { throw new \LogicException('Could not load a KeycloakUser without an AccessToken.'); @@ -72,25 +81,34 @@ public function loadUserByIdentifier($identifier): UserInterface try { $resourceOwner = $this->iamClient->fetchUserFromToken($identifier); if (!$resourceOwner) { - $this->keycloakClientLogger->info('KeycloakUserProvider::loadUserByIdentifier', [ - 'message' => 'User not found', - 'token' => $identifier->getToken(), - ]); + $this->keycloakClientLogger->info( + 'KeycloakUserProvider::loadUserByIdentifier', + [ + 'message' => 'User not found', + 'token' => $identifier->getToken(), + ], + ); throw new UserNotFoundException('User not found or invalid token.'); } - $this->keycloakClientLogger->info('KeycloakUserProvider::loadUserByIdentifier', [ - 'resourceOwner' => $resourceOwner->toArray(), - ]); + $this->keycloakClientLogger->info( + 'KeycloakUserProvider::loadUserByIdentifier', + [ + 'resourceOwner' => $resourceOwner->toArray(), + ], + ); return $resourceOwner; } catch (\UnexpectedValueException $e) { - $this->keycloakClientLogger->warning('KeycloakUserProvider::loadUserByIdentifier', [ - 'error' => $e->getMessage(), - 'message' => 'User should have been disconnected from Keycloak server', - 'token' => $identifier->getToken(), - ]); + $this->keycloakClientLogger->warning( + 'KeycloakUserProvider::loadUserByIdentifier', + [ + 'error' => $e->getMessage(), + 'message' => 'User should have been disconnected from Keycloak server', + 'token' => $identifier->getToken(), + ], + ); throw new UserNotFoundException('Failed to load user from token.'); } diff --git a/src/Serializer/AttributeNormalizer.php b/src/Serializer/AttributeNormalizer.php index c748c5e..ccbea7b 100644 --- a/src/Serializer/AttributeNormalizer.php +++ b/src/Serializer/AttributeNormalizer.php @@ -11,7 +11,7 @@ final class AttributeNormalizer implements NormalizerInterface { - /** @var array, array> $filteredProperties */ + /** @var array, array> */ private array $filteredProperties = []; public function __construct( @@ -21,22 +21,34 @@ public function __construct( } /** - * @inheritDoc * @param array $context + * + * @return array|string|int|float|bool|\ArrayObject|null */ - public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null - { + public function normalize( + mixed $data, + ?string $format = null, + array $context = [], + ): array|string|int|float|bool|\ArrayObject|null { $properties = $this->normalizer->normalize($data, $format, $context); if (!$this->keycloakVersion) { return $properties; } - foreach ($this->getFilteredProperties($data) as $property => $versions) { - if (array_key_exists('since', $versions) && version_compare($this->keycloakVersion, $versions['since']) < 0) { + foreach ( + $this->getFilteredProperties($data) as $property => $versions + ) { + if ( + array_key_exists('since', $versions) + && version_compare($this->keycloakVersion, $versions['since']) < 0 + ) { unset($properties[$property]); } - if (array_key_exists('until', $versions) && version_compare($this->keycloakVersion, $versions['until']) > 0) { + if ( + array_key_exists('until', $versions) + && version_compare($this->keycloakVersion, $versions['until']) > 0 + ) { unset($properties[$property]); } } @@ -45,17 +57,16 @@ public function normalize(mixed $data, ?string $format = null, array $context = } /** - * @inheritDoc * @param array $context */ - public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool - { + public function supportsNormalization( + mixed $data, + ?string $format = null, + array $context = [], + ): bool { return $data instanceof Representation; } - /** - * @inheritDoc - */ public function getSupportedTypes(?string $format): array { return [ @@ -63,23 +74,33 @@ public function getSupportedTypes(?string $format): array ]; } - private function getFilteredProperties(Representation $representation): array - { - if (array_key_exists($representation::class, $this->filteredProperties)) { + /** + * @return array + */ + private function getFilteredProperties( + Representation $representation, + ): array { + if ( + array_key_exists($representation::class, $this->filteredProperties) + ) { return $this->filteredProperties[$representation::class]; } $filteredProperties = []; - $properties = (new \ReflectionClass($representation))->getProperties(); + $properties = new \ReflectionClass($representation)->getProperties(); foreach ($properties as $property) { $sinceAttribute = $property->getAttributes(Since::class); foreach ($sinceAttribute as $since) { - $filteredProperties[$property->getName()]['since'] = $since->getArguments()['version']; + $filteredProperties[$property->getName()][ + 'since' + ] = $since->getArguments()['version']; } $untilAttribute = $property->getAttributes(Until::class); foreach ($untilAttribute as $until) { - $filteredProperties[$property->getName()]['until'] = $until->getArguments()['version']; + $filteredProperties[$property->getName()][ + 'until' + ] = $until->getArguments()['version']; } } diff --git a/src/Serializer/CollectionDenormalizer.php b/src/Serializer/CollectionDenormalizer.php index 30204e4..1d24b5b 100644 --- a/src/Serializer/CollectionDenormalizer.php +++ b/src/Serializer/CollectionDenormalizer.php @@ -5,35 +5,53 @@ namespace Mainick\KeycloakClientBundle\Serializer; use Mainick\KeycloakClientBundle\Representation\Collection\Collection; +use Mainick\KeycloakClientBundle\Representation\Representation; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; final readonly class CollectionDenormalizer implements DenormalizerInterface { - public function __construct( - private DenormalizerInterface $denormalizer, - ) { + public function __construct(private DenormalizerInterface $denormalizer) + { } - public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed - { - /** @var Collection $collection */ + /** + * @return Collection + */ + public function denormalize( + mixed $data, + string $type, + ?string $format = null, + array $context = [], + ): mixed { + /** @var Collection $collection */ $collection = new $type(); foreach ($data as $representation) { - $collection->add($this->denormalizer->denormalize($representation, $collection::getRepresentationClass(), $format, $context)); + $collection->add( + $this->denormalizer->denormalize( + $representation, + $collection::getRepresentationClass(), + $format, + $context, + ), + ); } return $collection; } - public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool - { + public function supportsDenormalization( + mixed $data, + string $type, + ?string $format = null, + array $context = [], + ): bool { return is_subclass_of($type, Collection::class); } public function getSupportedTypes(?string $format): array { return [ - Collection::class => true + Collection::class => true, ]; } } diff --git a/src/Serializer/MapDenormalizer.php b/src/Serializer/MapDenormalizer.php index 2c592e0..a0a0ac4 100644 --- a/src/Serializer/MapDenormalizer.php +++ b/src/Serializer/MapDenormalizer.php @@ -9,9 +9,7 @@ final class MapDenormalizer implements DenormalizerInterface { - /** - * @inheritDoc * @param array $context */ public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed @@ -28,21 +26,17 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } /** - * @inheritDoc * @param array $context */ public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { - return $type === Map::class; + return Map::class === $type; } - /** - * @inheritDoc - */ public function getSupportedTypes(?string $format): array { return [ - Map::class => true + Map::class => true, ]; } } diff --git a/src/Serializer/MapNormalizer.php b/src/Serializer/MapNormalizer.php index 570755a..7000383 100644 --- a/src/Serializer/MapNormalizer.php +++ b/src/Serializer/MapNormalizer.php @@ -9,13 +9,16 @@ final class MapNormalizer implements NormalizerInterface { - /** - * @inheritDoc * @param array $context + * + * @return \ArrayObject */ - public function normalize(mixed $data, ?string $format = null, array $context = []): \ArrayObject - { + public function normalize( + mixed $data, + ?string $format = null, + array $context = [], + ): \ArrayObject { if (!$data instanceof Map) { throw new \InvalidArgumentException('Data must be an instance of Map.'); } @@ -24,21 +27,20 @@ public function normalize(mixed $data, ?string $format = null, array $context = } /** - * @inheritDoc * @param array $context */ - public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool - { + public function supportsNormalization( + mixed $data, + ?string $format = null, + array $context = [], + ): bool { return $data instanceof Map; } - /** - * @inheritDoc - */ public function getSupportedTypes(?string $format): array { return [ - Map::class => true + Map::class => true, ]; } } diff --git a/src/Serializer/RepresentationDenormalizer.php b/src/Serializer/RepresentationDenormalizer.php index 125b878..a31a90b 100644 --- a/src/Serializer/RepresentationDenormalizer.php +++ b/src/Serializer/RepresentationDenormalizer.php @@ -7,30 +7,26 @@ use Mainick\KeycloakClientBundle\Representation\Collection\Collection; use Mainick\KeycloakClientBundle\Representation\Representation; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use Symfony\Component\Serializer\SerializerInterface; final readonly class RepresentationDenormalizer implements DenormalizerInterface { - public function __construct( - private DenormalizerInterface $denormalizer, - ) { + public function __construct(private DenormalizerInterface $denormalizer) + { } - /** - * @inheritDoc - */ - public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed - { + public function denormalize( + mixed $data, + string $type, + ?string $format = null, + array $context = [], + ): mixed { if (!is_array($data)) { throw new \InvalidArgumentException('Data expected to be an array for representation denormalization'); } $representation = new $type(); if (!$representation instanceof Representation) { - throw new \InvalidArgumentException(sprintf( - 'Type %s is not a valid Representation class.', - $type - )); + throw new \InvalidArgumentException(sprintf('Type %s is not a valid Representation class.', $type)); } $reflectionClass = new \ReflectionClass($type); @@ -43,26 +39,49 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $paramValue = $data[$paramName]; $paramType = $param->getType(); - if (null !== $paramType && !$paramType->isBuiltin() && is_array($paramValue)) { + if ( + null !== $paramType + && $paramType instanceof \ReflectionNamedType + && !$paramType->isBuiltin() + && is_array($paramValue) + ) { $paramTypeName = $paramType->getName(); // This is the recursive part if ( - class_exists($paramTypeName) && - (is_subclass_of($paramTypeName, Collection::class) || is_subclass_of($paramTypeName, Representation::class)) + class_exists($paramTypeName) + && (is_subclass_of( + $paramTypeName, + Collection::class, + ) + || is_subclass_of( + $paramTypeName, + Representation::class, + )) ) { - $paramValue = $this->denormalizer->denormalize($paramValue, $paramTypeName, $format, $context); + $paramValue = $this->denormalizer->denormalize( + $paramValue, + $paramTypeName, + $format, + $context, + ); } } $constructorParams[$paramName] = $paramValue; } else { - $constructorParams[$paramName] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + $constructorParams[ + $paramName + ] = $param->isDefaultValueAvailable() + ? $param->getDefaultValue() + : null; } } - $representation = $reflectionClass->newInstanceArgs($constructorParams); + $representation = $reflectionClass->newInstanceArgs( + $constructorParams, + ); } else { $representation = $type::from($data); @@ -71,21 +90,21 @@ class_exists($paramTypeName) && return $representation; } - /** - * @inheritDoc - */ - public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool - { - return is_array($data) && class_exists($type) && is_subclass_of($type, Representation::class); + public function supportsDenormalization( + mixed $data, + string $type, + ?string $format = null, + array $context = [], + ): bool { + return is_array($data) + && class_exists($type) + && is_subclass_of($type, Representation::class); } - /** - * @inheritDoc - */ public function getSupportedTypes(?string $format): array { return [ - Representation::class => true + Representation::class => true, ]; } } diff --git a/src/Serializer/Serializer.php b/src/Serializer/Serializer.php index 0f155ed..d8b6b86 100644 --- a/src/Serializer/Serializer.php +++ b/src/Serializer/Serializer.php @@ -26,7 +26,7 @@ public function __construct( classMetadataFactory: $classMetadataFactory, nameConverter: $metadataAwareNameConverter, defaultContext: [ - PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC + PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC, ] ); diff --git a/src/Service/ClientsService.php b/src/Service/ClientsService.php index 09b18ed..5baeb1f 100644 --- a/src/Service/ClientsService.php +++ b/src/Service/ClientsService.php @@ -11,46 +11,91 @@ use Mainick\KeycloakClientBundle\Representation\Collection\UserCollection; use Mainick\KeycloakClientBundle\Representation\Collection\UserSessionCollection; use Mainick\KeycloakClientBundle\Representation\CredentialRepresentation; +use Mainick\KeycloakClientBundle\Representation\GroupRepresentation; use Mainick\KeycloakClientBundle\Representation\RoleRepresentation; +use Mainick\KeycloakClientBundle\Representation\UserRepresentation; final class ClientsService extends Service { /** * @return ClientCollection|null */ - public function all(string $realm, ?Criteria $criteria = null): ?ClientCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/clients', ClientCollection::class, $criteria); + public function all( + string $realm, + ?Criteria $criteria = null, + ): ?ClientCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/clients', + ClientCollection::class, + $criteria, + ); } - public function get(string $realm, string $clientUuid): ?ClientRepresentation - { - return $this->executeQuery('admin/realms/'.$realm.'/clients/'.$clientUuid, ClientRepresentation::class); + public function get( + string $realm, + string $clientUuid, + ): ?ClientRepresentation { + return $this->executeQuery( + 'admin/realms/'.$realm.'/clients/'.$clientUuid, + ClientRepresentation::class, + ); } public function create(string $realm, ClientRepresentation $client): bool { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/clients', $client); + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'.$realm.'/clients', + $client, + ); } - public function update(string $realm, string $clientUuid, ClientRepresentation $client): bool - { - return $this->executeCommand(HttpMethodEnum::PUT, 'admin/realms/'.$realm.'/clients/'.$clientUuid, $client); + public function update( + string $realm, + string $clientUuid, + ClientRepresentation $client, + ): bool { + return $this->executeCommand( + HttpMethodEnum::PUT, + 'admin/realms/'.$realm.'/clients/'.$clientUuid, + $client, + ); } public function delete(string $realm, string $clientUuid): bool { - return $this->executeCommand(HttpMethodEnum::DELETE, 'admin/realms/'.$realm.'/clients/'.$clientUuid); + return $this->executeCommand( + HttpMethodEnum::DELETE, + 'admin/realms/'.$realm.'/clients/'.$clientUuid, + ); } - public function getClientSecret(string $realm, string $clientUuid): ?CredentialRepresentation - { - return $this->executeQuery('admin/realms/'.$realm.'/clients/'.$clientUuid.'/client-secret', CredentialRepresentation::class); + public function getClientSecret( + string $realm, + string $clientUuid, + ): ?CredentialRepresentation { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/client-secret', + CredentialRepresentation::class, + ); } - public function getUserSessions(string $realm, string $clientUuid): ?UserSessionCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/clients/'.$clientUuid.'/user-sessions', UserSessionCollection::class); + public function getUserSessions( + string $realm, + string $clientUuid, + ): ?UserSessionCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/user-sessions', + UserSessionCollection::class, + ); } /** @@ -58,33 +103,71 @@ public function getUserSessions(string $realm, string $clientUuid): ?UserSession */ public function roles(string $realm, string $clientUuid): ?RoleCollection { - return $this->executeQuery('admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles', RoleCollection::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles', + RoleCollection::class, + ); } - public function role(string $realm, string $clientUuid, string $roleName): ?RoleRepresentation - { - return $this->executeQuery('admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName, RoleRepresentation::class); + public function role( + string $realm, + string $clientUuid, + string $roleName, + ): ?RoleRepresentation { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/roles/'. + $roleName, + RoleRepresentation::class, + ); } - public function createRole(string $realm, string $clientUuid, RoleRepresentation $role): bool - { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles', $role); + public function createRole( + string $realm, + string $clientUuid, + RoleRepresentation $role, + ): bool { + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles', + $role, + ); } - public function updateRole(string $realm, string $clientUuid, string $roleName, RoleRepresentation $role): bool - { + public function updateRole( + string $realm, + string $clientUuid, + string $roleName, + RoleRepresentation $role, + ): bool { return $this->executeCommand( HttpMethodEnum::PUT, - 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName, - $role + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/roles/'. + $roleName, + $role, ); } - public function deleteRole(string $realm, string $clientUuid, string $roleName): bool - { + public function deleteRole( + string $realm, + string $clientUuid, + string $roleName, + ): bool { return $this->executeCommand( HttpMethodEnum::DELETE, - 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/roles/'. + $roleName, ); } @@ -95,13 +178,18 @@ public function getRoleGroups( string $realm, string $clientUuid, string $roleName, - ?Criteria $criteria = null - ): ?GroupCollection - { + ?Criteria $criteria = null, + ): ?GroupCollection { return $this->executeQuery( - 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName.'/groups', + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/roles/'. + $roleName. + '/groups', GroupCollection::class, - $criteria + $criteria, ); } @@ -112,14 +200,18 @@ public function getRoleUsers( string $realm, string $clientUuid, string $roleName, - ?Criteria $criteria = null - ): ?UserCollection - { + ?Criteria $criteria = null, + ): ?UserCollection { return $this->executeQuery( - 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName.'/users', + 'admin/realms/'. + $realm. + '/clients/'. + $clientUuid. + '/roles/'. + $roleName. + '/users', UserCollection::class, - $criteria + $criteria, ); } - } diff --git a/src/Service/Criteria.php b/src/Service/Criteria.php index 912333e..2fd65b3 100644 --- a/src/Service/Criteria.php +++ b/src/Service/Criteria.php @@ -9,33 +9,32 @@ /** * @param array $criteria */ - public function __construct( - private array $criteria = [] - ) { + public function __construct(private array $criteria = []) + { } + /** + * @return array + */ public function jsonSerialize(): array { return array_filter( - array_map( - static function ($value) { - if (is_bool($value)) { - return $value ? 'true' : 'false'; - } + array_map(static function ($value) { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } - if ($value instanceof \DateTimeInterface) { - return $value->format('Y-m-d'); - } + if ($value instanceof \DateTimeInterface) { + return $value->format('Y-m-d'); + } - if ($value instanceof \Stringable) { - return $value->__toString(); - } + if ($value instanceof \Stringable) { + return $value->__toString(); + } - return $value; - }, - $this->criteria - ), - static fn($value) => null !== $value + return $value; + }, $this->criteria), + static fn ($value) => null !== $value, ); } } diff --git a/src/Service/GroupsService.php b/src/Service/GroupsService.php index 9329d29..662dafd 100644 --- a/src/Service/GroupsService.php +++ b/src/Service/GroupsService.php @@ -9,20 +9,31 @@ use Mainick\KeycloakClientBundle\Representation\Collection\UserCollection; use Mainick\KeycloakClientBundle\Representation\GroupRepresentation; use Mainick\KeycloakClientBundle\Representation\RoleRepresentation; +use Mainick\KeycloakClientBundle\Representation\UserRepresentation; final class GroupsService extends Service { /** * @return GroupCollection|null */ - public function all(string $realm, ?Criteria $criteria = null): ?GroupCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/groups', GroupCollection::class, $criteria); + public function all( + string $realm, + ?Criteria $criteria = null, + ): ?GroupCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/groups', + GroupCollection::class, + $criteria, + ); } public function count(string $realm, ?Criteria $criteria = null): int { - $count = $this->executeQuery('admin/realms/'.$realm.'/groups/count', 'array', $criteria); + $count = $this->executeQuery( + 'admin/realms/'.$realm.'/groups/count', + 'array', + $criteria, + ); if (null === $count) { return 0; } @@ -33,34 +44,69 @@ public function count(string $realm, ?Criteria $criteria = null): int /** * @return GroupCollection|null */ - public function children(string $realm, string $groupId, ?Criteria $criteria = null): ?GroupCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId.'/children', GroupCollection::class, $criteria); + public function children( + string $realm, + string $groupId, + ?Criteria $criteria = null, + ): ?GroupCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/groups/'.$groupId.'/children', + GroupCollection::class, + $criteria, + ); } public function get(string $realm, string $groupId): ?GroupRepresentation { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId, GroupRepresentation::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/groups/'.$groupId, + GroupRepresentation::class, + ); } public function create(string $realm, GroupRepresentation $group): bool { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/groups', $group); + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'.$realm.'/groups', + $group, + ); } - public function createChild(string $realm, string $parentGroupId, GroupRepresentation $group): bool - { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/groups/'.$parentGroupId.'/children', $group); + public function createChild( + string $realm, + string $parentGroupId, + GroupRepresentation $group, + ): bool { + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'. + $realm. + '/groups/'. + $parentGroupId. + '/children', + $group, + ); } - public function update(string $realm, string $groupId, GroupRepresentation $group): bool - { - return $this->executeCommand(HttpMethodEnum::PUT, 'admin/realms/'.$realm.'/groups/'.$groupId, $group); + public function update( + string $realm, + string $groupId, + GroupRepresentation $group, + ): bool { + return $this->executeCommand( + HttpMethodEnum::PUT, + 'admin/realms/'.$realm.'/groups/'.$groupId, + $group, + ); } public function delete(string $realm, string $groupId): bool { - return $this->executeCommand(HttpMethodEnum::DELETE, 'admin/realms/'.$realm.'/groups/'.$groupId); + return $this->executeCommand( + HttpMethodEnum::DELETE, + 'admin/realms/'.$realm.'/groups/'.$groupId, + ); } /** @@ -68,7 +114,10 @@ public function delete(string $realm, string $groupId): bool */ public function users(string $realm, string $groupId): ?UserCollection { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId.'/members', UserCollection::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/groups/'.$groupId.'/members', + UserCollection::class, + ); } /** @@ -76,74 +125,149 @@ public function users(string $realm, string $groupId): ?UserCollection */ public function realmRoles(string $realm, string $groupId): ?RoleCollection { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm', RoleCollection::class); + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm', + RoleCollection::class, + ); } /** * @return RoleCollection|null */ - public function availableRealmRoles(string $realm, string $groupId): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm/available', RoleCollection::class); + public function availableRealmRoles( + string $realm, + string $groupId, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm/available', + RoleCollection::class, + ); } - public function addRealmRole(string $realm, string $groupId, RoleRepresentation $role): bool - { + public function addRealmRole( + string $realm, + string $groupId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::POST, - 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm', - $roles + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm', + $roles, ); } - public function removeRealmRole(string $realm, string $groupId, RoleRepresentation $role): bool - { + public function removeRealmRole( + string $realm, + string $groupId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::DELETE, - 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm', - $roles + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm', + $roles, ); } /** * @return RoleCollection|null */ - public function clientRoles(string $realm, string $clientUuid, string $groupId): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid, RoleCollection::class); + public function clientRoles( + string $realm, + string $clientUuid, + string $groupId, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid, + RoleCollection::class, + ); } /** * @return RoleCollection|null */ - public function availableClientRoles(string $realm, string $clientUuid, string $groupId): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid.'/available', RoleCollection::class); + public function availableClientRoles( + string $realm, + string $clientUuid, + string $groupId, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid. + '/available', + RoleCollection::class, + ); } - public function addClientRole(string $realm, string $clientUuid, string $groupId, RoleRepresentation $role): bool - { + public function addClientRole( + string $realm, + string $clientUuid, + string $groupId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::POST, - 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid, - $roles + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid, + $roles, ); } - public function removeClientRole(string $realm, string $clientUuid, string $groupId, RoleRepresentation $role): bool - { + public function removeClientRole( + string $realm, + string $clientUuid, + string $groupId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::DELETE, - 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid, - $roles + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid, + $roles, ); } } diff --git a/src/Service/RealmsService.php b/src/Service/RealmsService.php index 1f83f4e..f5d3fd2 100644 --- a/src/Service/RealmsService.php +++ b/src/Service/RealmsService.php @@ -4,14 +4,8 @@ namespace Mainick\KeycloakClientBundle\Service; -use Mainick\KeycloakClientBundle\Representation\Collection\GroupCollection; use Mainick\KeycloakClientBundle\Representation\Collection\RealmCollection; -use Mainick\KeycloakClientBundle\Representation\Collection\RoleCollection; -use Mainick\KeycloakClientBundle\Representation\Collection\UserCollection; -use Mainick\KeycloakClientBundle\Representation\GroupRepresentation; use Mainick\KeycloakClientBundle\Representation\RealmRepresentation; -use Mainick\KeycloakClientBundle\Representation\RoleRepresentation; -use Mainick\KeycloakClientBundle\Representation\UserRepresentation; final class RealmsService extends Service { diff --git a/src/Service/RolesService.php b/src/Service/RolesService.php index e993f89..1b8883c 100644 --- a/src/Service/RolesService.php +++ b/src/Service/RolesService.php @@ -7,52 +7,90 @@ use Mainick\KeycloakClientBundle\Representation\Collection\GroupCollection; use Mainick\KeycloakClientBundle\Representation\Collection\RoleCollection; use Mainick\KeycloakClientBundle\Representation\Collection\UserCollection; +use Mainick\KeycloakClientBundle\Representation\GroupRepresentation; use Mainick\KeycloakClientBundle\Representation\RoleRepresentation; +use Mainick\KeycloakClientBundle\Representation\UserRepresentation; final class RolesService extends Service { /** * @return RoleCollection|null */ - public function all(string $realm, ?Criteria $criteria = null): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/roles', RoleCollection::class, $criteria); + public function all( + string $realm, + ?Criteria $criteria = null, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/roles', + RoleCollection::class, + $criteria, + ); } public function get(string $realm, string $roleName): ?RoleRepresentation { - return $this->executeQuery('admin/realms/'.$realm.'/roles/'.$roleName, RoleRepresentation::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/roles/'.$roleName, + RoleRepresentation::class, + ); } public function create(string $realm, RoleRepresentation $role): bool { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/roles', $role); + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'.$realm.'/roles', + $role, + ); } - public function update(string $realm, string $roleName, RoleRepresentation $role): bool - { - return $this->executeCommand(HttpMethodEnum::PUT, 'admin/realms/'.$realm.'/roles/'.$roleName, $role); + public function update( + string $realm, + string $roleName, + RoleRepresentation $role, + ): bool { + return $this->executeCommand( + HttpMethodEnum::PUT, + 'admin/realms/'.$realm.'/roles/'.$roleName, + $role, + ); } public function delete(string $realm, string $roleName): bool { - return $this->executeCommand(HttpMethodEnum::DELETE, 'admin/realms/'.$realm.'/roles/'.$roleName); + return $this->executeCommand( + HttpMethodEnum::DELETE, + 'admin/realms/'.$realm.'/roles/'.$roleName, + ); } /** * @return GroupCollection|null */ - public function groups(string $realm, string $roleName, ?Criteria $criteria = null): ?GroupCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/roles/'.$roleName.'/groups', GroupCollection::class, $criteria); + public function groups( + string $realm, + string $roleName, + ?Criteria $criteria = null, + ): ?GroupCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/roles/'.$roleName.'/groups', + GroupCollection::class, + $criteria, + ); } /** * @return UserCollection|null */ - public function users(string $realm, string $roleName, ?Criteria $criteria = null): ?UserCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/roles/'.$roleName.'/users', UserCollection::class, $criteria); + public function users( + string $realm, + string $roleName, + ?Criteria $criteria = null, + ): ?UserCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/roles/'.$roleName.'/users', + UserCollection::class, + $criteria, + ); } - } diff --git a/src/Service/Service.php b/src/Service/Service.php index 6dbc99e..26247d0 100644 --- a/src/Service/Service.php +++ b/src/Service/Service.php @@ -8,6 +8,7 @@ use Mainick\KeycloakClientBundle\Exception\KeycloakAuthenticationException; use Mainick\KeycloakClientBundle\Provider\KeycloakAdminClient; use Mainick\KeycloakClientBundle\Representation\Collection\Collection; +use Mainick\KeycloakClientBundle\Representation\Collection\RoleCollection; use Mainick\KeycloakClientBundle\Representation\Representation; use Mainick\KeycloakClientBundle\Serializer\Serializer; use Mainick\KeycloakClientBundle\Token\AccessToken; @@ -25,21 +26,28 @@ public function __construct( protected readonly LoggerInterface $logger, protected readonly KeycloakAdminClient $keycloakAdminClient, ) { - $this->httpClient = $this->keycloakAdminClient->getKeycloakProvider()->getHttpClient(); + $this->httpClient = $this->keycloakAdminClient + ->getKeycloakProvider() + ->getHttpClient(); - $this->serializer = new Serializer($this->keycloakAdminClient->getVersion()); + $this->serializer = new Serializer( + $this->keycloakAdminClient->getVersion(), + ); } - protected function executeQuery(string $path, string $returnType, ?Criteria $criteria = null): mixed - { + protected function executeQuery( + string $path, + string $returnType, + ?Criteria $criteria = null, + ): mixed { if (!$this->isAuthorized()) { $this->inizializeAdminAccessToken(); } $response = $this->httpClient->request( HttpMethodEnum::GET->value, - $path . $this->getQueryParams($criteria), - $this->defaultOptions() + $path.$this->getQueryParams($criteria), + $this->defaultOptions(), ); if ($this->isSuccessful($response->getStatusCode())) { @@ -51,12 +59,14 @@ protected function executeQuery(string $path, string $returnType, ?Criteria $cri 'response' => $content, ]); - if ($content === '' || trim($content) === '') { + if ('' === $content || '' === trim($content)) { throw new \UnexpectedValueException('Empty response'); } - if ($returnType === 'array') { - return (new JsonDecode([JsonDecode::ASSOCIATIVE => true]))->decode($content, JsonEncoder::FORMAT); + if ('array' === $returnType) { + return new JsonDecode([ + JsonDecode::ASSOCIATIVE => true, + ])->decode($content, JsonEncoder::FORMAT); } return $this->serializer->deserialize($content, $returnType); @@ -65,34 +75,38 @@ protected function executeQuery(string $path, string $returnType, ?Criteria $cri return null; } + /** + * @param Representation|Collection|RoleCollection|array|null $payload + */ protected function executeCommand( HttpMethodEnum $method, string $path, - Representation|Collection|array|null $payload = null - ): bool - { + Representation|Collection|RoleCollection|array|null $payload = null, + ): bool { if (!$this->isAuthorized()) { $this->inizializeAdminAccessToken(); } $options = $this->defaultOptions(); if (null !== $payload) { - $options['json'] = $payload instanceof \JsonSerializable ? $payload->jsonSerialize() : $payload; + $options['json'] = + $payload instanceof \JsonSerializable + ? $payload->jsonSerialize() + : $payload; } - $response = $this->httpClient->request( - $method->value, - $path, - $options - ); + $response = $this->httpClient->request($method->value, $path, $options); if ($this->isSuccessful($response->getStatusCode())) { $content = $response->getBody()->getContents(); - $this->logger->info('KeycloakAdminClient::Service::executeCommand', [ - 'status_code' => $response->getStatusCode(), - 'response' => $content, - ]); + $this->logger->info( + 'KeycloakAdminClient::Service::executeCommand', + [ + 'status_code' => $response->getStatusCode(), + 'response' => $content, + ], + ); return true; } @@ -100,6 +114,9 @@ protected function executeCommand( return false; } + /** + * @return array + */ private function defaultOptions(): array { return [ @@ -107,7 +124,10 @@ private function defaultOptions(): array 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer '.$this->keycloakAdminClient->getAdminAccessToken()?->getToken(), + 'Authorization' => 'Bearer '. + $this->keycloakAdminClient + ->getAdminAccessToken() + ?->getToken(), ], ]; } @@ -118,17 +138,21 @@ private function getQueryParams(?Criteria $criteria): string return ''; } - return '?' . http_build_query($criteria->jsonSerialize()); + return '?'.http_build_query($criteria->jsonSerialize()); } - private function isSuccessful($statusCode): bool + private function isSuccessful(int $statusCode): bool { - return ($statusCode >= Response::HTTP_OK && $statusCode < Response::HTTP_MULTIPLE_CHOICES) || $statusCode === Response::HTTP_NOT_MODIFIED; + return ($statusCode >= Response::HTTP_OK + && $statusCode < Response::HTTP_MULTIPLE_CHOICES) + || Response::HTTP_NOT_MODIFIED === $statusCode; } private function isAuthorized(): bool { - return null !== $this->keycloakAdminClient->getAdminAccessToken() && false === $this->keycloakAdminClient->getAdminAccessToken()->hasExpired(); + return null !== $this->keycloakAdminClient->getAdminAccessToken() + && false === + $this->keycloakAdminClient->getAdminAccessToken()->hasExpired(); } private function inizializeAdminAccessToken(): void @@ -138,21 +162,33 @@ private function inizializeAdminAccessToken(): void throw new KeycloakAuthenticationException('No refresh token available'); } - $token = $this->keycloakAdminClient->getKeycloakProvider()->getAccessToken('refresh_token', [ - 'refresh_token' => $this->keycloakAdminClient->getAdminAccessToken()->getRefreshToken(), - ]); + $token = $this->keycloakAdminClient + ->getKeycloakProvider() + ->getAccessToken('refresh_token', [ + 'refresh_token' => $this->keycloakAdminClient + ->getAdminAccessToken() + ->getRefreshToken(), + ]); } catch (\Exception $e) { try { - $token = $this->keycloakAdminClient->getKeycloakProvider()->getAccessToken('password', [ - 'username' => $this->keycloakAdminClient->getUsername(), - 'password' => $this->keycloakAdminClient->getPassword(), - ]); + $token = $this->keycloakAdminClient + ->getKeycloakProvider() + ->getAccessToken('password', [ + 'username' => $this->keycloakAdminClient->getUsername(), + 'password' => $this->keycloakAdminClient->getPassword(), + ]); } catch (\Exception $e) { - $this->logger->error('KeycloakAdminClient::getAdminAccessToken', [ - 'error' => 'Authentication failed to Keycloak Admin API - '.$e->getMessage().' - '.$e->getTraceAsString(), - ]); + $this->logger->error( + 'KeycloakAdminClient::getAdminAccessToken', + [ + 'error' => 'Authentication failed to Keycloak Admin API - '. + $e->getMessage(). + ' - '. + $e->getTraceAsString(), + ], + ); throw new KeycloakAuthenticationException('Authentication failed to Keycloak Admin API'); } diff --git a/src/Service/UsersService.php b/src/Service/UsersService.php index eb28c8b..234dcd8 100644 --- a/src/Service/UsersService.php +++ b/src/Service/UsersService.php @@ -8,6 +8,7 @@ use Mainick\KeycloakClientBundle\Representation\Collection\RoleCollection; use Mainick\KeycloakClientBundle\Representation\Collection\UserCollection; use Mainick\KeycloakClientBundle\Representation\Collection\UserSessionCollection; +use Mainick\KeycloakClientBundle\Representation\GroupRepresentation; use Mainick\KeycloakClientBundle\Representation\RoleRepresentation; use Mainick\KeycloakClientBundle\Representation\UPConfig; use Mainick\KeycloakClientBundle\Representation\UserProfileMetadata; @@ -19,19 +20,32 @@ final class UsersService extends Service /** * @return UserCollection|null */ - public function all(string $realm, ?Criteria $criteria = null): ?UserCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/users', UserCollection::class, $criteria); + public function all( + string $realm, + ?Criteria $criteria = null, + ): ?UserCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/users', + UserCollection::class, + $criteria, + ); } public function get(string $realm, string $userId): ?UserRepresentation { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId, UserRepresentation::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/users/'.$userId, + UserRepresentation::class, + ); } public function count(string $realm, ?Criteria $criteria = null): int { - $count = $this->executeQuery('admin/realms/'.$realm.'/users/count', 'array', $criteria); + $count = $this->executeQuery( + 'admin/realms/'.$realm.'/users/count', + 'array', + $criteria, + ); if (null === $count) { return 0; } @@ -41,38 +55,71 @@ public function count(string $realm, ?Criteria $criteria = null): int public function create(string $realm, UserRepresentation $user): bool { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/users', $user); + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'.$realm.'/users', + $user, + ); } - public function update(string $realm, string $userId, UserRepresentation $user): bool - { - return $this->executeCommand(HttpMethodEnum::PUT, 'admin/realms/'.$realm.'/users/'.$userId, $user); + public function update( + string $realm, + string $userId, + UserRepresentation $user, + ): bool { + return $this->executeCommand( + HttpMethodEnum::PUT, + 'admin/realms/'.$realm.'/users/'.$userId, + $user, + ); } public function delete(string $realm, string $userId): bool { - return $this->executeCommand(HttpMethodEnum::DELETE, 'admin/realms/'.$realm.'/users/'.$userId); + return $this->executeCommand( + HttpMethodEnum::DELETE, + 'admin/realms/'.$realm.'/users/'.$userId, + ); } public function logout(string $realm, string $userId): bool { - return $this->executeCommand(HttpMethodEnum::POST, 'admin/realms/'.$realm.'/users/'.$userId.'/logout'); + return $this->executeCommand( + HttpMethodEnum::POST, + 'admin/realms/'.$realm.'/users/'.$userId.'/logout', + ); } /** * @return UserSessionCollection|null */ - public function sessions(string $realm, string $userId): ?UserSessionCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/sessions', UserSessionCollection::class); + public function sessions( + string $realm, + string $userId, + ): ?UserSessionCollection { + return $this->executeQuery( + 'admin/realms/'.$realm.'/users/'.$userId.'/sessions', + UserSessionCollection::class, + ); } /** * @return UserSessionCollection|null */ - public function offlineSessions(string $realm, string $userId, string $clientId): ?UserSessionCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/offline-sessions/'.$clientId, UserSessionCollection::class); + public function offlineSessions( + string $realm, + string $userId, + string $clientId, + ): ?UserSessionCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/offline-sessions/'. + $clientId, + UserSessionCollection::class, + ); } /** @@ -80,12 +127,18 @@ public function offlineSessions(string $realm, string $userId, string $clientId) */ public function groups(string $realm, string $userId): ?GroupCollection { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/groups', GroupCollection::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/users/'.$userId.'/groups', + GroupCollection::class, + ); } public function groupsCount(string $realm, string $userId): int { - $count = $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/groups/count', 'array'); + $count = $this->executeQuery( + 'admin/realms/'.$realm.'/users/'.$userId.'/groups/count', + 'array', + ); if (null === $count) { return 0; } @@ -93,14 +146,36 @@ public function groupsCount(string $realm, string $userId): int return (int) $count; } - public function joinGroup(string $realm, string $userId, string $groupId): bool - { - return $this->executeCommand(HttpMethodEnum::PUT, 'admin/realms/'.$realm.'/users/'.$userId.'/groups/'.$groupId); + public function joinGroup( + string $realm, + string $userId, + string $groupId, + ): bool { + return $this->executeCommand( + HttpMethodEnum::PUT, + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/groups/'. + $groupId, + ); } - public function leaveGroup(string $realm, string $userId, string $groupId): bool - { - return $this->executeCommand(HttpMethodEnum::DELETE, 'admin/realms/'.$realm.'/users/'.$userId.'/groups/'.$groupId); + public function leaveGroup( + string $realm, + string $userId, + string $groupId, + ): bool { + return $this->executeCommand( + HttpMethodEnum::DELETE, + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/groups/'. + $groupId, + ); } /** @@ -108,98 +183,192 @@ public function leaveGroup(string $realm, string $userId, string $groupId): bool */ public function realmRoles(string $realm, string $userId): ?RoleCollection { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm', RoleCollection::class); + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm', + RoleCollection::class, + ); } /** * @return RoleCollection|null */ - public function availableRealmRoles(string $realm, string $userId): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm/available', RoleCollection::class); + public function availableRealmRoles( + string $realm, + string $userId, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm/available', + RoleCollection::class, + ); } - public function addRealmRole(string $realm, string $userId, RoleRepresentation $role): bool - { + public function addRealmRole( + string $realm, + string $userId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::POST, - 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm', - $roles + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm', + $roles, ); } - public function removeRealmRole(string $realm, string $userId, RoleRepresentation $role): bool - { + public function removeRealmRole( + string $realm, + string $userId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::DELETE, - 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm', - $roles + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm', + $roles, ); } /** * @return RoleCollection|null */ - public function clientRoles(string $realm, string $clientUuid, string $userId): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid, RoleCollection::class); + public function clientRoles( + string $realm, + string $clientUuid, + string $userId, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid, + RoleCollection::class, + ); } /** * @return RoleCollection|null */ - public function availableClientRoles(string $realm, string $clientUuid, string $userId): ?RoleCollection - { - return $this->executeQuery('admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid.'/available', RoleCollection::class); + public function availableClientRoles( + string $realm, + string $clientUuid, + string $userId, + ): ?RoleCollection { + return $this->executeQuery( + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid. + '/available', + RoleCollection::class, + ); } - public function addClientRole(string $realm, string $clientUuid, string $userId, RoleRepresentation $role): bool - { + public function addClientRole( + string $realm, + string $clientUuid, + string $userId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::POST, - 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid, - $roles + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid, + $roles, ); } - public function removeClientRole(string $realm, string $clientUuid, string $userId, RoleRepresentation $role): bool - { + public function removeClientRole( + string $realm, + string $clientUuid, + string $userId, + RoleRepresentation $role, + ): bool { $roles = new RoleCollection(); $roles->add($role); + return $this->executeCommand( HttpMethodEnum::DELETE, - 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid, - $roles + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid, + $roles, ); } public function getProfileConfig(string $realm): ?UPConfig { - return $this->executeQuery('admin/realms/'.$realm.'/users/profile', UPConfig::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/users/profile', + UPConfig::class, + ); } public function getProfileMetadata(string $realm): ?UserProfileMetadata { - return $this->executeQuery('admin/realms/'.$realm.'/users/profile/metadata', UserProfileMetadata::class); + return $this->executeQuery( + 'admin/realms/'.$realm.'/users/profile/metadata', + UserProfileMetadata::class, + ); } public function resetPassword(string $realm, string $userId): bool { - return $this->executeCommand(HttpMethodEnum::PUT, 'admin/realms/'.$realm.'/users/'.$userId.'/reset-password'); + return $this->executeCommand( + HttpMethodEnum::PUT, + 'admin/realms/'.$realm.'/users/'.$userId.'/reset-password', + ); } - public function sendVerifyEmail(string $realm, string $userId, array $parameters): bool - { + /** + * @param array $parameters + */ + public function sendVerifyEmail( + string $realm, + string $userId, + array $parameters, + ): bool { return $this->executeCommand( HttpMethodEnum::PUT, - 'admin/realms/'.$realm.'/users/'.$userId.'/send-verify-email', - $parameters + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/send-verify-email', + $parameters, ); } } diff --git a/src/Token/AccessToken.php b/src/Token/AccessToken.php index 61f3b3d..213e67a 100644 --- a/src/Token/AccessToken.php +++ b/src/Token/AccessToken.php @@ -11,6 +11,7 @@ class AccessToken implements AccessTokenInterface protected string $accessToken; protected int $expires; protected string $refreshToken; + /** @var array */ protected array $values = []; public function __construct() @@ -63,11 +64,21 @@ public function hasExpired(): bool return $expires < time(); } + /** + * Returns the token values as an array. + * + * @return array + */ public function getValues(): array { return $this->values; } + /** + * Sets the token values from an array. + * + * @param array $values + */ public function setValues(array $values): AccessTokenInterface { $this->values = $values; @@ -80,6 +91,11 @@ public function __toString(): string return (string) $this->getToken(); } + /** + * Returns the token values as an array. + * + * @return array + */ public function jsonSerialize(): array { $parameters = $this->values; diff --git a/src/Token/HS256TokenDecoder.php b/src/Token/HS256TokenDecoder.php index f1521d9..e55077d 100644 --- a/src/Token/HS256TokenDecoder.php +++ b/src/Token/HS256TokenDecoder.php @@ -11,7 +11,6 @@ class HS256TokenDecoder implements TokenDecoderInterface { - public function decode(string $token, string $key): array { try { @@ -20,7 +19,8 @@ public function decode(string $token, string $key): array $json = json_encode($tokenDecoded, JSON_THROW_ON_ERROR); return json_decode($json, true, 512, JSON_THROW_ON_ERROR); - } catch (\Exception $e) { + } + catch (\Exception $e) { throw new TokenDecoderException('Error decoding token', $e); } } @@ -33,12 +33,12 @@ public function validateToken(string $realm, array $tokenDecoded): void throw TokenDecoderException::forExpiration(new \Exception('Token has expired')); } - if (str_contains($tokenDecoded['iss'], $realm) === false) { + if (false === str_contains($tokenDecoded['iss'], $realm)) { throw TokenDecoderException::forIssuerMismatch(new \Exception('Invalid token issuer')); } -// -// if ($tokenDecoded['aud'] !== 'account') { -// throw TokenDecoderException::forAudienceMismatch(new \Exception('Invalid token audience')); -// } + // + // if ($tokenDecoded['aud'] !== 'account') { + // throw TokenDecoderException::forAudienceMismatch(new \Exception('Invalid token audience')); + // } } } diff --git a/src/Token/JWKSTokenDecoder.php b/src/Token/JWKSTokenDecoder.php index d6f3b6c..9195676 100644 --- a/src/Token/JWKSTokenDecoder.php +++ b/src/Token/JWKSTokenDecoder.php @@ -1,4 +1,5 @@ $options + */ public function __construct( private ClientInterface $httpClient, - private array $options - ) - { + private array $options, + ) { foreach ($options as $allowOption => $value) { - if (!\in_array($allowOption, ['base_url', 'realm', 'alg', 'http_timeout', 'http_connect_timeout', 'allowed_jwks_domains'], true)) { - throw TokenDecoderException::forInvalidConfiguration(\sprintf( - "Unknown option '%s' for %s", + if ( + !\in_array( $allowOption, - self::class - )); + [ + 'base_url', + 'realm', + 'alg', + 'http_timeout', + 'http_connect_timeout', + 'allowed_jwks_domains', + ], + true, + ) + ) { + throw TokenDecoderException::forInvalidConfiguration(\sprintf("Unknown option '%s' for %s", $allowOption, self::class)); } } foreach (['base_url', 'realm'] as $requiredOption) { - if (!\array_key_exists($requiredOption, $this->options) || $this->options[$requiredOption] === null || $this->options[$requiredOption] === '') { - throw TokenDecoderException::forInvalidConfiguration(\sprintf( - "Missing or empty required option '%s' for %s", - $requiredOption, - self::class - )); + if ( + !\array_key_exists($requiredOption, $this->options) + || null === $this->options[$requiredOption] + || '' === $this->options[$requiredOption] + ) { + throw TokenDecoderException::forInvalidConfiguration(\sprintf("Missing or empty required option '%s' for %s", $requiredOption, self::class)); } } @@ -52,9 +63,9 @@ public function __construct( * from the JWKS endpoint. Callers may pass an empty string or any * placeholder value for $key when using this decoder. * - * @param string $token The encoded JWT to decode. - * @param string $key Unused in this JWKS-based implementation; present - * only to satisfy the TokenDecoderInterface. + * @param string $token the encoded JWT to decode + * @param string $key unused in this JWKS-based implementation; present + * only to satisfy the TokenDecoderInterface * * @throws TokenDecoderException */ @@ -62,15 +73,22 @@ public function decode(string $token, string $key): array { try { $parts = explode('.', $token); - if (\count($parts) !== 3 || $parts[0] === '' || $parts[1] === '' || $parts[2] === '') { - throw TokenDecoderException::forDecodingError( - 'Invalid JWT format: token must consist of header.payload.signature', - new \Exception('invalid token format') - ); + if ( + 3 !== \count($parts) + || '' === $parts[0] + || '' === $parts[1] + || '' === $parts[2] + ) { + throw TokenDecoderException::forDecodingError('Invalid JWT format: token must consist of header.payload.signature', new \Exception('invalid token format')); } [$headerB64] = $parts; - $header = json_decode($this->base64urlDecode($headerB64), true, 512, JSON_THROW_ON_ERROR); + $header = json_decode( + $this->base64urlDecode($headerB64), + true, + 512, + JSON_THROW_ON_ERROR, + ); $kid = $header['kid'] ?? ''; if (empty($kid)) { @@ -80,11 +98,11 @@ public function decode(string $token, string $key): array // Enforce a server-side algorithm instead of trusting the token header. // Default to RS256 (commonly used by Keycloak) if not explicitly configured. $algorithm = $this->options['alg'] ?? 'RS256'; - if (isset($header['alg']) && $algorithm !== (string) $header['alg']) { - throw TokenDecoderException::forDecodingError( - sprintf('Token algorithm "%s" does not match expected algorithm "%s"', $header['alg'], $algorithm), - new \Exception('algorithm mismatch') - ); + if ( + isset($header['alg']) + && $algorithm !== (string) $header['alg'] + ) { + throw TokenDecoderException::forDecodingError(sprintf('Token algorithm "%s" does not match expected algorithm "%s"', $header['alg'], $algorithm), new \Exception('algorithm mismatch')); } $keyObject = $this->getKeyForKid($kid, $algorithm); @@ -98,7 +116,7 @@ public function decode(string $token, string $key): array throw $e; } catch (\JsonException $e) { - throw TokenDecoderException::forDecodingError('JSON parsing failed: ' . $e->getMessage(), $e); + throw TokenDecoderException::forDecodingError('JSON parsing failed: '.$e->getMessage(), $e); } catch (\Exception $e) { throw TokenDecoderException::forDecodingError($e->getMessage(), $e); @@ -129,47 +147,48 @@ private function getKeyForKid(string $kid, string $algorithm): Key } // Filter to only include signing keys - $signingKeys = array_filter($jwksData['keys'], fn($jwk) => ($jwk['use'] ?? 'sig') === 'sig'); + $signingKeys = array_filter( + $jwksData['keys'], + fn ($jwk) => ($jwk['use'] ?? 'sig') === 'sig', + ); if (empty($signingKeys)) { throw TokenDecoderException::forJwksError('No signing keys found in JWKS endpoint', new \Exception('No sig keys in JWKS')); } try { - $keys = JWK::parseKeySet(['keys' => array_values($signingKeys)], $algorithm); - } catch (\Exception $e) { - throw TokenDecoderException::forJwksError( - sprintf('Failed to parse JWKS: %s', $e->getMessage()), - $e + $keys = JWK::parseKeySet( + ['keys' => array_values($signingKeys)], + $algorithm, ); } + catch (\Exception $e) { + throw TokenDecoderException::forJwksError(sprintf('Failed to parse JWKS: %s', $e->getMessage()), $e); + } if (!isset($keys[$kid])) { - throw TokenDecoderException::forJwksError( - sprintf('No matching signing key found for kid: %s', $kid), - new \Exception('Key ID not found in JWKS') - ); + throw TokenDecoderException::forJwksError(sprintf('No matching signing key found for kid: %s', $kid), new \Exception('Key ID not found in JWKS')); } return $keys[$kid]; } + /** + * @return array the JWKS as an array + */ private function fetchJwks(): array { $timeout = $this->options['http_timeout'] ?? 10; $connectTimeout = $this->options['http_connect_timeout'] ?? 5; - $url = sprintf('%s/realms/%s/protocol/openid-connect/certs', $this->options['base_url'], $this->options['realm']); + $url = sprintf( + '%s/realms/%s/protocol/openid-connect/certs', + $this->options['base_url'], + $this->options['realm'], + ); // Validate the constructed JWKS URL $this->validateJwksUrl($url); try { - if ($this->httpClient === null) { - throw TokenDecoderException::forJwksError( - 'HTTP client is not configured; unable to fetch JWKS.', - new \RuntimeException('Missing HTTP client for JWKS retrieval') - ); - } - $response = $this->httpClient->request('GET', $url, [ 'timeout' => $timeout, 'connect_timeout' => $connectTimeout, @@ -179,32 +198,23 @@ private function fetchJwks(): array $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); return $data; - } catch (GuzzleException $e) { - throw TokenDecoderException::forJwksError( - sprintf('Failed to fetch JWKS from %s: %s', $url, $e->getMessage()), - $e - ); - } catch (\JsonException $e) { - throw TokenDecoderException::forJwksError( - sprintf('Invalid JSON response from JWKS endpoint: %s', $e->getMessage()), - $e - ); - } catch (\Exception $e) { - throw TokenDecoderException::forJwksError( - sprintf('Unable to retrieve JWKS: %s', $e->getMessage()), - $e - ); + } + catch (GuzzleException $e) { + throw TokenDecoderException::forJwksError(sprintf('Failed to fetch JWKS from %s: %s', $url, $e->getMessage()), new \Exception($e->getMessage(), $e->getCode(), $e)); + } + catch (\JsonException $e) { + throw TokenDecoderException::forJwksError(sprintf('Invalid JSON response from JWKS endpoint: %s', $e->getMessage()), $e); + } + catch (\Exception $e) { + throw TokenDecoderException::forJwksError(sprintf('Unable to retrieve JWKS: %s', $e->getMessage()), $e); } } private function base64urlDecode(string $data): string { $decoded = base64_decode(strtr($data, '-_', '+/'), true); - if ($decoded === false) { - throw TokenDecoderException::forDecodingError( - 'Failed to decode base64url string', - new \Exception('Invalid base64url format') - ); + if (false === $decoded) { + throw TokenDecoderException::forDecodingError('Failed to decode base64url string', new \Exception('Invalid base64url format')); } return $decoded; @@ -219,35 +229,26 @@ private function validateBaseUrl(string $baseUrl): void { // Parse the URL $parsed = parse_url($baseUrl); - if ($parsed === false || !isset($parsed['scheme'], $parsed['host'])) { - throw TokenDecoderException::forInvalidConfiguration(sprintf( - 'Invalid base_url format: %s. Expected a valid URL with scheme and host.', - $baseUrl - )); + if (false === $parsed || !isset($parsed['scheme'], $parsed['host'])) { + throw TokenDecoderException::forInvalidConfiguration(sprintf('Invalid base_url format: %s. Expected a valid URL with scheme and host.', $baseUrl)); } // Only allow HTTPS (or HTTP for localhost/development) if (!in_array($parsed['scheme'], ['https', 'http'], true)) { - throw TokenDecoderException::forSecurityViolation(sprintf( - 'Invalid base_url scheme: %s. Only http and https are allowed.', - $parsed['scheme'] - )); + throw TokenDecoderException::forSecurityViolation(sprintf('Invalid base_url scheme: %s. Only http and https are allowed.', $parsed['scheme'])); } // Enforce HTTPS for non-localhost environments - if ($parsed['scheme'] === 'http' && !$this->isLocalhost($parsed['host'])) { - throw TokenDecoderException::forSecurityViolation(sprintf( - 'HTTP is only allowed for localhost. Use HTTPS for: %s', - $parsed['host'] - )); + if ( + 'http' === $parsed['scheme'] + && !$this->isLocalhost($parsed['host']) + ) { + throw TokenDecoderException::forSecurityViolation(sprintf('HTTP is only allowed for localhost. Use HTTPS for: %s', $parsed['host'])); } // Prevent private IP ranges and localhost in production unless explicitly localhost if (!$this->isAllowedHost($parsed['host'])) { - throw TokenDecoderException::forSecurityViolation(sprintf( - 'The host %s is not allowed. Private IPs and internal hosts are blocked for security.', - $parsed['host'] - )); + throw TokenDecoderException::forSecurityViolation(sprintf('The host %s is not allowed. Private IPs and internal hosts are blocked for security.', $parsed['host'])); } } @@ -259,10 +260,8 @@ private function validateBaseUrl(string $baseUrl): void private function validateJwksUrl(string $url): void { $parsed = parse_url($url); - if ($parsed === false || !isset($parsed['scheme'], $parsed['host'])) { - throw TokenDecoderException::forSecurityViolation( - 'Invalid JWKS URL format' - ); + if (false === $parsed || !isset($parsed['scheme'], $parsed['host'])) { + throw TokenDecoderException::forSecurityViolation('Invalid JWKS URL format'); } // Get allowed domains from configuration @@ -281,28 +280,24 @@ private function validateJwksUrl(string $url): void // Support wildcard subdomains (e.g., *.example.com) if (str_starts_with($allowedDomain, '*.')) { $domain = substr($allowedDomain, 2); - if ($host === $domain || str_ends_with($host, '.' . $domain)) { + if ($host === $domain || str_ends_with($host, '.'.$domain)) { $isAllowed = true; break; } - } elseif ($host === $allowedDomain) { + } + elseif ($host === $allowedDomain) { $isAllowed = true; break; } } if (!$isAllowed) { - throw TokenDecoderException::forSecurityViolation(sprintf( - 'JWKS URL host "%s" is not in the allowed domains whitelist', - $host - )); + throw TokenDecoderException::forSecurityViolation(sprintf('JWKS URL host "%s" is not in the allowed domains whitelist', $host)); } // Additional security check: ensure HTTPS or localhost - if ($parsed['scheme'] === 'http' && !$this->isLocalhost($host)) { - throw TokenDecoderException::forSecurityViolation( - 'JWKS endpoint must use HTTPS for non-localhost hosts' - ); + if ('http' === $parsed['scheme'] && !$this->isLocalhost($host)) { + throw TokenDecoderException::forSecurityViolation('JWKS endpoint must use HTTPS for non-localhost hosts'); } } @@ -311,8 +306,11 @@ private function validateJwksUrl(string $url): void */ private function isLocalhost(string $host): bool { - return in_array($host, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true) - || str_ends_with($host, '.localhost'); + return in_array( + $host, + ['localhost', '127.0.0.1', '::1', '0.0.0.0'], + true, + ) || str_ends_with($host, '.localhost'); } /** @@ -326,9 +324,16 @@ private function isAllowedHost(string $host): bool } // Check if it's an IP address - if (filter_var($host, FILTER_VALIDATE_IP) !== false) { + if (false !== filter_var($host, FILTER_VALIDATE_IP)) { // Block private IP ranges - if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { + if ( + false === + filter_var( + $host, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE, + ) + ) { return false; } } @@ -342,7 +347,7 @@ private function isAllowedHost(string $host): bool ]; foreach ($blockedHosts as $blocked) { - if (stripos($host, $blocked) !== false) { + if (false !== stripos($host, $blocked)) { return false; } } diff --git a/src/Token/KeycloakResourceOwner.php b/src/Token/KeycloakResourceOwner.php index ab60e78..62b4ba0 100644 --- a/src/Token/KeycloakResourceOwner.php +++ b/src/Token/KeycloakResourceOwner.php @@ -11,7 +11,7 @@ class KeycloakResourceOwner implements ResourceOwnerInterface, UserInterface { /** - * Raw response. + * @var array */ protected array $response; @@ -19,9 +19,13 @@ class KeycloakResourceOwner implements ResourceOwnerInterface, UserInterface /** * Creates new resource owner. + * + * @param array $response */ - public function __construct(array $response = [], ?AccessTokenInterface $accessToken = null) - { + public function __construct( + array $response = [], + ?AccessTokenInterface $accessToken = null, + ) { $this->response = $response; $this->accessToken = $accessToken; } @@ -93,6 +97,7 @@ private function getRealmRoles(): array * Get client roles. * * @param string|null $client_id Optional client ID to filter roles + * * @return array */ private function getClientRoles(?string $client_id = null): array @@ -100,18 +105,18 @@ private function getClientRoles(?string $client_id = null): array $resource_access = $this->response['resource_access'] ?? []; // If client_id is provided, return only roles for that client - if ($client_id !== null) { + if (null !== $client_id) { return $resource_access[$client_id]['roles'] ?? []; } // Otherwise, collect all roles from all clients return array_reduce( $resource_access, - static fn(array $carry, array $client): array => [ + static fn (array $carry, array $client): array => [ ...$carry, - ...($client['roles'] ?? []) + ...$client['roles'] ?? [], ], - [] + [], ); } @@ -122,7 +127,10 @@ private function getClientRoles(?string $client_id = null): array */ public function getRoles(?string $client_id = null): array { - return [...$this->getRealmRoles(), ...$this->getClientRoles($client_id)]; + return [ + ...$this->getRealmRoles(), + ...$this->getClientRoles($client_id), + ]; } /** diff --git a/src/Token/RS256TokenDecoder.php b/src/Token/RS256TokenDecoder.php index b93aa3c..ae13852 100644 --- a/src/Token/RS256TokenDecoder.php +++ b/src/Token/RS256TokenDecoder.php @@ -41,7 +41,7 @@ public function validateToken(string $realm, array $tokenDecoded): void throw TokenDecoderException::forExpiration(new \Exception('Token has expired')); } - if (str_contains($tokenDecoded['iss'], $realm) === false) { + if (false === str_contains($tokenDecoded['iss'], $realm)) { throw TokenDecoderException::forIssuerMismatch(new \Exception('Invalid token issuer')); } } diff --git a/src/Token/TokenDecoderFactory.php b/src/Token/TokenDecoderFactory.php index 98d6f48..4c3aaaa 100644 --- a/src/Token/TokenDecoderFactory.php +++ b/src/Token/TokenDecoderFactory.php @@ -13,8 +13,12 @@ class TokenDecoderFactory public const ALGORITHM_HS256 = 'HS256'; public const ALGORITHM_JWKS = 'JWKS'; - public static function create(string $algorithm, ClientInterface $httpClient, array $options = []): TokenDecoderInterface - { + /** @param array $options */ + public static function create( + string $algorithm, + ClientInterface $httpClient, + array $options = [], + ): TokenDecoderInterface { return match ($algorithm) { self::ALGORITHM_RS256 => new RS256TokenDecoder(), self::ALGORITHM_HS256 => new HS256TokenDecoder(), diff --git a/tests/EventSubscriber/TokenAuthListenerTest.php b/tests/EventSubscriber/TokenAuthListenerTest.php index b8fdd55..516193b 100644 --- a/tests/EventSubscriber/TokenAuthListenerTest.php +++ b/tests/EventSubscriber/TokenAuthListenerTest.php @@ -26,7 +26,9 @@ class MyController #[ExcludeTokenValidationAttribute] public function excludedRouteAction(): Response { - return new Response('Excluded route', Response::HTTP_OK, ['Content-Type' => 'text/plain']); + return new Response('Excluded route', Response::HTTP_OK, [ + 'Content-Type' => 'text/plain', + ]); } } @@ -35,64 +37,64 @@ class TokenAuthListenerTest extends TestCase use QueryBuilderTrait; public const ENCRYPTION_KEY = <<jwtTemplate, time() + 3600, time(), time()); - $this->access_token = JWT::encode(json_decode($jwt_tmp, true), self::ENCRYPTION_KEY, self::ENCRYPTION_ALGORITHM); + $this->access_token = JWT::encode( + json_decode($jwt_tmp, true), + self::ENCRYPTION_KEY, + self::ENCRYPTION_ALGORITHM, + ); } protected function tearDown(): void @@ -127,7 +133,11 @@ public function testCheckValidTokenOnRequest(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse ->allows('getBody') @@ -139,9 +149,7 @@ public function testCheckValidTokenOnRequest(): void // mock resource owner $jwt_tmp = sprintf($this->jwtTemplate, time() + 3600, time(), time()); $getResourceOwnerStream = $this->createMock(StreamInterface::class); - $getResourceOwnerStream - ->method('__toString') - ->willReturn($jwt_tmp); + $getResourceOwnerStream->method('__toString')->willReturn($jwt_tmp); $getResourceOwnerResponse = m::mock(ResponseInterface::class); $getResourceOwnerResponse ->allows('getBody') @@ -158,17 +166,23 @@ public function testCheckValidTokenOnRequest(): void $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); // mock event request $logger = $this->createMock(LoggerInterface::class); - $tokenAuthListener = new TokenAuthListener($logger, $this->keycloakClient); + $tokenAuthListener = new TokenAuthListener( + $logger, + $this->keycloakClient, + ); $request = new Request(); $request->headers->set('X-Auth-Token', $token->getToken()); $eventRequest = new RequestEvent( $this->createMock(HttpKernelInterface::class), $request, - HttpKernelInterface::MAIN_REQUEST + HttpKernelInterface::MAIN_REQUEST, ); // call checkValidToken @@ -188,18 +202,22 @@ public function testCheckValidTokenExcludesRouteWithAttribute(): void // when // Create a mock controller method with ExcludeTokenValidationAttribute - $controllerMethodWithAttribute = 'Mainick\KeycloakClientBundle\Tests\EventSubscriber\MyController::excludedRouteAction'; + $controllerMethodWithAttribute = + "Mainick\KeycloakClientBundle\Tests\EventSubscriber\MyController::excludedRouteAction"; // Mock the request for a route with ExcludeTokenValidationAttribute $request = new Request(); - $request->attributes->set('_controller', $controllerMethodWithAttribute); + $request->attributes->set( + '_controller', + $controllerMethodWithAttribute, + ); $request->headers->set('X-Auth-Token', $this->access_token); // Mock the Event $eventRequest = new RequestEvent( $this->createMock(HttpKernelInterface::class), $request, - HttpKernelInterface::MAIN_REQUEST + HttpKernelInterface::MAIN_REQUEST, ); // call checkValidToken diff --git a/tests/Provider/KeycloakClientTest.php b/tests/Provider/KeycloakClientTest.php index ba05ef0..fe663b4 100644 --- a/tests/Provider/KeycloakClientTest.php +++ b/tests/Provider/KeycloakClientTest.php @@ -19,65 +19,65 @@ class KeycloakClientTest extends TestCase use QueryBuilderTrait; public const ENCRYPTION_KEY = <<jwtTemplate, time() + 3600, time(), time()); - $this->access_token = JWT::encode(json_decode($jwt_tmp, true), self::ENCRYPTION_KEY, self::ENCRYPTION_ALGORITHM); + $this->access_token = JWT::encode( + json_decode($jwt_tmp, true), + self::ENCRYPTION_KEY, + self::ENCRYPTION_ALGORITHM, + ); } protected function tearDown(): void @@ -113,7 +117,11 @@ public function testRefreshToken(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -124,19 +132,22 @@ public function testRefreshToken(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(2) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(2)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $refreshToken = $this->keycloakClient->refreshToken($token); // then $this->assertEquals($this->access_token, $refreshToken->getToken()); - $this->assertEquals('mock_refresh_token', $refreshToken->getRefreshToken()); + $this->assertEquals( + 'mock_refresh_token', + $refreshToken->getRefreshToken(), + ); } public function testVerifyToken(): void @@ -145,7 +156,11 @@ public function testVerifyToken(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -156,14 +171,14 @@ public function testVerifyToken(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $user = $this->keycloakClient->verifyToken($token); // then @@ -178,7 +193,11 @@ public function testUserInfo(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -190,9 +209,7 @@ public function testUserInfo(): void $jwt_tmp = sprintf($this->jwtTemplate, time() + 3600, time(), time()); $getResourceOwnerStream = $this->createMock(StreamInterface::class); - $getResourceOwnerStream - ->method('__toString') - ->willReturn($jwt_tmp); + $getResourceOwnerStream->method('__toString')->willReturn($jwt_tmp); $getResourceOwnerResponse = m::mock(ResponseInterface::class); $getResourceOwnerResponse @@ -209,7 +226,10 @@ public function testUserInfo(): void $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $user = $this->keycloakClient->userInfo($token); // then @@ -225,7 +245,11 @@ public function testAuthenticate(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -236,30 +260,34 @@ public function testAuthenticate(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); // then $this->assertEquals($this->access_token, $token->getToken()); $this->assertEquals(time() + 3600, $token->getExpires()); $this->assertEquals('mock_refresh_token', $token->getRefreshToken()); - $this->assertIsArray($token->getValues()); + $this->assertIsArray($token->getValues()); // @phpstan-ignore method.alreadyNarrowedType $this->assertArrayHasKey('scope', $token->getValues()); } - public function testAuthenticateByCode() + public function testAuthenticateByCode(): void { // given $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -270,10 +298,7 @@ public function testAuthenticateByCode() ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when @@ -283,7 +308,7 @@ public function testAuthenticateByCode() $this->assertEquals($this->access_token, $token->getToken()); $this->assertEquals(time() + 3600, $token->getExpires()); $this->assertEquals('mock_refresh_token', $token->getRefreshToken()); - $this->assertIsArray($token->getValues()); + $this->assertIsArray($token->getValues()); // @phpstan-ignore method.alreadyNarrowedType $this->assertArrayHasKey('scope', $token->getValues()); } @@ -293,7 +318,11 @@ public function testGetRolesUser(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -304,16 +333,19 @@ public function testGetRolesUser(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $user = $this->keycloakClient->verifyToken($token); - $roles_name = array_map(fn ($role) => $role->name, $user->applicationRoles); + $roles_name = array_map( + fn ($role) => $role->name, + $user->applicationRoles, + ); // then $this->assertIsArray($user->applicationRoles); @@ -326,7 +358,11 @@ public function testHasRoleInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -337,14 +373,14 @@ public function testHasRoleInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $hasRole = $this->keycloakClient->hasRole($token, 'test-app-role-user'); // then @@ -357,7 +393,11 @@ public function testHasAnyRoleInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -368,15 +408,18 @@ public function testHasAnyRoleInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $anyRole = $this->keycloakClient->hasAnyRole($token, ['test-app-role-user', 'test-app-role-admin']); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $anyRole = $this->keycloakClient->hasAnyRole($token, [ + 'test-app-role-user', + 'test-app-role-admin', + ]); // then $this->assertTrue($anyRole); @@ -388,7 +431,11 @@ public function testHasAllRolesInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -399,15 +446,18 @@ public function testHasAllRolesInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $allRoles = $this->keycloakClient->hasAllRoles($token, ['test-app-role-user', 'view-profile']); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $allRoles = $this->keycloakClient->hasAllRoles($token, [ + 'test-app-role-user', + 'view-profile', + ]); // then $this->assertTrue($allRoles); @@ -419,7 +469,11 @@ public function testGetGroupsUser(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -430,14 +484,14 @@ public function testGetGroupsUser(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $user = $this->keycloakClient->verifyToken($token); $groups_name = array_map(fn ($group) => $group->name, $user->groups); @@ -452,7 +506,11 @@ public function testHasGroupInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -463,15 +521,18 @@ public function testHasGroupInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $hasGroup = $this->keycloakClient->hasGroup($token, 'test-app-group-user'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $hasGroup = $this->keycloakClient->hasGroup( + $token, + 'test-app-group-user', + ); // then $this->assertTrue($hasGroup); @@ -483,7 +544,11 @@ public function testHasAnyGroupInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -494,15 +559,18 @@ public function testHasAnyGroupInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $anyGroup = $this->keycloakClient->hasAnyGroup($token, ['test-app-group-user', 'test-app-group-not-exists']); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $anyGroup = $this->keycloakClient->hasAnyGroup($token, [ + 'test-app-group-user', + 'test-app-group-not-exists', + ]); // then $this->assertTrue($anyGroup); @@ -514,7 +582,11 @@ public function testHasAllGroupsInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -525,15 +597,18 @@ public function testHasAllGroupsInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $allGroups = $this->keycloakClient->hasAllGroups($token, ['test-app-group-user', 'test-app-group-admin']); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $allGroups = $this->keycloakClient->hasAllGroups($token, [ + 'test-app-group-user', + 'test-app-group-admin', + ]); // then $this->assertTrue($allGroups); @@ -545,7 +620,11 @@ public function testGetScopeUser(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -556,14 +635,14 @@ public function testGetScopeUser(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $user = $this->keycloakClient->verifyToken($token); $scope_name = array_map(fn ($scope) => $scope->name, $user->scope); @@ -578,7 +657,11 @@ public function testHasScopeInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -589,14 +672,14 @@ public function testHasScopeInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); $hasScope = $this->keycloakClient->hasScope($token, 'openid'); // then @@ -609,7 +692,11 @@ public function testHasAnyScopeInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -620,15 +707,18 @@ public function testHasAnyScopeInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $hasAnyScope = $this->keycloakClient->hasAnyScope($token, ['openid', 'roles_clients']); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $hasAnyScope = $this->keycloakClient->hasAnyScope($token, [ + 'openid', + 'roles_clients', + ]); // then $this->assertTrue($hasAnyScope); @@ -640,7 +730,11 @@ public function testHasAllScopesInUserSOnes(): void $getAccessTokenStream = $this->createMock(StreamInterface::class); $getAccessTokenStream ->method('__toString') - ->willReturn('{"access_token":"'.$this->access_token.'","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}'); + ->willReturn( + '{"access_token":"'. + $this->access_token. + '","expires_in":3600,"refresh_token":"mock_refresh_token","scope":"email","token_type":"bearer"}', + ); $getAccessTokenResponse = m::mock(ResponseInterface::class); $getAccessTokenResponse @@ -651,15 +745,18 @@ public function testHasAllScopesInUserSOnes(): void ->andReturns(['content-type' => 'application/json']); $client = m::mock(ClientInterface::class); - $client - ->expects('send') - ->times(1) - ->andReturns($getAccessTokenResponse); + $client->expects('send')->times(1)->andReturns($getAccessTokenResponse); $this->keycloakClient->setHttpClient($client); // when - $token = $this->keycloakClient->authenticate('mock_user', 'mock_password'); - $hasAllScopes = $this->keycloakClient->hasAllScopes($token, ['openid', 'profile']); + $token = $this->keycloakClient->authenticate( + 'mock_user', + 'mock_password', + ); + $hasAllScopes = $this->keycloakClient->hasAllScopes($token, [ + 'openid', + 'profile', + ]); // then $this->assertTrue($hasAllScopes); diff --git a/tests/Security/KeycloakAuthenticatorTest.php b/tests/Security/KeycloakAuthenticatorTest.php index 52a8359..2294139 100644 --- a/tests/Security/KeycloakAuthenticatorTest.php +++ b/tests/Security/KeycloakAuthenticatorTest.php @@ -24,65 +24,65 @@ class KeycloakAuthenticatorTest extends TestCase { public const ENCRYPTION_KEY = <<markTestSkipped('The Symfony Security component is not installed.'); + $this->markTestSkipped( + 'The Symfony Security component is not installed.', + ); } $jwt_tmp = sprintf($this->jwtTemplate, time() + 3600, time(), time()); - $this->access_token = JWT::encode(json_decode($jwt_tmp, true), self::ENCRYPTION_KEY, self::ENCRYPTION_ALGORITHM); + $this->access_token = JWT::encode( + json_decode($jwt_tmp, true), + self::ENCRYPTION_KEY, + self::ENCRYPTION_ALGORITHM, + ); $this->iamClient = m::mock(KeycloakClient::class); $accessToken = m::mock(AccessTokenInterface::class); - $accessToken - ->allows('getToken') - ->andReturns($this->access_token); + $accessToken->allows('getToken')->andReturns($this->access_token); $accessToken ->allows('getRefreshToken') ->andReturns('mock_refresh_token'); @@ -123,7 +127,7 @@ protected function setUp(): void $this->authenticator = new KeycloakAuthenticator( $this->createMock(LoggerInterface::class), $this->iamClient, - $this->userProvider + $this->userProvider, ); } @@ -157,7 +161,10 @@ public function testAuthenticateSuccessfulAuthentication(): void $userBadge = $passport->getBadge(UserBadge::class); $this->assertNotNull($userBadge); $this->assertEquals($this->resourceOwner, $userBadge->getUser()); - $this->assertEquals($this->access_token, $userBadge->getUserIdentifier()); + $this->assertEquals( + $this->access_token, + $userBadge->getUserIdentifier(), + ); } public function testAuthenticateInvalidState(): void @@ -178,7 +185,9 @@ public function testAuthenticateInvalidState(): void // when $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('query state (invalid_state) is not the same as session state (some_state)'); + $this->expectExceptionMessage( + 'query state (invalid_state) is not the same as session state (some_state)', + ); $this->authenticator->authenticate($request); } @@ -199,7 +208,9 @@ public function testAuthenticateMissingCode(): void // when $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Authentication failed! Did you authorize our app?'); + $this->expectExceptionMessage( + 'Authentication failed! Did you authorize our app?', + ); $this->authenticator->authenticate($request); } } diff --git a/tests/Serializer/CollectionDenormalizerTest.php b/tests/Serializer/CollectionDenormalizerTest.php index a4d1003..b1d587a 100644 --- a/tests/Serializer/CollectionDenormalizerTest.php +++ b/tests/Serializer/CollectionDenormalizerTest.php @@ -29,14 +29,14 @@ public function testDenormalizeRealmCollection(): void 'id' => '1', 'realm' => 'master', 'displayName' => 'Master Realm', - 'enabled' => true + 'enabled' => true, ], [ 'id' => '2', 'realm' => 'test', 'displayName' => 'Test Realm', - 'enabled' => false - ] + 'enabled' => false, + ], ]; $realm1 = new RealmRepresentation( @@ -56,9 +56,10 @@ public function testDenormalizeRealmCollection(): void $innerDenormalizer->expects($this->exactly(2)) ->method('denormalize') ->willReturnCallback(function ($data, $type, $format, $context) use ($realm1, $realm2) { - if ($data['id'] === '1') { + if ('1' === $data['id']) { return $realm1; } + return $realm2; }); @@ -99,14 +100,14 @@ classMetadataFactory: $classMetadataFactory, 'id' => '1', 'realm' => 'master', 'displayName' => 'Master Realm', - 'enabled' => true + 'enabled' => true, ], [ 'id' => '2', 'realm' => 'test', 'displayName' => 'Test Realm', - 'enabled' => false - ] + 'enabled' => false, + ], ]; // Esecuzione diff --git a/tests/Service/ClientsServiceTest.php b/tests/Service/ClientsServiceTest.php index c4b4fe5..ca28f22 100644 --- a/tests/Service/ClientsServiceTest.php +++ b/tests/Service/ClientsServiceTest.php @@ -90,9 +90,9 @@ public function testAll(): void $this->httpClient ->shouldReceive('request') - ->with('GET','admin/realms/'.$realm.'/clients', m::on(function($options) { - return isset($options['headers']['Authorization']) && - $options['headers']['Authorization'] === 'Bearer mock_token'; + ->with('GET', 'admin/realms/'.$realm.'/clients', m::on(function ($options) { + return isset($options['headers']['Authorization']) + && 'Bearer mock_token' === $options['headers']['Authorization']; })) ->andReturn($response); @@ -226,7 +226,7 @@ public function testCreate(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/clients', m::on(function($options) { + ->with('POST', 'admin/realms/'.$realm.'/clients', m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); @@ -259,7 +259,7 @@ public function testUpdate(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/clients/client1', m::on(function($options) { + ->with('PUT', 'admin/realms/'.$realm.'/clients/client1', m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); @@ -298,6 +298,7 @@ public function testDelete(): void // then $this->assertTrue($result); } + public function testRoles(): void { // given @@ -405,7 +406,7 @@ public function testCreateRole(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles', m::on(function($options) { + ->with('POST', 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles', m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); @@ -439,7 +440,7 @@ public function testUpdateRole(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName, m::on(function($options) { + ->with('PUT', 'admin/realms/'.$realm.'/clients/'.$clientUuid.'/roles/'.$roleName, m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); diff --git a/tests/Service/GroupsServiceTest.php b/tests/Service/GroupsServiceTest.php index 7bdb092..213dc46 100644 --- a/tests/Service/GroupsServiceTest.php +++ b/tests/Service/GroupsServiceTest.php @@ -15,7 +15,6 @@ use Mainick\KeycloakClientBundle\Serializer\Serializer; use Mainick\KeycloakClientBundle\Service\Criteria; use Mainick\KeycloakClientBundle\Service\GroupsService; -use Mainick\KeycloakClientBundle\Service\HttpMethodEnum; use Mainick\KeycloakClientBundle\Token\AccessToken; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -43,11 +42,19 @@ protected function setUp(): void $this->serializer = m::mock(Serializer::class); $keycloakProvider = m::mock(Keycloak::class); - $keycloakProvider->shouldReceive('getHttpClient')->andReturn($this->httpClient); - - $this->keycloakAdminClient->shouldReceive('getKeycloakProvider')->andReturn($keycloakProvider); - $this->keycloakAdminClient->shouldReceive('getBaseUrl')->andReturn('http://mock.url/auth'); - $this->keycloakAdminClient->shouldReceive('getVersion')->andReturn('17.0.1'); + $keycloakProvider + ->shouldReceive('getHttpClient') + ->andReturn($this->httpClient); + + $this->keycloakAdminClient + ->shouldReceive('getKeycloakProvider') + ->andReturn($keycloakProvider); + $this->keycloakAdminClient + ->shouldReceive('getBaseUrl') + ->andReturn('http://mock.url/auth'); + $this->keycloakAdminClient + ->shouldReceive('getVersion') + ->andReturn('17.0.1'); $this->adminAccessToken = new AccessToken(); $this->adminAccessToken @@ -56,11 +63,13 @@ protected function setUp(): void ->setRefreshToken('mock_refresh_token') ->setValues(['scope' => 'email']); - $this->keycloakAdminClient->shouldReceive('getAdminAccessToken')->andReturn($this->adminAccessToken); + $this->keycloakAdminClient + ->shouldReceive('getAdminAccessToken') + ->andReturn($this->adminAccessToken); $this->groupsService = new GroupsService( $this->logger, - $this->keycloakAdminClient + $this->keycloakAdminClient, ); $reflection = new \ReflectionClass($this->groupsService); @@ -79,7 +88,8 @@ public function testAll(): void { // given $realm = 'test-realm'; - $responseBody = '[{"id":"group1","name":"group1"},{"id":"group2","name":"group2"}]'; + $responseBody = + '[{"id":"group1","name":"group1"},{"id":"group2","name":"group2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -89,10 +99,15 @@ public function testAll(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups', m::on(function($options) { - return isset($options['headers']['Authorization']) && - $options['headers']['Authorization'] === 'Bearer mock_token'; - })) + ->with( + 'GET', + 'admin/realms/'.$realm.'/groups', + m::on(function ($options) { + return isset($options['headers']['Authorization']) + && 'Bearer mock_token' === + $options['headers']['Authorization']; + }), + ) ->andReturn($response); $groupCollection = new GroupCollection(); @@ -128,7 +143,8 @@ public function testAllWithCriteria(): void // given $criteria = new Criteria(['briefRepresentation' => 'true']); $realm = 'test-realm'; - $responseBody = '[{"id":"group1","name":"group1"},{"id":"group2","name":"group2"}]'; + $responseBody = + '[{"id":"group1","name":"group1"},{"id":"group2","name":"group2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -138,7 +154,11 @@ public function testAllWithCriteria(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups?briefRepresentation=true', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/groups?briefRepresentation=true', + m::type('array'), + ) ->andReturn($response); $groupCollection = new GroupCollection(); @@ -183,7 +203,11 @@ public function testCount(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/count', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/groups/count', + m::type('array'), + ) ->andReturn($response); $this->serializer @@ -197,6 +221,7 @@ public function testCount(): void $result = $this->groupsService->count($realm); // then + // @phpstan-ignore method.alreadyNarrowedType $this->assertIsInt($result); $this->assertEquals(2, $result); } @@ -206,7 +231,8 @@ public function testChildren(): void // given $realm = 'test-realm'; $groupId = 'parent-group'; - $responseBody = '[{"id":"child1","name":"child1"},{"id":"child2","name":"child2"}]'; + $responseBody = + '[{"id":"child1","name":"child1"},{"id":"child2","name":"child2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -216,7 +242,11 @@ public function testChildren(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId.'/children', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/groups/'.$groupId.'/children', + m::type('array'), + ) ->andReturn($response); $groupCollection = new GroupCollection(); @@ -262,7 +292,11 @@ public function testGet(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId, m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/groups/'.$groupId, + m::type('array'), + ) ->andReturn($response); $group = new GroupRepresentation(); @@ -305,9 +339,13 @@ public function testCreate(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/groups', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'.$realm.'/groups', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -337,15 +375,27 @@ public function testCreateChild(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/groups/'.$parentGroupId.'/children', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'. + $realm. + '/groups/'. + $parentGroupId. + '/children', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->groupsService->createChild($realm, $parentGroupId, $group); + $result = $this->groupsService->createChild( + $realm, + $parentGroupId, + $group, + ); // then $this->assertTrue($result); @@ -370,9 +420,13 @@ public function testUpdate(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/groups/'.$groupId, m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'PUT', + 'admin/realms/'.$realm.'/groups/'.$groupId, + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -399,7 +453,11 @@ public function testDelete(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/groups/'.$groupId, m::type('array')) + ->with( + 'DELETE', + 'admin/realms/'.$realm.'/groups/'.$groupId, + m::type('array'), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -416,7 +474,8 @@ public function testUsers(): void // given $realm = 'test-realm'; $groupId = 'group1'; - $responseBody = '[{"id":"user1","username":"user1"},{"id":"user2","username":"user2"}]'; + $responseBody = + '[{"id":"user1","username":"user1"},{"id":"user2","username":"user2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -426,7 +485,11 @@ public function testUsers(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId.'/members', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/groups/'.$groupId.'/members', + m::type('array'), + ) ->andReturn($response); $userCollection = new UserCollection(); @@ -462,7 +525,8 @@ public function testRealmRoles(): void // given $realm = 'test-realm'; $groupId = 'group1'; - $responseBody = '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; + $responseBody = + '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -472,7 +536,15 @@ public function testRealmRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm', + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -508,7 +580,8 @@ public function testAvailableRealmRoles(): void // given $realm = 'test-realm'; $groupId = 'group1'; - $responseBody = '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; + $responseBody = + '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -518,7 +591,15 @@ public function testAvailableRealmRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm/available', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm/available', + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -568,9 +649,17 @@ public function testAddRealmRole(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -601,15 +690,27 @@ public function testRemoveRealmRole(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/realm', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'DELETE', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/realm', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->groupsService->removeRealmRole($realm, $groupId, $role); + $result = $this->groupsService->removeRealmRole( + $realm, + $groupId, + $role, + ); // then $this->assertTrue($result); @@ -621,7 +722,8 @@ public function testClientRoles(): void $realm = 'test-realm'; $clientUuid = 'client1'; $groupId = 'group1'; - $responseBody = '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; + $responseBody = + '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -631,7 +733,16 @@ public function testClientRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid, m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid, + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -652,7 +763,11 @@ public function testClientRoles(): void $this->logger->shouldReceive('info')->once(); // when - $result = $this->groupsService->clientRoles($realm, $clientUuid, $groupId); + $result = $this->groupsService->clientRoles( + $realm, + $clientUuid, + $groupId, + ); // then $this->assertInstanceOf(RoleCollection::class, $result); @@ -668,7 +783,8 @@ public function testAvailableClientRoles(): void $realm = 'test-realm'; $clientUuid = 'client1'; $groupId = 'group1'; - $responseBody = '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; + $responseBody = + '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -678,7 +794,17 @@ public function testAvailableClientRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid.'/available', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid. + '/available', + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -699,7 +825,11 @@ public function testAvailableClientRoles(): void $this->logger->shouldReceive('info')->once(); // when - $result = $this->groupsService->availableClientRoles($realm, $clientUuid, $groupId); + $result = $this->groupsService->availableClientRoles( + $realm, + $clientUuid, + $groupId, + ); // then $this->assertInstanceOf(RoleCollection::class, $result); @@ -729,15 +859,29 @@ public function testAddClientRole(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid, m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid, + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->groupsService->addClientRole($realm, $clientUuid, $groupId, $role); + $result = $this->groupsService->addClientRole( + $realm, + $clientUuid, + $groupId, + $role, + ); // then $this->assertTrue($result); @@ -763,15 +907,29 @@ public function testRemoveClientRole(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/groups/'.$groupId.'/role-mappings/clients/'.$clientUuid, m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'DELETE', + 'admin/realms/'. + $realm. + '/groups/'. + $groupId. + '/role-mappings/clients/'. + $clientUuid, + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->groupsService->removeClientRole($realm, $clientUuid, $groupId, $role); + $result = $this->groupsService->removeClientRole( + $realm, + $clientUuid, + $groupId, + $role, + ); // then $this->assertTrue($result); diff --git a/tests/Service/RealmsServiceTest.php b/tests/Service/RealmsServiceTest.php index 350f560..dc902c2 100644 --- a/tests/Service/RealmsServiceTest.php +++ b/tests/Service/RealmsServiceTest.php @@ -83,9 +83,9 @@ public function testAll(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms', m::on(function($options) { - return isset($options['headers']['Authorization']) && - $options['headers']['Authorization'] === 'Bearer mock_token'; + ->with('GET', 'admin/realms', m::on(function ($options) { + return isset($options['headers']['Authorization']) + && 'Bearer mock_token' === $options['headers']['Authorization']; })) ->andReturn($response); @@ -213,7 +213,7 @@ public function testCreate(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/', m::on(function($options) { + ->with('POST', 'admin/realms/', m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); @@ -244,7 +244,7 @@ public function testUpdate(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/test', m::on(function($options) { + ->with('PUT', 'admin/realms/test', m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); diff --git a/tests/Service/RolesServiceTest.php b/tests/Service/RolesServiceTest.php index 9de6b62..110821a 100644 --- a/tests/Service/RolesServiceTest.php +++ b/tests/Service/RolesServiceTest.php @@ -88,9 +88,9 @@ public function testAll(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/roles', m::on(function($options) { - return isset($options['headers']['Authorization']) && - $options['headers']['Authorization'] === 'Bearer mock_token'; + ->with('GET', 'admin/realms/'.$realm.'/roles', m::on(function ($options) { + return isset($options['headers']['Authorization']) + && 'Bearer mock_token' === $options['headers']['Authorization']; })) ->andReturn($response); @@ -227,7 +227,7 @@ public function testCreate(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/roles', m::on(function($options) { + ->with('POST', 'admin/realms/'.$realm.'/roles', m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); @@ -260,7 +260,7 @@ public function testUpdate(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/roles/'.$roleName, m::on(function($options) { + ->with('PUT', 'admin/realms/'.$realm.'/roles/'.$roleName, m::on(function ($options) { return isset($options['json']); })) ->andReturn($response); diff --git a/tests/Service/UsersServiceTest.php b/tests/Service/UsersServiceTest.php index c013a6c..46eaf62 100644 --- a/tests/Service/UsersServiceTest.php +++ b/tests/Service/UsersServiceTest.php @@ -18,7 +18,6 @@ use Mainick\KeycloakClientBundle\Representation\UserSessionRepresentation; use Mainick\KeycloakClientBundle\Serializer\Serializer; use Mainick\KeycloakClientBundle\Service\Criteria; -use Mainick\KeycloakClientBundle\Service\HttpMethodEnum; use Mainick\KeycloakClientBundle\Service\UsersService; use Mainick\KeycloakClientBundle\Token\AccessToken; use Mockery as m; @@ -47,11 +46,19 @@ protected function setUp(): void $this->serializer = m::mock(Serializer::class); $keycloakProvider = m::mock(Keycloak::class); - $keycloakProvider->shouldReceive('getHttpClient')->andReturn($this->httpClient); - - $this->keycloakAdminClient->shouldReceive('getKeycloakProvider')->andReturn($keycloakProvider); - $this->keycloakAdminClient->shouldReceive('getBaseUrl')->andReturn('http://mock.url/auth'); - $this->keycloakAdminClient->shouldReceive('getVersion')->andReturn('17.0.1'); + $keycloakProvider + ->shouldReceive('getHttpClient') + ->andReturn($this->httpClient); + + $this->keycloakAdminClient + ->shouldReceive('getKeycloakProvider') + ->andReturn($keycloakProvider); + $this->keycloakAdminClient + ->shouldReceive('getBaseUrl') + ->andReturn('http://mock.url/auth'); + $this->keycloakAdminClient + ->shouldReceive('getVersion') + ->andReturn('17.0.1'); $this->adminAccessToken = new AccessToken(); $this->adminAccessToken @@ -60,11 +67,13 @@ protected function setUp(): void ->setRefreshToken('mock_refresh_token') ->setValues(['scope' => 'email']); - $this->keycloakAdminClient->shouldReceive('getAdminAccessToken')->andReturn($this->adminAccessToken); + $this->keycloakAdminClient + ->shouldReceive('getAdminAccessToken') + ->andReturn($this->adminAccessToken); $this->usersService = new UsersService( $this->logger, - $this->keycloakAdminClient + $this->keycloakAdminClient, ); $reflection = new \ReflectionClass($this->usersService); @@ -83,7 +92,8 @@ public function testAll(): void { // given $realm = 'test-realm'; - $responseBody = '[{"id":"user1","username":"user1"},{"id":"user2","username":"user2"}]'; + $responseBody = + '[{"id":"user1","username":"user1"},{"id":"user2","username":"user2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -93,10 +103,15 @@ public function testAll(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users', m::on(function($options) { - return isset($options['headers']['Authorization']) && - $options['headers']['Authorization'] === 'Bearer mock_token'; - })) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users', + m::on(function ($options) { + return isset($options['headers']['Authorization']) + && 'Bearer mock_token' === + $options['headers']['Authorization']; + }), + ) ->andReturn($response); $userCollection = new UserCollection(); @@ -132,7 +147,8 @@ public function testAllWithCriteria(): void // given $criteria = new Criteria(['briefRepresentation' => 'true']); $realm = 'test-realm'; - $responseBody = '[{"id":"user1","username":"user1"},{"id":"user2","username":"user2"}]'; + $responseBody = + '[{"id":"user1","username":"user1"},{"id":"user2","username":"user2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -142,7 +158,11 @@ public function testAllWithCriteria(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users?briefRepresentation=true', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users?briefRepresentation=true', + m::type('array'), + ) ->andReturn($response); $userCollection = new UserCollection(); @@ -178,7 +198,8 @@ public function testGet(): void // given $realm = 'test-realm'; $userId = 'user1'; - $responseBody = '{"id":"user1","username":"user1","firstName":"John","lastName":"Doe"}'; + $responseBody = + '{"id":"user1","username":"user1","firstName":"John","lastName":"Doe"}'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -188,7 +209,11 @@ public function testGet(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId, m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users/'.$userId, + m::type('array'), + ) ->andReturn($response); $user = new UserRepresentation(); @@ -230,7 +255,11 @@ public function testCount(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/count', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users/count', + m::type('array'), + ) ->andReturn($response); $this->serializer @@ -244,6 +273,7 @@ public function testCount(): void $result = $this->usersService->count($realm); // then + // @phpstan-ignore method.alreadyNarrowedType $this->assertIsInt($result); $this->assertEquals(10, $result); } @@ -268,9 +298,13 @@ public function testCreate(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/users', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'.$realm.'/users', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -304,9 +338,13 @@ public function testUpdate(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/users/'.$userId, m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'PUT', + 'admin/realms/'.$realm.'/users/'.$userId, + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -333,7 +371,11 @@ public function testDelete(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/users/'.$userId, m::type('array')) + ->with( + 'DELETE', + 'admin/realms/'.$realm.'/users/'.$userId, + m::type('array'), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -381,7 +423,8 @@ public function testSessions(): void // given $realm = 'test-realm'; $userId = 'user1'; - $responseBody = '[{"id":"session1","userId":"user1"},{"id":"session2","userId":"user1"}]'; + $responseBody = + '[{"id":"session1","userId":"user1"},{"id":"session2","userId":"user1"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -391,7 +434,11 @@ public function testSessions(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/sessions', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users/'.$userId.'/sessions', + m::type('array'), + ) ->andReturn($response); $sessionCollection = new UserSessionCollection(); @@ -428,7 +475,8 @@ public function testOfflineSessions(): void $realm = 'test-realm'; $userId = 'user1'; $clientId = 'client1'; - $responseBody = '[{"id":"session1","userId":"user1"},{"id":"session2","userId":"user1"}]'; + $responseBody = + '[{"id":"session1","userId":"user1"},{"id":"session2","userId":"user1"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -438,7 +486,16 @@ public function testOfflineSessions(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/offline-sessions/'.$clientId, m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/offline-sessions/'. + $clientId, + m::type('array'), + ) ->andReturn($response); $sessionCollection = new UserSessionCollection(); @@ -459,7 +516,11 @@ public function testOfflineSessions(): void $this->logger->shouldReceive('info')->once(); // when - $result = $this->usersService->offlineSessions($realm, $userId, $clientId); + $result = $this->usersService->offlineSessions( + $realm, + $userId, + $clientId, + ); // then $this->assertInstanceOf(UserSessionCollection::class, $result); @@ -474,7 +535,8 @@ public function testGroups(): void // given $realm = 'test-realm'; $userId = 'user1'; - $responseBody = '[{"id":"group1","name":"group1"},{"id":"group2","name":"group2"}]'; + $responseBody = + '[{"id":"group1","name":"group1"},{"id":"group2","name":"group2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -484,7 +546,11 @@ public function testGroups(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/groups', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users/'.$userId.'/groups', + m::type('array'), + ) ->andReturn($response); $groupCollection = new GroupCollection(); @@ -530,7 +596,15 @@ public function testGroupsCount(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/groups/count', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/groups/count', + m::type('array'), + ) ->andReturn($response); $this->serializer @@ -544,6 +618,7 @@ public function testGroupsCount(): void $result = $this->usersService->groupsCount($realm, $userId); // then + // @phpstan-ignore method.alreadyNarrowedType $this->assertIsInt($result); $this->assertEquals(2, $result); } @@ -564,7 +639,16 @@ public function testJoinGroup(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/users/'.$userId.'/groups/'.$groupId, m::type('array')) + ->with( + 'PUT', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/groups/'. + $groupId, + m::type('array'), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -592,7 +676,16 @@ public function testLeaveGroup(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/users/'.$userId.'/groups/'.$groupId, m::type('array')) + ->with( + 'DELETE', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/groups/'. + $groupId, + m::type('array'), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -609,7 +702,8 @@ public function testRealmRoles(): void // given $realm = 'test-realm'; $userId = 'user1'; - $responseBody = '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; + $responseBody = + '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -619,7 +713,15 @@ public function testRealmRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm', + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -655,7 +757,8 @@ public function testAvailableRealmRoles(): void // given $realm = 'test-realm'; $userId = 'user1'; - $responseBody = '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; + $responseBody = + '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -665,7 +768,15 @@ public function testAvailableRealmRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm/available', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm/available', + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -715,9 +826,17 @@ public function testAddRealmRole(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -748,9 +867,17 @@ public function testRemoveRealmRole(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/realm', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'DELETE', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/realm', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -768,7 +895,8 @@ public function testClientRoles(): void $realm = 'test-realm'; $clientUuid = 'client1'; $userId = 'user1'; - $responseBody = '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; + $responseBody = + '[{"id":"role1","name":"role1"},{"id":"role2","name":"role2"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -778,7 +906,16 @@ public function testClientRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid, m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid, + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -799,7 +936,11 @@ public function testClientRoles(): void $this->logger->shouldReceive('info')->once(); // when - $result = $this->usersService->clientRoles($realm, $clientUuid, $userId); + $result = $this->usersService->clientRoles( + $realm, + $clientUuid, + $userId, + ); // then $this->assertInstanceOf(RoleCollection::class, $result); @@ -815,7 +956,8 @@ public function testAvailableClientRoles(): void $realm = 'test-realm'; $clientUuid = 'client1'; $userId = 'user1'; - $responseBody = '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; + $responseBody = + '[{"id":"role3","name":"role3"},{"id":"role4","name":"role4"}]'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -825,7 +967,17 @@ public function testAvailableClientRoles(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid.'/available', m::type('array')) + ->with( + 'GET', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid. + '/available', + m::type('array'), + ) ->andReturn($response); $roleCollection = new RoleCollection(); @@ -846,7 +998,11 @@ public function testAvailableClientRoles(): void $this->logger->shouldReceive('info')->once(); // when - $result = $this->usersService->availableClientRoles($realm, $clientUuid, $userId); + $result = $this->usersService->availableClientRoles( + $realm, + $clientUuid, + $userId, + ); // then $this->assertInstanceOf(RoleCollection::class, $result); @@ -876,15 +1032,29 @@ public function testAddClientRole(): void $this->httpClient ->shouldReceive('request') - ->with('POST', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid, m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'POST', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid, + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->usersService->addClientRole($realm, $clientUuid, $userId, $role); + $result = $this->usersService->addClientRole( + $realm, + $clientUuid, + $userId, + $role, + ); // then $this->assertTrue($result); @@ -910,15 +1080,29 @@ public function testRemoveClientRole(): void $this->httpClient ->shouldReceive('request') - ->with('DELETE', 'admin/realms/'.$realm.'/users/'.$userId.'/role-mappings/clients/'.$clientUuid, m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'DELETE', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/role-mappings/clients/'. + $clientUuid, + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->usersService->removeClientRole($realm, $clientUuid, $userId, $role); + $result = $this->usersService->removeClientRole( + $realm, + $clientUuid, + $userId, + $role, + ); // then $this->assertTrue($result); @@ -928,7 +1112,8 @@ public function testGetProfileConfig(): void { // given $realm = 'test-realm'; - $responseBody = '{"attributes":[{"name":"firstName","displayName":"First name"}]}'; + $responseBody = + '{"attributes":[{"name":"firstName","displayName":"First name"}]}'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -938,7 +1123,11 @@ public function testGetProfileConfig(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/profile', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users/profile', + m::type('array'), + ) ->andReturn($response); $config = new UPConfig(); @@ -962,7 +1151,8 @@ public function testGetProfileMetadata(): void { // given $realm = 'test-realm'; - $responseBody = '{"userProfileAttributeMetadata":[{"name":"firstName","displayName":"First name"}]}'; + $responseBody = + '{"userProfileAttributeMetadata":[{"name":"firstName","displayName":"First name"}]}'; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -972,7 +1162,11 @@ public function testGetProfileMetadata(): void $this->httpClient ->shouldReceive('request') - ->with('GET', 'admin/realms/'.$realm.'/users/profile/metadata', m::type('array')) + ->with( + 'GET', + 'admin/realms/'.$realm.'/users/profile/metadata', + m::type('array'), + ) ->andReturn($response); $metadata = new UserProfileMetadata(); @@ -1007,7 +1201,15 @@ public function testResetPassword(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/users/'.$userId.'/reset-password', m::type('array')) + ->with( + 'PUT', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/reset-password', + m::type('array'), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); @@ -1024,7 +1226,10 @@ public function testSendVerifyEmail(): void // given $realm = 'test-realm'; $userId = 'user1'; - $parameters = ['clientId' => 'client1', 'redirectUri' => 'http://example.com']; + $parameters = [ + 'clientId' => 'client1', + 'redirectUri' => 'http://example.com', + ]; $responseBody = ''; $stream = $this->createMock(StreamInterface::class); $stream->method('getContents')->willReturn($responseBody); @@ -1035,15 +1240,27 @@ public function testSendVerifyEmail(): void $this->httpClient ->shouldReceive('request') - ->with('PUT', 'admin/realms/'.$realm.'/users/'.$userId.'/send-verify-email', m::on(function($options) { - return isset($options['json']); - })) + ->with( + 'PUT', + 'admin/realms/'. + $realm. + '/users/'. + $userId. + '/send-verify-email', + m::on(function ($options) { + return isset($options['json']); + }), + ) ->andReturn($response); $this->logger->shouldReceive('info')->once(); // when - $result = $this->usersService->sendVerifyEmail($realm, $userId, $parameters); + $result = $this->usersService->sendVerifyEmail( + $realm, + $userId, + $parameters, + ); // then $this->assertTrue($result); diff --git a/tests/Token/JWKSTokenDecoderTest.php b/tests/Token/JWKSTokenDecoderTest.php index ea41a49..a6b9d9f 100644 --- a/tests/Token/JWKSTokenDecoderTest.php +++ b/tests/Token/JWKSTokenDecoderTest.php @@ -1,4 +1,5 @@ expectException(TokenDecoderException::class); $this->expectExceptionMessage('Invalid base_url format'); - new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'not-a-valid-url', - 'realm' => 'test-realm', - ]); + new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'not-a-valid-url', + 'realm' => 'test-realm', + ]); } public function testConstructorRejectsHttpForNonLocalhost(): void @@ -34,24 +33,20 @@ public function testConstructorRejectsHttpForNonLocalhost(): void $this->expectException(TokenDecoderException::class); $this->expectExceptionMessage('HTTP is only allowed for localhost'); - new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'http://keycloak.example.com', - 'realm' => 'test-realm', - ]); + new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'http://keycloak.example.com', + 'realm' => 'test-realm', + ]); } public function testConstructorAcceptsHttpForLocalhost(): void { $httpClient = $this->createMock(ClientInterface::class); - $decoder = new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'http://localhost:8080', - 'realm' => 'test-realm', - ]); + $decoder = new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'http://localhost:8080', + 'realm' => 'test-realm', + ]); $this->assertInstanceOf(JWKSTokenDecoder::class, $decoder); } @@ -63,12 +58,10 @@ public function testConstructorRejectsPrivateIpRanges(): void $this->expectException(TokenDecoderException::class); $this->expectExceptionMessage('is not allowed'); - new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'https://192.168.1.1', - 'realm' => 'test-realm', - ]); + new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'https://192.168.1.1', + 'realm' => 'test-realm', + ]); } public function testConstructorRejectsMetadataEndpoints(): void @@ -78,23 +71,20 @@ public function testConstructorRejectsMetadataEndpoints(): void $this->expectException(TokenDecoderException::class); $this->expectExceptionMessage('is not allowed'); - new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'https://169.254.169.254', - 'realm' => 'test-realm', - ]); + new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'https://169.254.169.254', + 'realm' => 'test-realm', + ]); } public function testConstructorAcceptsValidHttpsUrl(): void { $httpClient = $this->createMock(ClientInterface::class); - $decoder = new JWKSTokenDecoder( - $httpClient, [ - 'base_url' => 'https://keycloak.example.com', - 'realm' => 'test-realm', - ]); + $decoder = new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'https://keycloak.example.com', + 'realm' => 'test-realm', + ]); $this->assertInstanceOf(JWKSTokenDecoder::class, $decoder); } @@ -103,15 +93,16 @@ public function testFetchJwksValidatesDomainWhitelist(): void { $httpClient = $this->createMock(ClientInterface::class); - $decoder = new JWKSTokenDecoder( - $httpClient, [ - 'base_url' => 'https://keycloak.example.com', - 'realm' => 'test-realm', - 'allowed_jwks_domains' => ['different-domain.com'], - ]); + $decoder = new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'https://keycloak.example.com', + 'realm' => 'test-realm', + 'allowed_jwks_domains' => ['different-domain.com'], + ]); // Create a sample JWT token (doesn't need to be valid for this test) - $header = base64_encode(json_encode(['kid' => 'test-kid', 'alg' => 'RS256'])); + $header = base64_encode( + json_encode(['kid' => 'test-kid', 'alg' => 'RS256']), + ); $payload = base64_encode(json_encode(['sub' => 'test'])); $signature = base64_encode('test-signature'); $token = "$header.$payload.$signature"; @@ -119,8 +110,12 @@ public function testFetchJwksValidatesDomainWhitelist(): void try { $decoder->decode($token, ''); $this->fail('Expected TokenDecoderException to be thrown'); - } catch (TokenDecoderException $e) { - $this->assertStringContainsString('Security violation: JWKS URL host "keycloak.example.com" is not in the allowed domains whitelist', $e->getMessage()); + } + catch (TokenDecoderException $e) { + $this->assertStringContainsString( + 'Security violation: JWKS URL host "keycloak.example.com" is not in the allowed domains whitelist', + $e->getMessage(), + ); } } @@ -128,17 +123,22 @@ public function testFetchJwksAllowsBaseUrlDomainByDefault(): void { // Create a mock stream for the response body $stream = $this->createMock(StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode([ - 'keys' => [ + $stream->method('getContents')->willReturn( + json_encode( [ - 'kid' => 'test-kid', - 'use' => 'sig', - 'kty' => 'RSA', - 'n' => 'test-modulus', - 'e' => 'AQAB', + 'keys' => [ + [ + 'kid' => 'test-kid', + 'use' => 'sig', + 'kty' => 'RSA', + 'n' => 'test-modulus', + 'e' => 'AQAB', + ], + ], ], - ], - ], JSON_THROW_ON_ERROR)); + JSON_THROW_ON_ERROR, + ), + ); // Create a mock response $response = $this->createMock(Response::class); @@ -148,13 +148,11 @@ public function testFetchJwksAllowsBaseUrlDomainByDefault(): void $httpClient = $this->createMock(ClientInterface::class); $httpClient->method('request')->willReturn($response); - $decoder = new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'https://keycloak.example.com', - 'realm' => 'test-realm', - // No allowed_jwks_domains specified - should default to base_url domain - ]); + $decoder = new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'https://keycloak.example.com', + 'realm' => 'test-realm', + // No allowed_jwks_domains specified - should default to base_url domain + ]); // This should not throw an exception because the JWKS URL uses the same domain as base_url $this->assertInstanceOf(JWKSTokenDecoder::class, $decoder); @@ -164,17 +162,22 @@ public function testWildcardDomainMatching(): void { // Create a mock stream for the response body $stream = $this->createMock(StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode([ - 'keys' => [ + $stream->method('getContents')->willReturn( + json_encode( [ - 'kid' => 'test-kid', - 'use' => 'sig', - 'kty' => 'RSA', - 'n' => 'test-modulus', - 'e' => 'AQAB', + 'keys' => [ + [ + 'kid' => 'test-kid', + 'use' => 'sig', + 'kty' => 'RSA', + 'n' => 'test-modulus', + 'e' => 'AQAB', + ], + ], ], - ], - ], JSON_THROW_ON_ERROR)); + JSON_THROW_ON_ERROR, + ), + ); // Create a mock response $response = $this->createMock(Response::class); @@ -184,13 +187,11 @@ public function testWildcardDomainMatching(): void $httpClient = $this->createMock(ClientInterface::class); $httpClient->method('request')->willReturn($response); - $decoder = new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'https://auth.example.com', - 'realm' => 'test-realm', - 'allowed_jwks_domains' => ['*.example.com'], - ]); + $decoder = new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'https://auth.example.com', + 'realm' => 'test-realm', + 'allowed_jwks_domains' => ['*.example.com'], + ]); // This should not throw an exception because auth.example.com matches *.example.com $this->assertInstanceOf(JWKSTokenDecoder::class, $decoder); @@ -208,12 +209,10 @@ public function testJwksUrlValidationRejectsHttpForNonLocalhost(): void $this->expectExceptionMessage('HTTP is only allowed for localhost.'); // Constructing the decoder with an HTTP non-localhost base URL should fail validation - $decoder = new JWKSTokenDecoder( - $httpClient, - [ - 'base_url' => 'http://keycloak.example.com', - 'realm' => 'test-realm', - ]); + $decoder = new JWKSTokenDecoder($httpClient, [ + 'base_url' => 'http://keycloak.example.com', + 'realm' => 'test-realm', + ]); // Use reflection to modify the base_url to an HTTP non-localhost domain // This simulates a scenario where the JWKS URL would be HTTP for a non-localhost host @@ -225,21 +224,30 @@ public function testJwksUrlValidationRejectsHttpForNonLocalhost(): void $optionsProperty->setValue($decoder, $options); // Create a JWT token with proper structure - $header = json_encode([ - 'kid' => 'test-key-id', - 'alg' => 'RS256', - 'typ' => 'JWT', - ], JSON_THROW_ON_ERROR); - $payload = json_encode([ - 'sub' => 'test-user', - 'exp' => time() + 3600, - 'iat' => time(), - 'iss' => 'http://keycloak.example.com/auth/realms/test-realm', - ], JSON_THROW_ON_ERROR); + $header = json_encode( + [ + 'kid' => 'test-key-id', + 'alg' => 'RS256', + 'typ' => 'JWT', + ], + JSON_THROW_ON_ERROR, + ); + $payload = json_encode( + [ + 'sub' => 'test-user', + 'exp' => time() + 3600, + 'iat' => time(), + 'iss' => 'http://keycloak.example.com/auth/realms/test-realm', + ], + JSON_THROW_ON_ERROR, + ); // Base64url encode the token parts $headerEncoded = rtrim(strtr(base64_encode($header), '+/', '-_'), '='); - $payloadEncoded = rtrim(strtr(base64_encode($payload), '+/', '-_'), '='); + $payloadEncoded = rtrim( + strtr(base64_encode($payload), '+/', '-_'), + '=', + ); $token = "$headerEncoded.$payloadEncoded.fake-signature"; $this->expectException(TokenDecoderException::class); @@ -252,17 +260,26 @@ public function testJwksUrlValidationRejectsHttpForNonLocalhost(): void public function testDecodeThrowsExceptionForMissingKid(): void { // Create token without kid in header - $header = json_encode([ - 'alg' => 'RS256', - 'typ' => 'JWT', - ], JSON_THROW_ON_ERROR); - $payload = json_encode([ - 'sub' => 'test-user', - 'exp' => time() + 3600, - ], JSON_THROW_ON_ERROR); + $header = json_encode( + [ + 'alg' => 'RS256', + 'typ' => 'JWT', + ], + JSON_THROW_ON_ERROR, + ); + $payload = json_encode( + [ + 'sub' => 'test-user', + 'exp' => time() + 3600, + ], + JSON_THROW_ON_ERROR, + ); $headerEncoded = rtrim(strtr(base64_encode($header), '+/', '-_'), '='); - $payloadEncoded = rtrim(strtr(base64_encode($payload), '+/', '-_'), '='); + $payloadEncoded = rtrim( + strtr(base64_encode($payload), '+/', '-_'), + '=', + ); $token = "$headerEncoded.$payloadEncoded.fake-signature"; $httpClient = $this->createMock(ClientInterface::class); @@ -281,18 +298,27 @@ public function testDecodeThrowsExceptionForMissingKid(): void public function testDecodeThrowsExceptionForAlgorithmMismatch(): void { // Create token with HS256 algorithm - $header = json_encode([ - 'kid' => 'test-kid', - 'alg' => 'HS256', - 'typ' => 'JWT', - ], JSON_THROW_ON_ERROR); - $payload = json_encode([ - 'sub' => 'test-user', - 'exp' => time() + 3600, - ], JSON_THROW_ON_ERROR); + $header = json_encode( + [ + 'kid' => 'test-kid', + 'alg' => 'HS256', + 'typ' => 'JWT', + ], + JSON_THROW_ON_ERROR, + ); + $payload = json_encode( + [ + 'sub' => 'test-user', + 'exp' => time() + 3600, + ], + JSON_THROW_ON_ERROR, + ); $headerEncoded = rtrim(strtr(base64_encode($header), '+/', '-_'), '='); - $payloadEncoded = rtrim(strtr(base64_encode($payload), '+/', '-_'), '='); + $payloadEncoded = rtrim( + strtr(base64_encode($payload), '+/', '-_'), + '=', + ); $token = "$headerEncoded.$payloadEncoded.fake-signature"; $httpClient = $this->createMock(ClientInterface::class); @@ -304,7 +330,9 @@ public function testDecodeThrowsExceptionForAlgorithmMismatch(): void ]); $this->expectException(TokenDecoderException::class); - $this->expectExceptionMessage('Token algorithm "HS256" does not match expected algorithm "RS256"'); + $this->expectExceptionMessage( + 'Token algorithm "HS256" does not match expected algorithm "RS256"', + ); $decoder->decode($token, ''); } @@ -312,19 +340,28 @@ public function testDecodeThrowsExceptionForAlgorithmMismatch(): void public function testDecodeThrowsExceptionForKidNotFoundInJwks(): void { // Create token with kid that doesn't exist in JWKS - $header = json_encode([ - 'kid' => 'non-existent-kid', - 'alg' => 'RS256', - 'typ' => 'JWT', - ], JSON_THROW_ON_ERROR); - $payload = json_encode([ - 'sub' => 'test-user', - 'exp' => time() + 3600, - 'iss' => 'https://keycloak.example.com/realms/test-realm', - ], JSON_THROW_ON_ERROR); + $header = json_encode( + [ + 'kid' => 'non-existent-kid', + 'alg' => 'RS256', + 'typ' => 'JWT', + ], + JSON_THROW_ON_ERROR, + ); + $payload = json_encode( + [ + 'sub' => 'test-user', + 'exp' => time() + 3600, + 'iss' => 'https://keycloak.example.com/realms/test-realm', + ], + JSON_THROW_ON_ERROR, + ); $headerEncoded = rtrim(strtr(base64_encode($header), '+/', '-_'), '='); - $payloadEncoded = rtrim(strtr(base64_encode($payload), '+/', '-_'), '='); + $payloadEncoded = rtrim( + strtr(base64_encode($payload), '+/', '-_'), + '=', + ); $token = "$headerEncoded.$payloadEncoded.fake-signature"; // Mock JWKS with different kid @@ -341,7 +378,9 @@ public function testDecodeThrowsExceptionForKidNotFoundInJwks(): void ]; $stream = $this->createMock(StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode($jwksData, JSON_THROW_ON_ERROR)); + $stream + ->method('getContents') + ->willReturn(json_encode($jwksData, JSON_THROW_ON_ERROR)); $response = $this->createMock(Response::class); $response->method('getBody')->willReturn($stream); @@ -355,7 +394,9 @@ public function testDecodeThrowsExceptionForKidNotFoundInJwks(): void ]); $this->expectException(TokenDecoderException::class); - $this->expectExceptionMessage('No matching signing key found for kid: non-existent-kid'); + $this->expectExceptionMessage( + 'No matching signing key found for kid: non-existent-kid', + ); $decoder->decode($token, ''); } @@ -421,7 +462,7 @@ public function testValidateTokenAcceptsValidToken(): void $decoder->validateToken('test-realm', $validToken); // If we get here without exception, the test passes - $this->assertTrue(true); + $this->assertTrue(true); // @phpstan-ignore method.alreadyNarrowedType } public function testDecodeThrowsExceptionForInvalidJwtFormat(): void @@ -434,7 +475,9 @@ public function testDecodeThrowsExceptionForInvalidJwtFormat(): void ]); $this->expectException(TokenDecoderException::class); - $this->expectExceptionMessage('Invalid JWT format: token must consist of header.payload.signature'); + $this->expectExceptionMessage( + 'Invalid JWT format: token must consist of header.payload.signature', + ); // Invalid token with only 2 parts $decoder->decode('invalid.token', ''); @@ -443,25 +486,36 @@ public function testDecodeThrowsExceptionForInvalidJwtFormat(): void public function testDecodeThrowsExceptionForEmptyJwksKeys(): void { // Create token - $header = json_encode([ - 'kid' => 'test-kid', - 'alg' => 'RS256', - 'typ' => 'JWT', - ], JSON_THROW_ON_ERROR); - $payload = json_encode([ - 'sub' => 'test-user', - 'exp' => time() + 3600, - ], JSON_THROW_ON_ERROR); + $header = json_encode( + [ + 'kid' => 'test-kid', + 'alg' => 'RS256', + 'typ' => 'JWT', + ], + JSON_THROW_ON_ERROR, + ); + $payload = json_encode( + [ + 'sub' => 'test-user', + 'exp' => time() + 3600, + ], + JSON_THROW_ON_ERROR, + ); $headerEncoded = rtrim(strtr(base64_encode($header), '+/', '-_'), '='); - $payloadEncoded = rtrim(strtr(base64_encode($payload), '+/', '-_'), '='); + $payloadEncoded = rtrim( + strtr(base64_encode($payload), '+/', '-_'), + '=', + ); $token = "$headerEncoded.$payloadEncoded.fake-signature"; // Mock JWKS with empty keys array $jwksData = ['keys' => []]; $stream = $this->createMock(StreamInterface::class); - $stream->method('getContents')->willReturn(json_encode($jwksData, JSON_THROW_ON_ERROR)); + $stream + ->method('getContents') + ->willReturn(json_encode($jwksData, JSON_THROW_ON_ERROR)); $response = $this->createMock(Response::class); $response->method('getBody')->willReturn($stream); @@ -480,4 +534,3 @@ public function testDecodeThrowsExceptionForEmptyJwksKeys(): void $decoder->decode($token, ''); } } -