To implement your own form fields, you need to have a thorough understanding
of the data flow within a form field. A form field stores its data in three
different representations:
(1) the format required by the form's object
(2) a normalized format for internal processing
(3) the format used for display
A date field, for example, may store a date as "Y-m-d" string (1) in the
object. To facilitate processing in the field, this value is normalized
to a DateTime object (2). In the HTML representation of your form, a
localized string (3) is presented to and modified by the user.
In most cases, format (1) and format (2) will be the same. For example,
a checkbox field uses a Boolean value both for internal processing as for
storage in the object. In these cases you simply need to set a value
transformer to convert between formats (2) and (3). You can do this by
calling setValueTransformer() in the configure() method.
In some cases though it makes sense to make format (1) configurable. To
demonstrate this, let's extend our above date field to store the value
either as "Y-m-d" string or as timestamp. Internally we still want to
use a DateTime object for processing. To convert the data from string/integer
to DateTime you can set a normalization transformer by calling
setNormalizationTransformer() in configure(). The normalized data is then
converted to the displayed data as described before.
/** * {@inheritDoc} */ public function bind($data) { if ($this->mode === self::GROUP) { parent::bind($data); } else { Field::bind($data); } }
/** * {@inheritDoc} */ protected function configure() { // default precision is locale specific (usually around 3) $this->addOption('precision'); $this->addOption('grouping', false); $this->addOption('rounding-mode', NumberToLocalizedStringTransformer::ROUND_HALFUP); parent::configure(); $this->setValueTransformer(new NumberToLocalizedStringTransformer(array('precision' => $this->getOption('precision'), 'grouping' => $this->getOption('grouping'), 'rounding-mode' => $this->getOption('rounding-mode')))); }
/** * {@inheritDoc} */ protected function configure() { $this->addOption('value'); parent::configure(); $this->setValueTransformer(new BooleanToStringTransformer()); }
/** * {@inheritDoc} */ public function writeProperty(&$objectOrArray) { $isReference = false; // If the data is identical to the value in $objectOrArray, we are // dealing with a reference if ($this->getPropertyPath() !== null) { $isReference = $this->getData() === $this->getPropertyPath()->getValue($objectOrArray); } // Don't write into $objectOrArray if $objectOrArray is an object, // $isReference is true (see above) and the option "by_reference" is // true as well if (!is_object($objectOrArray) || !$isReference || !$this->getOption('by_reference')) { parent::writeProperty($objectOrArray); } }
/** * {@inheritDoc} */ public function isEmpty() { if ($this->mode === self::FORM) { return parent::isEmpty(); } return Field::isEmpty(); }
/** * {@inheritDoc} */ public function addError(FieldError $error, PropertyPathIterator $pathIterator = null, $type = null) { if (null !== $pathIterator) { if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) { $pathIterator->next(); if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') { $pathIterator->next(); } if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) { $this->get($pathIterator->current())->addError($error, $pathIterator, $type); return; } } else { if ($type === self::DATA_ERROR) { $iterator = new RecursiveFieldIterator($this); $iterator = new \RecursiveIteratorIterator($iterator); foreach ($iterator as $field) { if (null !== ($fieldPath = $field->getPropertyPath())) { if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) { if ($pathIterator->hasNext()) { $pathIterator->next(); } $field->addError($error, $pathIterator, $type); return; } } } } } } parent::addError($error); }
/** * {@inheritDoc} */ public function submit($data) { if ($this->mode === self::FORM) { parent::submit($data); } else { Field::submit($data); } }
/** * Sets the locale of this field. * * @see Localizable */ public function setLocale($locale) { parent::setLocale($locale); foreach ($this->fields as $field) { $field->setLocale($locale); } }
protected function configure() { $this->addOption('foo'); $this->addRequiredOption('bar'); parent::configure(); }
/** * Expose method for testing purposes */ public function getNormalizedData() { return parent::getNormalizedData(); }
/** * {@inheritDoc} */ public function getAttributes() { return array_merge(parent::getAttributes(), array('type' => 'text/javascript', 'src' => $this->_src)); }
/** * {@inheritDoc} */ public function writeProperty(&$objectOrArray) { $data = $this->getData(); // Don't update parent if data is a composite type (object or array) // and "by_reference" option is true, because then we expect that // we are working with a reference to the parent's data if (!(is_object($data) || is_array($data)) || !$this->getOption('by_reference')) { parent::writeProperty($objectOrArray); } }
/** * Distributes the generator among all nested fields * * @param HtmlGeneratorInterface $generator */ public function setGenerator(HtmlGeneratorInterface $generator) { parent::setGenerator($generator); // TESTME foreach ($this->fields as $field) { $field->setGenerator($generator); } }