public function transform($value) { if (null !== $value && !is_scalar($value)) { throw new UnexpectedTypeException($value, 'scalar'); } return FormUtil::toArrayKey($value); }
/** * finds the method used to append values to the named property * * @param mixed $object * @param string $property */ private function findAdderMethod($object, $property) { if (method_exists($object, $method = 'add' . $property)) { return $method; } if (class_exists('Symfony\\Component\\PropertyAccess\\StringUtil') && method_exists('Symfony\\Component\\PropertyAccess\\StringUtil', 'singularify')) { foreach ((array) StringUtil::singularify($property) as $singularForm) { if (method_exists($object, $method = 'add' . $singularForm)) { return $method; } } } elseif (class_exists('Symfony\\Component\\Form\\Util\\FormUtil') && method_exists('Symfony\\Component\\Form\\Util\\FormUtil', 'singularify')) { foreach ((array) FormUtil::singularify($property) as $singularForm) { if (method_exists($object, $method = 'add' . $singularForm)) { return $method; } } } if (method_exists($object, $method = 'add' . rtrim($property, 's'))) { return $method; } if (substr($property, -3) === 'ies' && method_exists($object, $method = 'add' . substr($key, 0, -3) . 'y')) { return $method; } }
public function testToArrayKeys() { $in = $out = array(); foreach ($this->toArrayKeyProvider() as $call) { $in[] = $call[0]; $out[] = $call[1]; } $this->assertSame($out, FormUtil::toArrayKeys($in)); }
/** * @param array $array * * @return array * * @throws UnexpectedTypeException if the given value is not an array */ public function transform($array) { if (null === $array) { return array(); } if (!is_array($array)) { throw new UnexpectedTypeException($array, 'array'); } return FormUtil::toArrayKeys($array); }
/** * Transforms a single choice to a format appropriate for the nested * checkboxes/radio buttons. * * The result is an array with the options as keys and true/false as values, * depending on whether a given option is selected. If this field is rendered * as select tag, the value is not modified. * * @param mixed $value An array if "multiple" is set to true, a scalar * value otherwise. * * @return mixed An array * * @throws UnexpectedTypeException if the given value is not scalar * @throws TransformationFailedException if the choices can not be retrieved */ public function transform($value) { if (!is_scalar($value) && null !== $value) { throw new UnexpectedTypeException($value, 'scalar'); } try { $choices = $this->choiceList->getChoices(); } catch (\Exception $e) { throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e); } $value = FormUtil::toArrayKey($value); foreach (array_keys($choices) as $key) { $choices[$key] = $key === $value; } return $choices; }
public function noAdderRemoverData() { $data = array(); $car = $this->getMock(__CLASS__ . '_CarNoAdderAndRemover'); $propertyPath = new PropertyPath('axes'); $expectedMessage = sprintf('Neither element "axes" nor method "setAxes()" exists in class ' . '"%s", nor could adders and removers be found based on the ' . 'guessed singulars: %s (provide a singular by suffixing the ' . 'property path with "|{singular}" to override the guesser)', get_class($car), implode(', ', (array) ($singulars = FormUtil::singularify('Axes')))); $data[] = array($car, $propertyPath, $expectedMessage); /* Temporarily disabled in 2.1 $propertyPath = new PropertyPath('axes|boo'); $expectedMessage = sprintf( 'Neither element "axes" nor method "setAxes()" exists in class ' .'"%s", nor could adders and removers be found based on the ' .'passed singular: %s', get_class($car), 'boo' ); $data[] = array($car, $propertyPath, $expectedMessage); */ $car = $this->getMock(__CLASS__ . '_CarNoAdderAndRemoverWithProperty'); $propertyPath = new PropertyPath('axes'); $expectedMessage = sprintf('Property "axes" is not public in class "%s", nor could adders and ' . 'removers be found based on the guessed singulars: %s ' . '(provide a singular by suffixing the property path with ' . '"|{singular}" to override the guesser). Maybe you should ' . 'create the method "setAxes()"?', get_class($car), implode(', ', (array) ($singulars = FormUtil::singularify('Axes')))); $data[] = array($car, $propertyPath, $expectedMessage); return $data; }
/** * {@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)); } }
/** * Sets the value of the property at the given index in the path * * @param object $objectOrArray The object or array to traverse * @param integer $currentIndex The index of the modified property in the path * @param mixed $value The value to set */ protected function writeProperty(&$objectOrArray, $currentIndex, $value) { $property = $this->elements[$currentIndex]; if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) { if (!$objectOrArray instanceof \ArrayAccess) { throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \\ArrayAccess', $property, get_class($objectOrArray))); } $objectOrArray[$property] = $value; } elseif (is_object($objectOrArray)) { $reflClass = new ReflectionClass($objectOrArray); $setter = 'set' . $this->camelize($property); $addMethod = null; $removeMethod = null; $plural = null; // Check if the parent has matching methods to add/remove items if (is_array($value) || $value instanceof Traversable) { $singular = $this->singulars[$currentIndex]; if (null !== $singular) { $addMethod = 'add' . ucfirst($singular); $removeMethod = 'remove' . ucfirst($singular); if (!$this->isAccessible($reflClass, $addMethod, 1)) { throw new InvalidPropertyException(sprintf('The public method "%s" with exactly one required parameter was not found on class %s', $addMethod, $reflClass->getName())); } if (!$this->isAccessible($reflClass, $removeMethod, 1)) { throw new InvalidPropertyException(sprintf('The public method "%s" with exactly one required parameter was not found on class %s', $removeMethod, $reflClass->getName())); } } else { // The plural form is the last element of the property path $plural = ucfirst($this->elements[$this->length - 1]); // Any of the two methods is required, but not yet known $singulars = (array) FormUtil::singularify($plural); foreach ($singulars as $singular) { $addMethodName = 'add' . $singular; $removeMethodName = 'remove' . $singular; if ($this->isAccessible($reflClass, $addMethodName, 1)) { $addMethod = $addMethodName; } if ($this->isAccessible($reflClass, $removeMethodName, 1)) { $removeMethod = $removeMethodName; } if ($addMethod && !$removeMethod) { throw new InvalidPropertyException(sprintf('Found the public method "%s", but did not find a public "%s" on class %s', $addMethodName, $removeMethodName, $reflClass->getName())); } if ($removeMethod && !$addMethod) { throw new InvalidPropertyException(sprintf('Found the public method "%s", but did not find a public "%s" on class %s', $removeMethodName, $addMethodName, $reflClass->getName())); } if ($addMethod && $removeMethod) { break; } } } } // Collection with matching adder/remover in $objectOrArray if ($addMethod && $removeMethod) { $itemsToAdd = is_object($value) ? clone $value : $value; $itemToRemove = array(); $previousValue = $this->readProperty($objectOrArray, $currentIndex); if (is_array($previousValue) || $previousValue instanceof Traversable) { foreach ($previousValue as $previousItem) { foreach ($value as $key => $item) { if ($item === $previousItem) { // Item found, don't add unset($itemsToAdd[$key]); // Next $previousItem continue 2; } } // Item not found, add to remove list $itemToRemove[] = $previousItem; } } foreach ($itemToRemove as $item) { $objectOrArray->{$removeMethod}($item); } foreach ($itemsToAdd as $item) { $objectOrArray->{$addMethod}($item); } } elseif ($reflClass->hasMethod($setter)) { if (!$reflClass->getMethod($setter)->isPublic()) { throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName())); } $objectOrArray->{$setter}($value); } elseif ($reflClass->hasMethod('__set')) { // needed to support magic method __set $objectOrArray->{$property} = $value; } elseif ($reflClass->hasProperty($property)) { if (!$reflClass->getProperty($property)->isPublic()) { throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()"?', $property, $reflClass->getName(), $setter)); } $objectOrArray->{$property} = $value; } elseif (property_exists($objectOrArray, $property)) { // needed to support \stdClass instances $objectOrArray->{$property} = $value; } else { throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName())); } } else { $objectOrArray[$property] = $value; } }
/** * @dataProvider isChoiceSelectedProvider */ public function testIsChoiceSelected($expected, $choice, $value) { $this->assertSame($expected, FormUtil::isChoiceSelected($choice, $value)); }
/** * Sets the value of the property at the given index in the path * * @param object|array $objectOrArray The object or array to write to. * @param string $property The property to write. * @param string|null $singular The singular form of the property name or null. * @param Boolean $isIndex Whether to interpret the property as index. * @param mixed $value The value to write. * * @throws InvalidPropertyException If the property does not exist. * @throws PropertyAccessDeniedException If the property cannot be accessed due to * access restrictions (private or protected). */ private function writeProperty(&$objectOrArray, $property, $singular, $isIndex, $value) { $adderRemoverError = null; if ($isIndex) { if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \\ArrayAccess', $property, get_class($objectOrArray))); } $objectOrArray[$property] = $value; } elseif (is_object($objectOrArray)) { $reflClass = new ReflectionClass($objectOrArray); // The plural form is the last element of the property path $plural = $this->camelize($this->elements[$this->length - 1]); // Any of the two methods is required, but not yet known $singulars = null !== $singular ? array($singular) : (array) FormUtil::singularify($plural); if (is_array($value) || $value instanceof Traversable) { $methods = $this->findAdderAndRemover($reflClass, $singulars); if (null !== $methods) { // At this point the add and remove methods have been found // Use iterator_to_array() instead of clone in order to prevent side effects // see https://github.com/symfony/symfony/issues/4670 $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; $itemToRemove = array(); $propertyValue = $this->readProperty($objectOrArray, $property, $isIndex); $previousValue = $propertyValue[self::VALUE]; if (is_array($previousValue) || $previousValue instanceof Traversable) { foreach ($previousValue as $previousItem) { foreach ($value as $key => $item) { if ($item === $previousItem) { // Item found, don't add unset($itemsToAdd[$key]); // Next $previousItem continue 2; } } // Item not found, add to remove list $itemToRemove[] = $previousItem; } } foreach ($itemToRemove as $item) { call_user_func(array($objectOrArray, $methods[1]), $item); } foreach ($itemsToAdd as $item) { call_user_func(array($objectOrArray, $methods[0]), $item); } return; } else { $adderRemoverError = ', nor could adders and removers be found based on the '; if (null === $singular) { // $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)'; $adderRemoverError .= 'guessed singulars: ' . implode(', ', $singulars); } else { $adderRemoverError .= 'passed singular: ' . $singular; } } } $setter = 'set' . $this->camelize($property); if ($reflClass->hasMethod($setter)) { if (!$reflClass->getMethod($setter)->isPublic()) { throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name)); } $objectOrArray->{$setter}($value); } elseif ($reflClass->hasMethod('__set')) { // needed to support magic method __set $objectOrArray->{$property} = $value; } elseif ($reflClass->hasProperty($property)) { if (!$reflClass->getProperty($property)->isPublic()) { throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter)); } $objectOrArray->{$property} = $value; } elseif (property_exists($objectOrArray, $property)) { // needed to support \stdClass instances $objectOrArray->{$property} = $value; } else { throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError)); } } else { throw new InvalidPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } }
public function isChoiceSelected(FormView $view, ChoiceView $choice) { return FormUtil::isChoiceSelected($choice->getValue(), $view->get('value')); }
/** * {@inheritdoc} */ public function isEmpty() { foreach ($this->children as $child) { if (!$child->isEmpty()) { return false; } } return FormUtil::isEmpty($this->modelData) || array() === $this->modelData; }
public function transform($value) { return FormUtil::toArrayKey($value); }
/** * @dataProvider singularifyProvider */ public function testSingularify($plural, $singular) { $this->assertEquals($singular, FormUtil::singularify($plural)); }
/** * Searches for add and remove methods. * * @param \ReflectionClass $reflClass The reflection class for the given object * @param string|null $singular The singular form of the property name or null. * * @return array|null An array containin the adder and remover when found, null otherwise. * * @throws InvalidPropertyException If the property does not exist. */ private function findAdderAndRemover(\ReflectionClass $reflClass, $singular) { if (null !== $singular) { $addMethod = 'add' . $this->camelize($singular); $removeMethod = 'remove' . $this->camelize($singular); if (!$this->isAccessible($reflClass, $addMethod, 1)) { throw new InvalidPropertyException(sprintf('The public method "%s" with exactly one required parameter was not found on class %s', $addMethod, $reflClass->name)); } if (!$this->isAccessible($reflClass, $removeMethod, 1)) { throw new InvalidPropertyException(sprintf('The public method "%s" with exactly one required parameter was not found on class %s', $removeMethod, $reflClass->name)); } return array($addMethod, $removeMethod); } // The plural form is the last element of the property path $plural = $this->camelize($this->elements[$this->length - 1]); // Any of the two methods is required, but not yet known $singulars = (array) FormUtil::singularify($plural); foreach ($singulars as $singular) { $addMethod = 'add' . $singular; $removeMethod = 'remove' . $singular; $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1); $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1); if ($addMethodFound && $removeMethodFound) { return array($addMethod, $removeMethod); } if ($addMethodFound xor $removeMethodFound) { throw new InvalidPropertyException(sprintf('Found the public method "%s", but did not find a public "%s" on class %s', $addMethodFound ? $addMethod : $removeMethod, $addMethodFound ? $removeMethod : $addMethod, $reflClass->name)); } } return null; }
public function isChoiceSelected($choice) { $choice = FormUtil::toArrayKey($choice); // The value should already have been converted by value transformers, // otherwise we had to do the conversion on every call of this method if (is_array($this->vars['value'])) { return false !== array_search($choice, $this->vars['value'], true); } return $choice === $this->vars['value']; }
public function onBindNormData(FilterDataEvent $event) { $originalData = $event->getForm()->getNormData(); // If we are not allowed to change anything, return immediately if (!$this->allowAdd && !$this->allowDelete) { // Don't set to the snapshot as then we are switching from the // original object to its copy, which might break things $event->setData($originalData); return; } $form = $event->getForm(); $data = $event->getData(); $childPropertyPath = null; $parentData = null; $addMethod = null; $removeMethod = null; $propertyPath = null; $plural = null; if ($form->hasParent() && $form->getAttribute('property_path')) { $propertyPath = new PropertyPath($form->getAttribute('property_path')); $childPropertyPath = $propertyPath; $parentData = $form->getParent()->getClientData(); $lastElement = $propertyPath->getElement($propertyPath->getLength() - 1); // If the property path contains more than one element, the parent // data is the object at the parent property path if ($propertyPath->getLength() > 1) { $parentData = $propertyPath->getParent()->getValue($parentData); // Property path relative to $parentData $childPropertyPath = new PropertyPath($lastElement); } // The plural form is the last element of the property path $plural = ucfirst($lastElement); } 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 !== $originalData && !is_array($originalData) && !($originalData instanceof \Traversable && $originalData instanceof \ArrayAccess)) { throw new UnexpectedTypeException($originalData, 'array or (\\Traversable and \\ArrayAccess)'); } // Check if the parent has matching methods to add/remove items if ($this->mergeStrategy & self::MERGE_INTO_PARENT && is_object($parentData)) { $reflClass = new \ReflectionClass($parentData); $addMethodNeeded = $this->allowAdd && !$this->addMethod; $removeMethodNeeded = $this->allowDelete && !$this->removeMethod; // Any of the two methods is required, but not yet known if ($addMethodNeeded || $removeMethodNeeded) { $singulars = (array) FormUtil::singularify($plural); foreach ($singulars as $singular) { // Try to find adder, but don't override preconfigured one if ($addMethodNeeded) { $addMethod = 'add' . $singular; // False alert if (!$this->isAccessible($reflClass, $addMethod, 1)) { $addMethod = null; } } // Try to find remover, but don't override preconfigured one if ($removeMethodNeeded) { $removeMethod = 'remove' . $singular; // False alert if (!$this->isAccessible($reflClass, $removeMethod, 1)) { $removeMethod = null; } } // Found all that we need. Abort search. if ((!$addMethodNeeded || $addMethod) && (!$removeMethodNeeded || $removeMethod)) { break; } // False alert $addMethod = null; $removeMethod = null; } } // Set preconfigured adder if ($this->allowAdd && $this->addMethod) { $addMethod = $this->addMethod; if (!$this->isAccessible($reflClass, $addMethod, 1)) { throw new FormException(sprintf('The public method "%s" could not be found on class %s', $addMethod, $reflClass->getName())); } } // Set preconfigured remover if ($this->allowDelete && $this->removeMethod) { $removeMethod = $this->removeMethod; if (!$this->isAccessible($reflClass, $removeMethod, 1)) { throw new FormException(sprintf('The public method "%s" could not be found on class %s', $removeMethod, $reflClass->getName())); } } } // Calculate delta between $data and the snapshot created in PRE_BIND $itemsToDelete = array(); $itemsToAdd = is_object($data) ? clone $data : $data; if ($this->dataSnapshot) { foreach ($this->dataSnapshot as $originalItem) { foreach ($data as $key => $item) { if ($item === $originalItem) { // Item found, next original item unset($itemsToAdd[$key]); continue 2; } } // Item not found, remember for deletion foreach ($originalData as $key => $item) { if ($item === $originalItem) { $itemsToDelete[$key] = $item; continue 2; } } } } if ($addMethod || $removeMethod) { // If methods to add and to remove exist, call them now, if allowed if ($removeMethod) { foreach ($itemsToDelete as $item) { $parentData->{$removeMethod}($item); } } if ($addMethod) { foreach ($itemsToAdd as $item) { $parentData->{$addMethod}($item); } } $event->setData($childPropertyPath->getValue($parentData)); } elseif ($this->mergeStrategy & self::MERGE_NORMAL) { if (!$originalData) { // No original data was set. Set it if allowed if ($this->allowAdd) { $originalData = $data; } } else { // Original data is an array-like structure // Add and remove items in the original variable if ($this->allowDelete) { foreach ($itemsToDelete as $key => $item) { unset($originalData[$key]); } } if ($this->allowAdd) { foreach ($itemsToAdd as $key => $item) { if (!isset($originalData[$key])) { $originalData[$key] = $item; } else { $originalData[] = $item; } } } } $event->setData($originalData); } }
public function isEmpty() { foreach ($this->children as $child) { if (!$child->isEmpty()) { return false; } } return FormUtil::isEmpty($this->modelData) || 0 === count($this->modelData) || $this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData); }
public function noAdderRemoverData() { $data = array(); $propertyPath = new PropertyPath('axes'); $expectedMessage = sprintf('Neither element "axes" nor method "setAxes()" exists in class ' . '"{class}", nor could adders and removers be found based on the ' . 'guessed singulars: %s (provide a singular by suffixing the ' . 'property path with "|{singular}" to override the guesser)', implode(', ', (array) ($singulars = FormUtil::singularify('Axes')))); $data[] = array($propertyPath, $expectedMessage); $propertyPath = new PropertyPath('axes|boo'); $expectedMessage = sprintf('Neither element "axes" nor method "setAxes()" exists in class ' . '"{class}", nor could adders and removers be found based on the ' . 'passed singular: %s', 'boo'); $data[] = array($propertyPath, $expectedMessage); return $data; }