Formularze w Symfony – budowa i konfiguracja

Ile nerwów zjadły te niby proste elementy aplikacji, jakimi są formularze. Dopóki zawierają pola podstawowe, nie jest ich wiele i nie zawierają jakieś zaszytej logiki, to nie ma problemu. Jednak jeśli przychodzi do bardziej skomplikowanych układów, zależności między elementami, a co gorsza zależności między formularzami umiejscowionymi na kolejnych stronach np. w jakimś flow to zaczyna się jazda bez trzymanki. Formularze w Symfony to nie jest prosta sprawa. W tym tekście poznasz pare trików jak sobie z nimi radzić. Jeśli jednak nie sa one dla Ciebie tajemnicą zapraszam do tekstu o walidacji.

Formularze w Symfony  – etatowa rzeczywistość

W aktualnej pracy uchodzę za mistrza formularzy, ponieważ cześć systemu przede mnie utrzymywana opiera się właśnie głównie na nich i przetwarzaniu danych z nich wychodzących zgodnie ze skomplikowaną biznesową logiką. Czy robiliście kiedyś formularz, gdzie elementy zależą od siebie (ich prezentacja, jak i budowa)? Może spotkaliście się np. z 7 formularzami, które następowały po sobie, a ich struktura była zależna od konfiguracji danych wprowadzonych we wcześniejszych z nich? Brzmi trochę strasznie, prawda? Niestety to moja etatowa rzeczywistość.

Budowa

Formularze w Symfony możemy budować w wielu miejscach, może to być bezpośrednio kontroler, jednak ja polecam inne podejście, stwórz odpowiedni katalog w bundlu np. EventBundle/Form. Umiejscowienie plików w osobnym katalogu wprowadza ład i sprawia, że możemy używać tego samego formularza w wielu miejscach z zachowaniem zasady DRY.

Plik z definicją formularza w moim przypadku EventFormType musi dziedziczyć z AbstractType i wygląda następująco:

<?php

namespace EventBundle\Form\Type;

 * Class EventFormType
 * @package EventBundle\Form\Type
 */
class EventFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add(
                'type',
                ChoiceType::class,
                [
                    'choices' => $this->typeChoiceOptionsProvider->getChoiceOptions(),
                    'expanded' => false,
                    'multiple' => false,
                    'label' => false,
                    'choice_translation_domain' => 'messages',
                ]
            )
            ->add(
                'startDate',
                TextType::class,
                [
                    'label' => false,
                    'required' => true,
                ]
            )
            ->add(
                'endDate',
                HiddenType::class,
                [
                    'label' => false,
                ]
            )
            ->add('comment', TextareaType::class,
                [
                'label' => false,
                'required' => false
                ]
            )
            ->add(
                'isCyclic',
                CheckboxType::class,
                [
                    'label' => 'event.cyclic',
                    'required' => false,
                    'attr' => [
                        'class' => 'cyclic-control',
                    ],
                ]
            )
            ->add(
                'cyclicEndDate',
                TextType::class,
                [
                    'label' => false,
                    'required' => false,
                ]
            )
            ->add(
                'cyclicInterval',
                ChoiceType::class,
                [
                    'choices' => $this->cyclicIntervalOptionsProvider->getChoiceOptions(),
                    'expanded' => false,
                    'multiple' => false,
                    'label' => false,
                    'choice_translation_domain' => 'charts',
                ]
            )
            ->add(
                'rotary',
                CheckboxType::class,
                [
                    'label' => 'group.rotary',
                    'required' => false,
                    'attr' => [
                        'class' => 'rotary-control',
                    ],
                ]
            )
            ->add(
                'firstRotaryGroup',
                ChoiceType::class,
                [
                    'choices' => $this->rotaryGroupChoiceOptionsProvider->getChoiceOptions(),
                    'expanded' => false,
                    'multiple' => false,
                    'label' => false,
                    'choice_translation_domain' => 'messages',
                ]
            )
            ->add(
                'users',
                ChoiceType::class,
                [
                    'choices' => $this->userChoiceOptionsProvider->getChoiceOptions(),
                    'expanded' => false,
                    'multiple' => true,
                    'label' => false,
                    'required' => false,
                    'choice_translation_domain' => 'messages',
                    'attr' => [
                        'class' => 'select2 form-control',
                    ],
                ]
            )
            ->add(
                'groups',
                ChoiceType::class,
                [
                    'choices' => $this->groupChoiceOptionsProvider->getChoiceOptions(),
                    'expanded' => false,
                    'multiple' => true,
                    'label' => false,
                    'required' => false,
                    'choice_translation_domain' => 'messages',
                    'attr' => [
                        'class' => 'select2 form-control',
                    ],
                ]
            )
            ->add(
                'obligatoryUsers',
                CollectionType::class,
                [
                    'allow_add' => true,
                ]
                )
            ->add('save', SubmitType::class, ['label' => 'Save'])
            ->add('saveInCycle', SubmitType::class, ['label' => 'Save in cycle']);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => EventModel::class,
                'validation_groups' => [
                    EventModel::class,
                    'getValidationGroups',
                ],
            ]
        );
    }
}

Pominęłam konstruktor, abyś skupił się jedynie na budowie formularza, więc nie zwracaj uwagi na odwołania do zmiennych będących serwisami, o tym w innym poście.

Modele

Elementem, na który chciałabym, abyś zwrócił uwagę, jest data_class w metodzie configureOptions. Dlaczego? Ponieważ jest kilka podejść do klas będących modelami, które obsługują formularze w Symfony. Opcji jest kilka:

  • Encja
  • Tablica
  • Dedykowany model

Z mojego doświadczenia szczerze polecam Ci stworzenie dedykowanego modelu danych dla formularza.

Po pierwsze masz całkowitą kontrolę nad danymi w formie obiektowej, które są znacznie wygodniejsze od tablicy. Po drugie encja obiektu, tak jak u mnie wydarzenia nie zawsze będzie się pokrywać w 100% z formularzem. Co, jeśli będę potrzebować dodatkowych pól niebędących cechami encji? Dla przykładu pole isCyclic w powyższym formularzu. Po co encja wydarzenia ma mieć flagę, że akurat to dane wydarzenie jest cykliczne, nie wystarczy wpis z kodem cyklu? Ma kod — jest cykliczne, nie ma kodu — jest jednorazowe. Flaga byłaby jedynie duplikatem danych.

Tip

Na prawdę, uwierzcie mi, przechodzę codziennie przeprawę z formularzami na dużym poziomie skomplikowania i przeszłam już każdą z tych opcji. Chcąc wykorzystywać większość, jak nie wszystkie zalety formularzy w Symfony, tworzyć własne typy pól, definiować zależności, zaszywać logikę itd. używaj modeli dedykowanych. Ok, dojdzie Ci jedna warstwa, gdzie będziesz musiał mapować model danych na encje, ale zrobisz to świadomie, będziesz w pełni kontrolował proces i sprawisz, że będzie bardziej elastyczny i będzie to generować o wiele mniej problemów w przyszłości.

Front

Aby formularz dotarł do frontu, trzeba go przekazać w kontrolerze. Tak jak wspominałam, nie polecam budować formularzy w kontrolerach, lepszym sposobem są odpowiednie klasy, które wystarczy wywołać w ten sposób:

$form = $this->createForm($this->get('event.form_type'), new EventModel());

Jeśli nasz form jest serwisem, bo potrzebujesz doprowadzić w konstruktorze inne serwisy lub tak, jeśli nie:

$form = $this->createForm(new EventFormType(), new EventModel());

W powyższych przykładach model jest zawsze nowym obiektem, ponieważ zakładam, że jest to pierwsze wywołanie forma np. dla akcji add. Jeśli np. chcesz stworzyć model dla akcji edit, musicie wcześniej stworzyć ten model, nałożyć na niego dane i dopiero przekazać do formularza.

Kolejny krok to odpowiednio zaprezentować w twigu. Najprostszym sposobem jest użycie wbudowanych w twiga elementów:

{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

Te trzy linie wyświetlą formularz przekazany pod zmienną form. Jego wygląd jednak może pozostawiać wiele do życzenia. Jeśli nie potrzebujesz coś więcej, chcesz wykorzystać np. bootstrapa, musisz trochę popracować nad sposobem renderowania. Więcej szczegółów możesz znaleźć na githubie projetu CrossSkills.

<div class="row">
    {{ form_start(form, {'action': path('group_edit',{'id': id })}) }}
    {{ form_errors(form) }}
    <div class="col-md-9">
        <div class="data-select">
            <div class="row">
                <div class="col-md-6">
                    <div id="event_users">
                        {{ form_widget(form.users) }}
                        {{ form_label(form.users) }}
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="col-md-3 title">
        <div class="form-group">
            <label>{{ 'group.name'|trans }}</label>
            {{ form_row(form.name, {'attr':{'class':'form-control' }}) }}
        </div>
        <div class="form-group">

            <label>{{ 'event.type'|trans }}</label>
            {{ form_row(form.eventTypeId, {'attr':{'class':'form-control' }}) }}
        </div>
        <div class="form-group">

            {{ form_row(form.rotary) }}
        </div>

        {{ form_widget(form._token) }}

        <div class="submit edit">
            {{ form_widget(form.save,{'label': 'save'|trans,'attr': {'class': 'btn btn-success'}}) }}
        </div>
        {{ form_end(form) }}
    </div>
</div>

Dzisiaj to tylko mały wstęp, cały cykl będzie zgłębiał po kolei szczegółowe elementy pracy z formularzami. Zobaczysz różne sposoby na walidację, dowiesz się jak tworzyć swoje typy i czy warto, jak działać na eventach formowych. Poruszymy temat budowania zależności między formami i dowiesz się, dlaczego analityk obrywa ode mnie za takie pomysły. Jeśli ciekawi Cię ten temat to zapraszam.

Masz doświadczenie i znasz formularze w Symfony? Może masz związaną z nimi ciekawą historię. Podziel się nią z nami, a wspólnie się czegoś nauczymy. Jeśli chcesz poznać też inne punkty widzenia, polecam ten tekst.

Podobne posty

Jestem programistką, która lubi mieć ręce pełne roboty. Do życia potrzebuje komputera z internetem i kubka gorącej kawy. Więcej na stronie o mnie.

Comments

ZOSTAW ODPOWIEDŹ

Please enter your comment!
Please enter your name here