This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

3
vendor/symfony/form/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,198 @@
<?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\Form;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractExtension implements FormExtensionInterface
{
/**
* The types provided by this extension.
*
* @var FormTypeInterface[] An array of FormTypeInterface
*/
private $types;
/**
* The type extensions provided by this extension.
*
* @var FormTypeExtensionInterface[] An array of FormTypeExtensionInterface
*/
private $typeExtensions;
/**
* The type guesser provided by this extension.
*
* @var FormTypeGuesserInterface
*/
private $typeGuesser;
/**
* Whether the type guesser has been loaded.
*
* @var bool
*/
private $typeGuesserLoaded = false;
/**
* {@inheritdoc}
*/
public function getType($name)
{
if (null === $this->types) {
$this->initTypes();
}
if (!isset($this->types[$name])) {
throw new InvalidArgumentException(sprintf('The type "%s" can not be loaded by this extension', $name));
}
return $this->types[$name];
}
/**
* {@inheritdoc}
*/
public function hasType($name)
{
if (null === $this->types) {
$this->initTypes();
}
return isset($this->types[$name]);
}
/**
* {@inheritdoc}
*/
public function getTypeExtensions($name)
{
if (null === $this->typeExtensions) {
$this->initTypeExtensions();
}
return isset($this->typeExtensions[$name])
? $this->typeExtensions[$name]
: array();
}
/**
* {@inheritdoc}
*/
public function hasTypeExtensions($name)
{
if (null === $this->typeExtensions) {
$this->initTypeExtensions();
}
return isset($this->typeExtensions[$name]) && count($this->typeExtensions[$name]) > 0;
}
/**
* {@inheritdoc}
*/
public function getTypeGuesser()
{
if (!$this->typeGuesserLoaded) {
$this->initTypeGuesser();
}
return $this->typeGuesser;
}
/**
* Registers the types.
*
* @return FormTypeInterface[] An array of FormTypeInterface instances
*/
protected function loadTypes()
{
return array();
}
/**
* Registers the type extensions.
*
* @return FormTypeExtensionInterface[] An array of FormTypeExtensionInterface instances
*/
protected function loadTypeExtensions()
{
return array();
}
/**
* Registers the type guesser.
*
* @return FormTypeGuesserInterface|null A type guesser
*/
protected function loadTypeGuesser()
{
}
/**
* Initializes the types.
*
* @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface
*/
private function initTypes()
{
$this->types = array();
foreach ($this->loadTypes() as $type) {
if (!$type instanceof FormTypeInterface) {
throw new UnexpectedTypeException($type, 'Symfony\Component\Form\FormTypeInterface');
}
$this->types[get_class($type)] = $type;
}
}
/**
* Initializes the type extensions.
*
* @throws UnexpectedTypeException if any registered type extension is not
* an instance of FormTypeExtensionInterface
*/
private function initTypeExtensions()
{
$this->typeExtensions = array();
foreach ($this->loadTypeExtensions() as $extension) {
if (!$extension instanceof FormTypeExtensionInterface) {
throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface');
}
$type = $extension->getExtendedType();
$this->typeExtensions[$type][] = $extension;
}
}
/**
* Initializes the type guesser.
*
* @throws UnexpectedTypeException if the type guesser is not an instance of FormTypeGuesserInterface
*/
private function initTypeGuesser()
{
$this->typeGuesserLoaded = true;
$this->typeGuesser = $this->loadTypeGuesser();
if (null !== $this->typeGuesser && !$this->typeGuesser instanceof FormTypeGuesserInterface) {
throw new UnexpectedTypeException($this->typeGuesser, 'Symfony\Component\Form\FormTypeGuesserInterface');
}
}
}

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\Form;
/**
* Default implementation of {@link FormRendererEngineInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractRendererEngine implements FormRendererEngineInterface
{
/**
* The variable in {@link FormView} used as cache key.
*/
const CACHE_KEY_VAR = 'cache_key';
/**
* @var array
*/
protected $defaultThemes;
/**
* @var array
*/
protected $themes = array();
/**
* @var array
*/
protected $resources = array();
/**
* @var array
*/
private $resourceHierarchyLevels = array();
/**
* Creates a new renderer engine.
*
* @param array $defaultThemes The default themes. The type of these
* themes is open to the implementation.
*/
public function __construct(array $defaultThemes = array())
{
$this->defaultThemes = $defaultThemes;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormView $view, $themes)
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
// Do not cast, as casting turns objects into arrays of properties
$this->themes[$cacheKey] = is_array($themes) ? $themes : array($themes);
// Unset instead of resetting to an empty array, in order to allow
// implementations (like TwigRendererEngine) to check whether $cacheKey
// is set at all.
unset($this->resources[$cacheKey], $this->resourceHierarchyLevels[$cacheKey]);
}
/**
* {@inheritdoc}
*/
public function getResourceForBlockName(FormView $view, $blockName)
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
if (!isset($this->resources[$cacheKey][$blockName])) {
$this->loadResourceForBlockName($cacheKey, $view, $blockName);
}
return $this->resources[$cacheKey][$blockName];
}
/**
* {@inheritdoc}
*/
public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, $hierarchyLevel)
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
$blockName = $blockNameHierarchy[$hierarchyLevel];
if (!isset($this->resources[$cacheKey][$blockName])) {
$this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
}
return $this->resources[$cacheKey][$blockName];
}
/**
* {@inheritdoc}
*/
public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, $hierarchyLevel)
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
$blockName = $blockNameHierarchy[$hierarchyLevel];
if (!isset($this->resources[$cacheKey][$blockName])) {
$this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
}
// If $block was previously rendered loaded with loadTemplateForBlock(), the template
// is cached but the hierarchy level is not. In this case, we know that the block
// exists at this very hierarchy level, so we can just set it.
if (!isset($this->resourceHierarchyLevels[$cacheKey][$blockName])) {
$this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
}
return $this->resourceHierarchyLevels[$cacheKey][$blockName];
}
/**
* Loads the cache with the resource for a given block name.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view
* @param FormView $view The form view for finding the applying themes
* @param string $blockName The name of the block to load
*
* @return bool True if the resource could be loaded, false otherwise
*/
abstract protected function loadResourceForBlockName($cacheKey, FormView $view, $blockName);
/**
* Loads the cache with the resource for a specific level of a block hierarchy.
*
* @see getResourceForBlockHierarchy()
*
* @param string $cacheKey The cache key used for storing the
* resource.
* @param FormView $view The form view for finding the applying
* themes.
* @param array $blockNameHierarchy The block hierarchy, with the most
* specific block name at the end.
* @param int $hierarchyLevel The level in the block hierarchy that
* should be loaded.
*
* @return bool True if the resource could be loaded, false otherwise
*/
private function loadResourceForBlockNameHierarchy($cacheKey, FormView $view, array $blockNameHierarchy, $hierarchyLevel)
{
$blockName = $blockNameHierarchy[$hierarchyLevel];
// Try to find a template for that block
if ($this->loadResourceForBlockName($cacheKey, $view, $blockName)) {
// If loadTemplateForBlock() returns true, it was able to populate the
// cache. The only missing thing is to set the hierarchy level at which
// the template was found.
$this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
return true;
}
if ($hierarchyLevel > 0) {
$parentLevel = $hierarchyLevel - 1;
$parentBlockName = $blockNameHierarchy[$parentLevel];
// The next two if statements contain slightly duplicated code. This is by intention
// and tries to avoid execution of unnecessary checks in order to increase performance.
if (isset($this->resources[$cacheKey][$parentBlockName])) {
// It may happen that the parent block is already loaded, but its level is not.
// In this case, the parent block must have been loaded by loadResourceForBlock(),
// which does not check the hierarchy of the block. Subsequently the block must have
// been found directly on the parent level.
if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlockName])) {
$this->resourceHierarchyLevels[$cacheKey][$parentBlockName] = $parentLevel;
}
// Cache the shortcuts for further accesses
$this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
$this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
return true;
}
if ($this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $parentLevel)) {
// Cache the shortcuts for further accesses
$this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
$this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
return true;
}
}
// Cache the result for further accesses
$this->resources[$cacheKey][$blockName] = false;
$this->resourceHierarchyLevels[$cacheKey][$blockName] = false;
return false;
}
}

65
vendor/symfony/form/AbstractType.php vendored Normal file
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\Form;
use Symfony\Component\Form\Util\StringUtil;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractType implements FormTypeInterface
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return StringUtil::fqcnToBlockPrefix(get_class($this));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

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\Form;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractTypeExtension implements FormTypeExtensionInterface
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
}
}

442
vendor/symfony/form/Button.php vendored Normal file
View File

@@ -0,0 +1,442 @@
<?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\Form;
use Symfony\Component\Form\Exception\AlreadySubmittedException;
use Symfony\Component\Form\Exception\BadMethodCallException;
/**
* A form button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class Button implements \IteratorAggregate, FormInterface
{
/**
* @var FormInterface|null
*/
private $parent;
/**
* @var FormConfigInterface
*/
private $config;
/**
* @var bool
*/
private $submitted = false;
/**
* Creates a new button from a form configuration.
*
* @param FormConfigInterface $config The button's configuration
*/
public function __construct(FormConfigInterface $config)
{
$this->config = $config;
}
/**
* Unsupported method.
*
* @param mixed $offset
*
* @return bool Always returns false
*/
public function offsetExists($offset)
{
return false;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $offset
*
* @throws BadMethodCallException
*/
public function offsetGet($offset)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $offset
* @param mixed $value
*
* @throws BadMethodCallException
*/
public function offsetSet($offset, $value)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $offset
*
* @throws BadMethodCallException
*/
public function offsetUnset($offset)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* {@inheritdoc}
*/
public function setParent(FormInterface $parent = null)
{
$this->parent = $parent;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return $this->parent;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param int|string|FormInterface $child
* @param null $type
* @param array $options
*
* @throws BadMethodCallException
*/
public function add($child, $type = null, array $options = array())
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $name
*
* @throws BadMethodCallException
*/
public function get($name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @param string $name
*
* @return bool Always returns false
*/
public function has($name)
{
return false;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $name
*
* @throws BadMethodCallException
*/
public function remove($name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* {@inheritdoc}
*/
public function all()
{
return array();
}
/**
* {@inheritdoc}
*/
public function getErrors($deep = false, $flatten = true)
{
return new FormErrorIterator($this, array());
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $modelData
*/
public function setData($modelData)
{
// called during initialization of the form tree
// noop
}
/**
* Unsupported method.
*/
public function getData()
{
}
/**
* Unsupported method.
*/
public function getNormData()
{
}
/**
* Unsupported method.
*/
public function getViewData()
{
}
/**
* Unsupported method.
*
* @return array Always returns an empty array
*/
public function getExtraData()
{
return array();
}
/**
* Returns the button's configuration.
*
* @return FormConfigInterface The configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Returns whether the button is submitted.
*
* @return bool true if the button was submitted
*/
public function isSubmitted()
{
return $this->submitted;
}
/**
* Returns the name by which the button is identified in forms.
*
* @return string The name of the button
*/
public function getName()
{
return $this->config->getName();
}
/**
* Unsupported method.
*/
public function getPropertyPath()
{
}
/**
* Unsupported method.
*
* @param FormError $error
*
* @throws BadMethodCallException
*/
public function addError(FormError $error)
{
throw new BadMethodCallException('Buttons cannot have errors.');
}
/**
* Unsupported method.
*
* @return bool Always returns true
*/
public function isValid()
{
return true;
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function isRequired()
{
return false;
}
/**
* {@inheritdoc}
*/
public function isDisabled()
{
if (null === $this->parent || !$this->parent->isDisabled()) {
return $this->config->getDisabled();
}
return true;
}
/**
* Unsupported method.
*
* @return bool Always returns true
*/
public function isEmpty()
{
return true;
}
/**
* Unsupported method.
*
* @return bool Always returns true
*/
public function isSynchronized()
{
return true;
}
/**
* Unsupported method.
*/
public function getTransformationFailure()
{
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function initialize()
{
throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.');
}
/**
* Unsupported method.
*
* @param mixed $request
*
* @throws BadMethodCallException
*/
public function handleRequest($request = null)
{
throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.');
}
/**
* Submits data to the button.
*
* @param null|string $submittedData The data
* @param bool $clearMissing Not used
*
* @return $this
*
* @throws Exception\AlreadySubmittedException If the button has already been submitted.
*/
public function submit($submittedData, $clearMissing = true)
{
if ($this->submitted) {
throw new AlreadySubmittedException('A form can only be submitted once');
}
$this->submitted = true;
return $this;
}
/**
* {@inheritdoc}
*/
public function getRoot()
{
return $this->parent ? $this->parent->getRoot() : $this;
}
/**
* {@inheritdoc}
*/
public function isRoot()
{
return null === $this->parent;
}
/**
* {@inheritdoc}
*/
public function createView(FormView $parent = null)
{
if (null === $parent && $this->parent) {
$parent = $this->parent->createView();
}
$type = $this->config->getType();
$options = $this->config->getOptions();
$view = $type->createView($this, $parent);
$type->buildView($view, $this, $options);
$type->finishView($view, $this, $options);
return $view;
}
/**
* Unsupported method.
*
* @return int Always returns 0
*/
public function count()
{
return 0;
}
/**
* Unsupported method.
*
* @return \EmptyIterator Always returns an empty iterator
*/
public function getIterator()
{
return new \EmptyIterator();
}
}

819
vendor/symfony/form/ButtonBuilder.php vendored Normal file
View File

@@ -0,0 +1,819 @@
<?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\Form;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\BadMethodCallException;
/**
* A builder for {@link Button} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
{
/**
* @var bool
*/
protected $locked = false;
/**
* @var bool
*/
private $disabled;
/**
* @var ResolvedFormTypeInterface
*/
private $type;
/**
* @var string
*/
private $name;
/**
* @var array
*/
private $attributes = array();
/**
* @var array
*/
private $options;
/**
* Creates a new button builder.
*
* @param string $name The name of the button
* @param array $options The button's options
*
* @throws InvalidArgumentException If the name is empty.
*/
public function __construct($name, array $options = array())
{
$name = (string) $name;
if ('' === $name) {
throw new InvalidArgumentException('Buttons cannot have empty names.');
}
$this->name = $name;
$this->options = $options;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string|int|FormBuilderInterface $child
* @param string|FormTypeInterface $type
* @param array $options
*
* @throws BadMethodCallException
*/
public function add($child, $type = null, array $options = array())
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $name
* @param string|FormTypeInterface $type
* @param array $options
*
* @throws BadMethodCallException
*/
public function create($name, $type = null, array $options = array())
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $name
*
* @throws BadMethodCallException
*/
public function get($name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $name
*
* @throws BadMethodCallException
*/
public function remove($name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @param string $name
*
* @return bool Always returns false
*/
public function has($name)
{
return false;
}
/**
* Returns the children.
*
* @return array Always returns an empty array
*/
public function all()
{
return array();
}
/**
* Creates the button.
*
* @return Button The button
*/
public function getForm()
{
return new Button($this->getFormConfig());
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $eventName
* @param callable $listener
* @param int $priority
*
* @throws BadMethodCallException
*/
public function addEventListener($eventName, $listener, $priority = 0)
{
throw new BadMethodCallException('Buttons do not support event listeners.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param EventSubscriberInterface $subscriber
*
* @throws BadMethodCallException
*/
public function addEventSubscriber(EventSubscriberInterface $subscriber)
{
throw new BadMethodCallException('Buttons do not support event subscribers.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param DataTransformerInterface $viewTransformer
* @param bool $forcePrepend
*
* @throws BadMethodCallException
*/
public function addViewTransformer(DataTransformerInterface $viewTransformer, $forcePrepend = false)
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @throws BadMethodCallException
*/
public function resetViewTransformers()
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param DataTransformerInterface $modelTransformer
* @param bool $forceAppend
*
* @throws BadMethodCallException
*/
public function addModelTransformer(DataTransformerInterface $modelTransformer, $forceAppend = false)
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @throws BadMethodCallException
*/
public function resetModelTransformers()
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* {@inheritdoc}
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function setAttributes(array $attributes)
{
$this->attributes = $attributes;
return $this;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param DataMapperInterface $dataMapper
*
* @throws BadMethodCallException
*/
public function setDataMapper(DataMapperInterface $dataMapper = null)
{
throw new BadMethodCallException('Buttons do not support data mappers.');
}
/**
* Set whether the button is disabled.
*
* @param bool $disabled Whether the button is disabled
*
* @return $this
*/
public function setDisabled($disabled)
{
$this->disabled = $disabled;
return $this;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $emptyData
*
* @throws BadMethodCallException
*/
public function setEmptyData($emptyData)
{
throw new BadMethodCallException('Buttons do not support empty data.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param bool $errorBubbling
*
* @throws BadMethodCallException
*/
public function setErrorBubbling($errorBubbling)
{
throw new BadMethodCallException('Buttons do not support error bubbling.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param bool $required
*
* @throws BadMethodCallException
*/
public function setRequired($required)
{
throw new BadMethodCallException('Buttons cannot be required.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param null $propertyPath
*
* @throws BadMethodCallException
*/
public function setPropertyPath($propertyPath)
{
throw new BadMethodCallException('Buttons do not support property paths.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param bool $mapped
*
* @throws BadMethodCallException
*/
public function setMapped($mapped)
{
throw new BadMethodCallException('Buttons do not support data mapping.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param bool $byReference
*
* @throws BadMethodCallException
*/
public function setByReference($byReference)
{
throw new BadMethodCallException('Buttons do not support data mapping.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param bool $compound
*
* @throws BadMethodCallException
*/
public function setCompound($compound)
{
throw new BadMethodCallException('Buttons cannot be compound.');
}
/**
* Sets the type of the button.
*
* @param ResolvedFormTypeInterface $type The type of the button
*
* @return $this
*/
public function setType(ResolvedFormTypeInterface $type)
{
$this->type = $type;
return $this;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $data
*
* @throws BadMethodCallException
*/
public function setData($data)
{
throw new BadMethodCallException('Buttons do not support data.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param bool $locked
*
* @throws BadMethodCallException
*/
public function setDataLocked($locked)
{
throw new BadMethodCallException('Buttons do not support data locking.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param FormFactoryInterface $formFactory
*
* @throws BadMethodCallException
*/
public function setFormFactory(FormFactoryInterface $formFactory)
{
throw new BadMethodCallException('Buttons do not support form factories.');
}
/**
* Unsupported method.
*
* @param string $action
*
* @throws BadMethodCallException
*/
public function setAction($action)
{
throw new BadMethodCallException('Buttons do not support actions.');
}
/**
* Unsupported method.
*
* @param string $method
*
* @throws BadMethodCallException
*/
public function setMethod($method)
{
throw new BadMethodCallException('Buttons do not support methods.');
}
/**
* Unsupported method.
*
* @param RequestHandlerInterface $requestHandler
*
* @throws BadMethodCallException
*/
public function setRequestHandler(RequestHandlerInterface $requestHandler)
{
throw new BadMethodCallException('Buttons do not support request handlers.');
}
/**
* Unsupported method.
*
* @param bool $initialize
*
* @return $this
*
* @throws BadMethodCallException
*/
public function setAutoInitialize($initialize)
{
if (true === $initialize) {
throw new BadMethodCallException('Buttons do not support automatic initialization.');
}
return $this;
}
/**
* Unsupported method.
*
* @param bool $inheritData
*
* @throws BadMethodCallException
*/
public function setInheritData($inheritData)
{
throw new BadMethodCallException('Buttons do not support data inheritance.');
}
/**
* Builds and returns the button configuration.
*
* @return FormConfigInterface
*/
public function getFormConfig()
{
// This method should be idempotent, so clone the builder
$config = clone $this;
$config->locked = true;
return $config;
}
/**
* Unsupported method.
*/
public function getEventDispatcher()
{
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* Unsupported method.
*/
public function getPropertyPath()
{
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getMapped()
{
return false;
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getByReference()
{
return false;
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getCompound()
{
return false;
}
/**
* Returns the form type used to construct the button.
*
* @return ResolvedFormTypeInterface The button's type
*/
public function getType()
{
return $this->type;
}
/**
* Unsupported method.
*
* @return array Always returns an empty array
*/
public function getViewTransformers()
{
return array();
}
/**
* Unsupported method.
*
* @return array Always returns an empty array
*/
public function getModelTransformers()
{
return array();
}
/**
* Unsupported method.
*/
public function getDataMapper()
{
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getRequired()
{
return false;
}
/**
* Returns whether the button is disabled.
*
* @return bool Whether the button is disabled
*/
public function getDisabled()
{
return $this->disabled;
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getErrorBubbling()
{
return false;
}
/**
* Unsupported method.
*/
public function getEmptyData()
{
}
/**
* Returns additional attributes of the button.
*
* @return array An array of key-value combinations
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Returns whether the attribute with the given name exists.
*
* @param string $name The attribute name
*
* @return bool Whether the attribute exists
*/
public function hasAttribute($name)
{
return array_key_exists($name, $this->attributes);
}
/**
* Returns the value of the given attribute.
*
* @param string $name The attribute name
* @param mixed $default The value returned if the attribute does not exist
*
* @return mixed The attribute value
*/
public function getAttribute($name, $default = null)
{
return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
}
/**
* Unsupported method.
*/
public function getData()
{
}
/**
* Unsupported method.
*/
public function getDataClass()
{
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getDataLocked()
{
return false;
}
/**
* Unsupported method.
*/
public function getFormFactory()
{
}
/**
* Unsupported method.
*/
public function getAction()
{
}
/**
* Unsupported method.
*/
public function getMethod()
{
}
/**
* Unsupported method.
*/
public function getRequestHandler()
{
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getAutoInitialize()
{
return false;
}
/**
* Unsupported method.
*
* @return bool Always returns false
*/
public function getInheritData()
{
return false;
}
/**
* Returns all options passed during the construction of the button.
*
* @return array The passed options
*/
public function getOptions()
{
return $this->options;
}
/**
* Returns whether a specific option exists.
*
* @param string $name The option name,
*
* @return bool Whether the option exists
*/
public function hasOption($name)
{
return array_key_exists($name, $this->options);
}
/**
* Returns the value of a specific option.
*
* @param string $name The option name
* @param mixed $default The value returned if the option does not exist
*
* @return mixed The option value
*/
public function getOption($name, $default = null)
{
return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
}
/**
* Unsupported method.
*
* @return int Always returns 0
*/
public function count()
{
return 0;
}
/**
* Unsupported method.
*
* @return \EmptyIterator Always returns an empty iterator
*/
public function getIterator()
{
return new \EmptyIterator();
}
}

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\Form;
/**
* A type that should be converted into a {@link Button} instance.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ButtonTypeInterface extends FormTypeInterface
{
}

347
vendor/symfony/form/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,347 @@
CHANGELOG
=========
3.3.0
-----
* deprecated using "choices" option in ``CountryType``, ``CurrencyType``, ``LanguageType``, ``LocaleType``, and
``TimezoneType`` when "choice_loader" is not ``null``
* added `Symfony\Component\Form\FormErrorIterator::findByCodes()`
* added `getTypedExtensions`, `getTypes`, and `getTypeGuessers` to `Symfony\Component\Form\Test\FormIntegrationTestCase`
* added `FormPass`
3.2.0
-----
* added `CallbackChoiceLoader`
* implemented `ChoiceLoaderInterface` in children of `ChoiceType`
3.1.0
-----
* deprecated the "choices_as_values" option of ChoiceType
* deprecated support for data objects that implements both `Traversable` and
`ArrayAccess` in `ResizeFormListener::preSubmit` method
* Using callable strings as choice options in `ChoiceType` has been deprecated
and will be used as `PropertyPath` instead of callable in Symfony 4.0.
* implemented `DataTransformerInterface` in `TextType`
* deprecated caching loaded choice list in `LazyChoiceList::$loadedList`
3.0.0
-----
* removed `FormTypeInterface::setDefaultOptions()` method
* removed `AbstractType::setDefaultOptions()` method
* removed `FormTypeExtensionInterface::setDefaultOptions()` method
* removed `AbstractTypeExtension::setDefaultOptions()` method
* added `FormTypeInterface::configureOptions()` method
* added `FormTypeExtensionInterface::configureOptions()` method
2.8.0
-----
* added option "choice_translation_domain" to DateType, TimeType and DateTimeType.
* deprecated option "read_only" in favor of "attr['readonly']"
* added the html5 "range" FormType
* deprecated the "cascade_validation" option in favor of setting "constraints"
with the Valid constraint
* moved data trimming logic of TrimListener into StringUtil
* [BC BREAK] When registering a type extension through the DI extension, the tag alias has to match the actual extended type.
2.7.0
-----
* added option "choice_translation_domain" to ChoiceType.
* deprecated option "precision" in favor of "scale"
* deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions().
* deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions().
* added new ChoiceList interface and implementations in the Symfony\Component\Form\ChoiceList namespace
* added new ChoiceView in the Symfony\Component\Form\ChoiceList\View namespace
* choice groups are now represented by ChoiceGroupView objects in the view
* deprecated the old ChoiceList interface and implementations
* deprecated the old ChoiceView class
* added CheckboxListMapper and RadioListMapper
* deprecated ChoiceToBooleanArrayTransformer and ChoicesToBooleanArrayTransformer
* deprecated FixCheckboxInputListener and FixRadioInputListener
* deprecated the "choice_list" option of ChoiceType
* added new options to ChoiceType:
* "choices_as_values"
* "choice_loader"
* "choice_label"
* "choice_name"
* "choice_value"
* "choice_attr"
* "group_by"
2.6.2
-----
* Added back the `model_timezone` and `view_timezone` options for `TimeType`, `DateType`
and `BirthdayType`
2.6.0
-----
* added "html5" option to Date, Time and DateTimeFormType to be able to
enable/disable HTML5 input date when widget option is "single_text"
* added "label_format" option with possible placeholders "%name%" and "%id%"
* [BC BREAK] drop support for model_timezone and view_timezone options in TimeType, DateType and BirthdayType,
update to 2.6.2 to get back support for these options
2.5.0
------
* deprecated options "max_length" and "pattern" in favor of putting these values in "attr" option
* added an option for multiple files upload
* form errors now reference their cause (constraint violation, exception, ...)
* form errors now remember which form they were originally added to
* [BC BREAK] added two optional parameters to FormInterface::getErrors() and
changed the method to return a Symfony\Component\Form\FormErrorIterator
instance instead of an array
* errors mapped to unsubmitted forms are discarded now
* ObjectChoiceList now compares choices by their value, if a value path is
given
* you can now pass interface names in the "data_class" option
* [BC BREAK] added `FormInterface::getTransformationFailure()`
2.4.0
-----
* moved CSRF implementation to the new Security CSRF sub-component
* deprecated CsrfProviderInterface and its implementations
* deprecated options "csrf_provider" and "intention" in favor of the new options "csrf_token_manager" and "csrf_token_id"
2.3.0
-----
* deprecated FormPerformanceTestCase and FormIntegrationTestCase in the Symfony\Component\Form\Tests namespace and moved them to the Symfony\Component\Form\Test namespace
* deprecated TypeTestCase in the Symfony\Component\Form\Tests\Extension\Core\Type namespace and moved it to the Symfony\Component\Form\Test namespace
* changed FormRenderer::humanize() to humanize also camel cased field name
* added RequestHandlerInterface and FormInterface::handleRequest()
* deprecated passing a Request instance to FormInterface::bind()
* added options "method" and "action" to FormType
* deprecated option "virtual" in favor "inherit_data"
* deprecated VirtualFormAwareIterator in favor of InheritDataAwareIterator
* [BC BREAK] removed the "array" type hint from DataMapperInterface
* improved forms inheriting their parent data to actually return that data from getData(), getNormData() and getViewData()
* added component-level exceptions for various SPL exceptions
changed all uses of the deprecated Exception class to use more specialized exceptions instead
removed NotInitializedException, NotValidException, TypeDefinitionException, TypeLoaderException, CreationException
* added events PRE_SUBMIT, SUBMIT and POST_SUBMIT
* deprecated events PRE_BIND, BIND and POST_BIND
* [BC BREAK] renamed bind() and isBound() in FormInterface to submit() and isSubmitted()
* added methods submit() and isSubmitted() to Form
* deprecated bind() and isBound() in Form
* deprecated AlreadyBoundException in favor of AlreadySubmittedException
* added support for PATCH requests
* [BC BREAK] added initialize() to FormInterface
* [BC BREAK] added getAutoInitialize() to FormConfigInterface
* [BC BREAK] added setAutoInitialize() to FormConfigBuilderInterface
* [BC BREAK] initialization for Form instances added to a form tree must be manually disabled
* PRE_SET_DATA is now guaranteed to be called after children were added by the form builder,
unless FormInterface::setData() is called manually
* fixed CSRF error message to be translated
* custom CSRF error messages can now be set through the "csrf_message" option
* fixed: expanded single-choice fields now show a radio button for the empty value
2.2.0
-----
* TrimListener now removes unicode whitespaces
* deprecated getParent(), setParent() and hasParent() in FormBuilderInterface
* FormInterface::add() now accepts a FormInterface instance OR a field's name, type and options
* removed special characters between the choice or text fields of DateType unless
the option "format" is set to a custom value
* deprecated FormException and introduced ExceptionInterface instead
* [BC BREAK] FormException is now an interface
* protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig()
* [BC BREAK] inserted argument `$message` in the constructor of `FormError`
* the PropertyPath class and related classes were moved to a dedicated
PropertyAccess component. During the move, InvalidPropertyException was
renamed to NoSuchPropertyException. FormUtil was split: FormUtil::singularify()
can now be found in Symfony\Component\PropertyAccess\StringUtil. The methods
getValue() and setValue() from PropertyPath were extracted into a new class
PropertyAccessor.
* added an optional PropertyAccessorInterface parameter to FormType,
ObjectChoiceList and PropertyPathMapper
* [BC BREAK] PropertyPathMapper and FormType now have a constructor
* [BC BREAK] setting the option "validation_groups" to ``false`` now disables validation
instead of assuming group "Default"
2.1.0
-----
* [BC BREAK] ``read_only`` field attribute now renders as ``readonly="readonly"``, use ``disabled`` instead
* [BC BREAK] child forms now aren't validated anymore by default
* made validation of form children configurable (new option: cascade_validation)
* added support for validation groups as callbacks
* made the translation catalogue configurable via the "translation_domain" option
* added Form::getErrorsAsString() to help debugging forms
* allowed setting different options for RepeatedType fields (like the label)
* added support for empty form name at root level, this enables rendering forms
without form name prefix in field names
* [BC BREAK] form and field names must start with a letter, digit or underscore
and only contain letters, digits, underscores, hyphens and colons
* [BC BREAK] changed default name of the prototype in the "collection" type
from "$$name$$" to "\__name\__". No dollars are appended/prepended to custom
names anymore.
* [BC BREAK] improved ChoiceListInterface
* [BC BREAK] added SimpleChoiceList and LazyChoiceList as replacement of
ArrayChoiceList
* added ChoiceList and ObjectChoiceList to use objects as choices
* [BC BREAK] removed EntitiesToArrayTransformer and EntityToIdTransformer.
The former has been replaced by CollectionToArrayTransformer in combination
with EntityChoiceList, the latter is not required in the core anymore.
* [BC BREAK] renamed
* ArrayToBooleanChoicesTransformer to ChoicesToBooleanArrayTransformer
* ScalarToBooleanChoicesTransformer to ChoiceToBooleanArrayTransformer
* ArrayToChoicesTransformer to ChoicesToValuesTransformer
* ScalarToChoiceTransformer to ChoiceToValueTransformer
to be consistent with the naming in ChoiceListInterface.
They were merged into ChoiceList and have no public equivalent anymore.
* choice fields now throw a FormException if neither the "choices" nor the
"choice_list" option is set
* the radio type is now a child of the checkbox type
* the collection, choice (with multiple selection) and entity (with multiple
selection) types now make use of addXxx() and removeXxx() methods in your
model if you set "by_reference" to false. For a custom, non-recognized
singular form, set the "property_path" option like this: "plural|singular"
* forms now don't create an empty object anymore if they are completely
empty and not required. The empty value for such forms is null.
* added constant Guess::VERY_HIGH_CONFIDENCE
* [BC BREAK] The methods `add`, `remove`, `setParent`, `bind` and `setData`
in class Form now throw an exception if the form is already bound
* fields of constrained classes without a NotBlank or NotNull constraint are
set to not required now, as stated in the docs
* fixed TimeType and DateTimeType to not display seconds when "widget" is
"single_text" unless "with_seconds" is set to true
* checkboxes of in an expanded multiple-choice field don't include the choice
in their name anymore. Their names terminate with "[]" now.
* deprecated FormValidatorInterface and substituted its implementations
by event subscribers
* simplified CSRF protection and removed the csrf type
* deprecated FieldType and merged it into FormType
* added new option "compound" that lets you switch between field and form behavior
* [BC BREAK] renamed theme blocks
* "field_*" to "form_*"
* "field_widget" to "form_widget_simple"
* "widget_choice_options" to "choice_widget_options"
* "generic_label" to "form_label"
* added theme blocks "form_widget_compound", "choice_widget_expanded" and
"choice_widget_collapsed" to make theming more modular
* ValidatorTypeGuesser now guesses "collection" for array type constraint
* added method `guessPattern` to FormTypeGuesserInterface to guess which pattern to use in the HTML5 attribute "pattern"
* deprecated method `guessMinLength` in favor of `guessPattern`
* labels don't display field attributes anymore. Label attributes can be
passed in the "label_attr" option/variable
* added option "mapped" which should be used instead of setting "property_path" to false
* [BC BREAK] "data_class" now *must* be set if a form maps to an object and should be left empty otherwise
* improved error mapping on forms
* dot (".") rules are now allowed to map errors assigned to a form to
one of its children
* errors are not mapped to unsynchronized forms anymore
* [BC BREAK] changed Form constructor to accept a single `FormConfigInterface` object
* [BC BREAK] changed argument order in the FormBuilder constructor
* added Form method `getViewData`
* deprecated Form methods
* `getTypes`
* `getErrorBubbling`
* `getNormTransformers`
* `getClientTransformers`
* `getAttribute`
* `hasAttribute`
* `getClientData`
* added FormBuilder methods
* `getTypes`
* `addViewTransformer`
* `getViewTransformers`
* `resetViewTransformers`
* `addModelTransformer`
* `getModelTransformers`
* `resetModelTransformers`
* deprecated FormBuilder methods
* `prependClientTransformer`
* `appendClientTransformer`
* `getClientTransformers`
* `resetClientTransformers`
* `prependNormTransformer`
* `appendNormTransformer`
* `getNormTransformers`
* `resetNormTransformers`
* deprecated the option "validation_constraint" in favor of the new
option "constraints"
* removed superfluous methods from DataMapperInterface
* `mapFormToData`
* `mapDataToForm`
* added `setDefaultOptions` to FormTypeInterface and FormTypeExtensionInterface
which accepts an OptionsResolverInterface instance
* deprecated the methods `getDefaultOptions` and `getAllowedOptionValues`
in FormTypeInterface and FormTypeExtensionInterface
* options passed during construction can now be accessed from FormConfigInterface
* added FormBuilderInterface and FormConfigEditorInterface
* [BC BREAK] the method `buildForm` in FormTypeInterface and FormTypeExtensionInterface
now receives a FormBuilderInterface instead of a FormBuilder instance
* [BC BREAK] the method `buildViewBottomUp` was renamed to `finishView` in
FormTypeInterface and FormTypeExtensionInterface
* [BC BREAK] the options array is now passed as last argument of the
methods
* `buildView`
* `finishView`
in FormTypeInterface and FormTypeExtensionInterface
* [BC BREAK] no options are passed to `getParent` of FormTypeInterface anymore
* deprecated DataEvent and FilterDataEvent in favor of the new FormEvent which is
now passed to all events thrown by the component
* FormEvents::BIND now replaces FormEvents::BIND_NORM_DATA
* FormEvents::PRE_SET_DATA now replaces FormEvents::SET_DATA
* FormEvents::PRE_BIND now replaces FormEvents::BIND_CLIENT_DATA
* deprecated FormEvents::SET_DATA, FormEvents::BIND_CLIENT_DATA and
FormEvents::BIND_NORM_DATA
* [BC BREAK] reversed the order of the first two arguments to `createNamed`
and `createNamedBuilder` in `FormFactoryInterface`
* deprecated `getChildren` in Form and FormBuilder in favor of `all`
* deprecated `hasChildren` in Form and FormBuilder in favor of `count`
* FormBuilder now implements \IteratorAggregate
* [BC BREAK] compound forms now always need a data mapper
* FormBuilder now maintains the order when explicitly adding form builders as children
* ChoiceType now doesn't add the empty value anymore if the choices already contain an empty element
* DateType, TimeType and DateTimeType now show empty values again if not required
* [BC BREAK] fixed rendering of errors for DateType, BirthdayType and similar ones
* [BC BREAK] fixed: form constraints are only validated if they belong to the validated group
* deprecated `bindRequest` in `Form` and replaced it by a listener to FormEvents::PRE_BIND
* fixed: the "data" option supersedes default values from the model
* changed DateType to refer to the "format" option for calculating the year and day choices instead
of padding them automatically
* [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now if the widget is
"single_text", in order to support the HTML 5 date field out of the box
* added the option "format" to DateTimeType
* [BC BREAK] DateTimeType now outputs RFC 3339 dates by default, as generated and
consumed by HTML5 browsers, if the widget is "single_text"
* deprecated the options "data_timezone" and "user_timezone" in DateType, DateTimeType and TimeType
and renamed them to "model_timezone" and "view_timezone"
* fixed: TransformationFailedExceptions thrown in the model transformer are now caught by the form
* added FormRegistryInterface, ResolvedFormTypeInterface and ResolvedFormTypeFactoryInterface
* deprecated FormFactory methods
* `addType`
* `hasType`
* `getType`
* [BC BREAK] FormFactory now expects a FormRegistryInterface and a ResolvedFormTypeFactoryInterface as constructor argument
* [BC BREAK] The method `createBuilder` in FormTypeInterface is not supported anymore for performance reasons
* [BC BREAK] Removed `setTypes` from FormBuilder
* deprecated AbstractType methods
* `getExtensions`
* `setExtensions`
* ChoiceType now caches its created choice lists to improve performance
* [BC BREAK] Rows of a collection field cannot be themed individually anymore. All rows in the collection
field now have the same block names, which contains "entry" where it previously contained the row index.
* [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name.
* added FormRendererInterface, FormRendererEngineInterface and implementations of these interfaces
* [BC BREAK] removed the following methods from FormUtil:
* `toArrayKey`
* `toArrayKeys`
* `isChoiceGroup`
* `isChoiceSelected`
* [BC BREAK] renamed method `renderBlock` in FormHelper to `block` and changed its signature
* made FormView properties public and deprecated their accessor methods
* made the normalized data of a form accessible in the template through the variable "form.vars.data"
* made the original data of a choice accessible in the template through the property "choice.data"
* added convenience class Forms and FormFactoryBuilderInterface

View File

@@ -0,0 +1,75 @@
<?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\Form;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
class CallbackTransformer implements DataTransformerInterface
{
/**
* The callback used for forward transform.
*
* @var callable
*/
private $transform;
/**
* The callback used for reverse transform.
*
* @var callable
*/
private $reverseTransform;
/**
* Constructor.
*
* @param callable $transform The forward transform callback
* @param callable $reverseTransform The reverse transform callback
*/
public function __construct(callable $transform, callable $reverseTransform)
{
$this->transform = $transform;
$this->reverseTransform = $reverseTransform;
}
/**
* Transforms a value from the original representation to a transformed representation.
*
* @param mixed $data The value in the original representation
*
* @return mixed The value in the transformed representation
*
* @throws UnexpectedTypeException when the argument is not of the expected type
* @throws TransformationFailedException when the transformation fails
*/
public function transform($data)
{
return call_user_func($this->transform, $data);
}
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* @param mixed $data The value in the transformed representation
*
* @return mixed The value in the original representation
*
* @throws UnexpectedTypeException when the argument is not of the expected type
* @throws TransformationFailedException when the transformation fails
*/
public function reverseTransform($data)
{
return call_user_func($this->reverseTransform, $data);
}
}

View File

@@ -0,0 +1,246 @@
<?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\Form\ChoiceList;
/**
* A list of choices with arbitrary data types.
*
* The user of this class is responsible for assigning string values to the
* choices. Both the choices and their values are passed to the constructor.
* Each choice must have a corresponding value (with the same array key) in
* the value array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ArrayChoiceList implements ChoiceListInterface
{
/**
* The choices in the list.
*
* @var array
*/
protected $choices;
/**
* The values indexed by the original keys.
*
* @var array
*/
protected $structuredValues;
/**
* The original keys of the choices array.
*
* @var int[]|string[]
*/
protected $originalKeys;
/**
* The callback for creating the value for a choice.
*
* @var callable
*/
protected $valueCallback;
/**
* Creates a list with the given choices and values.
*
* The given choice array must have the same array keys as the value array.
*
* @param array|\Traversable $choices The selectable choices
* @param callable|null $value The callable for creating the value
* for a choice. If `null` is passed,
* incrementing integers are used as
* values
*/
public function __construct($choices, callable $value = null)
{
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
if (null === $value && $this->castableToString($choices)) {
$value = function ($choice) {
return false === $choice ? '0' : (string) $choice;
};
}
if (null !== $value) {
// If a deterministic value generator was passed, use it later
$this->valueCallback = $value;
} else {
// Otherwise simply generate incrementing integers as values
$i = 0;
$value = function () use (&$i) {
return $i++;
};
}
// If the choices are given as recursive array (i.e. with explicit
// choice groups), flatten the array. The grouping information is needed
// in the view only.
$this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues);
$this->choices = $choicesByValues;
$this->originalKeys = $keysByValues;
$this->structuredValues = $structuredValues;
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
return $this->choices;
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return array_map('strval', array_keys($this->choices));
}
/**
* {@inheritdoc}
*/
public function getStructuredValues()
{
return $this->structuredValues;
}
/**
* {@inheritdoc}
*/
public function getOriginalKeys()
{
return $this->originalKeys;
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
$choices = array();
foreach ($values as $i => $givenValue) {
if (array_key_exists($givenValue, $this->choices)) {
$choices[$i] = $this->choices[$givenValue];
}
}
return $choices;
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
$values = array();
// Use the value callback to compare choices by their values, if present
if ($this->valueCallback) {
$givenValues = array();
foreach ($choices as $i => $givenChoice) {
$givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice);
}
return array_intersect($givenValues, array_keys($this->choices));
}
// Otherwise compare choices by identity
foreach ($choices as $i => $givenChoice) {
foreach ($this->choices as $value => $choice) {
if ($choice === $givenChoice) {
$values[$i] = (string) $value;
break;
}
}
}
return $values;
}
/**
* Flattens an array into the given output variables.
*
* @param array $choices The array to flatten
* @param callable $value The callable for generating choice values
* @param array $choicesByValues The flattened choices indexed by the
* corresponding values
* @param array $keysByValues The original keys indexed by the
* corresponding values
* @param array $structuredValues The values indexed by the original keys
*
* @internal Must not be used by user-land code
*/
protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues)
{
if (null === $choicesByValues) {
$choicesByValues = array();
$keysByValues = array();
$structuredValues = array();
}
foreach ($choices as $key => $choice) {
if (is_array($choice)) {
$this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]);
continue;
}
$choiceValue = (string) call_user_func($value, $choice);
$choicesByValues[$choiceValue] = $choice;
$keysByValues[$choiceValue] = $key;
$structuredValues[$key] = $choiceValue;
}
}
/**
* Checks whether the given choices can be cast to strings without
* generating duplicates.
*
* @param array $choices The choices
* @param array|null $cache The cache for previously checked entries. Internal
*
* @return bool Returns true if the choices can be cast to strings and
* false otherwise.
*/
private function castableToString(array $choices, array &$cache = array())
{
foreach ($choices as $choice) {
if (is_array($choice)) {
if (!$this->castableToString($choice, $cache)) {
return false;
}
continue;
} elseif (!is_scalar($choice)) {
return false;
}
$choice = false === $choice ? '0' : (string) $choice;
if (isset($cache[$choice])) {
return false;
}
$cache[$choice] = true;
}
return true;
}
}

View File

@@ -0,0 +1,116 @@
<?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\Form\ChoiceList;
/**
* A list of choices that can be selected in a choice field.
*
* A choice list assigns unique string values to each of a list of choices.
* These string values are displayed in the "value" attributes in HTML and
* submitted back to the server.
*
* The acceptable data types for the choices depend on the implementation.
* Values must always be strings and (within the list) free of duplicates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceListInterface
{
/**
* Returns all selectable choices.
*
* @return array The selectable choices indexed by the corresponding values
*/
public function getChoices();
/**
* Returns the values for the choices.
*
* The values are strings that do not contain duplicates.
*
* @return string[] The choice values
*/
public function getValues();
/**
* Returns the values in the structure originally passed to the list.
*
* Contrary to {@link getValues()}, the result is indexed by the original
* keys of the choices. If the original array contained nested arrays, these
* nested arrays are represented here as well:
*
* $form->add('field', 'choice', array(
* 'choices' => array(
* 'Decided' => array('Yes' => true, 'No' => false),
* 'Undecided' => array('Maybe' => null),
* ),
* ));
*
* In this example, the result of this method is:
*
* array(
* 'Decided' => array('Yes' => '0', 'No' => '1'),
* 'Undecided' => array('Maybe' => '2'),
* )
*
* @return string[] The choice values
*/
public function getStructuredValues();
/**
* Returns the original keys of the choices.
*
* The original keys are the keys of the choice array that was passed in the
* "choice" option of the choice type. Note that this array may contain
* duplicates if the "choice" option contained choice groups:
*
* $form->add('field', 'choice', array(
* 'choices' => array(
* 'Decided' => array(true, false),
* 'Undecided' => array(null),
* ),
* ));
*
* In this example, the original key 0 appears twice, once for `true` and
* once for `null`.
*
* @return int[]|string[] The original choice keys indexed by the
* corresponding choice values
*/
public function getOriginalKeys();
/**
* Returns the choices corresponding to the given values.
*
* The choices are returned with the same keys and in the same order as the
* corresponding values in the given array.
*
* @param string[] $values An array of choice values. Non-existing values in
* this array are ignored
*
* @return array An array of choices
*/
public function getChoicesForValues(array $values);
/**
* Returns the values corresponding to the given choices.
*
* The values are returned with the same keys and in the same order as the
* corresponding choices in the given array.
*
* @param array $choices An array of choices. Non-existing choices in this
* array are ignored
*
* @return string[] An array of choice values
*/
public function getValuesForChoices(array $choices);
}

View File

@@ -0,0 +1,174 @@
<?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\Form\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
/**
* Caches the choice lists created by the decorated factory.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CachingFactoryDecorator implements ChoiceListFactoryInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $decoratedFactory;
/**
* @var ChoiceListInterface[]
*/
private $lists = array();
/**
* @var ChoiceListView[]
*/
private $views = array();
/**
* Generates a SHA-256 hash for the given value.
*
* Optionally, a namespace string can be passed. Calling this method will
* the same values, but different namespaces, will return different hashes.
*
* @param mixed $value The value to hash
* @param string $namespace Optional. The namespace
*
* @return string The SHA-256 hash
*
* @internal Should not be used by user-land code.
*/
public static function generateHash($value, $namespace = '')
{
if (is_object($value)) {
$value = spl_object_hash($value);
} elseif (is_array($value)) {
array_walk_recursive($value, function (&$v) {
if (is_object($v)) {
$v = spl_object_hash($v);
}
});
}
return hash('sha256', $namespace.':'.serialize($value));
}
/**
* Flattens an array into the given output variable.
*
* @param array $array The array to flatten
* @param array $output The flattened output
*
* @internal Should not be used by user-land code
*/
private static function flatten(array $array, &$output)
{
if (null === $output) {
$output = array();
}
foreach ($array as $key => $value) {
if (is_array($value)) {
self::flatten($value, $output);
continue;
}
$output[$key] = $value;
}
}
/**
* Decorates the given factory.
*
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
*/
public function __construct(ChoiceListFactoryInterface $decoratedFactory)
{
$this->decoratedFactory = $decoratedFactory;
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface The decorated factory
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*/
public function createListFromChoices($choices, $value = null)
{
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// The value is not validated on purpose. The decorated factory may
// decide which values to accept and which not.
// We ignore the choice groups for caching. If two choice lists are
// requested with the same choices, but a different grouping, the same
// choice list is returned.
self::flatten($choices, $flatChoices);
$hash = self::generateHash(array($flatChoices, $value), 'fromChoices');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
$hash = self::generateHash(array($loader, $value), 'fromLoader');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
// The input is not validated on purpose. This way, the decorated
// factory may decide which input to accept and which not.
$hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr));
if (!isset($this->views[$hash])) {
$this->views[$hash] = $this->decoratedFactory->createView(
$list,
$preferredChoices,
$label,
$index,
$groupBy,
$attr
);
}
return $this->views[$hash];
}
}

View File

@@ -0,0 +1,97 @@
<?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\Form\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
/**
* Creates {@link ChoiceListInterface} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceListFactoryInterface
{
/**
* Creates a choice list for the given choices.
*
* The choices should be passed in the values of the choices array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param array|\Traversable $choices The choices
* @param null|callable $value The callable generating the choice
* values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromChoices($choices, $value = null);
/**
* Creates a choice list that is loaded with the given loader.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable $value The callable generating the choice
* values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null);
/**
* Creates a view for the given choice list.
*
* Callables may be passed for all optional arguments. The callables receive
* the choice as first and the array key as the second argument.
*
* * The callable for the label and the name should return the generated
* label/choice name.
* * The callable for the preferred choices should return true or false,
* depending on whether the choice should be preferred or not.
* * The callable for the grouping should return the group name or null if
* a choice should not be grouped.
* * The callable for the attributes should return an array of HTML
* attributes that will be inserted in the tag of the choice.
*
* If no callable is passed, the labels will be generated from the choice
* keys. The view indices will be generated using an incrementing integer
* by default.
*
* The preferred choices can also be passed as array. Each choice that is
* contained in that array will be marked as preferred.
*
* The attributes can be passed as multi-dimensional array. The keys should
* match the keys of the choices. The values should be arrays of HTML
* attributes that should be added to the respective choice.
*
* @param ChoiceListInterface $list The choice list
* @param null|array|callable $preferredChoices The preferred choices
* @param null|callable $label The callable generating the
* choice labels
* @param null|callable $index The callable generating the
* view indices
* @param null|callable $groupBy The callable generating the
* group names
* @param null|array|callable $attr The callable generating the
* HTML attributes
*
* @return ChoiceListView The choice list view
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null);
}

View File

@@ -0,0 +1,243 @@
<?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\Form\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
/**
* Default implementation of {@link ChoiceListFactoryInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DefaultChoiceListFactory implements ChoiceListFactoryInterface
{
/**
* {@inheritdoc}
*/
public function createListFromChoices($choices, $value = null)
{
return new ArrayChoiceList($choices, $value);
}
/**
* {@inheritdoc}
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
return new LazyChoiceList($loader, $value);
}
/**
* {@inheritdoc}
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
$preferredViews = array();
$otherViews = array();
$choices = $list->getChoices();
$keys = $list->getOriginalKeys();
if (!is_callable($preferredChoices) && !empty($preferredChoices)) {
$preferredChoices = function ($choice) use ($preferredChoices) {
return in_array($choice, $preferredChoices, true);
};
}
// The names are generated from an incrementing integer by default
if (null === $index) {
$index = 0;
}
// If $groupBy is a callable, choices are added to the group with the
// name returned by the callable. If the callable returns null, the
// choice is not added to any group
if (is_callable($groupBy)) {
foreach ($choices as $value => $choice) {
self::addChoiceViewGroupedBy(
$groupBy,
$choice,
(string) $value,
$label,
$keys,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
} else {
// Otherwise use the original structure of the choices
self::addChoiceViewsGroupedBy(
$list->getStructuredValues(),
$label,
$choices,
$keys,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
// Remove any empty group view that may have been created by
// addChoiceViewGroupedBy()
foreach ($preferredViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
unset($preferredViews[$key]);
}
}
foreach ($otherViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
unset($otherViews[$key]);
}
}
return new ChoiceListView($otherViews, $preferredViews);
}
private static function addChoiceView($choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
// $value may be an integer or a string, since it's stored in the array
// keys. We want to guarantee it's a string though.
$key = $keys[$value];
$nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value);
// BC normalize label to accept a false value
if (null === $label) {
// If the labels are null, use the original choice key by default
$label = (string) $key;
} elseif (false !== $label) {
// If "choice_label" is set to false and "expanded" is true, the value false
// should be passed on to the "label" option of the checkboxes/radio buttons
$dynamicLabel = call_user_func($label, $choice, $key, $value);
$label = false === $dynamicLabel ? false : (string) $dynamicLabel;
}
$view = new ChoiceView(
$choice,
$value,
$label,
// The attributes may be a callable or a mapping from choice indices
// to nested arrays
is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array())
);
// $isPreferred may be null if no choices are preferred
if ($isPreferred && call_user_func($isPreferred, $choice, $key, $value)) {
$preferredViews[$nextIndex] = $view;
} else {
$otherViews[$nextIndex] = $view;
}
}
private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
foreach ($groupBy as $key => $value) {
if (null === $value) {
continue;
}
// Add the contents of groups to new ChoiceGroupView instances
if (is_array($value)) {
$preferredViewsForGroup = array();
$otherViewsForGroup = array();
self::addChoiceViewsGroupedBy(
$value,
$label,
$choices,
$keys,
$index,
$attr,
$isPreferred,
$preferredViewsForGroup,
$otherViewsForGroup
);
if (count($preferredViewsForGroup) > 0) {
$preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup);
}
if (count($otherViewsForGroup) > 0) {
$otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup);
}
continue;
}
// Add ungrouped items directly
self::addChoiceView(
$choices[$value],
$value,
$label,
$keys,
$index,
$attr,
$isPreferred,
$preferredViews,
$otherViews
);
}
}
private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
$groupLabel = call_user_func($groupBy, $choice, $keys[$value], $value);
if (null === $groupLabel) {
// If the callable returns null, don't group the choice
self::addChoiceView(
$choice,
$value,
$label,
$keys,
$index,
$attr,
$isPreferred,
$preferredViews,
$otherViews
);
return;
}
$groupLabel = (string) $groupLabel;
// Initialize the group views if necessary. Unnecessarily built group
// views will be cleaned up at the end of createView()
if (!isset($preferredViews[$groupLabel])) {
$preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
$otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
}
self::addChoiceView(
$choice,
$value,
$label,
$keys,
$index,
$attr,
$isPreferred,
$preferredViews[$groupLabel]->choices,
$otherViews[$groupLabel]->choices
);
}
}

View File

@@ -0,0 +1,229 @@
<?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\Form\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* Adds property path support to a choice list factory.
*
* Pass the decorated factory to the constructor:
*
* ```php
* $decorator = new PropertyAccessDecorator($factory);
* ```
*
* You can now pass property paths for generating choice values, labels, view
* indices, HTML attributes and for determining the preferred choices and the
* choice groups:
*
* ```php
* // extract values from the $value property
* $list = $createListFromChoices($objects, 'value');
* ```
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyAccessDecorator implements ChoiceListFactoryInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $decoratedFactory;
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* Decorates the given factory.
*
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
* @param null|PropertyAccessorInterface $propertyAccessor The used property accessor
*/
public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null)
{
$this->decoratedFactory = $decoratedFactory;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface The decorated factory
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*
* @param array|\Traversable $choices The choices
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromChoices($choices, $value = null)
{
if (is_string($value) && !is_callable($value)) {
$value = new PropertyPath($value);
} elseif (is_string($value) && is_callable($value)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($value instanceof PropertyPath) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
// The callable may be invoked with a non-object/array value
// when such values are passed to
// ChoiceListInterface::getValuesForChoices(). Handle this case
// so that the call to getValue() doesn't break.
if (is_object($choice) || is_array($choice)) {
return $accessor->getValue($choice, $value);
}
};
}
return $this->decoratedFactory->createListFromChoices($choices, $value);
}
/**
* {@inheritdoc}
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
if (is_string($value) && !is_callable($value)) {
$value = new PropertyPath($value);
} elseif (is_string($value) && is_callable($value)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($value instanceof PropertyPath) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
// The callable may be invoked with a non-object/array value
// when such values are passed to
// ChoiceListInterface::getValuesForChoices(). Handle this case
// so that the call to getValue() doesn't break.
if (is_object($choice) || is_array($choice)) {
return $accessor->getValue($choice, $value);
}
};
}
return $this->decoratedFactory->createListFromLoader($loader, $value);
}
/**
* {@inheritdoc}
*
* @param ChoiceListInterface $list The choice list
* @param null|array|callable|string|PropertyPath $preferredChoices The preferred choices
* @param null|callable|string|PropertyPath $label The callable or path generating the choice labels
* @param null|callable|string|PropertyPath $index The callable or path generating the view indices
* @param null|callable|string|PropertyPath $groupBy The callable or path generating the group names
* @param null|array|callable|string|PropertyPath $attr The callable or path generating the HTML attributes
*
* @return ChoiceListView The choice list view
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
$accessor = $this->propertyAccessor;
if (is_string($label) && !is_callable($label)) {
$label = new PropertyPath($label);
} elseif (is_string($label) && is_callable($label)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($label instanceof PropertyPath) {
$label = function ($choice) use ($accessor, $label) {
return $accessor->getValue($choice, $label);
};
}
if (is_string($preferredChoices) && !is_callable($preferredChoices)) {
$preferredChoices = new PropertyPath($preferredChoices);
} elseif (is_string($preferredChoices) && is_callable($preferredChoices)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($preferredChoices instanceof PropertyPath) {
$preferredChoices = function ($choice) use ($accessor, $preferredChoices) {
try {
return $accessor->getValue($choice, $preferredChoices);
} catch (UnexpectedTypeException $e) {
// Assume not preferred if not readable
return false;
}
};
}
if (is_string($index) && !is_callable($index)) {
$index = new PropertyPath($index);
} elseif (is_string($index) && is_callable($index)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($index instanceof PropertyPath) {
$index = function ($choice) use ($accessor, $index) {
return $accessor->getValue($choice, $index);
};
}
if (is_string($groupBy) && !is_callable($groupBy)) {
$groupBy = new PropertyPath($groupBy);
} elseif (is_string($groupBy) && is_callable($groupBy)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($groupBy instanceof PropertyPath) {
$groupBy = function ($choice) use ($accessor, $groupBy) {
try {
return $accessor->getValue($choice, $groupBy);
} catch (UnexpectedTypeException $e) {
// Don't group if path is not readable
}
};
}
if (is_string($attr) && !is_callable($attr)) {
$attr = new PropertyPath($attr);
} elseif (is_string($attr) && is_callable($attr)) {
@trigger_error('Passing callable strings is deprecated since version 3.1 and PropertyAccessDecorator will treat them as property paths in 4.0. You should use a "\Closure" instead.', E_USER_DEPRECATED);
}
if ($attr instanceof PropertyPath) {
$attr = function ($choice) use ($accessor, $attr) {
return $accessor->getValue($choice, $attr);
};
}
return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
}
}

View File

@@ -0,0 +1,203 @@
<?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\Form\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
/**
* A choice list that loads its choices lazily.
*
* The choices are fetched using a {@link ChoiceLoaderInterface} instance.
* If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is
* called, the choice list is only loaded partially for improved performance.
*
* Once {@link getChoices()} or {@link getValues()} is called, the list is
* loaded fully.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class LazyChoiceList implements ChoiceListInterface
{
/**
* The choice loader.
*
* @var ChoiceLoaderInterface
*/
private $loader;
/**
* The callable creating string values for each choice.
*
* If null, choices are simply cast to strings.
*
* @var null|callable
*/
private $value;
/**
* @var ChoiceListInterface|null
*
* @deprecated Since 3.1, to be removed in 4.0. Cache the choice list in the {@link ChoiceLoaderInterface} instead.
*/
private $loadedList;
/**
* @var bool
*
* @deprecated Flag used for BC layer since 3.1. To be removed in 4.0.
*/
private $loaded = false;
/**
* Creates a lazily-loaded list using the given loader.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable $value The callable generating the choice
* values
*/
public function __construct(ChoiceLoaderInterface $loader, callable $value = null)
{
$this->loader = $loader;
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
if ($this->loaded) {
// We can safely invoke the {@link ChoiceLoaderInterface} assuming it has the list
// in cache when the lazy list is already loaded
if ($this->loadedList !== $this->loader->loadChoiceList($this->value)) {
@trigger_error(sprintf('Caching the choice list in %s is deprecated since 3.1 and will not happen in 4.0. Cache the list in the %s instead.', __CLASS__, ChoiceLoaderInterface::class), E_USER_DEPRECATED);
}
return $this->loadedList->getChoices();
}
// BC
$this->loadedList = $this->loader->loadChoiceList($this->value);
$this->loaded = true;
return $this->loadedList->getChoices();
// In 4.0 keep the following line only:
// return $this->loader->loadChoiceList($this->value)->getChoices()
}
/**
* {@inheritdoc}
*/
public function getValues()
{
if ($this->loaded) {
// Check whether the loader has the same cache
if ($this->loadedList !== $this->loader->loadChoiceList($this->value)) {
@trigger_error(sprintf('Caching the choice list in %s is deprecated since 3.1 and will not happen in 4.0. Cache the list in the %s instead.', __CLASS__, ChoiceLoaderInterface::class), E_USER_DEPRECATED);
}
return $this->loadedList->getValues();
}
// BC
$this->loadedList = $this->loader->loadChoiceList($this->value);
$this->loaded = true;
return $this->loadedList->getValues();
// In 4.0 keep the following line only:
// return $this->loader->loadChoiceList($this->value)->getValues()
}
/**
* {@inheritdoc}
*/
public function getStructuredValues()
{
if ($this->loaded) {
// Check whether the loader has the same cache
if ($this->loadedList !== $this->loader->loadChoiceList($this->value)) {
@trigger_error(sprintf('Caching the choice list in %s is deprecated since 3.1 and will not happen in 4.0. Cache the list in the %s instead.', __CLASS__, ChoiceLoaderInterface::class), E_USER_DEPRECATED);
}
return $this->loadedList->getStructuredValues();
}
// BC
$this->loadedList = $this->loader->loadChoiceList($this->value);
$this->loaded = true;
return $this->loadedList->getStructuredValues();
// In 4.0 keep the following line only:
// return $this->loader->loadChoiceList($this->value)->getStructuredValues();
}
/**
* {@inheritdoc}
*/
public function getOriginalKeys()
{
if ($this->loaded) {
// Check whether the loader has the same cache
if ($this->loadedList !== $this->loader->loadChoiceList($this->value)) {
@trigger_error(sprintf('Caching the choice list in %s is deprecated since 3.1 and will not happen in 4.0. Cache the list in the %s instead.', __CLASS__, ChoiceLoaderInterface::class), E_USER_DEPRECATED);
}
return $this->loadedList->getOriginalKeys();
}
// BC
$this->loadedList = $this->loader->loadChoiceList($this->value);
$this->loaded = true;
return $this->loadedList->getOriginalKeys();
// In 4.0 keep the following line only:
// return $this->loader->loadChoiceList($this->value)->getOriginalKeys();
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
if ($this->loaded) {
// Check whether the loader has the same cache
if ($this->loadedList !== $this->loader->loadChoiceList($this->value)) {
@trigger_error(sprintf('Caching the choice list in %s is deprecated since 3.1 and will not happen in 4.0. Cache the list in the %s instead.', __CLASS__, ChoiceLoaderInterface::class), E_USER_DEPRECATED);
}
return $this->loadedList->getChoicesForValues($values);
}
return $this->loader->loadChoicesForValues($values, $this->value);
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
if ($this->loaded) {
// Check whether the loader has the same cache
if ($this->loadedList !== $this->loader->loadChoiceList($this->value)) {
@trigger_error(sprintf('Caching the choice list in %s is deprecated since 3.1 and will not happen in 4.0. Cache the list in the %s instead.', __CLASS__, ChoiceLoaderInterface::class), E_USER_DEPRECATED);
}
return $this->loadedList->getValuesForChoices($choices);
}
return $this->loader->loadValuesForChoices($choices, $this->value);
}
}

View File

@@ -0,0 +1,77 @@
<?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\Form\ChoiceList\Loader;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
/**
* Loads an {@link ArrayChoiceList} instance from a callable returning an array of choices.
*
* @author Jules Pietri <jules@heahprod.com>
*/
class CallbackChoiceLoader implements ChoiceLoaderInterface
{
private $callback;
/**
* The loaded choice list.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* @param callable $callback The callable returning an array of choices
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(call_user_func($this->callback), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
if (empty($values)) {
return array();
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
if (empty($choices)) {
return array();
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,76 @@
<?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\Form\ChoiceList\Loader;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* Loads a choice list.
*
* The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()}
* can be used to load the list only partially in cases where a fully-loaded
* list is not necessary.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceLoaderInterface
{
/**
* Loads a list of choices.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param null|callable $value The callable which generates the values
* from choices
*
* @return ChoiceListInterface The loaded choice list
*/
public function loadChoiceList($value = null);
/**
* Loads the choices corresponding to the given values.
*
* The choices are returned with the same keys and in the same order as the
* corresponding values in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param string[] $values An array of choice values. Non-existing
* values in this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return array An array of choices
*/
public function loadChoicesForValues(array $values, $value = null);
/**
* Loads the values corresponding to the given choices.
*
* The values are returned with the same keys and in the same order as the
* corresponding choices in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param array $choices An array of choices. Non-existing choices in
* this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return string[] An array of choice values
*/
public function loadValuesForChoices(array $choices, $value = null);
}

View File

@@ -0,0 +1,57 @@
<?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\Form\ChoiceList\View;
/**
* Represents a group of choices in templates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceGroupView implements \IteratorAggregate
{
/**
* The label of the group.
*
* @var string
*/
public $label;
/**
* The choice views in the group.
*
* @var ChoiceGroupView[]|ChoiceView[]
*/
public $choices;
/**
* Creates a new choice group view.
*
* @param string $label The label of the group
* @param ChoiceGroupView[]|ChoiceView[] $choices The choice views in the
* group.
*/
public function __construct($label, array $choices = array())
{
$this->label = $label;
$this->choices = $choices;
}
/**
* {@inheritdoc}
*
* @return self[]|ChoiceView[]
*/
public function getIterator()
{
return new \ArrayIterator($this->choices);
}
}

View File

@@ -0,0 +1,71 @@
<?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\Form\ChoiceList\View;
/**
* Represents a choice list in templates.
*
* A choice list contains choices and optionally preferred choices which are
* displayed in the very beginning of the list. Both choices and preferred
* choices may be grouped in {@link ChoiceGroupView} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceListView
{
/**
* The choices.
*
* @var ChoiceGroupView[]|ChoiceView[]
*/
public $choices;
/**
* The preferred choices.
*
* @var ChoiceGroupView[]|ChoiceView[]
*/
public $preferredChoices;
/**
* Creates a new choice list view.
*
* @param ChoiceGroupView[]|ChoiceView[] $choices The choice views
* @param ChoiceGroupView[]|ChoiceView[] $preferredChoices The preferred
* choice views.
*/
public function __construct(array $choices = array(), array $preferredChoices = array())
{
$this->choices = $choices;
$this->preferredChoices = $preferredChoices;
}
/**
* Returns whether a placeholder is in the choices.
*
* A placeholder must be the first child element, not be in a group and have an empty value.
*
* @return bool
*/
public function hasPlaceholder()
{
if ($this->preferredChoices) {
$firstChoice = reset($this->preferredChoices);
return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
}
$firstChoice = reset($this->choices);
return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
}
}

View File

@@ -0,0 +1,64 @@
<?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\Form\ChoiceList\View;
/**
* Represents a choice in templates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceView
{
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
/**
* The original choice value.
*
* @var mixed
*/
public $data;
/**
* Additional attributes for the HTML tag.
*
* @var array
*/
public $attr;
/**
* Creates a new choice view.
*
* @param mixed $data The original choice
* @param string $value The view representation of the choice
* @param string $label The label displayed to humans
* @param array $attr Additional attributes for the HTML tag
*/
public function __construct($data, $value, $label, array $attr = array())
{
$this->data = $data;
$this->value = $value;
$this->label = $label;
$this->attr = $attr;
}
}

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\Form;
/**
* A clickable form element.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ClickableInterface
{
/**
* Returns whether this element was clicked.
*
* @return bool Whether this element was clicked
*/
public function isClicked();
}

View File

@@ -0,0 +1,38 @@
<?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\Form;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface DataMapperInterface
{
/**
* Maps properties of some data to a list of forms.
*
* @param mixed $data Structured data
* @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances
*
* @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported.
*/
public function mapDataToForms($data, $forms);
/**
* Maps the data of a list of forms into the properties of some data.
*
* @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances
* @param mixed $data Structured data
*
* @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported.
*/
public function mapFormsToData($forms, &$data);
}

View File

@@ -0,0 +1,77 @@
<?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\Form;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforms a value between different representations.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface DataTransformerInterface
{
/**
* Transforms a value from the original representation to a transformed representation.
*
* This method is called on two occasions inside a form field:
*
* 1. When the form field is initialized with the data attached from the datasource (object or array).
* 2. When data from a request is submitted using {@link Form::submit()} to transform the new input data
* back into the renderable format. For example if you have a date field and submit '2009-10-10'
* you might accept this value because its easily parsed, but the transformer still writes back
* "2009/10/10" onto the form field (for further displaying or other purposes).
*
* This method must be able to deal with empty values. Usually this will
* be NULL, but depending on your implementation other empty values are
* possible as well (such as empty strings). The reasoning behind this is
* that value transformers must be chainable. If the transform() method
* of the first value transformer outputs NULL, the second value transformer
* must be able to process that value.
*
* By convention, transform() should return an empty string if NULL is
* passed.
*
* @param mixed $value The value in the original representation
*
* @return mixed The value in the transformed representation
*
* @throws TransformationFailedException When the transformation fails.
*/
public function transform($value);
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* This method is called when {@link Form::submit()} is called to transform the requests tainted data
* into an acceptable format for your data processing/model layer.
*
* This method must be able to deal with empty values. Usually this will
* be an empty string, but depending on your implementation other empty
* values are possible as well (such as NULL). The reasoning behind
* this is that value transformers must be chainable. If the
* reverseTransform() method of the first value transformer outputs an
* empty string, the second value transformer must be able to process that
* value.
*
* By convention, reverseTransform() should return NULL if an empty string
* is passed.
*
* @param mixed $value The value in the transformed representation
*
* @return mixed The value in the original representation
*
* @throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($value);
}

View File

@@ -0,0 +1,109 @@
<?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\Form\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds all services with the tags "form.type", "form.type_extension" and
* "form.type_guesser" as arguments of the "form.extension" service.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
private $formExtensionService;
private $formTypeTag;
private $formTypeExtensionTag;
private $formTypeGuesserTag;
public function __construct($formExtensionService = 'form.extension', $formTypeTag = 'form.type', $formTypeExtensionTag = 'form.type_extension', $formTypeGuesserTag = 'form.type_guesser')
{
$this->formExtensionService = $formExtensionService;
$this->formTypeTag = $formTypeTag;
$this->formTypeExtensionTag = $formTypeExtensionTag;
$this->formTypeGuesserTag = $formTypeGuesserTag;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->formExtensionService)) {
return;
}
$definition = $container->getDefinition($this->formExtensionService);
if (new IteratorArgument(array()) != $definition->getArgument(2)) {
return;
}
$definition->replaceArgument(0, $this->processFormTypes($container, $definition));
$definition->replaceArgument(1, $this->processFormTypeExtensions($container));
$definition->replaceArgument(2, $this->processFormTypeGuessers($container));
}
private function processFormTypes(ContainerBuilder $container, Definition $definition)
{
// Get service locator argument
$servicesMap = array();
// Builds an array with fully-qualified type class names as keys and service IDs as values
foreach ($container->findTaggedServiceIds($this->formTypeTag, true) as $serviceId => $tag) {
// Add form type service to the service locator
$serviceDefinition = $container->getDefinition($serviceId);
$servicesMap[$serviceDefinition->getClass()] = new Reference($serviceId);
}
return ServiceLocatorTagPass::register($container, $servicesMap);
}
private function processFormTypeExtensions(ContainerBuilder $container)
{
$typeExtensions = array();
foreach ($this->findAndSortTaggedServices($this->formTypeExtensionTag, $container) as $reference) {
$serviceId = (string) $reference;
$serviceDefinition = $container->getDefinition($serviceId);
$tag = $serviceDefinition->getTag($this->formTypeExtensionTag);
if (isset($tag[0]['extended_type'])) {
$extendedType = $tag[0]['extended_type'];
} else {
throw new InvalidArgumentException(sprintf('"%s" tagged services must have the extended type configured using the extended_type/extended-type attribute, none was configured for the "%s" service.', $this->formTypeExtensionTag, $serviceId));
}
$typeExtensions[$extendedType][] = new Reference($serviceId);
}
foreach ($typeExtensions as $extendedType => $extensions) {
$typeExtensions[$extendedType] = new IteratorArgument($extensions);
}
return $typeExtensions;
}
private function processFormTypeGuessers(ContainerBuilder $container)
{
$guessers = array();
foreach ($container->findTaggedServiceIds($this->formTypeGuesserTag, true) as $serviceId => $tags) {
$guessers[] = new Reference($serviceId);
}
return new IteratorArgument($guessers);
}
}

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\Form\Exception;
/**
* Thrown when an operation is called that is not acceptable after submitting
* a form.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class AlreadySubmittedException extends LogicException
{
}

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\Form\Exception;
/**
* Base BadMethodCallException for the Form component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}

View File

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

View File

@@ -0,0 +1,16 @@
<?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\Form\Exception;
class InvalidConfigurationException extends InvalidArgumentException
{
}

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\Form\Exception;
/**
* Base LogicException for Form component.
*
* @author Alexander Kotynia <aleksander.kot@gmail.com>
*/
class LogicException 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\Form\Exception;
/**
* Base OutOfBoundsException for Form component.
*
* @author Alexander Kotynia <aleksander.kot@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\Form\Exception;
/**
* Base RuntimeException for the Form component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?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\Form\Exception;
class StringCastException 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\Form\Exception;
/**
* Indicates a value transformation error.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TransformationFailedException extends RuntimeException
{
}

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\Form\Exception;
class UnexpectedTypeException extends InvalidArgumentException
{
public function __construct($value, $expectedType)
{
parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value)));
}
}

View File

@@ -0,0 +1,82 @@
<?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\Form\Extension\Core;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Represents the main form extension, which loads the core functionality.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CoreExtension extends AbstractExtension
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
}
protected function loadTypes()
{
return array(
new Type\FormType($this->propertyAccessor),
new Type\BirthdayType(),
new Type\CheckboxType(),
new Type\ChoiceType($this->choiceListFactory),
new Type\CollectionType(),
new Type\CountryType(),
new Type\DateIntervalType(),
new Type\DateType(),
new Type\DateTimeType(),
new Type\EmailType(),
new Type\HiddenType(),
new Type\IntegerType(),
new Type\LanguageType(),
new Type\LocaleType(),
new Type\MoneyType(),
new Type\NumberType(),
new Type\PasswordType(),
new Type\PercentType(),
new Type\RadioType(),
new Type\RangeType(),
new Type\RepeatedType(),
new Type\SearchType(),
new Type\TextareaType(),
new Type\TextType(),
new Type\TimeType(),
new Type\TimezoneType(),
new Type\UrlType(),
new Type\FileType(),
new Type\ButtonType(),
new Type\SubmitType(),
new Type\ResetType(),
new Type\CurrencyType(),
);
}
}

View File

@@ -0,0 +1,67 @@
<?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\Form\Extension\Core\DataMapper;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Maps choices to/from checkbox forms.
*
* A {@link ChoiceListInterface} implementation is used to find the
* corresponding string values for the choices. Each checkbox form whose "value"
* option corresponds to any of the selected values is marked as selected.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CheckboxListMapper implements DataMapperInterface
{
/**
* {@inheritdoc}
*/
public function mapDataToForms($choices, $checkboxes)
{
if (null === $choices) {
$choices = array();
}
if (!is_array($choices)) {
throw new UnexpectedTypeException($choices, 'array');
}
foreach ($checkboxes as $checkbox) {
$value = $checkbox->getConfig()->getOption('value');
$checkbox->setData(in_array($value, $choices, true));
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData($checkboxes, &$choices)
{
if (!is_array($choices)) {
throw new UnexpectedTypeException($choices, 'array');
}
$values = array();
foreach ($checkboxes as $checkbox) {
if ($checkbox->getData()) {
// construct an array of choice values
$values[] = $checkbox->getConfig()->getOption('value');
}
}
$choices = $values;
}
}

View File

@@ -0,0 +1,98 @@
<?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\Form\Extension\Core\DataMapper;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Maps arrays/objects to/from forms using property paths.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathMapper implements DataMapperInterface
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* Creates a new property path mapper.
*
* @param PropertyAccessorInterface $propertyAccessor The property accessor
*/
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* {@inheritdoc}
*/
public function mapDataToForms($data, $forms)
{
$empty = null === $data || array() === $data;
if (!$empty && !is_array($data) && !is_object($data)) {
throw new UnexpectedTypeException($data, 'object, array or empty');
}
foreach ($forms as $form) {
$propertyPath = $form->getPropertyPath();
$config = $form->getConfig();
if (!$empty && null !== $propertyPath && $config->getMapped()) {
$form->setData($this->propertyAccessor->getValue($data, $propertyPath));
} else {
$form->setData($form->getConfig()->getData());
}
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData($forms, &$data)
{
if (null === $data) {
return;
}
if (!is_array($data) && !is_object($data)) {
throw new UnexpectedTypeException($data, 'object, array or empty');
}
foreach ($forms as $form) {
$propertyPath = $form->getPropertyPath();
$config = $form->getConfig();
// Write-back is disabled if the form is not synchronized (transformation failed),
// if the form was not submitted and if the form is disabled (modification not allowed)
if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
// If the field is of type DateTime and the data is the same skip the update to
// keep the original object hash
if ($form->getData() instanceof \DateTime && $form->getData() == $this->propertyAccessor->getValue($data, $propertyPath)) {
continue;
}
// If the data is identical to the value in $data, we are
// dealing with a reference
if (!is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
}
}
}
}
}

View File

@@ -0,0 +1,66 @@
<?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\Form\Extension\Core\DataMapper;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Maps choices to/from radio forms.
*
* A {@link ChoiceListInterface} implementation is used to find the
* corresponding string values for the choices. The radio form whose "value"
* option corresponds to the selected value is marked as selected.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RadioListMapper implements DataMapperInterface
{
/**
* {@inheritdoc}
*/
public function mapDataToForms($choice, $radios)
{
if (!is_string($choice)) {
throw new UnexpectedTypeException($choice, 'string');
}
foreach ($radios as $radio) {
$value = $radio->getConfig()->getOption('value');
$radio->setData($choice === $value);
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData($radios, &$choice)
{
if (null !== $choice && !is_string($choice)) {
throw new UnexpectedTypeException($choice, 'null or string');
}
$choice = null;
foreach ($radios as $radio) {
if ($radio->getData()) {
if ('placeholder' === $radio->getName()) {
return;
}
$choice = $radio->getConfig()->getOption('value');
return;
}
}
}
}

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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ArrayToPartsTransformer implements DataTransformerInterface
{
private $partMapping;
public function __construct(array $partMapping)
{
$this->partMapping = $partMapping;
}
public function transform($array)
{
if (null === $array) {
$array = array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = array();
foreach ($this->partMapping as $partKey => $originalKeys) {
if (empty($array)) {
$result[$partKey] = null;
} else {
$result[$partKey] = array_intersect_key($array, array_flip($originalKeys));
}
}
return $result;
}
public function reverseTransform($array)
{
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = array();
$emptyKeys = array();
foreach ($this->partMapping as $partKey => $originalKeys) {
if (!empty($array[$partKey])) {
foreach ($originalKeys as $originalKey) {
if (isset($array[$partKey][$originalKey])) {
$result[$originalKey] = $array[$partKey][$originalKey];
}
}
} else {
$emptyKeys[] = $partKey;
}
}
if (count($emptyKeys) > 0) {
if (count($emptyKeys) === count($this->partMapping)) {
// All parts empty
return;
}
throw new TransformationFailedException(
sprintf('The keys "%s" should not be empty', implode('", "', $emptyKeys)
));
}
return $result;
}
}

View File

@@ -0,0 +1,67 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
abstract class BaseDateTimeTransformer implements DataTransformerInterface
{
protected static $formats = array(
\IntlDateFormatter::NONE,
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
);
protected $inputTimezone;
protected $outputTimezone;
/**
* Constructor.
*
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
*
* @throws UnexpectedTypeException if a timezone is not a string
* @throws InvalidArgumentException if a timezone is not valid
*/
public function __construct($inputTimezone = null, $outputTimezone = null)
{
if (null !== $inputTimezone && !is_string($inputTimezone)) {
throw new UnexpectedTypeException($inputTimezone, 'string');
}
if (null !== $outputTimezone && !is_string($outputTimezone)) {
throw new UnexpectedTypeException($outputTimezone, 'string');
}
$this->inputTimezone = $inputTimezone ?: date_default_timezone_get();
$this->outputTimezone = $outputTimezone ?: date_default_timezone_get();
// Check if input and output timezones are valid
try {
new \DateTimeZone($this->inputTimezone);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Input timezone is invalid: %s.', $this->inputTimezone), $e->getCode(), $e);
}
try {
new \DateTimeZone($this->outputTimezone);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Output timezone is invalid: %s.', $this->outputTimezone), $e->getCode(), $e);
}
}
}

View File

@@ -0,0 +1,85 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforms between a Boolean and a string.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class BooleanToStringTransformer implements DataTransformerInterface
{
/**
* The value emitted upon transform if the input is true.
*
* @var string
*/
private $trueValue;
/**
* Sets the value emitted upon transform if the input is true.
*
* @param string $trueValue
*/
public function __construct($trueValue)
{
$this->trueValue = $trueValue;
}
/**
* Transforms a Boolean into a string.
*
* @param bool $value Boolean value
*
* @return string String value
*
* @throws TransformationFailedException If the given value is not a Boolean.
*/
public function transform($value)
{
if (null === $value) {
return;
}
if (!is_bool($value)) {
throw new TransformationFailedException('Expected a Boolean.');
}
return $value ? $this->trueValue : null;
}
/**
* Transforms a string into a Boolean.
*
* @param string $value String value
*
* @return bool Boolean value
*
* @throws TransformationFailedException If the given value is not a string.
*/
public function reverseTransform($value)
{
if (null === $value) {
return false;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return true;
}
}

View File

@@ -0,0 +1,58 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceToValueTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function transform($choice)
{
return (string) current($this->choiceList->getValuesForChoices(array($choice)));
}
public function reverseTransform($value)
{
if (null !== $value && !is_string($value)) {
throw new TransformationFailedException('Expected a string or null.');
}
$choices = $this->choiceList->getChoicesForValues(array((string) $value));
if (1 !== count($choices)) {
if (null === $value || '' === $value) {
return;
}
throw new TransformationFailedException(sprintf('The choice "%s" does not exist or is not unique', $value));
}
return current($choices);
}
}

View File

@@ -0,0 +1,82 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoicesToValuesTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* @param array $array
*
* @return array
*
* @throws TransformationFailedException If the given value is not an array.
*/
public function transform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
return $this->choiceList->getValuesForChoices($array);
}
/**
* @param array $array
*
* @return array
*
* @throws TransformationFailedException If the given value is not an array
* or if no matching choice could be
* found for some given value.
*/
public function reverseTransform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$choices = $this->choiceList->getChoicesForValues($array);
if (count($choices) !== count($array)) {
throw new TransformationFailedException('Could not find all matching choices for the given values');
}
return $choices;
}
}

View File

@@ -0,0 +1,95 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Passes a value through multiple value transformers.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DataTransformerChain implements DataTransformerInterface
{
/**
* The value transformers.
*
* @var DataTransformerInterface[]
*/
protected $transformers;
/**
* Uses the given value transformers to transform values.
*
* @param array $transformers
*/
public function __construct(array $transformers)
{
$this->transformers = $transformers;
}
/**
* Passes the value through the transform() method of all nested transformers.
*
* The transformers receive the value in the same order as they were passed
* to the constructor. Each transformer receives the result of the previous
* transformer as input. The output of the last transformer is returned
* by this method.
*
* @param mixed $value The original value
*
* @return mixed The transformed value
*
* @throws TransformationFailedException
*/
public function transform($value)
{
foreach ($this->transformers as $transformer) {
$value = $transformer->transform($value);
}
return $value;
}
/**
* Passes the value through the reverseTransform() method of all nested
* transformers.
*
* The transformers receive the value in the reverse order as they were passed
* to the constructor. Each transformer receives the result of the previous
* transformer as input. The output of the last transformer is returned
* by this method.
*
* @param mixed $value The transformed value
*
* @return mixed The reverse-transformed value
*
* @throws TransformationFailedException
*/
public function reverseTransform($value)
{
for ($i = count($this->transformers) - 1; $i >= 0; --$i) {
$value = $this->transformers[$i]->reverseTransform($value);
}
return $value;
}
/**
* @return DataTransformerInterface[]
*/
public function getTransformers()
{
return $this->transformers;
}
}

View File

@@ -0,0 +1,174 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a normalized date interval and an interval string/array.
*
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalToArrayTransformer implements DataTransformerInterface
{
const YEARS = 'years';
const MONTHS = 'months';
const DAYS = 'days';
const HOURS = 'hours';
const MINUTES = 'minutes';
const SECONDS = 'seconds';
const INVERT = 'invert';
private static $availableFields = array(
self::YEARS => 'y',
self::MONTHS => 'm',
self::DAYS => 'd',
self::HOURS => 'h',
self::MINUTES => 'i',
self::SECONDS => 's',
self::INVERT => 'r',
);
private $fields;
private $pad;
/**
* @param string[] $fields The date fields
* @param bool $pad Whether to use padding
*/
public function __construct(array $fields = null, $pad = false)
{
if (null === $fields) {
$fields = array('years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert');
}
$this->fields = $fields;
$this->pad = (bool) $pad;
}
/**
* Transforms a normalized date interval into an interval array.
*
* @param \DateInterval $dateInterval Normalized date interval
*
* @return array Interval array
*
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
*/
public function transform($dateInterval)
{
if (null === $dateInterval) {
return array_intersect_key(
array(
'years' => '',
'months' => '',
'weeks' => '',
'days' => '',
'hours' => '',
'minutes' => '',
'seconds' => '',
'invert' => false,
),
array_flip($this->fields)
);
}
if (!$dateInterval instanceof \DateInterval) {
throw new UnexpectedTypeException($dateInterval, '\DateInterval');
}
$result = array();
foreach (self::$availableFields as $field => $char) {
$result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
}
if (in_array('weeks', $this->fields, true)) {
$result['weeks'] = 0;
if (isset($result['days']) && (int) $result['days'] >= 7) {
$result['weeks'] = (string) floor($result['days'] / 7);
$result['days'] = (string) ($result['days'] % 7);
}
}
$result['invert'] = '-' === $result['invert'];
$result = array_intersect_key($result, array_flip($this->fields));
return $result;
}
/**
* Transforms an interval array into a normalized date interval.
*
* @param array $value Interval array
*
* @return \DateInterval Normalized date interval
*
* @throws UnexpectedTypeException If the given value is not an array.
* @throws TransformationFailedException If the value could not be transformed.
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
if (!is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
}
if ('' === implode('', $value)) {
return;
}
$emptyFields = array();
foreach ($this->fields as $field) {
if (!isset($value[$field])) {
$emptyFields[] = $field;
}
}
if (count($emptyFields) > 0) {
throw new TransformationFailedException(sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields)));
}
if (isset($value['invert']) && !is_bool($value['invert'])) {
throw new TransformationFailedException('The value of "invert" must be boolean');
}
foreach (self::$availableFields as $field => $char) {
if ($field !== 'invert' && isset($value[$field]) && !ctype_digit((string) $value[$field])) {
throw new TransformationFailedException(sprintf('This amount of "%s" is invalid', $field));
}
}
try {
if (!empty($value['weeks'])) {
$interval = sprintf(
'P%sY%sM%sWT%sH%sM%sS',
empty($value['years']) ? '0' : $value['years'],
empty($value['months']) ? '0' : $value['months'],
empty($value['weeks']) ? '0' : $value['weeks'],
empty($value['hours']) ? '0' : $value['hours'],
empty($value['minutes']) ? '0' : $value['minutes'],
empty($value['seconds']) ? '0' : $value['seconds']
);
} else {
$interval = sprintf(
'P%sY%sM%sDT%sH%sM%sS',
empty($value['years']) ? '0' : $value['years'],
empty($value['months']) ? '0' : $value['months'],
empty($value['days']) ? '0' : $value['days'],
empty($value['hours']) ? '0' : $value['hours'],
empty($value['minutes']) ? '0' : $value['minutes'],
empty($value['seconds']) ? '0' : $value['seconds']
);
}
$dateInterval = new \DateInterval($interval);
if (isset($value['invert'])) {
$dateInterval->invert = $value['invert'] ? 1 : 0;
}
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
return $dateInterval;
}
}

View File

@@ -0,0 +1,104 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a date string and a DateInterval object.
*
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalToStringTransformer implements DataTransformerInterface
{
private $format;
private $parseSigned;
/**
* Transforms a \DateInterval instance to a string.
*
* @see \DateInterval::format() for supported formats
*
* @param string $format The date format
* @param bool $parseSigned Whether to parse as a signed interval
*/
public function __construct($format = 'P%yY%mM%dDT%hH%iM%sS', $parseSigned = false)
{
$this->format = $format;
$this->parseSigned = $parseSigned;
}
/**
* Transforms a DateInterval object into a date string with the configured format.
*
* @param \DateInterval $value A DateInterval object
*
* @return string An ISO 8601 or relative date string like date interval presentation
*
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
*/
public function transform($value)
{
if (null === $value) {
return '';
}
if (!$value instanceof \DateInterval) {
throw new UnexpectedTypeException($value, '\DateInterval');
}
return $value->format($this->format);
}
/**
* Transforms a date string in the configured format into a DateInterval object.
*
* @param string $value An ISO 8601 or date string like date interval presentation
*
* @return \DateInterval An instance of \DateInterval
*
* @throws UnexpectedTypeException If the given value is not a string.
* @throws TransformationFailedException If the date interval could not be parsed.
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
if (!is_string($value)) {
throw new UnexpectedTypeException($value, 'string');
}
if ('' === $value) {
return;
}
if (!$this->isISO8601($value)) {
throw new TransformationFailedException('Non ISO 8601 date strings are not supported yet');
}
$valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/';
if (!preg_match($valuePattern, $value)) {
throw new TransformationFailedException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format));
}
try {
$dateInterval = new \DateInterval($value);
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
return $dateInterval;
}
private function isISO8601($string)
{
return preg_match('/^P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
}
}

View File

@@ -0,0 +1,194 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a normalized time and a localized time string/array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToArrayTransformer extends BaseDateTimeTransformer
{
private $pad;
private $fields;
/**
* Constructor.
*
* @param string $inputTimezone The input timezone
* @param string $outputTimezone The output timezone
* @param array $fields The date fields
* @param bool $pad Whether to use padding
*
* @throws UnexpectedTypeException if a timezone is not a string
*/
public function __construct($inputTimezone = null, $outputTimezone = null, array $fields = null, $pad = false)
{
parent::__construct($inputTimezone, $outputTimezone);
if (null === $fields) {
$fields = array('year', 'month', 'day', 'hour', 'minute', 'second');
}
$this->fields = $fields;
$this->pad = (bool) $pad;
}
/**
* Transforms a normalized date into a localized date.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return array Localized date
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return array_intersect_key(array(
'year' => '',
'month' => '',
'day' => '',
'hour' => '',
'minute' => '',
'second' => '',
), array_flip($this->fields));
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
if ($this->inputTimezone !== $this->outputTimezone) {
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $dateTime;
}
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
}
$result = array_intersect_key(array(
'year' => $dateTime->format('Y'),
'month' => $dateTime->format('m'),
'day' => $dateTime->format('d'),
'hour' => $dateTime->format('H'),
'minute' => $dateTime->format('i'),
'second' => $dateTime->format('s'),
), array_flip($this->fields));
if (!$this->pad) {
foreach ($result as &$entry) {
// remove leading zeros
$entry = (string) (int) $entry;
}
// unset reference to keep scope clear
unset($entry);
}
return $result;
}
/**
* Transforms a localized date into a normalized date.
*
* @param array $value Localized date
*
* @return \DateTime Normalized date
*
* @throws TransformationFailedException If the given value is not an array,
* if the value could not be transformed
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
if (!is_array($value)) {
throw new TransformationFailedException('Expected an array.');
}
if ('' === implode('', $value)) {
return;
}
$emptyFields = array();
foreach ($this->fields as $field) {
if (!isset($value[$field])) {
$emptyFields[] = $field;
}
}
if (count($emptyFields) > 0) {
throw new TransformationFailedException(
sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields)
));
}
if (isset($value['month']) && !ctype_digit((string) $value['month'])) {
throw new TransformationFailedException('This month is invalid');
}
if (isset($value['day']) && !ctype_digit((string) $value['day'])) {
throw new TransformationFailedException('This day is invalid');
}
if (isset($value['year']) && !ctype_digit((string) $value['year'])) {
throw new TransformationFailedException('This year is invalid');
}
if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) {
throw new TransformationFailedException('This is an invalid date');
}
if (isset($value['hour']) && !ctype_digit((string) $value['hour'])) {
throw new TransformationFailedException('This hour is invalid');
}
if (isset($value['minute']) && !ctype_digit((string) $value['minute'])) {
throw new TransformationFailedException('This minute is invalid');
}
if (isset($value['second']) && !ctype_digit((string) $value['second'])) {
throw new TransformationFailedException('This second is invalid');
}
try {
$dateTime = new \DateTime(sprintf(
'%s-%s-%s %s:%s:%s',
empty($value['year']) ? '1970' : $value['year'],
empty($value['month']) ? '1' : $value['month'],
empty($value['day']) ? '1' : $value['day'],
empty($value['hour']) ? '0' : $value['hour'],
empty($value['minute']) ? '0' : $value['minute'],
empty($value['second']) ? '0' : $value['second']
),
new \DateTimeZone($this->outputTimezone)
);
if ($this->inputTimezone !== $this->outputTimezone) {
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
}
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
return $dateTime;
}
}

View File

@@ -0,0 +1,201 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a normalized time and a localized time string.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
{
private $dateFormat;
private $timeFormat;
private $pattern;
private $calendar;
/**
* Constructor.
*
* @see BaseDateTimeTransformer::formats for available format options
*
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
* @param int $dateFormat The date format
* @param int $timeFormat The time format
* @param int $calendar One of the \IntlDateFormatter calendar constants
* @param string $pattern A pattern to pass to \IntlDateFormatter
*
* @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string
*/
public function __construct($inputTimezone = null, $outputTimezone = null, $dateFormat = null, $timeFormat = null, $calendar = \IntlDateFormatter::GREGORIAN, $pattern = null)
{
parent::__construct($inputTimezone, $outputTimezone);
if (null === $dateFormat) {
$dateFormat = \IntlDateFormatter::MEDIUM;
}
if (null === $timeFormat) {
$timeFormat = \IntlDateFormatter::SHORT;
}
if (!in_array($dateFormat, self::$formats, true)) {
throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats));
}
if (!in_array($timeFormat, self::$formats, true)) {
throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats));
}
$this->dateFormat = $dateFormat;
$this->timeFormat = $timeFormat;
$this->calendar = $calendar;
$this->pattern = $pattern;
}
/**
* Transforms a normalized date into a localized date string/array.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return string|array Localized date string/array
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
* or if the date could not be transformed.
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
$value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp());
if (intl_get_error_code() != 0) {
throw new TransformationFailedException(intl_get_error_message());
}
return $value;
}
/**
* Transforms a localized date string/array into a normalized date.
*
* @param string|array $value Localized date string/array
*
* @return \DateTime Normalized date
*
* @throws TransformationFailedException if the given value is not a string,
* if the date could not be parsed
*/
public function reverseTransform($value)
{
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $value) {
return;
}
// date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due
// to DST changes
$dateOnly = $this->isPatternDateOnly();
$timestamp = $this->getIntlDateFormatter($dateOnly)->parse($value);
if (intl_get_error_code() != 0) {
throw new TransformationFailedException(intl_get_error_message());
}
try {
if ($dateOnly) {
// we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight
$dateTime = new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone));
} else {
// read timestamp into DateTime object - the formatter delivers a timestamp
$dateTime = new \DateTime(sprintf('@%s', $timestamp));
}
// set timezone separately, as it would be ignored if set via the constructor,
// see http://php.net/manual/en/datetime.construct.php
$dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
if ($this->outputTimezone !== $this->inputTimezone) {
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
}
return $dateTime;
}
/**
* Returns a preconfigured IntlDateFormatter instance.
*
* @param bool $ignoreTimezone Use UTC regardless of the configured timezone.
*
* @return \IntlDateFormatter
*
* @throws TransformationFailedException in case the date formatter can not be constructed.
*/
protected function getIntlDateFormatter($ignoreTimezone = false)
{
$dateFormat = $this->dateFormat;
$timeFormat = $this->timeFormat;
$timezone = $ignoreTimezone ? 'UTC' : $this->outputTimezone;
if (class_exists('IntlTimeZone', false)) {
// see https://bugs.php.net/bug.php?id=66323
$timezone = \IntlTimeZone::createTimeZone($timezone);
}
$calendar = $this->calendar;
$pattern = $this->pattern;
$intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern);
// new \intlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/bug.php?id=66323
if (!$intlDateFormatter) {
throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
}
$intlDateFormatter->setLenient(false);
return $intlDateFormatter;
}
/**
* Checks if the pattern contains only a date.
*
* @return bool
*/
protected function isPatternDateOnly()
{
if (null === $this->pattern) {
return false;
}
// strip escaped text
$pattern = preg_replace("#'(.*?)'#", '', $this->pattern);
// check for the absence of time-related placeholders
return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern);
}
}

View File

@@ -0,0 +1,94 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
{
/**
* Transforms a normalized date into a localized date.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return string The formatted date
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
if ($this->inputTimezone !== $this->outputTimezone) {
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $dateTime;
}
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
}
return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c'));
}
/**
* Transforms a formatted string following RFC 3339 into a normalized date.
*
* @param string $rfc3339 Formatted string
*
* @return \DateTime Normalized date
*
* @throws TransformationFailedException If the given value is not a string,
* if the value could not be transformed
*/
public function reverseTransform($rfc3339)
{
if (!is_string($rfc3339)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $rfc3339) {
return;
}
try {
$dateTime = new \DateTime($rfc3339);
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
}
if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $rfc3339, $matches)) {
if (!checkdate($matches[2], $matches[3], $matches[1])) {
throw new TransformationFailedException(sprintf(
'The date "%s-%s-%s" is not a valid date.',
$matches[1],
$matches[2],
$matches[3]
));
}
}
return $dateTime;
}
}

View File

@@ -0,0 +1,146 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a date string and a DateTime object.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToStringTransformer extends BaseDateTimeTransformer
{
/**
* Format used for generating strings.
*
* @var string
*/
private $generateFormat;
/**
* Format used for parsing strings.
*
* Different than the {@link $generateFormat} because formats for parsing
* support additional characters in PHP that are not supported for
* generating strings.
*
* @var string
*/
private $parseFormat;
/**
* Transforms a \DateTime instance to a string.
*
* @see \DateTime::format() for supported formats
*
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
* @param string $format The date format
*
* @throws UnexpectedTypeException if a timezone is not a string
*/
public function __construct($inputTimezone = null, $outputTimezone = null, $format = 'Y-m-d H:i:s')
{
parent::__construct($inputTimezone, $outputTimezone);
$this->generateFormat = $this->parseFormat = $format;
// See http://php.net/manual/en/datetime.createfromformat.php
// The character "|" in the format makes sure that the parts of a date
// that are *not* specified in the format are reset to the corresponding
// values from 1970-01-01 00:00:00 instead of the current time.
// Without "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 12:32:47",
// where the time corresponds to the current server time.
// With "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 00:00:00",
// which is at least deterministic and thus used here.
if (false === strpos($this->parseFormat, '|')) {
$this->parseFormat .= '|';
}
}
/**
* Transforms a DateTime object into a date string with the configured format
* and timezone.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return string A value as produced by PHP's date() function
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $dateTime;
}
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
return $dateTime->format($this->generateFormat);
}
/**
* Transforms a date string in the configured timezone into a DateTime object.
*
* @param string $value A value as produced by PHP's date() function
*
* @return \DateTime An instance of \DateTime
*
* @throws TransformationFailedException If the given value is not a string,
* or could not be transformed
*/
public function reverseTransform($value)
{
if (empty($value)) {
return;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
$outputTz = new \DateTimeZone($this->outputTimezone);
$dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
$lastErrors = \DateTime::getLastErrors();
if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) {
throw new TransformationFailedException(
implode(', ', array_merge(
array_values($lastErrors['warnings']),
array_values($lastErrors['errors'])
))
);
}
try {
if ($this->inputTimezone !== $this->outputTimezone) {
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
}
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
return $dateTime;
}
}

View File

@@ -0,0 +1,80 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforms between a timestamp and a DateTime object.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToTimestampTransformer extends BaseDateTimeTransformer
{
/**
* Transforms a DateTime object into a timestamp in the configured timezone.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return int A timestamp
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return;
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
return $dateTime->getTimestamp();
}
/**
* Transforms a timestamp in the configured timezone into a DateTime object.
*
* @param string $value A timestamp
*
* @return \DateTime A \DateTime object
*
* @throws TransformationFailedException If the given value is not a timestamp
* or if the given timestamp is invalid
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
try {
$dateTime = new \DateTime();
$dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
$dateTime->setTimestamp($value);
if ($this->inputTimezone !== $this->outputTimezone) {
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
}
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
return $dateTime;
}
}

View File

@@ -0,0 +1,47 @@
<?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\Form\Extension\Core\DataTransformer;
/**
* Transforms between an integer and a localized number with grouping
* (each thousand) and comma separators.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
{
/**
* Constructs a transformer.
*
* @param int $scale Unused
* @param bool $grouping Whether thousands should be grouped
* @param int $roundingMode One of the ROUND_ constants in this class
*/
public function __construct($scale = 0, $grouping = false, $roundingMode = self::ROUND_DOWN)
{
if (null === $roundingMode) {
$roundingMode = self::ROUND_DOWN;
}
parent::__construct(0, $grouping, $roundingMode);
}
/**
* {@inheritdoc}
*/
public function reverseTransform($value)
{
$result = parent::reverseTransform($value);
return null !== $result ? (int) $result : null;
}
}

View File

@@ -0,0 +1,88 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforms between a normalized format and a localized money string.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
{
private $divisor;
public function __construct($scale = 2, $grouping = true, $roundingMode = self::ROUND_HALF_UP, $divisor = 1)
{
if (null === $grouping) {
$grouping = true;
}
if (null === $scale) {
$scale = 2;
}
parent::__construct($scale, $grouping, $roundingMode);
if (null === $divisor) {
$divisor = 1;
}
$this->divisor = $divisor;
}
/**
* Transforms a normalized format into a localized money string.
*
* @param int|float $value Normalized number
*
* @return string Localized money string
*
* @throws TransformationFailedException If the given value is not numeric or
* if the value can not be transformed.
*/
public function transform($value)
{
if (null !== $value) {
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
$value /= $this->divisor;
}
return parent::transform($value);
}
/**
* Transforms a localized money string into a normalized format.
*
* @param string $value Localized money string
*
* @return int|float Normalized number
*
* @throws TransformationFailedException If the given value is not a string
* or if the value can not be transformed.
*/
public function reverseTransform($value)
{
$value = parent::reverseTransform($value);
if (null !== $value) {
$value *= $this->divisor;
}
return $value;
}
}

View File

@@ -0,0 +1,277 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforms between a number type and a localized number with grouping
* (each thousand) and comma separators.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class NumberToLocalizedStringTransformer implements DataTransformerInterface
{
/**
* Rounds a number towards positive infinity.
*
* Rounds 1.4 to 2 and -1.4 to -1.
*/
const ROUND_CEILING = \NumberFormatter::ROUND_CEILING;
/**
* Rounds a number towards negative infinity.
*
* Rounds 1.4 to 1 and -1.4 to -2.
*/
const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR;
/**
* Rounds a number away from zero.
*
* Rounds 1.4 to 2 and -1.4 to -2.
*/
const ROUND_UP = \NumberFormatter::ROUND_UP;
/**
* Rounds a number towards zero.
*
* Rounds 1.4 to 1 and -1.4 to -1.
*/
const ROUND_DOWN = \NumberFormatter::ROUND_DOWN;
/**
* Rounds to the nearest number and halves to the next even number.
*
* Rounds 2.5, 1.6 and 1.5 to 2 and 1.4 to 1.
*/
const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN;
/**
* Rounds to the nearest number and halves away from zero.
*
* Rounds 2.5 to 3, 1.6 and 1.5 to 2 and 1.4 to 1.
*/
const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP;
/**
* Rounds to the nearest number and halves towards zero.
*
* Rounds 2.5 and 1.6 to 2, 1.5 and 1.4 to 1.
*/
const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN;
protected $grouping;
protected $roundingMode;
private $scale;
public function __construct($scale = null, $grouping = false, $roundingMode = self::ROUND_HALF_UP)
{
if (null === $grouping) {
$grouping = false;
}
if (null === $roundingMode) {
$roundingMode = self::ROUND_HALF_UP;
}
$this->scale = $scale;
$this->grouping = $grouping;
$this->roundingMode = $roundingMode;
}
/**
* Transforms a number type into localized number.
*
* @param int|float $value Number value
*
* @return string Localized value
*
* @throws TransformationFailedException If the given value is not numeric
* or if the value can not be transformed.
*/
public function transform($value)
{
if (null === $value) {
return '';
}
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
$formatter = $this->getNumberFormatter();
$value = $formatter->format($value);
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
// Convert fixed spaces to normal ones
$value = str_replace("\xc2\xa0", ' ', $value);
return $value;
}
/**
* Transforms a localized number into an integer or float.
*
* @param string $value The localized value
*
* @return int|float The numeric value
*
* @throws TransformationFailedException If the given value is not a string
* or if the value can not be transformed.
*/
public function reverseTransform($value)
{
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $value) {
return;
}
if ('NaN' === $value) {
throw new TransformationFailedException('"NaN" is not a valid number');
}
$position = 0;
$formatter = $this->getNumberFormatter();
$groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
$decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) {
$value = str_replace('.', $decSep, $value);
}
if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) {
$value = str_replace(',', $decSep, $value);
}
if (false !== strpos($value, $decSep)) {
$type = \NumberFormatter::TYPE_DOUBLE;
} else {
$type = PHP_INT_SIZE === 8
? \NumberFormatter::TYPE_INT64
: \NumberFormatter::TYPE_INT32;
}
$result = $formatter->parse($value, $type, $position);
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
if ($result >= PHP_INT_MAX || $result <= -PHP_INT_MAX) {
throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like');
}
if (is_int($result) && $result === (int) $float = (float) $result) {
$result = $float;
}
if (false !== $encoding = mb_detect_encoding($value, null, true)) {
$length = mb_strlen($value, $encoding);
$remainder = mb_substr($value, $position, $length, $encoding);
} else {
$length = strlen($value);
$remainder = substr($value, $position, $length);
}
// After parsing, position holds the index of the character where the
// parsing stopped
if ($position < $length) {
// Check if there are unrecognized characters at the end of the
// number (excluding whitespace characters)
$remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
if ('' !== $remainder) {
throw new TransformationFailedException(
sprintf('The number contains unrecognized characters: "%s"', $remainder)
);
}
}
// NumberFormatter::parse() does not round
return $this->round($result);
}
/**
* Returns a preconfigured \NumberFormatter instance.
*
* @return \NumberFormatter
*/
protected function getNumberFormatter()
{
$formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
if (null !== $this->scale) {
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}
$formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
return $formatter;
}
/**
* Rounds a number according to the configured scale and rounding mode.
*
* @param int|float $number A number
*
* @return int|float The rounded number
*/
private function round($number)
{
if (null !== $this->scale && null !== $this->roundingMode) {
// shift number to maintain the correct scale during rounding
$roundingCoef = pow(10, $this->scale);
// string representation to avoid rounding errors, similar to bcmul()
$number = (string) ($number * $roundingCoef);
switch ($this->roundingMode) {
case self::ROUND_CEILING:
$number = ceil($number);
break;
case self::ROUND_FLOOR:
$number = floor($number);
break;
case self::ROUND_UP:
$number = $number > 0 ? ceil($number) : floor($number);
break;
case self::ROUND_DOWN:
$number = $number > 0 ? floor($number) : ceil($number);
break;
case self::ROUND_HALF_EVEN:
$number = round($number, 0, PHP_ROUND_HALF_EVEN);
break;
case self::ROUND_HALF_UP:
$number = round($number, 0, PHP_ROUND_HALF_UP);
break;
case self::ROUND_HALF_DOWN:
$number = round($number, 0, PHP_ROUND_HALF_DOWN);
break;
}
$number /= $roundingCoef;
}
return $number;
}
}

View File

@@ -0,0 +1,149 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a normalized format (integer or float) and a percentage value.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class PercentToLocalizedStringTransformer implements DataTransformerInterface
{
const FRACTIONAL = 'fractional';
const INTEGER = 'integer';
protected static $types = array(
self::FRACTIONAL,
self::INTEGER,
);
private $type;
private $scale;
/**
* Constructor.
*
* @see self::$types for a list of supported types
*
* @param int $scale The scale
* @param string $type One of the supported types
*
* @throws UnexpectedTypeException if the given value of type is unknown
*/
public function __construct($scale = null, $type = null)
{
if (null === $scale) {
$scale = 0;
}
if (null === $type) {
$type = self::FRACTIONAL;
}
if (!in_array($type, self::$types, true)) {
throw new UnexpectedTypeException($type, implode('", "', self::$types));
}
$this->type = $type;
$this->scale = $scale;
}
/**
* Transforms between a normalized format (integer or float) into a percentage value.
*
* @param int|float $value Normalized value
*
* @return string Percentage value
*
* @throws TransformationFailedException If the given value is not numeric or
* if the value could not be transformed.
*/
public function transform($value)
{
if (null === $value) {
return '';
}
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
if (self::FRACTIONAL == $this->type) {
$value *= 100;
}
$formatter = $this->getNumberFormatter();
$value = $formatter->format($value);
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
// replace the UTF-8 non break spaces
return $value;
}
/**
* Transforms between a percentage value into a normalized format (integer or float).
*
* @param string $value Percentage value
*
* @return int|float Normalized value
*
* @throws TransformationFailedException If the given value is not a string or
* if the value could not be transformed.
*/
public function reverseTransform($value)
{
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $value) {
return;
}
$formatter = $this->getNumberFormatter();
// replace normal spaces so that the formatter can read them
$value = $formatter->parse(str_replace(' ', "\xc2\xa0", $value));
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
if (self::FRACTIONAL == $this->type) {
$value /= 100;
}
return $value;
}
/**
* Returns a preconfigured \NumberFormatter instance.
*
* @return \NumberFormatter
*/
protected function getNumberFormatter()
{
$formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
return $formatter;
}
}

View File

@@ -0,0 +1,91 @@
<?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\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ValueToDuplicatesTransformer implements DataTransformerInterface
{
private $keys;
public function __construct(array $keys)
{
$this->keys = $keys;
}
/**
* Duplicates the given value through the array.
*
* @param mixed $value The value
*
* @return array The array
*/
public function transform($value)
{
$result = array();
foreach ($this->keys as $key) {
$result[$key] = $value;
}
return $result;
}
/**
* Extracts the duplicated value from an array.
*
* @param array $array
*
* @return mixed The value
*
* @throws TransformationFailedException If the given value is not an array or
* if the given array can not be transformed.
*/
public function reverseTransform($array)
{
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = current($array);
$emptyKeys = array();
foreach ($this->keys as $key) {
if (isset($array[$key]) && '' !== $array[$key] && false !== $array[$key] && array() !== $array[$key]) {
if ($array[$key] !== $result) {
throw new TransformationFailedException(
'All values in the array should be the same'
);
}
} else {
$emptyKeys[] = $key;
}
}
if (count($emptyKeys) > 0) {
if (count($emptyKeys) == count($this->keys)) {
// All keys empty
return;
}
throw new TransformationFailedException(
sprintf('The keys "%s" should not be empty', implode('", "', $emptyKeys)
));
}
return $result;
}
}

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\Form\Extension\Core\EventListener;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Adds a protocol to a URL if it doesn't already have one.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FixUrlProtocolListener implements EventSubscriberInterface
{
private $defaultProtocol;
/**
* Constructor.
*
* @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data
*/
public function __construct($defaultProtocol = 'http')
{
$this->defaultProtocol = $defaultProtocol;
}
public function onSubmit(FormEvent $event)
{
$data = $event->getData();
if ($this->defaultProtocol && $data && !preg_match('~^[\w+.-]+://~', $data)) {
$event->setData($this->defaultProtocol.'://'.$data);
}
}
public static function getSubscribedEvents()
{
return array(FormEvents::SUBMIT => 'onSubmit');
}
}

View File

@@ -0,0 +1,128 @@
<?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\Form\Extension\Core\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MergeCollectionListener implements EventSubscriberInterface
{
/**
* Whether elements may be added to the collection.
*
* @var bool
*/
private $allowAdd;
/**
* Whether elements may be removed from the collection.
*
* @var bool
*/
private $allowDelete;
/**
* Creates a new listener.
*
* @param bool $allowAdd Whether values might be added to the
* collection.
* @param bool $allowDelete Whether values might be removed from the
* collection.
*/
public function __construct($allowAdd = false, $allowDelete = false)
{
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::SUBMIT => 'onSubmit',
);
}
public function onSubmit(FormEvent $event)
{
$dataToMergeInto = $event->getForm()->getNormData();
$data = $event->getData();
if (null === $data) {
$data = array();
}
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
}
if (null !== $dataToMergeInto && !is_array($dataToMergeInto) && !($dataToMergeInto instanceof \Traversable && $dataToMergeInto instanceof \ArrayAccess)) {
throw new UnexpectedTypeException($dataToMergeInto, 'array or (\Traversable and \ArrayAccess)');
}
// If we are not allowed to change anything, return immediately
if ($data === $dataToMergeInto || (!$this->allowAdd && !$this->allowDelete)) {
$event->setData($dataToMergeInto);
return;
}
if (!$dataToMergeInto) {
// No original data was set. Set it if allowed
if ($this->allowAdd) {
$dataToMergeInto = $data;
}
} else {
// Calculate delta
$itemsToAdd = is_object($data) ? clone $data : $data;
$itemsToDelete = array();
foreach ($dataToMergeInto as $beforeKey => $beforeItem) {
foreach ($data as $afterKey => $afterItem) {
if ($afterItem === $beforeItem) {
// Item found, next original item
unset($itemsToAdd[$afterKey]);
continue 2;
}
}
// Item not found, remember for deletion
$itemsToDelete[] = $beforeKey;
}
// Remove deleted items before adding to free keys that are to be
// replaced
if ($this->allowDelete) {
foreach ($itemsToDelete as $key) {
unset($dataToMergeInto[$key]);
}
}
// Add remaining items
if ($this->allowAdd) {
foreach ($itemsToAdd as $key => $item) {
if (!isset($dataToMergeInto[$key])) {
$dataToMergeInto[$key] = $item;
} else {
$dataToMergeInto[] = $item;
}
}
}
}
$event->setData($dataToMergeInto);
}
}

View File

@@ -0,0 +1,183 @@
<?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\Form\Extension\Core\EventListener;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Resize a collection form element based on the data sent from the client.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResizeFormListener implements EventSubscriberInterface
{
/**
* @var string
*/
protected $type;
/**
* @var array
*/
protected $options;
/**
* Whether children could be added to the group.
*
* @var bool
*/
protected $allowAdd;
/**
* Whether children could be removed from the group.
*
* @var bool
*/
protected $allowDelete;
/**
* @var bool
*/
private $deleteEmpty;
public function __construct($type, array $options = array(), $allowAdd = false, $allowDelete = false, $deleteEmpty = false)
{
$this->type = $type;
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
$this->options = $options;
$this->deleteEmpty = $deleteEmpty;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
// (MergeCollectionListener, MergeDoctrineCollectionListener)
FormEvents::SUBMIT => array('onSubmit', 50),
);
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
if (null === $data) {
$data = array();
}
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
}
// First remove all rows
foreach ($form as $name => $child) {
$form->remove($name);
}
// Then add all rows again in the correct order
foreach ($data as $name => $value) {
$form->add($name, $this->type, array_replace(array(
'property_path' => '['.$name.']',
), $this->options));
}
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
if ($data instanceof \Traversable && $data instanceof \ArrayAccess) {
@trigger_error('Support for objects implementing both \Traversable and \ArrayAccess is deprecated since version 3.1 and will be removed in 4.0. Use an array instead.', E_USER_DEPRECATED);
}
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
$data = array();
}
// Remove all empty rows
if ($this->allowDelete) {
foreach ($form as $name => $child) {
if (!isset($data[$name])) {
$form->remove($name);
}
}
}
// Add all additional rows
if ($this->allowAdd) {
foreach ($data as $name => $value) {
if (!$form->has($name)) {
$form->add($name, $this->type, array_replace(array(
'property_path' => '['.$name.']',
), $this->options));
}
}
}
}
public function onSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// At this point, $data is an array or an array-like object that already contains the
// new entries, which were added by the data mapper. The data mapper ignores existing
// entries, so we need to manually unset removed entries in the collection.
if (null === $data) {
$data = array();
}
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
}
if ($this->deleteEmpty) {
$previousData = $event->getForm()->getData();
foreach ($form as $name => $child) {
$isNew = !isset($previousData[$name]);
// $isNew can only be true if allowAdd is true, so we don't
// need to check allowAdd again
if ($child->isEmpty() && ($isNew || $this->allowDelete)) {
unset($data[$name]);
$form->remove($name);
}
}
}
// The data mapper only adds, but does not remove items, so do this
// here
if ($this->allowDelete) {
$toDelete = array();
foreach ($data as $name => $child) {
if (!$form->has($name)) {
$toDelete[] = $name;
}
}
foreach ($toDelete as $name) {
unset($data[$name]);
}
}
$event->setData($data);
}
}

View File

@@ -0,0 +1,41 @@
<?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\Form\Extension\Core\EventListener;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Util\StringUtil;
/**
* Trims string data.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TrimListener implements EventSubscriberInterface
{
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
if (!is_string($data)) {
return;
}
$event->setData(StringUtil::trim($data));
}
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SUBMIT => 'preSubmit');
}
}

View File

@@ -0,0 +1,124 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Encapsulates common logic of {@link FormType} and {@link ButtonType}.
*
* This type does not appear in the form's type inheritance chain and as such
* cannot be extended (via {@link \Symfony\Component\Form\FormExtensionInterface}) nor themed.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class BaseType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setDisabled($options['disabled']);
$builder->setAutoInitialize($options['auto_initialize']);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$name = $form->getName();
$blockName = $options['block_name'] ?: $form->getName();
$translationDomain = $options['translation_domain'];
$labelFormat = $options['label_format'];
if ($view->parent) {
if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
$id = sprintf('%s_%s', $view->parent->vars['id'], $name);
$fullName = sprintf('%s[%s]', $parentFullName, $name);
$uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
} else {
$id = $name;
$fullName = $name;
$uniqueBlockPrefix = '_'.$blockName;
}
if (null === $translationDomain) {
$translationDomain = $view->parent->vars['translation_domain'];
}
if (!$labelFormat) {
$labelFormat = $view->parent->vars['label_format'];
}
} else {
$id = $name;
$fullName = $name;
$uniqueBlockPrefix = '_'.$blockName;
// Strip leading underscores and digits. These are allowed in
// form names, but not in HTML4 ID attributes.
// http://www.w3.org/TR/html401/struct/global.html#adef-id
$id = ltrim($id, '_0123456789');
}
$blockPrefixes = array();
for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
array_unshift($blockPrefixes, $type->getBlockPrefix());
}
$blockPrefixes[] = $uniqueBlockPrefix;
$view->vars = array_replace($view->vars, array(
'form' => $view,
'id' => $id,
'name' => $name,
'full_name' => $fullName,
'disabled' => $form->isDisabled(),
'label' => $options['label'],
'label_format' => $labelFormat,
'multipart' => false,
'attr' => $options['attr'],
'block_prefixes' => $blockPrefixes,
'unique_block_prefix' => $uniqueBlockPrefix,
'translation_domain' => $translationDomain,
// Using the block name here speeds up performance in collection
// forms, where each entry has the same full block name.
// Including the type is important too, because if rows of a
// collection form have different types (dynamically), they should
// be rendered differently.
// https://github.com/symfony/symfony/issues/5038
'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'block_name' => null,
'disabled' => false,
'label' => null,
'label_format' => null,
'attr' => array(),
'translation_domain' => null,
'auto_initialize' => true,
));
$resolver->setAllowedTypes('attr', 'array');
}
}

View File

@@ -0,0 +1,44 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BirthdayType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('years', range(date('Y') - 120, date('Y')));
$resolver->setAllowedTypes('years', 'array');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\DateType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'birthday';
}
}

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\Form\Extension\Core\Type;
use Symfony\Component\Form\ButtonTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A form button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ButtonType extends BaseType implements ButtonTypeInterface
{
/**
* {@inheritdoc}
*/
public function getParent()
{
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'button';
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
'auto_initialize' => false,
));
}
}

View File

@@ -0,0 +1,72 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CheckboxType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Unlike in other types, where the data is NULL by default, it
// needs to be a Boolean here. setData(null) is not acceptable
// for checkboxes and radio buttons (unless a custom model
// transformer handles this case).
// We cannot solve this case via overriding the "data" option, because
// doing so also calls setDataLocked(true).
$builder->setData(isset($options['data']) ? $options['data'] : false);
$builder->addViewTransformer(new BooleanToStringTransformer($options['value']));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'value' => $options['value'],
'checked' => null !== $form->getViewData(),
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$emptyData = function (FormInterface $form, $viewData) {
return $viewData;
};
$resolver->setDefaults(array(
'value' => '1',
'empty_data' => $emptyData,
'compound' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'checkbox';
}
}

View File

@@ -0,0 +1,450 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ChoiceType extends AbstractType
{
/**
* Caches created choice lists.
*
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
new PropertyAccessDecorator(
new DefaultChoiceListFactory()
)
);
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choiceList = $this->createChoiceList($options);
$builder->setAttribute('choice_list', $choiceList);
if ($options['expanded']) {
$builder->setDataMapper($options['multiple'] ? new CheckboxListMapper() : new RadioListMapper());
// Initialize all choices before doing the index check below.
// This helps in cases where index checks are optimized for non
// initialized choice lists. For example, when using an SQL driver,
// the index check would read in one SQL query and the initialization
// requires another SQL query. When the initialization is done first,
// one SQL query is sufficient.
$choiceListView = $this->createChoiceListView($choiceList, $options);
$builder->setAttribute('choice_list_view', $choiceListView);
// Check if the choices already contain the empty value
// Only add the placeholder option if this is not the case
if (null !== $options['placeholder'] && 0 === count($choiceList->getChoicesForValues(array('')))) {
$placeholderView = new ChoiceView(null, '', $options['placeholder']);
// "placeholder" is a reserved name
$this->addSubForm($builder, 'placeholder', $placeholderView, $options);
}
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
$this->addSubForms($builder, $choiceListView->choices, $options);
// Make sure that scalar, submitted values are converted to arrays
// which can be submitted to the checkboxes/radio buttons
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (null === $data) {
$emptyData = $form->getConfig()->getEmptyData();
if (false === FormUtil::isEmpty($emptyData) && array() !== $emptyData) {
$data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData;
}
}
// Convert the submitted data to a string, if scalar, before
// casting it to an array
if (!is_array($data)) {
$data = (array) (string) $data;
}
// A map from submitted values to integers
$valueMap = array_flip($data);
// Make a copy of the value map to determine whether any unknown
// values were submitted
$unknownValues = $valueMap;
// Reconstruct the data as mapping from child names to values
$data = array();
foreach ($form as $child) {
$value = $child->getConfig()->getOption('value');
// Add the value to $data with the child's name as key
if (isset($valueMap[$value])) {
$data[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
}
}
// The empty value is always known, independent of whether a
// field exists for it or not
unset($unknownValues['']);
// Throw exception if unknown values were submitted
if (count($unknownValues) > 0) {
throw new TransformationFailedException(sprintf(
'The choices "%s" do not exist in the choice list.',
implode('", "', array_keys($unknownValues))
));
}
$event->setData($data);
});
}
if ($options['multiple']) {
// <select> tag with "multiple" option or list of checkbox inputs
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
} else {
// <select> tag without "multiple" option or list of radio inputs
$builder->addViewTransformer(new ChoiceToValueTransformer($choiceList));
}
if ($options['multiple'] && $options['by_reference']) {
// Make sure the collection created during the client->norm
// transformation is merged back into the original collection
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
}
// To avoid issues when the submitted choices are arrays (i.e. array to string conversions),
// we have to ensure that all elements of the submitted choice data are NULL, strings or ints.
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!is_array($data)) {
return;
}
foreach ($data as $v) {
if (null !== $v && !is_string($v) && !is_int($v)) {
throw new TransformationFailedException('All choices submitted must be NULL, strings or ints.');
}
}
}, 256);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$choiceTranslationDomain = $options['choice_translation_domain'];
if ($view->parent && null === $choiceTranslationDomain) {
$choiceTranslationDomain = $view->vars['translation_domain'];
}
/** @var ChoiceListInterface $choiceList */
$choiceList = $form->getConfig()->getAttribute('choice_list');
/** @var ChoiceListView $choiceListView */
$choiceListView = $form->getConfig()->hasAttribute('choice_list_view')
? $form->getConfig()->getAttribute('choice_list_view')
: $this->createChoiceListView($choiceList, $options);
$view->vars = array_replace($view->vars, array(
'multiple' => $options['multiple'],
'expanded' => $options['expanded'],
'preferred_choices' => $choiceListView->preferredChoices,
'choices' => $choiceListView->choices,
'separator' => '-------------------',
'placeholder' => null,
'choice_translation_domain' => $choiceTranslationDomain,
));
// The decision, whether a choice is selected, is potentially done
// thousand of times during the rendering of a template. Provide a
// closure here that is optimized for the value of the form, to
// avoid making the type check inside the closure.
if ($options['multiple']) {
$view->vars['is_selected'] = function ($choice, array $values) {
return in_array($choice, $values, true);
};
} else {
$view->vars['is_selected'] = function ($choice, $value) {
return $choice === $value;
};
}
// Check if the choices already contain the empty value
$view->vars['placeholder_in_choices'] = $choiceListView->hasPlaceholder();
// Only add the empty value option if this is not the case
if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) {
$view->vars['placeholder'] = $options['placeholder'];
}
if ($options['multiple'] && !$options['expanded']) {
// Add "[]" to the name in case a select tag with multiple options is
// displayed. Otherwise only one of the selected options is sent in the
// POST request.
$view->vars['full_name'] .= '[]';
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($options['expanded']) {
// Radio buttons should have the same name as the parent
$childName = $view->vars['full_name'];
// Checkboxes should append "[]" to allow multiple selection
if ($options['multiple']) {
$childName .= '[]';
}
foreach ($view as $childView) {
$childView->vars['full_name'] = $childName;
}
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$emptyData = function (Options $options) {
if ($options['expanded'] && !$options['multiple']) {
return;
}
if ($options['multiple']) {
return array();
}
return '';
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$choicesAsValuesNormalizer = function (Options $options, $choicesAsValues) {
// Not set by the user
if (null === $choicesAsValues) {
return true;
}
// Set by the user
if (true !== $choicesAsValues) {
throw new \RuntimeException(sprintf('The "choices_as_values" option of the %s should not be used. Remove it and flip the contents of the "choices" option instead.', get_class($this)));
}
@trigger_error('The "choices_as_values" option is deprecated since version 3.1 and will be removed in 4.0. You should not use it anymore.', E_USER_DEPRECATED);
return true;
};
$placeholderNormalizer = function (Options $options, $placeholder) {
if ($options['multiple']) {
// never use an empty value for this case
return;
} elseif ($options['required'] && ($options['expanded'] || isset($options['attr']['size']) && $options['attr']['size'] > 1)) {
// placeholder for required radio buttons or a select with size > 1 does not make sense
return;
} elseif (false === $placeholder) {
// an empty value should be added but the user decided otherwise
return;
} elseif ($options['expanded'] && '' === $placeholder) {
// never use an empty label for radio buttons
return 'None';
}
// empty value has been set explicitly
return $placeholder;
};
$compound = function (Options $options) {
return $options['expanded'];
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (true === $choiceTranslationDomain) {
return $options['translation_domain'];
}
return $choiceTranslationDomain;
};
$resolver->setDefaults(array(
'multiple' => false,
'expanded' => false,
'choices' => array(),
'choices_as_values' => null, // deprecated since 3.1
'choice_loader' => null,
'choice_label' => null,
'choice_name' => null,
'choice_value' => null,
'choice_attr' => null,
'preferred_choices' => array(),
'group_by' => null,
'empty_data' => $emptyData,
'placeholder' => $placeholderDefault,
'error_bubbling' => false,
'compound' => $compound,
// The view data is always a string, even if the "data" option
// is manually set to an object.
// See https://github.com/symfony/symfony/pull/5582
'data_class' => null,
'choice_translation_domain' => true,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setNormalizer('choices_as_values', $choicesAsValuesNormalizer);
$resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable'));
$resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string'));
$resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'));
$resolver->setAllowedTypes('choice_label', array('null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('group_by', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'choice';
}
/**
* Adds the sub fields for an expanded choice field.
*
* @param FormBuilderInterface $builder The form builder
* @param array $choiceViews The choice view objects
* @param array $options The build options
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $name => $choiceView) {
// Flatten groups
if (is_array($choiceView)) {
$this->addSubForms($builder, $choiceView, $options);
continue;
}
if ($choiceView instanceof ChoiceGroupView) {
$this->addSubForms($builder, $choiceView->choices, $options);
continue;
}
$this->addSubForm($builder, $name, $choiceView, $options);
}
}
/**
* @param FormBuilderInterface $builder
* @param $name
* @param $choiceView
* @param array $options
*
* @return mixed
*/
private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $choiceView, array $options)
{
$choiceOpts = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'attr' => $choiceView->attr,
'translation_domain' => $options['translation_domain'],
'block_name' => 'entry',
);
if ($options['multiple']) {
$choiceType = __NAMESPACE__.'\CheckboxType';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = __NAMESPACE__.'\RadioType';
}
$builder->add($name, $choiceType, $choiceOpts);
}
private function createChoiceList(array $options)
{
if (null !== $options['choice_loader']) {
return $this->choiceListFactory->createListFromLoader(
$options['choice_loader'],
$options['choice_value']
);
}
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : array();
return $this->choiceListFactory->createListFromChoices($choices, $options['choice_value']);
}
private function createChoiceListView(ChoiceListInterface $choiceList, array $options)
{
return $this->choiceListFactory->createView(
$choiceList,
$options['preferred_choices'],
$options['choice_label'],
$options['choice_name'],
$options['group_by'],
$options['choice_attr']
);
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CollectionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['allow_add'] && $options['prototype']) {
$prototypeOptions = array_replace(array(
'required' => $options['required'],
'label' => $options['prototype_name'].'label__',
), $options['entry_options']);
if (null !== $options['prototype_data']) {
$prototypeOptions['data'] = $options['prototype_data'];
}
$prototype = $builder->create($options['prototype_name'], $options['entry_type'], $prototypeOptions);
$builder->setAttribute('prototype', $prototype->getForm());
}
$resizeListener = new ResizeFormListener(
$options['entry_type'],
$options['entry_options'],
$options['allow_add'],
$options['allow_delete'],
$options['delete_empty']
);
$builder->addEventSubscriber($resizeListener);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'allow_add' => $options['allow_add'],
'allow_delete' => $options['allow_delete'],
));
if ($form->getConfig()->hasAttribute('prototype')) {
$prototype = $form->getConfig()->getAttribute('prototype');
$view->vars['prototype'] = $prototype->setParent($form)->createView($view);
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($form->getConfig()->hasAttribute('prototype') && $view->vars['prototype']->vars['multipart']) {
$view->vars['multipart'] = true;
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$entryOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'entry';
return $value;
};
$resolver->setDefaults(array(
'allow_add' => false,
'allow_delete' => false,
'prototype' => true,
'prototype_data' => null,
'prototype_name' => '__name__',
'entry_type' => __NAMESPACE__.'\TextType',
'entry_options' => array(),
'delete_empty' => false,
));
$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'collection';
}
}

View File

@@ -0,0 +1,118 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CountryType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Country loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getRegionBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'country';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getRegionBundle()->getCountryNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,118 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CurrencyType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Currency loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getCurrencyBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'currency';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getCurrencyBundle()->getCurrencyNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,293 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalType extends AbstractType
{
private $timeParts = array(
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
'seconds',
);
private static $widgets = array(
'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
'integer' => 'Symfony\Component\Form\Extension\Core\Type\IntegerType',
'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
);
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['with_years'] && !$options['with_months'] && !$options['with_weeks'] && !$options['with_days'] && !$options['with_hours'] && !$options['with_minutes'] && !$options['with_seconds']) {
throw new InvalidConfigurationException('You must enable at least one interval field.');
}
if ($options['with_invert'] && 'single_text' === $options['widget']) {
throw new InvalidConfigurationException('The single_text widget does not support invertible intervals.');
}
if ($options['with_weeks'] && $options['with_days']) {
throw new InvalidConfigurationException('You can not enable weeks and days fields together.');
}
$format = 'P';
$parts = array();
if ($options['with_years']) {
$format .= '%yY';
$parts[] = 'years';
}
if ($options['with_months']) {
$format .= '%mM';
$parts[] = 'months';
}
if ($options['with_weeks']) {
$format .= '%wW';
$parts[] = 'weeks';
}
if ($options['with_days']) {
$format .= '%dD';
$parts[] = 'days';
}
if ($options['with_hours'] || $options['with_minutes'] || $options['with_seconds']) {
$format .= 'T';
}
if ($options['with_hours']) {
$format .= '%hH';
$parts[] = 'hours';
}
if ($options['with_minutes']) {
$format .= '%iM';
$parts[] = 'minutes';
}
if ($options['with_seconds']) {
$format .= '%sS';
$parts[] = 'seconds';
}
if ($options['with_invert']) {
$parts[] = 'invert';
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new DateIntervalToStringTransformer($format));
} else {
$childOptions = array();
foreach ($this->timeParts as $part) {
if ($options['with_'.$part]) {
$childOptions[$part] = array(
'error_bubbling' => true,
'label' => $options['labels'][$part],
);
if ('choice' === $options['widget']) {
$childOptions[$part]['choice_translation_domain'] = false;
$childOptions[$part]['choices'] = $options[$part];
$childOptions[$part]['placeholder'] = $options['placeholder'][$part];
}
}
}
// Append generic carry-along options
foreach (array('required', 'translation_domain') as $passOpt) {
foreach ($this->timeParts as $part) {
if ($options['with_'.$part]) {
$childOptions[$part][$passOpt] = $options[$passOpt];
}
}
}
foreach ($this->timeParts as $part) {
if ($options['with_'.$part]) {
$childForm = $builder->create($part, self::$widgets[$options['widget']], $childOptions[$part]);
if ('integer' === $options['widget']) {
$childForm->addModelTransformer(
new ReversedTransformer(
new IntegerToLocalizedStringTransformer()
)
);
}
$builder->add($childForm);
}
}
if ($options['with_invert']) {
$builder->add('invert', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', array(
'label' => $options['labels']['invert'],
'error_bubbling' => true,
'required' => false,
'translation_domain' => $options['translation_domain'],
));
}
$builder->addViewTransformer(new DateIntervalToArrayTransformer($parts, 'text' === $options['widget']));
}
if ('string' === $options['input']) {
$builder->addModelTransformer(
new ReversedTransformer(
new DateIntervalToStringTransformer($format)
)
);
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(
new ReversedTransformer(
new DateIntervalToArrayTransformer($parts)
)
);
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$vars = array(
'widget' => $options['widget'],
'with_invert' => $options['with_invert'],
);
foreach ($this->timeParts as $part) {
$vars['with_'.$part] = $options['with_'.$part];
}
$view->vars = array_replace($view->vars, $vars);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$timeParts = $this->timeParts;
$compound = function (Options $options) {
return $options['widget'] !== 'single_text';
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault, $timeParts) {
if (is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(array_fill_keys($timeParts, $default), $placeholder);
}
return array_fill_keys($timeParts, $placeholder);
};
$labelsNormalizer = function (Options $options, array $labels) {
return array_replace(array(
'years' => null,
'months' => null,
'days' => null,
'weeks' => null,
'hours' => null,
'minutes' => null,
'seconds' => null,
'invert' => 'Negative interval',
), array_filter($labels, function ($label) {
return null !== $label;
}));
};
$resolver->setDefaults(
array(
'with_years' => true,
'with_months' => true,
'with_days' => true,
'with_weeks' => false,
'with_hours' => false,
'with_minutes' => false,
'with_seconds' => false,
'with_invert' => false,
'years' => range(0, 100),
'months' => range(0, 12),
'weeks' => range(0, 52),
'days' => range(0, 31),
'hours' => range(0, 24),
'minutes' => range(0, 60),
'seconds' => range(0, 60),
'widget' => 'choice',
'input' => 'dateinterval',
'placeholder' => $placeholderDefault,
'by_reference' => true,
'error_bubbling' => false,
// If initialized with a \DateInterval object, FormType initializes
// this option to "\DateInterval". Since the internal, normalized
// representation is not \DateInterval, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
'labels' => array(),
)
);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('labels', $labelsNormalizer);
$resolver->setAllowedValues(
'input',
array(
'dateinterval',
'string',
'array',
)
);
$resolver->setAllowedValues(
'widget',
array(
'single_text',
'text',
'integer',
'choice',
)
);
// Don't clone \DateInterval classes, as i.e. format()
// does not work after that
$resolver->setAllowedValues('by_reference', true);
$resolver->setAllowedTypes('years', 'array');
$resolver->setAllowedTypes('months', 'array');
$resolver->setAllowedTypes('weeks', 'array');
$resolver->setAllowedTypes('days', 'array');
$resolver->setAllowedTypes('hours', 'array');
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
$resolver->setAllowedTypes('with_years', 'bool');
$resolver->setAllowedTypes('with_months', 'bool');
$resolver->setAllowedTypes('with_weeks', 'bool');
$resolver->setAllowedTypes('with_days', 'bool');
$resolver->setAllowedTypes('with_hours', 'bool');
$resolver->setAllowedTypes('with_minutes', 'bool');
$resolver->setAllowedTypes('with_seconds', 'bool');
$resolver->setAllowedTypes('with_invert', 'bool');
$resolver->setAllowedTypes('labels', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'dateinterval';
}
}

View File

@@ -0,0 +1,290 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DateTimeType extends AbstractType
{
const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
/**
* This is not quite the HTML5 format yet, because ICU lacks the
* capability of parsing and generating RFC 3339 dates, which
* are like the below pattern but with a timezone suffix. The
* timezone suffix is.
*
* * "Z" for UTC
* * "(-|+)HH:mm" for other timezones (note the colon!)
*
* For more information see:
*
* http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
* http://www.w3.org/TR/html-markup/input.datetime.html
* http://tools.ietf.org/html/rfc3339
*
* An ICU ticket was created:
* http://icu-project.org/trac/ticket/9421
*
* It was supposedly fixed, but is not available in all PHP installations
* yet. To temporarily circumvent this issue, DateTimeToRfc3339Transformer
* is used when the format matches this constant.
*/
const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";
private static $acceptedFormats = array(
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
);
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$parts = array('year', 'month', 'day', 'hour');
$dateParts = array('year', 'month', 'day');
$timeParts = array('hour');
if ($options['with_minutes']) {
$parts[] = 'minute';
$timeParts[] = 'minute';
}
if ($options['with_seconds']) {
$parts[] = 'second';
$timeParts[] = 'second';
}
$dateFormat = is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
$timeFormat = self::DEFAULT_TIME_FORMAT;
$calendar = \IntlDateFormatter::GREGORIAN;
$pattern = is_string($options['format']) ? $options['format'] : null;
if (!in_array($dateFormat, self::$acceptedFormats, true)) {
throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
}
if ('single_text' === $options['widget']) {
if (self::HTML5_FORMAT === $pattern) {
$builder->addViewTransformer(new DateTimeToRfc3339Transformer(
$options['model_timezone'],
$options['view_timezone']
));
} else {
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
$options['model_timezone'],
$options['view_timezone'],
$dateFormat,
$timeFormat,
$calendar,
$pattern
));
}
} else {
// Only pass a subset of the options to children
$dateOptions = array_intersect_key($options, array_flip(array(
'years',
'months',
'days',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'html5',
'invalid_message',
'invalid_message_parameters',
)));
$timeOptions = array_intersect_key($options, array_flip(array(
'hours',
'minutes',
'seconds',
'with_minutes',
'with_seconds',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'html5',
'invalid_message',
'invalid_message_parameters',
)));
if (null !== $options['date_widget']) {
$dateOptions['widget'] = $options['date_widget'];
}
if (null !== $options['time_widget']) {
$timeOptions['widget'] = $options['time_widget'];
}
if (null !== $options['date_format']) {
$dateOptions['format'] = $options['date_format'];
}
$dateOptions['input'] = $timeOptions['input'] = 'array';
$dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
$builder
->addViewTransformer(new DataTransformerChain(array(
new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
new ArrayToPartsTransformer(array(
'date' => $dateParts,
'time' => $timeParts,
)),
)))
->add('date', __NAMESPACE__.'\DateType', $dateOptions)
->add('time', __NAMESPACE__.'\TimeType', $timeOptions)
;
}
if ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('timestamp' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
));
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
// Change the input to a HTML5 datetime input if
// * the widget is set to "single_text"
// * the format matches the one expected by HTML5
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
$view->vars['type'] = 'datetime';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
// Defaults to the value of "widget"
$dateWidget = function (Options $options) {
return $options['widget'];
};
// Defaults to the value of "widget"
$timeWidget = function (Options $options) {
return $options['widget'];
};
$resolver->setDefaults(array(
'input' => 'datetime',
'model_timezone' => null,
'view_timezone' => null,
'format' => self::HTML5_FORMAT,
'date_format' => null,
'widget' => null,
'date_widget' => $dateWidget,
'time_widget' => $timeWidget,
'with_minutes' => true,
'with_seconds' => false,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
));
// Don't add some defaults in order to preserve the defaults
// set in DateType and TimeType
$resolver->setDefined(array(
'placeholder',
'choice_translation_domain',
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
));
$resolver->setAllowedValues('input', array(
'datetime',
'string',
'timestamp',
'array',
));
$resolver->setAllowedValues('date_widget', array(
null, // inherit default from DateType
'single_text',
'text',
'choice',
));
$resolver->setAllowedValues('time_widget', array(
null, // inherit default from TimeType
'single_text',
'text',
'choice',
));
// This option will overwrite "date_widget" and "time_widget" options
$resolver->setAllowedValues('widget', array(
null, // default, don't overwrite options
'single_text',
'text',
'choice',
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'datetime';
}
}

View File

@@ -0,0 +1,345 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
class DateType extends AbstractType
{
const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
const HTML5_FORMAT = 'yyyy-MM-dd';
private static $acceptedFormats = array(
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
);
private static $widgets = array(
'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
);
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$dateFormat = is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT;
$timeFormat = \IntlDateFormatter::NONE;
$calendar = \IntlDateFormatter::GREGORIAN;
$pattern = is_string($options['format']) ? $options['format'] : null;
if (!in_array($dateFormat, self::$acceptedFormats, true)) {
throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
}
if ('single_text' === $options['widget']) {
if (null !== $pattern && false === strpos($pattern, 'y') && false === strpos($pattern, 'M') && false === strpos($pattern, 'd')) {
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern));
}
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
$options['model_timezone'],
$options['view_timezone'],
$dateFormat,
$timeFormat,
$calendar,
$pattern
));
} else {
if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd'))) {
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
}
$yearOptions = $monthOptions = $dayOptions = array(
'error_bubbling' => true,
);
$formatter = new \IntlDateFormatter(
\Locale::getDefault(),
$dateFormat,
$timeFormat,
// see https://bugs.php.net/bug.php?id=66323
class_exists('IntlTimeZone', false) ? \IntlTimeZone::createDefault() : null,
$calendar,
$pattern
);
// new \IntlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/bug.php?id=66323
if (!$formatter) {
throw new InvalidOptionsException(intl_get_error_message(), intl_get_error_code());
}
$formatter->setLenient(false);
if ('choice' === $options['widget']) {
// Only pass a subset of the options to children
$yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years']));
$yearOptions['placeholder'] = $options['placeholder']['year'];
$yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
$monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months']));
$monthOptions['placeholder'] = $options['placeholder']['month'];
$monthOptions['choice_translation_domain'] = $options['choice_translation_domain']['month'];
$dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days']));
$dayOptions['placeholder'] = $options['placeholder']['day'];
$dayOptions['choice_translation_domain'] = $options['choice_translation_domain']['day'];
}
// Append generic carry-along options
foreach (array('required', 'translation_domain') as $passOpt) {
$yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt];
}
$builder
->add('year', self::$widgets[$options['widget']], $yearOptions)
->add('month', self::$widgets[$options['widget']], $monthOptions)
->add('day', self::$widgets[$options['widget']], $dayOptions)
->addViewTransformer(new DateTimeToArrayTransformer(
$options['model_timezone'], $options['view_timezone'], array('year', 'month', 'day')
))
->setAttribute('formatter', $formatter)
;
}
if ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'Y-m-d')
));
} elseif ('timestamp' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], array('year', 'month', 'day'))
));
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
// Change the input to a HTML5 date input if
// * the widget is set to "single_text"
// * the format matches the one expected by HTML5
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
$view->vars['type'] = 'date';
}
if ($form->getConfig()->hasAttribute('formatter')) {
$pattern = $form->getConfig()->getAttribute('formatter')->getPattern();
// remove special characters unless the format was explicitly specified
if (!is_string($options['format'])) {
// remove quoted strings first
$pattern = preg_replace('/\'[^\']+\'/', '', $pattern);
// remove remaining special chars
$pattern = preg_replace('/[^yMd]+/', '', $pattern);
}
// set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
// lookup various formats at http://userguide.icu-project.org/formatparse/datetime
if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) {
$pattern = preg_replace(array('/y+/', '/M+/', '/d+/'), array('{{ year }}', '{{ month }}', '{{ day }}'), $pattern);
} else {
// default fallback
$pattern = '{{ year }}{{ month }}{{ day }}';
}
$view->vars['date_pattern'] = $pattern;
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(
array('year' => $default, 'month' => $default, 'day' => $default),
$placeholder
);
}
return array(
'year' => $placeholder,
'month' => $placeholder,
'day' => $placeholder,
);
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
array('year' => $default, 'month' => $default, 'day' => $default),
$choiceTranslationDomain
);
}
return array(
'year' => $choiceTranslationDomain,
'month' => $choiceTranslationDomain,
'day' => $choiceTranslationDomain,
);
};
$format = function (Options $options) {
return 'single_text' === $options['widget'] ? DateType::HTML5_FORMAT : DateType::DEFAULT_FORMAT;
};
$resolver->setDefaults(array(
'years' => range(date('Y') - 5, date('Y') + 5),
'months' => range(1, 12),
'days' => range(1, 31),
'widget' => 'choice',
'input' => 'datetime',
'format' => $format,
'model_timezone' => null,
'view_timezone' => null,
'placeholder' => $placeholderDefault,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
'choice_translation_domain' => false,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedValues('input', array(
'datetime',
'string',
'timestamp',
'array',
));
$resolver->setAllowedValues('widget', array(
'single_text',
'text',
'choice',
));
$resolver->setAllowedTypes('format', array('int', 'string'));
$resolver->setAllowedTypes('years', 'array');
$resolver->setAllowedTypes('months', 'array');
$resolver->setAllowedTypes('days', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'date';
}
private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $timestamps)
{
$pattern = $formatter->getPattern();
$timezone = $formatter->getTimezoneId();
$formattedTimestamps = array();
$formatter->setTimeZone('UTC');
if (preg_match($regex, $pattern, $matches)) {
$formatter->setPattern($matches[0]);
foreach ($timestamps as $timestamp => $choice) {
$formattedTimestamps[$formatter->format($timestamp)] = $choice;
}
// I'd like to clone the formatter above, but then we get a
// segmentation fault, so let's restore the old state instead
$formatter->setPattern($pattern);
}
$formatter->setTimeZone($timezone);
return $formattedTimestamps;
}
private function listYears(array $years)
{
$result = array();
foreach ($years as $year) {
if (false !== $y = gmmktime(0, 0, 0, 6, 15, $year)) {
$result[$y] = $year;
}
}
return $result;
}
private function listMonths(array $months)
{
$result = array();
foreach ($months as $month) {
$result[gmmktime(0, 0, 0, $month, 15)] = $month;
}
return $result;
}
private function listDays(array $days)
{
$result = array();
foreach ($days as $day) {
$result[gmmktime(0, 0, 0, 5, $day)] = $day;
}
return $result;
}
}

View File

@@ -0,0 +1,33 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class EmailType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'email';
}
}

View File

@@ -0,0 +1,98 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FileType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// submitted data for an input file (not required) without choosing any file
if (array(null) === $data) {
$emptyData = $form->getConfig()->getEmptyData();
$data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData;
$event->setData($data);
}
});
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['multiple']) {
$view->vars['full_name'] .= '[]';
$view->vars['attr']['multiple'] = 'multiple';
}
$view->vars = array_replace($view->vars, array(
'type' => 'file',
'value' => '',
));
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multipart'] = true;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$dataClass = function (Options $options) {
return $options['multiple'] ? null : 'Symfony\Component\HttpFoundation\File\File';
};
$emptyData = function (Options $options) {
return $options['multiple'] ? array() : null;
};
$resolver->setDefaults(array(
'compound' => false,
'data_class' => $dataClass,
'empty_data' => $emptyData,
'multiple' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'file';
}
}

View File

@@ -0,0 +1,203 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class FormType extends BaseType
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$isDataOptionSet = array_key_exists('data', $options);
$builder
->setRequired($options['required'])
->setErrorBubbling($options['error_bubbling'])
->setEmptyData($options['empty_data'])
->setPropertyPath($options['property_path'])
->setMapped($options['mapped'])
->setByReference($options['by_reference'])
->setInheritData($options['inherit_data'])
->setCompound($options['compound'])
->setData($isDataOptionSet ? $options['data'] : null)
->setDataLocked($isDataOptionSet)
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
->setMethod($options['method'])
->setAction($options['action']);
if ($options['trim']) {
$builder->addEventSubscriber(new TrimListener());
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$name = $form->getName();
if ($view->parent) {
if ('' === $name) {
throw new LogicException('Form node with empty name can be used only as root form node.');
}
// Complex fields are read-only if they themselves or their parents are.
if (!isset($view->vars['attr']['readonly']) && isset($view->parent->vars['attr']['readonly']) && false !== $view->parent->vars['attr']['readonly']) {
$view->vars['attr']['readonly'] = true;
}
}
$view->vars = array_replace($view->vars, array(
'errors' => $form->getErrors(),
'valid' => $form->isSubmitted() ? $form->isValid() : true,
'value' => $form->getViewData(),
'data' => $form->getNormData(),
'required' => $form->isRequired(),
'size' => null,
'label_attr' => $options['label_attr'],
'compound' => $form->getConfig()->getCompound(),
'method' => $form->getConfig()->getMethod(),
'action' => $form->getConfig()->getAction(),
'submitted' => $form->isSubmitted(),
));
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$multipart = false;
foreach ($view->children as $child) {
if ($child->vars['multipart']) {
$multipart = true;
break;
}
}
$view->vars['multipart'] = $multipart;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
// Derive "data_class" option from passed "data" object
$dataClass = function (Options $options) {
return isset($options['data']) && is_object($options['data']) ? get_class($options['data']) : null;
};
// Derive "empty_data" closure from "data_class" option
$emptyData = function (Options $options) {
$class = $options['data_class'];
if (null !== $class) {
return function (FormInterface $form) use ($class) {
return $form->isEmpty() && !$form->isRequired() ? null : new $class();
};
}
return function (FormInterface $form) {
return $form->getConfig()->getCompound() ? array() : '';
};
};
// Wrap "post_max_size_message" in a closure to translate it lazily
$uploadMaxSizeMessage = function (Options $options) {
return function () use ($options) {
return $options['post_max_size_message'];
};
};
// For any form that is not represented by a single HTML control,
// errors should bubble up by default
$errorBubbling = function (Options $options) {
return $options['compound'];
};
// If data is given, the form is locked to that data
// (independent of its value)
$resolver->setDefined(array(
'data',
));
$resolver->setDefaults(array(
'data_class' => $dataClass,
'empty_data' => $emptyData,
'trim' => true,
'required' => true,
'property_path' => null,
'mapped' => true,
'by_reference' => true,
'error_bubbling' => $errorBubbling,
'label_attr' => array(),
'inherit_data' => false,
'compound' => true,
'method' => 'POST',
// According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
// section 4.2., empty URIs are considered same-document references
'action' => '',
'attr' => array(),
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
'upload_max_size_message' => $uploadMaxSizeMessage, // internal
));
$resolver->setAllowedTypes('label_attr', 'array');
$resolver->setAllowedTypes('upload_max_size_message', array('callable'));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'form';
}
}

View File

@@ -0,0 +1,40 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class HiddenType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// hidden fields cannot have a required attribute
'required' => false,
// Pass errors to the parent
'error_bubbling' => true,
'compound' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'hidden';
}
}

View File

@@ -0,0 +1,68 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IntegerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(
new IntegerToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode']
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// default scale is locale specific (usually around 3)
'scale' => null,
'grouping' => false,
// Integer cast rounds towards 0, so do the same when displaying fractions
'rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_DOWN,
'compound' => false,
));
$resolver->setAllowedValues('rounding_mode', array(
IntegerToLocalizedStringTransformer::ROUND_FLOOR,
IntegerToLocalizedStringTransformer::ROUND_DOWN,
IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN,
IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN,
IntegerToLocalizedStringTransformer::ROUND_HALF_UP,
IntegerToLocalizedStringTransformer::ROUND_UP,
IntegerToLocalizedStringTransformer::ROUND_CEILING,
));
$resolver->setAllowedTypes('scale', array('null', 'int'));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'integer';
}
}

View File

@@ -0,0 +1,118 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LanguageType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Language loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getLanguageBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'language';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getLanguageBundle()->getLanguageNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,118 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LocaleType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Locale loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getLocaleBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'locale';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getLocaleBundle()->getLocaleNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,113 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MoneyType extends AbstractType
{
protected static $patterns = array();
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer(new MoneyToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
null,
$options['divisor']
))
;
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['money_pattern'] = self::getPattern($options['currency']);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'scale' => 2,
'grouping' => false,
'divisor' => 1,
'currency' => 'EUR',
'compound' => false,
));
$resolver->setAllowedTypes('scale', 'int');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'money';
}
/**
* Returns the pattern for this locale.
*
* The pattern contains the placeholder "{{ widget }}" where the HTML tag should
* be inserted
*/
protected static function getPattern($currency)
{
if (!$currency) {
return '{{ widget }}';
}
$locale = \Locale::getDefault();
if (!isset(self::$patterns[$locale])) {
self::$patterns[$locale] = array();
}
if (!isset(self::$patterns[$locale][$currency])) {
$format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
$pattern = $format->formatCurrency('123', $currency);
// the spacings between currency symbol and number are ignored, because
// a single space leads to better readability in combination with input
// fields
// the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8)
preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123(?:[,.]0+)?[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/u', $pattern, $matches);
if (!empty($matches[1])) {
self::$patterns[$locale][$currency] = $matches[1].' {{ widget }}';
} elseif (!empty($matches[2])) {
self::$patterns[$locale][$currency] = '{{ widget }} '.$matches[2];
} else {
self::$patterns[$locale][$currency] = '{{ widget }}';
}
}
return self::$patterns[$locale][$currency];
}
}

View File

@@ -0,0 +1,66 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NumberType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new NumberToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode']
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// default scale is locale specific (usually around 3)
'scale' => null,
'grouping' => false,
'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP,
'compound' => false,
));
$resolver->setAllowedValues('rounding_mode', array(
NumberToLocalizedStringTransformer::ROUND_FLOOR,
NumberToLocalizedStringTransformer::ROUND_DOWN,
NumberToLocalizedStringTransformer::ROUND_HALF_DOWN,
NumberToLocalizedStringTransformer::ROUND_HALF_EVEN,
NumberToLocalizedStringTransformer::ROUND_HALF_UP,
NumberToLocalizedStringTransformer::ROUND_UP,
NumberToLocalizedStringTransformer::ROUND_CEILING,
));
$resolver->setAllowedTypes('scale', array('null', 'int'));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'number';
}
}

View File

@@ -0,0 +1,57 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PasswordType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['always_empty'] || !$form->isSubmitted()) {
$view->vars['value'] = '';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'always_empty' => true,
'trim' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'password';
}
}

View File

@@ -0,0 +1,55 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PercentType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new PercentToLocalizedStringTransformer($options['scale'], $options['type']));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'scale' => 0,
'type' => 'fractional',
'compound' => false,
));
$resolver->setAllowedValues('type', array(
'fractional',
'integer',
));
$resolver->setAllowedTypes('scale', 'int');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'percent';
}
}

View File

@@ -0,0 +1,33 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class RadioType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\CheckboxType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'radio';
}
}

View File

@@ -0,0 +1,33 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class RangeType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'range';
}
}

View File

@@ -0,0 +1,71 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RepeatedType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Overwrite required option for child fields
$options['first_options']['required'] = $options['required'];
$options['second_options']['required'] = $options['required'];
if (!isset($options['options']['error_bubbling'])) {
$options['options']['error_bubbling'] = $options['error_bubbling'];
}
$builder
->addViewTransformer(new ValueToDuplicatesTransformer(array(
$options['first_name'],
$options['second_name'],
)))
->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options']))
->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options']))
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'type' => __NAMESPACE__.'\TextType',
'options' => array(),
'first_options' => array(),
'second_options' => array(),
'first_name' => 'first',
'second_name' => 'second',
'error_bubbling' => false,
));
$resolver->setAllowedTypes('options', 'array');
$resolver->setAllowedTypes('first_options', 'array');
$resolver->setAllowedTypes('second_options', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'repeated';
}
}

View File

@@ -0,0 +1,39 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ButtonTypeInterface;
/**
* A reset button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResetType extends AbstractType implements ButtonTypeInterface
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ButtonType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'reset';
}
}

View File

@@ -0,0 +1,33 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class SearchType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'search';
}
}

View File

@@ -0,0 +1,46 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\SubmitButtonTypeInterface;
/**
* A submit button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SubmitType extends AbstractType implements SubmitButtonTypeInterface
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['clicked'] = $form->isClicked();
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ButtonType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'submit';
}
}

View File

@@ -0,0 +1,67 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextType extends AbstractType implements DataTransformerInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// When empty_data is explicitly set to an empty string,
// a string should always be returned when NULL is submitted
// This gives more control and thus helps preventing some issues
// with PHP 7 which allows type hinting strings in functions
// See https://github.com/symfony/symfony/issues/5906#issuecomment-203189375
if ('' === $options['empty_data']) {
$builder->addViewTransformer($this);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'compound' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'text';
}
/**
* {@inheritdoc}
*/
public function transform($data)
{
// Model data should not be transformed
return $data;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($data)
{
return null === $data ? '' : $data;
}
}

View File

@@ -0,0 +1,43 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class TextareaType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['pattern'] = null;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'textarea';
}
}

View File

@@ -0,0 +1,277 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimeType extends AbstractType
{
private static $widgets = array(
'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
);
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$parts = array('hour');
$format = 'H';
if ($options['with_seconds'] && !$options['with_minutes']) {
throw new InvalidConfigurationException('You can not disable minutes if you have enabled seconds.');
}
if ($options['with_minutes']) {
$format .= ':i';
$parts[] = 'minute';
}
if ($options['with_seconds']) {
$format .= ':s';
$parts[] = 'second';
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
// handle seconds ignored by user's browser when with_seconds enabled
// https://codereview.chromium.org/450533009/
if ($options['with_seconds']) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $e) {
$data = $e->getData();
if ($data && preg_match('/^\d{2}:\d{2}$/', $data)) {
$e->setData($data.':00');
}
});
}
} else {
$hourOptions = $minuteOptions = $secondOptions = array(
'error_bubbling' => true,
);
if ('choice' === $options['widget']) {
$hours = $minutes = array();
foreach ($options['hours'] as $hour) {
$hours[str_pad($hour, 2, '0', STR_PAD_LEFT)] = $hour;
}
// Only pass a subset of the options to children
$hourOptions['choices'] = $hours;
$hourOptions['placeholder'] = $options['placeholder']['hour'];
$hourOptions['choice_translation_domain'] = $options['choice_translation_domain']['hour'];
if ($options['with_minutes']) {
foreach ($options['minutes'] as $minute) {
$minutes[str_pad($minute, 2, '0', STR_PAD_LEFT)] = $minute;
}
$minuteOptions['choices'] = $minutes;
$minuteOptions['placeholder'] = $options['placeholder']['minute'];
$minuteOptions['choice_translation_domain'] = $options['choice_translation_domain']['minute'];
}
if ($options['with_seconds']) {
$seconds = array();
foreach ($options['seconds'] as $second) {
$seconds[str_pad($second, 2, '0', STR_PAD_LEFT)] = $second;
}
$secondOptions['choices'] = $seconds;
$secondOptions['placeholder'] = $options['placeholder']['second'];
$secondOptions['choice_translation_domain'] = $options['choice_translation_domain']['second'];
}
// Append generic carry-along options
foreach (array('required', 'translation_domain') as $passOpt) {
$hourOptions[$passOpt] = $options[$passOpt];
if ($options['with_minutes']) {
$minuteOptions[$passOpt] = $options[$passOpt];
}
if ($options['with_seconds']) {
$secondOptions[$passOpt] = $options[$passOpt];
}
}
}
$builder->add('hour', self::$widgets[$options['widget']], $hourOptions);
if ($options['with_minutes']) {
$builder->add('minute', self::$widgets[$options['widget']], $minuteOptions);
}
if ($options['with_seconds']) {
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);
}
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget']));
}
if ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'H:i:s')
));
} elseif ('timestamp' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
));
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'widget' => $options['widget'],
'with_minutes' => $options['with_minutes'],
'with_seconds' => $options['with_seconds'],
));
// Change the input to a HTML5 time input if
// * the widget is set to "single_text"
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget']) {
$view->vars['type'] = 'time';
// we need to force the browser to display the seconds by
// adding the HTML attribute step if not already defined.
// Otherwise the browser will not display and so not send the seconds
// therefore the value will always be considered as invalid.
if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) {
$view->vars['attr']['step'] = 1;
}
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(
array('hour' => $default, 'minute' => $default, 'second' => $default),
$placeholder
);
}
return array(
'hour' => $placeholder,
'minute' => $placeholder,
'second' => $placeholder,
);
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
array('hour' => $default, 'minute' => $default, 'second' => $default),
$choiceTranslationDomain
);
}
return array(
'hour' => $choiceTranslationDomain,
'minute' => $choiceTranslationDomain,
'second' => $choiceTranslationDomain,
);
};
$resolver->setDefaults(array(
'hours' => range(0, 23),
'minutes' => range(0, 59),
'seconds' => range(0, 59),
'widget' => 'choice',
'input' => 'datetime',
'with_minutes' => true,
'with_seconds' => false,
'model_timezone' => null,
'view_timezone' => null,
'placeholder' => $placeholderDefault,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
'choice_translation_domain' => false,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedValues('input', array(
'datetime',
'string',
'timestamp',
'array',
));
$resolver->setAllowedValues('widget', array(
'single_text',
'text',
'choice',
));
$resolver->setAllowedTypes('hours', 'array');
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'time';
}
}

View File

@@ -0,0 +1,144 @@
<?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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimezoneType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Timezone loaded choice list.
*
* The choices are generated from the ICU function \DateTimeZone::listIdentifiers().
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'timezone';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList($this->getTimezones(), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
/**
* Returns a normalized array of timezone choices.
*
* @return array The timezone choices
*/
private static function getTimezones()
{
$timezones = array();
foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$parts = explode('/', $timezone);
if (count($parts) > 2) {
$region = $parts[0];
$name = $parts[1].' - '.$parts[2];
} elseif (count($parts) > 1) {
$region = $parts[0];
$name = $parts[1];
} else {
$region = 'Other';
$name = $parts[0];
}
$timezones[$region][str_replace('_', ' ', $name)] = $timezone;
}
return $timezones;
}
}

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\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UrlType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (null !== $options['default_protocol']) {
$builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol']));
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('default_protocol', 'http');
$resolver->setAllowedTypes('default_protocol', array('null', 'string'));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'url';
}
}

View File

@@ -0,0 +1,63 @@
<?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\Form\Extension\Csrf;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* This extension protects forms by using a CSRF token.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfExtension extends AbstractExtension
{
/**
* @var CsrfTokenManagerInterface
*/
private $tokenManager;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var null|string
*/
private $translationDomain;
/**
* Constructor.
*
* @param CsrfTokenManagerInterface $tokenManager The CSRF token manager
* @param TranslatorInterface $translator The translator for translating error messages
* @param null|string $translationDomain The translation domain for translating
*/
public function __construct(CsrfTokenManagerInterface $tokenManager, TranslatorInterface $translator = null, $translationDomain = null)
{
$this->tokenManager = $tokenManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
/**
* {@inheritdoc}
*/
protected function loadTypeExtensions()
{
return array(
new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain),
);
}
}

View File

@@ -0,0 +1,116 @@
<?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\Form\Extension\Csrf\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfValidationListener implements EventSubscriberInterface
{
/**
* The name of the CSRF field.
*
* @var string
*/
private $fieldName;
/**
* The generator for CSRF tokens.
*
* @var CsrfTokenManagerInterface
*/
private $tokenManager;
/**
* A text mentioning the tokenId of the CSRF token.
*
* Validation of the token will only succeed if it was generated in the
* same session and with the same tokenId.
*
* @var string
*/
private $tokenId;
/**
* The message displayed in case of an error.
*
* @var string
*/
private $errorMessage;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var null|string
*/
private $translationDomain;
/**
* @var ServerParams
*/
private $serverParams;
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SUBMIT => 'preSubmit',
);
}
public function __construct($fieldName, CsrfTokenManagerInterface $tokenManager, $tokenId, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null, ServerParams $serverParams = null)
{
$this->fieldName = $fieldName;
$this->tokenManager = $tokenManager;
$this->tokenId = $tokenId;
$this->errorMessage = $errorMessage;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->serverParams = $serverParams ?: new ServerParams();
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$postRequestSizeExceeded = $form->getConfig()->getMethod() === 'POST' && $this->serverParams->hasPostMaxSizeBeenExceeded();
if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) {
$data = $event->getData();
if (!isset($data[$this->fieldName]) || !$this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName]))) {
$errorMessage = $this->errorMessage;
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($errorMessage, array(), $this->translationDomain);
}
$form->addError(new FormError($errorMessage));
}
if (is_array($data)) {
unset($data[$this->fieldName]);
$event->setData($data);
}
}
}
}

View File

@@ -0,0 +1,137 @@
<?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\Form\Extension\Csrf\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeCsrfExtension extends AbstractTypeExtension
{
/**
* @var CsrfTokenManagerInterface
*/
private $defaultTokenManager;
/**
* @var bool
*/
private $defaultEnabled;
/**
* @var string
*/
private $defaultFieldName;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var null|string
*/
private $translationDomain;
/**
* @var ServerParams
*/
private $serverParams;
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null, ServerParams $serverParams = null)
{
$this->defaultTokenManager = $defaultTokenManager;
$this->defaultEnabled = $defaultEnabled;
$this->defaultFieldName = $defaultFieldName;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->serverParams = $serverParams;
}
/**
* Adds a CSRF field to the form when the CSRF protection is enabled.
*
* @param FormBuilderInterface $builder The form builder
* @param array $options The options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['csrf_protection']) {
return;
}
$builder
->addEventSubscriber(new CsrfValidationListener(
$options['csrf_field_name'],
$options['csrf_token_manager'],
$options['csrf_token_id'] ?: ($builder->getName() ?: get_class($builder->getType()->getInnerType())),
$options['csrf_message'],
$this->translator,
$this->translationDomain,
$this->serverParams
))
;
}
/**
* Adds a CSRF field to the root form view.
*
* @param FormView $view The form view
* @param FormInterface $form The form
* @param array $options The options
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($options['csrf_protection'] && !$view->parent && $options['compound']) {
$factory = $form->getConfig()->getFormFactory();
$tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: get_class($form->getConfig()->getType()->getInnerType()));
$data = (string) $options['csrf_token_manager']->getToken($tokenId);
$csrfForm = $factory->createNamed($options['csrf_field_name'], 'Symfony\Component\Form\Extension\Core\Type\HiddenType', $data, array(
'mapped' => false,
));
$view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => $this->defaultEnabled,
'csrf_field_name' => $this->defaultFieldName,
'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
'csrf_token_manager' => $this->defaultTokenManager,
'csrf_token_id' => null,
));
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

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