Blog webdeveloperski Patryk yarpo Jar

Zend/PHP: Dlaczego stosowanie view modeli jest dobre

Autor wiadomości Październik 23, 2014

Chcialbym w tym wpisie pokazac, co zrobic, aby kod kontrolerow i plikow widoku byl przyjemniejszy w czytaniu i prostszy w utrzymaniu. Dodatkowo, za darmo uda sie nam zyskac duzo prostszy do testowania kod! Na co czekac? Zaczynajmy!

Na poczatek warto wiedziec:

  • Podstawy PHP,
  • Swiadomosc istnienia MVC,
  • Posiadanie zdrowego rozsadku i instynktu samozachowawczego.

Opis problemu

Czy w Waszym kodzie zdarza sie Wam widziec tego typu kod w widoku:

<?php if (isset($data) && isset($data['abc'])) : ?>
    <?php echo $this->escapeHtml($data['abc']);
<?php endif; ?>

Jesli tak (i jesli nie podoba Ci sie to) swietnie trafiles. Zaraz pokaze, co mozna zrobic, aby:

  • kod byl czytelniejszy,
  • refaktoryzja byla duzo prostsza,
  • napisanie (lub naprawienie) unit testow nie przyprawialo Cie o bol glowy,
  • wyrzucic (raz na zawsze!) programowanie defensywne i zastosowac agresywnym ofensywnym podejsciem.

Czytelniejszy kod

Aby wniesc do kodu jakosc, rzadko zdarza sie, ze mozna zastosowac banalne rozwiazanie w jednym tylko punkcie projektu. Bardzo czesto chodzi o calosciowe rozwiazanie wymuszajace prawidlowe podejscie do problemu.

W przykladzie opisujacym problem pokazalem, ze do widoku ktos przekazuje tablice asocjacyjna. Tablica asocjacyjna jest wygodna do napisania kodu raz. Tablica asocjacyjna jest wrogiem duzego projektu. Prosze sprobowac podmienic kilka kluczy w duzym projekcie... A potem dla porownanie pomienic kilka nazw w klasie modelu.

// controller:
public function exampleAction()
{
    $data = $this->getDataFromApi();
    $exampleViewModel = new ExampleViewModel($data);

    return new ViewModel([
        'exampleViewModel' => $exampleViewModel
    ]);
}

Tak wygladalaby klasa `ExampleViewModel`:

class ExampleModelView
{
    private $name;
    private $surname;
    private $age;

    public function __construct($data)
    {
        if (false === is_array($data)) {
            throw new \InvalidArgumentException('Expected array, ' . gettype($data) . ' given');
        }
        $this->setName(ArrayUtils::get($data, 'name'));
        $this->setSurname(ArrayUtils::get($data, 'surname'));
        $this->setAge(ArrayUtils::getInteger($data, 'age'));
    }

    public function canBuyAlcohol()
    {
        return ($this->getAge() >= 18);
    }

    // gettery i settery pomijam w przykladzie
}

Dodatkowo uzywam tu ArrayUtils, ktora mogalaby wygladac w ten sposob:

class ArrayUtils
{
    public static function get($data, $key)
    {
        if (false === array_key_exists($key, $data)) {
            throw new \OutOfBoundsException('Key ' . $key . ' not found in array.');
        }

        return $data[$key];
    }

    public static function getInteger($data, $key)
    {
        $value = self::get($data, $key);

        if (false === is_int($value)) {
            throw new \InvalidArgumentException('Integer expected, ' . gettype($value) . ' given');
        }

        return $value;
    }
}

A w widoku wygladaloby to tak:

// example.phtml
<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; >

Czy taki kod nie jest przyjemniejszy od tablicolubnego:

<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; >

To bylo pytanie retoryczne. Sadze, ze kazdy przyzna, ze kod proponowany przeze mnie jest co najmniej tak samo czytelny, jak zaifowany przyklad z samymi isset'ami. Moim zdaniem kod jest duzo bardziej czytelny.

Prostsza refaktoryzacja

W codziennej pracy z PHP uzywam PHPStorma. Nikt mi za to nie placi, ale reklamuje go przy kazdej okazji 😉

Mimo, ze w PHP nie ma silnego typowania i niezbyt mozna czasem po samym kodzie powiedziec, jakiego typu zmienna mamy (co bardzo utrudnia automatyczna refaktoryzacje), to z wykorzystaniem odpowiednich technik i pewnych hackow da rade podpowiedziec Twojemu IDE, czym jest dana zmienna i jak powinien ja podczas refaktorinkgu potraktowac.

Jak to zrobic? Adnotacje w kodzie

// example.phtml - z adnotacjami:
<?php /** @var $exampleViewModel ExampleViewModel */ ?>

<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; >

Co zyskalismy?

  • autouzupelnianie kodu zgodnie z tym, co IDE znajdzie we wskazanej klasie,
  • w przypadku refaktoringu (np. nazwy jakiejs publicznej metody klasy `ExampleViewModel`) wszystkie wystapienia powinny zostac znalezione,
  • po otworzeniu pliku widzimy z jakimi danymi mamy do czynienia (nie musisz uruchamiac strony, aby dojsc do tego miejsca i uzyc `print_r`... uzycie trybu debugowania jest tylko troszke lepsze).

Warto tez zauwazyc, ze mozna wykorzystac takie adnotacje takze w przypadku pol klasy:

class ExampleModelView
{
    /** @var string */
    private $name;
    /** @var $surname string
    private $surname;
    /** 
     * @var int $age 
     */
    private $age;

    /**
     * @param int $age
     *
     * @return ExampleViewModel
     *
     * @throws \InvalidArgumentException
     */
    public function setAge($age)
    {
        if (false === is_int($age)) {
            throw new \InvalidArgumentException('Expected int, ' . gettype($age) . ' given');
        }
        $this->age = $age;

        return $this;
    }
}

Troche rozbudowalem metode `setAge`, aby moc dodatkowo pokazac jeszcze kilka innych adnotacji. W przypadku definicji wlasciwosci klasy wszystkie 3 beda dzialac. Dodatkowo jesli tak zdefiniujesz sobie wlasciwosci klasy to generujac automatycznie gettery i settery dostaniesz odpowiednie adnotacje za darmo. W przypadku wartosci prymitywnych to moze nie jest najwazniejsze, ale jesli uzywasz agregacji bardzo moze to pomoc, zarowno w tworzeniu, jak i utrzymaniu kodu.

Podobnie mozna definiowac zmienne lokalne (np. gdy zwracasz cos z generycznej metody w stylu Doctrine'owego `$entityManager->find(MyClass:CLASS, $id)`):

/** @var $myClassEntity MyClass */
$myClassEntity = $entityManager->find(MyClass:CLASS, $id);

Co zyskalismy? Dokladnie to samo, co wczesniej listowalem.

Czy warto?

Moze jesli pracujesz sam nad niewielkim kodem uznasz, ze nie. Po co dodawac ok 20% kodu (statystyki z sonara), aby tylko troche lepiej rozumiec kod? Otoz, w miare jak projekt sie rozrasta tego typu podpowiedzi i ulatwienia (szczegolnie przy odpowiednim IDE) beda na wage zlota.

Unit testy

We wstepie tego wpisu obiecalem, ze bedzie latwiej takze testowac taki kod. O ile testowanie tablic asocjacyjnych rozsianych po plikach warstwy prezentacji jest trudne (albo czesto niemozliwe lub duzo drozsze - selenium itp.), o tyle jesli mamy stworzony odpowiedni model jego testowanie jest banalne. Co wiecej podczas populacji danymi takiego modelu mozemy dokonac walidacji - zarowno pod katem struktury przekazanych danych, jak i pod katem wartosci (typ, zakres liczb, dlugosc ciagow znakow, format dat itp.).

Przykladowe unit testy dla `ExampleViewModel`:

class ExampleModelViewTest extends \PHPUnit_Framework_TestCase
{
    /** @expectedException \InvalidArgumentException */
    public function test_nullGiven_shouldThrowInvalidArgumentException()
    {
        new ExampleModelView(null);
    }

    /** @expectedException \OutOfBoundsException */
    public function test_uppercaseKeysInArray_shouldThrowOutOfBoundsException()
    {
        new ExampleModelView(['NAME' => 'John', 'surname' => 'Doe', 'age' => 20]);
    }

    /** @expectedException \InvalidArgumentException */
    public function test_invalidAgeType_shouldThrowOutOfBoundsException()
    {
        $data = $this->getData();
        $data['age'] = '20';
        new ExampleModelView($data);
    }

    public function test_gettersSetters_shouldBeOk()
    {
        $model = new ExampleModelView($this->getData());
        $this->assertEquals('John', $model->getName());
        $this->assertEquals('Doe', $model->getSurname());
        $this->assertEquals(20, $model->getAge());

        $this->assertTrue($model->canBuyAlcohol());

        $model->setAge(17);
        $this->assertFalse($model->canBuyAlcohol());
    }

    private function getData()
    {
        return ['name' => 'John', 'surname' => 'Doe', 'age' => 20];
    }
}

(w oryginalnym zestawie testow bylo jeszcze kilka innych przypadkow, a `canBuyAlcohol` mial kilka dedykowanych metod. Tu raczej pokazalem, ze calosc jest testowalna. Na pewno mozna napisac lepsze testy 😉 )

Unit testy dla pozostalego kodu zostawiam jako zadanie domowe. `ArrayUtils` jest wiecej niz banalne do przetestowania. Natomiast w kontrolerze polecalbym stworzenie serwisu do pobierania danych z API, odpowiednio go zamockowac i gotowe :).

Programowanie defensywne

Poki co odesle do dobrego artykuly na ten temat. W przyszlosci planuje jednak glebiej poruszyc to na moim blogu.

Warto pamietac, ze zbyt czeste powtorzenia sprawdzajace, czy jakas wartosc istnieje jest wyraznym sygnalem, ze projekt cierpi na raka isset/empty (nullizacja kodu). Gdy otrzymujesz jakies dane powinienes je weryfikowac / walidowac. Jesli wszystko jest z nimi poprawnie, powinienes moc z nich korzystac bez ciaglego sprawdzania czy istnieja.

Podsumowanie

Mam nadzieje, ze wpis ten pozwoli Wam troche szerzej spojrzec na zagadnienie tworzenia kodu latwiejszego w utrzymaniu. Oczywiscie stosowanie modeli w widoku nie jest niczym odkrywczym. Co wiecej - potrafi takze prowadzic do niezbyt atrakcyjnych rozwiazan. Nie jest to lek na cale zlo, a jedynie jedna cegielka jakosci w projekcie.

A jakie sa Wasze doswiadczenia? Moze ktos potrafi pisac kod oparty o asocjacyjne tablice, ktory spelnia wszystkie podane przeze mnie kryteria?

Komentarze (0) Trackbacks (0)

Brak komentarzy.


Leave a comment

 

Brak trackbacków.