v3.0 - improve container handler & refactor (#202)

This commit is contained in:
Farhad Safarov 2021-08-22 08:27:44 +03:00 committed by GitHub
parent 8a7744a540
commit 20604f13e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 259 additions and 442 deletions

View File

@ -73,7 +73,6 @@ jobs:
- 8.0
symfony-version:
- 3
- 4
- 5

View File

@ -2,28 +2,21 @@
namespace Psalm\SymfonyPsalmPlugin\Handler;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
use Psalm\Storage\FileStorage;
use Psalm\SymfonyPsalmPlugin\Issue\NamingConventionViolation;
use Psalm\SymfonyPsalmPlugin\Issue\PrivateService;
use Psalm\SymfonyPsalmPlugin\Issue\ServiceNotFound;
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
use Psalm\SymfonyPsalmPlugin\Symfony\Service;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface
{
@ -88,8 +81,9 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
return;
}
$service = self::$containerMeta->get($serviceId);
if ($service) {
try {
$service = self::$containerMeta->get($serviceId, $context->self);
if (!self::followsNamingConvention($serviceId) && false === strpos($serviceId, '\\')) {
IssueBuffer::accepts(
new NamingConventionViolation(new CodeLocation($statements_source, $expr->args[0]->value)),
@ -97,7 +91,7 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
);
}
$class = $service->getClassName();
$class = $service->getClass();
if ($class) {
$codebase->classlikes->addFullyQualifiedClassName($class);
$event->setReturnTypeCandidate(new Union([new TNamedObject($class)]));
@ -112,7 +106,7 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
);
}
}
} else {
} catch (ServiceNotFoundException $e) {
IssueBuffer::accepts(
new ServiceNotFound($serviceId, new CodeLocation($statements_source, $expr->args[0]->value)),
$statements_source->getSuppressedIssues()
@ -128,7 +122,6 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
$codebase = $event->getCodebase();
$statements_source = $event->getStatementsSource();
$storage = $event->getStorage();
$stmt = $event->getStmt();
$fileStorage = $codebase->file_storage_provider->get($statements_source->getFilePath());
@ -140,64 +133,6 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
}
}
}
// see https://symfony.com/doc/current/service_container/service_subscribers_locators.html
if (self::$containerMeta && $stmt instanceof Class_ && in_array('getsubscribedservices', array_keys($storage->methods))) {
foreach ($stmt->stmts as $classStmt) {
if ($classStmt instanceof ClassMethod && 'getSubscribedServices' === $classStmt->name->name && $classStmt->stmts) {
foreach ($classStmt->stmts as $methodStmt) {
if (!$methodStmt instanceof Return_) {
continue;
}
$return = $methodStmt->expr;
if ($return instanceof Expr\Array_) {
self::addSubscribedServicesArray($return, $codebase, $fileStorage);
} elseif ($return instanceof Expr\FuncCall) {
$funcName = $return->name;
if ($funcName instanceof Name && in_array('array_merge', $funcName->parts)) {
foreach ($return->args as $arg) {
if ($arg->value instanceof Expr\Array_) {
self::addSubscribedServicesArray($arg->value, $codebase, $fileStorage);
}
}
}
}
}
}
}
}
}
private static function addSubscribedServicesArray(Expr\Array_ $array, Codebase $codebase, FileStorage $fileStorage): void
{
if (!self::$containerMeta) {
return;
}
foreach ($array->items as $arrayItem) {
if ($arrayItem instanceof Expr\ArrayItem) {
$value = $arrayItem->value;
if (!$value instanceof Expr\ClassConstFetch) {
continue;
}
/** @var string $className */
$className = $value->class->getAttribute('resolvedName');
$key = $arrayItem->key;
$serviceId = $key instanceof String_ ? $key->value : $className;
if (null === self::$containerMeta->get($className)) {
$service = new Service($serviceId, $className);
$service->setIsPublic(true);
self::$containerMeta->add($service);
}
$codebase->queueClassLikeForScanning($className);
$fileStorage->referenced_classlikes[strtolower($className)] = $className;
}
}
}
private static function isContainerMethod(string $declaringMethodId, string $methodName): bool

View File

@ -8,6 +8,7 @@ use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
class ParameterBagHandler implements AfterMethodCallAnalysisInterface
{
@ -36,8 +37,15 @@ class ParameterBagHandler implements AfterMethodCallAnalysisInterface
$argument = $expr->args[0]->value->value;
try {
$parameter = self::$containerMeta->getParameter($argument);
} catch (ParameterNotFoundException $e) {
// maybe emit ParameterNotFound issue
return;
}
// @todo find a better way to calculate return type
switch (gettype(self::$containerMeta->getParameter($argument))) {
switch (gettype($parameter)) {
case 'string':
$event->setReturnTypeCandidate(new Union([Atomic::create('string')]));
break;

View File

@ -5,47 +5,66 @@ declare(strict_types=1);
namespace Psalm\SymfonyPsalmPlugin\Symfony;
use Psalm\Exception\ConfigException;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
class ContainerMeta
{
/**
* @psalm-var array<string, Service>
*/
private $services = [];
/**
* @var array<string>
*/
private $classNames = [];
/**
* @var array<string, mixed>
* @var array<string, string>
*/
private $parameters = [];
private $classLocators = [];
/**
* @var array<string, array<string, string>>
*/
private $serviceLocators = [];
/**
* @var ContainerBuilder
*/
private $container;
public function __construct(array $containerXmlPaths)
{
$this->init($containerXmlPaths);
}
public function get(string $id): ?Service
/**
* @throws ServiceNotFoundException
*/
public function get(string $id, ?string $contextClass = null): Definition
{
if (isset($this->services[$id])) {
return $this->services[$id];
if ($contextClass && isset($this->classLocators[$contextClass]) && isset($this->serviceLocators[$this->classLocators[$contextClass]]) && isset($this->serviceLocators[$this->classLocators[$contextClass]][$id])) {
$id = $this->serviceLocators[$this->classLocators[$contextClass]][$id];
try {
$definition = $this->getDefinition($id);
} catch (ServiceNotFoundException $e) {
if (!class_exists($id)) {
throw $e;
}
$definition = new Definition($id);
}
$definition->setPublic(true);
} else {
$definition = $this->getDefinition($id);
}
return null;
}
public function add(Service $service): void
{
if (($alias = $service->getAlias()) && isset($this->services[$alias])) {
$aliasedService = $this->services[$alias];
$service->setClassName($aliasedService->getClassName());
}
$this->services[$service->getId()] = $service;
return $definition;
}
/**
@ -53,11 +72,7 @@ class ContainerMeta
*/
public function getParameter(string $key)
{
if (isset($this->parameters[$key])) {
return $this->parameters[$key];
}
return null;
return $this->container->getParameter($key);
}
/**
@ -70,103 +85,82 @@ class ContainerMeta
private function init(array $containerXmlPaths): void
{
/** @var string $containerXmlPath */
foreach ($containerXmlPaths as $containerXmlPath) {
$xmlPath = realpath($containerXmlPath);
if (!$xmlPath || !file_exists($xmlPath)) {
continue;
$this->container = new ContainerBuilder();
$xml = new XmlFileLoader($this->container, new FileLocator());
$containerXmlPath = null;
foreach ($containerXmlPaths as $filePath) {
$containerXmlPath = realpath((string) $filePath);
if ($containerXmlPath) {
break;
}
$xml = simplexml_load_file($xmlPath);
if (!$xml->services instanceof \SimpleXMLElement) {
throw new ConfigException($xmlPath.' is not a valid container xml file');
}
/** @psalm-var \SimpleXMLElement $serviceXml */
foreach ($xml->services->service as $serviceXml) {
/** @psalm-var \SimpleXMLElement $serviceAttributes */
$serviceAttributes = $serviceXml->attributes();
$className = (string) $serviceAttributes->class;
if ($className) {
$this->classNames[] = $className;
}
$service = new Service((string) $serviceAttributes->id, $className);
if (isset($serviceAttributes->alias)) {
$service->setAlias((string) $serviceAttributes->alias);
}
if (3 < Kernel::MAJOR_VERSION) {
$service->setIsPublic('true' === (string) $serviceAttributes->public);
} else {
$service->setIsPublic('false' !== (string) $serviceAttributes->public);
}
$this->add($service);
}
/** @var \SimpleXMLElement $parameter */
foreach ($xml->parameters->parameter as $parameter) {
$value = $this->getXmlParameterValue($parameter);
$attributes = $parameter->attributes();
if (!isset($attributes->key)) {
continue;
}
$this->parameters[(string) $attributes->key] = $value;
}
return;
}
throw new ConfigException('Container xml file(s) not found at ');
if (!$containerXmlPath) {
throw new ConfigException('Container xml file(s) not found!');
}
$xml->load($containerXmlPath);
foreach ($this->container->getDefinitions() as $definition) {
$definitionFactory = $definition->getFactory();
if ($definition->hasTag('container.service_locator_context') && is_array($definitionFactory)) {
/** @var Reference $reference */
$reference = $definitionFactory[0];
$this->classLocators[$definition->getTag('container.service_locator_context')[0]['id']] = (string) $reference;
} elseif ($definition->hasTag('container.service_locator')) {
continue;
} elseif ($className = $definition->getClass()) {
$this->classNames[] = $className;
}
}
foreach ($this->container->findTaggedServiceIds('container.service_locator') as $key => $_) {
$definition = $this->container->getDefinition($key);
foreach ($definition->getArgument(0) as $id => $argument) {
if ($argument instanceof Reference) {
$this->addServiceLocator($key, $id, $argument);
} elseif ($argument instanceof ServiceClosureArgument) {
foreach ($argument->getValues() as $value) {
$this->addServiceLocator($key, $id, $value);
}
}
}
}
}
private function addServiceLocator(string $key, string $id, Reference $reference): void
{
$this->serviceLocators[$key][$id] = (string) $reference;
try {
$definition = $this->getDefinition((string) $reference);
$className = $definition->getClass();
if ($className) {
$this->classNames[] = $className;
}
} catch (ServiceNotFoundException $e) {
}
}
/**
* @return mixed
* @throws ServiceNotFoundException
*/
private function getXmlParameterValue(\SimpleXMLElement $parameter)
private function getDefinition(string $id): Definition
{
$value = null;
$attributes = $parameter->attributes();
if (isset($attributes->type)) {
switch ((string) $attributes->type) {
case 'binary':
$value = base64_decode((string) $parameter, true);
break;
case 'collection':
foreach ($parameter->children() as $child) {
$childAttributes = $child->attributes();
if (isset($childAttributes->key)) {
$value[(string) $childAttributes->key] = $this->getXmlParameterValue($child);
} else {
$value[] = $this->getXmlParameterValue($child);
}
}
break;
case 'string':
default:
$value = (string) $parameter;
break;
}
} else {
$value = (string) $parameter;
if ('true' === $value || 'false' === $value) {
$value = (bool) $value;
} elseif ('null' === $value) {
$value = null;
} elseif (is_numeric($value)) {
if (false === strpos($value, '.')) {
$value = (int) $value;
} else {
$value = (float) $value;
}
try {
$definition = $this->container->getDefinition($id);
} catch (ServiceNotFoundException $serviceNotFoundException) {
try {
$alias = $this->container->getAlias($id);
} catch (InvalidArgumentException $e) {
throw $serviceNotFoundException;
}
$definition = $this->container->getDefinition((string) $alias);
$definition->setPublic($alias->isPublic());
}
return $value;
return $definition;
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace Psalm\SymfonyPsalmPlugin\Symfony;
class Service
{
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $className;
/**
* @var bool
*/
private $isPublic = false;
/**
* @var string|null
*/
private $alias = null;
public function __construct(string $id, string $className)
{
$this->id = $id;
$this->className = $className;
}
public function isPublic(): bool
{
return $this->isPublic;
}
public function setIsPublic(bool $isPublic): void
{
$this->isPublic = $isPublic;
}
public function getId(): string
{
return $this->id;
}
public function getClassName(): string
{
return $this->className;
}
public function getAlias(): ?string
{
return $this->alias;
}
public function setAlias(string $alias): void
{
$this->alias = $alias;
}
public function setClassName(string $className): void
{
$this->className = $className;
}
}

View File

@ -6,18 +6,18 @@ Feature: Service Subscriber
"""
<containerXml>../../tests/acceptance/container.xml</containerXml>
"""
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices
Given I have the following code
And I have the following code preamble
"""
<?php
namespace App\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class SomeController implements ServiceSubscriberInterface
class DummyController implements ServiceSubscriberInterface
{
private $container;
@ -26,96 +26,34 @@ Feature: Service Subscriber
$this->container = $container;
}
public function __invoke()
{
/** @psalm-trace $entityManager */
$entityManager = $this->container->get('em');
/** @psalm-trace $validator */
$validator = $this->container->get(ValidatorInterface::class);
}
public static function getSubscribedServices()
{
return [
'em' => EntityManagerInterface::class, // with key
ValidatorInterface::class, // without key
// takes container.xml into account
];
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
| Trace | $validator: Symfony\Component\Validator\Validator\ValidatorInterface |
And I see no other errors
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices using array_merge
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices
Given I have the following code
"""
<?php
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class SomeController extends AbstractController
{
public function __invoke()
{
/** @psalm-trace $entityManager */
$entityManager = $this->container->get('custom_service');
}
/** @psalm-trace $service1 */
$service1 = $this->container->get('dummy_service_with_locator');
public static function getSubscribedServices(): array
{
return array_merge([
'custom_service' => EntityManagerInterface::class,
], parent::getSubscribedServices());
/** @psalm-trace $service2 */
$service2 = $this->container->get('dummy_service_with_locator2');
/** @psalm-trace $service3 */
$service3 = $this->container->get('dummy_service_with_locator3');
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
And I see no other errors
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices, already defined as an alias in containerXml
Given I have the following code
"""
<?php
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class SomeController implements ServiceSubscriberInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function __invoke()
{
/** @psalm-trace $kernel */
$kernel = $this->container->get('http_kernel');
}
public static function getSubscribedServices(): array
{
return [
'http_kernel' => HttpKernelInterface::class,
];
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| Trace | $kernel: Symfony\Component\HttpKernel\HttpKernel |
| Type | Message |
| Trace | $service1: Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService |
| Trace | $service2: Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService |
| Trace | $service3: Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService |
And I see no other errors

View File

@ -1,42 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="kernel.environment">dev</parameter>
<parameter key="debug_enabled">true</parameter>
<parameter key="debug_disabled">false</parameter>
<parameter key="version" type="string">1</parameter>
<parameter key="integer_one">1</parameter>
<parameter key="pi">3.14</parameter>
<parameter key="collection1" type="collection">
<parameter key="key1">val1</parameter>
<parameter key="key2">val2</parameter>
<parameters>
<parameter key="kernel.environment">dev</parameter>
<parameter key="debug_enabled">true</parameter>
<parameter key="debug_disabled">false</parameter>
<parameter key="version" type="string">1</parameter>
<parameter key="integer_one">1</parameter>
<parameter key="pi">3.14</parameter>
<parameter key="collection1" type="collection">
<parameter key="key1">val1</parameter>
<parameter key="key2">val2</parameter>
</parameter>
<parameter key="nested_collection" type="collection">
<parameter key="key">val</parameter>
<parameter key="child_collection" type="collection">
<parameter key="boolean">true</parameter>
<parameter key="float">2.18</parameter>
<parameter key="grandchild_collection" type="collection">
<parameter key="string">something</parameter>
</parameter>
<parameter key="nested_collection" type="collection">
<parameter key="key">val</parameter>
<parameter key="child_collection" type="collection">
<parameter key="boolean">true</parameter>
<parameter key="float">2.18</parameter>
<parameter key="grandchild_collection" type="collection">
<parameter key="string">something</parameter>
</parameter>
</parameter>
</parameter>
</parameters>
<services>
<service id="foo" alias="no_such_service" public="true"/>
<service id="service_container" class="Symfony\Component\DependencyInjection\ContainerInterface" public="true" synthetic="true"/>
<service id="http_kernel" class="Symfony\Component\HttpKernel\HttpKernel" public="true">
<tag name="container.hot_path"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="controller_resolver"/>
<argument type="service" id="request_stack"/>
<argument type="service" id="argument_resolver"/>
</service>
<service id="Symfony\Component\HttpKernel\HttpKernelInterface" alias="http_kernel" public="true"/>
<service id="Foo\Bar" class="Foo\Bar" public="false"/>
<service id="private_service" class="Foo\Bar" public="false"/>
<service id="dummy_private_service" class="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService" public="false"/>
<service id="public_service_wo_public_attr" class="Foo\Bar"/>
<service id="wronglyNamedService" class="Foo\Bar" public="true" />
</services>
</parameter>
</parameter>
</parameters>
<services>
<service id="doctrine.orm.entity_manager" alias="doctrine.orm.default_entity_manager" public="true"/>
<service id="doctrine.orm.default_entity_manager" class="Doctrine\ORM\EntityManager" public="true" lazy="true">
<tag name="container.preload" class="Doctrine\ORM\Proxy\Autoloader"/>
<argument type="service" id="doctrine.dbal.default_connection"/>
<argument type="service" id="doctrine.orm.default_configuration"/>
<factory class="Doctrine\ORM\EntityManager" method="create"/>
<configurator service="doctrine.orm.default_manager_configurator" method="configure"/>
</service>
<service id="foo" alias="no_such_service" public="true"/>
<service id="service_container" class="Symfony\Component\DependencyInjection\ContainerInterface" public="true" synthetic="true"/>
<service id="http_kernel" class="Symfony\Component\HttpKernel\HttpKernel" public="true">
<tag name="container.hot_path"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="controller_resolver"/>
<argument type="service" id="request_stack"/>
<argument type="service" id="argument_resolver"/>
</service>
<service id="Symfony\Component\HttpKernel\HttpKernelInterface" alias="http_kernel" public="true"/>
<service id="Foo\Bar" class="Foo\Bar" public="false"/>
<service id="private_service" class="Foo\Bar" public="false"/>
<service id="dummy_private_service" class="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService" public="false"/>
<service id="public_service_wo_public_attr" class="Foo\Bar"/>
<service id="wronglyNamedService" class="Foo\Bar" public="true" />
<service id=".service_locator.123xyz" class="Symfony\Component\DependencyInjection\ServiceLocator" public="false">
<tag name="container.service_locator"/>
<argument type="collection">
<argument key="dummy_service_with_locator" type="service" id="dummy_private_service"/>
<argument key="dummy_service_with_locator2" type="service" id="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService"/>
<argument key="dummy_service_with_locator3" type="service_closure" id="Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService"/>
<argument key="validator" type="service" id="debug.validator"/>
</argument>
</service>
<service id=".service_locator.123xyz.App\Controller\DummyController" class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator_context" id="App\Controller\DummyController"/>
<argument>App\Controller\DummyController</argument>
<argument type="service" id="service_container"/>
<factory service=".service_locator.123xyz" method="withContext"/>
</service>
</services>
</container>

View File

@ -5,7 +5,9 @@ namespace Psalm\SymfonyPsalmPlugin\Tests\Symfony;
use PHPUnit\Framework\TestCase;
use Psalm\Exception\ConfigException;
use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta;
use Psalm\SymfonyPsalmPlugin\Symfony\Service;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\HttpKernel\Kernel;
/**
@ -29,86 +31,38 @@ class ContainerMetaTest extends TestCase
}
/**
* @testdox service attributes for > Symfony 3
* @testdox service attributes
* @dataProvider publicServices
*/
public function testServices($id, string $className, bool $isPublic)
{
if (3 === Kernel::MAJOR_VERSION) {
$this->markTestSkipped('Should run for > Symfony 3');
}
$service = $this->containerMeta->get($id);
$this->assertInstanceOf(Service::class, $service);
$this->assertSame($className, $service->getClassName());
$this->assertSame($isPublic, $service->isPublic());
$serviceDefinition = $this->containerMeta->get($id);
$this->assertInstanceOf(Definition::class, $serviceDefinition);
$this->assertSame($className, $serviceDefinition->getClass());
$this->assertSame($isPublic, $serviceDefinition->isPublic());
}
public function publicServices()
public function publicServices(): iterable
{
return [
[
'id' => 'service_container',
'className' => 'Symfony\Component\DependencyInjection\ContainerInterface',
'isPublic' => true,
],
[
'id' => 'Foo\Bar',
'className' => 'Foo\Bar',
'isPublic' => false,
],
[
'id' => 'Symfony\Component\HttpKernel\HttpKernelInterface',
'className' => 'Symfony\Component\HttpKernel\HttpKernel',
'isPublic' => true,
],
[
'id' => 'public_service_wo_public_attr',
'className' => 'Foo\Bar',
'isPublic' => false,
],
yield [
'id' => 'service_container',
'className' => 'Symfony\Component\DependencyInjection\ContainerInterface',
'isPublic' => true,
];
}
/**
* @testdox service attributes for Symfony 3
* @dataProvider publicServices3
*/
public function testServices3($id, string $className, bool $isPublic)
{
if (Kernel::MAJOR_VERSION > 3) {
$this->markTestSkipped('Should run for Symfony 3');
}
$service = $this->containerMeta->get($id);
$this->assertInstanceOf(Service::class, $service);
$this->assertSame($className, $service->getClassName());
$this->assertSame($isPublic, $service->isPublic());
}
public function publicServices3()
{
return [
[
'id' => 'service_container',
'className' => 'Symfony\Component\DependencyInjection\ContainerInterface',
'isPublic' => true,
],
[
'id' => 'Foo\Bar',
'className' => 'Foo\Bar',
'isPublic' => false,
],
[
'id' => 'Symfony\Component\HttpKernel\HttpKernelInterface',
'className' => 'Symfony\Component\HttpKernel\HttpKernel',
'isPublic' => true,
],
[
'id' => 'public_service_wo_public_attr',
'className' => 'Foo\Bar',
'isPublic' => true,
],
yield [
'id' => 'Foo\Bar',
'className' => 'Foo\Bar',
'isPublic' => false,
];
yield [
'id' => 'public_service_wo_public_attr',
'className' => 'Foo\Bar',
'isPublic' => Kernel::MAJOR_VERSION < 5,
];
yield [
'id' => 'doctrine.orm.entity_manager',
'className' => 'Doctrine\ORM\EntityManager',
'isPublic' => true,
];
}
@ -126,6 +80,7 @@ class ContainerMetaTest extends TestCase
*/
public function testNonExistentService()
{
$this->expectException(ServiceNotFoundException::class);
$this->assertNull($this->containerMeta->get('non-existent-service'));
}
@ -136,7 +91,7 @@ class ContainerMetaTest extends TestCase
{
$containerMeta = new ContainerMeta(['non-existent-file.xml', __DIR__.'/../../acceptance/container.xml']);
$service = $containerMeta->get('service_container');
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClassName());
$this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClass());
}
public function testGetParameter(): void
@ -161,4 +116,33 @@ class ContainerMetaTest extends TestCase
]
], $this->containerMeta->getParameter('nested_collection'));
}
public function testGetParameterP(): void
{
$this->expectException(ParameterNotFoundException::class);
$this->containerMeta->getParameter('non_existent');
}
/**
* @dataProvider serviceLocatorProvider
*/
public function testGetServiceWithContext(string $id, string $contextClass, string $expectedClass): void
{
$serviceDefinition = $this->containerMeta->get($id, $contextClass);
$this->assertSame($expectedClass, $serviceDefinition->getClass());
}
public function serviceLocatorProvider(): iterable
{
yield [
'dummy_service_with_locator2',
'App\Controller\DummyController',
'Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService'
];
yield [
'dummy_service_with_locator3',
'App\Controller\DummyController',
'Psalm\SymfonyPsalmPlugin\Tests\Fixture\DummyPrivateService'
];
}
}