Skip to content
Closed
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ parameters:
container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
```

The XML file should exist prior to running PHPStan.

If you want this file to be created automatically and resolved based on `APP_ENV` you can use this:

```yaml
parameters:
symfony:
container_xml_path: tests/container-loader.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit too magic for my taste. It's called container_xml_path. There should be a different config key for this scenario.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both config keys are filled with a value, the extension should throw an exception so that the user knows they should provide either XML or PHP, but not both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure if that makes it more clear.

The container_xml_path is just points to a PHP file that returns the XML.

How would you call it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should make it more general and instead have a file that returns the whole container (which we can turn into the XML ourselves for our own purposes).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then it could be called container_loader

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before doing it like this, I tried to create a new ServiceContainerServiceMapFactory and ServiceContainerParameterMapFactory that would actually load the real container via a separate container_loader file.

The problem is, the container is compiled, and does not contain the data we need.

```

```php
//tests/container-loader.php

use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;

require __DIR__ . '/../vendor/autoload.php';

(new Dotenv())->bootEnv(__DIR__ . '/../.env');

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();

return file_get_contents($kernel->getContainer()->getParameter('debug.container.dump'));
```

## Constant hassers

Sometimes, when you are dealing with optional dependencies, the `::has()` methods can cause problems. For example, the following construct would complain that the condition is always either on or off, depending on whether you have the dependency for `service` installed:
Expand Down
10 changes: 8 additions & 2 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,23 @@ services:
arguments:
consoleApplicationLoader: %symfony.console_application_loader%

# xml container resolver
-
factory: PHPStan\Symfony\XmlContainerResolver
arguments:
containerXmlPath: %symfony.container_xml_path%

# service map
symfony.serviceMapFactory:
class: PHPStan\Symfony\ServiceMapFactory
factory: PHPStan\Symfony\XmlServiceMapFactory(%symfony.container_xml_path%)
factory: PHPStan\Symfony\XmlServiceMapFactory
-
factory: @symfony.serviceMapFactory::create()

# parameter map
symfony.parameterMapFactory:
class: PHPStan\Symfony\ParameterMapFactory
factory: PHPStan\Symfony\XmlParameterMapFactory(%symfony.container_xml_path%)
factory: PHPStan\Symfony\XmlParameterMapFactory
-
factory: @symfony.parameterMapFactory::create()

Expand Down
55 changes: 55 additions & 0 deletions src/Symfony/XmlContainerResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);

namespace PHPStan\Symfony;

use PHPStan\ShouldNotHappenException;
use SimpleXMLElement;

final class XmlContainerResolver
{

/** @var string|null */
private $containerXmlPath;

/** @var SimpleXMLElement|null */
private $container;

public function __construct(?string $containerXmlPath)
{
$this->containerXmlPath = $containerXmlPath;
}

public function getContainer(): ?SimpleXMLElement
{
if ($this->containerXmlPath === null) {
return null;
}

if ($this->container !== null) {
return $this->container;
}

if (pathinfo($this->containerXmlPath, PATHINFO_EXTENSION) === 'php') {
$fileContents = require $this->containerXmlPath;

if (!is_string($fileContents)) {
throw new ShouldNotHappenException();
}
} else {
$fileContents = file_get_contents($this->containerXmlPath);
if ($fileContents === false) {
throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXmlPath));
}
}

$container = @simplexml_load_string($fileContents);
if ($container === false) {
throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXmlPath));
}

$this->container = $container;

return $this->container;
}

}
24 changes: 8 additions & 16 deletions src/Symfony/XmlParameterMapFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,25 @@
final class XmlParameterMapFactory implements ParameterMapFactory
{

/** @var string|null */
private $containerXml;
/** @var XmlContainerResolver */
private $containerResolver;

public function __construct(?string $containerXml)
public function __construct(XmlContainerResolver $containerResolver)
{
$this->containerXml = $containerXml;
$this->containerResolver = $containerResolver;
}

public function create(): ParameterMap
{
if ($this->containerXml === null) {
return new FakeParameterMap();
}
$container = $this->containerResolver->getContainer();

$fileContents = file_get_contents($this->containerXml);
if ($fileContents === false) {
throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXml));
}

$xml = @simplexml_load_string($fileContents);
if ($xml === false) {
throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml));
if ($container === null) {
return new FakeParameterMap();
}

/** @var \PHPStan\Symfony\Parameter[] $parameters */
$parameters = [];
foreach ($xml->parameters->parameter as $def) {
foreach ($container->parameters->parameter as $def) {
/** @var \SimpleXMLElement $attrs */
$attrs = $def->attributes();

Expand Down
26 changes: 8 additions & 18 deletions src/Symfony/XmlServiceMapFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,33 @@

namespace PHPStan\Symfony;

use function simplexml_load_string;
use function sprintf;
use function strpos;
use function substr;

final class XmlServiceMapFactory implements ServiceMapFactory
{

/** @var string|null */
private $containerXml;
/** @var XmlContainerResolver */
private $containerResolver;

public function __construct(?string $containerXml)
public function __construct(XmlContainerResolver $containerResolver)
{
$this->containerXml = $containerXml;
$this->containerResolver = $containerResolver;
}

public function create(): ServiceMap
{
if ($this->containerXml === null) {
return new FakeServiceMap();
}
$container = $this->containerResolver->getContainer();

$fileContents = file_get_contents($this->containerXml);
if ($fileContents === false) {
throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXml));
}

$xml = @simplexml_load_string($fileContents);
if ($xml === false) {
throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml));
if ($container === null) {
return new FakeServiceMap();
}

/** @var \PHPStan\Symfony\Service[] $services */
$services = [];
/** @var \PHPStan\Symfony\Service[] $aliases */
$aliases = [];
foreach ($xml->services->service as $def) {
foreach ($container->services->service as $def) {
/** @var \SimpleXMLElement $attrs */
$attrs = $def->attributes();
if (!isset($attrs->id)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Symfony;

use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlContainerResolver;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ContainerInterfacePrivateServiceRule>
*/
final class ContainerInterfacePrivateServicePhpLoaderRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container_loader.php')))->create());
}

public function testGetPrivateService(): void
{
if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) {
self::markTestSkipped();
}
$this->analyse(
[
__DIR__ . '/ExampleController.php',
],
[
[
'Service "private" is private.',
12,
],
]
);
}

public function testGetPrivateServiceInLegacyServiceSubscriber(): void
{
if (!interface_exists('Symfony\\Component\\DependencyInjection\\ServiceSubscriberInterface')) {
self::markTestSkipped('The test needs Symfony\Component\DependencyInjection\ServiceSubscriberInterface class.');
}

if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) {
self::markTestSkipped();
}

$this->analyse(
[
__DIR__ . '/ExampleLegacyServiceSubscriber.php',
__DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php',
__DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php',
],
[]
);
}

public function testGetPrivateServiceInServiceSubscriber(): void
{
if (!interface_exists('Symfony\Contracts\Service\ServiceSubscriberInterface')) {
self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberInterface class.');
}

if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) {
self::markTestSkipped();
}

$this->analyse(
[
__DIR__ . '/ExampleServiceSubscriber.php',
__DIR__ . '/ExampleServiceSubscriberFromAbstractController.php',
__DIR__ . '/ExampleServiceSubscriberFromLegacyController.php',
],
[]
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Symfony;

use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlContainerResolver;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;

Expand All @@ -14,7 +15,7 @@ final class ContainerInterfacePrivateServiceRuleFakeTest extends RuleTestCase

protected function getRule(): Rule
{
return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(null))->create());
return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(null)))->create());
}

public function testGetPrivateService(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Symfony;

use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlContainerResolver;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;

Expand All @@ -14,7 +15,7 @@ final class ContainerInterfacePrivateServiceRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create());
return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml')))->create());
}

public function testGetPrivateService(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\PrettyPrinter\Standard;
use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlContainerResolver;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension;
Expand All @@ -16,7 +17,7 @@ final class ContainerInterfaceUnknownServiceRuleFakeTest extends RuleTestCase

protected function getRule(): Rule
{
return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), new Standard());
return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(null)))->create(), new Standard());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\PrettyPrinter\Standard;
use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlContainerResolver;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension;
Expand All @@ -17,7 +18,7 @@ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), new Standard());
return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml')))->create(), new Standard());
}

/**
Expand Down
3 changes: 3 additions & 0 deletions tests/Rules/Symfony/container_loader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php declare(strict_types = 1);

return file_get_contents(__DIR__ . '/container.xml');
4 changes: 2 additions & 2 deletions tests/Symfony/DefaultParameterMapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ final class DefaultParameterMapTest extends TestCase
*/
public function testGetParameter(string $key, callable $validator): void
{
$factory = new XmlParameterMapFactory(__DIR__ . '/container.xml');
$factory = new XmlParameterMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml'));
$validator($factory->create()->getParameter($key));
}

public function testGetParameterEscapedPath(): void
{
$factory = new XmlParameterMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml');
$factory = new XmlParameterMapFactory(new XmlContainerResolver(__DIR__ . '/containers/bugfix%2Fcontainer.xml'));
$serviceMap = $factory->create();

self::assertNotNull($serviceMap->getParameter('app.string'));
Expand Down
4 changes: 2 additions & 2 deletions tests/Symfony/DefaultServiceMapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ final class DefaultServiceMapTest extends TestCase
*/
public function testGetService(string $id, callable $validator): void
{
$factory = new XmlServiceMapFactory(__DIR__ . '/container.xml');
$factory = new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml'));
$validator($factory->create()->getService($id));
}

public function testGetContainerEscapedPath(): void
{
$factory = new XmlServiceMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml');
$factory = new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/containers/bugfix%2Fcontainer.xml'));
$serviceMap = $factory->create();

self::assertNotNull($serviceMap->getService('withClass'));
Expand Down