From 3310616570444198cbc2a2a9567cb68e64510e38 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:03:55 +0530 Subject: [PATCH 01/15] Extract scoped resource container from http --- README.md | 28 ++++-- src/DI/Container.php | 197 ++++++++++++++++++++++++++++++---------- src/DI/Resource.php | 7 ++ tests/ContainerTest.php | 93 +++++++++++++++++-- 4 files changed, 263 insertions(+), 62 deletions(-) create mode 100644 src/DI/Resource.php diff --git a/README.md b/README.md index ef48f2e..94005a1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/http.svg) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://discord.gg/GSeTUeA) -Utopia DI library is simple and lite library for managing dependency injections. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io). +Utopia DI is a small dependency injection container with scoped resources. It is designed to stay simple while still covering the resource lifecycle used across the Utopia libraries. This library is maintained by the [Appwrite team](https://appwrite.io). Although this library is part of the Utopia Framework project it is dependency free, and can be used as standalone with any other PHP project or framework. @@ -15,25 +15,39 @@ Although this library is part of the Utopia Framework project it is dependency f Install using Composer: ```bash -composer require utopia-php/http +composer require utopia-php/di ``` - ```php require_once __DIR__.'/../vendor/autoload.php'; use Utopia\DI\Container; -TBD +$di = new Container(); + +$di->setResource('config', fn () => ['region' => 'eu-west-1']); +$di->setResource('db', fn (array $config) => new PDO(...), ['config']); + +$db = $di->getResource('db', 'request-1'); +``` + +Resources are cached per context. Definitions registered in the default `utopia` context are automatically available in other contexts, while context-specific definitions override the default for that request. + +```php +$di->setResource('request-id', fn () => 'req-1', context: 'request-1'); + +$requestResources = $di->getResources(['db', 'request-id'], 'request-1'); + +$di->purge('request-1'); ``` ## System Requirements -Utopia HTTP requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible. +Utopia DI requires PHP 8.2 or later. We recommend using the latest PHP version whenever possible. ## More from Utopia -Our ecosystem supports other thin PHP projects aiming to extend the core PHP Utopia HTTP. +Our ecosystem supports other thin PHP projects aiming to extend the core PHP Utopia libraries. Each project is focused on solving a single, very simple problem and you can use composer to include any of them in your next project. @@ -45,7 +59,7 @@ All code contributions - including those of people having commit access - must g Fork the project, create a feature branch, and send us a pull request. -You can refer to the [Contributing Guide](https://github.com/utopia-php/http/blob/master/CONTRIBUTING.md) for more info. +You can refer to the [Contributing Guide](https://github.com/utopia-php/di/blob/master/CONTRIBUTING.md) for more info. For security issues, please email security@appwrite.io instead of posting a public issue in GitHub. diff --git a/src/DI/Container.php b/src/DI/Container.php index fe4a607..f791b6d 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -6,127 +6,230 @@ class Container { + public const DEFAULT_CONTEXT = 'utopia'; + /** - * @var array + * @var array> */ protected array $dependencies = []; /** - * @var array + * @var array> */ protected array $instances = []; - public function __construct() - { - $di = new Dependency(); - $di->setName('di'); - $di->setCallback(function () { - return $this; - }); - $this->dependencies[$di->getName()] = $di; - } - /** * Set a dependency. * * @param Dependency|Injection $dependency + * @param string $context * @return self * * @throws Exception */ - public function set(Dependency|Injection $dependency): self + public function set(Dependency|Injection $dependency, string $context = self::DEFAULT_CONTEXT): self { if ($dependency->getName() === 'di') { throw new Exception("'di' is a reserved keyword."); } - if (\array_key_exists($dependency->getName(), $this->instances)) { - unset($this->instances[$dependency->getName()]); - } + $this->dependencies[$context] ??= []; + $this->instances[$context] ??= []; - $this->dependencies[$dependency->getName()] = $dependency; + unset($this->instances[$context][$dependency->getName()]); + $this->dependencies[$context][$dependency->getName()] = $dependency; return $this; } /** - * Get a dependency. + * Register a callable resource. * * @param string $name + * @param callable $callback + * @param string[] $dependencies + * @param string $context + * @return self + */ + public function setResource(string $name, callable $callback, array $dependencies = [], string $context = self::DEFAULT_CONTEXT): self + { + $resource = new Resource(); + $resource + ->setName($name) + ->setCallback($callback) + ; + + foreach ($dependencies as $dependency) { + $resource->inject($dependency); + } + + return $this->set($resource, $context); + } + + /** + * Get a dependency. * + * @param string $name + * @param string $context + * @param bool $fresh * @return mixed + * + * @throws Exception */ - public function get(string $name): mixed + public function get(string $name, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed { - if (!\array_key_exists($name, $this->dependencies)) { - throw new Exception('Failed to find dependency: "' . $name . '"'); + if ($name === 'di') { + return $this; } - return $this->inject($this->dependencies[$name]); + $injection = $this->getDefinition($name, $context); + + return $this->inject($injection, $context, $fresh); } /** - * Check if a dependency exists. + * Alias for get(). * * @param string $name + * @param string $context + * @param bool $fresh + * @return mixed * + * @throws Exception + */ + public function getResource(string $name, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed + { + return $this->get($name, $context, $fresh); + } + + /** + * Resolve multiple dependencies for a context. + * + * @param string[] $names + * @param string $context + * @return array + * + * @throws Exception + */ + public function getResources(array $names, string $context = self::DEFAULT_CONTEXT): array + { + $resources = []; + + foreach ($names as $name) { + $resources[$name] = $this->get($name, $context); + } + + return $resources; + } + + /** + * Check if a dependency exists in the current or default context. + * + * @param string $name + * @param string $context * @return bool */ - public function has(string $name): bool + public function has(string $name, string $context = self::DEFAULT_CONTEXT): bool { - return \array_key_exists($name, $this->dependencies); + if ($name === 'di') { + return true; + } + + return isset($this->dependencies[$context][$name]) || isset($this->dependencies[self::DEFAULT_CONTEXT][$name]); } /** * Resolve the dependencies of a given injection. * * @param Injection $injection + * @param string $context * @param bool $fresh - * * @return mixed + * + * @throws Exception */ - public function inject(Injection $injection, bool $fresh = false): mixed // Route + public function inject(Injection $injection, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed { - if (\array_key_exists($injection->getName(), $this->instances) && !$fresh) { - return $this->instances[$injection->getName()]; + $this->instances[$context] ??= []; + + if (\array_key_exists($injection->getName(), $this->instances[$context]) && !$fresh) { + return $this->instances[$context][$injection->getName()]; } $arguments = []; foreach ($injection->getDependencies() as $dependency) { + $arguments[] = $this->get($dependency, $context); + } - if (\array_key_exists($dependency, $this->instances)) { - $arguments[] = $this->instances[$dependency]; - continue; - } - - if (!\array_key_exists($dependency, $this->dependencies)) { - throw new Exception('Failed to find dependency: "' . $dependency . '"'); - } + $resolved = \call_user_func_array($injection->getCallback(), $arguments); + $this->instances[$context][$injection->getName()] = $resolved; - $arguments[] = $this->get($dependency); + return $resolved; + } + /** + * Refresh a dependency instance. + * + * @param string $name + * @param string|null $context + * @return self + */ + public function refresh(string $name, ?string $context = null): self + { + if ($name === 'di') { + return $this; } - $resolved = \call_user_func_array($injection->getCallback(), $arguments); + if ($context !== null) { + unset($this->instances[$context][$name]); + + return $this; + } - $this->instances[$injection->getName()] = $resolved; + foreach (\array_keys($this->instances) as $instanceContext) { + unset($this->instances[$instanceContext][$name]); + } - return $resolved; + return $this; } + /** - * Refresh a dependency + * Remove context-specific registrations and cached instances. * - * @param string $name + * @param string $context * @return self - * @throws Exception */ - public function refresh(string $name): self + public function purge(string $context): self { - if(\array_key_exists($name, $this->instances)) { - unset($this->instances[$name]); + if ($context === self::DEFAULT_CONTEXT) { + $this->instances[$context] = []; + + return $this; } + unset($this->dependencies[$context], $this->instances[$context]); + return $this; } + + /** + * @param string $name + * @param string $context + * @return Injection + * + * @throws Exception + */ + protected function getDefinition(string $name, string $context): Injection + { + if (isset($this->dependencies[$context][$name])) { + return $this->dependencies[$context][$name]; + } + + if (isset($this->dependencies[self::DEFAULT_CONTEXT][$name])) { + return $this->dependencies[self::DEFAULT_CONTEXT][$name]; + } + + throw new Exception('Failed to find dependency: "' . $name . '"'); + } } diff --git a/src/DI/Resource.php b/src/DI/Resource.php new file mode 100644 index 0000000..eeb7987 --- /dev/null +++ b/src/DI/Resource.php @@ -0,0 +1,7 @@ +setName('user') ->inject('age') - ->setCallback(fn ($age) => 'John Doe is '.$age.' years old.'); + ->setCallback(fn (int $age) => 'John Doe is '.$age.' years old.') ; $age = new Dependency(); $age ->setName('age') - ->setCallback(fn () => 25); + ->setCallback(fn () => 25) ; $this->container @@ -36,8 +33,88 @@ public function setUp(): void ; } - public function testResolution() + public function tearDown(): void { - $this->assertEquals('John Doe is 25 years old.', $this->container->get('user')); + $this->container = null; + } + + public function testResolution(): void + { + $this->assertSame('John Doe is 25 years old.', $this->container->get('user')); + } + + public function testCanResolveResourcesWithDependenciesAndPerContextCache(): void + { + $counter = 0; + + $this->container + ->setResource('counter', function () use (&$counter) { + $counter++; + + return $counter; + }) + ->setResource('message', fn (int $counter) => "counter-{$counter}", ['counter']) + ; + + $this->assertSame(1, $this->container->getResource('counter', 'request-1')); + $this->assertSame(1, $this->container->getResource('counter', 'request-1')); + $this->assertSame('counter-1', $this->container->getResource('message', 'request-1')); + + $this->assertSame(2, $this->container->getResource('counter', 'request-2')); + $this->assertSame('counter-2', $this->container->getResource('message', 'request-2')); + } + + public function testContextSpecificDefinitionsOverrideDefaultContext(): void + { + $this->container + ->setResource('greeting', fn () => 'hello from default') + ->setResource('greeting', fn () => 'hello from request-1', context: 'request-1') + ; + + $this->assertSame('hello from request-1', $this->container->getResource('greeting', 'request-1')); + $this->assertSame('hello from default', $this->container->getResource('greeting', 'request-2')); + } + + public function testCanResolveResourceListsAndContainerReference(): void + { + $this->container + ->setResource('name', fn () => 'utopia') + ->setResource( + 'summary', + fn (string $name, Container $di) => "{$name}-".($di === $this->container ? 'same' : 'different'), + ['name', 'di'] + ) + ; + + $resources = $this->container->getResources(['name', 'summary'], 'request-1'); + + $this->assertSame('utopia', $resources['name']); + $this->assertSame('utopia-same', $resources['summary']); + } + + public function testCanRefreshAndPurgeContexts(): void + { + $counter = 0; + + $this->container + ->setResource('counter', function () use (&$counter) { + $counter++; + + return $counter; + }) + ->setResource('request-id', fn () => 'request-1', context: 'request-1') + ; + + $this->assertSame(1, $this->container->getResource('counter', 'request-1')); + + $this->container->refresh('counter', 'request-1'); + + $this->assertSame(2, $this->container->getResource('counter', 'request-1')); + $this->assertTrue($this->container->has('request-id', 'request-1')); + + $this->container->purge('request-1'); + + $this->assertFalse($this->container->has('request-id', 'request-1')); + $this->assertSame(3, $this->container->getResource('counter', 'request-1')); } } From 300bf2ebe1d0bd1fa1feae3300f5386040f1c278 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:16:50 +0530 Subject: [PATCH 02/15] upgrade: phpstan and pint --- .github/workflows/ci.yml | 77 +++++++++++++++++++++++++++ .github/workflows/codeql-analysis.yml | 17 ------ .github/workflows/lint.yml | 16 ------ .github/workflows/test.yml | 28 ---------- composer.json | 10 ++-- composer.lock | 48 ++++++++--------- phpstan.neon | 2 +- 7 files changed, 105 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/lint.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..154e0a1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: "CI" + +on: + pull_request: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run Analyze + run: composer analyze + + format: + name: Format + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run Format + run: composer format:check + + tests: + name: Tests + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ["8.2", "8.3", "nightly"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP ${{ matrix.php-versions }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run Tests + run: composer test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 126ef74..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "CodeQL" - -on: [pull_request] -jobs: - lint: - name: CodeQL - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Run CodeQL - run: | - docker run --rm -v $PWD:/app composer sh -c \ - "composer install --profile --ignore-platform-reqs && composer check" - \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 864d317..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Linter" - -on: [pull_request] -jobs: - lint: - name: Linter - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Run Linter - run: | - docker run --rm -v $PWD:/app composer sh -c \ - "composer install --profile --ignore-platform-reqs && composer lint" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 06837d1..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Tests" - -on: [pull_request] -jobs: - lint: - name: Tests - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: ['8.2', '8.3', 'nightly'] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup PHP ${{ matrix.php-versions }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Install dependencies - run: composer install --no-progress --no-suggest - - - name: Run Tests - run: vendor/bin/phpunit --configuration phpunit.xml \ No newline at end of file diff --git a/composer.json b/composer.json index 7c412d3..6bbb37c 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "php", "framework", "http", - "upf" + "utopia" ], "license": "MIT", "autoload": { @@ -16,9 +16,9 @@ } }, "scripts": { - "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "check": "vendor/bin/phpstan analyse -c phpstan.neon", + "format:check": "vendor/bin/pint --test", + "analyze": "vendor/bin/phpstan analyse -c phpstan.neon", "test": "vendor/bin/phpunit --configuration phpunit.xml" }, "require": { @@ -26,9 +26,9 @@ }, "require-dev": { "phpunit/phpunit": "^9.5.25", - "laravel/pint": "^1.2", + "laravel/pint": "^1.27", "swoole/ide-helper": "4.8.3", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.1", "phpbench/phpbench": "^1.2" } } diff --git a/composer.lock b/composer.lock index d5c5fc5..9e78a20 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a9214c1b1e10d26dde2c89b8b0718584", + "content-hash": "032ab9eeb6b2176d02994ba3905364e8", "packages": [], "packages-dev": [ { @@ -232,16 +232,16 @@ }, { "name": "laravel/pint", - "version": "v1.15.1", + "version": "v1.27.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5f288b5e79938cc72f5c298d384e639de87507c6" + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5f288b5e79938cc72f5c298d384e639de87507c6", - "reference": "5f288b5e79938cc72f5c298d384e639de87507c6", + "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", "shasum": "" }, "require": { @@ -249,16 +249,16 @@ "ext-mbstring": "*", "ext-tokenizer": "*", "ext-xml": "*", - "php": "^8.1.0" + "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.52.1", - "illuminate/view": "^10.48.4", - "larastan/larastan": "^2.9.2", - "laravel-zero/framework": "^10.3.0", - "mockery/mockery": "^1.6.11", - "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.5" + "friendsofphp/php-cs-fixer": "^3.93.1", + "illuminate/view": "^12.51.0", + "larastan/larastan": "^3.9.2", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.5" }, "bin": [ "builds/pint" @@ -284,6 +284,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -294,7 +295,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-04-02T14:28:47+00:00" + "time": "2026-02-10T20:00:20+00:00" }, { "name": "myclabs/deep-copy", @@ -733,20 +734,15 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.67", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" - }, + "version": "2.1.40", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -787,7 +783,7 @@ "type": "github" } ], - "time": "2024-04-16T07:22:02+00:00" + "time": "2026-02-23T15:04:35+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3435,12 +3431,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.2" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/phpstan.neon b/phpstan.neon index d275a39..d54e3b0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,4 +4,4 @@ parameters: level: 5 paths: - src - - tests \ No newline at end of file + - tests From 42934a42d8569d226154a0b11dd4230655dbec0d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:17:25 +0530 Subject: [PATCH 03/15] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94005a1..bffc5ea 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ use Utopia\DI\Container; $di = new Container(); $di->setResource('config', fn () => ['region' => 'eu-west-1']); -$di->setResource('db', fn (array $config) => new PDO(...), ['config']); +$di->setResource('db', fn (array $config) => new PDO('dsn', 'username', 'password'), ['config']); $db = $di->getResource('db', 'request-1'); ``` From a477152b0c428f55c9dc4b07a8e4478f756432b2 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:45:58 +0530 Subject: [PATCH 04/15] Simplify DI types around Resource --- src/DI/Container.php | 20 ++++----- src/DI/Dependency.php | 7 ---- src/DI/Injection.php | 91 ----------------------------------------- src/DI/Resource.php | 86 +++++++++++++++++++++++++++++++++++++- tests/ContainerTest.php | 6 +-- 5 files changed, 98 insertions(+), 112 deletions(-) delete mode 100644 src/DI/Dependency.php delete mode 100644 src/DI/Injection.php diff --git a/src/DI/Container.php b/src/DI/Container.php index f791b6d..a86b69d 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -9,7 +9,7 @@ class Container public const DEFAULT_CONTEXT = 'utopia'; /** - * @var array> + * @var array> */ protected array $dependencies = []; @@ -21,13 +21,13 @@ class Container /** * Set a dependency. * - * @param Dependency|Injection $dependency + * @param Resource $dependency * @param string $context * @return self * * @throws Exception */ - public function set(Dependency|Injection $dependency, string $context = self::DEFAULT_CONTEXT): self + public function set(Resource $dependency, string $context = self::DEFAULT_CONTEXT): self { if ($dependency->getName() === 'di') { throw new Exception("'di' is a reserved keyword."); @@ -67,7 +67,7 @@ public function setResource(string $name, callable $callback, array $dependencie } /** - * Get a dependency. + * Get a resource. * * @param string $name * @param string $context @@ -123,7 +123,7 @@ public function getResources(array $names, string $context = self::DEFAULT_CONTE } /** - * Check if a dependency exists in the current or default context. + * Check if a resource exists in the current or default context. * * @param string $name * @param string $context @@ -139,16 +139,16 @@ public function has(string $name, string $context = self::DEFAULT_CONTEXT): bool } /** - * Resolve the dependencies of a given injection. + * Resolve the dependencies of a given resource. * - * @param Injection $injection + * @param Resource $injection * @param string $context * @param bool $fresh * @return mixed * * @throws Exception */ - public function inject(Injection $injection, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed + public function inject(Resource $injection, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed { $this->instances[$context] ??= []; @@ -216,11 +216,11 @@ public function purge(string $context): self /** * @param string $name * @param string $context - * @return Injection + * @return Resource * * @throws Exception */ - protected function getDefinition(string $name, string $context): Injection + protected function getDefinition(string $name, string $context): Resource { if (isset($this->dependencies[$context][$name])) { return $this->dependencies[$context][$name]; diff --git a/src/DI/Dependency.php b/src/DI/Dependency.php deleted file mode 100644 index 906a629..0000000 --- a/src/DI/Dependency.php +++ /dev/null @@ -1,7 +0,0 @@ -name = $name; - return $this; - } - - /** - * Get the value of name - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Set Callback - * - * @param mixed $callback - * @return self - */ - public function setCallback(mixed $callback): self - { - $this->callback = $callback; - - return $this; - } - - /** - * Get the value of callback - * - * @return mixed - */ - public function getCallback(): mixed - { - return $this->callback; - } - - /** - * Get the value of dependencies - * - * @return string[] - */ - public function getDependencies(): array - { - return $this->dependencies; - } - - /** - * Depenedency - * - * @param string $name - * @return self - * - * @throws Exception - */ - public function inject(string $name): self - { - if (array_key_exists($name, $this->dependencies)) { - throw new Exception('Dependency already declared for '.$name); - } - - $this->dependencies[] = $name; - - return $this; - } -} diff --git a/src/DI/Resource.php b/src/DI/Resource.php index eeb7987..8d0af3c 100644 --- a/src/DI/Resource.php +++ b/src/DI/Resource.php @@ -2,6 +2,90 @@ namespace Utopia\DI; -class Resource extends Dependency +use Exception; + +class Resource { + protected array $dependencies = []; + protected $callback; + + /** + * @var string + */ + protected string $name = ''; + + /** + * Set the value of name + * + * @param string $name + * + * @return self + */ + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + /** + * Get the value of name + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Set Callback + * + * @param mixed $callback + * @return self + */ + public function setCallback(mixed $callback): self + { + $this->callback = $callback; + + return $this; + } + + /** + * Get the value of callback + * + * @return mixed + */ + public function getCallback(): mixed + { + return $this->callback; + } + + /** + * Get the value of dependencies + * + * @return string[] + */ + public function getDependencies(): array + { + return $this->dependencies; + } + + /** + * Depenedency + * + * @param string $name + * @return self + * + * @throws Exception + */ + public function inject(string $name): self + { + if (array_key_exists($name, $this->dependencies)) { + throw new Exception('Dependency already declared for '.$name); + } + + $this->dependencies[] = $name; + + return $this; + } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 537134f..8b67f1e 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase; use Utopia\DI\Container; -use Utopia\DI\Dependency; +use Utopia\DI\Resource; class ContainerTest extends TestCase { @@ -14,14 +14,14 @@ public function setUp(): void { $this->container = new Container(); - $user = new Dependency(); + $user = new Resource(); $user ->setName('user') ->inject('age') ->setCallback(fn (int $age) => 'John Doe is '.$age.' years old.') ; - $age = new Dependency(); + $age = new Resource(); $age ->setName('age') ->setCallback(fn () => 25) From 1989f5f9e477c8ec925c64ca6ea22e2115d95ee8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:59:39 +0530 Subject: [PATCH 05/15] Fix README config resource example --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bffc5ea..0957589 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,16 @@ use Utopia\DI\Container; $di = new Container(); -$di->setResource('config', fn () => ['region' => 'eu-west-1']); -$di->setResource('db', fn (array $config) => new PDO('dsn', 'username', 'password'), ['config']); +$di->setResource('config', fn () => [ + 'dsn' => 'mysql:host=localhost;dbname=app', + 'username' => 'root', + 'password' => 'secret', +]); +$di->setResource( + 'db', + fn (array $config) => new PDO($config['dsn'], $config['username'], $config['password']), + ['config'] +); $db = $di->getResource('db', 'request-1'); ``` From 0c7671d69ba95d3f7801d4a993ec6b6c78a4cf9b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 12:45:52 +0530 Subject: [PATCH 06/15] Invalidate default resource caches across contexts --- src/DI/Container.php | 7 ++++++- tests/ContainerTest.php | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/DI/Container.php b/src/DI/Container.php index a86b69d..7a8d185 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -36,7 +36,12 @@ public function set(Resource $dependency, string $context = self::DEFAULT_CONTEX $this->dependencies[$context] ??= []; $this->instances[$context] ??= []; - unset($this->instances[$context][$dependency->getName()]); + if ($context === self::DEFAULT_CONTEXT) { + $this->refresh($dependency->getName()); + } else { + unset($this->instances[$context][$dependency->getName()]); + } + $this->dependencies[$context][$dependency->getName()] = $dependency; return $this; diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 8b67f1e..94587ef 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -117,4 +117,17 @@ public function testCanRefreshAndPurgeContexts(): void $this->assertFalse($this->container->has('request-id', 'request-1')); $this->assertSame(3, $this->container->getResource('counter', 'request-1')); } + + public function testUpdatingDefaultContextInvalidatesCachedInstancesAcrossContexts(): void + { + $this->container->setResource('config', fn () => 'v1'); + + $this->assertSame('v1', $this->container->getResource('config', 'request-1')); + $this->assertSame('v1', $this->container->getResource('config', 'request-2')); + + $this->container->setResource('config', fn () => 'v2'); + + $this->assertSame('v2', $this->container->getResource('config', 'request-1')); + $this->assertSame('v2', $this->container->getResource('config', 'request-2')); + } } From f560b65497a34a96735167b29e066a3c8ff3fc16 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 11 Mar 2026 15:40:42 +0530 Subject: [PATCH 07/15] fix ordering --- src/DI/Container.php | 4 ++-- tests/ContainerTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/DI/Container.php b/src/DI/Container.php index 7a8d185..831a660 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -164,10 +164,10 @@ public function inject(Resource $injection, string $context = self::DEFAULT_CONT $arguments = []; foreach ($injection->getDependencies() as $dependency) { - $arguments[] = $this->get($dependency, $context); + $arguments[$dependency] = $this->get($dependency, $context); } - $resolved = \call_user_func_array($injection->getCallback(), $arguments); + $resolved = ($injection->getCallback())(...$arguments); $this->instances[$context][$injection->getName()] = $resolved; return $resolved; diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 94587ef..1716d12 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -64,6 +64,19 @@ public function testCanResolveResourcesWithDependenciesAndPerContextCache(): voi $this->assertSame('counter-2', $this->container->getResource('message', 'request-2')); } + public function testDependencyArrayOrderDoesNotNeedToMatchCallbackParameterOrder(): void + { + $container = new Container(); + + $container + ->setResource('a', fn () => 'value-a') + ->setResource('b', fn () => 'value-b') + ->setResource('combined', fn (string $b, string $a): string => "{$a}:{$b}", ['a', 'b']) + ; + + $this->assertSame('value-a:value-b', $container->get('combined')); + } + public function testContextSpecificDefinitionsOverrideDefaultContext(): void { $this->container From 3d8a454963a1db81cd63bc4f0f74c1488716e6b8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 11 Mar 2026 15:53:32 +0530 Subject: [PATCH 08/15] reviews --- src/DI/Container.php | 13 ++++++++++--- tests/ContainerTest.php | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/DI/Container.php b/src/DI/Container.php index 831a660..25056bd 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -112,16 +112,17 @@ public function getResource(string $name, string $context = self::DEFAULT_CONTEX * * @param string[] $names * @param string $context + * @param bool $fresh * @return array * * @throws Exception */ - public function getResources(array $names, string $context = self::DEFAULT_CONTEXT): array + public function getResources(array $names, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): array { $resources = []; foreach ($names as $name) { - $resources[$name] = $this->get($name, $context); + $resources[$name] = $this->get($name, $context, $fresh); } return $resources; @@ -200,7 +201,13 @@ public function refresh(string $name, ?string $context = null): self } /** - * Remove context-specific registrations and cached instances. + * Purge resources for a context. + * + * Container::purge clears cached instances from $this->instances. For + * self::DEFAULT_CONTEXT it preserves registrations in $this->dependencies + * and only resets $this->instances[self::DEFAULT_CONTEXT]. For any other + * context it removes both $this->dependencies[$context] and + * $this->instances[$context]. * * @param string $context * @return self diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 1716d12..0c7b64f 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -105,6 +105,20 @@ public function testCanResolveResourceListsAndContainerReference(): void $this->assertSame('utopia-same', $resources['summary']); } + public function testGetResourcesCanBypassCacheWithFreshFlag(): void + { + $counter = 0; + + $this->container->setResource('counter', function () use (&$counter) { + $counter++; + + return $counter; + }); + + $this->assertSame(['counter' => 1], $this->container->getResources(['counter'])); + $this->assertSame(['counter' => 2], $this->container->getResources(['counter'], fresh: true)); + } + public function testCanRefreshAndPurgeContexts(): void { $counter = 0; From 432f8a0a773bc94df5310f4bd0802611547721cc Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 09:56:25 +0530 Subject: [PATCH 09/15] refactor: align container with psr-11 --- README.md | 29 ++-- composer.json | 9 +- composer.lock | 111 +++++++------- src/DI/Container.php | 263 +++++++++------------------------- src/DI/ContainerException.php | 10 ++ src/DI/NotFoundException.php | 10 ++ src/DI/Resource.php | 91 ------------ tests/ContainerTest.php | 172 +++++++++++----------- 8 files changed, 249 insertions(+), 446 deletions(-) create mode 100644 src/DI/ContainerException.php create mode 100644 src/DI/NotFoundException.php delete mode 100644 src/DI/Resource.php diff --git a/README.md b/README.md index 0957589..5cbfe70 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ Logo

-[![Build Status](https://travis-ci.org/utopia-php/http.svg?branch=master)](https://travis-ci.org/utopia-php/http) -![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/http.svg) +[![CI](https://github.com/utopia-php/di/actions/workflows/ci.yml/badge.svg)](https://github.com/utopia-php/di/actions/workflows/ci.yml) +![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/di.svg) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://discord.gg/GSeTUeA) -Utopia DI is a small dependency injection container with scoped resources. It is designed to stay simple while still covering the resource lifecycle used across the Utopia libraries. This library is maintained by the [Appwrite team](https://appwrite.io). +Utopia DI is a small PSR-11 compatible dependency injection container with parent-child scopes. It is designed to stay simple while still covering the dependency lifecycle used across the Utopia libraries. This library is maintained by the [Appwrite team](https://appwrite.io). Although this library is part of the Utopia Framework project it is dependency free, and can be used as standalone with any other PHP project or framework. @@ -21,32 +21,37 @@ composer require utopia-php/di ```php require_once __DIR__.'/../vendor/autoload.php'; +use Psr\Container\ContainerInterface; use Utopia\DI\Container; $di = new Container(); -$di->setResource('config', fn () => [ +$di->set('config', fn (ContainerInterface $container) => [ 'dsn' => 'mysql:host=localhost;dbname=app', 'username' => 'root', 'password' => 'secret', ]); -$di->setResource( +$di->set( 'db', - fn (array $config) => new PDO($config['dsn'], $config['username'], $config['password']), - ['config'] + fn (ContainerInterface $container) => new PDO( + $container->get('config')['dsn'], + $container->get('config')['username'], + $container->get('config')['password'] + ) ); -$db = $di->getResource('db', 'request-1'); +$db = $di->get('db'); ``` -Resources are cached per context. Definitions registered in the default `utopia` context are automatically available in other contexts, while context-specific definitions override the default for that request. +Factories are resolved once per container instance. Child scopes fall back to the parent container until they override a definition locally. ```php -$di->setResource('request-id', fn () => 'req-1', context: 'request-1'); +$request = $di->scope(); -$requestResources = $di->getResources(['db', 'request-id'], 'request-1'); +$request->set('request-id', fn (ContainerInterface $container) => 'req-1'); -$di->purge('request-1'); +$request->get('db'); +$request->get('request-id'); ``` ## System Requirements diff --git a/composer.json b/composer.json index 6bbb37c..eb4ba6a 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,10 @@ "type": "library", "keywords": [ "php", - "framework", - "http", + "di", + "dependency-injection", + "container", + "psr-11", "utopia" ], "license": "MIT", @@ -22,7 +24,8 @@ "test": "vendor/bin/phpunit --configuration phpunit.xml" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "psr/container": "^2.0" }, "require-dev": { "phpunit/phpunit": "^9.5.25", diff --git a/composer.lock b/composer.lock index 9e78a20..caf1f36 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "032ab9eeb6b2176d02994ba3905364e8", - "packages": [], + "content-hash": "d3f9d5818ef93cf6e5cb171780f04291", + "packages": [ + { + "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" + } + ], "packages-dev": [ { "name": "doctrine/annotations", @@ -1256,59 +1310,6 @@ }, "time": "2021-02-03T23:26:27+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/log", "version": "3.0.0", diff --git a/src/DI/Container.php b/src/DI/Container.php index 25056bd..e080925 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -2,246 +2,113 @@ namespace Utopia\DI; -use Exception; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; -class Container +/** + * @phpstan-consistent-constructor + */ +class Container implements ContainerInterface { - public const DEFAULT_CONTEXT = 'utopia'; - /** - * @var array> + * @var array */ - protected array $dependencies = []; + private array $definitions = []; /** - * @var array> + * @var array */ - protected array $instances = []; + private array $resolved = []; /** - * Set a dependency. - * - * @param Resource $dependency - * @param string $context - * @return self - * - * @throws Exception + * @var array */ - public function set(Resource $dependency, string $context = self::DEFAULT_CONTEXT): self - { - if ($dependency->getName() === 'di') { - throw new Exception("'di' is a reserved keyword."); - } + private array $resolving = []; - $this->dependencies[$context] ??= []; - $this->instances[$context] ??= []; - - if ($context === self::DEFAULT_CONTEXT) { - $this->refresh($dependency->getName()); - } else { - unset($this->instances[$context][$dependency->getName()]); - } - - $this->dependencies[$context][$dependency->getName()] = $dependency; - - return $this; + public function __construct( + private ?ContainerInterface $parent = null, + ) { } /** - * Register a callable resource. + * Register a dependency factory on the current container. * - * @param string $name - * @param callable $callback - * @param string[] $dependencies - * @param string $context - * @return self + * @param string $id + * @param callable(ContainerInterface): mixed $factory + * @return static */ - public function setResource(string $name, callable $callback, array $dependencies = [], string $context = self::DEFAULT_CONTEXT): self + public function set(string $id, callable $factory): static { - $resource = new Resource(); - $resource - ->setName($name) - ->setCallback($callback) - ; - - foreach ($dependencies as $dependency) { - $resource->inject($dependency); - } - - return $this->set($resource, $context); - } + $this->definitions[$id] = $factory; + unset($this->resolved[$id]); - /** - * Get a resource. - * - * @param string $name - * @param string $context - * @param bool $fresh - * @return mixed - * - * @throws Exception - */ - public function get(string $name, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed - { - if ($name === 'di') { - return $this; - } - - $injection = $this->getDefinition($name, $context); - - return $this->inject($injection, $context, $fresh); - } - - /** - * Alias for get(). - * - * @param string $name - * @param string $context - * @param bool $fresh - * @return mixed - * - * @throws Exception - */ - public function getResource(string $name, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed - { - return $this->get($name, $context, $fresh); - } - - /** - * Resolve multiple dependencies for a context. - * - * @param string[] $names - * @param string $context - * @param bool $fresh - * @return array - * - * @throws Exception - */ - public function getResources(array $names, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): array - { - $resources = []; - - foreach ($names as $name) { - $resources[$name] = $this->get($name, $context, $fresh); - } - - return $resources; - } - - /** - * Check if a resource exists in the current or default context. - * - * @param string $name - * @param string $context - * @return bool - */ - public function has(string $name, string $context = self::DEFAULT_CONTEXT): bool - { - if ($name === 'di') { - return true; - } - - return isset($this->dependencies[$context][$name]) || isset($this->dependencies[self::DEFAULT_CONTEXT][$name]); + return $this; } /** - * Resolve the dependencies of a given resource. + * Resolve an entry from the current container or its parent chain. * - * @param Resource $injection - * @param string $context - * @param bool $fresh + * @param string $id * @return mixed * - * @throws Exception - */ - public function inject(Resource $injection, string $context = self::DEFAULT_CONTEXT, bool $fresh = false): mixed - { - $this->instances[$context] ??= []; - - if (\array_key_exists($injection->getName(), $this->instances[$context]) && !$fresh) { - return $this->instances[$context][$injection->getName()]; - } - - $arguments = []; - - foreach ($injection->getDependencies() as $dependency) { - $arguments[$dependency] = $this->get($dependency, $context); - } - - $resolved = ($injection->getCallback())(...$arguments); - $this->instances[$context][$injection->getName()] = $resolved; - - return $resolved; - } - - /** - * Refresh a dependency instance. - * - * @param string $name - * @param string|null $context - * @return self + * @throws ContainerExceptionInterface */ - public function refresh(string $name, ?string $context = null): self + public function get(string $id): mixed { - if ($name === 'di') { - return $this; + if (\array_key_exists($id, $this->resolved)) { + return $this->resolved[$id]; } - if ($context !== null) { - unset($this->instances[$context][$name]); - - return $this; + if (\array_key_exists($id, $this->definitions)) { + if (isset($this->resolving[$id])) { + throw new ContainerException('Circular dependency detected for "'.$id.'".'); + } + + $this->resolving[$id] = true; + + try { + $resolved = ($this->definitions[$id])($this); + } catch (NotFoundException $exception) { + throw $exception; + } catch (ContainerExceptionInterface $exception) { + throw $exception; + } catch (\Throwable $exception) { + throw new ContainerException( + 'Failed to resolve dependency "'.$id.'".', + previous: $exception + ); + } finally { + unset($this->resolving[$id]); + } + + $this->resolved[$id] = $resolved; + + return $resolved; } - foreach (\array_keys($this->instances) as $instanceContext) { - unset($this->instances[$instanceContext][$name]); + if ($this->parent instanceof ContainerInterface) { + return $this->parent->get($id); } - return $this; + throw new NotFoundException('Dependency not found: '.$id); } - /** - * Purge resources for a context. - * - * Container::purge clears cached instances from $this->instances. For - * self::DEFAULT_CONTEXT it preserves registrations in $this->dependencies - * and only resets $this->instances[self::DEFAULT_CONTEXT]. For any other - * context it removes both $this->dependencies[$context] and - * $this->instances[$context]. - * - * @param string $context - * @return self - */ - public function purge(string $context): self + public function has(string $id): bool { - if ($context === self::DEFAULT_CONTEXT) { - $this->instances[$context] = []; - - return $this; + if (\array_key_exists($id, $this->definitions)) { + return true; } - unset($this->dependencies[$context], $this->instances[$context]); - - return $this; + return $this->parent?->has($id) ?? false; } /** - * @param string $name - * @param string $context - * @return Resource + * Create a child container that falls back to the current container. * - * @throws Exception + * @return static */ - protected function getDefinition(string $name, string $context): Resource + public function scope(): static { - if (isset($this->dependencies[$context][$name])) { - return $this->dependencies[$context][$name]; - } - - if (isset($this->dependencies[self::DEFAULT_CONTEXT][$name])) { - return $this->dependencies[self::DEFAULT_CONTEXT][$name]; - } - - throw new Exception('Failed to find dependency: "' . $name . '"'); + return new static($this); } } diff --git a/src/DI/ContainerException.php b/src/DI/ContainerException.php new file mode 100644 index 0000000..136ac9e --- /dev/null +++ b/src/DI/ContainerException.php @@ -0,0 +1,10 @@ +name = $name; - return $this; - } - - /** - * Get the value of name - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Set Callback - * - * @param mixed $callback - * @return self - */ - public function setCallback(mixed $callback): self - { - $this->callback = $callback; - - return $this; - } - - /** - * Get the value of callback - * - * @return mixed - */ - public function getCallback(): mixed - { - return $this->callback; - } - - /** - * Get the value of dependencies - * - * @return string[] - */ - public function getDependencies(): array - { - return $this->dependencies; - } - - /** - * Depenedency - * - * @param string $name - * @return self - * - * @throws Exception - */ - public function inject(string $name): self - { - if (array_key_exists($name, $this->dependencies)) { - throw new Exception('Dependency already declared for '.$name); - } - - $this->dependencies[] = $name; - - return $this; - } -} diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 0c7b64f..267eb9b 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -3,8 +3,11 @@ namespace Utopia\DI\Tests; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use RuntimeException; use Utopia\DI\Container; -use Utopia\DI\Resource; +use Utopia\DI\ContainerException; +use Utopia\DI\NotFoundException; class ContainerTest extends TestCase { @@ -14,22 +17,12 @@ public function setUp(): void { $this->container = new Container(); - $user = new Resource(); - $user - ->setName('user') - ->inject('age') - ->setCallback(fn (int $age) => 'John Doe is '.$age.' years old.') - ; - - $age = new Resource(); - $age - ->setName('age') - ->setCallback(fn () => 25) - ; - $this->container - ->set($user) - ->set($age) + ->set('age', fn (ContainerInterface $container) => 25) + ->set( + 'user', + fn (ContainerInterface $container) => 'John Doe is '.$container->get('age').' years old.' + ) ; } @@ -38,123 +31,128 @@ public function tearDown(): void $this->container = null; } + public function testImplementsPsrContainerInterface(): void + { + $this->assertInstanceOf(ContainerInterface::class, $this->container); + } + public function testResolution(): void { $this->assertSame('John Doe is 25 years old.', $this->container->get('user')); } - public function testCanResolveResourcesWithDependenciesAndPerContextCache(): void + public function testFactoriesAreResolvedOncePerContainer(): void { $counter = 0; - $this->container - ->setResource('counter', function () use (&$counter) { - $counter++; - - return $counter; - }) - ->setResource('message', fn (int $counter) => "counter-{$counter}", ['counter']) - ; + $this->container->set('counter', function (ContainerInterface $container) use (&$counter) { + $counter++; - $this->assertSame(1, $this->container->getResource('counter', 'request-1')); - $this->assertSame(1, $this->container->getResource('counter', 'request-1')); - $this->assertSame('counter-1', $this->container->getResource('message', 'request-1')); + return $counter; + }); - $this->assertSame(2, $this->container->getResource('counter', 'request-2')); - $this->assertSame('counter-2', $this->container->getResource('message', 'request-2')); + $this->assertSame(1, $this->container->get('counter')); + $this->assertSame(1, $this->container->get('counter')); } - public function testDependencyArrayOrderDoesNotNeedToMatchCallbackParameterOrder(): void + public function testScopedContainersFallbackToParentDefinitions(): void { - $container = new Container(); + $request = $this->container->scope(); - $container - ->setResource('a', fn () => 'value-a') - ->setResource('b', fn () => 'value-b') - ->setResource('combined', fn (string $b, string $a): string => "{$a}:{$b}", ['a', 'b']) - ; - - $this->assertSame('value-a:value-b', $container->get('combined')); + $this->assertSame('John Doe is 25 years old.', $request->get('user')); + $this->assertTrue($request->has('user')); } - public function testContextSpecificDefinitionsOverrideDefaultContext(): void + public function testScopedContainersCanOverrideParentDefinitions(): void { - $this->container - ->setResource('greeting', fn () => 'hello from default') - ->setResource('greeting', fn () => 'hello from request-1', context: 'request-1') - ; - - $this->assertSame('hello from request-1', $this->container->getResource('greeting', 'request-1')); - $this->assertSame('hello from default', $this->container->getResource('greeting', 'request-2')); - } + $request = $this->container->scope(); - public function testCanResolveResourceListsAndContainerReference(): void - { - $this->container - ->setResource('name', fn () => 'utopia') - ->setResource( - 'summary', - fn (string $name, Container $di) => "{$name}-".($di === $this->container ? 'same' : 'different'), - ['name', 'di'] + $request + ->set('age', fn (ContainerInterface $container) => 30) + ->set( + 'user', + fn (ContainerInterface $container) => 'John Doe is '.$container->get('age').' years old.' ) ; - $resources = $this->container->getResources(['name', 'summary'], 'request-1'); - - $this->assertSame('utopia', $resources['name']); - $this->assertSame('utopia-same', $resources['summary']); + $this->assertSame('John Doe is 30 years old.', $request->get('user')); + $this->assertSame('John Doe is 25 years old.', $this->container->get('user')); } - public function testGetResourcesCanBypassCacheWithFreshFlag(): void + public function testScopeUsesParentCacheUntilDefinitionsAreOverridden(): void { $counter = 0; - $this->container->setResource('counter', function () use (&$counter) { + $this->container->set('counter', function (ContainerInterface $container) use (&$counter) { $counter++; return $counter; }); - $this->assertSame(['counter' => 1], $this->container->getResources(['counter'])); - $this->assertSame(['counter' => 2], $this->container->getResources(['counter'], fresh: true)); + $request = $this->container->scope(); + + $this->assertSame(1, $this->container->get('counter')); + $this->assertSame(1, $request->get('counter')); + + $request->set('counter', function (ContainerInterface $container) use (&$counter) { + $counter++; + + return $counter; + }); + + $this->assertSame(2, $request->get('counter')); + $this->assertSame(2, $request->get('counter')); } - public function testCanRefreshAndPurgeContexts(): void + public function testCanCacheNullValues(): void { $counter = 0; - $this->container - ->setResource('counter', function () use (&$counter) { - $counter++; + $this->container->set('nullable', function (ContainerInterface $container) use (&$counter) { + $counter++; - return $counter; - }) - ->setResource('request-id', fn () => 'request-1', context: 'request-1') - ; + return null; + }); - $this->assertSame(1, $this->container->getResource('counter', 'request-1')); + $this->assertNull($this->container->get('nullable')); + $this->assertNull($this->container->get('nullable')); + $this->assertSame(1, $counter); + } - $this->container->refresh('counter', 'request-1'); + public function testMissingDependencyThrowsNotFoundException(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('Dependency not found: missing'); - $this->assertSame(2, $this->container->getResource('counter', 'request-1')); - $this->assertTrue($this->container->has('request-id', 'request-1')); + $this->container->get('missing'); + } - $this->container->purge('request-1'); + public function testFactoryFailuresThrowContainerException(): void + { + $this->container->set('broken', function (ContainerInterface $container) { + throw new RuntimeException('boom'); + }); - $this->assertFalse($this->container->has('request-id', 'request-1')); - $this->assertSame(3, $this->container->getResource('counter', 'request-1')); + try { + $this->container->get('broken'); + $this->fail('Expected a container exception.'); + } catch (ContainerException $exception) { + $this->assertSame('Failed to resolve dependency "broken".', $exception->getMessage()); + $this->assertInstanceOf(RuntimeException::class, $exception->getPrevious()); + $this->assertSame('boom', $exception->getPrevious()->getMessage()); + } } - public function testUpdatingDefaultContextInvalidatesCachedInstancesAcrossContexts(): void + public function testCircularDependenciesThrowContainerException(): void { - $this->container->setResource('config', fn () => 'v1'); - - $this->assertSame('v1', $this->container->getResource('config', 'request-1')); - $this->assertSame('v1', $this->container->getResource('config', 'request-2')); + $this->container + ->set('a', fn (ContainerInterface $container) => $container->get('b')) + ->set('b', fn (ContainerInterface $container) => $container->get('a')) + ; - $this->container->setResource('config', fn () => 'v2'); + $this->expectException(ContainerException::class); + $this->expectExceptionMessage('Circular dependency detected for "a".'); - $this->assertSame('v2', $this->container->getResource('config', 'request-1')); - $this->assertSame('v2', $this->container->getResource('config', 'request-2')); + $this->container->get('a'); } } From 36225364984a37a59cd3dfd385544d95e91407e6 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 10:01:09 +0530 Subject: [PATCH 10/15] refactor: add dependency definitions --- .../.github/workflows/linter.yml | 20 ------- README.md | 53 +++++++++++++------ src/DI/Container.php | 10 ++-- src/DI/Dependency.php | 35 ++++++++++++ .../{ => Exceptions}/ContainerException.php | 4 +- src/DI/{ => Exceptions}/NotFoundException.php | 4 +- tests/ContainerTest.php | 29 +++++++++- 7 files changed, 110 insertions(+), 45 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/.github/workflows/linter.yml create mode 100644 src/DI/Dependency.php rename src/DI/{ => Exceptions}/ContainerException.php (84%) rename src/DI/{ => Exceptions}/NotFoundException.php (84%) diff --git a/.github/ISSUE_TEMPLATE/.github/workflows/linter.yml b/.github/ISSUE_TEMPLATE/.github/workflows/linter.yml deleted file mode 100644 index c2f8cb6..0000000 --- a/.github/ISSUE_TEMPLATE/.github/workflows/linter.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: "Linter" - -on: [pull_request] -jobs: - lint: - name: Linter - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - - run: git checkout HEAD^2 - - - name: Run Linter - run: | - docker run --rm -v $PWD:/app composer sh -c \ - "composer install --profile --ignore-platform-reqs && composer lint" \ No newline at end of file diff --git a/README.md b/README.md index 5cbfe70..a4bb4ad 100644 --- a/README.md +++ b/README.md @@ -23,35 +23,58 @@ require_once __DIR__.'/../vendor/autoload.php'; use Psr\Container\ContainerInterface; use Utopia\DI\Container; +use Utopia\DI\Dependency; $di = new Container(); -$di->set('config', fn (ContainerInterface $container) => [ - 'dsn' => 'mysql:host=localhost;dbname=app', - 'username' => 'root', - 'password' => 'secret', -]); $di->set( - 'db', - fn (ContainerInterface $container) => new PDO( - $container->get('config')['dsn'], - $container->get('config')['username'], - $container->get('config')['password'] + key: 'age', + factory: new Dependency( + injections: [], + callback: fn () => 25 + ) +); + +$di->set( + key: 'john', + factory: new Dependency( + injections: ['age'], + callback: fn (int $age) => 'John Doe is '.$age.' years old.' ) ); -$db = $di->get('db'); +$john = $di->get('john'); ``` -Factories are resolved once per container instance. Child scopes fall back to the parent container until they override a definition locally. +You can still register plain factories directly when you want access to the container instance. ```php +$di->set( + key: 'config', + factory: fn (ContainerInterface $container) => [ + 'dsn' => 'mysql:host=localhost;dbname=app', + 'username' => 'root', + 'password' => 'secret', + ] +); + $request = $di->scope(); -$request->set('request-id', fn (ContainerInterface $container) => 'req-1'); +$request->set( + key: 'db', + factory: fn (ContainerInterface $container) => new PDO( + $container->get('config')['dsn'], + $container->get('config')['username'], + $container->get('config')['password'] + ) +); +``` + +Factories are resolved once per container instance. Child scopes fall back to the parent container until they override a definition locally. -$request->get('db'); -$request->get('request-id'); +```php +$child = $di->scope(); +$child->get('john'); ``` ## System Requirements diff --git a/src/DI/Container.php b/src/DI/Container.php index e080925..f9ecff5 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -4,6 +4,8 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Utopia\DI\Exceptions\ContainerException; +use Utopia\DI\Exceptions\NotFoundException; /** * @phpstan-consistent-constructor @@ -33,14 +35,14 @@ public function __construct( /** * Register a dependency factory on the current container. * - * @param string $id + * @param string $key * @param callable(ContainerInterface): mixed $factory * @return static */ - public function set(string $id, callable $factory): static + public function set(string $key, callable $factory): static { - $this->definitions[$id] = $factory; - unset($this->resolved[$id]); + $this->definitions[$key] = $factory; + unset($this->resolved[$key]); return $this; } diff --git a/src/DI/Dependency.php b/src/DI/Dependency.php new file mode 100644 index 0000000..a9bf710 --- /dev/null +++ b/src/DI/Dependency.php @@ -0,0 +1,35 @@ +injections as $injection) { + $arguments[] = $container->get($injection); + } + + return ($this->callback)(...$arguments); + } +} diff --git a/src/DI/ContainerException.php b/src/DI/Exceptions/ContainerException.php similarity index 84% rename from src/DI/ContainerException.php rename to src/DI/Exceptions/ContainerException.php index 136ac9e..bc4b4fc 100644 --- a/src/DI/ContainerException.php +++ b/src/DI/Exceptions/ContainerException.php @@ -1,9 +1,9 @@ assertSame('John Doe is 25 years old.', $this->container->get('user')); } + public function testCanRegisterDependencyObjects(): void + { + $container = new Container(); + + $container + ->set( + key: 'age', + factory: new Dependency( + injections: [], + callback: fn () => 25 + ) + ) + ->set( + key: 'john', + factory: new Dependency( + injections: ['age'], + callback: fn (int $age) => 'John Doe is '.$age.' years old.' + ) + ) + ; + + $this->assertSame('John Doe is 25 years old.', $container->get('john')); + } + public function testFactoriesAreResolvedOncePerContainer(): void { $counter = 0; From eadeae4a8123b85e62013519760b871f69977667 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 10:05:11 +0530 Subject: [PATCH 11/15] add rector --- composer.json | 16 +- composer.lock | 788 +++++++++++++++++++++++----------------- phpstan.neon | 4 +- rector.php | 23 ++ src/DI/Container.php | 12 +- src/DI/Dependency.php | 5 +- tests/ContainerTest.php | 26 +- 7 files changed, 503 insertions(+), 371 deletions(-) create mode 100644 rector.php diff --git a/composer.json b/composer.json index eb4ba6a..935ae21 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,14 @@ "scripts": { "format": "vendor/bin/pint", "format:check": "vendor/bin/pint --test", - "analyze": "vendor/bin/phpstan analyse -c phpstan.neon", + "analyze": "vendor/bin/phpstan analyse --memory-limit=512M", + "refactor": "vendor/bin/rector process", + "refactor:check": "vendor/bin/rector process --dry-run", + "fix": [ + "@refactor", + "@analyze", + "@format" + ], "test": "vendor/bin/phpunit --configuration phpunit.xml" }, "require": { @@ -28,10 +35,11 @@ "psr/container": "^2.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.25", "laravel/pint": "^1.27", - "swoole/ide-helper": "4.8.3", + "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^2.1", - "phpbench/phpbench": "^1.2" + "phpunit/phpunit": "^9.5.25", + "rector/rector": "^2.2", + "swoole/ide-helper": "4.8.3" } } diff --git a/composer.lock b/composer.lock index caf1f36..2a91d3c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d3f9d5818ef93cf6e5cb171780f04291", + "content-hash": "222d09b036646494e3ff92cec9723b1d", "packages": [ { "name": "psr/container", @@ -63,16 +63,16 @@ "packages-dev": [ { "name": "doctrine/annotations", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f" + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", "shasum": "" }, "require": { @@ -84,10 +84,10 @@ "require-dev": { "doctrine/cache": "^2.0", "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.8.0", + "phpstan/phpstan": "^1.10.28", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6", - "vimeo/psalm": "^4.10" + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" }, "suggest": { "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" @@ -133,36 +133,36 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.1" + "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, - "time": "2023-02-02T22:02:53+00:00" + "abandoned": true, + "time": "2024-09-05T10:17:24+00:00" }, { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -189,7 +189,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -205,7 +205,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "doctrine/lexer", @@ -286,16 +286,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.1", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", + "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", "shasum": "" }, "require": { @@ -306,13 +306,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.93.1", - "illuminate/view": "^12.51.0", - "larastan/larastan": "^3.9.2", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.5" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.5", + "shipfastlabs/agent-detector": "^1.0.2" }, "bin": [ "builds/pint" @@ -349,20 +350,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-02-10T20:00:20+00:00" + "time": "2026-03-10T20:37:18+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -370,11 +371,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "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", @@ -400,7 +402,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -408,20 +410,20 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -432,7 +434,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -440,7 +442,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -464,9 +466,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -588,24 +590,24 @@ }, { "name": "phpbench/container", - "version": "2.2.2", + "version": "2.2.3", "source": { "type": "git", "url": "https://github.com/phpbench/container.git", - "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33" + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/container/zipball/a59b929e00b87b532ca6d0edd8eca0967655af33", - "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33", + "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196", "shasum": "" }, "require": { "psr/container": "^1.0|^2.0", - "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0" + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", + "php-cs-fixer/shim": "^3.89", "phpstan/phpstan": "^0.12.52", "phpunit/phpunit": "^8" }, @@ -633,73 +635,22 @@ "description": "Simple, configurable, service container.", "support": { "issues": "https://github.com/phpbench/container/issues", - "source": "https://github.com/phpbench/container/tree/2.2.2" - }, - "time": "2023-10-30T13:38:26+00:00" - }, - { - "name": "phpbench/dom", - "version": "0.3.3", - "source": { - "type": "git", - "url": "https://github.com/phpbench/dom.git", - "reference": "786a96db538d0def931f5b19225233ec42ec7a72" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpbench/dom/zipball/786a96db538d0def931f5b19225233ec42ec7a72", - "reference": "786a96db538d0def931f5b19225233ec42ec7a72", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": "^7.3||^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.0||^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } + "source": "https://github.com/phpbench/container/tree/2.2.3" }, - "autoload": { - "psr-4": { - "PhpBench\\Dom\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Leech", - "email": "daniel@dantleech.com" - } - ], - "description": "DOM wrapper to simplify working with the PHP DOM implementation", - "support": { - "issues": "https://github.com/phpbench/dom/issues", - "source": "https://github.com/phpbench/dom/tree/0.3.3" - }, - "time": "2023-03-06T23:46:57+00:00" + "time": "2025-11-06T09:05:13+00:00" }, { "name": "phpbench/phpbench", - "version": "1.2.15", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "f7000319695cfad04a57fc64bf7ef7abdf4c437c" + "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/f7000319695cfad04a57fc64bf7ef7abdf4c437c", - "reference": "f7000319695cfad04a57fc64bf7ef7abdf4c437c", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c", + "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c", "shasum": "" }, "require": { @@ -710,30 +661,31 @@ "ext-reflection": "*", "ext-spl": "*", "ext-tokenizer": "*", - "php": "^8.1", - "phpbench/container": "^2.1", - "phpbench/dom": "~0.3.3", + "php": "^8.2", + "phpbench/container": "^2.2", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", - "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/finder": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/process": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.1 || ^7.0 || ^8.0", + "symfony/finder": "^6.1 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0", + "symfony/process": "^6.1 || ^7.0 || ^8.0", "webmozart/glob": "^4.6" }, "require-dev": { "dantleech/invoke": "^2.0", - "friendsofphp/php-cs-fixer": "^3.0", + "ergebnis/composer-normalize": "^2.39", "jangregor/phpstan-prophecy": "^1.0", - "phpspec/prophecy": "dev-master", + "php-cs-fixer/shim": "^3.9", + "phpspec/prophecy": "^1.22", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.0", - "rector/rector": "^0.18.10", - "symfony/error-handler": "^5.2 || ^6.0 || ^7.0", - "symfony/var-dumper": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^11.5", + "rector/rector": "^1.2", + "sebastian/exporter": "^6.3.2", + "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" }, "suggest": { "ext-xdebug": "For Xdebug profiling extension." @@ -776,7 +728,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.2.15" + "source": "https://github.com/phpbench/phpbench/tree/1.5.1" }, "funding": [ { @@ -784,7 +736,7 @@ "type": "github" } ], - "time": "2023-11-29T12:21:11+00:00" + "time": "2026-03-05T08:18:58+00:00" }, { "name": "phpstan/phpstan", @@ -841,35 +793,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -878,7 +830,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -907,7 +859,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -915,7 +867,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1160,45 +1112,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.19", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -1243,7 +1195,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -1254,12 +1206,20 @@ "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": "2024-04-05T04:35:58+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/cache", @@ -1312,16 +1272,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -1356,9 +1316,69 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "rector/rector", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/bbd37aedd8df749916cffa2a947cfc4714d1ba2c", + "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.38" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.3.8" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-02-22T09:45:50+00:00" }, { "name": "sebastian/cli-parser", @@ -1529,16 +1549,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -1591,15 +1611,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "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": "2022-09-14T12:41:17+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -1789,16 +1821,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -1854,28 +1886,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "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": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -1918,15 +1962,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "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": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -2099,16 +2155,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -2150,15 +2206,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "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": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -2325,23 +2393,23 @@ }, { "name": "seld/jsonlint", - "version": "1.10.2", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", - "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", "shasum": "" }, "require": { "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.5", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" }, "bin": [ @@ -2373,7 +2441,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" }, "funding": [ { @@ -2385,7 +2453,7 @@ "type": "tidelift" } ], - "time": "2024-02-07T12:57:50+00:00" + "time": "2024-07-11T14:55:45+00:00" }, { "name": "swoole/ide-helper", @@ -2431,46 +2499,39 @@ }, { "name": "symfony/console", - "version": "v7.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fde915cd8e7eb99b3d531d3d5c09531429c3f9e5" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fde915cd8e7eb99b3d531d3d5c09531429c3f9e5", - "reference": "fde915cd8e7eb99b3d531d3d5c09531429c3f9e5", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "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": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.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/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": { @@ -2504,7 +2565,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.0.6" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -2515,25 +2576,29 @@ "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-04-01T11:04:53+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -2541,12 +2606,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -2571,7 +2636,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -2587,27 +2652,30 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v7.0.6", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "408105dff4c104454100730bdfd1a9cdd993f04d" + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/408105dff4c104454100730bdfd1a9cdd993f04d", - "reference": "408105dff4c104454100730bdfd1a9cdd993f04d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", "shasum": "" }, "require": { - "php": ">=8.2", + "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": { @@ -2634,7 +2702,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.0.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" }, "funding": [ { @@ -2645,32 +2713,36 @@ "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-03-21T19:37:36+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/finder", - "version": "v7.0.0", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56" + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", - "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2698,7 +2770,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.0.0" + "source": "https://github.com/symfony/finder/tree/v8.0.6" }, "funding": [ { @@ -2709,29 +2781,33 @@ "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": "2023-10-31T17:59:56+00:00" + "time": "2026-01-29T09:41:02+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.0.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -2765,7 +2841,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -2776,29 +2852,33 @@ "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": "2023-08-08T10:20:21+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -2809,8 +2889,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2844,7 +2924,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -2855,29 +2935,33 @@ "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-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -2885,8 +2969,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2922,7 +3006,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -2933,29 +3017,33 @@ "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-01-29T20:11:03+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -2963,8 +3051,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3003,7 +3091,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -3014,29 +3102,34 @@ "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-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -3047,8 +3140,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3083,7 +3176,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -3094,29 +3187,33 @@ "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-01-29T20:11:03+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/process", - "version": "v7.0.4", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -3144,7 +3241,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.0.4" + "source": "https://github.com/symfony/process/tree/v8.0.5" }, "funding": [ { @@ -3155,42 +3252,47 @@ "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-02-22T20:27:20+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.2", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", - "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3226,7 +3328,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -3237,43 +3339,47 @@ "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": "2023-12-19T21:51:00+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "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/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "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": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3312,7 +3418,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -3323,25 +3429,29 @@ "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-02-01T13:17:36+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -3370,7 +3480,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -3378,7 +3488,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/glob", diff --git a/phpstan.neon b/phpstan.neon index d54e3b0..902da9b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ parameters: - scanDirectories: - - vendor/swoole/ide-helper level: 5 paths: - src - tests + scanDirectories: + - vendor/swoole/ide-helper diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..43f6cbf --- /dev/null +++ b/rector.php @@ -0,0 +1,23 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->withSkip([ + __DIR__ . '/vendor', + ThrowWithPreviousExceptionRector::class, + ]) + ->withPhpSets(php82: true) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + typeDeclarations: true, + phpunitCodeQuality: true, + ); diff --git a/src/DI/Container.php b/src/DI/Container.php index f9ecff5..3790c40 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -28,16 +28,14 @@ class Container implements ContainerInterface private array $resolving = []; public function __construct( - private ?ContainerInterface $parent = null, + private readonly ?ContainerInterface $parent = null, ) { } /** * Register a dependency factory on the current container. * - * @param string $key * @param callable(ContainerInterface): mixed $factory - * @return static */ public function set(string $key, callable $factory): static { @@ -50,8 +48,6 @@ public function set(string $key, callable $factory): static /** * Resolve an entry from the current container or its parent chain. * - * @param string $id - * @return mixed * * @throws ContainerExceptionInterface */ @@ -70,9 +66,7 @@ public function get(string $id): mixed try { $resolved = ($this->definitions[$id])($this); - } catch (NotFoundException $exception) { - throw $exception; - } catch (ContainerExceptionInterface $exception) { + } catch (NotFoundException|ContainerExceptionInterface $exception) { throw $exception; } catch (\Throwable $exception) { throw new ContainerException( @@ -106,8 +100,6 @@ public function has(string $id): bool /** * Create a child container that falls back to the current container. - * - * @return static */ public function scope(): static { diff --git a/src/DI/Dependency.php b/src/DI/Dependency.php index a9bf710..4b1ff71 100644 --- a/src/DI/Dependency.php +++ b/src/DI/Dependency.php @@ -11,16 +11,13 @@ class Dependency * @param callable $callback */ public function __construct( - private array $injections, + private readonly array $injections, private $callback, ) { } /** * Resolve the configured injections from the container and invoke the callback. - * - * @param ContainerInterface $container - * @return mixed */ public function __invoke(ContainerInterface $container): mixed { diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 150dc73..c213099 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -1,5 +1,7 @@ container = new Container(); $this->container - ->set('age', fn (ContainerInterface $container) => 25) + ->set('age', fn (ContainerInterface $container): int => 25) ->set( 'user', - fn (ContainerInterface $container) => 'John Doe is '.$container->get('age').' years old.' + fn (ContainerInterface $container): string => 'John Doe is '.$container->get('age').' years old.' ) ; } @@ -51,14 +53,14 @@ public function testCanRegisterDependencyObjects(): void key: 'age', factory: new Dependency( injections: [], - callback: fn () => 25 + callback: fn (): int => 25 ) ) ->set( key: 'john', factory: new Dependency( injections: ['age'], - callback: fn (int $age) => 'John Doe is '.$age.' years old.' + callback: fn (int $age): string => 'John Doe is '.$age.' years old.' ) ) ; @@ -70,7 +72,7 @@ public function testFactoriesAreResolvedOncePerContainer(): void { $counter = 0; - $this->container->set('counter', function (ContainerInterface $container) use (&$counter) { + $this->container->set('counter', function (ContainerInterface $container) use (&$counter): int { $counter++; return $counter; @@ -93,10 +95,10 @@ public function testScopedContainersCanOverrideParentDefinitions(): void $request = $this->container->scope(); $request - ->set('age', fn (ContainerInterface $container) => 30) + ->set('age', fn (ContainerInterface $container): int => 30) ->set( 'user', - fn (ContainerInterface $container) => 'John Doe is '.$container->get('age').' years old.' + fn (ContainerInterface $container): string => 'John Doe is '.$container->get('age').' years old.' ) ; @@ -108,7 +110,7 @@ public function testScopeUsesParentCacheUntilDefinitionsAreOverridden(): void { $counter = 0; - $this->container->set('counter', function (ContainerInterface $container) use (&$counter) { + $this->container->set('counter', function (ContainerInterface $container) use (&$counter): int { $counter++; return $counter; @@ -119,7 +121,7 @@ public function testScopeUsesParentCacheUntilDefinitionsAreOverridden(): void $this->assertSame(1, $this->container->get('counter')); $this->assertSame(1, $request->get('counter')); - $request->set('counter', function (ContainerInterface $container) use (&$counter) { + $request->set('counter', function (ContainerInterface $container) use (&$counter): int { $counter++; return $counter; @@ -133,7 +135,7 @@ public function testCanCacheNullValues(): void { $counter = 0; - $this->container->set('nullable', function (ContainerInterface $container) use (&$counter) { + $this->container->set('nullable', function (ContainerInterface $container) use (&$counter): null { $counter++; return null; @@ -154,7 +156,7 @@ public function testMissingDependencyThrowsNotFoundException(): void public function testFactoryFailuresThrowContainerException(): void { - $this->container->set('broken', function (ContainerInterface $container) { + $this->container->set('broken', function (ContainerInterface $container): void { throw new RuntimeException('boom'); }); From 08950276a28c73c343bc5dcc26932430c11f8eed Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 10:05:49 +0530 Subject: [PATCH 12/15] update workflow --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 154e0a1..8e57136 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,29 @@ jobs: - name: Run Analyze run: composer analyze + refactor: + name: Refactor + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run Refactor Check + run: composer refactor:check + format: name: Format runs-on: ubuntu-latest From 66f3091bc89db14995215a6e09e9332a2434a83b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 10:16:22 +0530 Subject: [PATCH 13/15] split out rector --- .github/workflows/ci.yml | 10 +- .gitignore | 3 +- composer.json | 32 +++--- composer.lock | 225 +++++++++++++++---------------------- tests/ContainerTest.php | 2 +- tools/rector/composer.json | 18 +++ tools/rector/composer.lock | 135 ++++++++++++++++++++++ 7 files changed, 268 insertions(+), 157 deletions(-) create mode 100644 tools/rector/composer.json create mode 100644 tools/rector/composer.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e57136..dba4dad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,11 +41,11 @@ jobs: php-version: "8.3" coverage: none - - name: Validate composer.json and composer.lock - run: composer validate --strict + - name: Validate tools/rector/composer.json + run: composer validate --strict --working-dir=tools/rector - - name: Install dependencies - run: composer install --prefer-dist --no-progress + - name: Install Rector dependencies + run: composer install --prefer-dist --no-progress --working-dir=tools/rector - name: Run Refactor Check run: composer refactor:check @@ -78,7 +78,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ["8.2", "8.3", "nightly"] + php-versions: ["8.2", "8.3", "8.4"] steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index 002c90b..5f89131 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor/ /.idea/ -*.cache \ No newline at end of file +*.cache +/tools/rector/vendor/ diff --git a/composer.json b/composer.json index 935ae21..8d1730a 100644 --- a/composer.json +++ b/composer.json @@ -11,35 +11,39 @@ "utopia" ], "license": "MIT", + "require": { + "php": ">=8.2", + "psr/container": "^2.0" + }, + "require-dev": { + "laravel/pint": "^1.27", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" + }, "autoload": { "psr-4": { "Utopia\\": "src/", "Tests\\E2E\\": "tests/e2e" } }, + "config": { + "platform": { + "php": "8.2" + } + }, "scripts": { "format": "vendor/bin/pint", "format:check": "vendor/bin/pint --test", "analyze": "vendor/bin/phpstan analyse --memory-limit=512M", - "refactor": "vendor/bin/rector process", - "refactor:check": "vendor/bin/rector process --dry-run", + "refactor": "tools/rector/vendor/bin/rector process --config=rector.php", + "refactor:check": "tools/rector/vendor/bin/rector process --dry-run --config=rector.php", "fix": [ "@refactor", "@analyze", "@format" ], "test": "vendor/bin/phpunit --configuration phpunit.xml" - }, - "require": { - "php": ">=8.2", - "psr/container": "^2.0" - }, - "require-dev": { - "laravel/pint": "^1.27", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^9.5.25", - "rector/rector": "^2.2", - "swoole/ide-helper": "4.8.3" } } diff --git a/composer.lock b/composer.lock index 2a91d3c..d5e2367 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "222d09b036646494e3ff92cec9723b1d", + "content-hash": "5704a5eda191e5bbb8a67d91b7ec9b4f", "packages": [ { "name": "psr/container", @@ -140,29 +140,30 @@ }, { "name": "doctrine/instantiator", - "version": "2.1.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^8.4" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^14", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58" + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -189,7 +190,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -205,7 +206,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T06:47:08+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "doctrine/lexer", @@ -1320,66 +1321,6 @@ }, "time": "2024-09-11T13:17:53+00:00" }, - { - "name": "rector/rector", - "version": "2.3.8", - "source": { - "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/bbd37aedd8df749916cffa2a947cfc4714d1ba2c", - "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c", - "shasum": "" - }, - "require": { - "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.38" - }, - "conflict": { - "rector/rector-doctrine": "*", - "rector/rector-downgrade-php": "*", - "rector/rector-phpunit": "*", - "rector/rector-symfony": "*" - }, - "suggest": { - "ext-dom": "To manipulate phpunit.xml via the custom-rule command" - }, - "bin": [ - "bin/rector" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Instant Upgrade and Automated Refactoring of any PHP code", - "homepage": "https://getrector.com/", - "keywords": [ - "automation", - "dev", - "migration", - "refactoring" - ], - "support": { - "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.8" - }, - "funding": [ - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2026-02-22T09:45:50+00:00" - }, { "name": "sebastian/cli-parser", "version": "1.0.2", @@ -2499,39 +2440,47 @@ }, { "name": "symfony/console", - "version": "v8.0.7", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-mbstring": "^1.0", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.4|^8.0" + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "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" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2565,7 +2514,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.7" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -2585,7 +2534,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:22+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2656,25 +2605,25 @@ }, { "name": "symfony/filesystem", - "version": "v8.0.6", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^7.4|^8.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2702,7 +2651,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.6" }, "funding": [ { @@ -2722,27 +2671,27 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/finder", - "version": "v8.0.6", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^7.4|^8.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2770,7 +2719,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.6" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -2790,24 +2739,24 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:41:02+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/options-resolver", - "version": "v8.0.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + "reference": "b38026df55197f9e39a44f3215788edf83187b80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -2841,7 +2790,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" }, "funding": [ { @@ -2861,7 +2810,7 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:55:31+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3200,20 +3149,20 @@ }, { "name": "symfony/process", - "version": "v8.0.5", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", - "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -3241,7 +3190,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.5" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -3261,7 +3210,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:08:38+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/service-contracts", @@ -3352,34 +3301,35 @@ }, { "name": "symfony/string", - "version": "v8.0.6", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", "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" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "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/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3418,7 +3368,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/string/tree/v7.4.6" }, "funding": [ { @@ -3438,7 +3388,7 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "theseer/tokenizer", @@ -3549,5 +3499,8 @@ "php": ">=8.2" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.2" + }, "plugin-api-version": "2.6.0" } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index c213099..b1ffa02 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -156,7 +156,7 @@ public function testMissingDependencyThrowsNotFoundException(): void public function testFactoryFailuresThrowContainerException(): void { - $this->container->set('broken', function (ContainerInterface $container): void { + $this->container->set('broken', function (ContainerInterface $container): never { throw new RuntimeException('boom'); }); diff --git a/tools/rector/composer.json b/tools/rector/composer.json new file mode 100644 index 0000000..7a416da --- /dev/null +++ b/tools/rector/composer.json @@ -0,0 +1,18 @@ +{ + "name": "utopia-php/di-rector", + "description": "Rector tooling for utopia-php/di", + "type": "project", + "license": "MIT", + "config": { + "platform": { + "php": "8.3.0" + } + }, + "scripts": { + "refactor": "vendor/bin/rector process --config=../../rector.php", + "refactor:check": "vendor/bin/rector process --dry-run --config=../../rector.php" + }, + "require-dev": { + "rector/rector": "^2.2" + } +} diff --git a/tools/rector/composer.lock b/tools/rector/composer.lock new file mode 100644 index 0000000..2eb50f1 --- /dev/null +++ b/tools/rector/composer.lock @@ -0,0 +1,135 @@ +{ + "_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": "c7361960ab6626c5811289d496a9c742", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "2.1.40", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "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-02-23T15:04:35+00:00" + }, + { + "name": "rector/rector", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/bbd37aedd8df749916cffa2a947cfc4714d1ba2c", + "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.38" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.3.8" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-02-22T09:45:50+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.3.0" + }, + "plugin-api-version": "2.6.0" +} From f4bbf60d05edb2342ea09d44e90573180c212ec7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 10:43:30 +0530 Subject: [PATCH 14/15] update docs --- CONTRIBUTING.md | 136 ++++++++++++++++++++++-------------------------- README.md | 27 +++++++++- 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ab8893..b4d952d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,109 +1,99 @@ # Contributing -We would ❤️ for you to contribute to Utopia HTTP and help make it better! We want contributing to this library to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, new docs as well as updates and tweaks, blog posts, workshops, and more. +Thanks for contributing to Utopia DI. -## How to Start? +This repository contains a small PSR-11 compatible dependency injection container used across the Utopia libraries. Contributions should keep that scope intact: small surface area, predictable behavior, and strong test coverage. -If you are worried or don’t know where to start, check out our next section explaining what kind of help we could use and where can you get involved. You can reach out with questions to [Eldad Fux (@eldadfux)](https://twitter.com/eldadfux) or [@appwrite_io](https://twitter.com/appwrite_io) on Twitter, and anyone from the [Appwrite team on Discord](https://discord.gg/GSeTUeA). You can also submit an issue, and a maintainer can guide you! +## Code Of Conduct -You can get an in-depth understanding of Utopia HTTP in our [Getting Started](docs/Getting-Starting-Guide.md) guide. +Please read and follow the [Code of Conduct](CODE_OF_CONDUCT.md). -## Code of Conduct +## Before You Start -Help us keep Utopia HTTP open and inclusive. Please read and follow our [Code of Conduct](/CODE_OF_CONDUCT.md). +- For bug fixes, documentation updates, and small improvements, open a pull request directly. +- For larger API changes or new features, open an issue first so maintainers can confirm the direction before implementation. +- For security issues, do not open a public issue. Email `security@appwrite.io` instead. -## Submit a Pull Request 🚀 +## Development Setup -Branch naming convention is as following +Utopia DI requires PHP 8.2 or later. -`TYPE-ISSUE_ID-DESCRIPTION` +1. Fork the repository and clone your fork. +2. Create a branch from `main`. +3. Install root dependencies: -example: - -``` -doc-548-submit-a-pull-request-section-to-contribution-guide +```bash +composer install ``` -When `TYPE` can be: - -- **feat** - is a new feature -- **doc** - documentation only changes -- **cicd** - changes related to CI/CD system -- **fix** - a bug fix -- **refactor** - code change that neither fixes a bug nor adds a feature - -**All PRs must include a commit message with the changes description!** - -For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to: +4. Install Rector dependencies if you plan to run refactoring checks: -1. `git pull`, before creating a new branch, pull the changes from upstream. Your master needs to be up to date. - -``` -$ git pull +```bash +composer install -d tools/rector ``` -2. Create new branch from `master` like: `doc-548-submit-a-pull-request-section-to-contribution-guide`
+## Branches And Commits -``` -$ git checkout -b [name_of_your_new_branch] -``` +Use a short, descriptive branch name. Examples: -3. Work - commit - repeat ( be sure to be in your branch ) +- `fix/scope-cache-behavior` +- `docs/readme-scope-example` +- `chore/update-tooling` -4. Push changes to GitHub +Write commit messages that clearly describe the change. Keep each pull request focused on a single concern. -``` -$ git push origin [name_of_your_new_branch] -``` - -5. Submit your changes for review - If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button. -6. Start a Pull Request - Now submit the pull request and click on `Create pull request`. -7. Get a code review approval/reject -8. After approval, merge your PR -9. GitHub will automatically delete the branch after the merge is done. (they can still be restored). - -### Testing - -- `docker-compose up -d` -- `docker-compose exec web vendor/bin/phpunit --configuration phpunit.xml` -- `docker-compose exec web vendor/bin/psalm --show-info=true` - -## Introducing New Features +## Local Checks -We would 💖 you to contribute to Utopia HTTP, but we would also like to make sure this library is as great as possible and loyal to its vision and mission statement 🙏. +Run the relevant checks before opening a pull request: -For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request. - -This will allow the community to have sufficient discussion about the new feature value and how it fits in the product roadmap and vision. - -This is also important for the repository owners to be able to give technical input and different emphasis regarding the feature design and architecture. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc). +```bash +composer test +composer analyze +composer format:check +composer refactor:check +``` -## Other Ways to Help +If you want to apply the automated fixes first: -Pull requests are great, but there are many other areas where you can help: +```bash +composer fix +``` -### Blogging & Speaking +Notes: -Creating blog posts, giving talks, or developing tutorials about one of this library's many features are excellent ways to contribute and help our project grow. +- `composer test` runs PHPUnit using [phpunit.xml](phpunit.xml). +- `composer analyze` runs PHPStan using [phpstan.neon](phpstan.neon). +- `composer format:check` runs Pint in check mode. +- `composer refactor:check` requires `tools/rector` dependencies to be installed first. -### Presenting at Meetups +## Pull Requests -Presenting at meetups and conferences about your Utopia projects. Your unique challenges and successes in building things with this library can provide great speaking material. We’d love to review your conference talk abstract, so get in touch with us if you’d like some help! +When opening a pull request: -### Sending Feedbacks & Reporting Bugs +- Base it on `main`. +- Explain the problem and the approach you took to solve it. +- Link the related issue when there is one. +- Add or update tests for behavior changes. +- Update documentation when public behavior or examples change. +- Avoid mixing unrelated refactors with functional changes. -Sending feedback is a great way for us to understand your different use cases of this library better. If you had any issues, bugs, or want to share about your experience, feel free to do so on our GitHub issues page or at our [Discord channel](https://discord.gg/GSeTUeA). +All changes should go through pull request review before merging. -### Submitting New Ideas +## What Maintainers Look For -If you think this library could use a new feature, please open an issue on our GitHub repository, stating as much information as you can think about your new idea and it's implications. We would also use this issue to gather more information, get more feedback from the community, and have a proper discussion about the new feature. +The strongest contributions usually have these properties: -### Improving Documentation +- The change matches the library's narrow scope. +- The public API stays simple and consistent. +- Edge cases are covered with tests. +- Error messages stay clear and actionable. +- Documentation reflects the final behavior. -Submitting documentation updates, enhancements, designs, or bug fixes. Spelling or grammar fixes will be very much appreciated. +## Other Ways To Help -### Helping Someone +You can also contribute by: -Searching for Utopia HTTP on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to this repo! +- reporting bugs and inconsistencies +- improving examples and documentation +- reviewing pull requests +- helping other users in issues and community channels diff --git a/README.md b/README.md index a4bb4ad..b9befb8 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,34 @@ $request->set( ); ``` -Factories are resolved once per container instance. Child scopes fall back to the parent container until they override a definition locally. +Factories are resolved once per container instance. A child scope behaves in two distinct ways: + +- If the child does not define a key, it falls back to the parent and reuses the parent's resolved value. +- If the child defines the same key locally, it resolves and caches its own value without changing the parent. ```php +$counter = 0; + +$di->set('requestId', function () use (&$counter): string { + $counter++; + + return 'request-'.$counter; +}); + +$di->get('requestId'); // "request-1" + $child = $di->scope(); -$child->get('john'); + +$child->get('requestId'); // "request-1" (falls back to the parent cache) + +$child->set('requestId', function () use (&$counter): string { + $counter++; + + return 'request-'.$counter; +}); + +$child->get('requestId'); // "request-2" (child now uses its own local definition) +$di->get('requestId'); // "request-1" (parent is unchanged) ``` ## System Requirements From dd50801581ffa3c8f1a0e34f641d9cbead86e39f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 12:17:26 +0530 Subject: [PATCH 15/15] chore: remove limitation to use particular order --- README.md | 2 ++ src/DI/Dependency.php | 2 +- tests/ContainerTest.php | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b9befb8..0b1aad3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ $di->set( $john = $di->get('john'); ``` +For `Dependency` factories, the `injections` array is matched to callback parameter names, so the array order does not need to mirror the callback signature. + You can still register plain factories directly when you want access to the container instance. ```php diff --git a/src/DI/Dependency.php b/src/DI/Dependency.php index 4b1ff71..1846fea 100644 --- a/src/DI/Dependency.php +++ b/src/DI/Dependency.php @@ -24,7 +24,7 @@ public function __invoke(ContainerInterface $container): mixed $arguments = []; foreach ($this->injections as $injection) { - $arguments[] = $container->get($injection); + $arguments[$injection] = $container->get($injection); } return ($this->callback)(...$arguments); diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index b1ffa02..7ad2fcb 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -68,6 +68,37 @@ public function testCanRegisterDependencyObjects(): void $this->assertSame('John Doe is 25 years old.', $container->get('john')); } + public function testDependencyOrderDoesNotNeedToMatchCallbackParameterOrder(): void + { + $container = new Container(); + + $container + ->set( + key: 'a', + factory: new Dependency( + injections: [], + callback: fn (): string => 'value-a' + ) + ) + ->set( + key: 'b', + factory: new Dependency( + injections: [], + callback: fn (): string => 'value-b' + ) + ) + ->set( + key: 'combined', + factory: new Dependency( + injections: ['a', 'b'], + callback: fn (string $b, string $a): string => "{$a}:{$b}" + ) + ) + ; + + $this->assertSame('value-a:value-b', $container->get('combined')); + } + public function testFactoriesAreResolvedOncePerContainer(): void { $counter = 0;