Posty oznaczone etykietą symfony

Symfony 2: pierwsze wrażenia

Nowości w Symfony 2

  • PHP 5.3 jako standard i korzystanie z przestrzeni nazw
  • Zupełnie nowy kod, zdecydowane odchudzenie klas, czuć lekkość
  • Dependency Injection (IoC)
  • Events
  • Modułowa architektura, nie tylko pod postacią katalogów o nazwie modules/plugin
  • Rezygnacja z pluginów jako mikroaplikacji na rzecz Bundles
  • Źródła hostowane na GitHub

Bundle 1: Marketing

Prezentacje, konferencje, świetny design stron przemawiają za rewolucyjnością nowego produktu SensioLabs.

Faktycznie, w świecie PHP może być to jakaś rewolucja, ale chyba głównie za sprawą samego PHP 5.3 i namespaces zaciągniętych z "szóstki". A źrodła Symfony? Ano jak to PHP - trochę Javy trochę C, np. klasy Request/Response w których zaimplementowano tylko akcesory (powstają obiekty o niezmiennym stanie). W Django najważniejsze middleware opierają się na możliwości modyfikacji obiektu Request dodając np. właściwość user. Proste i dziala. W Symfony 2 to nie przejdzie, ubijany jest dynamizm języka programowania.

Bundle 2: Dependency Injection

Kiedyś bardzo potrzebowałem takiego mechanizmu. Otóż za pomocą konfiguracji (PHP/Yaml/XML/etc) można zdefiniować skonfigurowane usługi pod unikalnymi nazwami. Konfiguracja może odbywać się przez wstrzyknięcie argumentów do konstruktora lub wybranych metod (argumentami mogą być wartości lub odwołania do innych zdefiniowanych usług).

W teorii można swobodnie wymieniać/parametryzować poszczególne instancje klas. W praktyce stosuje się to rzadko lub wcale.

Popełniłem kiedyś taki plugin do Symfony 1.x - sfIocPlugin - wzorowałem się na IoC ze Springa. ''' Dependency Injection w Symfony może przydać się przy większych projektach, gdzie pewne usługi mogą być wymienialne w zależności od wdrożenia. Osobiście jednak uważam, że realizacja "większych projektów" w PHP to utopia. Może komuś się to jednak przyda.

EDIT: Dependency Injection sprawdza się w unit testach (o ile środowisko unit testów umożliwia użycie DI), szczegóknie w połączeniu z mock-objects.

Bundle 3: Events

System zdarzeń w Symfony 2 jest prosty. Jest dość dobrą realizacją wzorca obserwatora. Autor przyznaje, że wzorował się na notyfikacjach z Cococa.

Implementacja ta przypomina mi naszego firmowego EventBrokera napisanego chyba w 2006 roku. Osobiście wolę django.signals z racji rejestrowania obserwatora do konkretnej instancji sygnału. Z kolei w Symfony obserwator (listener) rejestrowany jest w instancji dispatchera podając nazwę sygnału.

Nasuwają się dwie wady (z doświadczenia):

  • dispatcher może nie mieć zarejestrowanego zdarzenia (trzeba znaleźć odpowiedni dispatcher, przez który będzie przebiegał event),
  • rejestracja i notyfikacje odbywają się przez nazwę - robiąc literówkę przy connect nie mamy szans na informacje o błędzie; takie błędy znajduje się zwykle po kilku godzinach.

Bundle 4: Super wydajność

Wydajność została zwiększona, ale nie uważam że o te mityczne 2.5x.

Benchmark strony "Congratulations..." (index.php) z sandboxa, dla ab -c 100 -n 1000:

  • Symfony 2: 32.18 [#/sec]
  • Symfony 1.2: 19.58 [#/sec]

To jest nieco powyżej 1.5x. Żeby jednak trochę podrasować wersję 1.2 wyłączyłem w settings.yml użycie bazy danych (test Symfony 2 też nie używał BD).

Widok o podobnej złożoności w Django serwowany jest nawet do 200 rq/sec. Strona główna portalu opartym na Django (z użyciem memcache) jest serwowana w trybie developerskim w porywach do 160 req/sec.

Wydajność Symfony 2 nie zachwyca.

Bundle 5: Formularze

Czytałem tylko kod. Są to niemal te same nieudolne formularze, o których już pisałem - Symfony forms vs Django forms.

Podsumowanie

Szkoda czasu.

Symfony forms vs Django forms

Mam okazję pracować z obydwoma frameworkami i mogę je porównać w praktyce. Django zacząłem używać jakieś dwa lata temu, a Symfony nieco wcześniej (od wydania stabilnej wersji 1.0).

Ostatnimi czasy, z braku możliwości upgrade Symfony w projekcie, przeportowałem mechanizm formularzy z wersji 1.1 do 1.0.20.

Piersze wrażenie

Mechanika formularzy w Symfony mocno przypomina newforms z Django. Powszechnie wiadomo, że Fabien jest fanem Django, więc nie zdziwiło mnie zbyt specjalnie, że wzorował się właśnie na nim.

Mamy do dyspozycji podstawową klasę formularza sfForm (django.forms.Form), oraz klasę dedykowaną do modelu Propela sfPropelForm (django.forms.ModelForm).

Jest do dyspozycji zbiór widgetów w klasach sfWidgetForm (django.forms.widgets) do renderingu pól oraz zestaw walidatorów w klasach sfValidator (w Django sprawdzanie poprawności danych jest rozwiązane przez wywoływanie metod clean_FIELDNAME() formularza, o ile zostały zdefiniowane, oraz metody clean() każdego pola). W Symfony jest możliwość napisania własnych specyficznych widgetów i walidatorów.

Używanie formularzy jest podobne - w widoku (sf: akcji), najczęściej przy requescie wysłanym POST-em instancjonujemy formularz i wypelniamy go danymi z requestu, sprawdzamy czy jest prawidlowy (sfForm::is_valid()). W przypadku sfPropelForm (ModelForm) po walidacji wywołujemy save().

Definiowanie formularzy

W Django używamy class properties i zagnieżdżonej klasy Meta.

class MyForm(forms.Form):
  name = forms.CharField()
  birthdate = formd.DateField()

  class Meta:
    exclude = ('birthdate',)

W Symfony formularz konfigurowany jest w metodzie sfForm::configure(), która uruchamiana jest domyslnie w konstruktorze.

class MyForm extends sfForm
{
  protected function configure() {
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));
  }
}

Ops, ale.. jak wykluczyć datę urodzin?

Google dają odpowiedź:

  • Q: How to exclude a field?
  • A: You have to remove the widget and the validator.

W Symfony zwykle kończy się to na klasie pochodnej, przeciążeniu configure() z serią unset na widgetSchema i validators. Możliwe jest (wszystko jest możlwie do realizacji, ale z różną efektywnością i poziomem trudności), ale trochę uciążliwe.

Django-Symfony 1:0

Uważny czytelnik zwróci uwagę, że w Symfony nie definiujemy pół formularza (sic!) tylko oddzielnie widgety i validatory. Mimo, że istnieje klasa sfFormField, jest ona używana już po bindingu (zawiera w sobie wartość pola - w Django jest BoundField, które opakowuje Field i dostarcza wartość).

Jaka jest zatem kolejna wada formularzy Symfony? Nie ma zdefiniowanych klas pól formularzy, które naturalnie łączą widgety i walidaję danych, np. CharField związane jest domyślnie z widgetem TextInput i posiada domyślną walidację w metodzie CharField.clean(). W Symfony trzeba dodać walidację samodzielnie:

class MyForm extends sfForm
{
  protected function configure() {
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));

    $this->setValidators(array(
        'name' => new sfValidatorString(array('required'=>true)),
        ));
  }
}

Walidacja pola jest zwykle specyficzna. W Symfony trzeba napisać dedykowaną klasę walidatora. W Django implementujemy metodę clean_FIELDNAME w klasie formularza. Można też przygotować dedykowaną klasę Field.

Django-Symfony 2:0

I18n (czyli formularze po polsku)

W Django etykiety i komunikaty walidacji muszą być opakowane w wywołanie funkcji gettext. W komunikatach podstawowych pól używany jest ugettext_lazy. Etykiety musimy zdefiniować ręcznie (za pomocą argumentu label przy definiowaniu pola formularza).

class MyForm(forms.Form):
  name = forms.CharField(label=_('Name'))
  birthdate = formd.DateField(label=_('Birth date'))

  class Meta:
    exclude = ('birthdate',)

Po stronie Symfony zrealizowano to nieco inaczej - przez nastawienie funkcji callbacka do funkcji translate:

  protected function configure() {
      $this->widgetSchema->getFormFormatter()->setTranslationCallable(
          array(sfContext::getInstance()->getI18N(), '__'));
      /* ... */
  }

Pełny kod formularza w Symfony:

class MyForm extends sfForm
{
  protected function configure() {

    $this->widgetSchema->setNameFormat('my_form[%s]');
    $this->widgetSchema->getFormFormatter()->setTranslationCallable(
        array(sfContext::getInstance()->getI18N(), '__'));

    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));

    $this->setValidators(array(
        'name' => new sfValidatorString(array('required'=>true)),
        'birthdate' => new sfValidatorDate(array('required'=>true)),
        ));
  }
}

Pamiętacie jeszcze, że w Django ten sam opis formularza składa się tylko 5 linii kodu? :)

Mimo innego sposobu używania gettext(), to zarówno Django, jak i w Symfony dają radę. Remis.

Django-Symfony 3:1

Domyślne dane (initial parameters)

Django:

  data = {'name': 'Stranger',}
  form = MyForm(initial=data)

Symfony:

  $form = new MyForm();
  $form->setDefaults(array('name'=>'Stranger'));

Praktycznie to samo. Jednak zauważyłem, że pomimo nastawienia wartości domyślnych w formularzu sfPropelForm, pusta wartość pola modelu napisuje to, co przekazujemy w defaults. Jest to dość duży problem, ale może dotyczyć tylko wersji formularzy z Symfony 1.1. Remis.

Django-Symfony 4:2

Binding

Formularze Django w konstruktorze przyjmują parametr data, do którego zwykle przekazuje się request.POST. Analogicznie jest z przesłanymi plikami - request.FILES. Przykład:

def my_form(request):
  if request.method == 'POST':
      form = MyForm(request.POST, request.FILES)
      if form.is_valid():
          model = form.save()
          return redirect(model)

W Symfony jest dość podobnie, aczkolwiek binding należy wykonać oddzielną metodą bind():

public function executeMy_form() {
  $form = new MyForm();
  if($this->getRequest()->getMethod() == sfWebRequest::POST) {
      $form->bind($this->getRequestParameter('my_form'),
        $this->getRequest()->getFiles('my_form'));
      if($form->isValid()) {
          $form->save();
          $this->redirect('/somewhere/');
      }
   }
}

Należy też pamiętać o użyciu prawidłowej zmiennej z requestu - w klasie formularza definiujemy name format (setNameFormat) i te obydwie części muszą się zgadzać. W ten sposób złamano DRY i nie jestem, czy w Symfony da się ten problem jakoś ominąć. Remis.

Django-Symfony 5:3

Częste problemy/tips

  • Własny rendering formularzy. W Symfony musicie pamiętać o jawnym używaniu gettext(), jeśli nie używacie metod render*() pól/formularza.

  • Dostosowywanie formularzy do potrzeb projektu. Django daje duże możliwości dzięki przestrzeniom nazw Pythona i konfiguracji urls/settings. Zwykle w projekcie dodaje się dedykowaną aplikację z modułem zawierającym dostosowane formularze i najczęściej zmienia się konfigurację urls (w reusable apps mamy zwykle do dyspozycji parametr form_class w widokach). Symfony, przynajmniej w wersji 1.0.X, nie daje mechanizmu zbliżonego do urls, chociaż można użyć factories (http://www.symfony-project.org/book/1_0/17-Extending-Symfony#chapter_17_factories) albo innych rozwiązań (ja stosowałem własny kontener IoC). Warto o tym pamiętać, bo we wdrożeniach bardzo często dostosowuje się właśnie formularze.

  • Odpowiednik klasy ModelForm w Symfony nie potrafi "w locie" zbudować definicji formularza. Bazowe klasy dla poszczególnych modeli muszą być wygenerowane (sic!). Zatem użyteczność sfPropelForm bez wygenerowanych bazowych klas jest niewielka i ogranicza się do zaimplementowanej metody save().

Fantazje Fabiena

Na koniec, jako ciekawostkę, dodam krótkie podsumowanie tego, co Fabien zaimplementował będąc albo przemęczonym, albo na kacu ;)

  • schemat widgetów/validatorów (kontener/dict) dziedziczy bezpośrednio po klasie Widget/Validator (znakomity przykład do czego nie używać dziedziczenia)

  • forms embedding (formularze w formularzach) - przyznam, że nie mogę tego pojąć. Nie mamy formsetow, ale możemy zagnieżdżać formularze w sobie.

  • form merging (łączenie formularzy) - nie widze zastosowania (formularz hermetyzuje logikę, sprawdzanie poprawności i sposób prezentacji - po co łączyć, użyjmy kilku formularzy lub Formsetu)

  • I18N w kontekście translacji (gettext) - zahermetyzowane wewnątrz formularza, lecz z możliwością wymiany callable na dowolny; trudne/każdorazowe definiowanie domyślnego callable; gettext powinien być używany, nie zawarty wewnątrz klasy formularza

Podsumowanie

Formularze w Symfony są, a to już lepsze niż nic, które było w wersji 1.0.X. Ostateczny wynik rozgrywki Django-Symfony 5:3. Jeśli ktoś nie używał formularzy w Symfony, to gorąco polecam. Jeśli ktoś ma dylemat co wybrać, polecam Django z racji szerszych możliwości i skrócenia czasu realizacji zadań.