Issue 1:
@bschussek this was supposed to be a blog post, but never get published as it is too much technical ....

While migrating the FormatterBundle to Symfony2.3, some small details can take a bit longer to fix than expecting. There is one main issue with the Form Component which makes it pretty impossible for a FormType to access the different fields.

Let's explain how we try to avoid a BC break in the SonataFormatterBundle:

The Formatter Bundle provides a easy way to convert raw markup (markdow, textile, ...) into a final state. The visual representation is

In order to make it work a FormatterType is used. The type allows to define a format (mardown, html, etc ...) to be applied on a target field by reading the data from a source field. So if you have a Post entity, the entity will have :

a content field : the transformed field

a rawContent field : the source field

a contentFormatter field: the format field

As < Symfony2.3, it was possible for a FromType to retrieve the parent form builder. However from 2.3, the FormBuilder::getParent is not available anymore, due to the fact that the parent builder can be changed so it is very hard to rely on it (on some very specific edge cases).

The parent builder was useful to attach an event listener on the parent form to apply the transformation once the form is being posted.

As of Symfony2.3, The FormatterType is broken ... let's see the others options we have:

The inherit_data option can be a solution ~ from the PHP doc: "Sets whether the form should read and write the data of its parent.".

But by looking at the Form class, the Form::setData is not called if the field is defined as ‘inherit_data’. This method is responsible for notifying listeners when data is set. So by using the inherit_data, it is not possible to use FormEvents::PRE_SET_DATA event inside the FormatterType::buildForm

public function getData()
{
if ($this->config->getInheritData()) {
if (!$this->parent) {
throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
}

return $this->parent->getData();
}

if (!$this->defaultDataSet) {
$this->setData($this->config->getData());
}

return $this->modelData;
}

After digging into the Form Component source code, the internal event dispatcher is set per FormBuilder and while a FormType::buildForm is called, the callee method does not copy the event dispatcher from the FormatterType builder to the parent (when inherit_data is set to true). This can be the expected behavior as inherited_data means "Sets whether the form should read and write the data of its parent.", we can expect the event dispatcher being shared. By looking at the code, this change is not as easy as it might look. While a Form is being built by using the FormBuilder and the FormType the options are not always available.

So in order to find a solution and keep going, we introduce a BC break in the FormatterType.

The solution: pass the parent event dispatcher to the form type. This might not be the best solution, but at least it will work as before and might break on some edge cases (can someone point me to an edge case?)

The BC break and the fix: sonata-project/SonataFormatterBundle@2d1a412

Issue 2:
Probably in Symfony 2.4 I'm going to make a few changes to the form architecture to enforce a proper form lifecycle. A BC layer will be provided until Symfony 3.0.

I'm writing this mainly as instructions to my future me, but anyone interested in the topic is invited to chime in :)

Summary

Form::setData() will be deprecated in favor of Form::initialize()

Form::initialize() can only ever be called once (usually automatically by the form system) guaranteeing the best possible performance

Form::initialize() is guaranteed to be called on every element in the form tree (including those with "inherit_data" set), ensuring that all *_SET_DATA events are fired on time

side effects from Form::submit(), getData(), getNormData() and getViewData() will be removed

DataMapperInterface will be deprecated in favor of DataMapper2Interface (everybody cheer now ;) )

Question

After you read the text ( ;) ) please help me by answering the following question:

Are there use cases where calling Form::setData() is absolutely necessary and cannot be changed so that the default data is set in one of the other ways demonstrated below (last code sample)?

Motivation

Motivation 1: Dynamic Forms

The motivation behind are dynamic forms (#5807). Dynamic forms rely on setData() being called in order to manipulate the form tree dynamically. This must be done before get*Data(), submit() or createView() are called, because these methods rely on the form tree being readily initialized.

Motivation 2: *_SET_DATA events and "inherit_data"

Currently, forms with the "inherit_data" option set cannot have *_SET_DATA event listeners, because setData() is never called on these forms (#8253, #8607, #8748).

Motivation 3: Performance

Depending on the number of nested forms and the number of event listeners, setData() can be a very expensive operation. The number of setData() calls were already reduced once in d4f4038 by introducing deferred data initialization. It is still possible to call setData() more than once though. Guaranteeing a single setData() call would also guarantee an optimal performance.

Motivation 4: Remove side effects

The result of deferred data initialization is that getData(), getNormData(), getViewData() and submit() now have side effects. I would like to remove those.

Motivation 5: Inline data mapping code

Currently, when writing a custom data mapper, most of the code from PropertyPathMapper has to be duplicated. In fact, the only implementation-dependent lines are

$form->setData($this->propertyAccessor->getValue($data, $propertyPath));

and

$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());

Even worse, the other code in the data mappers is responsible for maintaining the correct lifecycle of the form. I would like to inline this critical code into Form and provide a new, more minimal data mapper interface.

Proposed Lifecycle

The proposed form lifecycle will consist of four phases, which partially overlap.

Note in advance: I'm talking about the existing low-level API here. Don't confuse it with the high-level API that you typically use in your controllers (types, options, etc.)

Phase 1: Configuration

In this phase, form builders are created and configured.

The phase is complete when a form builder is turned into a form by calling getForm(). At this point, the default data and other configuration of the form are frozen.

$builder = $factory->createBuilder()
->setData('default')
->setDisabled(true)
->setCompound(false)
// ...

Phase 2: Assembling

In this phase, the form tree is assembled by connecting the Form instances. Typically, this is done during FormBuilder::getForm(), but in dynamic forms you often need to call Form::add() and Form::remove() manually.

This phase is never really complete. The form tree can be changed in the later phases to allow a dynamic modification of the form tree.

// Configuration
$builder = $factory->createBuilder()
->add('name', 'text')
->add('email', 'email');

// Assembling
// Form instances are created, Form::add() is called
$form = $builder->getForm();

// Manual modifications
$form->add('submit', 'submit');
$form->remove('email');

Phase 3: Initialization

During initialization, the form tree is populated with default values. Initialization starts when Form::initialize($defaultData = null) is called on the root form. The call will cascade through the form tree.

After the call is complete, initialize() is guaranteed to have been called on every element in the form tree. If elements are added later on, initialize() will be called on them during Form::add().

initialize() is very similar to the current setData(), i.e. the form's data is set and *_SET_DATA events are fired. Consequently, after the initialization of a form tree, all *_SET_DATA events are fired and their effects visible.

There are three major differences between initialize() and the current setData():

First, initialize() must only be called once. Since initialize() cascades through the form tree, this implies that initialize() must only be called on the root form.

Second, the data argument in can be omitted. This happens mostly for the root form and when "mapped" is set to false. In this case, the default data will be retrieved from the form's configuration, which has been created in Phase 1.

$form = $factory->createBuilder('form', null, array('auto_initialize' => false))
->add('name', 'text')
->add('email', 'email')
// passing the data as second argument to
// FormFactory::createBuilder() or through the option
// "data" is equivalent to the below statement
->setData(array('name' => 'foo', 'email' => 'bar'))
->getForm();

$form->initialize();
// sets the default data stored above

// implies $form->get('name')->initialize('foo')
// implies $form->get('email')->initialize('bar')

If the "auto_initialize" option is not set to false, as in this example, initialize() will be called automatically (this is already happening right now for your Symfony 2.3 forms).

Third and last, contrary to the current setData(), initialized forms cannot be added as children of other forms anymore.

$form = $factory->createForm('text', null, array('auto_initialize' => false));
$form->initialize('foo');

$otherForm->add($form); // boom

$form = $factory->createForm('text');

$otherForm->add($form); // boom, auto initialized

Phase 4: Submission

After a form tree is readily initialized and all *_SET_DATA events triggered, the form can be submitted. Forms must not be submitted before initialization is complete.

// Phase 1 and 2 inside the form type
// Phase 3 due to automatic initialization
$form = $this->createForm('my_type');

// Phase 4
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
// ...
}

The only difference will be that there will most likely be an "auto_submit" option with a default of true, sparing you of calling handleRequest() manually.

// Phase 1 and 2 inside the form type
// Phase 3 due to automatic initialization
// Phase 4 due to automatic submission
$form = $this->createForm('my_type');

if ($form->isSubmitted() && $form->isValid()) {
// ...
}

Implementation

A few details on what changes are necessary to make the above happen.

Introduce new DataMapperInterface

A new data mapper interface will be introduced. Due to BC, it will have a very uninspring name:

interface DataMapper2Interface
{
/**
* Returns true if the data is considered empty. The
* preconfigured default value will be used in this case.
*
* @param mixed $data
*
* @return Boolean
*/
public function isEmpty($data);

/**
* Returns whether the data can be read. For example,
* an ArrayAccessMapper will return true if and only if
* $data is an array.
*
* @param mixed $data
*
* @return Boolean
*/
public function isReadable($data);

/**
* Returns a textual representation of the types that
* this mapper can read, which will be included in the
* error message generated if isReadable() returns false.
*
* @return string
*/
public function getReadableTypes();

/**
* Returns the mapped value from $data by accessing $path.
* $path can be anything, depending on the implementation
* (e.g. property path, XPath, array index etc.)
*
* @param mixed $data
* @param mixed $path
*
* @return mixed The value read from $data
*/
public function readValue($data, $path);

/**
* Writes a value into $data.
*
* @param mixed $data
* @param mixed $path
* @param mixed $value
*/
public function writeValue($data, $path, $value)
}

This mapper can be used alternatively to an implementation of the current DataMapperInterface. The other code contained in current DataMapperInterface implementation will be executed by Form directly (see Motivation 5).

Call initialize() on forms with "inherit_data" true

Once the data mapping code is inlined into Form, initialize() can also be called on forms with the "inherit_data" option set (see Motivation 2). This is currently impossible, because the current data mappers only have access to the parent's view data, but need to pass the parent's model data instead.

Remove side effects

The side effects from getData(), getNormData(),

getViewData() and submit() should be removed. Instead, exceptions should be thrown.

// old
if (!$this->defaultDataSet) {
$this->setData($this->getConfig()->getData());
}

// new
if (!$this->defaultDataSet) {
throw new RuntimeException('getData() must be called after initializing the form. Make sure "auto_initialize" is not disabled on the root form.');
}

This is possible, because initialize(), which is guaranteed to be called, also guarantees to fill the form with the appropriate default data.

Deprecate setData()

As last step, setData() will be deprecated. Consequently, you should only set default data during the configuration phase by one of the already existing means:

// passed to the factory
$form = $factory->create('form', $default);

// set on the builder
$form = $factory->createBuilder('form')
->setData($default)
->getForm();

// passed as option
$form = $factory->create('form', null, array('data' => $default));
$form->add('field', 'text', array('data' => $default));

// deprecated
$form->setData($default);

If you made it up to here, you have my full respect :) Please don' forget to answer my question above.