Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions src/ConfigurableContainerDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* Configurable PHPStan extension for Containers.
*
* @package DPanta\PHPStan\Containers
*/

declare(strict_types=1);

namespace DPanta\PHPStan\Containers;

/**
* PHPStan extension that resolves Container::get(Foo::class) to Foo for a
* container class supplied through configuration.
*
* PHPStan keys dynamic return type extensions by the class returned from
* getClass() and only consults them when the called value's type has that
* class in its ancestry. The bundled StellarWP and PSR extensions therefore
* cannot match a container whose namespace has been rewritten, for example by
* a Strauss/Mozart prefixer that turns
* StellarWP\ContainerContract\ContainerInterface into
* Acme\Vendor\StellarWP\ContainerContract\ContainerInterface.
*
* Register this extension once per prefixed container interface in your
* phpstan.neon:
*
* services:
* -
* class: DPanta\PHPStan\Containers\ConfigurableContainerDynamicReturnTypeExtension
* arguments:
* containerClass: Acme\Vendor\StellarWP\ContainerContract\ContainerInterface
* tags:
* - phpstan.broker.dynamicMethodReturnTypeExtension
*/
final class ConfigurableContainerDynamicReturnTypeExtension extends AbstractContainerDynamicReturnTypeExtension
{
/**
* @var class-string
*/
private string $containerClass;

/**
* @param class-string $containerClass The container interface or class this extension applies to.
*/
public function __construct(string $containerClass)
{
$this->containerClass = $containerClass;
}

/**
* Get the class this extension applies to.
*
* @return class-string
*/
public function getClass(): string
{
return $this->containerClass;
}
}
38 changes: 38 additions & 0 deletions tests/ConfigurableContainerDynamicReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace DPanta\PHPStan\Containers\Tests;

use PHPStan\Testing\TypeInferenceTestCase;

class ConfigurableContainerDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
{
/**
* @return iterable<mixed>
*/
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'];
}
}
14 changes: 14 additions & 0 deletions tests/configurable-test.neon
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions tests/data/PrefixedContainerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace DPanta\PHPStan\Containers\Tests\Data;

/**
* A container interface standing in for a namespace-prefixed (e.g. Strauss)
* copy that shares no ancestor with the bundled StellarWP/PSR interfaces.
*/
interface PrefixedContainerInterface
{
/**
* @param string $id
* @return mixed
*/
public function get(string $id);
}
46 changes: 46 additions & 0 deletions tests/data/configurable-container-types.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace DPanta\PHPStan\Containers\Tests\Data;

use function PHPStan\Testing\assertType;

/**
* @param PrefixedContainerInterface $container
*/
function testConfigurableContainerGetWithClassString(PrefixedContainerInterface $container): void
{
$service = $container->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);
}
Loading