Created
July 6, 2012 06:07
-
-
Save merk/3058342 to your computer and use it in GitHub Desktop.
Symfony2 Form Polycollection for use with objects in an inheritance structure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// ... | |
class AbstractInvoiceLineType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
// ... | |
$builder->add('_type', 'hidden', array( | |
'data' => $this->getName(), | |
'mapped' => false | |
)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/config/config.yml | |
services: | |
# ... | |
infinite_form.polycollection_type: | |
class: Infinite\Helper\Form\Type\PolyCollectionType | |
tags: | |
form_type: | |
name: form.type | |
alias: polycollection |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// src/Bundle/Form/Type/InvoiceType.php | |
// ... | |
$builder->add('lines', 'polycollection', array( | |
'types' => array( | |
'ibms_invoice_line', | |
'ibms_invoice_part_line', | |
), | |
'allow_add' => true, | |
'allow_delete' => true, | |
'by_reference' => false | |
)); | |
// ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Copyright (c) 2012 Infinite Networks | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is furnished | |
to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* (c) Infinite Networks <http://www.infinite.net.au> | |
* | |
* For the full copyright and license information, please view the LICENSE | |
* file that was distributed with this source code. | |
*/ | |
namespace Infinite\Helper\Form\Type; | |
use Infinite\Helper\Form\EventListener\ResizePolyFormListener; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\FormBuilderInterface; | |
use Symfony\Component\Form\FormInterface; | |
use Symfony\Component\Form\FormTypeInterface; | |
use Symfony\Component\Form\FormView; | |
use Symfony\Component\OptionsResolver\OptionsResolverInterface; | |
/** | |
* A collection type that will take an array of other form types | |
* to use for each of the classes in an inheritance tree. | |
* | |
* Allows you to have a collection of objects in a Doctrine Single | |
* Inheritance strategy with different form types for each of | |
* the classes in the tree. | |
* | |
* @author Tim Nagel <[email protected]> | |
*/ | |
class PolyCollectionType extends AbstractType | |
{ | |
/** | |
* {@inheritdoc} | |
*/ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$prototypes = $this->buildPrototypes($builder, $options); | |
if ($options['allow_add'] && $options['prototype']) { | |
$builder->setAttribute('prototypes', $prototypes); | |
} | |
$resizeListener = new ResizePolyFormListener( | |
$builder->getFormFactory(), | |
$prototypes, | |
$options['options'], | |
$options['allow_add'], | |
$options['allow_delete'] | |
); | |
$builder->addEventSubscriber($resizeListener); | |
} | |
/** | |
* Builds prototypes for each of the form types used for the collection. | |
* | |
* @param \Symfony\Component\Form\FormBuilderInterface $builder | |
* @param array $options | |
* | |
* @return array | |
*/ | |
protected function buildPrototypes(FormBuilderInterface $builder, array $options) | |
{ | |
$prototypes = array(); | |
foreach ($options['types'] as $type) { | |
$key = ($type instanceof FormTypeInterface) ? | |
$type->getName() : | |
$builder->getFormFactory()->getType($type)->getName(); | |
$prototype = $this->buildPrototype($builder, $options['prototype_name'], $type, $options['options'])->getForm(); | |
$prototypes[$key] = $prototype; | |
} | |
return $prototypes; | |
} | |
/** | |
* Builds an individual prototype. | |
* | |
* @param \Symfony\Component\Form\FormBuilderInterface $builder | |
* @param string $name | |
* @param string|FormTypeInterface $type | |
* @param array $options | |
* | |
* @return \Symfony\Component\Form\FormBuilderInterface | |
*/ | |
protected function buildPrototype(FormBuilderInterface $builder, $name, $type, array $options) | |
{ | |
$prototype = $builder->create($name, $type, array_replace(array( | |
'label' => $name . 'label__', | |
), $options)); | |
return $prototype; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function buildView(FormView $view, FormInterface $form, array $options) | |
{ | |
$view->vars['allow_add'] = $options['allow_add']; | |
$view->vars['allow_delete'] = $options['allow_delete']; | |
if ($form->getConfig()->hasAttribute('prototypes')) { | |
$view->vars['prototypes'] = array_map(function (FormInterface $prototype) use ($view) { | |
return $prototype->createView($view); | |
}, $form->getConfig()->getAttribute('prototypes')); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function finishView(FormView $view, FormInterface $form, array $options) | |
{ | |
if ($form->getConfig()->hasAttribute('prototypes')) { | |
$multiparts = array_filter( | |
$view->vars['prototypes'], | |
function (FormView $prototype) { | |
return $prototype->vars['multipart']; | |
} | |
); | |
if ($multiparts) { | |
$view->vars['multipart'] = true; | |
} | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setDefaultOptions(OptionsResolverInterface $resolver) | |
{ | |
$resolver->setDefaults(array( | |
'allow_add' => false, | |
'allow_delete' => false, | |
'prototype' => true, | |
'prototype_name' => '__name__', | |
'types' => array(), | |
'type_name' => '_type', | |
'options' => array(), | |
)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getName() | |
{ | |
return 'polycollection'; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// src/Infinite/Helper/Form/EventListener/ResizePolyFormListener.php | |
/* | |
* (c) Infinite Networks <http://www.infinite.net.au> | |
* | |
* For the full copyright and license information, please view the LICENSE | |
* file that was distributed with this source code. | |
*/ | |
namespace Infinite\Helper\Form\EventListener; | |
use Assert\Assertion; | |
use Symfony\Component\Form\Exception\UnexpectedTypeException; | |
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener; | |
use Symfony\Component\Form\FormEvent; | |
use Symfony\Component\Form\FormFactoryInterface; | |
use Symfony\Component\Form\FormInterface; | |
use Symfony\Component\Form\FormTypeInterface; | |
use Symfony\Component\Security\Core\Util\ClassUtils; | |
/** | |
* A Form Resize listener capable of coping with a polycollection. | |
* | |
* @author Tim Nagel <[email protected]> | |
*/ | |
class ResizePolyFormListener extends ResizeFormListener | |
{ | |
/** | |
* Stores an array of Types with the Type name as the key. | |
* | |
* @var array | |
*/ | |
protected $typeMap = array(); | |
/** | |
* Stores an array of types with the Data Class as the key. | |
* | |
* @var array | |
*/ | |
protected $classMap = array(); | |
public function __construct(FormFactoryInterface $factory, array $prototypes, array $options = array(), $allowAdd = false, $allowDelete = false) | |
{ | |
foreach ($prototypes as $prototype) { | |
/** @var FormInterface $prototype */ | |
$dataClass = $prototype->getConfig()->getDataClass(); | |
$types = $prototype->getConfig()->getTypes(); | |
$type = end($types); | |
$typeKey = $type instanceof FormTypeInterface ? $type->getName() : $type; | |
$this->typeMap[$typeKey] = $type; | |
$this->classMap[$dataClass] = $type; | |
} | |
$defaultTypes = reset($prototypes)->getConfig()->getTypes(); | |
$defaultType = end($defaultTypes); | |
parent::__construct($factory, $defaultType, $options, $allowAdd, $allowDelete); | |
} | |
/** | |
* Returns the form type for the supplied object. If a specific | |
* form type is not found, it will return the default form type. | |
* | |
* @param object $object | |
* @return string | |
*/ | |
protected function getTypeForObject($object) | |
{ | |
$class = get_class($object); | |
$class = ClassUtils::getRealClass($class); | |
if (array_key_exists($class, $this->classMap)) { | |
return $this->classMap[$class]; | |
} | |
return $this->type; | |
} | |
/** | |
* Checks the form data for a hidden _type field that indicates | |
* the form type to use to process the data. | |
* | |
* @param array $data | |
* @return string|FormTypeInterface | |
* @throws \InvalidArgumentException when _type is not present or is invalid | |
*/ | |
protected function getTypeForData(array $data) | |
{ | |
if (!array_key_exists('_type', $data) or !array_key_exists($data['_type'], $this->typeMap)) { | |
throw new \InvalidArgumentException('Unable to determine the Type for given data'); | |
} | |
return $this->typeMap[$data['_type']]; | |
} | |
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) { | |
$type = $this->getTypeForObject($value); | |
$form->add($this->factory->createNamed($name, $type, null, array_replace(array( | |
'property_path' => '['.$name.']', | |
), $this->options))); | |
} | |
} | |
public function preBind(FormEvent $event) | |
{ | |
$form = $event->getForm(); | |
$data = $event->getData(); | |
if (null === $data || '' === $data) { | |
$data = array(); | |
} | |
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { | |
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); | |
} | |
// 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)) { | |
$type = $this->getTypeForData($value); | |
$form->add($this->factory->createNamed($name, $type, null, array_replace(array( | |
'property_path' => '['.$name.']', | |
), $this->options))); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% form_theme form.lines _self %} | |
{% block form %} | |
{# ... #} | |
<div class="row"> | |
<div class="span12"> | |
<fieldset class="lines"> | |
<legend>{{ type }} Lines</legend> | |
{{ form_row(form.lines) }} | |
</fieldset> | |
</div> | |
</div> | |
{# ... #} | |
{% endblock form %} | |
{% block polycollection_row %} | |
<table class="collection table"> | |
<thead> | |
<tr> | |
<th>Description | |
<th>Quantity | |
<th>Unit Price | |
<th class="span2">Options | |
<tfoot> | |
<tr> | |
<td colspan="1"> | |
<td colspan="3"> | |
{% for type, line in form.getVar('prototypes') %} | |
<a class="add_item btn btn-primary line_{{ type }}" data-prototype="{{ form_row(line) | escape }}" href="#"><i class="icon-plus icon-white"></i> Add {{ type }}</a> | |
{% endfor %} | |
<tbody class="items"> | |
{{ form_widget(form) }} | |
</table> | |
{% endblock polycollection_row %} | |
{% block infinite_invoice_line_row %} | |
{# HTML for a basic invoice line with description, quantity and price #} | |
{% endblock infinite_invoice_line_row %} | |
{% block infinite_invoice_job_line_row %} | |
{# HTML for a job invoice line with job identifier/number and price #} | |
{% endblock infinite_invoice_job_line_row %} |
Expecting to release a bundle for this in the next week or so.
News?
Bundle has been released as InfiniteFormBundle.
https://github.com/infinite-networks/InfiniteFormBundle
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@merk Bundle ? :-)