📜 ⬆️ ⬇️

Forms Architecture in Symfony2

Picture to attract attention

Symfony 2 has a completely new component for working with forms , which, as far as I know, will easily replace most of these libraries for PHP both in functionality and, if possible, in extending it (of course, if you do not take into account small flaws when working with JavaScript . The development of this component took more than two years, although I started thinking about it somewhere else in 2009 or even earlier. With each new version, this component becomes more and more stable, and a completely stable version is expected with the release of Symfony 2.2.

This post is dedicated to the release of Zend Framework 2 Form RFC, since it seems to me that its developers, in fact, have done a lot of what has already been done by us. Of course, it is clear to everyone that Zend Framework 2 should have an interlayer for working with forms, which fully takes into account the features of the components supplied with the framework. The goal of this post is to attempt to show that Symfony2 Forms fits these requirements perfectly. The functionality inherent in Symfony2 can be easily removed: the code for processing forms and all levels of abstractions are completely independent. Attaching the same support for the features of the Zend components is also not difficult.
')
Creating a generic library for working with forms, which covers all possible use cases arising during development, was not an easy test, a long and complicated affair, which, moreover, has not yet been completed. Collaboration and continued collaborative development should help achieve more flexible and simple form management from PHP.


We begin this post, perhaps, with thanks to all those who participated in the development of frameworks and libraries for processing forms that influenced our work. Then, before describing the architecture itself, I would like to familiarize you with the key aspects of designing a convenient and flexible component for working with forms.

In this post you will not find examples of the use of "killer features" of this component, for this there is documentation . Also, you will not find here any information on its use, so to speak, out of context, regardless of the framework. Such information should be described, for example, in Gist .


Influence


The form component has been influenced by a variety of frameworks written in various languages, including symfony 1 , Zend Framework 1 , Django , Ruby on Rails , Struts and JSF . In addition, it contains similarities with Formlets , WUI, and iData , libraries for working with forms written for functional programming languages ​​such as Links, Curry, and Clean.

Key aspects


Key aspects in designing a component for working with forms are:


Let's briefly go through each of these aspects before discussing how the implemented architecture covers them.

Increased level of abstraction


Raising the level of abstraction allows you to take any part of the form — even the entire form — and place it in a data structure that you can then use again. Consider a date selection form in which there are three drop-down lists for selection by day, month, and year. First you need a code that will generate HTML with all the options drop-down lists. Then, you need code that converts data from the format of your application (for example, a DateTime object if we are talking about PHP) into a format suitable for our presentation (in order to mark the selected value of the lists). Of course, you will need the reverse procedure. Now imagine a situation where you have to add another drop-down list for some additional data. In this case, you will have to duplicate all the code for this list and adapt it to the new requirements.

Introducing abstraction solves this problem by providing suitable structures for describing and reusing your code.

Extensibility


Extensibility is based on two concepts related to abstraction:
  1. Specialization is a logical sequence of abstractions. If it is possible to abstract the functional into generalized data structures, then they can also be expanded into more specialized structures. For example, we can extend the above example with the selection of a date by adding fields for choosing the time. If it were not possible to expand the functionality of the existing field to select a date, then we would have to rewrite a substantial part of the functionality.
  2. Impurities are the opposite of specialization. For example, you wanted to change all existing fields so that there would be an asterisk ("*") in their descriptions, which would show that these fields are required. Using a specialization approach for this is somewhat impractical, since you will have to expand all the available fields , implementing the same functionality. Impurities allow you to connect functionality to existing objects without having to redefine them. In addition, the functionality added in this way will be inherited for all child objects in the inheritance tree.


Extensibility also refers to a more in-depth expansion of the functional, with the help of events. This we will consider later.

Linker


If we analyze the last example, we can notice that there is no particular difference between the fields ( composite fields , such as in the considered example, or primitive ones , such as text input field) and the forms are not present. And fields and forms
  1. Accept default values ​​from models (from arrays, from dates, from strings ...)
  2. Transform values ​​so that it can be used in a view.
  3. Generate HTML
  4. Accept user input
  5. Convert value back to model format.
  6. Can validate data


We can implement fields and forms using the same fundamental data structures. By adding a linker, a tool that allows you to add nested structures (see the Linker design pattern), we can create a form of any complexity. Instead of delimiting the fields and forms, let's talk about the forms and their constituent parts . As soon as a form has a descendant, it must also
  1. pass default values ​​(array or object) to its descendants
  2. transfer the input values ​​of each child back to the array or object


Separation of tasks

We can divide the tasks described above into several specific groups:


Each of these groups of tasks must be implemented by a separate component with an explicitly defined interface. This will allow you to replace any of these components with your own: for example, replace the view processor, or, for example, the validation layer.

Model binding


In most cases, forms are directly tied to structures, one way or another, described in the domain model . Consider the example of sending user profile information. Suppose that all user information is stored in a separate table in your database. The table stores information about the properties that the profile contains, which data type is used for each of the properties , their default values and data entry restrictions . Ideally, your application should also contain the Profile class with all the necessary properties , linked to a table in the database, for example, by means of the Doctrine2 ORM . This class may contain more information about the profile than is stored in the table, for example, if a profile contains a set of items of interest to the user. For simplicity, we will take the list of these items from the configuration file.

Usually, information describing an object (let's call it metadata ) should be duplicated in a layer of forms. The developer has to make sure that the fields have not changed, which of them can be edited, make sure that the form displays the corresponding HTML widgets, that some of the fields could not remain empty, and so on. It is because of these things that the creation of forms becomes a rather ungrateful thing.

Binding models designed to make a difference. It is based on two ideas:
  1. Use existing metadata during form creation in order to reduce the amount of duplicate code and / or settings.
The default values should be obtained from the domain model (in our case it will be an instance of the Profile class) and the input data should also be recorded in our model.


Dynamic behaviors

Last but not least, these are dynamic behaviors for forms. Gone are the days when the code for the form was fully implemented on the server, where a check on the security of the data entered by the user was carried out under rigidly defined structures. Today it is fashionable to use JavaScript to change forms on the client side to improve the usability of the system.

Imagine a form in the form of a table. Each column has fields of the same type, each row is an object on the server. The form has buttons that allow you to add or delete rows. With all this, the possibility of validation of forms should remain, they must be successfully processed on the server.

Dynamic behaviors were invented to implement such a form. Moreover, their use is not limited to relatively simple tabular forms. You can create a form that responds to any changes in its structure made on the client side. Unfortunately, this problem is not solved in most libraries, despite the demand for such mechanisms.

High level architecture



High level architecture


Let me provide a high-level description of the layout architecture in Symfony2, designed to work with forms. Its cornerstone is the Form component. This component implements the basis for building and processing forms, as well as using the Symfony2 event dispatcher to handle internal events. Over this component is a layer of plug-in extensions:



The top level contains components that implement the formation of HTML-i. By default, Symfony2 has two such components: one for rendering forms through Twig (supplied in the Twig bridge ) and the other for rendering through the good old PHP (supplied with FrameworkBundle ).

Probably the most interesting feature of this architecture is that any of the components can be replaced. You can write an extension so that the forms use the validation component from Zend. You can also write a view component that implements rendering through Smarty or any other template engine. You can even remove the kernel extension and write your basic form types for simple fields. Even the underlying event dispatcher can be replaced with your component that implements the EventDispatcherInterface interface . This gives a good advantage in flexibility, despite some inconveniences.

Low-level architecture



This section discusses the internal architecture of the form component. As mentioned above, the form and all its parts are represented in the form of the same data structure that implements the “Linker” design pattern. In the Form component, this structure is described by the FormInterface interface. The main class that implements this interface is the Form class, which uses three components, with the help of which the whole thing works:


All these components are passed to the Form object constructor and cannot be changed after the form is created, as this may lead to undesirable changes in the form state. Since the interface of the designer of the Form object is quite complex, the FormBuilder was implemented, which simplifies the creation of instances of these objects.

A form view is a data structure that describes a view. That is, instead of calling the Form class directly in templates, you will work with an instance of the FormView class. This object stores additional information that is specific only for presentation, such as name attributes for HTML elements of forms, their IDs, etc.

The UML diagram below shows the essence of the architecture.

UML architecture diagram

As you can see from this diagram, the life cycle of a form consists of three different views:



Since configuring form constructors their presentation is quite tedious, Symfony2 implements the basic types of forms that already implement the basic settings. Form types support dynamic inheritance. This means that you can extend various base types based on the options passed to the form constructor. The following diagram shows all types supplied with Symfony2 (types marked in green come with the kernel, while yellow types come with additional bundles):

Type hierarchy

Impurities, which we mentioned earlier, are implemented in SYmfony2 as so-called type extensions. These extensions can be connected to existing types of forms and add some behavior to them. In Symfony2, for example, there are extensions to add CSRF protection, applied to the type “form” (and as a result, all types that inherit this type).

The form factory retrieves the type hierarchy from the loaded extensions, and uses it to customize the FormBuilder and FormView objects . It is important to note that this stage can be controlled. For example, the type " choice " allows you to specify in the settings attribute " choices ", which contains all the values ​​for the options that should be displayed.

There is another rather important concept of the Form component - type predictors . These "predictors" are trying to predict the type and options of the form field, based on the metadata taken from the object of the domain of definition to which the form is attached (if, of course, it is fixed). For example, if any of the properties of our object contains a one-to-many relationship between the Tag object, type predictors will automatically configure this form field for this property so that it is a selection field, with the ability to select multiple values, as well as will use all instances of Tag objects as options for this field. A similar concept is used in ModelForms in the Django framework. True, there is one rather large difference: type predictors use metadata provided not only by ORM, but also all object metadata. Symfony2 comes with three type predictors: one uses metadata provided by Doctrine2, the other is provided by Propel, and another uses validation rules.

The concepts described above can be described with the following UML diagram.

image

Conclusion


What I wanted to show in this post is that the Symfony2 Form component has a well-designed architecture that covers many important aspects of form processing.

This architecture solves the problems of insufficient levels of abstractions , specialization and impurities , providing a dynamic inheritance tree of types of forms and their extensions. It solves the problem of composition , distributing the tasks of processing the form to all its elements. It also provides a clear separation of tasks between components, so you can replace them with your own. Using the metadata of the domain objects when creating a form, the described architecture implements data binding to these models, which allows reading and writing data directly. We also paid attention to dynamic behaviors , providing the ability to track certain events that occur during form processing using our handlers, for example, to validate or filter data.

Interested? Learn the code . Experiment with it . And help us integrate it into your favorite framework.

From the translator: translations of this article exist, but they seemed to me not to be of very high quality. Yes, and I think it does not hurt. There are inaccuracies in the translation, therefore I will be grateful if you point them out in the LAN. Thanks for attention.

Source: https://habr.com/ru/post/149444/


All Articles