Skip to content

Instantly share code, notes, and snippets.

@merk
Created July 6, 2012 06:07
Show Gist options
  • Save merk/3058342 to your computer and use it in GitHub Desktop.
Save merk/3058342 to your computer and use it in GitHub Desktop.
Symfony2 Form Polycollection for use with objects in an inheritance structure
<?php
// ...
class AbstractInvoiceLineType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('_type', 'hidden', array(
'data' => $this->getName(),
'mapped' => false
));
}
}
# app/config/config.yml
services:
# ...
infinite_form.polycollection_type:
class: Infinite\Helper\Form\Type\PolyCollectionType
tags:
form_type:
name: form.type
alias: polycollection
<?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
));
// ...
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.
<?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';
}
}
<?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)));
}
}
}
}
}
{% 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 %}
@adrienbrault
Copy link

@merk Bundle ? :-)

@merk
Copy link
Author

merk commented Mar 10, 2013

Expecting to release a bundle for this in the next week or so.

@marcospassos
Copy link

News?

@merk
Copy link
Author

merk commented Jul 22, 2013

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