Na odsiecz tablicom w walce z ViewModelem


yarpo napisał na swoim blogu artykuł o ViewModelu w aplikacji opartej o Zend Framework. Nie pozostawił suchej nitki na tablicach asocjacyjnych jako sposobie przekazywania danych do widoku, pokazał w zamian wykorzystanie tytułowego ViewModelu. W pełni zgadzam się z przedstawioną diagnozą problemów, ale zgłaszam zdanie odrębne odnośnie ich rozwiązania, a w szczególności – co do potępienia tablic asocjacyjnych.

Zgadzam się, że to:

<p><?php echo $this->escapeHtml($exampleViewModel->getName()); ?></p>
<p><?php echo $this->escapeHtml($exampleViewModel->getSurname()); ?></p>
<?php if ($exampleViewModel->canBuyAlcohol()) : ?>
    <img src='reklama_alkoholu.jpg' alt='Piwo bezalkoholowe, oczywiscie ;)' />
<?php endif; >

Wygląda lepiej niż to:

<p>
    <?php if (isset($data) && isset($data['name'])) : ?>
        <?php echo $this->escapeHtml($data['name']); ?>
    <?php endif; ?>
</p>
<?php if (isset($data) && isset($data['age']) && is_numeric($data['age']) && $data['age'] >= 18): ?>
    <img src='reklama_alkoholu.jpg' alt='Piwo bezalkoholowe, oczywiscie ;)' />
<?php endif; >

Ale jeszcze lepiej można to napisać tak:

<p>{{ name }}</p>
<p>{{ surname }}</p>
{% if age >= 18 %}
    <img src='reklama_alkoholu.jpg' alt='Piwo bezalkoholowe, oczywiscie ;)' />
{% endif %}

Wystarczy skorzystać z Twiga – a to jeszcze nie jest ostatnie słowo tego silnika szablonów (zaraz kolejne słowa, cierpliwości). W Symfony2, aby skorzystać z takiego szablonu wystarczy przekazać do niego tablicę asocjacyjną. Pewien kłopot pojawia się, jeśli nie jesteśmy pewni, czy odpowiednie klucze są w tej tablicy – coś, na co szczególnie zwracał uwagę yarpo. Ale kłopot to niewielki. Po pierwsze, jedynym prawdziwym zagrożeniem, jest brak kluczy, bo już jeśli tylko klucze będą zdefiniowane, ale wartości będą puste, Twig sobie poradzi. Potrzebujemy zatem obejścia na brak kluczy. Pierwsze rozwiązanie – jeszcze na etapie tworzenia odpowiedzi możemy zmergować naszą tablicę z domyślną, gdzie znajdziemy wszystkie potrzebne klucze z pustymi wartościami. Oczywiście, wspominam tylko dla porządku, tak aby to prawdziwe wartości nadpisały nulle gdzie trzeba. Ale jest też drugi sposób, którym zahaczymy o inny ważny aspekt Twiga. Przyjmijmy, że nie mamy kluczy name i surname. Taki szablon załatwi sprawę:

<p>{{ name|default("John") }}</p>
<p>{{ surname|default("Doe") }}</p>
{% if age >= 18 %}
    <img src='reklama_alkoholu.jpg' alt='Piwo bezalkoholowe, oczywiscie ;)' />
{% endif %}

Gdy nie dostarczymy tych kluczy, filtr default podłoży domyślne (zaskoczenie, szok, niedowierzanie) wartości które mu podaliśmy. Działa to nie tylko przy wyświetlaniu, ale również przy dostarczaniu wartości do wyrażeń.

Filtry nie są jedynymi zaawansowanymi narzędziami, które otrzymujemy od Twiga – mamy też funkcje, testy i parę innych, mniej popularnych. A co najciekawsze, do zdefiniowanego już zestawu możemy dopisać swoje własne komponenty za pomocą Twig Extension – to już czysta zabawa z PHPem. Aż się prosi o wykorzystanie testu zamiast porównania age >= 18

class DemoExtension extends \Twig_Extension {

	public function getTests()
	{
		return [
			new \Twig_SimpleTest('alcoholAllowed', [$this, 'isAlcoholAllowed'])
		];
	}

	public function isAlcoholAllowed($age)
	{
		return ($age >= 18);
	}

	public function getName()
	{
		return 'demo_extension';
	}
}
services:
    demo.twig.demo_extension:
        class: app\DemoBundle\Twig\DemoExtension
        tags:
            - { name: twig.extension }
<p>{{ name|default("John") }}</p>
<p>{{ surname|default("Doe") }}</p>
{% if age is alcoholAllowed %}
    <img src='reklama_alkoholu.jpg' alt='Piwo bezalkoholowe, oczywiscie ;)' />
{% endif %}

Przyznaję, początkowy narzut na konfigurację Twig Extension istnieje, ale po pierwsze nie jest aż tak duży, po drugie szybko zacznie się zwracać, jeśli dodamy inne własne testy, funkcje, filtry albo nawet operatory. Oczywiście filtr isAlcoholAllowed jest banalny, ale łatwo sobie wyobrazić bardziej rozbudowaną implementację zależną np. od lokalizacji. Na listingu szablonu nie ma pomyłki odnośnie is – taka jest składnia wywołania testu i dlatego odpowiednio zdefiniowałem nazwę.

Sporo miejsca w listingu oryginalnego szablonu zajmuje escepowanie – w Twigu możemy włączyć escapowanie w ustawieniach lub stosować filtr escape (lub krócej e) przyjmujący jedną z kilku strategii – szczegóły na http://twig.sensiolabs.org/doc/filters/escape.html.

Ogólnie Twig jest kompletnym silnikiem szablonów pełnym użytecznych i przemyślanych konstrukcji. Moją ulubioną jest chyba for … else … endfor

{% for element in elements %}
<!-- wyświetlanie elementu, np. wiersz tabelki -->
{% else %}
<!-- komunikat o braku rekordów do wyświetlenia -->
{% endfor %}

Po więcej szczegółów, przykładów i ciekawostek odsyłam na http://twig.sensiolabs.org/

Ale zaraz, oryginalnie mowa była o Zendzie, a Twig to projekt powiązany z Symfony… Nie próbowałem, ale z (najwidoczniej zrealizowanego) założenia Twig nie ma być dostępny jedynie dla Symfony i dać się zintegrować również np. z Zendem. Obiecująco wygląda https://github.com/ZF-Commons/ZfcTwig (choć ostatnie commity dawno). Jeśli ktoś spróbuje – proszę o sygnał.

Dla podsumowania. Oczywiście zastosowanie wzorca ViewModel jest czym innym niż zastosowanie silnika szablonów. Chciałem jednak pokazać, że zamiast dokładać własną warstwę VM możemy posiłkować się sprawdzonym, przetestowanym i wygodnym rozwiązaniem przy którym do przekazania danych wystarczy tablica asocjacyjna. Które z rozwiązań jest lepsze – to jak zwykle zależy od sytuacji.

PS. Największym minusem Twiga póki co jest to, że nie działa mi tu na blogu kolorowanie jego składni. Bo już np. wspominane przez yarpo IntelliJ/PHPStorm uzbrojony w kilka wtyczek zna Twiga i podpowiada nie tylko konstrukcje, ale też nazwy pól czy nawet w potrzebie elementy formularzy.

, ,