2685458942_43202ea04c_o

Nest Starter cz. I

Napisanie aplikacji w PHP która będzie rozwijana nie jest prostym zadaniem. Zawsze gdy zaczyna się nowy projekt to wszyscy są podekscytowani że tym razem wszystko będzie pięknie, czysto i wspaniale. Niestety w większości przypadków wszystko kończy się tak samo. Mamy kod który wcale nie jest taki prosty w utrzymaniu. Terminy zaczynają gonić i kod się pogarsza i za każdym razem jak coś dodajemy do aplikacji prace nad nią stają się coraz trudniejsze. Dlaczego tak jest?

Odpowiedź jest w miarę prosta: Jeżeli na początku wszyscy są podekscytowani i do tego nie mają dyscypliny TDD to szybkie wdrażanie nowych funkcji powoduje że architektura staje się coraz to gorsza.

Rozwiązanie jest proste:

  • Zanim rozpoczniesz prace nad aplikacja przemyśl jej architekture.
  • Framework nie jest częścią aplikacji jest pluginem.
  • Przestrzeganie TDD. (3 zasady TDD)

Aby pokazać praktyczne rozwiązanie zademonstruje je na przykładzie prostej strony webowej. Strona bedzie się składać z 3 podstron (strony głównej, strony about, strony kontaktu).

Struktura projektu

bin/
features/
spec/
App/
    Boundary/
    Entity/
    Repository/
    UseCase/
Plugin/
    Storage/
    WebServer/
       Controller/
       Resources/
          assets/
             less/
             public/
          bin/
          cache/
          config/
          views/

Do załadowania wszystkich narzędzi potrzebnych do pracy użyjemy Composera. Oto podstawowy composer.json:

{
    "require": {
        "nexik/nest-core": "dev-master"
    },
    "require-dev": {
        "behat/behat":               "~3.0",
        "bossa/phpspec2-expect":     "*",
        "phpspec/phpspec":           "~2.0",
        "phpspec/prophecy":          "~1.0",
        "squizlabs/php_codesniffer": "~1.0"
    },
    "autoload": {
        "psr-4": {
            "App\\" : "App",
            "Plugin\\": "Plugin"
        }
    },
    "config": {
        "bin-dir": "bin/"
    },
    "minimum-stability": "dev"
}

Szybkie wyjaśnienie struktury projektu:

  • App\ tutaj znajdzie się kod naszej aplikacji
  • App\UseCase\ tutaj znajdą się wszystkie przypadki użycia czy historyjki scrumowe.
  • App\Entity\ tutaj znajdą się podstawowe pojęcia aplikacji takie jak np strona statyczna
  • App\Repository\ tutaj znajdą się interfejsy w jaki sposób UseCase będzie mógł dobrać się do instancji Entity
  • App\Boundary\ tutaj znajdą się klasy DTO które będą przesyłane pomiędzy Pluginani a Aplikacją. Obiekty te zapewniają wspólny interfejs w komunikacji aplikacji z resztą świata.
  • Plugin\ tutaj znajdą się wszystkie pluginy związane z detalami. Tymi detalami są m.in Framework czy Baza danych
  • Plugin\Storage\ tutaj znajdą się implementacje interfejsów z App\Repository które będą zwracać potrzebne dane
  • Plugin\WebServer\ tutaj znajdzie się implementacja obsługi requestu HTTP który zostanie przetworzony na potrzeby danego UseCase’a oraz przyjęcie odpowiedzi z UseCase’a i przygotowanie strony HTML i wysłanie jej do klienta
  • Plugin\WebServer\ tutaj znajdzie się tak zwany Delivery Mechanism (framework)
  • Plugin\WebServer\Controller\ lista controllerów które będą reagować na request HTTP
  • Plugin\WebServer\Resources\ pliki nie przechowujacę klasy PHP
  • Plugin\WebServer\Resources\assets\ pliki potrzebne do przygotowania albo dostarczenia UI (html, css, js)
  • Plugin\WebServer\Resources\bin\ katalog do plikiem console przydatnym przy pracach nad projektem, zastosowanie np czyszczenie zawartosci katalogów cache
  • Plugin\WebServer\Resources\cache\ tutaj będą trzymane wszystko to co zajmuje za dużo czasu aby wykonywać za każdym requestem
  • Plugin\WebServer\Resources\config\ wszystkie pliki konfiguracyjne takie jak reguły routingu (routing.yml), główna konfiguracja (config.yml), definicje DI (services.yml)
  • Plugin\WebServer\Resources\views\ pliki templatek

Dodatkowo są katalogi które służą czysto tworzeniu aplikacji:

  • bin/ przydatne skrypty podczas pracy
  • features/ Testy BDD. Tutaj znajdą się opisy wszystkich użyć aplikacji, tak zwane Story.
  • spec/ Testy TDD. Tutaj się znajdą specyfikacje jak ma wyglądać API klas które mają później zostać stworzone.

Do implementacji projektu skorzystamy z:

Następnym razem pokaże jak zapisać wymagania aplikacji za pomocą Behata i jak rozpocząć pracę nad aplikacją nie myśląc jeszcze o Pluginach przy pomocy PHPSpec.

Źródło zdjęcia:  https://www.flickr.com/photos/blondie5000/2685458942

6988157282_d2c10fc165_o

DbInjectionAware formularz w Phalconie

Pisząc pewien projekt w Phalconie, natknąłem się na sytuację gdy formularz musiał mieć validator Uniqueness. Oczywiście taki validator istnieje, ale w Modelach: Phalcon\Mvc\Model\Validator\Uniqueness. Niestety postanowiłem nie korzystać z phalconowych modeli z powodu łamania zasady SOLID.

Pomysł

Wpadłem na pomysł aby stworzyć własny validator który by korzystał z połączenia Db i sprawdzał czy podana wartość już istnieje w bazie. Tutaj jednak pojawiło się pytanie w jaki sposób wstrzyknąć instancje połączenia z bazą danych do validatora?

Pierwszym pomysłem było skorzystać z Dependency Injection w Phalconie jako Service Locator. Jednak uważam że o ile można, to należy omijać Service Locatora. Z tego powodu postawiłem rozwiązać problem trochę okreżną drogą, ale za to łatwo testowalną.

Rozwiązanie

Rozwiązaniem jest połączenie własnego FormManagera oraz Interfejsu DbInjectionAwareInterface.

<?php
namespace My\App\Forms;

class Manager extends \Phalcon\DI\Injectable
{
    private $definitions;

    public function set($name, $definition)
    {
        if (is_string($definition)) {
            $definition = ['className' => $definition];
        }

        $this->definitions[$name] = $definition;
    }

    public function get($name)
    {
        if ($this->has($name)) {
            $className = $this->definitions[$name]['className'];
            $form = new $className();

            if ($form instanceof DbInjectionAwareInterface) {
                $form->setDb($this->getDI()->get('db'));
            }

            if (method_exists($form, 'configure') {
                $form->configure();
            }

            return $form;
        }
    }

    public function has($name)
    {
        return isset($this->definitions[$name]);
    }
}

Moja wersja Form Managera sprawdza przy tworzeniu instancji formularza czy implementuje DbInjectionAwareInterface, jeżeli tak to wstrzykuje db.

Dodatkowo jeżeli formularz posiada metodę configure to jest on odpalany przed zwróceniem obiektu formularza. Jest to spowodowane tym, że wbudowana metoda initialize() jest odpalana podczas konstruktora. W konstruktorze nie ma jeszcze dostępu do wstrzyknietej instancji db. Z tego powodu dodałem obsługę metody configure.

<?php
namespace My\App\Forms;

interface DbInjectionAwareInterface
{
    public function getDb();
    public function setDb($db);
}

Teraz wystarczy tylko odpowiednie zmiany w DI aplikacji

<?php

$di->set('formManager', 'My\App\Forms\Manager');
$formManager = $di->get('formManager');
$formManager->set('register', 'My\Forms\RegisterForm');

Oczywiście aby się nie powtarzać RegisterForm może extendować po klasie która ma zaimplementowany DbInjectionAwareInterface. Jednak ja napisałem do tego traitsa:

<?php
namespace My\App\Di;

trait DbInjectableTrait
{
    private $db;

    public function setDb($db)
    {
        $this->db = $db;
    }

    public function getDb()
    {
        return $this->db;
    }
}
<?php
namespace My\Forms;

use My\App\Di\DbInjectableTrait;
use My\App\Di\DbInjectionAwareInterface;
use Phalcon\Forms\Form;

class RegisterForm extends Form implements DbInjectionAwareInterface
{
    use DbInjectableTrait;

    public function configure()
    {
        //...
    }
}

Autor zdjęcia: Philip Taylor