Initial import

This commit is contained in:
sas.fajri
2026-05-25 20:01:37 +07:00
commit 710d7c1b97
10371 changed files with 2381698 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,46 @@
CHANGELOG
=========
2.6.0
-----
* deprecated OptionsResolverInterface
* [BC BREAK] removed "array" type hint from OptionsResolverInterface methods
setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and
addAllowedTypes()
* added OptionsResolver::setDefault()
* added OptionsResolver::hasDefault()
* added OptionsResolver::setNormalizer()
* added OptionsResolver::isRequired()
* added OptionsResolver::getRequiredOptions()
* added OptionsResolver::isMissing()
* added OptionsResolver::getMissingOptions()
* added OptionsResolver::setDefined()
* added OptionsResolver::isDefined()
* added OptionsResolver::getDefinedOptions()
* added OptionsResolver::remove()
* added OptionsResolver::clear()
* deprecated OptionsResolver::replaceDefaults()
* deprecated OptionsResolver::setOptional() in favor of setDefined()
* deprecated OptionsResolver::isKnown() in favor of isDefined()
* [BC BREAK] OptionsResolver::isRequired() returns true now if a required
option has a default value set
* [BC BREAK] merged Options into OptionsResolver and turned Options into an
interface
* deprecated Options::overload() (now in OptionsResolver)
* deprecated Options::set() (now in OptionsResolver)
* deprecated Options::get() (now in OptionsResolver)
* deprecated Options::has() (now in OptionsResolver)
* deprecated Options::replace() (now in OptionsResolver)
* [BC BREAK] Options::get() (now in OptionsResolver) can only be used within
lazy option/normalizer closures now
* [BC BREAK] removed Traversable interface from Options since using within
lazy option/normalizer closures resulted in exceptions
* [BC BREAK] removed Options::all() since using within lazy option/normalizer
closures resulted in exceptions
* [BC BREAK] OptionDefinitionException now extends LogicException instead of
RuntimeException
* [BC BREAK] normalizers are not executed anymore for unset options
* normalizers are executed after validating the options now
* [BC BREAK] an UndefinedOptionsException is now thrown instead of an
InvalidOptionsException when non-existing options are passed

View File

@@ -0,0 +1,22 @@
<?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\OptionsResolver\Exception;
/**
* Thrown when trying to read an option outside of or write it inside of
* {@link \Symfony\Component\OptionsResolver\Options::resolve()}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class AccessException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\OptionsResolver\Exception;
/**
* Marker interface for all exceptions thrown by the OptionsResolver component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\OptionsResolver\Exception;
/**
* Thrown when an argument is invalid.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,23 @@
<?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\OptionsResolver\Exception;
/**
* Thrown when the value of an option does not match its validation rules.
*
* You should make sure a valid value is passed to the option.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidOptionsException extends InvalidArgumentException
{
}

View File

@@ -0,0 +1,23 @@
<?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\OptionsResolver\Exception;
/**
* Exception thrown when a required option is missing.
*
* Add the option to the passed options array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MissingOptionsException extends InvalidArgumentException
{
}

View File

@@ -0,0 +1,26 @@
<?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\OptionsResolver\Exception;
/**
* Thrown when trying to read an option that has no value set.
*
* When accessing optional options from within a lazy option or normalizer you should first
* check whether the optional option is set. You can do this with `isset($options['optional'])`.
* In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can
* occur when evaluating lazy options.
*
* @author Tobias Schultze <http://tobion.de>
*/
class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\OptionsResolver\Exception;
/**
* Thrown when two lazy options have a cyclic dependency.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class OptionDefinitionException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,24 @@
<?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\OptionsResolver\Exception;
/**
* Exception thrown when an undefined option is passed.
*
* You should remove the options in question from your code or define them
* beforehand.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UndefinedOptionsException extends InvalidArgumentException
{
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2018 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,22 @@
<?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\OptionsResolver;
/**
* Contains resolved option values.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*/
interface Options extends \ArrayAccess, \Countable
{
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
<?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\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since version 2.6, to be removed in 3.0. Use {@link OptionsResolver} instead.
*/
interface OptionsResolverInterface
{
/**
* Sets default option values.
*
* The options can either be values of any types or closures that
* evaluate the option value lazily. These closures must have one
* of the following signatures:
*
* function (Options $options)
* function (Options $options, $value)
*
* The second parameter passed to the closure is the previously
* set default value, in case you are overwriting an existing
* default value.
*
* The closures should return the lazily created option value.
*
* @param array $defaultValues A list of option names as keys and default
* values or closures as values
*
* @return $this
*/
public function setDefaults(array $defaultValues);
/**
* Replaces default option values.
*
* Old defaults are erased, which means that closures passed here cannot
* access the previous default value. This may be useful to improve
* performance if the previous default value is calculated by an expensive
* closure.
*
* @param array $defaultValues A list of option names as keys and default
* values or closures as values
*
* @return $this
*/
public function replaceDefaults(array $defaultValues);
/**
* Sets optional options.
*
* This method declares valid option names without setting default values for them.
* If these options are not passed to {@link resolve()} and no default has been set
* for them, they will be missing in the final options array. This can be helpful
* if you want to determine whether an option has been set or not because otherwise
* {@link resolve()} would trigger an exception for unknown options.
*
* @param array $optionNames A list of option names
*
* @return $this
*/
public function setOptional(array $optionNames);
/**
* Sets required options.
*
* If these options are not passed to {@link resolve()} and no default has been set for
* them, an exception will be thrown.
*
* @param array $optionNames A list of option names
*
* @return $this
*/
public function setRequired($optionNames);
/**
* Sets allowed values for a list of options.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values
*
* @return $this
*
* @throws InvalidOptionsException if an option has not been defined
* (see {@link isKnown()}) for which
* an allowed value is set
*/
public function setAllowedValues($allowedValues);
/**
* Adds allowed values for a list of options.
*
* The values are merged with the allowed values defined previously.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values
*
* @return $this
*
* @throws InvalidOptionsException if an option has not been defined
* (see {@link isKnown()}) for which
* an allowed value is set
*/
public function addAllowedValues($allowedValues);
/**
* Sets allowed types for a list of options.
*
* @param array $allowedTypes A list of option names as keys and type
* names passed as string or array as values
*
* @return $this
*
* @throws InvalidOptionsException if an option has not been defined for
* which an allowed type is set
*/
public function setAllowedTypes($allowedTypes);
/**
* Adds allowed types for a list of options.
*
* The types are merged with the allowed types defined previously.
*
* @param array $allowedTypes A list of option names as keys and type
* names passed as string or array as values
*
* @return $this
*
* @throws InvalidOptionsException if an option has not been defined for
* which an allowed type is set
*/
public function addAllowedTypes($allowedTypes);
/**
* Sets normalizers that are applied on resolved options.
*
* The normalizers should be closures with the following signature:
*
* function (Options $options, $value)
*
* The second parameter passed to the closure is the value of
* the option.
*
* The closure should return the normalized value.
*
* @param array $normalizers An array of closures
*
* @return $this
*/
public function setNormalizers(array $normalizers);
/**
* Returns whether an option is known.
*
* An option is known if it has been passed to either {@link setDefaults()},
* {@link setRequired()} or {@link setOptional()} before.
*
* @param string $option The name of the option
*
* @return bool Whether the option is known
*/
public function isKnown($option);
/**
* Returns whether an option is required.
*
* An option is required if it has been passed to {@link setRequired()},
* but not to {@link setDefaults()}. That is, the option has been declared
* as required and no default value has been set.
*
* @param string $option The name of the option
*
* @return bool Whether the option is required
*/
public function isRequired($option);
/**
* Returns the combination of the default and the passed options.
*
* @param array $options The custom option values
*
* @return array A list of options and their values
*
* @throws InvalidOptionsException if any of the passed options has not
* been defined or does not contain an
* allowed value
* @throws MissingOptionsException if a required option is missing
* @throws OptionDefinitionException if a cyclic dependency is detected
* between two lazy options
*/
public function resolve(array $options = array());
}

View File

@@ -0,0 +1,15 @@
OptionsResolver Component
=========================
The OptionsResolver component is `array_replace` on steroids. It allows you to
create an options system with required options, defaults, validation (type,
value), normalization and more.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/options_resolver.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,734 @@
<?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\OptionsResolver\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @group legacy
*/
class LegacyOptionsResolverTest extends TestCase
{
/**
* @var OptionsResolver
*/
private $resolver;
protected function setUp()
{
$this->resolver = new OptionsResolver();
}
public function testResolve()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));
$options = array(
'two' => '20',
);
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), $this->resolver->resolve($options));
}
public function testResolveNumericOptions()
{
$this->resolver->setDefaults(array(
'1' => '1',
'2' => '2',
));
$options = array(
'2' => '20',
);
$this->assertEquals(array(
'1' => '1',
'2' => '20',
), $this->resolver->resolve($options));
}
public function testResolveLazy()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => function (Options $options) {
return '20';
},
));
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), $this->resolver->resolve(array()));
}
public function testTypeAliasesForAllowedTypes()
{
$this->resolver->setDefaults(array(
'force' => false,
));
$this->resolver->setAllowedTypes(array(
'force' => 'boolean',
));
$this->assertSame(array('force' => true), $this->resolver->resolve(array(
'force' => true,
)));
}
public function testResolveLazyDependencyOnOptional()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => function (Options $options) {
return $options['one'].'2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->resolver->resolve($options));
}
public function testResolveLazyDependencyOnMissingOptionalWithoutDefault()
{
$test = $this;
$this->resolver->setOptional(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => function (Options $options) use ($test) {
/* @var TestCase $test */
$test->assertArrayNotHasKey('one', $options);
return '2';
},
));
$options = array();
$this->assertEquals(array(
'two' => '2',
), $this->resolver->resolve($options));
}
public function testResolveLazyDependencyOnOptionalWithoutDefault()
{
$test = $this;
$this->resolver->setOptional(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => function (Options $options) use ($test) {
/* @var TestCase $test */
$test->assertArrayHasKey('one', $options);
return $options['one'].'2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->resolver->resolve($options));
}
public function testResolveLazyDependencyOnRequired()
{
$this->resolver->setRequired(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => function (Options $options) {
return $options['one'].'2';
},
));
$options = array(
'one' => '10',
);
$this->assertEquals(array(
'one' => '10',
'two' => '102',
), $this->resolver->resolve($options));
}
public function testResolveLazyReplaceDefaults()
{
$test = $this;
$this->resolver->setDefaults(array(
'one' => function (Options $options) use ($test) {
/* @var TestCase $test */
$test->fail('Previous closure should not be executed');
},
));
$this->resolver->replaceDefaults(array(
'one' => function (Options $options, $previousValue) {
return '1';
},
));
$this->assertEquals(array(
'one' => '1',
), $this->resolver->resolve(array()));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
* @expectedExceptionMessage The option "foo" does not exist. Defined options are: "one", "three", "two".
*/
public function testResolveFailsIfNonExistingOption()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setRequired(array(
'two',
));
$this->resolver->setOptional(array(
'three',
));
$this->resolver->resolve(array(
'foo' => 'bar',
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
*/
public function testResolveFailsIfMissingRequiredOption()
{
$this->resolver->setRequired(array(
'one',
));
$this->resolver->setDefaults(array(
'two' => '2',
));
$this->resolver->resolve(array(
'two' => '20',
));
}
public function testResolveSucceedsIfOptionValueAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedValues(array(
'one' => array('1', 'one'),
));
$options = array(
'one' => 'one',
);
$this->assertEquals(array(
'one' => 'one',
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionValueAllowed2()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));
$this->resolver->setAllowedValues(array(
'one' => '1',
'two' => '2',
));
$this->resolver->addAllowedValues(array(
'one' => 'one',
'two' => 'two',
));
$options = array(
'one' => '1',
'two' => 'two',
);
$this->assertEquals(array(
'one' => '1',
'two' => 'two',
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionalWithAllowedValuesNotSet()
{
$this->resolver->setRequired(array(
'one',
));
$this->resolver->setOptional(array(
'two',
));
$this->resolver->setAllowedValues(array(
'one' => array('1', 'one'),
'two' => array('2', 'two'),
));
$options = array(
'one' => '1',
);
$this->assertEquals(array(
'one' => '1',
), $this->resolver->resolve($options));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionValueNotAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedValues(array(
'one' => array('1', 'one'),
));
$this->resolver->resolve(array(
'one' => '2',
));
}
public function testResolveSucceedsIfOptionTypeAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedTypes(array(
'one' => 'string',
));
$options = array(
'one' => 'one',
);
$this->assertEquals(array(
'one' => 'one',
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionTypeAllowedPassArray()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedTypes(array(
'one' => array('string', 'bool'),
));
$options = array(
'one' => true,
);
$this->assertEquals(array(
'one' => true,
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionTypeAllowedPassObject()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedTypes(array(
'one' => 'object',
));
$object = new \stdClass();
$options = array(
'one' => $object,
);
$this->assertEquals(array(
'one' => $object,
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionTypeAllowedPassClass()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedTypes(array(
'one' => '\stdClass',
));
$object = new \stdClass();
$options = array(
'one' => $object,
);
$this->assertEquals(array(
'one' => $object,
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionTypeAllowedAddTypes()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));
$this->resolver->setAllowedTypes(array(
'one' => 'string',
'two' => 'bool',
));
$this->resolver->addAllowedTypes(array(
'one' => 'float',
'two' => 'integer',
));
$options = array(
'one' => 1.23,
'two' => false,
);
$this->assertEquals(array(
'one' => 1.23,
'two' => false,
), $this->resolver->resolve($options));
}
public function testResolveSucceedsIfOptionalWithTypeAndWithoutValue()
{
$this->resolver->setOptional(array(
'one',
'two',
));
$this->resolver->setAllowedTypes(array(
'one' => 'string',
'two' => 'int',
));
$options = array(
'two' => 1,
);
$this->assertEquals(array(
'two' => 1,
), $this->resolver->resolve($options));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionTypeNotAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedTypes(array(
'one' => array('string', 'bool'),
));
$this->resolver->resolve(array(
'one' => 1.23,
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionTypeNotAllowedMultipleOptions()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));
$this->resolver->setAllowedTypes(array(
'one' => 'string',
'two' => 'bool',
));
$this->resolver->resolve(array(
'one' => 'foo',
'two' => 1.23,
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionTypeNotAllowedAddTypes()
{
$this->resolver->setDefaults(array(
'one' => '1',
));
$this->resolver->setAllowedTypes(array(
'one' => 'string',
));
$this->resolver->addAllowedTypes(array(
'one' => 'bool',
));
$this->resolver->resolve(array(
'one' => 1.23,
));
}
public function testFluidInterface()
{
$this->resolver->setDefaults(array('one' => '1'))
->replaceDefaults(array('one' => '2'))
->setAllowedValues(array('one' => array('1', '2')))
->addAllowedValues(array('one' => array('3')))
->setRequired(array('two'))
->setOptional(array('three'));
$options = array(
'two' => '2',
);
$this->assertEquals(array(
'one' => '2',
'two' => '2',
), $this->resolver->resolve($options));
}
public function testKnownIfDefaultWasSet()
{
$this->assertFalse($this->resolver->isKnown('foo'));
$this->resolver->setDefaults(array(
'foo' => 'bar',
));
$this->assertTrue($this->resolver->isKnown('foo'));
}
public function testKnownIfRequired()
{
$this->assertFalse($this->resolver->isKnown('foo'));
$this->resolver->setRequired(array(
'foo',
));
$this->assertTrue($this->resolver->isKnown('foo'));
}
public function testKnownIfOptional()
{
$this->assertFalse($this->resolver->isKnown('foo'));
$this->resolver->setOptional(array(
'foo',
));
$this->assertTrue($this->resolver->isKnown('foo'));
}
public function testRequiredIfRequired()
{
$this->assertFalse($this->resolver->isRequired('foo'));
$this->resolver->setRequired(array(
'foo',
));
$this->assertTrue($this->resolver->isRequired('foo'));
}
public function testNormalizersTransformFinalOptions()
{
$this->resolver->setDefaults(array(
'foo' => 'bar',
'bam' => 'baz',
));
$this->resolver->setNormalizers(array(
'foo' => function (Options $options, $value) {
return $options['bam'].'['.$value.']';
},
));
$expected = array(
'foo' => 'baz[bar]',
'bam' => 'baz',
);
$this->assertEquals($expected, $this->resolver->resolve(array()));
$expected = array(
'foo' => 'boo[custom]',
'bam' => 'boo',
);
$this->assertEquals($expected, $this->resolver->resolve(array(
'foo' => 'custom',
'bam' => 'boo',
)));
}
public function testResolveWithoutOptionSucceedsIfRequiredAndDefaultValue()
{
$this->resolver->setRequired(array(
'foo',
));
$this->resolver->setDefaults(array(
'foo' => 'bar',
));
$this->assertEquals(array(
'foo' => 'bar',
), $this->resolver->resolve(array()));
}
public function testResolveWithoutOptionSucceedsIfDefaultValueAndRequired()
{
$this->resolver->setDefaults(array(
'foo' => 'bar',
));
$this->resolver->setRequired(array(
'foo',
));
$this->assertEquals(array(
'foo' => 'bar',
), $this->resolver->resolve(array()));
}
public function testResolveSucceedsIfOptionRequiredAndValueAllowed()
{
$this->resolver->setRequired(array(
'one', 'two',
));
$this->resolver->setAllowedValues(array(
'two' => array('2'),
));
$options = array(
'one' => '1',
'two' => '2',
);
$this->assertEquals($options, $this->resolver->resolve($options));
}
public function testResolveSucceedsIfValueAllowedCallbackReturnsTrue()
{
$this->resolver->setRequired(array(
'test',
));
$this->resolver->setAllowedValues(array(
'test' => function ($value) {
return true;
},
));
$options = array(
'test' => true,
);
$this->assertEquals($options, $this->resolver->resolve($options));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfValueAllowedCallbackReturnsFalse()
{
$this->resolver->setRequired(array(
'test',
));
$this->resolver->setAllowedValues(array(
'test' => function ($value) {
return false;
},
));
$options = array(
'test' => true,
);
$this->assertEquals($options, $this->resolver->resolve($options));
}
public function testClone()
{
$this->resolver->setDefaults(array('one' => '1'));
$clone = clone $this->resolver;
// Changes after cloning don't affect each other
$this->resolver->setDefaults(array('two' => '2'));
$clone->setDefaults(array('three' => '3'));
$this->assertEquals(array(
'one' => '1',
'two' => '2',
), $this->resolver->resolve());
$this->assertEquals(array(
'one' => '1',
'three' => '3',
), $clone->resolve());
}
public function testOverloadReturnsThis()
{
$this->assertSame($this->resolver, $this->resolver->overload('foo', 'bar'));
}
public function testOverloadCallsSet()
{
$this->resolver->overload('foo', 'bar');
$this->assertSame(array('foo' => 'bar'), $this->resolver->resolve());
}
}

View File

@@ -0,0 +1,336 @@
<?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\OptionsResolver\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @group legacy
*/
class LegacyOptionsTest extends TestCase
{
/**
* @var OptionsResolver
*/
private $options;
protected function setUp()
{
$this->options = new OptionsResolver();
}
public function testSetLazyOption()
{
$this->options->set('foo', function (Options $options) {
return 'dynamic';
});
$this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve());
}
public function testOverloadKeepsPreviousValue()
{
$test = $this;
// defined by superclass
$this->options->set('foo', 'bar');
// defined by subclass
$this->options->overload('foo', function (Options $options, $previousValue) use ($test) {
/* @var TestCase $test */
$test->assertEquals('bar', $previousValue);
return 'dynamic';
});
$this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve());
}
public function testPreviousValueIsEvaluatedIfLazy()
{
$test = $this;
// defined by superclass
$this->options->set('foo', function (Options $options) {
return 'bar';
});
// defined by subclass
$this->options->overload('foo', function (Options $options, $previousValue) use ($test) {
/* @var TestCase $test */
$test->assertEquals('bar', $previousValue);
return 'dynamic';
});
$this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve());
}
public function testPreviousValueIsNotEvaluatedIfNoSecondArgument()
{
$test = $this;
// defined by superclass
$this->options->set('foo', function (Options $options) use ($test) {
$test->fail('Should not be called');
});
// defined by subclass, no $previousValue argument defined!
$this->options->overload('foo', function (Options $options) {
return 'dynamic';
});
$this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve());
}
public function testLazyOptionCanAccessOtherOptions()
{
$test = $this;
$this->options->set('foo', 'bar');
$this->options->set('bam', function (Options $options) use ($test) {
/* @var TestCase $test */
$test->assertEquals('bar', $options->get('foo'));
return 'dynamic';
});
$this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve());
}
public function testLazyOptionCanAccessOtherLazyOptions()
{
$test = $this;
$this->options->set('foo', function (Options $options) {
return 'bar';
});
$this->options->set('bam', function (Options $options) use ($test) {
/* @var TestCase $test */
$test->assertEquals('bar', $options->get('foo'));
return 'dynamic';
});
$this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve());
}
public function testNormalizer()
{
$this->options->set('foo', 'bar');
$this->options->setNormalizer('foo', function () {
return 'normalized';
});
$this->assertEquals(array('foo' => 'normalized'), $this->options->resolve());
}
public function testNormalizerReceivesUnnormalizedValue()
{
$this->options->set('foo', 'bar');
$this->options->setNormalizer('foo', function (Options $options, $value) {
return 'normalized['.$value.']';
});
$this->assertEquals(array('foo' => 'normalized[bar]'), $this->options->resolve());
}
public function testNormalizerCanAccessOtherOptions()
{
$test = $this;
$this->options->set('foo', 'bar');
$this->options->set('bam', 'baz');
$this->options->setNormalizer('bam', function (Options $options) use ($test) {
/* @var TestCase $test */
$test->assertEquals('bar', $options->get('foo'));
return 'normalized';
});
$this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve());
}
public function testNormalizerCanAccessOtherLazyOptions()
{
$test = $this;
$this->options->set('foo', function (Options $options) {
return 'bar';
});
$this->options->set('bam', 'baz');
$this->options->setNormalizer('bam', function (Options $options) use ($test) {
/* @var TestCase $test */
$test->assertEquals('bar', $options->get('foo'));
return 'normalized';
});
$this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve());
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testFailForCyclicDependencies()
{
$this->options->set('foo', function (Options $options) {
$options->get('bam');
});
$this->options->set('bam', function (Options $options) {
$options->get('foo');
});
$this->options->resolve();
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testFailForCyclicDependenciesBetweenNormalizers()
{
$this->options->set('foo', 'bar');
$this->options->set('bam', 'baz');
$this->options->setNormalizer('foo', function (Options $options) {
$options->get('bam');
});
$this->options->setNormalizer('bam', function (Options $options) {
$options->get('foo');
});
$this->options->resolve();
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption()
{
$this->options->set('foo', function (Options $options) {
$options->get('bam');
});
$this->options->set('bam', 'baz');
$this->options->setNormalizer('bam', function (Options $options) {
$options->get('foo');
});
$this->options->resolve();
}
public function testReplaceClearsAndSets()
{
$this->options->set('one', '1');
$this->options->replace(array(
'two' => '2',
'three' => function (Options $options) {
return '2' === $options['two'] ? '3' : 'foo';
},
));
$this->assertEquals(array(
'two' => '2',
'three' => '3',
), $this->options->resolve());
}
public function testClearRemovesAllOptions()
{
$this->options->set('one', 1);
$this->options->set('two', 2);
$this->options->clear();
$this->assertEmpty($this->options->resolve());
}
public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams()
{
$this->options->set('foo', 'bar');
$this->options->overload('foo', function () {
return 'test';
});
$resolved = $this->options->resolve();
$this->assertInternalType('callable', $resolved['foo']);
}
public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint()
{
$this->options->set('foo', 'bar');
$this->options->overload('foo', function ($object) {
return 'test';
});
$resolved = $this->options->resolve();
$this->assertInternalType('callable', $resolved['foo']);
}
public function testRemoveOptionAndNormalizer()
{
$this->options->set('foo1', 'bar');
$this->options->setNormalizer('foo1', function (Options $options) {
return '';
});
$this->options->set('foo2', 'bar');
$this->options->setNormalizer('foo2', function (Options $options) {
return '';
});
$this->options->remove('foo2');
$this->assertEquals(array('foo1' => ''), $this->options->resolve());
}
public function testReplaceOptionAndNormalizer()
{
$this->options->set('foo1', 'bar');
$this->options->setNormalizer('foo1', function (Options $options) {
return '';
});
$this->options->set('foo2', 'bar');
$this->options->setNormalizer('foo2', function (Options $options) {
return '';
});
$this->options->replace(array('foo1' => 'new'));
$this->assertEquals(array('foo1' => 'new'), $this->options->resolve());
}
public function testClearOptionAndNormalizer()
{
$this->options->set('foo1', 'bar');
$this->options->setNormalizer('foo1', function (Options $options) {
return '';
});
$this->options->set('foo2', 'bar');
$this->options->setNormalizer('foo2', function (Options $options) {
return '';
});
$this->options->clear();
$this->assertEmpty($this->options->resolve());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "symfony/options-resolver",
"type": "library",
"description": "Symfony OptionsResolver Component",
"keywords": ["options", "config", "configuration"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.9"
},
"autoload": {
"psr-4": { "Symfony\\Component\\OptionsResolver\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony OptionsResolver Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,227 @@
<?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\Polyfill\Ctype;
/**
* Ctype implementation through regex.
*
* @internal
*
* @author Gert de Pagter <BackEndTea@gmail.com>
*/
final class Ctype
{
/**
* Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise.
*
* @see https://php.net/ctype-alnum
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_alnum($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text);
}
/**
* Returns TRUE if every character in text is a letter, FALSE otherwise.
*
* @see https://php.net/ctype-alpha
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_alpha($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text);
}
/**
* Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise.
*
* @see https://php.net/ctype-cntrl
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_cntrl($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text);
}
/**
* Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
*
* @see https://php.net/ctype-digit
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_digit($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text);
}
/**
* Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise.
*
* @see https://php.net/ctype-graph
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_graph($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text);
}
/**
* Returns TRUE if every character in text is a lowercase letter.
*
* @see https://php.net/ctype-lower
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_lower($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text);
}
/**
* Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all.
*
* @see https://php.net/ctype-print
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_print($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text);
}
/**
* Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise.
*
* @see https://php.net/ctype-punct
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_punct($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text);
}
/**
* Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters.
*
* @see https://php.net/ctype-space
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_space($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text);
}
/**
* Returns TRUE if every character in text is an uppercase letter.
*
* @see https://php.net/ctype-upper
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_upper($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text);
}
/**
* Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise.
*
* @see https://php.net/ctype-xdigit
*
* @param string|int $text
*
* @return bool
*/
public static function ctype_xdigit($text)
{
$text = self::convert_int_to_char_for_ctype($text);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text);
}
/**
* Converts integers to their char versions according to normal ctype behaviour, if needed.
*
* If an integer between -128 and 255 inclusive is provided,
* it is interpreted as the ASCII value of a single character
* (negative values have 256 added in order to allow characters in the Extended ASCII range).
* Any other integer is interpreted as a string containing the decimal digits of the integer.
*
* @param string|int $int
*
* @return mixed
*/
private static function convert_int_to_char_for_ctype($int)
{
if (!\is_int($int)) {
return $int;
}
if ($int < -128 || $int > 255) {
return (string) $int;
}
if ($int < 0) {
$int += 256;
}
return \chr($int);
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,12 @@
Symfony Polyfill / Ctype
========================
This component provides `ctype_*` functions to users who run php versions without the ctype extension.
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
License
=======
This library is released under the [MIT license](LICENSE).

View File

@@ -0,0 +1,26 @@
<?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.
*/
use Symfony\Polyfill\Ctype as p;
if (!function_exists('ctype_alnum')) {
function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); }
function ctype_digit($text) { return p\Ctype::ctype_digit($text); }
function ctype_graph($text) { return p\Ctype::ctype_graph($text); }
function ctype_lower($text) { return p\Ctype::ctype_lower($text); }
function ctype_print($text) { return p\Ctype::ctype_print($text); }
function ctype_punct($text) { return p\Ctype::ctype_punct($text); }
function ctype_space($text) { return p\Ctype::ctype_space($text); }
function ctype_upper($text) { return p\Ctype::ctype_upper($text); }
function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); }
}

View File

@@ -0,0 +1,34 @@
{
"name": "symfony/polyfill-ctype",
"type": "library",
"description": "Symfony polyfill for ctype functions",
"keywords": ["polyfill", "compatibility", "portable", "ctype"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
"files": [ "bootstrap.php" ]
},
"suggest": {
"ext-ctype": "For best performance"
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
}
}
}

View File

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

View File

@@ -0,0 +1,29 @@
CHANGELOG
=========
2.7.0
------
* `UnexpectedTypeException` now expects three constructor arguments: The invalid property value,
the `PropertyPathInterface` object and the current index of the property path.
2.5.0
------
* allowed non alpha numeric characters in second level and deeper object properties names
* [BC BREAK] when accessing an index on an object that does not implement
ArrayAccess, a NoSuchIndexException is now thrown instead of the
semantically wrong NoSuchPropertyException
* [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface
2.3.0
------
* added PropertyAccessorBuilder, to enable or disable the support of "__call"
* added support for "__call" in the PropertyAccessor (disabled by default)
* [BC BREAK] changed PropertyAccessor to continue its search for a property or
method even if a non-public match was found. Before, a PropertyAccessDeniedException
was thrown in this case. Class PropertyAccessDeniedException was removed
now.
* deprecated PropertyAccess::getPropertyAccessor
* added PropertyAccess::createPropertyAccessor and PropertyAccess::createPropertyAccessorBuilder

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Thrown when a property path is not available.
*
* @author Stéphane Escandell <stephane.escandell@gmail.com>
*/
class AccessException extends RuntimeException
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Marker interface for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Base InvalidArgumentException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Thrown when a property path is malformed.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidPropertyPathException extends RuntimeException
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Thrown when an index cannot be found.
*
* @author Stéphane Escandell <stephane.escandell@gmail.com>
*/
class NoSuchIndexException extends AccessException
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Thrown when a property cannot be found.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NoSuchPropertyException extends AccessException
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Base OutOfBoundsException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\PropertyAccess\Exception;
/**
* Base RuntimeException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,50 @@
<?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\PropertyAccess\Exception;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
/**
* Thrown when a value does not match an expected type.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnexpectedTypeException extends RuntimeException
{
/**
* @param mixed $value The unexpected value found while traversing property path
* @param PropertyPathInterface $path The property path
* @param int $pathIndex The property path index when the unexpected value was found
*/
public function __construct($value, $path, $pathIndex = null)
{
if (3 === \func_num_args() && $path instanceof PropertyPathInterface) {
$message = sprintf(
'PropertyAccessor requires a graph of objects or arrays to operate on, '.
'but it found type "%s" while trying to traverse path "%s" at property "%s".',
\gettype($value),
(string) $path,
$path->getElement($pathIndex)
);
} else {
@trigger_error('The '.__CLASS__.' constructor now expects 3 arguments: the invalid property value, the '.__NAMESPACE__.'\PropertyPathInterface object and the current index of the property path.', E_USER_DEPRECATED);
$message = sprintf(
'Expected argument of type "%s", "%s" given',
$path,
\is_object($value) ? \get_class($value) : \gettype($value)
);
}
parent::__construct($message);
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2018 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,62 @@
<?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\PropertyAccess;
/**
* Entry point of the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
final class PropertyAccess
{
/**
* Creates a property accessor with the default configuration.
*
* @return PropertyAccessor
*/
public static function createPropertyAccessor()
{
return self::createPropertyAccessorBuilder()->getPropertyAccessor();
}
/**
* Creates a property accessor builder.
*
* @return PropertyAccessorBuilder
*/
public static function createPropertyAccessorBuilder()
{
return new PropertyAccessorBuilder();
}
/**
* Alias of {@link createPropertyAccessor}.
*
* @return PropertyAccessor
*
* @deprecated since version 2.3, to be removed in 3.0.
* Use {@link createPropertyAccessor()} instead.
*/
public static function getPropertyAccessor()
{
@trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.3 and will be removed in 3.0. Use the createPropertyAccessor() method instead.', E_USER_DEPRECATED);
return self::createPropertyAccessor();
}
/**
* This class cannot be instantiated.
*/
private function __construct()
{
}
}

View File

@@ -0,0 +1,812 @@
<?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\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
/**
* Default implementation of {@link PropertyAccessorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PropertyAccessor implements PropertyAccessorInterface
{
/**
* @internal
*/
const VALUE = 0;
/**
* @internal
*/
const REF = 1;
/**
* @internal
*/
const IS_REF_CHAINED = 2;
/**
* @internal
*/
const ACCESS_HAS_PROPERTY = 0;
/**
* @internal
*/
const ACCESS_TYPE = 1;
/**
* @internal
*/
const ACCESS_NAME = 2;
/**
* @internal
*/
const ACCESS_REF = 3;
/**
* @internal
*/
const ACCESS_ADDER = 4;
/**
* @internal
*/
const ACCESS_REMOVER = 5;
/**
* @internal
*/
const ACCESS_TYPE_METHOD = 0;
/**
* @internal
*/
const ACCESS_TYPE_PROPERTY = 1;
/**
* @internal
*/
const ACCESS_TYPE_MAGIC = 2;
/**
* @internal
*/
const ACCESS_TYPE_ADDER_AND_REMOVER = 3;
/**
* @internal
*/
const ACCESS_TYPE_NOT_FOUND = 4;
private $magicCall;
private $ignoreInvalidIndices;
private $readPropertyCache = array();
private $writePropertyCache = array();
private static $previousErrorHandler = false;
private static $errorHandler = array(__CLASS__, 'handleError');
private static $resultProto = array(self::VALUE => null);
/**
* Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead.
*
* @param bool $magicCall
* @param bool $throwExceptionOnInvalidIndex
*/
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false)
{
$this->magicCall = $magicCall;
$this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
}
/**
* {@inheritdoc}
*/
public function getValue($objectOrArray, $propertyPath)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
$zval = array(
self::VALUE => $objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return $propertyValues[\count($propertyValues) - 1][self::VALUE];
}
/**
* {@inheritdoc}
*/
public function setValue(&$objectOrArray, $propertyPath, $value)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
$zval = array(
self::VALUE => $objectOrArray,
self::REF => &$objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
$overwrite = true;
try {
if (\PHP_VERSION_ID < 70000 && false === self::$previousErrorHandler) {
self::$previousErrorHandler = set_error_handler(self::$errorHandler);
}
for ($i = \count($propertyValues) - 1; 0 <= $i; --$i) {
$zval = $propertyValues[$i];
unset($propertyValues[$i]);
// You only need set value for current element if:
// 1. it's the parent of the last index element
// OR
// 2. its child is not passed by reference
//
// This may avoid uncessary value setting process for array elements.
// For example:
// '[a][b][c]' => 'old-value'
// If you want to change its value to 'new-value',
// you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
if ($overwrite) {
$property = $propertyPath->getElement($i);
if ($propertyPath->isIndex($i)) {
if ($overwrite = !isset($zval[self::REF])) {
$ref = &$zval[self::REF];
$ref = $zval[self::VALUE];
}
$this->writeIndex($zval, $property, $value);
if ($overwrite) {
$zval[self::VALUE] = $zval[self::REF];
}
} else {
$this->writeProperty($zval, $property, $value);
}
// if current element is an object
// OR
// if current element's reference chain is not broken - current element
// as well as all its ancients in the property path are all passed by reference,
// then there is no need to continue the value setting process
if (\is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) {
break;
}
}
$value = $zval[self::VALUE];
}
} catch (\TypeError $e) {
try {
self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0);
} catch (InvalidArgumentException $e) {
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (\PHP_VERSION_ID < 70000 && false !== self::$previousErrorHandler) {
restore_error_handler();
self::$previousErrorHandler = false;
}
if (isset($e)) {
throw $e;
}
}
/**
* @internal
*/
public static function handleError($type, $message, $file, $line, $context)
{
if (E_RECOVERABLE_ERROR === $type) {
self::throwInvalidArgumentException($message, debug_backtrace(false), 1);
}
return null !== self::$previousErrorHandler && false !== \call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context);
}
private static function throwInvalidArgumentException($message, $trace, $i)
{
// the type mismatch is not caused by invalid arguments (but e.g. by an incompatible return type hint of the writer method)
if (0 !== strpos($message, 'Argument ')) {
return;
}
if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && array_key_exists(0, $trace[$i]['args'])) {
$pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface '));
$pos += \strlen($delim);
$type = $trace[$i]['args'][0];
$type = \is_object($type) ? \get_class($type) : \gettype($type);
throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type));
}
}
/**
* {@inheritdoc}
*/
public function isReadable($objectOrArray, $propertyPath)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
try {
$zval = array(
self::VALUE => $objectOrArray,
);
$this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return true;
} catch (AccessException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function isWritable($objectOrArray, $propertyPath)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
try {
$zval = array(
self::VALUE => $objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
for ($i = \count($propertyValues) - 1; 0 <= $i; --$i) {
$zval = $propertyValues[$i];
unset($propertyValues[$i]);
if ($propertyPath->isIndex($i)) {
if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
return false;
}
} else {
if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
return false;
}
}
if (\is_object($zval[self::VALUE])) {
return true;
}
}
return true;
} catch (AccessException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
}
}
/**
* Reads the path from an object up to a given path index.
*
* @param array $zval The array containing the object or array to read from
* @param PropertyPathInterface $propertyPath The property path to read
* @param int $lastIndex The index up to which should be read
* @param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
*
* @return array The values read in the path
*
* @throws UnexpectedTypeException if a value within the path is neither object nor array
* @throws NoSuchIndexException If a non-existing index is accessed
*/
private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true)
{
if (!\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0);
}
// Add the root object to the list
$propertyValues = array($zval);
for ($i = 0; $i < $lastIndex; ++$i) {
$property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i);
if ($isIndex) {
// Create missing nested arrays on demand
if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
(\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE]))
) {
if (!$ignoreInvalidIndices) {
if (!\is_array($zval[self::VALUE])) {
if (!$zval[self::VALUE] instanceof \Traversable) {
throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s".', $property, (string) $propertyPath));
}
$zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
}
throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".', $property, (string) $propertyPath, print_r(array_keys($zval[self::VALUE]), true)));
}
if ($i + 1 < $propertyPath->getLength()) {
if (isset($zval[self::REF])) {
$zval[self::VALUE][$property] = array();
$zval[self::REF] = $zval[self::VALUE];
} else {
$zval[self::VALUE] = array($property => array());
}
}
}
$zval = $this->readIndex($zval, $property);
} else {
$zval = $this->readProperty($zval, $property);
}
// the final value of the path must not be validated
if ($i + 1 < $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
}
if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) {
// Set the IS_REF_CHAINED flag to true if:
// current property is passed by reference and
// it is the first element in the property path or
// the IS_REF_CHAINED flag of its parent element is true
// Basically, this flag is true only when the reference chain from the top element to current element is not broken
$zval[self::IS_REF_CHAINED] = true;
}
$propertyValues[] = $zval;
}
return $propertyValues;
}
/**
* Reads a key from an array-like structure.
*
* @param array $zval The array containing the array or \ArrayAccess object to read from
* @param string|int $index The key to read
*
* @return array The array containing the value of the key
*
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/
private function readIndex($zval, $index)
{
if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, \get_class($zval[self::VALUE])));
}
$result = self::$resultProto;
if (isset($zval[self::VALUE][$index])) {
$result[self::VALUE] = $zval[self::VALUE][$index];
if (!isset($zval[self::REF])) {
// Save creating references when doing read-only lookups
} elseif (\is_array($zval[self::VALUE])) {
$result[self::REF] = &$zval[self::REF][$index];
} elseif (\is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
}
}
return $result;
}
/**
* Reads the a property from an object.
*
* @param array $zval The array containing the object to read from
* @param string $property The property to read
*
* @return array The array containing the value of the property
*
* @throws NoSuchPropertyException if the property does not exist or is not public
*/
private function readProperty($zval, $property)
{
if (!\is_object($zval[self::VALUE])) {
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property));
}
$result = self::$resultProto;
$object = $zval[self::VALUE];
$access = $this->getReadAccessInfo(\get_class($object), $property);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
$result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
}
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
$result[self::VALUE] = $object->$property;
if (isset($zval[self::REF])) {
$result[self::REF] = &$object->$property;
}
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
// we call the getter and hope the __call do the job
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} else {
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
}
// Objects are always passed around by reference
if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
}
return $result;
}
/**
* Guesses how to read the property value.
*
* @param string $class
* @param string $property
*
* @return array
*/
private function getReadAccessInfo($class, $property)
{
$key = $class.'::'.$property;
if (isset($this->readPropertyCache[$key])) {
$access = $this->readPropertyCache[$key];
} else {
$access = array();
$reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property);
$getter = 'get'.$camelProp;
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
$isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp;
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getter;
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $isser;
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $hasser;
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = false;
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = true;
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $getter;
} else {
$methods = array($getter, $getsetter, $isser, $hasser, '__get');
if ($this->magicCall) {
$methods[] = '__call';
}
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods "%s()" '.
'exist and have public access in class "%s".',
$property,
implode('()", "', $methods),
$reflClass->name
);
}
$this->readPropertyCache[$key] = $access;
}
return $access;
}
/**
* Sets the value of an index in a given array-accessible value.
*
* @param array $zval The array containing the array or \ArrayAccess object to write to
* @param string|int $index The index to write at
* @param mixed $value The value to write
*
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/
private function writeIndex($zval, $index, $value)
{
if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, \get_class($zval[self::VALUE])));
}
$zval[self::REF][$index] = $value;
}
/**
* Sets the value of a property in the given object.
*
* @param array $zval The array containing the object to write to
* @param string $property The property to write
* @param mixed $value The value to write
*
* @throws NoSuchPropertyException if the property does not exist or is not public
*/
private function writeProperty($zval, $property, $value)
{
if (!\is_object($zval[self::VALUE])) {
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?', $property));
}
$object = $zval[self::VALUE];
$access = $this->getWriteAccessInfo(\get_class($object), $property, $value);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value);
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]} = $value;
} elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
$this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
$object->$property = $value;
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value);
} else {
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
}
}
/**
* Adjusts a collection-valued property by calling add*() and remove*() methods.
*
* @param array $zval The array containing the object to write to
* @param string $property The property to write
* @param iterable $collection The collection to write
* @param string $addMethod The add*() method
* @param string $removeMethod The remove*() method
*/
private function writeCollection($zval, $property, $collection, $addMethod, $removeMethod)
{
// At this point the add and remove methods have been found
$previousValue = $this->readProperty($zval, $property);
$previousValue = $previousValue[self::VALUE];
if ($previousValue instanceof \Traversable) {
$previousValue = iterator_to_array($previousValue);
}
if ($previousValue && \is_array($previousValue)) {
if (\is_object($collection)) {
$collection = iterator_to_array($collection);
}
foreach ($previousValue as $key => $item) {
if (!\in_array($item, $collection, true)) {
unset($previousValue[$key]);
$zval[self::VALUE]->{$removeMethod}($item);
}
}
} else {
$previousValue = false;
}
foreach ($collection as $item) {
if (!$previousValue || !\in_array($item, $previousValue, true)) {
$zval[self::VALUE]->{$addMethod}($item);
}
}
}
/**
* Guesses how to write the property value.
*
* @param string $class
* @param string $property
* @param mixed $value
*
* @return array
*/
private function getWriteAccessInfo($class, $property, $value)
{
$key = $class.'::'.$property;
if (isset($this->writePropertyCache[$key])) {
$access = $this->writePropertyCache[$key];
} else {
$access = array();
$reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelized = $this->camelize($property);
$singulars = (array) StringUtil::singularify($camelized);
if (\is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
if (null !== $methods) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
$access[self::ACCESS_ADDER] = $methods[0];
$access[self::ACCESS_REMOVER] = $methods[1];
}
}
if (!isset($access[self::ACCESS_TYPE])) {
$setter = 'set'.$camelized;
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $setter;
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $setter;
} elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
'the new value must be an array or an instance of \Traversable, '.
'"%s" given.',
$property,
$reflClass->name,
implode('()", "', $methods),
\is_object($value) ? \get_class($value) : \gettype($value)
);
} else {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
'"__set()" or "__call()" exist and have public access in class "%s".',
$property,
implode('', array_map(function ($singular) {
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
}, $singulars)),
$setter,
$getsetter,
$reflClass->name
);
}
}
$this->writePropertyCache[$key] = $access;
}
return $access;
}
/**
* Returns whether a property is writable in the given object.
*
* @param object $object The object to write to
* @param string $property The property to write
*
* @return bool Whether the property is writable
*/
private function isPropertyWritable($object, $property)
{
if (!\is_object($object)) {
return false;
}
$access = $this->getWriteAccessInfo(\get_class($object), $property, array());
return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
|| self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
|| self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
|| (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property))
|| self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
}
/**
* Camelizes a given string.
*
* @param string $string Some string
*
* @return string The camelized version of the string
*/
private function camelize($string)
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
}
/**
* Searches for add and remove methods.
*
* @param \ReflectionClass $reflClass The reflection class for the given object
* @param array $singulars The singular form of the property name or null
*
* @return array|null An array containing the adder and remover when found, null otherwise
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
{
foreach ($singulars as $singular) {
$addMethod = 'add'.$singular;
$removeMethod = 'remove'.$singular;
$addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
$removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
if ($addMethodFound && $removeMethodFound) {
return array($addMethod, $removeMethod);
}
}
}
/**
* Returns whether a method is public and has the number of required parameters.
*
* @param \ReflectionClass $class The class of the method
* @param string $methodName The method name
* @param int $parameters The number of parameters
*
* @return bool Whether the method is public and has $parameters required parameters
*/
private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters)
{
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);
if ($method->isPublic()
&& $method->getNumberOfRequiredParameters() <= $parameters
&& $method->getNumberOfParameters() >= $parameters) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,102 @@
<?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\PropertyAccess;
/**
* A configurable builder to create a PropertyAccessor.
*
* @author Jérémie Augustin <jeremie.augustin@pixel-cookers.com>
*/
class PropertyAccessorBuilder
{
private $magicCall = false;
private $throwExceptionOnInvalidIndex = false;
/**
* Enables the use of "__call" by the PropertyAccessor.
*
* @return $this
*/
public function enableMagicCall()
{
$this->magicCall = true;
return $this;
}
/**
* Disables the use of "__call" by the PropertyAccessor.
*
* @return $this
*/
public function disableMagicCall()
{
$this->magicCall = false;
return $this;
}
/**
* @return bool whether the use of "__call" by the PropertyAccessor is enabled
*/
public function isMagicCallEnabled()
{
return $this->magicCall;
}
/**
* Enables exceptions when reading a non-existing index.
*
* This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue()
* which are always created on-the-fly.
*
* @return $this
*/
public function enableExceptionOnInvalidIndex()
{
$this->throwExceptionOnInvalidIndex = true;
return $this;
}
/**
* Disables exceptions when reading a non-existing index.
*
* Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index.
*
* @return $this
*/
public function disableExceptionOnInvalidIndex()
{
$this->throwExceptionOnInvalidIndex = false;
return $this;
}
/**
* @return bool whether an exception is thrown or null is returned when reading a non-existing index
*/
public function isExceptionOnInvalidIndexEnabled()
{
return $this->throwExceptionOnInvalidIndex;
}
/**
* Builds and returns a new PropertyAccessor object.
*
* @return PropertyAccessorInterface The built PropertyAccessor
*/
public function getPropertyAccessor()
{
return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex);
}
}

View File

@@ -0,0 +1,114 @@
<?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\PropertyAccess;
/**
* Writes and reads values to/from an object/array graph.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyAccessorInterface
{
/**
* Sets the value at the end of the property path of the object graph.
*
* Example:
*
* use Symfony\Component\PropertyAccess\PropertyAccess;
*
* $propertyAccessor = PropertyAccess::createPropertyAccessor();
*
* echo $propertyAccessor->setValue($object, 'child.name', 'Fabien');
* // equals echo $object->getChild()->setName('Fabien');
*
* This method first tries to find a public setter for each property in the
* path. The name of the setter must be the camel-cased property name
* prefixed with "set".
*
* If the setter does not exist, this method tries to find a public
* property. The value of the property is then changed.
*
* If neither is found, an exception is thrown.
*
* @param object|array $objectOrArray The object or array to modify
* @param string|PropertyPathInterface $propertyPath The property path to modify
* @param mixed $value The value to set at the end of the property path
*
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
*/
public function setValue(&$objectOrArray, $propertyPath, $value);
/**
* Returns the value at the end of the property path of the object graph.
*
* Example:
*
* use Symfony\Component\PropertyAccess\PropertyAccess;
*
* $propertyAccessor = PropertyAccess::createPropertyAccessor();
*
* echo $propertyAccessor->getValue($object, 'child.name);
* // equals echo $object->getChild()->getName();
*
* This method first tries to find a public getter for each property in the
* path. The name of the getter must be the camel-cased property name
* prefixed with "get", "is", or "has".
*
* If the getter does not exist, this method tries to find a public
* property. The value of the property is then returned.
*
* If none of them are found, an exception is thrown.
*
* @param object|array $objectOrArray The object or array to traverse
* @param string|PropertyPathInterface $propertyPath The property path to read
*
* @return mixed The value at the end of the property path
*
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object
* nor array
*/
public function getValue($objectOrArray, $propertyPath);
/**
* Returns whether a value can be written at a given property path.
*
* Whenever this method returns true, {@link setValue()} is guaranteed not
* to throw an exception when called with the same arguments.
*
* @param object|array $objectOrArray The object or array to check
* @param string|PropertyPathInterface $propertyPath The property path to check
*
* @return bool Whether the value can be set
*
* @throws Exception\InvalidArgumentException If the property path is invalid
*/
public function isWritable($objectOrArray, $propertyPath);
/**
* Returns whether a property path can be read from an object graph.
*
* Whenever this method returns true, {@link getValue()} is guaranteed not
* to throw an exception when called with the same arguments.
*
* @param object|array $objectOrArray The object or array to check
* @param string|PropertyPathInterface $propertyPath The property path to check
*
* @return bool Whether the property path can be read
*
* @throws Exception\InvalidArgumentException If the property path is invalid
*/
public function isReadable($objectOrArray, $propertyPath);
}

View File

@@ -0,0 +1,205 @@
<?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\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
/**
* Default implementation of {@link PropertyPathInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPath implements \IteratorAggregate, PropertyPathInterface
{
/**
* Character used for separating between plural and singular of an element.
*/
const SINGULAR_SEPARATOR = '|';
/**
* The elements of the property path.
*
* @var array
*/
private $elements = array();
/**
* The number of elements in the property path.
*
* @var int
*/
private $length;
/**
* Contains a Boolean for each property in $elements denoting whether this
* element is an index. It is a property otherwise.
*
* @var array
*/
private $isIndex = array();
/**
* String representation of the path.
*
* @var string
*/
private $pathAsString;
/**
* Constructs a property path from a string.
*
* @param PropertyPath|string $propertyPath The property path as string or instance
*
* @throws InvalidArgumentException If the given path is not a string
* @throws InvalidPropertyPathException If the syntax of the property path is not valid
*/
public function __construct($propertyPath)
{
// Can be used as copy constructor
if ($propertyPath instanceof self) {
/* @var PropertyPath $propertyPath */
$this->elements = $propertyPath->elements;
$this->length = $propertyPath->length;
$this->isIndex = $propertyPath->isIndex;
$this->pathAsString = $propertyPath->pathAsString;
return;
}
if (!\is_string($propertyPath)) {
throw new InvalidArgumentException(sprintf('The property path constructor needs a string or an instance of "Symfony\Component\PropertyAccess\PropertyPath". Got: "%s"', \is_object($propertyPath) ? \get_class($propertyPath) : \gettype($propertyPath)));
}
if ('' === $propertyPath) {
throw new InvalidPropertyPathException('The property path should not be empty.');
}
$this->pathAsString = $propertyPath;
$position = 0;
$remaining = $propertyPath;
// first element is evaluated differently - no leading dot for properties
$pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/';
while (preg_match($pattern, $remaining, $matches)) {
if ('' !== $matches[2]) {
$element = $matches[2];
$this->isIndex[] = false;
} else {
$element = $matches[3];
$this->isIndex[] = true;
}
$this->elements[] = $element;
$position += \strlen($matches[1]);
$remaining = $matches[4];
$pattern = '/^(\.([^\.|\[]++)|\[([^\]]++)\])(.*)/';
}
if ('' !== $remaining) {
throw new InvalidPropertyPathException(sprintf('Could not parse property path "%s". Unexpected token "%s" at position %d', $propertyPath, $remaining[0], $position));
}
$this->length = \count($this->elements);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->pathAsString;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
return $this->length;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
if ($this->length <= 1) {
return;
}
$parent = clone $this;
--$parent->length;
$parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '[')));
array_pop($parent->elements);
array_pop($parent->isIndex);
return $parent;
}
/**
* Returns a new iterator for this path.
*
* @return PropertyPathIteratorInterface
*/
public function getIterator()
{
return new PropertyPathIterator($this);
}
/**
* {@inheritdoc}
*/
public function getElements()
{
return $this->elements;
}
/**
* {@inheritdoc}
*/
public function getElement($index)
{
if (!isset($this->elements[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
}
return $this->elements[$index];
}
/**
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
}
return !$this->isIndex[$index];
}
/**
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
}
return $this->isIndex[$index];
}
}

View File

@@ -0,0 +1,299 @@
<?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\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathBuilder
{
private $elements = array();
private $isIndex = array();
/**
* Creates a new property path builder.
*
* @param PropertyPathInterface|string|null $path The path to initially store
* in the builder. Optional.
*/
public function __construct($path = null)
{
if (null !== $path) {
$this->append($path);
}
}
/**
* Appends a (sub-) path to the current path.
*
* @param PropertyPathInterface|string $path The path to append
* @param int $offset The offset where the appended
* piece starts in $path
* @param int $length The length of the appended piece
* If 0, the full path is appended
*/
public function append($path, $offset = 0, $length = 0)
{
if (\is_string($path)) {
$path = new PropertyPath($path);
}
if (0 === $length) {
$end = $path->getLength();
} else {
$end = $offset + $length;
}
for (; $offset < $end; ++$offset) {
$this->elements[] = $path->getElement($offset);
$this->isIndex[] = $path->isIndex($offset);
}
}
/**
* Appends an index element to the current path.
*
* @param string $name The name of the appended index
*/
public function appendIndex($name)
{
$this->elements[] = $name;
$this->isIndex[] = true;
}
/**
* Appends a property element to the current path.
*
* @param string $name The name of the appended property
*/
public function appendProperty($name)
{
$this->elements[] = $name;
$this->isIndex[] = false;
}
/**
* Removes elements from the current path.
*
* @param int $offset The offset at which to remove
* @param int $length The length of the removed piece
*
* @throws OutOfBoundsException if offset is invalid
*/
public function remove($offset, $length = 1)
{
if (!isset($this->elements[$offset])) {
throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
}
$this->resize($offset, $length, 0);
}
/**
* Replaces a sub-path by a different (sub-) path.
*
* @param int $offset The offset at which to replace
* @param int $length The length of the piece to replace
* @param PropertyPathInterface|string $path The path to insert
* @param int $pathOffset The offset where the inserted piece
* starts in $path
* @param int $pathLength The length of the inserted piece
* If 0, the full path is inserted
*
* @throws OutOfBoundsException If the offset is invalid
*/
public function replace($offset, $length, $path, $pathOffset = 0, $pathLength = 0)
{
if (\is_string($path)) {
$path = new PropertyPath($path);
}
if ($offset < 0 && abs($offset) <= $this->getLength()) {
$offset = $this->getLength() + $offset;
} elseif (!isset($this->elements[$offset])) {
throw new OutOfBoundsException('The offset '.$offset.' is not within the property path');
}
if (0 === $pathLength) {
$pathLength = $path->getLength() - $pathOffset;
}
$this->resize($offset, $length, $pathLength);
for ($i = 0; $i < $pathLength; ++$i) {
$this->elements[$offset + $i] = $path->getElement($pathOffset + $i);
$this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i);
}
ksort($this->elements);
}
/**
* Replaces a property element by an index element.
*
* @param int $offset The offset at which to replace
* @param string $name The new name of the element. Optional
*
* @throws OutOfBoundsException If the offset is invalid
*/
public function replaceByIndex($offset, $name = null)
{
if (!isset($this->elements[$offset])) {
throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = true;
}
/**
* Replaces an index element by a property element.
*
* @param int $offset The offset at which to replace
* @param string $name The new name of the element. Optional
*
* @throws OutOfBoundsException If the offset is invalid
*/
public function replaceByProperty($offset, $name = null)
{
if (!isset($this->elements[$offset])) {
throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = false;
}
/**
* Returns the length of the current path.
*
* @return int The path length
*/
public function getLength()
{
return \count($this->elements);
}
/**
* Returns the current property path.
*
* @return PropertyPathInterface The constructed property path
*/
public function getPropertyPath()
{
$pathAsString = $this->__toString();
return '' !== $pathAsString ? new PropertyPath($pathAsString) : null;
}
/**
* Returns the current property path as string.
*
* @return string The property path as string
*/
public function __toString()
{
$string = '';
foreach ($this->elements as $offset => $element) {
if ($this->isIndex[$offset]) {
$element = '['.$element.']';
} elseif ('' !== $string) {
$string .= '.';
}
$string .= $element;
}
return $string;
}
/**
* Resizes the path so that a chunk of length $cutLength is
* removed at $offset and another chunk of length $insertionLength
* can be inserted.
*
* @param int $offset The offset where the removed chunk starts
* @param int $cutLength The length of the removed chunk
* @param int $insertionLength The length of the inserted chunk
*/
private function resize($offset, $cutLength, $insertionLength)
{
// Nothing else to do in this case
if ($insertionLength === $cutLength) {
return;
}
$length = \count($this->elements);
if ($cutLength > $insertionLength) {
// More elements should be removed than inserted
$diff = $cutLength - $insertionLength;
$newLength = $length - $diff;
// Shift elements to the left (left-to-right until the new end)
// Max allowed offset to be shifted is such that
// $offset + $diff < $length (otherwise invalid index access)
// i.e. $offset < $length - $diff = $newLength
for ($i = $offset; $i < $newLength; ++$i) {
$this->elements[$i] = $this->elements[$i + $diff];
$this->isIndex[$i] = $this->isIndex[$i + $diff];
}
// All remaining elements should be removed
for (; $i < $length; ++$i) {
unset($this->elements[$i], $this->isIndex[$i]);
}
} else {
$diff = $insertionLength - $cutLength;
$newLength = $length + $diff;
$indexAfterInsertion = $offset + $insertionLength;
// $diff <= $insertionLength
// $indexAfterInsertion >= $insertionLength
// => $diff <= $indexAfterInsertion
// In each of the following loops, $i >= $diff must hold,
// otherwise ($i - $diff) becomes negative.
// Shift old elements to the right to make up space for the
// inserted elements. This needs to be done left-to-right in
// order to preserve an ascending array index order
// Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff,
// $i >= $diff is guaranteed.
for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) {
$this->elements[$i] = $this->elements[$i - $diff];
$this->isIndex[$i] = $this->isIndex[$i - $diff];
}
// Shift remaining elements to the right. Do this right-to-left
// so we don't overwrite elements before copying them
// The last written index is the immediate index after the inserted
// string, because the indices before that will be overwritten
// anyway.
// Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff,
// $i >= $diff is guaranteed.
for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
$this->elements[$i] = $this->elements[$i - $diff];
$this->isIndex[$i] = $this->isIndex[$i - $diff];
}
}
}
}

View File

@@ -0,0 +1,86 @@
<?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\PropertyAccess;
/**
* A sequence of property names or array indices.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyPathInterface extends \Traversable
{
/**
* Returns the string representation of the property path.
*
* @return string The path as string
*/
public function __toString();
/**
* Returns the length of the property path, i.e. the number of elements.
*
* @return int The path length
*/
public function getLength();
/**
* Returns the parent property path.
*
* The parent property path is the one that contains the same items as
* this one except for the last one.
*
* If this property path only contains one item, null is returned.
*
* @return PropertyPath The parent path or null
*/
public function getParent();
/**
* Returns the elements of the property path as array.
*
* @return array An array of property/index names
*/
public function getElements();
/**
* Returns the element at the given index in the property path.
*
* @param int $index The index key
*
* @return string A property or index name
*
* @throws Exception\OutOfBoundsException If the offset is invalid
*/
public function getElement($index);
/**
* Returns whether the element at the given index is a property.
*
* @param int $index The index in the property path
*
* @return bool Whether the element at this index is a property
*
* @throws Exception\OutOfBoundsException If the offset is invalid
*/
public function isProperty($index);
/**
* Returns whether the element at the given index is an array index.
*
* @param int $index The index in the property path
*
* @return bool Whether the element at this index is an array index
*
* @throws Exception\OutOfBoundsException If the offset is invalid
*/
public function isIndex($index);
}

View File

@@ -0,0 +1,49 @@
<?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\PropertyAccess;
/**
* Traverses a property path and provides additional methods to find out
* information about the current element.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface
{
protected $path;
/**
* @param PropertyPathInterface $path The property path to traverse
*/
public function __construct(PropertyPathInterface $path)
{
parent::__construct($path->getElements());
$this->path = $path;
}
/**
* {@inheritdoc}
*/
public function isIndex()
{
return $this->path->isIndex($this->key());
}
/**
* {@inheritdoc}
*/
public function isProperty()
{
return $this->path->isProperty($this->key());
}
}

View File

@@ -0,0 +1,34 @@
<?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\PropertyAccess;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator
{
/**
* Returns whether the current element in the property path is an array
* index.
*
* @return bool
*/
public function isIndex();
/**
* Returns whether the current element in the property path is a property
* name.
*
* @return bool
*/
public function isProperty();
}

View File

@@ -0,0 +1,14 @@
PropertyAccess Component
========================
The PropertyAccess component provides function to read and write from/to an
object or array using a simple string notation.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/property_access/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,226 @@
<?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\PropertyAccess;
/**
* Creates singulars from plurals.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class StringUtil
{
/**
* Map english plural to singular suffixes.
*
* @see http://english-zone.com/spelling/plurals.html
*/
private static $pluralMap = array(
// First entry: plural suffix, reversed
// Second entry: length of plural suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: singular suffix, normal
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
array('a', 1, true, true, array('on', 'um')),
// nebulae (nebula)
array('ea', 2, true, true, 'a'),
// services (service)
array('secivres', 8, true, true, 'service'),
// mice (mouse), lice (louse)
array('eci', 3, false, true, 'ouse'),
// geese (goose)
array('esee', 4, false, true, 'oose'),
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
array('i', 1, true, true, 'us'),
// men (man), women (woman)
array('nem', 3, true, true, 'man'),
// children (child)
array('nerdlihc', 8, true, true, 'child'),
// oxen (ox)
array('nexo', 4, false, false, 'ox'),
// indices (index), appendices (appendix), prices (price)
array('seci', 4, false, true, array('ex', 'ix', 'ice')),
// selfies (selfie)
array('seifles', 7, true, true, 'selfie'),
// movies (movie)
array('seivom', 6, true, true, 'movie'),
// feet (foot)
array('teef', 4, true, true, 'foot'),
// geese (goose)
array('eseeg', 5, true, true, 'goose'),
// teeth (tooth)
array('hteet', 5, true, true, 'tooth'),
// news (news)
array('swen', 4, true, true, 'news'),
// series (series)
array('seires', 6, true, true, 'series'),
// babies (baby)
array('sei', 3, false, true, 'y'),
// accesses (access), addresses (address), kisses (kiss)
array('sess', 4, true, false, 'ss'),
// analyses (analysis), ellipses (ellipsis), funguses (fungus),
// neuroses (neurosis), theses (thesis), emphases (emphasis),
// oases (oasis), crises (crisis), houses (house), bases (base),
// atlases (atlas)
array('ses', 3, true, true, array('s', 'se', 'sis')),
// objectives (objective), alternative (alternatives)
array('sevit', 5, true, true, 'tive'),
// drives (drive)
array('sevird', 6, false, true, 'drive'),
// lives (life), wives (wife)
array('sevi', 4, false, true, 'ife'),
// moves (move)
array('sevom', 5, true, true, 'move'),
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
array('sev', 3, true, true, array('f', 've', 'ff')),
// axes (axis), axes (ax), axes (axe)
array('sexa', 4, false, false, array('ax', 'axe', 'axis')),
// indexes (index), matrixes (matrix)
array('sex', 3, true, false, 'x'),
// quizzes (quiz)
array('sezz', 4, true, false, 'z'),
// bureaus (bureau)
array('suae', 4, false, true, 'eau'),
// roses (rose), garages (garage), cassettes (cassette),
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
// shoes (shoe)
array('se', 2, true, true, array('', 'e')),
// tags (tag)
array('s', 1, true, true, ''),
// chateaux (chateau)
array('xuae', 4, false, true, 'eau'),
// people (person)
array('elpoep', 6, true, true, 'person'),
);
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Returns the singular form of a word.
*
* If the method can't determine the form with certainty, an array of the
* possible singulars is returned.
*
* @param string $plural A word in plural form
*
* @return string|array The singular form or an array of possible singular
* forms
*/
public static function singularify($plural)
{
$pluralRev = strrev($plural);
$lowerPluralRev = strtolower($pluralRev);
$pluralLength = \strlen($lowerPluralRev);
// The outer loop iterates over the entries of the plural table
// The inner loop $j iterates over the characters of the plural suffix
// in the plural table to compare them with the characters of the actual
// given plural suffix
foreach (self::$pluralMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
// Compare characters in the plural table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerPluralRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the singular suffix to the singular array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $pluralLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the plural suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($pluralRev[$j - 1]);
if (\is_array($newSuffix)) {
$singulars = array();
foreach ($newSuffix as $newSuffixEntry) {
$singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $singulars;
}
return $newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix);
}
// Suffix is longer than word
if ($j === $pluralLength) {
break;
}
}
}
// Assume that plural and singular is identical
return $plural;
}
}

View File

@@ -0,0 +1,65 @@
<?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\PropertyAccess\Tests\Fixtures;
/**
* This class is a hand written simplified version of PHP native `ArrayObject`
* class, to show that it behaves differently than the PHP native implementation.
*/
class NonTraversableArrayObject implements \ArrayAccess, \Countable, \Serializable
{
private $array;
public function __construct(array $array = null)
{
$this->array = $array ?: array();
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset)
{
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset)
{
unset($this->array[$offset]);
}
public function count()
{
return \count($this->array);
}
public function serialize()
{
return serialize($this->array);
}
public function unserialize($serialized)
{
$this->array = (array) unserialize((string) $serialized);
}
}

View File

@@ -0,0 +1,36 @@
<?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\PropertyAccess\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ReturnTyped
{
public function getFoos(): array
{
return 'It doesn\'t respect the return type on purpose';
}
public function addFoo(\DateTime $dateTime)
{
}
public function removeFoo(\DateTime $dateTime)
{
}
public function setName($name): self
{
return 'This does not respect the return type on purpose.';
}
}

View File

@@ -0,0 +1,176 @@
<?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\PropertyAccess\Tests\Fixtures;
class TestClass
{
public $publicProperty;
protected $protectedProperty;
private $privateProperty;
private $publicAccessor;
private $publicMethodAccessor;
private $publicGetSetter;
private $publicAccessorWithDefaultValue;
private $publicAccessorWithRequiredAndDefaultValue;
private $publicAccessorWithMoreRequiredParameters;
private $publicIsAccessor;
private $publicHasAccessor;
private $publicGetter;
public function __construct($value)
{
$this->publicProperty = $value;
$this->publicAccessor = $value;
$this->publicMethodAccessor = $value;
$this->publicGetSetter = $value;
$this->publicAccessorWithDefaultValue = $value;
$this->publicAccessorWithRequiredAndDefaultValue = $value;
$this->publicAccessorWithMoreRequiredParameters = $value;
$this->publicIsAccessor = $value;
$this->publicHasAccessor = $value;
$this->publicGetter = $value;
}
public function setPublicAccessor($value)
{
$this->publicAccessor = $value;
}
public function setPublicAccessorWithDefaultValue($value = null)
{
$this->publicAccessorWithDefaultValue = $value;
}
public function setPublicAccessorWithRequiredAndDefaultValue($value, $optional = null)
{
$this->publicAccessorWithRequiredAndDefaultValue = $value;
}
public function setPublicAccessorWithMoreRequiredParameters($value, $needed)
{
$this->publicAccessorWithMoreRequiredParameters = $value;
}
public function getPublicAccessor()
{
return $this->publicAccessor;
}
public function getPublicAccessorWithDefaultValue()
{
return $this->publicAccessorWithDefaultValue;
}
public function getPublicAccessorWithRequiredAndDefaultValue()
{
return $this->publicAccessorWithRequiredAndDefaultValue;
}
public function getPublicAccessorWithMoreRequiredParameters()
{
return $this->publicAccessorWithMoreRequiredParameters;
}
public function setPublicIsAccessor($value)
{
$this->publicIsAccessor = $value;
}
public function isPublicIsAccessor()
{
return $this->publicIsAccessor;
}
public function setPublicHasAccessor($value)
{
$this->publicHasAccessor = $value;
}
public function hasPublicHasAccessor()
{
return $this->publicHasAccessor;
}
public function publicGetSetter($value = null)
{
if (null !== $value) {
$this->publicGetSetter = $value;
}
return $this->publicGetSetter;
}
public function getPublicMethodMutator()
{
return $this->publicGetSetter;
}
protected function setProtectedAccessor($value)
{
}
protected function getProtectedAccessor()
{
return 'foobar';
}
protected function setProtectedIsAccessor($value)
{
}
protected function isProtectedIsAccessor()
{
return 'foobar';
}
protected function setProtectedHasAccessor($value)
{
}
protected function hasProtectedHasAccessor()
{
return 'foobar';
}
private function setPrivateAccessor($value)
{
}
private function getPrivateAccessor()
{
return 'foobar';
}
private function setPrivateIsAccessor($value)
{
}
private function isPrivateIsAccessor()
{
return 'foobar';
}
private function setPrivateHasAccessor($value)
{
}
private function hasPrivateHasAccessor()
{
return 'foobar';
}
public function getPublicGetter()
{
return $this->publicGetter;
}
}

View File

@@ -0,0 +1,27 @@
<?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\PropertyAccess\Tests\Fixtures;
class TestClassIsWritable
{
protected $value;
public function getValue()
{
return $this->value;
}
public function __construct($value)
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,37 @@
<?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\PropertyAccess\Tests\Fixtures;
class TestClassMagicCall
{
private $magicCallProperty;
public function __construct($value)
{
$this->magicCallProperty = $value;
}
public function __call($method, array $args)
{
if ('getMagicCallProperty' === $method) {
return $this->magicCallProperty;
}
if ('getConstantMagicCallProperty' === $method) {
return 'constant value';
}
if ('setMagicCallProperty' === $method) {
$this->magicCallProperty = reset($args);
}
}
}

View File

@@ -0,0 +1,42 @@
<?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\PropertyAccess\Tests\Fixtures;
class TestClassMagicGet
{
private $magicProperty;
public $publicProperty;
public function __construct($value)
{
$this->magicProperty = $value;
}
public function __set($property, $value)
{
if ('magicProperty' === $property) {
$this->magicProperty = $value;
}
}
public function __get($property)
{
if ('magicProperty' === $property) {
return $this->magicProperty;
}
if ('constantMagicProperty' === $property) {
return 'constant value';
}
}
}

View File

@@ -0,0 +1,32 @@
<?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\PropertyAccess\Tests\Fixtures;
class TestClassSetValue
{
protected $value;
public function getValue()
{
return $this->value;
}
public function setValue($value)
{
$this->value = $value;
}
public function __construct($value)
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,31 @@
<?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\PropertyAccess\Tests\Fixtures;
class Ticket5775Object
{
private $property;
public function getProperty()
{
return $this->property;
}
private function setProperty()
{
}
public function __set($property, $value)
{
$this->$property = $value;
}
}

View File

@@ -0,0 +1,70 @@
<?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\PropertyAccess\Tests\Fixtures;
/**
* This class is a hand written simplified version of PHP native `ArrayObject`
* class, to show that it behaves differently than the PHP native implementation.
*/
class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable
{
private $array;
public function __construct(array $array = null)
{
$this->array = $array ?: array();
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset)
{
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset)
{
unset($this->array[$offset]);
}
public function getIterator()
{
return new \ArrayIterator($this->array);
}
public function count()
{
return \count($this->array);
}
public function serialize()
{
return serialize($this->array);
}
public function unserialize($serialized)
{
$this->array = (array) unserialize((string) $serialized);
}
}

View File

@@ -0,0 +1,51 @@
<?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\PropertyAccess\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class TypeHinted
{
private $date;
/**
* @var \Countable
*/
private $countable;
public function setDate(\DateTime $date)
{
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
/**
* @return \Countable
*/
public function getCountable()
{
return $this->countable;
}
/**
* @param \Countable $countable
*/
public function setCountable(\Countable $countable)
{
$this->countable = $countable;
}
}

View File

@@ -0,0 +1,87 @@
<?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\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
abstract class PropertyAccessorArrayAccessTest extends TestCase
{
/**
* @var PropertyAccessor
*/
protected $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
abstract protected function getContainer(array $array);
public function getValidPropertyPaths()
{
return array(
array($this->getContainer(array('firstName' => 'Bernhard')), '[firstName]', 'Bernhard'),
array($this->getContainer(array('person' => $this->getContainer(array('firstName' => 'Bernhard')))), '[person][firstName]', 'Bernhard'),
);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testGetValue($collection, $path, $value)
{
$this->assertSame($value, $this->propertyAccessor->getValue($collection, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueFailsIfNoSuchIndex()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->enableExceptionOnInvalidIndex()
->getPropertyAccessor();
$object = $this->getContainer(array('firstName' => 'Bernhard'));
$this->propertyAccessor->getValue($object, '[lastName]');
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue($collection, $path)
{
$this->propertyAccessor->setValue($collection, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable($collection, $path)
{
$this->assertTrue($this->propertyAccessor->isReadable($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsWritable($collection, $path)
{
$this->assertTrue($this->propertyAccessor->isWritable($collection, $path));
}
}

View File

@@ -0,0 +1,20 @@
<?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\PropertyAccess\Tests;
class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getContainer(array $array)
{
return new \ArrayObject($array);
}
}

View File

@@ -0,0 +1,20 @@
<?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\PropertyAccess\Tests;
class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest
{
protected function getContainer(array $array)
{
return $array;
}
}

View File

@@ -0,0 +1,56 @@
<?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\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyAccessorBuilder;
class PropertyAccessorBuilderTest extends TestCase
{
/**
* @var PropertyAccessorBuilder
*/
protected $builder;
protected function setUp()
{
$this->builder = new PropertyAccessorBuilder();
}
protected function tearDown()
{
$this->builder = null;
}
public function testEnableMagicCall()
{
$this->assertSame($this->builder, $this->builder->enableMagicCall());
}
public function testDisableMagicCall()
{
$this->assertSame($this->builder, $this->builder->disableMagicCall());
}
public function testIsMagicCallEnable()
{
$this->assertFalse($this->builder->isMagicCallEnabled());
$this->assertTrue($this->builder->enableMagicCall()->isMagicCallEnabled());
$this->assertFalse($this->builder->disableMagicCall()->isMagicCallEnabled());
}
public function testGetPropertyAccessor()
{
$this->assertInstanceOf('Symfony\Component\PropertyAccess\PropertyAccessor', $this->builder->getPropertyAccessor());
$this->assertInstanceOf('Symfony\Component\PropertyAccess\PropertyAccessor', $this->builder->enableMagicCall()->getPropertyAccessor());
}
}

View File

@@ -0,0 +1,200 @@
<?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\PropertyAccess\Tests;
class PropertyAccessorCollectionTest_Car
{
private $axes;
public function __construct($axes = null)
{
$this->axes = $axes;
}
// In the test, use a name that StringUtil can't uniquely singularify
public function addAxis($axis)
{
$this->axes[] = $axis;
}
public function removeAxis($axis)
{
foreach ($this->axes as $key => $value) {
if ($value === $axis) {
unset($this->axes[$key]);
return;
}
}
}
public function getAxes()
{
return $this->axes;
}
}
class PropertyAccessorCollectionTest_CarOnlyAdder
{
public function addAxis($axis)
{
}
public function getAxes()
{
}
}
class PropertyAccessorCollectionTest_CarOnlyRemover
{
public function removeAxis($axis)
{
}
public function getAxes()
{
}
}
class PropertyAccessorCollectionTest_CarNoAdderAndRemover
{
public function getAxes()
{
}
}
class PropertyAccessorCollectionTest_CompositeCar
{
public function getStructure()
{
}
public function setStructure($structure)
{
}
}
class PropertyAccessorCollectionTest_CarStructure
{
public function addAxis($axis)
{
}
public function removeAxis($axis)
{
}
public function getAxes()
{
}
}
abstract class PropertyAccessorCollectionTest extends PropertyAccessorArrayAccessTest
{
public function testSetValueCallsAdderAndRemoverForCollections()
{
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth'));
$axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third'));
$axesMergedCopy = \is_object($axesMerged) ? clone $axesMerged : $axesMerged;
// Don't use a mock in order to test whether the collections are
// modified while iterating them
$car = new PropertyAccessorCollectionTest_Car($axesBefore);
$this->propertyAccessor->setValue($car, 'axes', $axesMerged);
$this->assertEquals($axesAfter, $car->getAxes());
// The passed collection was not modified
$this->assertEquals($axesMergedCopy, $axesMerged);
}
public function testSetValueCallsAdderAndRemoverForNestedCollections()
{
$car = $this->getMockBuilder(__CLASS__.'_CompositeCar')->getMock();
$structure = $this->getMockBuilder(__CLASS__.'_CarStructure')->getMock();
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$car->expects($this->any())
->method('getStructure')
->will($this->returnValue($structure));
$structure->expects($this->at(0))
->method('getAxes')
->will($this->returnValue($axesBefore));
$structure->expects($this->at(1))
->method('removeAxis')
->with('fourth');
$structure->expects($this->at(2))
->method('addAxis')
->with('first');
$structure->expects($this->at(3))
->method('addAxis')
->with('third');
$this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()"/"removeAx()", "addAxe()"/"removeAxe()", "addAxis()"/"removeAxis()", "setAxes()", "axes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover
*/
public function testSetValueFailsIfNoAdderNorRemoverFound()
{
$car = $this->getMockBuilder(__CLASS__.'_CarNoAdderAndRemover')->getMock();
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
public function testIsWritableReturnsTrueIfAdderAndRemoverExists()
{
$car = $this->getMockBuilder(__CLASS__.'_Car')->getMock();
$this->assertTrue($this->propertyAccessor->isWritable($car, 'axes'));
}
public function testIsWritableReturnsFalseIfOnlyAdderExists()
{
$car = $this->getMockBuilder(__CLASS__.'_CarOnlyAdder')->getMock();
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes'));
}
public function testIsWritableReturnsFalseIfOnlyRemoverExists()
{
$car = $this->getMockBuilder(__CLASS__.'_CarOnlyRemover')->getMock();
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes'));
}
public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists()
{
$car = $this->getMockBuilder(__CLASS__.'_CarNoAdderAndRemover')->getMock();
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* expectedExceptionMessageRegExp /The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis()", "removeAxis()" but the new value must be an array or an instance of \Traversable, "string" given./
*/
public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable()
{
$car = $this->getMockBuilder(__CLASS__.'_Car')->getMock();
$this->propertyAccessor->setValue($car, 'axes', 'Not an array or Traversable');
}
}

View File

@@ -0,0 +1,22 @@
<?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\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Tests\Fixtures\NonTraversableArrayObject;
class PropertyAccessorNonTraversableArrayObjectTest extends PropertyAccessorArrayAccessTest
{
protected function getContainer(array $array)
{
return new NonTraversableArrayObject($array);
}
}

View File

@@ -0,0 +1,605 @@
<?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\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
class PropertyAccessorTest extends TestCase
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;
protected function setUp()
{
$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(
array((object) array('firstName' => 'Bernhard'), 'lastName'),
array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.lastName'),
array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].lastName'),
array(new TestClass('Bernhard'), 'protectedProperty'),
array(new TestClass('Bernhard'), 'privateProperty'),
array(new TestClass('Bernhard'), 'protectedAccessor'),
array(new TestClass('Bernhard'), 'protectedIsAccessor'),
array(new TestClass('Bernhard'), 'protectedHasAccessor'),
array(new TestClass('Bernhard'), 'privateAccessor'),
array(new TestClass('Bernhard'), 'privateIsAccessor'),
array(new TestClass('Bernhard'), 'privateHasAccessor'),
// Properties are not camelized
array(new TestClass('Bernhard'), 'public_property'),
);
}
public function getPathsWithMissingIndex()
{
return array(
array(array('firstName' => 'Bernhard'), '[lastName]'),
array(array(), '[index][lastName]'),
array(array('index' => array()), '[index][lastName]'),
array(array('index' => array('firstName' => 'Bernhard')), '[index][lastName]'),
array((object) array('property' => array('firstName' => 'Bernhard')), 'property[lastName]'),
);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testGetValue($objectOrArray, $path, $value)
{
$this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path)
{
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path)
{
$this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueThrowsExceptionIfNotArrayAccess()
{
$this->propertyAccessor->getValue(new \stdClass(), '[index]');
}
public function testGetValueReadsMagicGet()
{
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath()
{
$object = new \ArrayObject();
$array = array('child' => array('index' => $object));
$this->assertNull($this->propertyAccessor->getValue($array, '[child][index][foo][bar]'));
$this->assertSame(array(), $object->getArrayCopy());
}
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicGetThatReturnsConstant()
{
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'constantMagicProperty'));
}
public function testGetValueNotModifyObject()
{
$object = new \stdClass();
$object->firstName = array('Bernhard');
$this->assertNull($this->propertyAccessor->getValue($object, 'firstName[1]'));
$this->assertSame(array('Bernhard'), $object->firstName);
}
public function testGetValueNotModifyObjectException()
{
$propertyAccessor = new PropertyAccessor(false, true);
$object = new \stdClass();
$object->firstName = array('Bernhard');
try {
$propertyAccessor->getValue($object, 'firstName[1]');
} catch (NoSuchIndexException $e) {
}
$this->assertSame(array('Bernhard'), $object->firstName);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueDoesNotReadMagicCallByDefault()
{
$this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty');
}
public function testGetValueReadsMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicCallThatReturnsConstant()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path)
{
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testSetValueThrowsExceptionIfNotArrayAccess()
{
$object = new \stdClass();
$this->propertyAccessor->setValue($object, '[index]', 'Updated');
}
public function testSetValueUpdatesMagicSet()
{
$author = new TestClassMagicGet('Bernhard');
$this->propertyAccessor->setValue($author, 'magicProperty', 'Updated');
$this->assertEquals('Updated', $author->__get('magicProperty'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfThereAreMissingParameters()
{
$object = new TestClass('Bernhard');
$this->propertyAccessor->setValue($object, 'publicAccessorWithMoreRequiredParameters', 'Updated');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueDoesNotUpdateMagicCallByDefault()
{
$author = new TestClassMagicCall('Bernhard');
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
}
public function testSetValueUpdatesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$author = new TestClassMagicCall('Bernhard');
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
$this->assertEquals('Updated', $author->__call('getMagicCallProperty', array()));
}
/**
* @dataProvider getPathsWithUnexpectedType
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'value');
}
public function testGetValueWhenArrayValueIsNull()
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->assertNull($this->propertyAccessor->getValue(array('index' => array('nullable' => null)), '[index][nullable]'));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable($objectOrArray, $path)
{
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
*/
public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path)
{
// Non-existing indices can be read. In this case, null is returned
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
// When exceptions are enabled, non-existing indices cannot be read
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
public function testIsReadableRecognizesMagicGet()
{
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testIsReadableDoesNotRecognizeMagicCallByDefault()
{
$this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsReadableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsWritable($objectOrArray, $path)
{
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
*/
public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path)
{
// Non-existing indices can be written. Arrays are created on-demand.
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
// Non-existing indices can be written even if exceptions are enabled
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
}
public function testIsWritableRecognizesMagicSet()
{
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testIsWritableDoesNotRecognizeMagicCallByDefault()
{
$this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsWritableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path));
}
public function getValidPropertyPaths()
{
return array(
array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'),
array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'),
array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'),
array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'),
array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'),
array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'),
array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'),
array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'),
// Accessor methods
array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicGetSetter', 'Bernhard'),
// Methods are camelized
array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'),
array(new TestClass('Bernhard'), '_public_accessor', 'Bernhard'),
// Missing indices
array(array('index' => array()), '[index][firstName]', null),
array(array('root' => array('index' => array())), '[root][index][firstName]', null),
// Special chars
array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'),
array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'),
array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'),
array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'),
// nested objects and arrays
array(array('foo' => new TestClass('bar')), '[foo].publicGetSetter', 'bar'),
array(new TestClass(array('foo' => 'bar')), 'publicGetSetter[foo]', 'bar'),
array(new TestClass(new TestClass('bar')), 'publicGetter.publicGetSetter', 'bar'),
array(new TestClass(array('foo' => new TestClass('bar'))), 'publicGetter[foo].publicGetSetter', 'bar'),
array(new TestClass(new TestClass(new TestClass('bar'))), 'publicGetter.publicGetter.publicGetSetter', 'bar'),
array(new TestClass(array('foo' => array('baz' => new TestClass('bar')))), 'publicGetter[foo][baz].publicGetSetter', 'bar'),
);
}
public function testTicket5755()
{
$object = new Ticket5775Object();
$this->propertyAccessor->setValue($object, 'property', 'foobar');
$this->assertEquals('foobar', $object->getProperty());
}
public function testSetValueDeepWithMagicGetter()
{
$obj = new TestClassMagicGet('foo');
$obj->publicProperty = array('foo' => array('bar' => 'some_value'));
$this->propertyAccessor->setValue($obj, 'publicProperty[foo][bar]', 'Updated');
$this->assertSame('Updated', $obj->publicProperty['foo']['bar']);
}
public function getReferenceChainObjectsForSetValue()
{
return array(
array(array('a' => array('b' => array('c' => 'old-value'))), '[a][b][c]', 'new-value'),
array(new TestClassSetValue(new TestClassSetValue('old-value')), 'value.value', 'new-value'),
array(new TestClassSetValue(array('a' => array('b' => array('c' => new TestClassSetValue('old-value'))))), 'value[a][b][c].value', 'new-value'),
array(new TestClassSetValue(array('a' => array('b' => 'old-value'))), 'value[a][b]', 'new-value'),
array(new \ArrayIterator(array('a' => array('b' => array('c' => 'old-value')))), '[a][b][c]', 'new-value'),
);
}
/**
* @dataProvider getReferenceChainObjectsForSetValue
*/
public function testSetValueForReferenceChainIssue($object, $path, $value)
{
$this->propertyAccessor->setValue($object, $path, $value);
$this->assertEquals($value, $this->propertyAccessor->getValue($object, $path));
}
public function getReferenceChainObjectsForIsWritable()
{
return array(
array(new TestClassIsWritable(array('a' => array('b' => 'old-value'))), 'value[a][b]', false),
array(new TestClassIsWritable(new \ArrayIterator(array('a' => array('b' => 'old-value')))), 'value[a][b]', true),
array(new TestClassIsWritable(array('a' => array('b' => array('c' => new TestClassSetValue('old-value'))))), 'value[a][b][c].value', true),
);
}
/**
* @dataProvider getReferenceChainObjectsForIsWritable
*/
public function testIsWritableForReferenceChainIssue($object, $path, $value)
{
$this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "DateTime", "string" given
*/
public function testThrowTypeError()
{
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'date', 'This is a string, \DateTime expected.');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "DateTime", "NULL" given
*/
public function testThrowTypeErrorWithNullArgument()
{
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'date', null);
}
public function testSetTypeHint()
{
$date = new \DateTime();
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'date', $date);
$this->assertSame($date, $object->getDate());
}
public function testArrayNotBeeingOverwritten()
{
$value = array('value1' => 'foo', 'value2' => 'bar');
$object = new TestClass($value);
$this->propertyAccessor->setValue($object, 'publicAccessor[value2]', 'baz');
$this->assertSame('baz', $this->propertyAccessor->getValue($object, 'publicAccessor[value2]'));
$this->assertSame(array('value1' => 'foo', 'value2' => 'baz'), $object->getPublicAccessor());
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "Countable", "string" given
*/
public function testThrowTypeErrorWithInterface()
{
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'countable', 'This is a string, \Countable expected.');
}
/**
* @requires PHP 7
*
* @expectedException \TypeError
*/
public function testDoNotDiscardReturnTypeError()
{
$object = new ReturnTyped();
$this->propertyAccessor->setValue($object, 'foos', array(new \DateTime()));
}
/**
* @requires PHP 7
*
* @expectedException \TypeError
*/
public function testDoNotDiscardReturnTypeErrorWhenWriterMethodIsMisconfigured()
{
$object = new ReturnTyped();
$this->propertyAccessor->setValue($object, 'name', 'foo');
}
}

View File

@@ -0,0 +1,22 @@
<?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\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TraversableArrayObject;
class PropertyAccessorTraversableArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getContainer(array $array)
{
return new TraversableArrayObject($array);
}
}

View File

@@ -0,0 +1,288 @@
<?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\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathBuilderTest extends TestCase
{
const PREFIX = 'old1[old2].old3[old4][old5].old6';
/**
* @var PropertyPathBuilder
*/
private $builder;
protected function setUp()
{
$this->builder = new PropertyPathBuilder(new PropertyPath(self::PREFIX));
}
public function testCreateEmpty()
{
$builder = new PropertyPathBuilder();
$this->assertNull($builder->getPropertyPath());
}
public function testCreateCopyPath()
{
$this->assertEquals(new PropertyPath(self::PREFIX), $this->builder->getPropertyPath());
}
public function testAppendIndex()
{
$this->builder->appendIndex('new1');
$path = new PropertyPath(self::PREFIX.'[new1]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendProperty()
{
$this->builder->appendProperty('new1');
$path = new PropertyPath(self::PREFIX.'.new1');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppend()
{
$this->builder->append(new PropertyPath('new1[new2]'));
$path = new PropertyPath(self::PREFIX.'.new1[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendUsingString()
{
$this->builder->append('new1[new2]');
$path = new PropertyPath(self::PREFIX.'.new1[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendWithOffset()
{
$this->builder->append(new PropertyPath('new1[new2].new3'), 1);
$path = new PropertyPath(self::PREFIX.'[new2].new3');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendWithOffsetAndLength()
{
$this->builder->append(new PropertyPath('new1[new2].new3'), 1, 1);
$path = new PropertyPath(self::PREFIX.'[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByIndex()
{
$this->builder->replaceByIndex(1, 'new1');
$path = new PropertyPath('old1[new1].old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByIndexWithoutName()
{
$this->builder->replaceByIndex(0);
$path = new PropertyPath('[old1][old2].old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByIndexDoesNotAllowInvalidOffsets()
{
$this->builder->replaceByIndex(6, 'new1');
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByIndexDoesNotAllowNegativeOffsets()
{
$this->builder->replaceByIndex(-1, 'new1');
}
public function testReplaceByProperty()
{
$this->builder->replaceByProperty(1, 'new1');
$path = new PropertyPath('old1.new1.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByPropertyWithoutName()
{
$this->builder->replaceByProperty(1);
$path = new PropertyPath('old1.old2.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByPropertyDoesNotAllowInvalidOffsets()
{
$this->builder->replaceByProperty(6, 'new1');
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByPropertyDoesNotAllowNegativeOffsets()
{
$this->builder->replaceByProperty(-1, 'new1');
}
public function testReplace()
{
$this->builder->replace(1, 1, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceUsingString()
{
$this->builder->replace(1, 1, 'new1[new2].new3');
$path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceNegative()
{
$this->builder->replace(-1, 1, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('old1[old2].old3[old4][old5].new1[new2].new3');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @dataProvider provideInvalidOffsets
* @expectedException \OutOfBoundsException
*/
public function testReplaceDoesNotAllowInvalidOffsets($offset)
{
$this->builder->replace($offset, 1, new PropertyPath('new1[new2].new3'));
}
public function provideInvalidOffsets()
{
return array(
array(6),
array(-7),
);
}
public function testReplaceWithLengthGreaterOne()
{
$this->builder->replace(0, 2, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceSubstring()
{
$this->builder->replace(1, 1, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
$path = new PropertyPath('old1[new2].new3.new4.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceSubstringWithLengthGreaterOne()
{
$this->builder->replace(1, 2, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
$path = new PropertyPath('old1[new2].new3.new4[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
// https://github.com/symfony/symfony/issues/5605
public function testReplaceWithLongerPath()
{
// error occurs when path contains at least two more elements
// than the builder
$path = new PropertyPath('new1.new2.new3');
$builder = new PropertyPathBuilder(new PropertyPath('old1'));
$builder->replace(0, 1, $path);
$this->assertEquals($path, $builder->getPropertyPath());
}
public function testReplaceWithLongerPathKeepsOrder()
{
$path = new PropertyPath('new1.new2.new3');
$expected = new PropertyPath('new1.new2.new3.old2');
$builder = new PropertyPathBuilder(new PropertyPath('old1.old2'));
$builder->replace(0, 1, $path);
$this->assertEquals($expected, $builder->getPropertyPath());
}
public function testRemove()
{
$this->builder->remove(3);
$path = new PropertyPath('old1[old2].old3[old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testRemoveDoesNotAllowInvalidOffsets()
{
$this->builder->remove(6);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testRemoveDoesNotAllowNegativeOffsets()
{
$this->builder->remove(-1);
}
}

View File

@@ -0,0 +1,206 @@
<?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\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyPath;
class PropertyPathTest extends TestCase
{
public function testToString()
{
$path = new PropertyPath('reference.traversable[index].property');
$this->assertEquals('reference.traversable[index].property', $path->__toString());
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testDotIsRequiredBeforeProperty()
{
new PropertyPath('[index]property');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testDotCannotBePresentAtTheBeginning()
{
new PropertyPath('.property');
}
public function providePathsContainingUnexpectedCharacters()
{
return array(
array('property.'),
array('property.['),
array('property..'),
array('property['),
array('property[['),
array('property[.'),
array('property[]'),
);
}
/**
* @dataProvider providePathsContainingUnexpectedCharacters
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testUnexpectedCharacters($path)
{
new PropertyPath($path);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testPathCannotBeEmpty()
{
new PropertyPath('');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
*/
public function testPathCannotBeNull()
{
new PropertyPath(null);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
*/
public function testPathCannotBeFalse()
{
new PropertyPath(false);
}
public function testZeroIsValidPropertyPath()
{
$propertyPath = new PropertyPath('0');
$this->assertSame('0', (string) $propertyPath);
}
public function testGetParentWithDot()
{
$propertyPath = new PropertyPath('grandpa.parent.child');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParentWithIndex()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParentWhenThereIsNoParent()
{
$propertyPath = new PropertyPath('path');
$this->assertNull($propertyPath->getParent());
}
public function testCopyConstructor()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$copy = new PropertyPath($propertyPath);
$this->assertEquals($propertyPath, $copy);
}
public function testGetElement()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals('child', $propertyPath->getElement(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->getElement(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->getElement(-1);
}
public function testIsProperty()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertTrue($propertyPath->isProperty(1));
$this->assertFalse($propertyPath->isProperty(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isProperty(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isProperty(-1);
}
public function testIsIndex()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertFalse($propertyPath->isIndex(1));
$this->assertTrue($propertyPath->isIndex(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isIndex(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isIndex(-1);
}
}

View File

@@ -0,0 +1,172 @@
<?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\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\StringUtil;
class StringUtilTest extends TestCase
{
public function singularifyProvider()
{
// see http://english-zone.com/spelling/plurals.html
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
return array(
array('accesses', 'access'),
array('addresses', 'address'),
array('agendas', 'agenda'),
array('alumnae', 'alumna'),
array('alumni', 'alumnus'),
array('analyses', array('analys', 'analyse', 'analysis')),
array('antennae', 'antenna'),
array('antennas', 'antenna'),
array('appendices', array('appendex', 'appendix', 'appendice')),
array('arches', array('arch', 'arche')),
array('atlases', array('atlas', 'atlase', 'atlasis')),
array('axes', array('ax', 'axe', 'axis')),
array('babies', 'baby'),
array('bacteria', array('bacterion', 'bacterium')),
array('bases', array('bas', 'base', 'basis')),
array('batches', array('batch', 'batche')),
array('beaux', 'beau'),
array('bees', array('be', 'bee')),
array('boxes', 'box'),
array('boys', 'boy'),
array('bureaus', 'bureau'),
array('bureaux', 'bureau'),
array('buses', array('bus', 'buse', 'busis')),
array('bushes', array('bush', 'bushe')),
array('calves', array('calf', 'calve', 'calff')),
array('cars', 'car'),
array('cassettes', array('cassett', 'cassette')),
array('caves', array('caf', 'cave', 'caff')),
array('chateaux', 'chateau'),
array('cheeses', array('chees', 'cheese', 'cheesis')),
array('children', 'child'),
array('circuses', array('circus', 'circuse', 'circusis')),
array('cliffs', 'cliff'),
array('committee', 'committee'),
array('crises', array('cris', 'crise', 'crisis')),
array('criteria', array('criterion', 'criterium')),
array('cups', 'cup'),
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')),
array('echoes', array('echo', 'echoe')),
array('elves', array('elf', 'elve', 'elff')),
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'),
array('formulas', 'formula'),
array('fungi', 'fungus'),
array('funguses', array('fungus', 'funguse', 'fungusis')),
array('garages', array('garag', 'garage')),
array('geese', 'goose'),
array('halves', array('half', 'halve', 'halff')),
array('hats', 'hat'),
array('heroes', array('hero', 'heroe')),
array('hippopotamuses', array('hippopotamus', 'hippopotamuse', 'hippopotamusis')), //hippopotami
array('hoaxes', 'hoax'),
array('hooves', array('hoof', 'hoove', 'hooff')),
array('houses', array('hous', 'house', 'housis')),
array('indexes', 'index'),
array('indices', array('index', 'indix', 'indice')),
array('ions', 'ion'),
array('irises', array('iris', 'irise', 'irisis')),
array('kisses', 'kiss'),
array('knives', 'knife'),
array('lamps', 'lamp'),
array('leaves', array('leaf', 'leave', 'leaff')),
array('lice', 'louse'),
array('lives', 'life'),
array('matrices', array('matrex', 'matrix', 'matrice')),
array('matrixes', 'matrix'),
array('men', 'man'),
array('mice', 'mouse'),
array('moves', 'move'),
array('movies', 'movie'),
array('nebulae', 'nebula'),
array('neuroses', array('neuros', 'neurose', 'neurosis')),
array('news', 'news'),
array('oases', array('oas', 'oase', 'oasis')),
array('objectives', 'objective'),
array('oxen', 'ox'),
array('parties', 'party'),
array('people', 'person'),
array('persons', 'person'),
array('phenomena', array('phenomenon', 'phenomenum')),
array('photos', 'photo'),
array('pianos', 'piano'),
array('plateaux', 'plateau'),
array('poppies', 'poppy'),
array('prices', array('prex', 'prix', 'price')),
array('quizzes', 'quiz'),
array('radii', 'radius'),
array('roofs', 'roof'),
array('roses', array('ros', 'rose', 'rosis')),
array('sandwiches', array('sandwich', 'sandwiche')),
array('scarves', array('scarf', 'scarve', 'scarff')),
array('schemas', 'schema'), //schemata
array('selfies', 'selfie'),
array('series', 'series'),
array('services', 'service'),
array('sheriffs', 'sheriff'),
array('shoes', array('sho', 'shoe')),
array('spies', 'spy'),
array('staves', array('staf', 'stave', 'staff')),
array('stories', 'story'),
array('strata', array('straton', 'stratum')),
array('suitcases', array('suitcas', 'suitcase', 'suitcasis')),
array('syllabi', 'syllabus'),
array('tags', 'tag'),
array('teeth', 'tooth'),
array('theses', array('thes', 'these', 'thesis')),
array('thieves', array('thief', 'thieve', 'thieff')),
array('trees', array('tre', 'tree')),
array('waltzes', array('waltz', 'waltze')),
array('wives', 'wife'),
// test casing: if the first letter was uppercase, it should remain so
array('Men', 'Man'),
array('GrandChildren', 'GrandChild'),
array('SubTrees', array('SubTre', 'SubTree')),
// Known issues
//array('insignia', 'insigne'),
//array('insignias', 'insigne'),
//array('rattles', 'rattle'),
);
}
/**
* @dataProvider singularifyProvider
*/
public function testSingularify($plural, $singular)
{
$single = StringUtil::singularify($plural);
if (\is_string($singular) && \is_array($single)) {
$this->fail("--- Expected\n`string`: ".$singular."\n+++ Actual\n`array`: ".implode(', ', $single));
} elseif (\is_array($singular) && \is_string($single)) {
$this->fail("--- Expected\n`array`: ".implode(', ', $singular)."\n+++ Actual\n`string`: ".$single);
}
$this->assertEquals($singular, $single);
}
}

View File

@@ -0,0 +1,34 @@
{
"name": "symfony/property-access",
"type": "library",
"description": "Symfony PropertyAccess Component",
"keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property path"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.9",
"symfony/polyfill-ctype": "~1.8"
},
"autoload": {
"psr-4": { "Symfony\\Component\\PropertyAccess\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony PropertyAccess Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

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

149
one-api/vendor/symfony/yaml/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,149 @@
CHANGELOG
=========
3.4.0
-----
* added support for parsing YAML files using the `Yaml::parseFile()` or `Parser::parseFile()` method
* the `Dumper`, `Parser`, and `Yaml` classes are marked as final
* Deprecated the `!php/object:` tag which will be replaced by the
`!php/object` tag (without the colon) in 4.0.
* Deprecated the `!php/const:` tag which will be replaced by the
`!php/const` tag (without the colon) in 4.0.
* Support for the `!str` tag is deprecated, use the `!!str` tag instead.
* Deprecated using the non-specific tag `!` as its behavior will change in 4.0.
It will force non-evaluating your values in 4.0. Use plain integers or `!!float` instead.
3.3.0
-----
* Starting an unquoted string with a question mark followed by a space is
deprecated and will throw a `ParseException` in Symfony 4.0.
* Deprecated support for implicitly parsing non-string mapping keys as strings.
Mapping keys that are no strings will lead to a `ParseException` in Symfony
4.0. Use quotes to opt-in for keys to be parsed as strings.
Before:
```php
$yaml = <<<YAML
null: null key
true: boolean true
2.0: float key
YAML;
Yaml::parse($yaml);
```
After:
```php
$yaml = <<<YAML
"null": null key
"true": boolean true
"2.0": float key
YAML;
Yaml::parse($yaml);
```
* Omitted mapping values will be parsed as `null`.
* Omitting the key of a mapping is deprecated and will throw a `ParseException` in Symfony 4.0.
* Added support for dumping empty PHP arrays as YAML sequences:
```php
Yaml::dump([], 0, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
```
3.2.0
-----
* Mappings with a colon (`:`) that is not followed by a whitespace are deprecated
when the mapping key is not quoted and will lead to a `ParseException` in
Symfony 4.0 (e.g. `foo:bar` must be `foo: bar`).
* Added support for parsing PHP constants:
```php
Yaml::parse('!php/const:PHP_INT_MAX', Yaml::PARSE_CONSTANT);
```
* Support for silently ignoring duplicate mapping keys in YAML has been
deprecated and will lead to a `ParseException` in Symfony 4.0.
3.1.0
-----
* Added support to dump `stdClass` and `ArrayAccess` objects as YAML mappings
through the `Yaml::DUMP_OBJECT_AS_MAP` flag.
* Strings that are not UTF-8 encoded will be dumped as base64 encoded binary
data.
* Added support for dumping multi line strings as literal blocks.
* Added support for parsing base64 encoded binary data when they are tagged
with the `!!binary` tag.
* Added support for parsing timestamps as `\DateTime` objects:
```php
Yaml::parse('2001-12-15 21:59:43.10 -5', Yaml::PARSE_DATETIME);
```
* `\DateTime` and `\DateTimeImmutable` objects are dumped as YAML timestamps.
* Deprecated usage of `%` at the beginning of an unquoted string.
* Added support for customizing the YAML parser behavior through an optional bit field:
```php
Yaml::parse('{ "foo": "bar", "fiz": "cat" }', Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE | Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP);
```
* Added support for customizing the dumped YAML string through an optional bit field:
```php
Yaml::dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE | Yaml::DUMP_OBJECT);
```
3.0.0
-----
* Yaml::parse() now throws an exception when a blackslash is not escaped
in double-quoted strings
2.8.0
-----
* Deprecated usage of a colon in an unquoted mapping value
* Deprecated usage of @, \`, | and > at the beginning of an unquoted string
* When surrounding strings with double-quotes, you must now escape `\` characters. Not
escaping those characters (when surrounded by double-quotes) is deprecated.
Before:
```yml
class: "Foo\Var"
```
After:
```yml
class: "Foo\\Var"
```
2.1.0
-----
* Yaml::parse() does not evaluate loaded files as PHP files by default
anymore (call Yaml::enablePhpParsing() to get back the old behavior)

View File

@@ -0,0 +1,250 @@
<?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\Yaml\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
/**
* Validates YAML files syntax and outputs encountered errors.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LintCommand extends Command
{
protected static $defaultName = 'lint:yaml';
private $parser;
private $format;
private $displayCorrectFiles;
private $directoryIteratorProvider;
private $isReadableProvider;
public function __construct($name = null, $directoryIteratorProvider = null, $isReadableProvider = null)
{
parent::__construct($name);
$this->directoryIteratorProvider = $directoryIteratorProvider;
$this->isReadableProvider = $isReadableProvider;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDescription('Lints a file and outputs encountered errors')
->addArgument('filename', null, 'A file or a directory or STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->addOption('parse-tags', null, InputOption::VALUE_NONE, 'Parse custom tags')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints a YAML file and outputs to STDOUT
the first encountered syntax error.
You can validates YAML contents passed from STDIN:
<info>cat filename | php %command.full_name%</info>
You can also validate the syntax of a file:
<info>php %command.full_name% filename</info>
Or of a whole directory:
<info>php %command.full_name% dirname</info>
<info>php %command.full_name% dirname --format=json</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$filename = $input->getArgument('filename');
$this->format = $input->getOption('format');
$this->displayCorrectFiles = $output->isVerbose();
$flags = $input->getOption('parse-tags') ? Yaml::PARSE_CUSTOM_TAGS : 0;
if (!$filename) {
if (!$stdin = $this->getStdin()) {
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
}
return $this->display($io, [$this->validate($stdin, $flags)]);
}
if (!$this->isReadable($filename)) {
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
}
$filesInfo = [];
foreach ($this->getFiles($filename) as $file) {
$filesInfo[] = $this->validate(file_get_contents($file), $flags, $file);
}
return $this->display($io, $filesInfo);
}
private function validate($content, $flags, $file = null)
{
$prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) {
if (E_USER_DEPRECATED === $level) {
throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1);
}
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
});
try {
$this->getParser()->parse($content, Yaml::PARSE_CONSTANT | $flags);
} catch (ParseException $e) {
return ['file' => $file, 'line' => $e->getParsedLine(), 'valid' => false, 'message' => $e->getMessage()];
} finally {
restore_error_handler();
}
return ['file' => $file, 'valid' => true];
}
private function display(SymfonyStyle $io, array $files)
{
switch ($this->format) {
case 'txt':
return $this->displayTxt($io, $files);
case 'json':
return $this->displayJson($io, $files);
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format));
}
}
private function displayTxt(SymfonyStyle $io, array $filesInfo)
{
$countFiles = \count($filesInfo);
$erroredFiles = 0;
foreach ($filesInfo as $info) {
if ($info['valid'] && $this->displayCorrectFiles) {
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
} elseif (!$info['valid']) {
++$erroredFiles;
$io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
$io->text(sprintf('<error> >> %s</error>', $info['message']));
}
}
if (0 === $erroredFiles) {
$io->success(sprintf('All %d YAML files contain valid syntax.', $countFiles));
} else {
$io->warning(sprintf('%d YAML files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
}
return min($erroredFiles, 1);
}
private function displayJson(SymfonyStyle $io, array $filesInfo)
{
$errors = 0;
array_walk($filesInfo, function (&$v) use (&$errors) {
$v['file'] = (string) $v['file'];
if (!$v['valid']) {
++$errors;
}
});
$io->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return min($errors, 1);
}
private function getFiles($fileOrDirectory)
{
if (is_file($fileOrDirectory)) {
yield new \SplFileInfo($fileOrDirectory);
return;
}
foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
if (!\in_array($file->getExtension(), ['yml', 'yaml'])) {
continue;
}
yield $file;
}
}
private function getStdin()
{
if (0 !== ftell(STDIN)) {
return;
}
$inputs = '';
while (!feof(STDIN)) {
$inputs .= fread(STDIN, 1024);
}
return $inputs;
}
private function getParser()
{
if (!$this->parser) {
$this->parser = new Parser();
}
return $this->parser;
}
private function getDirectoryIterator($directory)
{
$default = function ($directory) {
return new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
};
if (null !== $this->directoryIteratorProvider) {
return \call_user_func($this->directoryIteratorProvider, $directory, $default);
}
return $default($directory);
}
private function isReadable($fileOrDirectory)
{
$default = function ($fileOrDirectory) {
return is_readable($fileOrDirectory);
};
if (null !== $this->isReadableProvider) {
return \call_user_func($this->isReadableProvider, $fileOrDirectory, $default);
}
return $default($fileOrDirectory);
}
}

132
one-api/vendor/symfony/yaml/Dumper.php vendored Normal file
View File

@@ -0,0 +1,132 @@
<?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\Yaml;
/**
* Dumper dumps PHP variables to YAML strings.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since version 3.4
*/
class Dumper
{
/**
* The amount of spaces to use for indentation of nested nodes.
*
* @var int
*/
protected $indentation;
/**
* @param int $indentation
*/
public function __construct($indentation = 4)
{
if ($indentation < 1) {
throw new \InvalidArgumentException('The indentation must be greater than zero.');
}
$this->indentation = $indentation;
}
/**
* Sets the indentation.
*
* @param int $num The amount of spaces to use for indentation of nested nodes
*
* @deprecated since version 3.1, to be removed in 4.0. Pass the indentation to the constructor instead.
*/
public function setIndentation($num)
{
@trigger_error('The '.__METHOD__.' method is deprecated since Symfony 3.1 and will be removed in 4.0. Pass the indentation to the constructor instead.', E_USER_DEPRECATED);
$this->indentation = (int) $num;
}
/**
* Dumps a PHP value to YAML.
*
* @param mixed $input The PHP value
* @param int $inline The level where you switch to inline YAML
* @param int $indent The level of indentation (used internally)
* @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
*
* @return string The YAML representation of the PHP value
*/
public function dump($input, $inline = 0, $indent = 0, $flags = 0)
{
if (\is_bool($flags)) {
@trigger_error('Passing a boolean flag to toggle exception handling is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
if ($flags) {
$flags = Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE;
} else {
$flags = 0;
}
}
if (\func_num_args() >= 5) {
@trigger_error('Passing a boolean flag to toggle object support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::DUMP_OBJECT flag instead.', E_USER_DEPRECATED);
if (func_get_arg(4)) {
$flags |= Yaml::DUMP_OBJECT;
}
}
$output = '';
$prefix = $indent ? str_repeat(' ', $indent) : '';
$dumpObjectAsInlineMap = true;
if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($input instanceof \ArrayObject || $input instanceof \stdClass)) {
$dumpObjectAsInlineMap = empty((array) $input);
}
if ($inline <= 0 || (!\is_array($input) && $dumpObjectAsInlineMap) || empty($input)) {
$output .= $prefix.Inline::dump($input, $flags);
} else {
$dumpAsMap = Inline::isHash($input);
foreach ($input as $key => $value) {
if ($inline >= 1 && Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && false !== strpos($value, "\n") && false === strpos($value, "\r\n")) {
// If the first line starts with a space character, the spec requires a blockIndicationIndicator
// http://www.yaml.org/spec/1.2/spec.html#id2793979
$blockIndentationIndicator = (' ' === substr($value, 0, 1)) ? (string) $this->indentation : '';
$output .= sprintf("%s%s%s |%s\n", $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator);
foreach (preg_split('/\n|\r\n/', $value) as $row) {
$output .= sprintf("%s%s%s\n", $prefix, str_repeat(' ', $this->indentation), $row);
}
continue;
}
$dumpObjectAsInlineMap = true;
if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \ArrayObject || $value instanceof \stdClass)) {
$dumpObjectAsInlineMap = empty((array) $value);
}
$willBeInlined = $inline - 1 <= 0 || !\is_array($value) && $dumpObjectAsInlineMap || empty($value);
$output .= sprintf('%s%s%s%s',
$prefix,
$dumpAsMap ? Inline::dump($key, $flags).':' : '-',
$willBeInlined ? ' ' : "\n",
$this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags)
).($willBeInlined ? "\n" : '');
}
}
return $output;
}
}

101
one-api/vendor/symfony/yaml/Escaper.php vendored Normal file
View File

@@ -0,0 +1,101 @@
<?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\Yaml;
/**
* Escaper encapsulates escaping rules for single and double-quoted
* YAML strings.
*
* @author Matthew Lewinski <matthew@lewinski.org>
*
* @internal
*/
class Escaper
{
// Characters that would cause a dumped string to require double quoting.
const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9";
// Mapping arrays for escaping a double quoted string. The backslash is
// first to ensure proper escaping because str_replace operates iteratively
// on the input arrays. This ordering of the characters avoids the use of strtr,
// which performs more slowly.
private static $escapees = ['\\', '\\\\', '\\"', '"',
"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07",
"\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f",
"\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17",
"\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f",
"\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9",
];
private static $escaped = ['\\\\', '\\"', '\\\\', '\\"',
'\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a',
'\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f',
'\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17',
'\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f',
'\\N', '\\_', '\\L', '\\P',
];
/**
* Determines if a PHP value would require double quoting in YAML.
*
* @param string $value A PHP value
*
* @return bool True if the value would require double quotes
*/
public static function requiresDoubleQuoting($value)
{
return 0 < preg_match('/'.self::REGEX_CHARACTER_TO_ESCAPE.'/u', $value);
}
/**
* Escapes and surrounds a PHP value with double quotes.
*
* @param string $value A PHP value
*
* @return string The quoted, escaped string
*/
public static function escapeWithDoubleQuotes($value)
{
return sprintf('"%s"', str_replace(self::$escapees, self::$escaped, $value));
}
/**
* Determines if a PHP value would require single quoting in YAML.
*
* @param string $value A PHP value
*
* @return bool True if the value would require single quotes
*/
public static function requiresSingleQuoting($value)
{
// Determines if a PHP value is entirely composed of a value that would
// require single quoting in YAML.
if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'])) {
return true;
}
// Determines if the PHP value contains any single characters that would
// cause it to require single quoting in YAML.
return 0 < preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` ]/x', $value);
}
/**
* Escapes and surrounds a PHP value with single quotes.
*
* @param string $value A PHP value
*
* @return string The quoted, escaped string
*/
public static function escapeWithSingleQuotes($value)
{
return sprintf("'%s'", str_replace('\'', '\'\'', $value));
}
}

View File

@@ -0,0 +1,21 @@
<?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\Yaml\Exception;
/**
* Exception class thrown when an error occurs during dumping.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class DumpException extends RuntimeException
{
}

View File

@@ -0,0 +1,21 @@
<?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\Yaml\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,139 @@
<?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\Yaml\Exception;
/**
* Exception class thrown when an error occurs during parsing.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ParseException extends RuntimeException
{
private $parsedFile;
private $parsedLine;
private $snippet;
private $rawMessage;
/**
* @param string $message The error message
* @param int $parsedLine The line where the error occurred
* @param string|null $snippet The snippet of code near the problem
* @param string|null $parsedFile The file name where the error occurred
* @param \Exception|null $previous The previous exception
*/
public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, \Exception $previous = null)
{
$this->parsedFile = $parsedFile;
$this->parsedLine = $parsedLine;
$this->snippet = $snippet;
$this->rawMessage = $message;
$this->updateRepr();
parent::__construct($this->message, 0, $previous);
}
/**
* Gets the snippet of code near the error.
*
* @return string The snippet of code
*/
public function getSnippet()
{
return $this->snippet;
}
/**
* Sets the snippet of code near the error.
*
* @param string $snippet The code snippet
*/
public function setSnippet($snippet)
{
$this->snippet = $snippet;
$this->updateRepr();
}
/**
* Gets the filename where the error occurred.
*
* This method returns null if a string is parsed.
*
* @return string The filename
*/
public function getParsedFile()
{
return $this->parsedFile;
}
/**
* Sets the filename where the error occurred.
*
* @param string $parsedFile The filename
*/
public function setParsedFile($parsedFile)
{
$this->parsedFile = $parsedFile;
$this->updateRepr();
}
/**
* Gets the line where the error occurred.
*
* @return int The file line
*/
public function getParsedLine()
{
return $this->parsedLine;
}
/**
* Sets the line where the error occurred.
*
* @param int $parsedLine The file line
*/
public function setParsedLine($parsedLine)
{
$this->parsedLine = $parsedLine;
$this->updateRepr();
}
private function updateRepr()
{
$this->message = $this->rawMessage;
$dot = false;
if ('.' === substr($this->message, -1)) {
$this->message = substr($this->message, 0, -1);
$dot = true;
}
if (null !== $this->parsedFile) {
$this->message .= sprintf(' in %s', json_encode($this->parsedFile, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
if ($this->parsedLine >= 0) {
$this->message .= sprintf(' at line %d', $this->parsedLine);
}
if ($this->snippet) {
$this->message .= sprintf(' (near "%s")', $this->snippet);
}
if ($dot) {
$this->message .= '.';
}
}
}

View File

@@ -0,0 +1,21 @@
<?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\Yaml\Exception;
/**
* Exception class thrown when an error occurs during parsing.
*
* @author Romain Neutron <imprec@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

903
one-api/vendor/symfony/yaml/Inline.php vendored Normal file
View File

@@ -0,0 +1,903 @@
<?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\Yaml;
use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Tag\TaggedValue;
/**
* Inline implements a YAML parser/dumper for the YAML inline syntax.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class Inline
{
const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')';
public static $parsedLineNumber = -1;
public static $parsedFilename;
private static $exceptionOnInvalidType = false;
private static $objectSupport = false;
private static $objectForMap = false;
private static $constantSupport = false;
/**
* @param int $flags
* @param int|null $parsedLineNumber
* @param string|null $parsedFilename
*/
public static function initialize($flags, $parsedLineNumber = null, $parsedFilename = null)
{
self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags);
self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags);
self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags);
self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags);
self::$parsedFilename = $parsedFilename;
if (null !== $parsedLineNumber) {
self::$parsedLineNumber = $parsedLineNumber;
}
}
/**
* Converts a YAML string to a PHP value.
*
* @param string $value A YAML string
* @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
* @param array $references Mapping of variable names to values
*
* @return mixed A PHP value
*
* @throws ParseException
*/
public static function parse($value, $flags = 0, $references = [])
{
if (\is_bool($flags)) {
@trigger_error('Passing a boolean flag to toggle exception handling is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
if ($flags) {
$flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
} else {
$flags = 0;
}
}
if (\func_num_args() >= 3 && !\is_array($references)) {
@trigger_error('Passing a boolean flag to toggle object support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.', E_USER_DEPRECATED);
if ($references) {
$flags |= Yaml::PARSE_OBJECT;
}
if (\func_num_args() >= 4) {
@trigger_error('Passing a boolean flag to toggle object for map support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.', E_USER_DEPRECATED);
if (func_get_arg(3)) {
$flags |= Yaml::PARSE_OBJECT_FOR_MAP;
}
}
if (\func_num_args() >= 5) {
$references = func_get_arg(4);
} else {
$references = [];
}
}
self::initialize($flags);
$value = trim($value);
if ('' === $value) {
return '';
}
if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
$mbEncoding = mb_internal_encoding();
mb_internal_encoding('ASCII');
}
try {
$i = 0;
$tag = self::parseTag($value, $i, $flags);
switch ($value[$i]) {
case '[':
$result = self::parseSequence($value, $flags, $i, $references);
++$i;
break;
case '{':
$result = self::parseMapping($value, $flags, $i, $references);
++$i;
break;
default:
$result = self::parseScalar($value, $flags, null, $i, null === $tag, $references);
}
if (null !== $tag) {
return new TaggedValue($tag, $result);
}
// some comments are allowed at the end
if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
}
return $result;
} finally {
if (isset($mbEncoding)) {
mb_internal_encoding($mbEncoding);
}
}
}
/**
* Dumps a given PHP variable to a YAML string.
*
* @param mixed $value The PHP variable to convert
* @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
*
* @return string The YAML string representing the PHP value
*
* @throws DumpException When trying to dump PHP resource
*/
public static function dump($value, $flags = 0)
{
if (\is_bool($flags)) {
@trigger_error('Passing a boolean flag to toggle exception handling is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
if ($flags) {
$flags = Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE;
} else {
$flags = 0;
}
}
if (\func_num_args() >= 3) {
@trigger_error('Passing a boolean flag to toggle object support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::DUMP_OBJECT flag instead.', E_USER_DEPRECATED);
if (func_get_arg(2)) {
$flags |= Yaml::DUMP_OBJECT;
}
}
switch (true) {
case \is_resource($value):
if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) {
throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value)));
}
return 'null';
case $value instanceof \DateTimeInterface:
return $value->format('c');
case \is_object($value):
if ($value instanceof TaggedValue) {
return '!'.$value->getTag().' '.self::dump($value->getValue(), $flags);
}
if (Yaml::DUMP_OBJECT & $flags) {
return '!php/object '.self::dump(serialize($value));
}
if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \stdClass || $value instanceof \ArrayObject)) {
return self::dumpArray($value, $flags & ~Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
}
if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) {
throw new DumpException('Object support when dumping a YAML file has been disabled.');
}
return 'null';
case \is_array($value):
return self::dumpArray($value, $flags);
case null === $value:
return 'null';
case true === $value:
return 'true';
case false === $value:
return 'false';
case ctype_digit($value):
return \is_string($value) ? "'$value'" : (int) $value;
case is_numeric($value):
$locale = setlocale(LC_NUMERIC, 0);
if (false !== $locale) {
setlocale(LC_NUMERIC, 'C');
}
if (\is_float($value)) {
$repr = (string) $value;
if (is_infinite($value)) {
$repr = str_ireplace('INF', '.Inf', $repr);
} elseif (floor($value) == $value && $repr == $value) {
// Preserve float data type since storing a whole number will result in integer value.
$repr = '!!float '.$repr;
}
} else {
$repr = \is_string($value) ? "'$value'" : (string) $value;
}
if (false !== $locale) {
setlocale(LC_NUMERIC, $locale);
}
return $repr;
case '' == $value:
return "''";
case self::isBinaryString($value):
return '!!binary '.base64_encode($value);
case Escaper::requiresDoubleQuoting($value):
return Escaper::escapeWithDoubleQuotes($value);
case Escaper::requiresSingleQuoting($value):
case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value):
case Parser::preg_match(self::getHexRegex(), $value):
case Parser::preg_match(self::getTimestampRegex(), $value):
return Escaper::escapeWithSingleQuotes($value);
default:
return $value;
}
}
/**
* Check if given array is hash or just normal indexed array.
*
* @internal
*
* @param array|\ArrayObject|\stdClass $value The PHP array or array-like object to check
*
* @return bool true if value is hash array, false otherwise
*/
public static function isHash($value)
{
if ($value instanceof \stdClass || $value instanceof \ArrayObject) {
return true;
}
$expectedKey = 0;
foreach ($value as $key => $val) {
if ($key !== $expectedKey++) {
return true;
}
}
return false;
}
/**
* Dumps a PHP array to a YAML string.
*
* @param array $value The PHP array to dump
* @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
*
* @return string The YAML string representing the PHP array
*/
private static function dumpArray($value, $flags)
{
// array
if (($value || Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE & $flags) && !self::isHash($value)) {
$output = [];
foreach ($value as $val) {
$output[] = self::dump($val, $flags);
}
return sprintf('[%s]', implode(', ', $output));
}
// hash
$output = [];
foreach ($value as $key => $val) {
$output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags));
}
return sprintf('{ %s }', implode(', ', $output));
}
/**
* Parses a YAML scalar.
*
* @param string $scalar
* @param int $flags
* @param string[] $delimiters
* @param int &$i
* @param bool $evaluate
* @param array $references
*
* @return string
*
* @throws ParseException When malformed inline YAML string is parsed
*
* @internal
*/
public static function parseScalar($scalar, $flags = 0, $delimiters = null, &$i = 0, $evaluate = true, $references = [], $legacyOmittedKeySupport = false)
{
if (\in_array($scalar[$i], ['"', "'"])) {
// quoted scalar
$output = self::parseQuotedScalar($scalar, $i);
if (null !== $delimiters) {
$tmp = ltrim(substr($scalar, $i), ' ');
if ('' === $tmp) {
throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
if (!\in_array($tmp[0], $delimiters)) {
throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
}
} else {
// "normal" string
if (!$delimiters) {
$output = substr($scalar, $i);
$i += \strlen($output);
// remove comments
if (Parser::preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) {
$output = substr($output, 0, $match[0][1]);
}
} elseif (Parser::preg_match('/^(.'.($legacyOmittedKeySupport ? '+' : '*').'?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
$output = $match[1];
$i += \strlen($output);
} else {
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar), self::$parsedLineNumber + 1, null, self::$parsedFilename);
}
// a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >)
if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) {
throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber + 1, $output, self::$parsedFilename);
}
if ($output && '%' === $output[0]) {
@trigger_error(self::getDeprecationMessage(sprintf('Not quoting the scalar "%s" starting with the "%%" indicator character is deprecated since Symfony 3.1 and will throw a ParseException in 4.0.', $output)), E_USER_DEPRECATED);
}
if ($evaluate) {
$output = self::evaluateScalar($output, $flags, $references);
}
}
return $output;
}
/**
* Parses a YAML quoted scalar.
*
* @param string $scalar
* @param int &$i
*
* @return string
*
* @throws ParseException When malformed inline YAML string is parsed
*/
private static function parseQuotedScalar($scalar, &$i)
{
if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
$output = substr($match[0], 1, \strlen($match[0]) - 2);
$unescaper = new Unescaper();
if ('"' == $scalar[$i]) {
$output = $unescaper->unescapeDoubleQuotedString($output);
} else {
$output = $unescaper->unescapeSingleQuotedString($output);
}
$i += \strlen($match[0]);
return $output;
}
/**
* Parses a YAML sequence.
*
* @param string $sequence
* @param int $flags
* @param int &$i
* @param array $references
*
* @return array
*
* @throws ParseException When malformed inline YAML string is parsed
*/
private static function parseSequence($sequence, $flags, &$i = 0, $references = [])
{
$output = [];
$len = \strlen($sequence);
++$i;
// [foo, bar, ...]
while ($i < $len) {
if (']' === $sequence[$i]) {
return $output;
}
if (',' === $sequence[$i] || ' ' === $sequence[$i]) {
++$i;
continue;
}
$tag = self::parseTag($sequence, $i, $flags);
switch ($sequence[$i]) {
case '[':
// nested sequence
$value = self::parseSequence($sequence, $flags, $i, $references);
break;
case '{':
// nested mapping
$value = self::parseMapping($sequence, $flags, $i, $references);
break;
default:
$isQuoted = \in_array($sequence[$i], ['"', "'"]);
$value = self::parseScalar($sequence, $flags, [',', ']'], $i, null === $tag, $references);
// the value can be an array if a reference has been resolved to an array var
if (\is_string($value) && !$isQuoted && false !== strpos($value, ': ')) {
// embedded mapping?
try {
$pos = 0;
$value = self::parseMapping('{'.$value.'}', $flags, $pos, $references);
} catch (\InvalidArgumentException $e) {
// no, it's not
}
}
--$i;
}
if (null !== $tag) {
$value = new TaggedValue($tag, $value);
}
$output[] = $value;
++$i;
}
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence), self::$parsedLineNumber + 1, null, self::$parsedFilename);
}
/**
* Parses a YAML mapping.
*
* @param string $mapping
* @param int $flags
* @param int &$i
* @param array $references
*
* @return array|\stdClass
*
* @throws ParseException When malformed inline YAML string is parsed
*/
private static function parseMapping($mapping, $flags, &$i = 0, $references = [])
{
$output = [];
$len = \strlen($mapping);
++$i;
$allowOverwrite = false;
// {foo: bar, bar:foo, ...}
while ($i < $len) {
switch ($mapping[$i]) {
case ' ':
case ',':
++$i;
continue 2;
case '}':
if (self::$objectForMap) {
return (object) $output;
}
return $output;
}
// key
$isKeyQuoted = \in_array($mapping[$i], ['"', "'"], true);
$key = self::parseScalar($mapping, $flags, [':', ' '], $i, false, [], true);
if (':' !== $key && false === $i = strpos($mapping, ':', $i)) {
break;
}
if (':' === $key) {
@trigger_error(self::getDeprecationMessage('Omitting the key of a mapping is deprecated and will throw a ParseException in 4.0.'), E_USER_DEPRECATED);
}
if (!$isKeyQuoted) {
$evaluatedKey = self::evaluateScalar($key, $flags, $references);
if ('' !== $key && $evaluatedKey !== $key && !\is_string($evaluatedKey) && !\is_int($evaluatedKey)) {
@trigger_error(self::getDeprecationMessage('Implicit casting of incompatible mapping keys to strings is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Quote your evaluable mapping keys instead.'), E_USER_DEPRECATED);
}
}
if (':' !== $key && !$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}'], true))) {
@trigger_error(self::getDeprecationMessage('Using a colon after an unquoted mapping key that is not followed by an indication character (i.e. " ", ",", "[", "]", "{", "}") is deprecated since Symfony 3.2 and will throw a ParseException in 4.0.'), E_USER_DEPRECATED);
}
if ('<<' === $key) {
$allowOverwrite = true;
}
while ($i < $len) {
if (':' === $mapping[$i] || ' ' === $mapping[$i]) {
++$i;
continue;
}
$tag = self::parseTag($mapping, $i, $flags);
switch ($mapping[$i]) {
case '[':
// nested sequence
$value = self::parseSequence($mapping, $flags, $i, $references);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
// But overwriting is allowed when a merge node is used in current block.
if ('<<' === $key) {
foreach ($value as $parsedValue) {
$output += $parsedValue;
}
} elseif ($allowOverwrite || !isset($output[$key])) {
if (null !== $tag) {
$output[$key] = new TaggedValue($tag, $value);
} else {
$output[$key] = $value;
}
} elseif (isset($output[$key])) {
@trigger_error(self::getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key)), E_USER_DEPRECATED);
}
break;
case '{':
// nested mapping
$value = self::parseMapping($mapping, $flags, $i, $references);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
// But overwriting is allowed when a merge node is used in current block.
if ('<<' === $key) {
$output += $value;
} elseif ($allowOverwrite || !isset($output[$key])) {
if (null !== $tag) {
$output[$key] = new TaggedValue($tag, $value);
} else {
$output[$key] = $value;
}
} elseif (isset($output[$key])) {
@trigger_error(self::getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key)), E_USER_DEPRECATED);
}
break;
default:
$value = self::parseScalar($mapping, $flags, [',', '}'], $i, null === $tag, $references);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
// But overwriting is allowed when a merge node is used in current block.
if ('<<' === $key) {
$output += $value;
} elseif ($allowOverwrite || !isset($output[$key])) {
if (null !== $tag) {
$output[$key] = new TaggedValue($tag, $value);
} else {
$output[$key] = $value;
}
} elseif (isset($output[$key])) {
@trigger_error(self::getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key)), E_USER_DEPRECATED);
}
--$i;
}
++$i;
continue 2;
}
}
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping), self::$parsedLineNumber + 1, null, self::$parsedFilename);
}
/**
* Evaluates scalars and replaces magic values.
*
* @param string $scalar
* @param int $flags
* @param array $references
*
* @return mixed The evaluated YAML string
*
* @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved
*/
private static function evaluateScalar($scalar, $flags, $references = [])
{
$scalar = trim($scalar);
$scalarLower = strtolower($scalar);
if (0 === strpos($scalar, '*')) {
if (false !== $pos = strpos($scalar, '#')) {
$value = substr($scalar, 1, $pos - 2);
} else {
$value = substr($scalar, 1);
}
// an unquoted *
if (false === $value || '' === $value) {
throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber + 1, $value, self::$parsedFilename);
}
if (!\array_key_exists($value, $references)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
}
return $references[$value];
}
switch (true) {
case 'null' === $scalarLower:
case '' === $scalar:
case '~' === $scalar:
return;
case 'true' === $scalarLower:
return true;
case 'false' === $scalarLower:
return false;
case '!' === $scalar[0]:
switch (true) {
case 0 === strpos($scalar, '!str'):
@trigger_error(self::getDeprecationMessage('Support for the !str tag is deprecated since Symfony 3.4. Use the !!str tag instead.'), E_USER_DEPRECATED);
return (string) substr($scalar, 5);
case 0 === strpos($scalar, '!!str '):
return (string) substr($scalar, 6);
case 0 === strpos($scalar, '! '):
@trigger_error(self::getDeprecationMessage('Using the non-specific tag "!" is deprecated since Symfony 3.4 as its behavior will change in 4.0. It will force non-evaluating your values in 4.0. Use plain integers or !!float instead.'), E_USER_DEPRECATED);
return (int) self::parseScalar(substr($scalar, 2), $flags);
case 0 === strpos($scalar, '!php/object:'):
if (self::$objectSupport) {
@trigger_error(self::getDeprecationMessage('The !php/object: tag to indicate dumped PHP objects is deprecated since Symfony 3.4 and will be removed in 4.0. Use the !php/object (without the colon) tag instead.'), E_USER_DEPRECATED);
return unserialize(substr($scalar, 12));
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
return;
case 0 === strpos($scalar, '!!php/object:'):
if (self::$objectSupport) {
@trigger_error(self::getDeprecationMessage('The !!php/object: tag to indicate dumped PHP objects is deprecated since Symfony 3.1 and will be removed in 4.0. Use the !php/object (without the colon) tag instead.'), E_USER_DEPRECATED);
return unserialize(substr($scalar, 13));
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
return;
case 0 === strpos($scalar, '!php/object'):
if (self::$objectSupport) {
return unserialize(self::parseScalar(substr($scalar, 12)));
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
return;
case 0 === strpos($scalar, '!php/const:'):
if (self::$constantSupport) {
@trigger_error(self::getDeprecationMessage('The !php/const: tag to indicate dumped PHP constants is deprecated since Symfony 3.4 and will be removed in 4.0. Use the !php/const (without the colon) tag instead.'), E_USER_DEPRECATED);
if (\defined($const = substr($scalar, 11))) {
return \constant($const);
}
throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
if (self::$exceptionOnInvalidType) {
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
return;
case 0 === strpos($scalar, '!php/const'):
if (self::$constantSupport) {
$i = 0;
if (\defined($const = self::parseScalar(substr($scalar, 11), 0, null, $i, false))) {
return \constant($const);
}
throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
if (self::$exceptionOnInvalidType) {
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
return;
case 0 === strpos($scalar, '!!float '):
return (float) substr($scalar, 8);
case 0 === strpos($scalar, '!!binary '):
return self::evaluateBinaryScalar(substr($scalar, 9));
default:
@trigger_error(self::getDeprecationMessage(sprintf('Using the unquoted scalar value "%s" is deprecated since Symfony 3.3 and will be considered as a tagged value in 4.0. You must quote it.', $scalar)), E_USER_DEPRECATED);
}
// Optimize for returning strings.
// no break
case '+' === $scalar[0] || '-' === $scalar[0] || '.' === $scalar[0] || is_numeric($scalar[0]):
switch (true) {
case Parser::preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar):
$scalar = str_replace('_', '', (string) $scalar);
// omitting the break / return as integers are handled in the next case
// no break
case ctype_digit($scalar):
$raw = $scalar;
$cast = (int) $scalar;
return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw);
case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
$raw = $scalar;
$cast = (int) $scalar;
return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw);
case is_numeric($scalar):
case Parser::preg_match(self::getHexRegex(), $scalar):
$scalar = str_replace('_', '', $scalar);
return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar;
case '.inf' === $scalarLower:
case '.nan' === $scalarLower:
return -log(0);
case '-.inf' === $scalarLower:
return log(0);
case Parser::preg_match('/^(-|\+)?[0-9][0-9,]*(\.[0-9_]+)?$/', $scalar):
case Parser::preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar):
if (false !== strpos($scalar, ',')) {
@trigger_error(self::getDeprecationMessage('Using the comma as a group separator for floats is deprecated since Symfony 3.2 and will be removed in 4.0.'), E_USER_DEPRECATED);
}
return (float) str_replace([',', '_'], '', $scalar);
case Parser::preg_match(self::getTimestampRegex(), $scalar):
if (Yaml::PARSE_DATETIME & $flags) {
// When no timezone is provided in the parsed date, YAML spec says we must assume UTC.
return new \DateTime($scalar, new \DateTimeZone('UTC'));
}
$timeZone = date_default_timezone_get();
date_default_timezone_set('UTC');
$time = strtotime($scalar);
date_default_timezone_set($timeZone);
return $time;
}
}
return (string) $scalar;
}
/**
* @param string $value
* @param int &$i
* @param int $flags
*
* @return string|null
*/
private static function parseTag($value, &$i, $flags)
{
if ('!' !== $value[$i]) {
return;
}
$tagLength = strcspn($value, " \t\n", $i + 1);
$tag = substr($value, $i + 1, $tagLength);
$nextOffset = $i + $tagLength + 1;
$nextOffset += strspn($value, ' ', $nextOffset);
// Is followed by a scalar
if ((!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && 'tagged' !== $tag) {
// Manage non-whitelisted scalars in {@link self::evaluateScalar()}
return;
}
// Built-in tags
if ($tag && '!' === $tag[0]) {
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
}
if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
$i = $nextOffset;
return $tag;
}
throw new ParseException(sprintf('Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
}
/**
* @param string $scalar
*
* @return string
*
* @internal
*/
public static function evaluateBinaryScalar($scalar)
{
$parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar));
if (0 !== (\strlen($parsedBinaryData) % 4)) {
throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', \strlen($parsedBinaryData)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) {
throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
}
return base64_decode($parsedBinaryData, true);
}
private static function isBinaryString($value)
{
return !preg_match('//u', $value) || preg_match('/[^\x00\x07-\x0d\x1B\x20-\xff]/', $value);
}
/**
* Gets a regex that matches a YAML date.
*
* @return string The regular expression
*
* @see http://www.yaml.org/spec/1.2/spec.html#id2761573
*/
private static function getTimestampRegex()
{
return <<<EOF
~^
(?P<year>[0-9][0-9][0-9][0-9])
-(?P<month>[0-9][0-9]?)
-(?P<day>[0-9][0-9]?)
(?:(?:[Tt]|[ \t]+)
(?P<hour>[0-9][0-9]?)
:(?P<minute>[0-9][0-9])
:(?P<second>[0-9][0-9])
(?:\.(?P<fraction>[0-9]*))?
(?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
(?::(?P<tz_minute>[0-9][0-9]))?))?)?
$~x
EOF;
}
/**
* Gets a regex that matches a YAML number in hexadecimal notation.
*
* @return string
*/
private static function getHexRegex()
{
return '~^0x[0-9a-f_]++$~i';
}
private static function getDeprecationMessage($message)
{
$message = rtrim($message, '.');
if (null !== self::$parsedFilename) {
$message .= ' in '.self::$parsedFilename;
}
if (-1 !== self::$parsedLineNumber) {
$message .= ' on line '.(self::$parsedLineNumber + 1);
}
return $message.'.';
}
}

19
one-api/vendor/symfony/yaml/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1138
one-api/vendor/symfony/yaml/Parser.php vendored Normal file

File diff suppressed because it is too large Load Diff

13
one-api/vendor/symfony/yaml/README.md vendored Normal file
View File

@@ -0,0 +1,13 @@
Yaml Component
==============
The Yaml component loads and dumps YAML files.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/yaml/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,48 @@
<?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\Yaml\Tag;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @author Guilhem N. <egetick@gmail.com>
*/
final class TaggedValue
{
private $tag;
private $value;
/**
* @param string $tag
* @param mixed $value
*/
public function __construct($tag, $value)
{
$this->tag = $tag;
$this->value = $value;
}
/**
* @return string
*/
public function getTag()
{
return $this->tag;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
}

View File

@@ -0,0 +1,139 @@
<?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\Yaml\Tests\Command;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Yaml\Command\LintCommand;
/**
* Tests the YamlLintCommand.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LintCommandTest extends TestCase
{
private $files;
public function testLintCorrectFile()
{
$tester = $this->createCommandTester();
$filename = $this->createFile('foo: bar');
$ret = $tester->execute(['filename' => $filename], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]);
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertRegExp('/^\/\/ OK in /', trim($tester->getDisplay()));
}
public function testLintIncorrectFile()
{
$incorrectContent = '
foo:
bar';
$tester = $this->createCommandTester();
$filename = $this->createFile($incorrectContent);
$ret = $tester->execute(['filename' => $filename], ['decorated' => false]);
$this->assertEquals(1, $ret, 'Returns 1 in case of error');
$this->assertContains('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay()));
}
public function testConstantAsKey()
{
$yaml = <<<YAML
!php/const 'Symfony\Component\Yaml\Tests\Command\Foo::TEST': bar
YAML;
$ret = $this->createCommandTester()->execute(['filename' => $this->createFile($yaml)], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]);
$this->assertSame(0, $ret, 'lint:yaml exits with code 0 in case of success');
}
public function testCustomTags()
{
$yaml = <<<YAML
foo: !my_tag {foo: bar}
YAML;
$ret = $this->createCommandTester()->execute(['filename' => $this->createFile($yaml), '--parse-tags' => true], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]);
$this->assertSame(0, $ret, 'lint:yaml exits with code 0 in case of success');
}
public function testCustomTagsError()
{
$yaml = <<<YAML
foo: !my_tag {foo: bar}
YAML;
$ret = $this->createCommandTester()->execute(['filename' => $this->createFile($yaml)], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]);
$this->assertSame(1, $ret, 'lint:yaml exits with code 1 in case of error');
}
/**
* @expectedException \RuntimeException
*/
public function testLintFileNotReadable()
{
$tester = $this->createCommandTester();
$filename = $this->createFile('');
unlink($filename);
$ret = $tester->execute(['filename' => $filename], ['decorated' => false]);
}
/**
* @return string Path to the new file
*/
private function createFile($content)
{
$filename = tempnam(sys_get_temp_dir().'/framework-yml-lint-test', 'sf-');
file_put_contents($filename, $content);
$this->files[] = $filename;
return $filename;
}
/**
* @return CommandTester
*/
protected function createCommandTester()
{
$application = new Application();
$application->add(new LintCommand());
$command = $application->find('lint:yaml');
return new CommandTester($command);
}
protected function setUp()
{
$this->files = [];
@mkdir(sys_get_temp_dir().'/framework-yml-lint-test');
}
protected function tearDown()
{
foreach ($this->files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
rmdir(sys_get_temp_dir().'/framework-yml-lint-test');
}
}
class Foo
{
const TEST = 'foo';
}

View File

@@ -0,0 +1,495 @@
<?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\Yaml\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
class DumperTest extends TestCase
{
protected $parser;
protected $dumper;
protected $path;
protected $array = [
'' => 'bar',
'foo' => '#bar',
'foo\'bar' => [],
'bar' => [1, 'foo'],
'foobar' => [
'foo' => 'bar',
'bar' => [1, 'foo'],
'foobar' => [
'foo' => 'bar',
'bar' => [1, 'foo'],
],
],
];
protected function setUp()
{
$this->parser = new Parser();
$this->dumper = new Dumper();
$this->path = __DIR__.'/Fixtures';
}
protected function tearDown()
{
$this->parser = null;
$this->dumper = null;
$this->path = null;
$this->array = null;
}
public function testIndentationInConstructor()
{
$dumper = new Dumper(7);
$expected = <<<'EOF'
'': bar
foo: '#bar'
'foo''bar': { }
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
EOF;
$this->assertEquals($expected, $dumper->dump($this->array, 4, 0));
}
/**
* @group legacy
*/
public function testSetIndentation()
{
$this->dumper->setIndentation(7);
$expected = <<<'EOF'
'': bar
foo: '#bar'
'foo''bar': { }
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
EOF;
$this->assertEquals($expected, $this->dumper->dump($this->array, 4, 0));
}
public function testSpecifications()
{
$files = $this->parser->parse(file_get_contents($this->path.'/index.yml'));
foreach ($files as $file) {
$yamls = file_get_contents($this->path.'/'.$file.'.yml');
// split YAMLs documents
foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) {
if (!$yaml) {
continue;
}
$test = $this->parser->parse($yaml);
if (isset($test['dump_skip']) && $test['dump_skip']) {
continue;
} elseif (isset($test['todo']) && $test['todo']) {
// TODO
} else {
eval('$expected = '.trim($test['php']).';');
$this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']);
}
}
}
}
public function testInlineLevel()
{
$expected = <<<'EOF'
{ '': bar, foo: '#bar', 'foo''bar': { }, bar: [1, foo], foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } }
EOF;
$this->assertEquals($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument');
$this->assertEquals($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument');
$expected = <<<'EOF'
'': bar
foo: '#bar'
'foo''bar': { }
bar: [1, foo]
foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } }
EOF;
$this->assertEquals($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument');
$expected = <<<'EOF'
'': bar
foo: '#bar'
'foo''bar': { }
bar:
- 1
- foo
foobar:
foo: bar
bar: [1, foo]
foobar: { foo: bar, bar: [1, foo] }
EOF;
$this->assertEquals($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument');
$expected = <<<'EOF'
'': bar
foo: '#bar'
'foo''bar': { }
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
foobar:
foo: bar
bar: [1, foo]
EOF;
$this->assertEquals($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument');
$expected = <<<'EOF'
'': bar
foo: '#bar'
'foo''bar': { }
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
foobar:
foo: bar
bar:
- 1
- foo
EOF;
$this->assertEquals($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument');
$this->assertEquals($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument');
}
public function testObjectSupportEnabled()
{
$dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_OBJECT);
$this->assertEquals('{ foo: !php/object \'O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}\', bar: 1 }', $dump, '->dump() is able to dump objects');
}
/**
* @group legacy
*/
public function testObjectSupportEnabledPassingTrue()
{
$dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, false, true);
$this->assertEquals('{ foo: !php/object \'O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}\', bar: 1 }', $dump, '->dump() is able to dump objects');
}
public function testObjectSupportDisabledButNoExceptions()
{
$dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1]);
$this->assertEquals('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled');
}
/**
* @expectedException \Symfony\Component\Yaml\Exception\DumpException
*/
public function testObjectSupportDisabledWithExceptions()
{
$this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE);
}
/**
* @group legacy
* @expectedException \Symfony\Component\Yaml\Exception\DumpException
*/
public function testObjectSupportDisabledWithExceptionsPassingTrue()
{
$this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, true);
}
public function testEmptyArray()
{
$dump = $this->dumper->dump([]);
$this->assertEquals('{ }', $dump);
$dump = $this->dumper->dump([], 0, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
$this->assertEquals('[]', $dump);
$dump = $this->dumper->dump([], 9, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
$this->assertEquals('[]', $dump);
$dump = $this->dumper->dump(new \ArrayObject(), 0, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
$this->assertEquals('{ }', $dump);
$dump = $this->dumper->dump(new \stdClass(), 0, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
$this->assertEquals('{ }', $dump);
}
/**
* @dataProvider getEscapeSequences
*/
public function testEscapedEscapeSequencesInQuotedScalar($input, $expected)
{
$this->assertEquals($expected, $this->dumper->dump($input));
}
public function getEscapeSequences()
{
return [
'empty string' => ['', "''"],
'null' => ["\x0", '"\\0"'],
'bell' => ["\x7", '"\\a"'],
'backspace' => ["\x8", '"\\b"'],
'horizontal-tab' => ["\t", '"\\t"'],
'line-feed' => ["\n", '"\\n"'],
'vertical-tab' => ["\v", '"\\v"'],
'form-feed' => ["\xC", '"\\f"'],
'carriage-return' => ["\r", '"\\r"'],
'escape' => ["\x1B", '"\\e"'],
'space' => [' ', "' '"],
'double-quote' => ['"', "'\"'"],
'slash' => ['/', '/'],
'backslash' => ['\\', '\\'],
'next-line' => ["\xC2\x85", '"\\N"'],
'non-breaking-space' => ["\xc2\xa0", '"\\_"'],
'line-separator' => ["\xE2\x80\xA8", '"\\L"'],
'paragraph-separator' => ["\xE2\x80\xA9", '"\\P"'],
'colon' => [':', "':'"],
];
}
public function testBinaryDataIsDumpedBase64Encoded()
{
$binaryData = file_get_contents(__DIR__.'/Fixtures/arrow.gif');
$expected = '{ data: !!binary '.base64_encode($binaryData).' }';
$this->assertSame($expected, $this->dumper->dump(['data' => $binaryData]));
}
public function testNonUtf8DataIsDumpedBase64Encoded()
{
// "für" (ISO-8859-1 encoded)
$this->assertSame('!!binary ZsM/cg==', $this->dumper->dump("f\xc3\x3fr"));
}
/**
* @dataProvider objectAsMapProvider
*/
public function testDumpObjectAsMap($object, $expected)
{
$yaml = $this->dumper->dump($object, 0, 0, Yaml::DUMP_OBJECT_AS_MAP);
$this->assertEquals($expected, Yaml::parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP));
}
public function objectAsMapProvider()
{
$tests = [];
$bar = new \stdClass();
$bar->class = 'classBar';
$bar->args = ['bar'];
$zar = new \stdClass();
$foo = new \stdClass();
$foo->bar = $bar;
$foo->zar = $zar;
$object = new \stdClass();
$object->foo = $foo;
$tests['stdClass'] = [$object, $object];
$arrayObject = new \ArrayObject();
$arrayObject['foo'] = 'bar';
$arrayObject['baz'] = 'foobar';
$parsedArrayObject = new \stdClass();
$parsedArrayObject->foo = 'bar';
$parsedArrayObject->baz = 'foobar';
$tests['ArrayObject'] = [$arrayObject, $parsedArrayObject];
$a = new A();
$tests['arbitrary-object'] = [$a, null];
return $tests;
}
public function testDumpingArrayObjectInstancesRespectsInlineLevel()
{
$deep = new \ArrayObject(['deep1' => 'd', 'deep2' => 'e']);
$inner = new \ArrayObject(['inner1' => 'b', 'inner2' => 'c', 'inner3' => $deep]);
$outer = new \ArrayObject(['outer1' => 'a', 'outer2' => $inner]);
$yaml = $this->dumper->dump($outer, 2, 0, Yaml::DUMP_OBJECT_AS_MAP);
$expected = <<<YAML
outer1: a
outer2:
inner1: b
inner2: c
inner3: { deep1: d, deep2: e }
YAML;
$this->assertSame($expected, $yaml);
}
public function testDumpingArrayObjectInstancesWithNumericKeysInlined()
{
$deep = new \ArrayObject(['d', 'e']);
$inner = new \ArrayObject(['b', 'c', $deep]);
$outer = new \ArrayObject(['a', $inner]);
$yaml = $this->dumper->dump($outer, 0, 0, Yaml::DUMP_OBJECT_AS_MAP);
$expected = <<<YAML
{ 0: a, 1: { 0: b, 1: c, 2: { 0: d, 1: e } } }
YAML;
$this->assertSame($expected, $yaml);
}
public function testDumpingArrayObjectInstancesWithNumericKeysRespectsInlineLevel()
{
$deep = new \ArrayObject(['d', 'e']);
$inner = new \ArrayObject(['b', 'c', $deep]);
$outer = new \ArrayObject(['a', $inner]);
$yaml = $this->dumper->dump($outer, 2, 0, Yaml::DUMP_OBJECT_AS_MAP);
$expected = <<<YAML
0: a
1:
0: b
1: c
2: { 0: d, 1: e }
YAML;
$this->assertEquals($expected, $yaml);
}
public function testDumpEmptyArrayObjectInstanceAsMap()
{
$this->assertSame('{ }', $this->dumper->dump(new \ArrayObject(), 2, 0, Yaml::DUMP_OBJECT_AS_MAP));
}
public function testDumpEmptyStdClassInstanceAsMap()
{
$this->assertSame('{ }', $this->dumper->dump(new \stdClass(), 2, 0, Yaml::DUMP_OBJECT_AS_MAP));
}
public function testDumpingStdClassInstancesRespectsInlineLevel()
{
$deep = new \stdClass();
$deep->deep1 = 'd';
$deep->deep2 = 'e';
$inner = new \stdClass();
$inner->inner1 = 'b';
$inner->inner2 = 'c';
$inner->inner3 = $deep;
$outer = new \stdClass();
$outer->outer1 = 'a';
$outer->outer2 = $inner;
$yaml = $this->dumper->dump($outer, 2, 0, Yaml::DUMP_OBJECT_AS_MAP);
$expected = <<<YAML
outer1: a
outer2:
inner1: b
inner2: c
inner3: { deep1: d, deep2: e }
YAML;
$this->assertSame($expected, $yaml);
}
public function testDumpMultiLineStringAsScalarBlock()
{
$data = [
'data' => [
'single_line' => 'foo bar baz',
'multi_line' => "foo\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz",
'multi_line_with_carriage_return' => "foo\nbar\r\nbaz",
'nested_inlined_multi_line_string' => [
'inlined_multi_line' => "foo\nbar\r\nempty line:\n\nbaz",
],
],
];
$this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
}
public function testDumpMultiLineStringAsScalarBlockWhenFirstLineHasLeadingSpace()
{
$data = [
'data' => [
'multi_line' => " the first line has leading spaces\nThe second line does not.",
],
];
$this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block_leading_space_in_first_line.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
}
public function testCarriageReturnIsMaintainedWhenDumpingAsMultiLineLiteralBlock()
{
$this->assertSame("- \"a\\r\\nb\\nc\"\n", $this->dumper->dump(["a\r\nb\nc"], 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The indentation must be greater than zero
*/
public function testZeroIndentationThrowsException()
{
new Dumper(0);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The indentation must be greater than zero
*/
public function testNegativeIndentationThrowsException()
{
new Dumper(-4);
}
}
class A
{
public $a = 'foo';
}

View File

@@ -0,0 +1,31 @@
--- %YAML:1.0
test: Simple Alias Example
brief: >
If you need to refer to the same item of data twice,
you can give that item an alias. The alias is a plain
string, starting with an ampersand. The item may then
be referred to by the alias throughout your document
by using an asterisk before the name of the alias.
This is called an anchor.
yaml: |
- &showell Steve
- Clark
- Brian
- Oren
- *showell
php: |
['Steve', 'Clark', 'Brian', 'Oren', 'Steve']
---
test: Alias of a Mapping
brief: >
An alias can be used on any item of data, including
sequences, mappings, and other complex data types.
yaml: |
- &hello
Meat: pork
Starch: potato
- banana
- *hello
php: |
[['Meat'=>'pork', 'Starch'=>'potato'], 'banana', ['Meat'=>'pork', 'Starch'=>'potato']]

View File

@@ -0,0 +1,202 @@
--- %YAML:1.0
test: Simple Sequence
brief: |
You can specify a list in YAML by placing each
member of the list on a new line with an opening
dash. These lists are called sequences.
yaml: |
- apple
- banana
- carrot
php: |
['apple', 'banana', 'carrot']
---
test: Sequence With Item Being Null In The Middle
brief: |
You can specify a list in YAML by placing each
member of the list on a new line with an opening
dash. These lists are called sequences.
yaml: |
- apple
-
- carrot
php: |
['apple', null, 'carrot']
---
test: Sequence With Last Item Being Null
brief: |
You can specify a list in YAML by placing each
member of the list on a new line with an opening
dash. These lists are called sequences.
yaml: |
- apple
- banana
-
php: |
['apple', 'banana', null]
---
test: Nested Sequences
brief: |
You can include a sequence within another
sequence by giving the sequence an empty
dash, followed by an indented list.
yaml: |
-
- foo
- bar
- baz
php: |
[['foo', 'bar', 'baz']]
---
test: Mixed Sequences
brief: |
Sequences can contain any YAML data,
including strings and other sequences.
yaml: |
- apple
-
- foo
- bar
- x123
- banana
- carrot
php: |
['apple', ['foo', 'bar', 'x123'], 'banana', 'carrot']
---
test: Deeply Nested Sequences
brief: |
Sequences can be nested even deeper, with each
level of indentation representing a level of
depth.
yaml: |
-
-
- uno
- dos
php: |
[[['uno', 'dos']]]
---
test: Simple Mapping
brief: |
You can add a keyed list (also known as a dictionary or
hash) to your document by placing each member of the
list on a new line, with a colon separating the key
from its value. In YAML, this type of list is called
a mapping.
yaml: |
foo: whatever
bar: stuff
php: |
['foo' => 'whatever', 'bar' => 'stuff']
---
test: Sequence in a Mapping
brief: |
A value in a mapping can be a sequence.
yaml: |
foo: whatever
bar:
- uno
- dos
php: |
['foo' => 'whatever', 'bar' => ['uno', 'dos']]
---
test: Nested Mappings
brief: |
A value in a mapping can be another mapping.
yaml: |
foo: whatever
bar:
fruit: apple
name: steve
sport: baseball
php: |
[
'foo' => 'whatever',
'bar' => [
'fruit' => 'apple',
'name' => 'steve',
'sport' => 'baseball'
]
]
---
test: Mixed Mapping
brief: |
A mapping can contain any assortment
of mappings and sequences as values.
yaml: |
foo: whatever
bar:
-
fruit: apple
name: steve
sport: baseball
- more
-
python: rocks
perl: papers
ruby: scissorses
php: |
[
'foo' => 'whatever',
'bar' => [
[
'fruit' => 'apple',
'name' => 'steve',
'sport' => 'baseball'
],
'more',
[
'python' => 'rocks',
'perl' => 'papers',
'ruby' => 'scissorses'
]
]
]
---
test: Mapping-in-Sequence Shortcut
todo: true
brief: |
If you are adding a mapping to a sequence, you
can place the mapping on the same line as the
dash as a shortcut.
yaml: |
- work on YAML.py:
- work on Store
php: |
[['work on YAML.py' => ['work on Store']]]
---
test: Sequence-in-Mapping Shortcut
todo: true
brief: |
The dash in a sequence counts as indentation, so
you can add a sequence inside of a mapping without
needing spaces as indentation.
yaml: |
allow:
- 'localhost'
- '%.sourceforge.net'
- '%.freepan.org'
php: |
['allow' => ['localhost', '%.sourceforge.net', '%.freepan.org']]
---
todo: true
test: Merge key
brief: |
A merge key ('<<') can be used in a mapping to insert other mappings. If
the value associated with the merge key is a mapping, each of its key/value
pairs is inserted into the current mapping.
yaml: |
mapping:
name: Joe
job: Accountant
<<:
age: 38
php: |
[
'mapping' =>
[
'name' => 'Joe',
'job' => 'Accountant',
'age' => 38
]
]

View File

@@ -0,0 +1,51 @@
---
test: One Element Mapping
brief: |
A mapping with one key/value pair
yaml: |
foo: bar
php: |
['foo' => 'bar']
---
test: Multi Element Mapping
brief: |
More than one key/value pair
yaml: |
red: baron
white: walls
blue: berries
php: |
[
'red' => 'baron',
'white' => 'walls',
'blue' => 'berries',
]
---
test: Values aligned
brief: |
Often times human editors of documents will align the values even
though YAML emitters generally don't.
yaml: |
red: baron
white: walls
blue: berries
php: |
[
'red' => 'baron',
'white' => 'walls',
'blue' => 'berries',
]
---
test: Colons aligned
brief: |
Spaces can come before the ': ' key/value separator.
yaml: |
red : baron
white : walls
blue : berries
php: |
[
'red' => 'baron',
'white' => 'walls',
'blue' => 'berries',
]

View File

@@ -0,0 +1,85 @@
--- %YAML:1.0
test: Trailing Document Separator
todo: true
brief: >
You can separate YAML documents
with a string of three dashes.
yaml: |
- foo: 1
bar: 2
---
more: stuff
python: |
[
[ { 'foo': 1, 'bar': 2 } ],
{ 'more': 'stuff' }
]
ruby: |
[ { 'foo' => 1, 'bar' => 2 } ]
---
test: Leading Document Separator
todo: true
brief: >
You can explicitly give an opening
document separator to your YAML stream.
yaml: |
---
- foo: 1
bar: 2
---
more: stuff
python: |
[
[ {'foo': 1, 'bar': 2}],
{'more': 'stuff'}
]
ruby: |
[ { 'foo' => 1, 'bar' => 2 } ]
---
test: YAML Header
todo: true
brief: >
The opening separator can contain directives
to the YAML parser, such as the version
number.
yaml: |
--- %YAML:1.0
foo: 1
bar: 2
php: |
['foo' => 1, 'bar' => 2]
documents: 1
---
test: Red Herring Document Separator
brief: >
Separators included in blocks or strings
are treated as blocks or strings, as the
document separator should have no indentation
preceding it.
yaml: |
foo: |
---
php: |
['foo' => "---\n"]
---
test: Multiple Document Separators in Block
brief: >
This technique allows you to embed other YAML
documents within literal blocks.
yaml: |
foo: |
---
foo: bar
---
yo: baz
bar: |
fooness
php: |
[
'foo' => "---\nfoo: bar\n---\nyo: baz\n",
'bar' => "fooness\n"
]

View File

@@ -0,0 +1,25 @@
---
test: Missing value for hash item
todo: true
brief: |
Third item in this hash doesn't have a value
yaml: |
okay: value
also okay: ~
causes error because no value specified
last key: value okay here too
python-error: causes error because no value specified
---
test: Not indenting enough
brief: |
There was a bug in PyYaml where it was off by one
in the indentation check. It was allowing the YAML
below.
# This is actually valid YAML now. Someone should tell showell.
yaml: |
foo:
firstline: 1
secondline: 2
php: |
['foo' => null, 'firstline' => 1, 'secondline' => 2]

View File

@@ -0,0 +1,60 @@
---
test: Simple Inline Array
brief: >
Sequences can be contained on a
single line, using the inline syntax.
Separate each entry with commas and
enclose in square brackets.
yaml: |
seq: [ a, b, c ]
php: |
['seq' => ['a', 'b', 'c']]
---
test: Simple Inline Hash
brief: >
Mapping can also be contained on
a single line, using the inline
syntax. Each key-value pair is
separated by a colon, with a comma
between each entry in the mapping.
Enclose with curly braces.
yaml: |
hash: { name: Steve, foo: bar }
php: |
['hash' => ['name' => 'Steve', 'foo' => 'bar']]
---
test: Multi-line Inline Collections
todo: true
brief: >
Both inline sequences and inline mappings
can span multiple lines, provided that you
indent the additional lines.
yaml: |
languages: [ Ruby,
Perl,
Python ]
websites: { YAML: yaml.org,
Ruby: ruby-lang.org,
Python: python.org,
Perl: use.perl.org }
php: |
[
'languages' => ['Ruby', 'Perl', 'Python'],
'websites' => [
'YAML' => 'yaml.org',
'Ruby' => 'ruby-lang.org',
'Python' => 'python.org',
'Perl' => 'use.perl.org'
]
]
---
test: Commas in Values (not in the spec!)
todo: true
brief: >
List items in collections are delimited by commas, but
there must be a space after each comma. This allows you
to add numbers without quoting.
yaml: |
attendances: [ 45,123, 70,000, 17,222 ]
php: |
['attendances' => [45123, 70000, 17222]]

View File

@@ -0,0 +1,176 @@
--- %YAML:1.0
test: Single ending newline
brief: >
A pipe character, followed by an indented
block of text is treated as a literal
block, in which newlines are preserved
throughout the block, including the final
newline.
yaml: |
---
this: |
Foo
Bar
php: |
['this' => "Foo\nBar\n"]
---
test: The '+' indicator
brief: >
The '+' indicator says to keep newlines at the end of text
blocks.
yaml: |
normal: |
extra new lines not kept
preserving: |+
extra new lines are kept
dummy: value
php: |
[
'normal' => "extra new lines not kept\n",
'preserving' => "extra new lines are kept\n\n\n",
'dummy' => 'value'
]
---
test: Three trailing newlines in literals
brief: >
To give you more control over how space
is preserved in text blocks, YAML has
the keep '+' and chomp '-' indicators.
The keep indicator will preserve all
ending newlines, while the chomp indicator
will strip all ending newlines.
yaml: |
clipped: |
This has one newline.
same as "clipped" above: "This has one newline.\n"
stripped: |-
This has no newline.
same as "stripped" above: "This has no newline."
kept: |+
This has four newlines.
same as "kept" above: "This has four newlines.\n\n\n\n"
php: |
[
'clipped' => "This has one newline.\n",
'same as "clipped" above' => "This has one newline.\n",
'stripped' => 'This has no newline.',
'same as "stripped" above' => 'This has no newline.',
'kept' => "This has four newlines.\n\n\n\n",
'same as "kept" above' => "This has four newlines.\n\n\n\n"
]
---
test: Extra trailing newlines with spaces
todo: true
brief: >
Normally, only a single newline is kept
from the end of a literal block, unless the
keep '+' character is used in combination
with the pipe. The following example
will preserve all ending whitespace
since the last line of both literal blocks
contains spaces which extend past the indentation
level.
yaml: |
---
this: |
Foo
kept: |+
Foo
php: |
['this' => "Foo\n\n \n",
'kept' => "Foo\n\n \n"]
---
test: Folded Block in a Sequence
brief: >
A greater-then character, followed by an indented
block of text is treated as a folded block, in
which lines of text separated by a single newline
are concatenated as a single line.
yaml: |
---
- apple
- banana
- >
can't you see
the beauty of yaml?
hmm
- dog
php: |
[
'apple',
'banana',
"can't you see the beauty of yaml? hmm\n",
'dog'
]
---
test: Folded Block as a Mapping Value
brief: >
Both literal and folded blocks can be
used in collections, as values in a
sequence or a mapping.
yaml: |
---
quote: >
Mark McGwire's
year was crippled
by a knee injury.
source: espn
php: |
[
'quote' => "Mark McGwire's year was crippled by a knee injury.\n",
'source' => 'espn'
]
---
test: Three trailing newlines in folded blocks
brief: >
The keep and chomp indicators can also
be applied to folded blocks.
yaml: |
clipped: >
This has one newline.
same as "clipped" above: "This has one newline.\n"
stripped: >-
This has no newline.
same as "stripped" above: "This has no newline."
kept: >+
This has four newlines.
same as "kept" above: "This has four newlines.\n\n\n\n"
php: |
[
'clipped' => "This has one newline.\n",
'same as "clipped" above' => "This has one newline.\n",
'stripped' => 'This has no newline.',
'same as "stripped" above' => 'This has no newline.',
'kept' => "This has four newlines.\n\n\n\n",
'same as "kept" above' => "This has four newlines.\n\n\n\n"
]

View File

@@ -0,0 +1,45 @@
--- %YAML:1.0
test: Empty Sequence
brief: >
You can represent the empty sequence
with an empty inline sequence.
yaml: |
empty: []
php: |
['empty' => []]
---
test: Empty Mapping
brief: >
You can represent the empty mapping
with an empty inline mapping.
yaml: |
empty: {}
php: |
['empty' => []]
---
test: Empty Sequence as Entire Document
yaml: |
[]
php: |
[]
---
test: Empty Mapping as Entire Document
yaml: |
{}
php: |
[]
---
test: Null as Document
yaml: |
~
php: |
null
---
test: Empty String
brief: >
You can represent an empty string
with a pair of quotes.
yaml: |
''
php: |
''

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
--- %YAML:1.0
test: Strings
brief: >
Any group of characters beginning with an
alphabetic or numeric character is a string,
unless it belongs to one of the groups below
(such as an Integer or Time).
yaml: |
String
php: |
'String'
---
test: String characters
brief: >
A string can contain any alphabetic or
numeric character, along with many
punctuation characters, including the
period, dash, space, quotes, exclamation, and
question mark.
yaml: |
- What's Yaml?
- It's for writing data structures in plain text.
- And?
- And what? That's not good enough for you?
- No, I mean, "And what about Yaml?"
- Oh, oh yeah. Uh.. Yaml for Ruby.
php: |
[
"What's Yaml?",
"It's for writing data structures in plain text.",
"And?",
"And what? That's not good enough for you?",
"No, I mean, \"And what about Yaml?\"",
"Oh, oh yeah. Uh.. Yaml for Ruby."
]
---
test: Indicators in Strings
brief: >
Be careful using indicators in strings. In particular,
the comma, colon, and pound sign must be used carefully.
yaml: |
the colon followed by space is an indicator: but is a string:right here
same for the pound sign: here we have it#in a string
the comma can, honestly, be used in most cases: [ but not in, inline collections ]
php: |
[
'the colon followed by space is an indicator' => 'but is a string:right here',
'same for the pound sign' => 'here we have it#in a string',
'the comma can, honestly, be used in most cases' => ['but not in', 'inline collections']
]
---
test: Forcing Strings
brief: >
Any YAML type can be forced into a string using the
explicit !!str method.
yaml: |
date string: !!str 2001-08-01
number string: !!str 192
php: |
[
'date string' => '2001-08-01',
'number string' => '192'
]
---
test: Single-quoted Strings
brief: >
You can also enclose your strings within single quotes,
which allows use of slashes, colons, and other indicators
freely. Inside single quotes, you can represent a single
quote in your string by using two single quotes next to
each other.
yaml: |
all my favorite symbols: '#:!/%.)'
a few i hate: '&(*'
why do i hate them?: 'it''s very hard to explain'
entities: '&pound; me'
php: |
[
'all my favorite symbols' => '#:!/%.)',
'a few i hate' => '&(*',
'why do i hate them?' => 'it\'s very hard to explain',
'entities' => '&pound; me'
]
---
test: Double-quoted Strings
brief: >
Enclosing strings in double quotes allows you
to use escapings to represent ASCII and
Unicode characters.
yaml: |
i know where i want my line breaks: "one here\nand another here\n"
php: |
[
'i know where i want my line breaks' => "one here\nand another here\n"
]
---
test: Multi-line Quoted Strings
todo: true
brief: >
Both single- and double-quoted strings may be
carried on to new lines in your YAML document.
They must be indented a step and indentation
is interpreted as a single space.
yaml: |
i want a long string: "so i'm going to
let it go on and on to other lines
until i end it with a quote."
php: |
['i want a long string' => "so i'm going to ".
"let it go on and on to other lines ".
"until i end it with a quote."
]
---
test: Plain scalars
todo: true
brief: >
Unquoted strings may also span multiple lines, if they
are free of YAML space indicators and indented.
yaml: |
- My little toe is broken in two places;
- I'm crazy to have skied this way;
- I'm not the craziest he's seen, since there was always the German guy
who skied for 3 hours on a broken shin bone (just below the kneecap);
- Nevertheless, second place is respectable, and he doesn't
recommend going for the record;
- He's going to put my foot in plaster for a month;
- This would impair my skiing ability somewhat for the
duration, as can be imagined.
php: |
[
"My little toe is broken in two places;",
"I'm crazy to have skied this way;",
"I'm not the craziest he's seen, since there was always ".
"the German guy who skied for 3 hours on a broken shin ".
"bone (just below the kneecap);",
"Nevertheless, second place is respectable, and he doesn't ".
"recommend going for the record;",
"He's going to put my foot in plaster for a month;",
"This would impair my skiing ability somewhat for the duration, ".
"as can be imagined."
]
---
test: 'Null'
brief: >
You can use the tilde '~' character for a null value.
yaml: |
name: Mr. Show
hosted by: Bob and David
date of next season: ~
php: |
[
'name' => 'Mr. Show',
'hosted by' => 'Bob and David',
'date of next season' => null
]
---
test: Boolean
brief: >
You can use 'true' and 'false' for Boolean values.
yaml: |
Is Gus a Liar?: true
Do I rely on Gus for Sustenance?: false
php: |
[
'Is Gus a Liar?' => true,
'Do I rely on Gus for Sustenance?' => false
]
---
test: Integers
dump_skip: true
brief: >
An integer is a series of numbers, optionally
starting with a positive or negative sign. Integers
may also contain commas for readability.
yaml: |
zero: 0
simple: 12
php: |
[
'zero' => 0,
'simple' => 12,
]
---
test: Positive Big Integer
deprecated: true
dump_skip: true
brief: >
An integer is a series of numbers, optionally
starting with a positive or negative sign. Integers
may also contain commas for readability.
yaml: |
one-thousand: 1,000
php: |
[
'one-thousand' => 1000.0,
]
---
test: Negative Big Integer
deprecated: true
dump_skip: true
brief: >
An integer is a series of numbers, optionally
starting with a positive or negative sign. Integers
may also contain commas for readability.
yaml: |
negative one-thousand: -1,000
php: |
[
'negative one-thousand' => -1000.0
]
---
test: Floats
dump_skip: true
brief: >
Floats are represented by numbers with decimals,
allowing for scientific notation, as well as
positive and negative infinity and "not a number."
yaml: |
a simple float: 2.00
scientific notation: 1.00009e+3
php: |
[
'a simple float' => 2.0,
'scientific notation' => 1000.09
]
---
test: Larger Float
dump_skip: true
deprecated: true
brief: >
Floats are represented by numbers with decimals,
allowing for scientific notation, as well as
positive and negative infinity and "not a number."
yaml: |
larger float: 1,000.09
php: |
[
'larger float' => 1000.09,
]
---
test: Time
todo: true
brief: >
You can represent timestamps by using
ISO8601 format, or a variation which
allows spaces between the date, time and
time zone.
yaml: |
iso8601: 2001-12-14t21:59:43.10-05:00
space separated: 2001-12-14 21:59:43.10 -05:00
php: |
[
'iso8601' => mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ),
'space separated' => mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" )
]
---
test: Date
todo: true
brief: >
A date can be represented by its year,
month and day in ISO8601 order.
yaml: |
1976-07-31
php: |
date( 1976, 7, 31 )

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

View File

@@ -0,0 +1,11 @@
--- %YAML:1.0
test: Miscellaneous
spec: 2.21
yaml: |
true: true
false: false
php: |
[
'true' => true,
'false' => false,
]

View File

@@ -0,0 +1 @@
value: <?php echo 1 + 2 + 3 ?>

Some files were not shown because too many files have changed in this diff Show More