Skip to content

Commit

Permalink
Merge pull request #28 from InvisibleSmiley/master
Browse files Browse the repository at this point in the history
Support Interop and PSR containers for $container->get() resolution
  • Loading branch information
Slamdunk authored Sep 22, 2021
2 parents f5c0da2 + 996c93d commit d9243d9
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 31 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
.php-cs-fixer.php export-ignore
Makefile export-ignore
phpstan.neon export-ignore
phpstan-baseline.neon export-ignore
phpunit.xml export-ignore
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@

This extension provides following features:

1. Provide correct return for `\Laminas\ServiceManager\ServiceLocatorInterface::get()`
1. Handle controller plugins that are called using magic `__call()` in subclasses of
1. Provide correct return type for `$container->get()` calls on containers of type
`\Laminas\ServiceManager\ServiceLocatorInterface`, `\Interop\Container\ContainerInterface` or `\Psr\Container\ContainerInterface`
2. Handle controller plugins that are called using magic `__call()` in subclasses of
`\Laminas\Mvc\Controller\AbstractController`
1. Provide correct return type for `plugin` method of `AbstractController`, `FilterChain`, `PhpRenderer` and `ValidatorChain`
1. `getApplication()`, `getRenderer()`, `getRequest()` and `getResponse()` methods on Controllers, MvcEvents, View,
3. Provide correct return type for `plugin` method of `AbstractController`, `FilterChain`, `PhpRenderer` and `ValidatorChain`
4. `getApplication()`, `getRenderer()`, `getRequest()` and `getResponse()` methods on Controllers, MvcEvents, View,
ViewEvent and Application returns the real instance instead of type-hinted interfaces
1. `getView()` method on `\Laminas\View\Helper\AbstractHelper` returns the real Renderer instance instead of type-hinted
5. `getView()` method on `\Laminas\View\Helper\AbstractHelper` returns the real Renderer instance instead of type-hinted
interface
1. `\Laminas\Stdlib\ArrayObject` is configured as a [Universal object crate](https://phpstan.org/config-reference#universal-object-crates)
1. Handle `\Laminas\Stdlib\AbstractOptions` magic properties
6. `\Laminas\Stdlib\ArrayObject` is configured as a [Universal object crate](https://phpstan.org/config-reference#universal-object-crates)
7. Handle `\Laminas\Stdlib\AbstractOptions` magic properties

## Installation

Expand Down
32 changes: 16 additions & 16 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,37 @@
],
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^0.12.88"
"phpstan/phpstan": "^0.12.99"
},
"conflict": {
"laminas/laminas-cache": "<2.11",
"laminas/laminas-cache": "<2.13",
"laminas/laminas-filter": "<2.11",
"laminas/laminas-form": "<2.16",
"laminas/laminas-hydrator": "<4.0",
"laminas/laminas-form": "<2.17",
"laminas/laminas-hydrator": "<4.3",
"laminas/laminas-i18n": "<2.11",
"laminas/laminas-inputfilter": "<2.12",
"laminas/laminas-log": "<2.13",
"laminas/laminas-mail": "<2.14",
"laminas/laminas-mail": "<2.15",
"laminas/laminas-mvc": "<3.2",
"laminas/laminas-paginator": "<2.10",
"laminas/laminas-validator": "<2.14"
"laminas/laminas-validator": "<2.15"
},
"require-dev": {
"laminas/laminas-cache": "^2.11.1",
"laminas/laminas-filter": "^2.11.0",
"laminas/laminas-form": "^2.16.3",
"laminas/laminas-hydrator": "^4.1.0",
"laminas/laminas-i18n": "^2.11.1",
"laminas/laminas-cache": "^2.13.0",
"laminas/laminas-filter": "^2.11.1",
"laminas/laminas-form": "^2.17.0",
"laminas/laminas-hydrator": "^4.3.1",
"laminas/laminas-i18n": "^2.11.2",
"laminas/laminas-inputfilter": "^2.12.0",
"laminas/laminas-log": "^2.13.1",
"laminas/laminas-mail": "^2.14.0",
"laminas/laminas-mail": "^2.15.0",
"laminas/laminas-mvc": "^3.2.0",
"laminas/laminas-paginator": "^2.10.0",
"laminas/laminas-validator": "^2.14.4",
"laminas/laminas-validator": "^2.15.0",
"malukenho/mcbumpface": "^1.1.5",
"phpstan/phpstan-phpunit": "^0.12.19",
"phpunit/phpunit": "^9.5.4",
"slam/php-cs-fixer-extensions": "^v3.0.1",
"phpstan/phpstan-phpunit": "^0.12.22",
"phpunit/phpunit": "^9.5.9",
"slam/php-cs-fixer-extensions": "^v3.1.0",
"slam/php-debug-r": "^v1.7.0"
},
"extra": {
Expand Down
7 changes: 7 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#2 \\$args of static method PHPStan\\\\Reflection\\\\ParametersAcceptorSelector\\:\\:selectFromArgs\\(\\) expects array\\<PhpParser\\\\Node\\\\Arg\\>, array\\<PhpParser\\\\Node\\\\Arg\\|PhpParser\\\\Node\\\\VariadicPlaceholder\\> given\\.$#"
count: 1
path: src/Type/Laminas/PluginMethodDynamicReturnTypeExtension/AbstractPluginMethodDynamicReturnTypeExtension.php

1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- phpstan-baseline.neon

parameters:
level: max
Expand Down
18 changes: 16 additions & 2 deletions src/Rules/Laminas/ServiceManagerGetMethodCallRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@

namespace LaminasPhpStan\Rules\Laminas;

use Interop\Container\ContainerInterface as InteropContainerInterface;
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\ServiceLocatorInterface;
use LaminasPhpStan\ServiceManagerLoader;
use LaminasPhpStan\Type\Laminas\ObjectServiceManagerType;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PHPStan\Analyser\Scope;
use PHPStan\Broker\Broker;
use PHPStan\Rules\Rule;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ObjectType;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use ReflectionClass;

/**
Expand Down Expand Up @@ -47,13 +50,17 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$argType = $scope->getType($node->args[0]->value);
$firstArg = $node->args[0];
if (! $firstArg instanceof Arg) {
return [];
}
$argType = $scope->getType($firstArg->value);
if (! $argType instanceof ConstantStringType) {
return [];
}

$calledOnType = $scope->getType($node->var);
if (! $calledOnType instanceof ObjectType || ! $calledOnType->isInstanceOf(ServiceLocatorInterface::class)->yes()) {
if (! $calledOnType instanceof ObjectType || ! $this->isTypeInstanceOfContainer($calledOnType)) {
return [];
}

Expand Down Expand Up @@ -97,4 +104,11 @@ public function processNode(Node $node, Scope $scope): array
$classDoesNotExistNote
)];
}

private function isTypeInstanceOfContainer(ObjectType $type): bool
{
return $type->isInstanceOf(ServiceLocatorInterface::class)->yes()
|| $type->isInstanceOf(InteropContainerInterface::class)->yes()
|| $type->isInstanceOf(PsrContainerInterface::class)->yes();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace LaminasPhpStan\Type\Laminas\PluginMethodDynamicReturnTypeExtension;

use LaminasPhpStan\ServiceManagerLoader;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
Expand Down Expand Up @@ -35,7 +36,16 @@ final public function getTypeFromMethodCall(
MethodCall $methodCall,
Scope $scope
): Type {
$argType = $scope->getType($methodCall->args[0]->value);
$firstArg = $methodCall->args[0];
if (! $firstArg instanceof Arg) {
throw new \PHPStan\ShouldNotHappenException(\sprintf(
'Argument passed to %s::%s should be a string, %s given',
$methodReflection->getDeclaringClass()->getName(),
$methodReflection->getName(),
$firstArg->getType()
));
}
$argType = $scope->getType($firstArg->value);
$strings = TypeUtils::getConstantStrings($argType);
$plugin = 1 === \count($strings) ? $strings[0]->getValue() : null;

Expand Down
12 changes: 11 additions & 1 deletion src/Type/Laminas/ServiceManagerGetDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\ServiceLocatorInterface;
use LaminasPhpStan\ServiceManagerLoader;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Broker\Broker;
Expand Down Expand Up @@ -62,7 +63,16 @@ public function getTypeFromMethodCall(

$serviceManager = $this->serviceManagerLoader->getServiceLocator($calledOnType->getClassName());

$argType = $scope->getType($methodCall->args[0]->value);
$firstArg = $methodCall->args[0];
if (! $firstArg instanceof Arg) {
throw new \PHPStan\ShouldNotHappenException(\sprintf(
'Argument passed to %s::%s should be a string, %s given',
$methodReflection->getDeclaringClass()->getName(),
$methodReflection->getName(),
$firstArg->getType()
));
}
$argType = $scope->getType($firstArg->value);
if (! $argType instanceof ConstantStringType) {
if ($serviceManager instanceof AbstractPluginManager) {
$refClass = new ReflectionClass($serviceManager);
Expand Down
4 changes: 4 additions & 0 deletions tests/Rules/Laminas/PluginManagerGetMethodCallRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

/**
* @covers \LaminasPhpStan\Rules\Laminas\ServiceManagerGetMethodCallRule
* @extends RuleTestCase<ServiceManagerGetMethodCallRule>
*/
final class PluginManagerGetMethodCallRuleTest extends RuleTestCase
{
Expand All @@ -22,6 +23,9 @@ protected function setUp(): void
$this->serviceManagerLoader = new ServiceManagerLoader(null);
}

/**
* @return Rule<\PhpParser\Node\Expr\MethodCall>
*/
protected function getRule(): Rule
{
return new ServiceManagerGetMethodCallRule($this->createBroker(), $this->serviceManagerLoader);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace LaminasPhpStan\Tests\Rules\Laminas\ServiceManagerGetMethodCallRule;

use Interop\Container\ContainerInterface;
use Laminas\Form\FormElementManager;
use Laminas\Mvc\Controller\ControllerManager;
use stdClass;

final class InteropContainerFoo
{
private ContainerInterface $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

public function foo(): void
{
$this->container->get('non_existent_service');

$this->container->get('EventManager');
$this->container->get('foo', 'bar');
$this->container->get([]);

$getterName = 'get';
$this->container->{$getterName}('EventManager');
$this->container->has('EventManager');

$stdClass = new stdClass();
$stdClass->get('non_existent_service');

$this->container->get(ControllerManager::class);
$this->container->get(FormElementManager::class);
}

public function get(string $foo): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace LaminasPhpStan\Tests\Rules\Laminas\ServiceManagerGetMethodCallRule;

use Laminas\Form\FormElementManager;
use Laminas\Mvc\Controller\ControllerManager;
use Psr\Container\ContainerInterface;
use stdClass;

final class PsrContainerFoo
{
private ContainerInterface $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

public function foo(): void
{
$this->container->get('non_existent_service');

$this->container->get('EventManager');
$this->container->get('foo', 'bar');
$this->container->get([]);

$getterName = 'get';
$this->container->{$getterName}('EventManager');
$this->container->has('EventManager');

$stdClass = new stdClass();
$stdClass->get('non_existent_service');

$this->container->get(ControllerManager::class);
$this->container->get(FormElementManager::class);
}

public function get(string $foo): void
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Laminas\ServiceManager\ServiceManager;
use stdClass;

final class Foo
final class ServiceManagerFoo
{
private ServiceManager $serviceManager;

Expand Down
28 changes: 25 additions & 3 deletions tests/Rules/Laminas/ServiceManagerGetMethodCallRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@

namespace LaminasPhpStan\Tests\Rules\Laminas;

use Interop\Container\ContainerInterface as InteropContainerInterface;
use Laminas\ServiceManager\ServiceManager;
use LaminasPhpStan\Rules\Laminas\ServiceManagerGetMethodCallRule;
use LaminasPhpStan\ServiceManagerLoader;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use Psr\Container\ContainerInterface as PsrContainerInterface;

/**
* @covers \LaminasPhpStan\Rules\Laminas\ServiceManagerGetMethodCallRule
* @covers \LaminasPhpStan\UnmappedAliasServiceLocatorProxy
* @extends RuleTestCase<ServiceManagerGetMethodCallRule>
*/
final class ServiceManagerGetMethodCallRuleTest extends RuleTestCase
{
Expand All @@ -22,16 +26,34 @@ protected function setUp(): void
$this->serviceManagerLoader = new ServiceManagerLoader(null);
}

/**
* @return string[][]
*/
public function provideContainerTypes(): array
{
return [
'ServiceManager' => ['ServiceManagerFoo.php', ServiceManager::class],
'Interop container' => ['InteropContainerFoo.php', InteropContainerInterface::class],
'PSR container' => ['PsrContainerFoo.php', PsrContainerInterface::class],
];
}

/**
* @return Rule<\PhpParser\Node\Expr\MethodCall>
*/
protected function getRule(): Rule
{
return new ServiceManagerGetMethodCallRule($this->createBroker(), $this->serviceManagerLoader);
}

public function testRule(): void
/**
* @dataProvider provideContainerTypes
*/
public function testRule(string $filename, string $containerClassname): void
{
$this->analyse([__DIR__ . '/ServiceManagerGetMethodCallRule/Foo.php'], [
$this->analyse([__DIR__ . '/ServiceManagerGetMethodCallRule/' . $filename], [
[
'The service "non_existent_service" was not configured in Laminas\ServiceManager\ServiceManager.',
'The service "non_existent_service" was not configured in ' . $containerClassname . '.',
23,
],
]);
Expand Down

0 comments on commit d9243d9

Please sign in to comment.