Merge branch '2.7'

This commit is contained in:
Tobias Schultze 2015-02-21 15:11:30 +01:00
commit b05fce2857
35 changed files with 408 additions and 316 deletions

View File

@ -803,7 +803,7 @@ class FrameworkExtension extends Extension
if ('file' === $config['cache']) {
$cacheDir = $container->getParameterBag()->resolveValue($config['file_cache_dir']);
if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true)) {
if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) {
throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir));
}

View File

@ -23,6 +23,12 @@ interface SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint);
/**
* Defines the position at which the provider is called.
* Possible values: pre_auth, form, http, and remember_me.
*
* @return string
*/
public function getPosition();
public function getKey();

View File

@ -6,11 +6,11 @@
{% set link = collector.controller.file|file_link(collector.controller.line) %}
{% if collector.controller.method %}
<span class="sf-toolbar-info-class sf-toolbar-info-with-next-pointer">{{ collector.controller.class|abbr_class }}</span>
<span class="sf-toolbar-info-method" onclick="{% if link %}window.location='{{link|e('js')}}';window.event.stopPropagation();return false;{% endif %}">
<span class="sf-toolbar-info-method"{% if link %} onclick="window.location='{{link|e('js')}}';window.event.stopPropagation();return false;"{% endif %}>
{{ collector.controller.method }}
</span>
{% else %}
<span class="sf-toolbar-info-class" onclick="{% if link %}window.location='{{link|e('js')}}';window.event.stopPropagation();return false;{% endif %}">{{ collector.controller.class|abbr_class }}</span>
<span class="sf-toolbar-info-class"{% if link %} onclick="window.location='{{link|e('js')}}';window.event.stopPropagation();return false;"{% endif %}>{{ collector.controller.class|abbr_class }}</span>
{% endif %}
{% else %}
<span class="sf-toolbar-info-class">{{ collector.controller }}</span>

View File

@ -173,7 +173,7 @@
var addEventListener;
if (document.addEventListener) {
if (document.attachEvent) {
addEventListener = function (element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
};

View File

@ -24,7 +24,7 @@
<div id="sfToolbarClearer-{{ token }}" style="clear: both; height: 38px;"></div>
{% endif %}
<div id="sfToolbarMainContent-{{ token }}" class="sf-toolbarreset" data-no-turbolink>
<div id="sfToolbarMainContent-{{ token }}" class="sf-toolbarreset clear-fix" data-no-turbolink>
{% for name, template in templates %}
{{ template.renderblock('toolbar', {
'collector': profile.getcollector(name),

View File

@ -22,6 +22,11 @@ class EnumNodeDefinition extends ScalarNodeDefinition
{
private $values;
/**
* @param array $values
*
* @return EnumNodeDefinition|$this
*/
public function values(array $values)
{
$values = array_unique($values);

View File

@ -56,7 +56,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param NodeParentInterface $parent The parent
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function setParent(NodeParentInterface $parent)
{
@ -70,7 +70,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param string $info The info text
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function info($info)
{
@ -82,7 +82,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param string|array $example
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function example($example)
{
@ -95,7 +95,7 @@ abstract class NodeDefinition implements NodeParentInterface
* @param string $key
* @param mixed $value
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function attribute($key, $value)
{
@ -146,7 +146,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param mixed $value The default value
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function defaultValue($value)
{
@ -159,7 +159,7 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Sets the node as required.
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function isRequired()
{
@ -173,7 +173,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param mixed $value
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function treatNullLike($value)
{
@ -187,7 +187,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param mixed $value
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function treatTrueLike($value)
{
@ -201,7 +201,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param mixed $value
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function treatFalseLike($value)
{
@ -213,7 +213,7 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Sets null as the default value.
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function defaultNull()
{
@ -223,7 +223,7 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Sets true as the default value.
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function defaultTrue()
{
@ -233,7 +233,7 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Sets false as the default value.
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function defaultFalse()
{
@ -253,7 +253,7 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Denies the node value being empty.
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function cannotBeEmpty()
{
@ -281,7 +281,7 @@ abstract class NodeDefinition implements NodeParentInterface
*
* @param bool $deny Whether the overwriting is forbidden or not
*
* @return NodeDefinition
* @return NodeDefinition|$this
*/
public function cannotBeOverwritten($deny = true)
{

View File

@ -170,6 +170,10 @@ class Command
/**
* Interacts with the user.
*
* This method is executed before the InputDefinition is validated.
* This means that this is the only place where the command can
* interactively ask for values of missing required arguments.
*
* @param InputInterface $input An InputInterface instance
* @param OutputInterface $output An OutputInterface instance
*/

View File

@ -100,8 +100,10 @@ class ArrayInput extends Input
$values = (array) $values;
foreach ($this->parameters as $k => $v) {
if (is_int($k) && in_array($v, $values)) {
return true;
if (is_int($k)) {
if (in_array($v, $values)) {
return true;
}
} elseif (in_array($k, $values)) {
return $v;
}

View File

@ -38,6 +38,15 @@ class ArrayInputTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($input->hasParameterOption('--foo'), '->hasParameterOption() returns true if an option is present in the passed parameters');
}
public function testGetParameterOption()
{
$input = new ArrayInput(array('name' => 'Fabien', '--foo' => 'bar'));
$this->assertEquals('bar', $input->getParameterOption('--foo'), '->getParameterOption() returns the option of specified name');
$input = new ArrayInput(array('Fabien', '--foo' => 'bar'));
$this->assertEquals('bar', $input->getParameterOption('--foo'), '->getParameterOption() returns the option of specified name');
}
public function testParseArguments()
{
$input = new ArrayInput(array('name' => 'foo'), new InputDefinition(array(new InputArgument('name'))));

View File

@ -341,7 +341,7 @@ class InputDefinitionTest extends \PHPUnit_Framework_TestCase
new InputOption('foo7', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, '', array(1, 2)),
));
$defaults = array(
'foo1' => null,
'foo1' => false,
'foo2' => null,
'foo3' => 'default',
'foo4' => null,
@ -349,7 +349,7 @@ class InputDefinitionTest extends \PHPUnit_Framework_TestCase
'foo6' => array(),
'foo7' => array(1, 2),
);
$this->assertEquals($defaults, $definition->getOptionDefaults(), '->getOptionDefaults() returns the default values for all options');
$this->assertSame($defaults, $definition->getOptionDefaults(), '->getOptionDefaults() returns the default values for all options');
}
public function testGetSynopsis()

View File

@ -234,7 +234,7 @@ class Finder implements \IteratorAggregate, \Countable
* $finder->date('> now - 2 hours');
* $finder->date('>= 2005-10-15');
*
* @param string $date A date rage string
* @param string $date A date range string
*
* @return Finder The current Finder instance
*

View File

@ -10,8 +10,8 @@ CHANGELOG
2.6.2
-----
* Added back the `model_timezone` and `view_timezone` options for `TimeType`, `DateType`
and `BirthdayType`
* Added back the `model_timezone` and `view_timezone` options for `TimeType`, `DateType`
and `BirthdayType`
2.6.0
-----

View File

@ -130,7 +130,7 @@ class File extends \SplFileInfo
protected function getTargetFile($directory, $name = null)
{
if (!is_dir($directory)) {
if (false === @mkdir($directory, 0777, true)) {
if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new FileException(sprintf('Unable to create the "%s" directory', $directory));
}
} elseif (!is_writable($directory)) {

View File

@ -616,8 +616,8 @@ class Request
* The following header keys are supported:
*
* * Request::HEADER_CLIENT_IP: defaults to X-Forwarded-For (see getClientIp())
* * Request::HEADER_CLIENT_HOST: defaults to X-Forwarded-Host (see getClientHost())
* * Request::HEADER_CLIENT_PORT: defaults to X-Forwarded-Port (see getClientPort())
* * Request::HEADER_CLIENT_HOST: defaults to X-Forwarded-Host (see getHost())
* * Request::HEADER_CLIENT_PORT: defaults to X-Forwarded-Port (see getPort())
* * Request::HEADER_CLIENT_PROTO: defaults to X-Forwarded-Proto (see getScheme() and isSecure())
*
* Setting an empty value allows to disable the trusted header for the given key.

View File

@ -110,7 +110,7 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
if (false === $name) {
$name = strtr($file, '\\', '/');
$name = substr($file, strrpos($file, '/') + 1);
$name = substr($name, strrpos($name, '/') + 1);
}
$this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt');

View File

@ -578,7 +578,7 @@ abstract class Kernel implements KernelInterface, TerminableInterface
{
foreach (array('cache' => $this->getCacheDir(), 'logs' => $this->getLogDir()) as $name => $dir) {
if (!is_dir($dir)) {
if (false === @mkdir($dir, 0777, true)) {
if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new \RuntimeException(sprintf("Unable to create the %s directory (%s)\n", $name, $dir));
}
} elseif (!is_writable($dir)) {

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
@ -51,15 +51,8 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function getValue($objectOrArray, $propertyPath)
{
if (is_string($propertyPath)) {
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
$propertyValues = & $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
@ -72,15 +65,8 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function setValue(&$objectOrArray, $propertyPath, $value)
{
if (is_string($propertyPath)) {
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
$propertyValues = & $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
@ -96,10 +82,6 @@ class PropertyAccessor implements PropertyAccessorInterface
$objectOrArray = & $propertyValues[$i][self::VALUE];
if ($overwrite) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, $i);
}
$property = $propertyPath->getElement($i);
if ($propertyPath->isIndex($i)) {
@ -119,24 +101,15 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function isReadable($objectOrArray, $propertyPath)
{
if (is_string($propertyPath)) {
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
try {
$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return true;
} catch (NoSuchIndexException $e) {
return false;
} catch (NoSuchPropertyException $e) {
} catch (AccessException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
@ -148,15 +121,8 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function isWritable($objectOrArray, $propertyPath)
{
if (is_string($propertyPath)) {
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
try {
@ -173,10 +139,6 @@ class PropertyAccessor implements PropertyAccessorInterface
$objectOrArray = $propertyValues[$i][self::VALUE];
if ($overwrite) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
return false;
}
$property = $propertyPath->getElement($i);
if ($propertyPath->isIndex($i)) {
@ -194,9 +156,9 @@ class PropertyAccessor implements PropertyAccessorInterface
}
return true;
} catch (NoSuchIndexException $e) {
} catch (AccessException $e) {
return false;
} catch (NoSuchPropertyException $e) {
} catch (UnexpectedTypeException $e) {
return false;
}
}
@ -217,13 +179,13 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true)
{
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, 0);
}
$propertyValues = array();
for ($i = 0; $i < $lastIndex; ++$i) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, $i);
}
$property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i);
@ -264,6 +226,11 @@ class PropertyAccessor implements PropertyAccessorInterface
$objectOrArray = & $propertyValue[self::VALUE];
// the final value of the path must not be validated
if ($i + 1 < $propertyPath->getLength() && !is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, $i+1);
}
$propertyValues[] = & $propertyValue;
}

View File

@ -197,7 +197,7 @@ class StringUtil
}
// Convert teeth to tooth, feet to foot
if (false !== ($pos = strpos($plural, 'ee')) && strlen($plural) > 3) {
if (false !== ($pos = strpos($plural, 'ee')) && strlen($plural) > 3 && 'feedback' !== $plural) {
return substr_replace($plural, 'oo', $pos, 2);
}

View File

@ -29,6 +29,20 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
$this->propertyAccessor = new PropertyAccessor();
}
public function getPathsWithUnexpectedType()
{
return array(
array('', 'foobar'),
array('foo', 'foobar'),
array(null, 'foobar'),
array(123, 'foobar'),
array((object) array('prop' => null), 'prop.foobar'),
array((object) array('prop' => (object) array('subProp' => null)), 'prop.subProp.foobar'),
array(array('index' => null), '[index][foobar]'),
array(array('index' => array('subIndex' => null)), '[index][subIndex][foobar]'),
);
}
public function getPathsWithMissingProperty()
{
return array(
@ -138,39 +152,13 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
}
/**
* @dataProvider getPathsWithUnexpectedType
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "string" while trying to traverse path "foobar" at property "foobar".
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray()
public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path)
{
$this->propertyAccessor->getValue('baz', 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "NULL" while trying to traverse path "foobar" at property "foobar".
*/
public function testGetValueThrowsExceptionIfNull()
{
$this->propertyAccessor->getValue(null, 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "string" while trying to traverse path "foobar" at property "foobar".
*/
public function testGetValueThrowsExceptionIfEmpty()
{
$this->propertyAccessor->getValue('', 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "NULL" while trying to traverse path "foobar.baz" at property "baz".
*/
public function testGetValueNestedExceptionMessage()
{
$this->propertyAccessor->getValue((object) array('foobar' => null), 'foobar.baz');
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
@ -260,47 +248,13 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
}
/**
* @dataProvider getPathsWithUnexpectedType
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "string" while trying to traverse path "foobar" at property "foobar".
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray()
public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path)
{
$value = 'baz';
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "NULL" while trying to traverse path "foobar" at property "foobar".
*/
public function testSetValueThrowsExceptionIfNull()
{
$value = null;
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "string" while trying to traverse path "foobar" at property "foobar".
*/
public function testSetValueThrowsExceptionIfEmpty()
{
$value = '';
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on, but it found type "NULL" while trying to traverse path "foobar.baz" at property "baz".
*/
public function testSetValueNestedExceptionMessage()
{
$value = (object) array('foobar' => null);
$this->propertyAccessor->setValue($value, 'foobar.baz', 'bam');
$this->propertyAccessor->setValue($objectOrArray, $path, 'value');
}
public function testGetValueWhenArrayValueIsNull()
@ -362,19 +316,12 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsReadableThrowsExceptionIfNotObjectOrArray()
/**
* @dataProvider getPathsWithUnexpectedType
*/
public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isReadable('baz', 'foobar'));
}
public function testIsReadableThrowsExceptionIfNull()
{
$this->assertFalse($this->propertyAccessor->isReadable(null, 'foobar'));
}
public function testIsReadableThrowsExceptionIfEmpty()
{
$this->assertFalse($this->propertyAccessor->isReadable('', 'foobar'));
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
@ -430,19 +377,12 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testNotObjectOrArrayIsNotWritable()
/**
* @dataProvider getPathsWithUnexpectedType
*/
public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isWritable('baz', 'foobar'));
}
public function testNullIsNotWritable()
{
$this->assertFalse($this->propertyAccessor->isWritable(null, 'foobar'));
}
public function testEmptyIsNotWritable()
{
$this->assertFalse($this->propertyAccessor->isWritable('', 'foobar'));
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path));
}
public function getValidPropertyPaths()

View File

@ -59,6 +59,7 @@ class StringUtilTest extends \PHPUnit_Framework_TestCase
array('data', array('daton', 'datum')),
array('days', 'day'),
array('discos', 'disco'),
array('devices', array('devex', 'devix', 'device')),
array('drives', 'drive'),
array('drivers', 'driver'),
array('dwarves', array('dwarf', 'dwarve', 'dwarff')),
@ -67,6 +68,7 @@ class StringUtilTest extends \PHPUnit_Framework_TestCase
array('emphases', array('emphas', 'emphase', 'emphasis')),
array('faxes', 'fax'),
array('feet', 'foot'),
array('feedback', 'feedback'),
array('foci', 'focus'),
array('focuses', array('focus', 'focuse', 'focusis')),
array('formulae', 'formula'),

View File

@ -14,11 +14,25 @@ namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\MappingException;
/**
* Base loader for validation metadata.
*
* This loader supports the loading of constraints from Symfony's default
* namespace (see {@link DEFAULT_NAMESPACE}) using the short class names of
* those constraints. Constraints can also be loaded using their fully
* qualified class names. At last, namespace aliases can be defined to load
* constraints with the syntax "alias:ShortName".
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractLoader implements LoaderInterface
{
/**
* Contains all known namespaces indexed by their prefix.
*
* The namespace to load constraints from by default.
*/
const DEFAULT_NAMESPACE = '\\Symfony\\Component\\Validator\\Constraints\\';
/**
* @var array
*/
protected $namespaces = array();
@ -26,6 +40,13 @@ abstract class AbstractLoader implements LoaderInterface
/**
* Adds a namespace alias.
*
* The namespace alias can be used to reference constraints from specific
* namespaces in {@link newConstraint()}:
*
* $this->addNamespaceAlias('mynamespace', '\\Acme\\Package\\Constraints\\');
*
* $constraint = $this->newConstraint('mynamespace:NotNull');
*
* @param string $alias The alias
* @param string $namespace The PHP namespace
*/
@ -37,16 +58,19 @@ abstract class AbstractLoader implements LoaderInterface
/**
* Creates a new constraint instance for the given constraint name.
*
* @param string $name The constraint name. Either a constraint relative
* to the default constraint namespace, or a fully
* qualified class name
* @param mixed $options The constraint options
* @param string $name The constraint name. Either a constraint relative
* to the default constraint namespace, or a fully
* qualified class name. Alternatively, the constraint
* may be preceded by a namespace alias and a colon.
* The namespace alias must have been defined using
* {@link addNamespaceAlias()}.
* @param mixed $options The constraint options
*
* @return Constraint
*
* @throws MappingException If the namespace prefix is undefined
*/
protected function newConstraint($name, $options)
protected function newConstraint($name, $options = null)
{
if (strpos($name, '\\') !== false && class_exists($name)) {
$className = (string) $name;
@ -59,7 +83,7 @@ abstract class AbstractLoader implements LoaderInterface
$className = $this->namespaces[$prefix].$className;
} else {
$className = 'Symfony\\Component\\Validator\\Constraints\\'.$name;
$className = self::DEFAULT_NAMESPACE.$name;
}
return new $className($options);

View File

@ -19,8 +19,16 @@ use Symfony\Component\Validator\Constraints\GroupSequenceProvider;
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* Loads validation metadata using a Doctrine annotation {@link Reader}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class AnnotationLoader implements LoaderInterface
{
/**
* @var Reader
*/
protected $reader;
public function __construct(Reader $reader)
@ -35,7 +43,7 @@ class AnnotationLoader implements LoaderInterface
{
$reflClass = $metadata->getReflectionClass();
$className = $reflClass->name;
$loaded = false;
$success = false;
foreach ($this->reader->getClassAnnotations($reflClass) as $constraint) {
if ($constraint instanceof GroupSequence) {
@ -46,7 +54,7 @@ class AnnotationLoader implements LoaderInterface
$metadata->addConstraint($constraint);
}
$loaded = true;
$success = true;
}
foreach ($reflClass->getProperties() as $property) {
@ -56,7 +64,7 @@ class AnnotationLoader implements LoaderInterface
$metadata->addPropertyConstraint($property->name, $constraint);
}
$loaded = true;
$success = true;
}
}
}
@ -77,11 +85,11 @@ class AnnotationLoader implements LoaderInterface
}
}
$loaded = true;
$success = true;
}
}
}
return $loaded;
return $success;
}
}

View File

@ -13,26 +13,42 @@ namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Exception\MappingException;
/**
* Base loader for loading validation metadata from a file.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see YamlFileLoader
* @see XmlFileLoader
*/
abstract class FileLoader extends AbstractLoader
{
/**
* The file to load.
*
* @var string
*/
protected $file;
/**
* Constructor.
* Creates a new loader.
*
* @param string $file The mapping file to load
*
* @throws MappingException if the mapping file does not exist
* @throws MappingException if the mapping file is not readable
* @throws MappingException If the file does not exist or is not readable
*/
public function __construct($file)
{
if (!is_file($file)) {
throw new MappingException(sprintf('The mapping file %s does not exist', $file));
throw new MappingException(sprintf('The mapping file "%s" does not exist', $file));
}
if (!is_readable($file)) {
throw new MappingException(sprintf('The mapping file %s is not readable', $file));
throw new MappingException(sprintf('The mapping file "%s" is not readable', $file));
}
if (!stream_is_local($this->file)) {
throw new MappingException(sprintf('The mapping file "%s" is not a local file', $file));
}
$this->file = $file;

View File

@ -12,21 +12,20 @@
namespace Symfony\Component\Validator\Mapping\Loader;
/**
* Creates mapping loaders for array of files.
*
* Abstract class, used by
* Base loader for loading validation metadata from a list of files.
*
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see YamlFileLoader
* @see XmlFileLoader
* @see YamlFilesLoader
* @see XmlFilesLoader
*/
abstract class FilesLoader extends LoaderChain
{
/**
* Array of mapping files.
* Creates a new loader.
*
* @param array $paths Array of file paths
* @param array $paths An array of file paths
*/
public function __construct(array $paths)
{
@ -34,15 +33,16 @@ abstract class FilesLoader extends LoaderChain
}
/**
* Array of mapping files.
* Returns an array of file loaders for the given file paths.
*
* @param array $paths Array of file paths
* @param array $paths An array of file paths
*
* @return LoaderInterface[] Array of metadata loaders
* @return LoaderInterface[] The metadata loaders
*/
protected function getFileLoaders($paths)
{
$loaders = array();
foreach ($paths as $path) {
$loaders[] = $this->getFileLoaderInstance($path);
}
@ -51,11 +51,11 @@ abstract class FilesLoader extends LoaderChain
}
/**
* Takes mapping file path.
* Creates a loader for the given file path.
*
* @param string $file
* @param string $path The file path
*
* @return LoaderInterface
* @return LoaderInterface The created loader
*/
abstract protected function getFileLoaderInstance($file);
abstract protected function getFileLoaderInstance($path);
}

View File

@ -15,25 +15,25 @@ use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* Calls multiple LoaderInterface instances in a chain.
* Loads validation metadata from multiple {@link LoaderInterface} instances.
*
* This class accepts multiple instances of LoaderInterface to be passed to the
* constructor. When loadClassMetadata() is called, the same method is called
* in <em>all</em> of these loaders, regardless of whether any of them was
* successful or not.
* Pass the loaders when constructing the chain. Once
* {@link loadClassMetadata()} is called, that method will be called on all
* loaders in the chain.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class LoaderChain implements LoaderInterface
{
/**
* @var LoaderInterface[]
*/
protected $loaders;
/**
* Accepts a list of LoaderInterface instances.
* @param LoaderInterface[] $loaders The metadata loaders to use
*
* @param LoaderInterface[] $loaders An array of LoaderInterface instances
*
* @throws MappingException If any of the loaders does not implement LoaderInterface
* @throws MappingException If any of the loaders has an invalid type
*/
public function __construct(array $loaders)
{

View File

@ -13,14 +13,19 @@ namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* Loads validation metadata into {@link ClassMetadata} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface LoaderInterface
{
/**
* Load a Class Metadata.
* Loads validation metadata into a {@link ClassMetadata} instance.
*
* @param ClassMetadata $metadata A metadata
* @param ClassMetadata $metadata The metadata to load
*
* @return bool
* @return bool Whether the loader succeeded
*/
public function loadClassMetadata(ClassMetadata $metadata);
}

View File

@ -14,10 +14,25 @@ namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* Loads validation metadata by calling a static method on the loaded class.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class StaticMethodLoader implements LoaderInterface
{
/**
* The name of the method to call.
*
* @var string
*/
protected $methodName;
/**
* Creates a new loader.
*
* @param string $methodName The name of the static method to call
*/
public function __construct($methodName = 'loadValidatorMetadata')
{
$this->methodName = $methodName;

View File

@ -15,10 +15,15 @@ use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* Loads validation metadata from an XML file.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class XmlFileLoader extends FileLoader
{
/**
* An array of SimpleXMLElement instances.
* The XML nodes of the mapping file.
*
* @var \SimpleXMLElement[]|null
*/
@ -30,9 +35,12 @@ class XmlFileLoader extends FileLoader
public function loadClassMetadata(ClassMetadata $metadata)
{
if (null === $this->classes) {
$this->classes = array();
// 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));
}
@ -43,33 +51,9 @@ class XmlFileLoader extends FileLoader
}
if (isset($this->classes[$metadata->getClassName()])) {
$xml = $this->classes[$metadata->getClassName()];
$classDescription = $this->classes[$metadata->getClassName()];
foreach ($xml->{'group-sequence-provider'} as $provider) {
$metadata->setGroupSequenceProvider(true);
}
foreach ($xml->{'group-sequence'} as $groupSequence) {
if (count($groupSequence->value) > 0) {
$metadata->setGroupSequence($this->parseValues($groupSequence[0]->value));
}
}
foreach ($this->parseConstraints($xml->constraint) as $constraint) {
$metadata->addConstraint($constraint);
}
foreach ($xml->property as $property) {
foreach ($this->parseConstraints($property->constraint) as $constraint) {
$metadata->addPropertyConstraint((string) $property['name'], $constraint);
}
}
foreach ($xml->getter as $getter) {
foreach ($this->parseConstraints($getter->constraint) as $constraint) {
$metadata->addGetterConstraint((string) $getter['property'], $constraint);
}
}
$this->loadClassMetadataFromXml($metadata, $classDescription);
return true;
}
@ -179,22 +163,57 @@ class XmlFileLoader extends FileLoader
}
/**
* Parse a XML File.
* Loads the XML class descriptions from the given file.
*
* @param string $file Path of file
* @param string $path The path of the XML file
*
* @return \SimpleXMLElement
* @return \SimpleXMLElement The class descriptions
*
* @throws MappingException
* @throws MappingException If the file could not be loaded
*/
protected function parseFile($file)
protected function parseFile($path)
{
try {
$dom = XmlUtils::loadFile($file, __DIR__.'/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd');
$dom = XmlUtils::loadFile($path, __DIR__.'/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd');
} catch (\Exception $e) {
throw new MappingException($e->getMessage(), $e->getCode(), $e);
}
return simplexml_import_dom($dom);
}
/**
* Loads the validation metadata from the given XML class description.
*
* @param ClassMetadata $metadata The metadata to load
* @param array $classDescription The XML class description
*/
private function loadClassMetadataFromXml(ClassMetadata $metadata, $classDescription)
{
foreach ($classDescription->{'group-sequence-provider'} as $_) {
$metadata->setGroupSequenceProvider(true);
}
foreach ($classDescription->{'group-sequence'} as $groupSequence) {
if (count($groupSequence->value) > 0) {
$metadata->setGroupSequence($this->parseValues($groupSequence[0]->value));
}
}
foreach ($this->parseConstraints($classDescription->constraint) as $constraint) {
$metadata->addConstraint($constraint);
}
foreach ($classDescription->property as $property) {
foreach ($this->parseConstraints($property->constraint) as $constraint) {
$metadata->addPropertyConstraint((string) $property['name'], $constraint);
}
}
foreach ($classDescription->getter as $getter) {
foreach ($this->parseConstraints($getter->constraint) as $constraint) {
$metadata->addGetterConstraint((string) $getter['property'], $constraint);
}
}
}
}

View File

@ -12,9 +12,10 @@
namespace Symfony\Component\Validator\Mapping\Loader;
/**
* Loads multiple xml mapping files.
* Loads validation metadata from a list of XML files.
*
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see FilesLoader
*/

View File

@ -14,10 +14,13 @@ namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Yaml\Parser as YamlParser;
/**
* Loads validation metadata from a YAML file.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class YamlFileLoader extends FileLoader
{
private $yamlParser;
/**
* An array of YAML class descriptions.
*
@ -25,35 +28,30 @@ class YamlFileLoader extends FileLoader
*/
protected $classes = null;
/**
* Caches the used YAML parser.
*
* @var YamlParser
*/
private $yamlParser;
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata)
{
if (null === $this->classes) {
if (!stream_is_local($this->file)) {
throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $this->file));
}
if (!file_exists($this->file)) {
throw new \InvalidArgumentException(sprintf('File "%s" not found.', $this->file));
}
if (null === $this->yamlParser) {
$this->yamlParser = new YamlParser();
}
$this->classes = $this->yamlParser->parse(file_get_contents($this->file));
// empty file
if (null === $this->classes) {
// This method may throw an exception. Do not modify the class'
// state before it completes
if (false === ($classes = $this->parseFile($this->file))) {
return false;
}
// not an array
if (!is_array($this->classes)) {
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $this->file));
}
$this->classes = $classes;
if (isset($this->classes['namespaces'])) {
foreach ($this->classes['namespaces'] as $alias => $namespace) {
@ -64,44 +62,10 @@ class YamlFileLoader extends FileLoader
}
}
// TODO validation
if (isset($this->classes[$metadata->getClassName()])) {
$yaml = $this->classes[$metadata->getClassName()];
$classDescription = $this->classes[$metadata->getClassName()];
if (isset($yaml['group_sequence_provider'])) {
$metadata->setGroupSequenceProvider((bool) $yaml['group_sequence_provider']);
}
if (isset($yaml['group_sequence'])) {
$metadata->setGroupSequence($yaml['group_sequence']);
}
if (isset($yaml['constraints']) && is_array($yaml['constraints'])) {
foreach ($this->parseNodes($yaml['constraints']) as $constraint) {
$metadata->addConstraint($constraint);
}
}
if (isset($yaml['properties']) && is_array($yaml['properties'])) {
foreach ($yaml['properties'] as $property => $constraints) {
if (null !== $constraints) {
foreach ($this->parseNodes($constraints) as $constraint) {
$metadata->addPropertyConstraint($property, $constraint);
}
}
}
}
if (isset($yaml['getters']) && is_array($yaml['getters'])) {
foreach ($yaml['getters'] as $getter => $constraints) {
if (null !== $constraints) {
foreach ($this->parseNodes($constraints) as $constraint) {
$metadata->addGetterConstraint($getter, $constraint);
}
}
}
}
$this->loadClassMetadataFromYaml($metadata, $classDescription);
return true;
}
@ -140,4 +104,76 @@ class YamlFileLoader extends FileLoader
return $values;
}
/**
* Loads the YAML class descriptions from the given file.
*
* @param string $path The path of the YAML file
*
* @return array|null The class descriptions or null, if the file was empty
*
* @throws \InvalidArgumentException If the file could not be loaded or did
* not contain a YAML array
*/
private function parseFile($path)
{
$classes = $this->yamlParser->parse(file_get_contents($path));
// empty file
if (null === $classes) {
return;
}
// not an array
if (!is_array($classes)) {
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $this->file));
}
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 loadClassMetadataFromYaml(ClassMetadata $metadata, array $classDescription)
{
if (isset($classDescription['group_sequence_provider'])) {
$metadata->setGroupSequenceProvider(
(bool) $classDescription['group_sequence_provider']
);
}
if (isset($classDescription['group_sequence'])) {
$metadata->setGroupSequence($classDescription['group_sequence']);
}
if (isset($classDescription['constraints']) && is_array($classDescription['constraints'])) {
foreach ($this->parseNodes($classDescription['constraints']) as $constraint) {
$metadata->addConstraint($constraint);
}
}
if (isset($classDescription['properties']) && is_array($classDescription['properties'])) {
foreach ($classDescription['properties'] as $property => $constraints) {
if (null !== $constraints) {
foreach ($this->parseNodes($constraints) as $constraint) {
$metadata->addPropertyConstraint($property, $constraint);
}
}
}
}
if (isset($classDescription['getters']) && is_array($classDescription['getters'])) {
foreach ($classDescription['getters'] as $getter => $constraints) {
if (null !== $constraints) {
foreach ($this->parseNodes($constraints) as $constraint) {
$metadata->addGetterConstraint($getter, $constraint);
}
}
}
}
}
}

View File

@ -12,9 +12,10 @@
namespace Symfony\Component\Validator\Mapping\Loader;
/**
* Loads multiple yaml mapping files.
* Loads validation metadata from a list of YAML files.
*
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see FilesLoader
*/

View File

@ -19,6 +19,7 @@ use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\True;
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
@ -105,15 +106,28 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, $metadata);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\MappingException
* @expectedExceptionMessage Document types are not allowed.
*/
public function testDocTypeIsNotAllowed()
public function testThrowExceptionIfDocTypeIsSet()
{
$loader = new XmlFileLoader(__DIR__.'/withdoctype.xml');
$metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity');
$this->setExpectedException('\Symfony\Component\Validator\Exception\MappingException');
$loader->loadClassMetadata($metadata);
}
/**
* @see https://github.com/symfony/symfony/pull/12158
*/
public function testDoNotModifyStateIfExceptionIsThrown()
{
$loader = new XmlFileLoader(__DIR__.'/withdoctype.xml');
$metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity');
try {
$loader->loadClassMetadata($metadata);
} catch (MappingException $e) {
$this->setExpectedException('\Symfony\Component\Validator\Exception\MappingException');
$loader->loadClassMetadata($metadata);
}
}
}

View File

@ -33,16 +33,31 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($loader->loadClassMetadata($metadata));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testLoadClassMetadataThrowsExceptionIfNotAnArray()
{
$loader = new YamlFileLoader(__DIR__.'/nonvalid-mapping.yml');
$metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity');
$this->setExpectedException('\InvalidArgumentException');
$loader->loadClassMetadata($metadata);
}
/**
* @see https://github.com/symfony/symfony/pull/12158
*/
public function testDoNotModifyStateIfExceptionIsThrown()
{
$loader = new YamlFileLoader(__DIR__.'/nonvalid-mapping.yml');
$metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity');
try {
$loader->loadClassMetadata($metadata);
} catch (\InvalidArgumentException $e) {
// Call again. Again an exception should be thrown
$this->setExpectedException('\InvalidArgumentException');
$loader->loadClassMetadata($metadata);
}
}
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml');

View File

@ -0,0 +1,3 @@
composer.lock
phpunit.xml
vendor/