Hi dear Symfony2 devs,

I was pushed by several people to refactor the Form component to be
more decoupled and make better use of DI. Although I wasn't really
convinced of this approach at first, I decided to give it a try - and
am pretty satisfied with the results. Most of the features have been
refactored, but there are still details that need to be worked on. You
can find the current progress here:

https://github.com/bschussek/symfony/tree/experimental

Important: This is experimental. It is not decided whether this will
ever be merged into master. I also don't recommend to use this in any
application yet as CSRF protection is not ported yet.

Thanks to Bulat for helping me.

This post will outline the problems this refactoring tries to fix and
the benefits that we gain. Also, we need to find out how to deal with
the RC that is planned for next week.

Fixed problems
----------------------
* The option implementation of Fields/Forms was buggy
* Forms could not be put into the DIC and retrieved from there
* Stubbing/mocking of Forms in controllers was impossible, because
they were created with "new"
* Dependencies where hidden inside configure() and could not be replaced
* Dependencies required in a field had to manually be added as options
of all parent forms (e.g. EntityManager)
* Field/Form by design contained lots of code that was relevant for
rendering only (getId(), getName(), getPattern() etc.)
* Because of the monolithic inheritance structure, it wasn't possible
to create fields that inherit the rendering of for example TextField
but the behaviour of another field
* Rendering was unflexible. It wasn't possible to override for example
the "row" template for a specific field type (needed for
RepeatedField, inline forms etc.)

Additional advantages
--------------------------------
* Fields and their renderers are now completely separated
* Rendering is more concise and flexible
* All fields of a specific type (e.g. text fields) used in an
application can be overridden/extended

(probably more that I forgot right now)

I want to explain how form creation and form rendering work with the
changed approach. Note that not all of the stuff I'm explaining here
is complete yet.

Form handling
---------------------
To create a form, you implement a form factory in your bundle. This
factory could look like this:

https://gist.github.com/842827

Binding will look like this:

https://gist.github.com/843094

And rendering something like this:

https://gist.github.com/843115

Rendering improvements
------------------------------------
The most interesting part I believe is the rendering. As you can see,
we passed $form->getRenderer() to the template. This renderer encloses
variables and methods that are available there. Example variables are
"id", "name", "value", "class" etc. The methods are "errors",
"widget", "label", "row" and "rest".

Each field and form has exactly one renderer. Depending on the field
type, this renderer has additional variables set that can be used in
the template. The renderer of a money field, for example, offers the
additional variable "money_pattern". The renderer of a form offers
"fields", "visible_fields" etc. All these variables can either be
statically set on the renderer by calling setVar(), or dynamically
using renderer plugins. They can also be overridden when calling
individual methods of the renderer, like "widget".

In form themes, these variables are available as global variables in
the blocks of the field:

{% block money_field %}
    {{ money_pattern }}
{% endblock %}

In normal templates, you can access the variables as properties of the
field renderer:

{# Assuming the field "price" is a money field #}
{{ form.price.money_pattern }}

As said above, each renderer also offers rendering methods. With

{{ field.widget }}

for example, you can output the widget of a field. You can also
override any of the variables in the call.

{{ field.wigdet({ 'money_pattern': '...' }) }}

Unlike before, there is no difference between HTML attributes and
template variables anymore. Each HTML attribute that should be
settable must be available as variable, for example:

{{ field.wigdet({ 'class': 'myclass' }) }}

{% block money_field %}
<div class="{{ class }}"> etc.

Because renderers are simple PHP objects, you can extend them and add
functionality. You could implement this renderer

https://gist.github.com/843139

and create a new form theme with a "help" block. You can then call

{{ field.help('My help text') }}

on all fields that have your custom renderer set. It is also possible
to override the renderers of all core fields to add functionality
globally.

An interesting default rendering method is "rest" (not implemented
yet, if you have better names tell me). Because each field has a
stateful renderer, we can track which field was rendered at least
once. With

{{ form.rest }}

you can render all fields that weren't explicitely rendered. This
replaces the "hidden" helper and solves lots of other problems. If
you, for example, forgot to render the hidden fields in a subform,
these will be rendered here. If you added a field in the PHP code, but
not in your template, it will be rendered here. The designer will
notice the problem and manually render the field correctly.

What about PHP rendering?
----------------------------------------
So far I've only talked about Twig. All renderers are connected to a
theme (an instance of ThemeInterface). By default, the component ships
with TwigTheme. By exchanging this object, you can either change the
template(s) used as themes or the whole theming engine. You could
implement a SmartyTheme that renders form fields using Smarty. Note
that the theming engine of the form and the templating engine where
you render the form are independent!

For example, currently there is only a TwigTheme, but you can render
it in PHP template nevertheless. Just pass the form renderer to the
template and use it like in Twig:

<?php echo $form->getWidget() ?>
<?php foreach ($form['visible_fields'] as $field): ?>
etc.

I will cut this post for now. See my implementation if you want to learn more.

Conclusion
----------------
Because of DI, lots of problems in the form architecture could be
fixed and the rendering could be significantly improved. The reason
for this is that Twig and other dependencies can now be injected.

Of course, there are problems too: The biggest one is that RC1 is
planned for next week. Most of the existing unit tests are green
again, but there are still lots of smaller things that need to be
fixed/worked on, and this won't be done next week.

I am very confident though that this refactoring can be finished very
soon. Most of the existing features have been ported, for the
remaining ones (FieldFactoryGuesser, CSRF protection and
CollectionField) I don't see any showstoppers - it just needs to be
done.

What I need:
------------------
* to know whether you want to see this in the first Symfony2 release.
Delivering it in later releases will be difficult because this is a
major BC break
* input. I have considered a lot of feedback in this refactoring that
I have got lately, but more input is always better.
* help. If you have time to help, you are very welcome.


Best wishes,
Bernhard

--
Software Architect & Engineer
Blog: http://webmozarts.com
Twitter: http://twitter.com/webmozart

-- 
If you want to report a vulnerability issue on symfony, please send it to 
security at symfony-project.com

You received this message because you are subscribed to the Google
Groups "symfony developers" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/symfony-devs?hl=en

Reply via email to