Skip to content

Commit

Permalink
[FrameworkBundle][CacheWarmer] Ignore exeptions thrown during reflect…
Browse files Browse the repository at this point in the history
…ion classes autoload
  • Loading branch information
fancyweb committed Jul 16, 2019
1 parent ee5e5de commit 2f39c7e
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 58 deletions.
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

/**
Expand Down Expand Up @@ -54,13 +55,13 @@ public function warmUp($cacheDir)
{
$arrayAdapter = new ArrayAdapter();

spl_autoload_register([PhpArrayAdapter::class, 'throwOnRequiredClass']);
spl_autoload_register([ClassExistenceResource::class, 'throwOnRequiredClass']);
try {
if (!$this->doWarmUp($cacheDir, $arrayAdapter)) {
return;
}
} finally {
spl_autoload_unregister([PhpArrayAdapter::class, 'throwOnRequiredClass']);
spl_autoload_unregister([ClassExistenceResource::class, 'throwOnRequiredClass']);
}

// the ArrayAdapter stores the values serialized
Expand Down
Expand Up @@ -17,6 +17,7 @@
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\DoctrineProvider;
use Symfony\Component\Config\Resource\ClassExistenceResource;

/**
* Warms up annotation caches for classes found in composer's autoload class map
Expand Down Expand Up @@ -66,15 +67,13 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
$this->readAllComponents($reader, $class);
} catch (\ReflectionException $e) {
// ignore failing reflection
} catch (AnnotationException $e) {
/*
* Ignore any AnnotationException to not break the cache warming process if an Annotation is badly
* configured or could not be found / read / etc.
*
* In particular cases, an Annotation in your code can be used and defined only for a specific
* environment but is always added to the annotations.map file by some Symfony default behaviors,
* and you always end up with a not found Annotation.
*/
} catch (\Exception $e) {
try {
ClassExistenceResource::throwOnRequiredClass($class, $e);

throw $e;
} catch (\ReflectionException $e) {
}
}
}

Expand All @@ -84,14 +83,32 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
private function readAllComponents(Reader $reader, $class)
{
$reflectionClass = new \ReflectionClass($class);
$reader->getClassAnnotations($reflectionClass);

try {
$reader->getClassAnnotations($reflectionClass);
} catch (AnnotationException $e) {
/*
* Ignore any AnnotationException to not break the cache warming process if an Annotation is badly
* configured or could not be found / read / etc.
*
* In particular cases, an Annotation in your code can be used and defined only for a specific
* environment but is always added to the annotations.map file by some Symfony default behaviors,
* and you always end up with a not found Annotation.
*/
}

foreach ($reflectionClass->getMethods() as $reflectionMethod) {
$reader->getMethodAnnotations($reflectionMethod);
try {
$reader->getMethodAnnotations($reflectionMethod);
} catch (AnnotationException $e) {
}
}

foreach ($reflectionClass->getProperties() as $reflectionProperty) {
$reader->getPropertyAnnotations($reflectionProperty);
try {
$reader->getPropertyAnnotations($reflectionProperty);
} catch (AnnotationException $e) {
}
}
}
}
Expand Up @@ -14,6 +14,7 @@
use Doctrine\Common\Annotations\AnnotationException;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
Expand Down Expand Up @@ -60,6 +61,13 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
// ignore failing reflection
} catch (AnnotationException $e) {
// ignore failing annotations
} catch (\Exception $e) {
try {
ClassExistenceResource::throwOnRequiredClass($mappedClass, $e);

throw $e;
} catch (\ReflectionException $e) {
}
}
}
}
Expand Down
Expand Up @@ -15,6 +15,7 @@
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\Validator\Mapping\Cache\Psr6Cache;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
Expand Down Expand Up @@ -65,6 +66,13 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
// ignore failing reflection
} catch (AnnotationException $e) {
// ignore failing annotations
} catch (\Exception $e) {
try {
ClassExistenceResource::throwOnRequiredClass($mappedClass, $e);

throw $e;
} catch (\ReflectionException $e) {
}
}
}
}
Expand Down
Expand Up @@ -85,6 +85,54 @@ public function testAnnotationsCacheWarmerWithDebugEnabled()
$reader->getPropertyAnnotations($refClass->getProperty('cacheDir'));
}

/**
* Test that the cache warming process is not broken if a class loader
* throws an exception (on class / file not found for example).
*/
public function testClassAutoloadException()
{
$this->assertFalse(class_exists($annotatedClass = 'C\C\C', false));

file_put_contents($this->cacheDir.'/annotations.map', sprintf('<?php return %s;', var_export([$annotatedClass], true)));
$warmer = new AnnotationsCacheWarmer(new AnnotationReader(), tempnam($this->cacheDir, __FUNCTION__), new ArrayAdapter());

spl_autoload_register($classLoader = function ($class) use ($annotatedClass) {
if ($class === $annotatedClass) {
throw new \DomainException('This exception should be caught by the warmer.');
}
}, true, true);

$warmer->warmUp($this->cacheDir);

spl_autoload_unregister($classLoader);
}

/**
* Test that the cache warming process is broken if a class loader throws an
* exception but that is unrelated to the class load.
*
* @expectedException \DomainException
* @expectedExceptionMessage This exception should not be caught by the warmer.
*/
public function testClassAutoloadExceptionWithUnrelatedException()
{
$this->assertFalse(class_exists($annotatedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_AnnotationsCacheWarmerTest', false));

file_put_contents($this->cacheDir.'/annotations.map', sprintf('<?php return %s;', var_export([$annotatedClass], true)));
$warmer = new AnnotationsCacheWarmer(new AnnotationReader(), tempnam($this->cacheDir, __FUNCTION__), new ArrayAdapter());

spl_autoload_register($classLoader = function ($class) use ($annotatedClass) {
if ($class === $annotatedClass) {
eval(sprintf('class '.$annotatedClass.'{}'));
throw new \DomainException('This exception should not be caught by the warmer.');
}
}, true, true);

$warmer->warmUp($this->cacheDir);

spl_autoload_unregister($classLoader);
}

/**
* @return \PHPUnit_Framework_MockObject_MockObject|Reader
*/
Expand Down
Expand Up @@ -77,4 +77,58 @@ public function testWarmUpWithoutLoader()
$this->assertInternalType('array', $values);
$this->assertCount(0, $values);
}

/**
* Test that the cache warming process is not broken if a class loader
* throws an exception (on class / file not found for example).
*/
public function testClassAutoloadException()
{
if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) {
$this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.');
}

$this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest', false));

$warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], tempnam(sys_get_temp_dir(), __FUNCTION__), new ArrayAdapter());

spl_autoload_register($classLoader = function ($class) use ($mappedClass) {
if ($class === $mappedClass) {
throw new \DomainException('This exception should be caught by the warmer.');
}
}, true, true);

$warmer->warmUp('foo');

spl_autoload_unregister($classLoader);
}

/**
* Test that the cache warming process is broken if a class loader throws an
* exception but that is unrelated to the class load.
*
* @expectedException \DomainException
* @expectedExceptionMessage This exception should not be caught by the warmer.
*/
public function testClassAutoloadExceptionWithUnrelatedException()
{
if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) {
$this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.');
}

$this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest', false));

$warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], tempnam(sys_get_temp_dir(), __FUNCTION__), new ArrayAdapter());

spl_autoload_register($classLoader = function ($class) use ($mappedClass) {
if ($class === $mappedClass) {
eval(sprintf('class '.$mappedClass.'{}'));
throw new \DomainException('This exception should not be caught by the warmer.');
}
}, true, true);

$warmer->warmUp('foo');

spl_autoload_unregister($classLoader);
}
}
Expand Up @@ -102,4 +102,54 @@ public function testWarmUpWithoutLoader()
$this->assertInternalType('array', $values);
$this->assertCount(0, $values);
}

/**
* Test that the cache warming process is not broken if a class loader
* throws an exception (on class / file not found for example).
*/
public function testClassAutoloadException()
{
$this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest', false));

$validatorBuilder = new ValidatorBuilder();
$validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/does_not_exist.yaml');
$warmer = new ValidatorCacheWarmer($validatorBuilder, tempnam(sys_get_temp_dir(), __FUNCTION__), new ArrayAdapter());

spl_autoload_register($classloader = function ($class) use ($mappedClass) {
if ($class === $mappedClass) {
throw new \DomainException('This exception should be caught by the warmer.');
}
}, true, true);

$warmer->warmUp('foo');

spl_autoload_unregister($classloader);
}

/**
* Test that the cache warming process is broken if a class loader throws an
* exception but that is unrelated to the class load.
*
* @expectedException \DomainException
* @expectedExceptionMessage This exception should not be caught by the warmer.
*/
public function testClassAutoloadExceptionWithUnrelatedException()
{
$this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest', false));

$validatorBuilder = new ValidatorBuilder();
$validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/does_not_exist.yaml');
$warmer = new ValidatorCacheWarmer($validatorBuilder, tempnam(sys_get_temp_dir(), __FUNCTION__), new ArrayAdapter());

spl_autoload_register($classLoader = function ($class) use ($mappedClass) {
if ($class === $mappedClass) {
eval(sprintf('class '.$mappedClass.'{}'));
throw new \DomainException('This exception should not be caught by the warmer.');
}
}, true, true);

$warmer->warmUp('foo');

spl_autoload_unregister($classLoader);
}
}
@@ -0,0 +1 @@
AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest: ~
@@ -0,0 +1 @@
AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest: ~
38 changes: 0 additions & 38 deletions src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php
Expand Up @@ -262,42 +262,4 @@ private function generateItems(array $keys)
}
}
}

/**
* @throws \ReflectionException When $class is not found and is required
*
* @internal
*/
public static function throwOnRequiredClass($class)
{
$e = new \ReflectionException("Class $class does not exist");
$trace = $e->getTrace();
$autoloadFrame = [
'function' => 'spl_autoload_call',
'args' => [$class],
];
$i = 1 + array_search($autoloadFrame, $trace, true);

if (isset($trace[$i]['function']) && !isset($trace[$i]['class'])) {
switch ($trace[$i]['function']) {
case 'get_class_methods':
case 'get_class_vars':
case 'get_parent_class':
case 'is_a':
case 'is_subclass_of':
case 'class_exists':
case 'class_implements':
case 'class_parents':
case 'trait_exists':
case 'defined':
case 'interface_exists':
case 'method_exists':
case 'property_exists':
case 'is_callable':
return;
}
}

throw $e;
}
}

0 comments on commit 2f39c7e

Please sign in to comment.