[FrameworkBundle] Introduce a cache warmer for Validator based on PhpArrayAdapter

This commit is contained in:
Titouan Galopin 2016-07-29 18:51:44 +02:00
parent 904279e142
commit 6bdaf0bceb
18 changed files with 350 additions and 71 deletions

View File

@ -0,0 +1,112 @@
<?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\Bundle\FrameworkBundle\CacheWarmer;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Validator\Mapping\Cache\Psr6Cache;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Validator\ValidatorBuilderInterface;
/**
* Warms up XML and YAML validator metadata.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class ValidatorCacheWarmer implements CacheWarmerInterface
{
private $validatorBuilder;
private $phpArrayFile;
private $fallbackPool;
/**
* @param ValidatorBuilderInterface $validatorBuilder
* @param string $phpArrayFile The PHP file where metadata are cached.
* @param CacheItemPoolInterface $fallbackPool The pool where runtime-discovered metadata are cached.
*/
public function __construct(ValidatorBuilderInterface $validatorBuilder, $phpArrayFile, CacheItemPoolInterface $fallbackPool)
{
$this->validatorBuilder = $validatorBuilder;
$this->phpArrayFile = $phpArrayFile;
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}
$this->fallbackPool = $fallbackPool;
}
/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
{
if (!method_exists($this->validatorBuilder, 'getLoaders')) {
return;
}
$adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool);
$arrayPool = new ArrayAdapter(0, false);
$loaders = $this->validatorBuilder->getLoaders();
$metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), new Psr6Cache($arrayPool));
foreach ($this->extractSupportedLoaders($loaders) as $loader) {
foreach ($loader->getMappedClasses() as $mappedClass) {
$metadataFactory->getMetadataFor($mappedClass);
}
}
$values = $arrayPool->getValues();
$adapter->warmUp($values);
foreach ($values as $k => $v) {
$item = $this->fallbackPool->getItem($k);
$this->fallbackPool->saveDeferred($item->set($v));
}
$this->fallbackPool->commit();
}
/**
* {@inheritdoc}
*/
public function isOptional()
{
return true;
}
/**
* @param LoaderInterface[] $loaders
*
* @return XmlFileLoader[]|YamlFileLoader[]
*/
private function extractSupportedLoaders(array $loaders)
{
$supportedLoaders = array();
foreach ($loaders as $loader) {
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) {
$supportedLoaders[] = $loader;
} elseif ($loader instanceof LoaderChain) {
$supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getDelegatedLoaders()));
}
}
return $supportedLoaders;
}
}

View File

@ -567,7 +567,7 @@ class Configuration implements ConfigurationInterface
->info('validation configuration')
->canBeEnabled()
->children()
->scalarNode('cache')->defaultValue('validator.mapping.cache.symfony')->end()
->scalarNode('cache')->end()
->booleanNode('enable_annotations')->defaultFalse()->end()
->arrayNode('static_method')
->defaultValue(array('loadValidatorMetadata'))

View File

@ -858,13 +858,17 @@ class FrameworkExtension extends Extension
}
}
if (!$container->getParameter('kernel.debug')) {
if (isset($config['cache']) && $config['cache']) {
@trigger_error('The "framework.validation.cache" option is deprecated since Symfony 3.2 and will be removed in 4.0. Configure the "cache.validator" service under "framework.cache.pools" instead.', E_USER_DEPRECATED);
$container->setParameter(
'validator.mapping.cache.prefix',
'validator_'.$this->getKernelRootHash($container)
);
$validatorBuilder->addMethodCall('setMetadataCache', array(new Reference($config['cache'])));
} elseif (!$container->getParameter('kernel.debug')) {
$validatorBuilder->addMethodCall('setMetadataCache', array(new Reference('validator.mapping.cache.symfony')));
}
}

View File

@ -6,6 +6,7 @@
<parameters>
<parameter key="validator.mapping.cache.prefix" />
<parameter key="validator.mapping.cache.file">%kernel.cache_dir%/validation.php</parameter>
</parameters>
<services>
@ -28,8 +29,21 @@
<service id="validator.mapping.class_metadata_factory" alias="validator" public="false" />
<service id="validator.mapping.cache.symfony" class="Symfony\Component\Validator\Mapping\Cache\Psr6Cache" public="false">
<service id="validator.mapping.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer" public="false">
<argument type="service" id="validator.builder" />
<argument>%validator.mapping.cache.file%</argument>
<argument type="service" id="cache.validator" />
<tag name="kernel.cache_warmer" />
</service>
<service id="validator.mapping.cache.symfony" class="Symfony\Component\Validator\Mapping\Cache\Psr6Cache" public="false">
<argument type="service">
<service class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
<argument>%validator.mapping.cache.file%</argument>
<argument type="service" id="cache.validator" />
</service>
</argument>
</service>
<service id="validator.mapping.cache.doctrine.apc" class="Symfony\Component\Validator\Mapping\Cache\DoctrineCache" public="false">

View File

@ -0,0 +1,78 @@
<?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\Bundle\FrameworkBundle\Tests\CacheWarmer;
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer;
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Validator\ValidatorBuilder;
class ValidatorCacheWarmerTest extends TestCase
{
public function testWarmUp()
{
$validatorBuilder = new ValidatorBuilder();
$validatorBuilder->addXmlMapping(__DIR__.'/../Fixtures/Validation/Resources/person.xml');
$validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/author.yml');
$validatorBuilder->addMethodMapping('loadValidatorMetadata');
$validatorBuilder->enableAnnotationMapping();
$file = sys_get_temp_dir().'/cache-validator.php';
@unlink($file);
$fallbackPool = new ArrayAdapter();
$warmer = new ValidatorCacheWarmer($validatorBuilder, $file, $fallbackPool);
$warmer->warmUp(dirname($file));
$this->assertFileExists($file);
$values = require $file;
$this->assertInternalType('array', $values);
$this->assertCount(2, $values);
$this->assertArrayHasKey('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Person', $values);
$this->assertArrayHasKey('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Author', $values);
$values = $fallbackPool->getValues();
$this->assertInternalType('array', $values);
$this->assertCount(2, $values);
$this->assertArrayHasKey('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Person', $values);
$this->assertArrayHasKey('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Author', $values);
}
public function testWarmUpWithoutLoader()
{
$validatorBuilder = new ValidatorBuilder();
$file = sys_get_temp_dir().'/cache-validator-without-loaders.php';
@unlink($file);
$fallbackPool = new ArrayAdapter();
$warmer = new ValidatorCacheWarmer($validatorBuilder, $file, $fallbackPool);
$warmer->warmUp(dirname($file));
$this->assertFileExists($file);
$values = require $file;
$this->assertInternalType('array', $values);
$this->assertCount(0, $values);
$values = $fallbackPool->getValues();
$this->assertInternalType('array', $values);
$this->assertCount(0, $values);
}
}

View File

@ -211,7 +211,6 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
'static_method' => array('loadValidatorMetadata'),
'translation_domain' => 'validators',
'strict_email' => false,
'cache' => 'validator.mapping.cache.symfony',
),
'annotations' => array(
'cache' => 'php_array',

View File

@ -56,7 +56,6 @@ $container->loadFromExtension('framework', array(
),
'validation' => array(
'enabled' => true,
'cache' => 'validator.mapping.cache.doctrine.apc',
),
'annotations' => array(
'cache' => 'file',

View File

@ -38,7 +38,7 @@
<framework:translator enabled="true" fallback="fr" logging="true">
<framework:path>%kernel.root_dir%/Fixtures/translations</framework:path>
</framework:translator>
<framework:validation enabled="true" cache="validator.mapping.cache.doctrine.apc" />
<framework:validation enabled="true" />
<framework:annotations cache="file" debug="true" file-cache-dir="%kernel.cache_dir%/annotations" />
<framework:serializer enabled="true" enable-annotations="true" name-converter="serializer.name_converter.camel_case_to_snake_case" />
<framework:property-info />

View File

@ -44,7 +44,6 @@ framework:
paths: ['%kernel.root_dir%/Fixtures/translations']
validation:
enabled: true
cache: validator.mapping.cache.doctrine.apc
annotations:
cache: file
debug: true

View File

@ -336,7 +336,7 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertSame('addMethodMapping', $calls[4][0]);
$this->assertSame(array('loadValidatorMetadata'), $calls[4][1]);
$this->assertSame('setMetadataCache', $calls[5][0]);
$this->assertEquals(array(new Reference('validator.mapping.cache.doctrine.apc')), $calls[5][1]);
$this->assertEquals(array(new Reference('validator.mapping.cache.symfony')), $calls[5][1]);
}
public function testValidationService()

View File

@ -0,0 +1,8 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation;
class Author
{
public $gender;
}

View File

@ -0,0 +1,8 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation;
class Person
{
public $gender;
}

View File

@ -0,0 +1,4 @@
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Author:
properties:
gender:
- Choice: { choices: [male, female, other], message: Choose a valid gender. }

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
<class name="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Person">
<property name="gender">
<constraint name="Choice">
<option name="choices">
<value>male</value>
<value>female</value>
<value>other</value>
</option>
<option name="message">Choose a valid gender.</option>
</constraint>
</property>
</class>
</constraint-mapping>

View File

@ -48,7 +48,7 @@
"symfony/expression-language": "~2.8|~3.0",
"symfony/process": "~2.8|~3.0",
"symfony/serializer": "~2.8|^3.0",
"symfony/validator": "~3.1",
"symfony/validator": "~3.2",
"symfony/yaml": "~3.2",
"symfony/property-info": "~2.8|~3.0",
"phpdocumentor/reflection-docblock": "^3.0",

View File

@ -35,19 +35,7 @@ class XmlFileLoader extends FileLoader
public function loadClassMetadata(ClassMetadata $metadata)
{
if (null === $this->classes) {
// This method may throw an exception. Do not modify the class'
// state before it completes
$xml = $this->parseFile($this->file);
$this->classes = array();
foreach ($xml->namespace as $namespace) {
$this->addNamespaceAlias((string) $namespace['prefix'], trim((string) $namespace));
}
foreach ($xml->class as $class) {
$this->classes[(string) $class['name']] = $class;
}
$this->loadClassesFromXml();
}
if (isset($this->classes[$metadata->getClassName()])) {
@ -61,6 +49,20 @@ class XmlFileLoader extends FileLoader
return false;
}
/**
* Return the names of the classes mapped in this file.
*
* @return string[] The classes names
*/
public function getMappedClasses()
{
if (null === $this->classes) {
$this->loadClassesFromXml();
}
return array_keys($this->classes);
}
/**
* Parses a collection of "constraint" XML nodes.
*
@ -182,6 +184,23 @@ class XmlFileLoader extends FileLoader
return simplexml_import_dom($dom);
}
private function loadClassesFromXml()
{
// This method may throw an exception. Do not modify the class'
// state before it completes
$xml = $this->parseFile($this->file);
$this->classes = array();
foreach ($xml->namespace as $namespace) {
$this->addNamespaceAlias((string) $namespace['prefix'], trim((string) $namespace));
}
foreach ($xml->class as $class) {
$this->classes[(string) $class['name']] = $class;
}
}
private function loadClassMetadataFromXml(ClassMetadata $metadata, \SimpleXMLElement $classDescription)
{
if (count($classDescription->{'group-sequence-provider'}) > 0) {

View File

@ -42,25 +42,7 @@ class YamlFileLoader extends FileLoader
public function loadClassMetadata(ClassMetadata $metadata)
{
if (null === $this->classes) {
if (null === $this->yamlParser) {
$this->yamlParser = new YamlParser();
}
// This method may throw an exception. Do not modify the class'
// state before it completes
if (false === ($classes = $this->parseFile($this->file))) {
return false;
}
$this->classes = $classes;
if (isset($this->classes['namespaces'])) {
foreach ($this->classes['namespaces'] as $alias => $namespace) {
$this->addNamespaceAlias($alias, $namespace);
}
unset($this->classes['namespaces']);
}
$this->loadClassesFromYaml();
}
if (isset($this->classes[$metadata->getClassName()])) {
@ -74,6 +56,20 @@ class YamlFileLoader extends FileLoader
return false;
}
/**
* Return the names of the classes mapped in this file.
*
* @return string[] The classes names
*/
public function getMappedClasses()
{
if (null === $this->classes) {
$this->loadClassesFromYaml();
}
return array_keys($this->classes);
}
/**
* Parses a collection of YAML nodes.
*
@ -137,12 +133,29 @@ class YamlFileLoader extends FileLoader
return $classes;
}
/**
* Loads the validation metadata from the given YAML class description.
*
* @param ClassMetadata $metadata The metadata to load
* @param array $classDescription The YAML class description
*/
private function loadClassesFromYaml()
{
if (null === $this->yamlParser) {
$this->yamlParser = new YamlParser();
}
// This method may throw an exception. Do not modify the class'
// state before it completes
if (false === ($classes = $this->parseFile($this->file))) {
return;
}
$this->classes = $classes;
if (isset($this->classes['namespaces'])) {
foreach ($this->classes['namespaces'] as $alias => $namespace) {
$this->addNamespaceAlias($alias, $namespace);
}
unset($this->classes['namespaces']);
}
}
private function loadClassMetadataFromYaml(ClassMetadata $metadata, array $classDescription)
{
if (isset($classDescription['group_sequence_provider'])) {

View File

@ -24,11 +24,10 @@ use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader;
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader;
use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader;
use Symfony\Component\Validator\Validator\RecursiveValidator;
/**
@ -282,6 +281,32 @@ class ValidatorBuilder implements ValidatorBuilderInterface
return $this;
}
/**
* @return LoaderInterface[]
*/
public function getLoaders()
{
$loaders = array();
foreach ($this->xmlMappings as $xmlMapping) {
$loaders[] = new XmlFileLoader($xmlMapping);
}
foreach ($this->yamlMappings as $yamlMappings) {
$loaders[] = new YamlFileLoader($yamlMappings);
}
foreach ($this->methodMappings as $methodName) {
$loaders[] = new StaticMethodLoader($methodName);
}
if ($this->annotationReader) {
$loaders[] = new AnnotationLoader($this->annotationReader);
}
return $loaders;
}
/**
* {@inheritdoc}
*/
@ -290,28 +315,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface
$metadataFactory = $this->metadataFactory;
if (!$metadataFactory) {
$loaders = array();
if (count($this->xmlMappings) > 1) {
$loaders[] = new XmlFilesLoader($this->xmlMappings);
} elseif (1 === count($this->xmlMappings)) {
$loaders[] = new XmlFileLoader($this->xmlMappings[0]);
}
if (count($this->yamlMappings) > 1) {
$loaders[] = new YamlFilesLoader($this->yamlMappings);
} elseif (1 === count($this->yamlMappings)) {
$loaders[] = new YamlFileLoader($this->yamlMappings[0]);
}
foreach ($this->methodMappings as $methodName) {
$loaders[] = new StaticMethodLoader($methodName);
}
if ($this->annotationReader) {
$loaders[] = new AnnotationLoader($this->annotationReader);
}
$loaders = $this->getLoaders();
$loader = null;
if (count($loaders) > 1) {