Skip to content

Instantly share code, notes, and snippets.

@HeahDude
Last active February 29, 2016 08:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HeahDude/0515f6747b39c110f450 to your computer and use it in GitHub Desktop.
Save HeahDude/0515f6747b39c110f450 to your computer and use it in GitHub Desktop.
CollectionType refactoring

In those examples, we assume calling the builder from a form type class definition or a controller, using help of this closure :

$class = function ($object) {
    $FQCN = get_class($object);
    $class = explode('\\', $FQCN);

    return end($class);
};

entry_type_map

Types: string, callable or Symfony\\Component\\Form\\FormTypeInterface Default: Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType (BC)

A string keeps the old behavior, a legacy logic so full BC here.

When it is a callable, it will be passed each entry as first argument and the index as second argument, allowing to use a polymorphic collection :

// given:
abstract class \CoreBundle\Entity\Ticket { ... }

class \AppBundle\Entity\Issue extends \CoreBundle\Entity\Ticket {}
class \AppBundle\Entity\PullRequest extends \CoreBundle\Entity\Ticket {}

class Todo
{
    // ...
    $tickets;
}

// use:
// TodoFormType::buildForm() with a private EntityManager property
$collection = $this->em->getRepository('CoreBundle\Entity\Ticket')->findAll();

$builder->add('tickets', CollectionType::class, array(
        'entry_type_map' => function ($entry) {
            if ($entry instanceof \AppBundle\Entity\Issue) {
                return 'CoreBundle\Form\Type\\'.ucfirst($entry->getState()).'IssueType';
            }

            return 'AppBundle\Form\PullRequestType',
        },
        'data' => $collection,
    ));

Note

Its usage should deprecate the entry_type option.

entry_index

Types: null, string, callable or Symfony\\Component\\PropertyAccessor\\PropertyPath Default: entry keys or numerical index (BC)

The index name is a string defining the name of the field in the form.

It should be unique for each entry. If generated indexes are not unique, an incrementing numeric index will be used instead.

A string will be converted to PropertyPath:

$builder->add('tickets', CollectionType::class, array(
        'entry_type_map' => function ($entry) {
            return 'AppBundle\Form\Type\\'.$class($entry).'Type';
        },
        'entry_index' => 'ref',
    ));

Behind the scene, the Symfony\\Component\\Form\\Extension\\Core\\Listener\\ResizeCollectionListListener will perform (maybe not not exactly in that call but this is the idea of what's happening) :

$builder->add($entry->getRef(), $collection->getTypeforEntry($entry), $options);

for each entry of the collection.

Defined as a callable, it get passed each entry as only argument :

$builder->add('tickets', CollectionType::class, array(
        'entry_type_map' => 'AppBundle\Form\Type\TicketType',
        'entry_index' => function ($entry) {
            return 'ticket_'.$entry->getOptionLabel());
            // could return 'ticket_symfony_symfony_pull_14050'
        },
    ));

Tip

By extension this is also the default label when humanized in form theme. So it should stay shorter when intended, unless relying on dynamic entry_options option to set the label.

Note

Using the second parameter in the callable of entry_type_map option may be usefull when you define it with the entry_index option :

$builder->add('tickets', CollectionType::class, array(
    'entry_type_map' => function ($entry, $index) {
        if (false !== strpos($index, 'pull') {
            return 'CoreBundle\Form\Type\PullRequestType';
        }

        return 'Appbundle\Form\EditIssueType';
    },
    'entry_index' => function ($entry) {
        return $entry->getOptionLabel;
        // would return "symfony_symfony_issue_9835"
        // and also "symfony_symfony-docs_issue_6144"
    },
    'data' => array_merge($collection, $docTickets()),
));

It is then possible to directly merge collections of any type easily, since $docTickets and $collection may have identic keys the merge will increment the keys. The option entry_index allow an easier mapping when using allow_add option.

entry_options

Types: array or callable Default: array() (BC)

Default array keeps BC.

Can also be a callable which get the entry passed as first argument, the field form type class as second, and the computed name with entry_index or the original index by default as third.

$builder->add('tickets', CollectionType::class, array(
        'entry_type_map' => ChoiceType::class,
        'entry_index' => 'optionLabel',
        'entry_options' => function ($entry, $type, $index) {
        // $type = $collection->getTypeforEntry($entry)
        // $index = $entry->getOptionLabel()
            $options = array(
                'data_class' => $entryClass = get_class($entry),
                'label' => 'Tickets:',
            );

            if ('CoreBundle\Form\Type\IssueType' === $type) {
                $options['choices'] = \CoreBundle\Entity\Issue::$states,
                'expanded' => true,
                $options['choice_attr'] = array('class' => 'option-issue');

                if ($entry instanceof \AppBundle\Entity\Issue) {
                    $options['multiple'] = true;
                }
            }

            if ('AppBundle\Form\Type\PullRequestType' === $type) {
                $option['choice_attr'] = array('data-ref' => $index);
            }

            return $options;
        },
    ));

Note

When this option is a callable, deprecated prototype_data option or the data key in prototype_options option should be defined, so it can be managed when passed to it, otherwise null case has to be handled in the callable.

locked_entries (todo)

Types: null, string, array, callable or Symfony\\Component\\PropertyAccessor\\PropertyPath Default: null (BC)

A string or array define one or more indexes that cannot be removed when using allow_delete or delete_empty.

filter_collection (or filter_entries) (todo)

Types: null, string, callable, or Symony\Component\PropertyAccessor\PropertyPath Default: null

Same as filter_choices see this issue.

A callable taking each entry as only argument. Or a string or a PropertyPath instance applied on each entry.

All cases must return a boolean, true to keep the entry in the collection, false to discard it.

empty_collection (todo)

same as empty_choices option of ChoiceType.

prototype_data (todo)

Should be deprecated in favor of prototype_options.

For BC it should be allowed to be a callable or an array so data depends on the form type.

As for allow_add, allow_delete, delete_empty.

prototype_options

Types: array or callable Default: value of entry_options

Overrides the name (index) option with prototype_name value.

prototype_index (todo)

Types: string, array or callable Default: __name__

Default use Symfony\Component\Form\Util\StringUtil::fqcnToBlockPrefix() to prefix default "__name__" when the collection is polymorphic.

When it is a callable it takes the form type FQCN as only argument.

If it is an array, keys must be FQCN and values must be prototype names. A value without index (or with the integer 0 as key) will be used as default for all form types not provided as keys.

Note

Sould deprecate prototype_name option.

allow_add, allow_delete and delete_empty (todo)

Types: Boolean, array or callable Default: false

When boolean, use the old way.

When using a polymorphic collection pass an array with form types FQCN as keys and boolean as values or a callable which takes the form type FQCN as only argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment