[DI] Only rebuild autowiring cache when actually needed

This commit is contained in:
Ryan Weaver 2016-03-12 15:10:13 -05:00 committed by Fabien Potencier
parent c5c63dc142
commit 3e976267c0
4 changed files with 304 additions and 1 deletions

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Config\AutowireServiceResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -49,6 +50,35 @@ class AutowirePass implements CompilerPassInterface
$this->ambiguousServiceTypes = array();
}
/**
* Creates a resource to help know if this service has changed.
*
* @param \ReflectionClass $reflectionClass
*
* @return AutowireServiceResource
*/
public static function createResourceForClass(\ReflectionClass $reflectionClass)
{
$metadata = array();
if ($constructor = $reflectionClass->getConstructor()) {
$metadata['__construct'] = self::getResourceMetadataForMethod($constructor);
}
// todo - when #17608 is merged, could refactor to private function to remove duplication
// of determining valid "setter" methods
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
$name = $reflectionMethod->getName();
if ($reflectionMethod->isStatic() || 1 !== $reflectionMethod->getNumberOfParameters() || 0 !== strpos($name, 'set')) {
continue;
}
$metadata[$name] = self::getResourceMetadataForMethod($reflectionMethod);
}
return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata);
}
/**
* Wires the given definition.
*
@ -63,7 +93,9 @@ class AutowirePass implements CompilerPassInterface
return;
}
$this->container->addClassResource($reflectionClass);
if ($this->container->isTrackingResources()) {
$this->container->addResource(static::createResourceForClass($reflectionClass));
}
if (!$constructor = $reflectionClass->getConstructor()) {
return;
@ -278,4 +310,25 @@ class AutowirePass implements CompilerPassInterface
}
$this->ambiguousServiceTypes[$type][] = $id;
}
static private function getResourceMetadataForMethod(\ReflectionMethod $method)
{
$methodArgumentsMetadata = array();
foreach ($method->getParameters() as $parameter) {
try {
$class = $parameter->getClass();
} catch (\ReflectionException $e) {
// type-hint is against a non-existent class
$class = false;
}
$methodArgumentsMetadata[] = array(
'class' => $class,
'isOptional' => $parameter->isOptional(),
'defaultValue' => $parameter->isOptional() ? $parameter->getDefaultValue() : null,
);
}
return $methodArgumentsMetadata;
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Config;
use Symfony\Component\Config\Resource\SelfCheckingResourceInterface;
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
class AutowireServiceResource implements SelfCheckingResourceInterface, \Serializable
{
private $class;
private $filePath;
private $autowiringMetadata = array();
public function __construct($class, $path, array $autowiringMetadata)
{
$this->class = $class;
$this->filePath = $path;
$this->autowiringMetadata = $autowiringMetadata;
}
public function isFresh($timestamp)
{
if (!file_exists($this->filePath)) {
return false;
}
// has the file *not* been modified? Definitely fresh
if (@filemtime($this->filePath) <= $timestamp) {
return true;
}
try {
$reflectionClass = new \ReflectionClass($this->class);
} catch (\ReflectionException $e) {
// the class does not exist anymore!
return false;
}
return AutowirePass::createResourceForClass($reflectionClass);
}
public function __toString()
{
return 'service.autowire.'.$this->class;
}
public function serialize()
{
return serialize(array($this->class, $this->filePath, $this->autowiringMetadata));
}
public function unserialize($serialized)
{
list($this->class, $this->filePath, $this->autowiringMetadata) = unserialize($serialized);
}
/**
* @deprecated Implemented for compatibility with Symfony 2.8
*/
public function getResource()
{
return $this->filePath;
}
}

View File

@ -413,6 +413,39 @@ class AutowirePassTest extends \PHPUnit_Framework_TestCase
$definition->getArguments()
);
}
/**
* @dataProvider getCreateResourceTests
*/
public function testCreateResourceForClass($className, $isEqual)
{
$startingResource = AutowirePass::createResourceForClass(
new \ReflectionClass(__NAMESPACE__.'\ClassForResource')
);
$newResource = AutowirePass::createResourceForClass(
new \ReflectionClass(__NAMESPACE__.'\\'.$className)
);
// hack so the objects don't differ by the class name
$startingReflObject = new \ReflectionObject($startingResource);
$reflProp = $startingReflObject->getProperty('class');
$reflProp->setAccessible(true);
$reflProp->setValue($startingResource, __NAMESPACE__.'\\'.$className);
if ($isEqual) {
$this->assertEquals($startingResource, $newResource);
} else {
$this->assertNotEquals($startingResource, $newResource);
}
}
public function getCreateResourceTests()
{
return array(
['IdenticalClassResource', true],
['ClassChangedConstructorArgs', false],
);
}
}
class Foo
@ -562,3 +595,26 @@ class MultipleArgumentsOptionalScalarNotReallyOptional
{
}
}
/*
* Classes used for testing createResourceForClass
*/
class ClassForResource
{
public function __construct($foo, Bar $bar = null)
{
}
public function setBar(Bar $bar)
{
}
}
class IdenticalClassResource extends ClassForResource
{
}
class ClassChangedConstructorArgs extends ClassForResource
{
public function __construct($foo, Bar $bar, $baz)
{
}
}

View File

@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Config;
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
use Symfony\Component\DependencyInjection\Config\AutowireServiceResource;
class AutowireServiceResourceTest extends \PHPUnit_Framework_TestCase
{
/**
* @var AutowireServiceResource
*/
private $resource;
private $file;
private $class;
private $time;
protected function setUp()
{
$this->file = realpath(sys_get_temp_dir()).'/tmp.php';
$this->time = time();
touch($this->file, $this->time);
$this->class = __NAMESPACE__.'\Foo';
$this->resource = new AutowireServiceResource(
$this->class,
$this->file,
array()
);
}
public function testToString()
{
$this->assertSame('service.autowire.'.$this->class, (string) $this->resource);
}
public function testSerializeUnserialize()
{
$unserialized = unserialize(serialize($this->resource));
$this->assertEquals($this->resource, $unserialized);
}
public function testIsFresh()
{
$this->assertTrue($this->resource->isFresh($this->time), '->isFresh() returns true if the resource has not changed in same second');
$this->assertTrue($this->resource->isFresh($this->time + 10), '->isFresh() returns true if the resource has not changed');
$this->assertFalse($this->resource->isFresh($this->time - 86400), '->isFresh() returns false if the resource has been updated');
}
public function testIsFreshForDeletedResources()
{
unlink($this->file);
$this->assertFalse($this->resource->isFresh($this->getStaleFileTime()), '->isFresh() returns false if the resource does not exist');
}
public function testIsNotFreshChangedResource()
{
$oldResource = new AutowireServiceResource(
$this->class,
$this->file,
array('will_be_different')
);
// test with a stale file *and* a resource that *will* be different than the actual
$this->assertFalse($oldResource->isFresh($this->getStaleFileTime()), '->isFresh() returns false if the constructor arguments have changed');
}
public function testIsFreshSameConstructorArgs()
{
$oldResource = AutowirePass::createResourceForClass(
new \ReflectionClass(__NAMESPACE__.'\Foo')
);
// test with a stale file *but* the resource will not be changed
$this->assertTrue($oldResource->isFresh($this->getStaleFileTime()), '->isFresh() returns false if the constructor arguments have changed');
}
public function testNotFreshIfClassNotFound()
{
$resource = new AutowireServiceResource(
'Some\Non\Existent\Class',
$this->file,
array()
);
$this->assertFalse($resource->isFresh($this->getStaleFileTime()), '->isFresh() returns false if the class no longer exists');
}
protected function tearDown()
{
if (!file_exists($this->file)) {
return;
}
unlink($this->file);
}
private function getStaleFileTime()
{
return $this->time - 10;
}
}
class Foo
{
public function __construct($foo)
{
}
}