From 9ff7bc0bdc2e06c584b715a4a06f1f5e787ce3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o=20Costa?= Date: Thu, 4 Jun 2026 10:34:11 -0300 Subject: [PATCH] Add support to namespaced containers --- CHANGELOG.md | 4 ++ README.md | 18 ++++++ ...bleContainerDynamicReturnTypeExtension.php | 60 +++++++++++++++++++ ...ontainerDynamicReturnTypeExtensionTest.php | 38 ++++++++++++ tests/configurable-test.neon | 14 +++++ tests/data/PrefixedContainerInterface.php | 18 ++++++ tests/data/configurable-container-types.php | 46 ++++++++++++++ 7 files changed, 198 insertions(+) create mode 100644 src/ConfigurableContainerDynamicReturnTypeExtension.php create mode 100644 tests/ConfigurableContainerDynamicReturnTypeExtensionTest.php create mode 100644 tests/configurable-test.neon create mode 100644 tests/data/PrefixedContainerInterface.php create mode 100644 tests/data/configurable-container-types.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd982b..85a422f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard. +## [Unreleased] + +* Feature - Add `ConfigurableContainerDynamicReturnTypeExtension` so namespace-prefixed (Strauss/Mozart) container interfaces can be matched by registering the target class through configuration. + ## [0.1.0] 2025-12-11 * Feature - Initial release of PHPStan Container Extensions. diff --git a/README.md b/README.md index 99b585a..ad6b391 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,24 @@ The extension resolves types when: When using string service IDs (e.g., `$container->get('mailer')`), the extension falls back to the default `mixed` return type. +## Prefixed containers (Strauss / Mozart) + +PHPStan keys dynamic return type extensions by the class returned from `getClass()` and only consults them when the value's type has that class in its ancestry. If you ship your dependencies through a namespace prefixer such as [Strauss](https://github.com/BrianHenryIE/strauss) or Mozart, your container interface is rewritten (e.g. `StellarWP\ContainerContract\ContainerInterface` becomes `Acme\Vendor\StellarWP\ContainerContract\ContainerInterface`). The bundled StellarWP and PSR extensions can no longer match it, because the prefixed copy shares no ancestor with the original interface. + +Register `ConfigurableContainerDynamicReturnTypeExtension` once per prefixed container interface in your `phpstan.neon`: + +```neon +services: + - + class: DPanta\PHPStan\Containers\ConfigurableContainerDynamicReturnTypeExtension + arguments: + containerClass: Acme\Vendor\StellarWP\ContainerContract\ContainerInterface + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension +``` + +The bundled defaults remain registered, so unprefixed containers keep working with zero configuration. + ## License MIT diff --git a/src/ConfigurableContainerDynamicReturnTypeExtension.php b/src/ConfigurableContainerDynamicReturnTypeExtension.php new file mode 100644 index 0000000..537fe15 --- /dev/null +++ b/src/ConfigurableContainerDynamicReturnTypeExtension.php @@ -0,0 +1,60 @@ +containerClass = $containerClass; + } + + /** + * Get the class this extension applies to. + * + * @return class-string + */ + public function getClass(): string + { + return $this->containerClass; + } +} diff --git a/tests/ConfigurableContainerDynamicReturnTypeExtensionTest.php b/tests/ConfigurableContainerDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..4a7af4f --- /dev/null +++ b/tests/ConfigurableContainerDynamicReturnTypeExtensionTest.php @@ -0,0 +1,38 @@ + + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/configurable-container-types.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/configurable-test.neon']; + } +} diff --git a/tests/configurable-test.neon b/tests/configurable-test.neon new file mode 100644 index 0000000..6950b7a --- /dev/null +++ b/tests/configurable-test.neon @@ -0,0 +1,14 @@ +includes: + - ../extension.neon + +parameters: + scanDirectories: + - %currentWorkingDirectory%/tests/data/ + +services: + - + class: DPanta\PHPStan\Containers\ConfigurableContainerDynamicReturnTypeExtension + arguments: + containerClass: DPanta\PHPStan\Containers\Tests\Data\PrefixedContainerInterface + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/tests/data/PrefixedContainerInterface.php b/tests/data/PrefixedContainerInterface.php new file mode 100644 index 0000000..a4c465b --- /dev/null +++ b/tests/data/PrefixedContainerInterface.php @@ -0,0 +1,18 @@ +get(\stdClass::class); + assertType('stdClass', $service); +} + +/** + * @param PrefixedContainerInterface $container + */ +function testConfigurableContainerGetWithDateTime(PrefixedContainerInterface $container): void +{ + $service = $container->get(\DateTime::class); + assertType('DateTime', $service); +} + +/** + * @param PrefixedContainerInterface $container + */ +function testConfigurableContainerGetWithStringId(PrefixedContainerInterface $container): void +{ + // Non-class strings fall back to the default mixed return type. + $service = $container->get('some.service.id'); + assertType('mixed', $service); +} + +/** + * @param PrefixedContainerInterface $container + * @param string $dynamicId + */ +function testConfigurableContainerGetWithDynamicId(PrefixedContainerInterface $container, string $dynamicId): void +{ + // Dynamic strings cannot be resolved to a type. + $service = $container->get($dynamicId); + assertType('mixed', $service); +}