Nest Starter cz. III

Po zdefiniowaniu wymagań pora zabrać się za ich implementacje. Jednak przed tym krokiem możemy sobie pracę ułatwić odpalając komendę bin/behat –apend-snippets. Komenda to dodaje wszystkie potrzebne definicje kroków potrzebnych do przetestowania wymagań.

Okej gdy szkielet klasy FeatureContext.php mamy gotowy to pora zabrać się za implementacje. W tym wpisie zajmiemy się content_page.feature.

Konfiguracja PhpSpec

Do tworzenia wszystkich klas użyjemy narzędzia PhpSpec, jednak aby ono generowało klasy tak jak sobie tego życzymy jest potrzebna odpowiednia konfiguracja. Najpierw najważniejszy plik czyli phpspec.yml:

formatter.name: pretty

suites:
    app:
        namespace: App
        src_path: .
    storage:
        namespace: Plugin\Storage
        src_path: Plugin/Storage
    webserver:
        namespace: Plugin\WebServer
        src_path: Plugin/WebServer

Plik szablonu generacji klas: (.phpspec/class.tpl)

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace %namespace%;

/**
 * %namespace%\%name%
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 */
class %name%
{
}

Szablon generowanych specyfikacji (.phpspec/specification.tpl)

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace %namespace%;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

/**
 * %namespace%\%name%
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 * @mixin  \%subject%
 */
class %name% extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('%subject%');
    }
}

Static Page

Gdy PhpSpec jest skonfigurowany to pora na pierwszy UseCase. Odpalamy komende bin/phpspec describe App\\UseCase\\ViewStaticPage a następnie bin/phpspec run i odpwiadamy „Y” na zadane pytanie: Do you want me to create `App\UseCase\ViewStaticPage` for you?

Teraz pora przygotować specyfikacje klasy (jego API). ViewStaticPage bedzie wymagać przy stworzeniu Repository (obiekt za pomocą którego będą pobierane strony statyczne). Implementacja Repository nie jest istotna dla działania UseCase. Wymagane jest by Repository implementowało interfejs App\Repository\PageRepositoryInterface.

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace App\Repository;

/**
 * Interface PageRepository
 *
 * @author  Tomasz Ślązok <tomek@landingi.com>
 */
interface PageRepositoryInterface
{
    public function findBySlug($slug);
}

Do zwrócenia strony będzie służyć metoda execute() która przyjmie jako parametr zmienna $slug. Natomiast metoda zwracać będzie w wyniku obiekt App\Entity\Page. Zaczniemy od specyfikacji. Tak wiec w pliku spec/App/UseCase/ViewStaticPageSpec.php dodajemy specyfikacje metody execute:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace spec\App\UseCase;

use PhpSpec\ObjectBehavior;

/**
 * spec\App\UseCase\ViewStaticPageSpec
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 * @mixin  \App\UseCase\ViewStaticPage
 */
class ViewStaticPageSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('App\UseCase\ViewStaticPage');
    }

    function it_return_page_for_slug()
    {
        $this->execute('slug')->shouldReturnAnInstanceOf('App\Entity\Page');
    }
}

Nastepnie uzywamy komendy bin/phpspec run aby PhpSpec stworzył metodę execute() za nas. Niestety metoda execute jest pusta i zwraca null. Z tego powodu gdy odpalimy ponownie bin/phpspec run to otrzymamy komunikat o błędzie.

✘ return page for slug 
    expected an instance of App\Entity\Page, but got null. 

Specyfikacja wymaga aby metoda zwróciła entity Page. Nie ma jeszcze takiej klasy więc szybko ją tworzymy za pomocą phpspeca:

> bin/phpspec describe App\\Entity\\Page
Specification for App\Entity\Page created in /home/nexik/projects/nest/nest-static/spec/App/Entity/PageSpec.php.

> bin/phpspec run                                                                        
  Do you want me to create `App\Entity\Page` for you? [Y/n] Y

Class App\Entity\Page created in /home/nexik/projects/nest/nest-static/App/Entity/Page.php.

Narazie nie interesuje nas implementacja klasy Page, tak więc możemy wrócić do definicji metody ViewStaticPage::execute(). Tworzymy kod który spełni wymagania naszej wcześniejszej specyfikacji:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace App\UseCase;

use App\Entity\Page;

/**
 * App\UseCase\ViewStaticPage
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 */
class ViewStaticPage
{
    public function execute($slug)
    {
        return new Page();
    }
}

Po tej zmianie dostajemy zielone światło, jednak nie jest to końcowy efekt jaki byśmy chcieli, dlatego musimy zmodyfikować trochę specyfikacje. Klasa ViewStaticPage nie powinna być odpowiedzialna za tworzenie obiektu Page. Jak już wcześniej pisałem, zadanie to należy wydelegować do obiektu Repository. Dodajemy odpowiednią zależność do specyfikacji:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */

namespace spec\App\UseCase;

use App\Entity\Page;
use App\Repository\PageRepositoryInterface as Repository;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

/**
 * spec\App\UseCase\ViewStaticPageSpec
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 * @mixin  \App\UseCase\ViewStaticPage
 */
class ViewStaticPageSpec extends ObjectBehavior
{
    function let(Repository $repository)
    {
        $this->beConstructedWith($repository);
    }

    function it_return_page_for_slug(Repository $repository)
    {
        $repository->findBySlug('slug')->willReturn(new Page());

        $this->execute('slug')->shouldReturnAnInstanceOf('App\Entity\Page');
    }
}

Dokonaliśmy kilka ciekawych rzeczy.

  • Najpierw usuneliśmy specyfikacje it_is_initializable, ponieważ jest to domyślna specyfikacja która po stworzeniu klasy ma bardzo małą wartość. Czym mniej niepotrzebnego kodu tym lepiej.
  • Następnie uzyliśmy w specyfikacji metody let(). Metoda ta służy do przygotowania obiektu którego testujemy. Metoda let() służy także jako specyfikacja constructora.
  • W metodzie let skorzystaliśmy z wewnętrznego mechanizmu mockowania jaki dostarcza PhpSpec. System mockowania służy do uproszczenia budowy zależności pomiędzy obiektami. Podczas budowania obiektu repository nie interesuje nas jego implementacja ważne tylko żeby implementował odpowiedni interfejs
  • W metodzie let() zdefiniowaliśmy zachowanie $repository->findBySlug(). Jeżeli zostanie przekazany jako parametr string slug to repository zwróci obiekt Page
  • W specyfikacji it_return_page_for_slug() zdefiniowalismy zachowanie repository dla wywolania findBySlug(‚slug’)

Po tych zmianach gdy odpalimy bin/phpspec run to dostaniemy komunikat o błędzie:

> bin/phpspec run
✘ return page for slug
   some predictions failed:
      Double\PageRepositoryInterface\PageRepositoryInterface\P1:
        No calls been made that match:
          Double\PageRepositoryInterface\PageRepositoryInterface\P1->findBySlug(exact("slug"))
        but expected at least one.

Musimy zaktualizować metodę ViewStaticPage::execute() aby korzystała z repository:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace App\UseCase;

use App\Entity\Page;
use App\Repository\PageRepositoryInterface as Repository;

/**
 * App\UseCaseViewStaticPage
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 */
class ViewStaticPage
{
    /**
     * @var \App\Repository\PageRepositoryInterface
     */
    private $repository;

    /**
     * Constructor
     *
     * @param Repository $repository
     */
    public function __construct(Repository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * Get static page
     *
     * @param $slug
     * @return \App\Entity\Page
     */
    public function execute($slug)
    {
        return $this->repository->findBySlug($slug);
    }
}

Teraz komenda bin/phpspec run zakończy się sukcesem. Trochę się nazbierało, następnym razem zabierzemy się za implementacje Repository dla stron statycznych w oparciu o pliki yaml a potem już tylko krok do napisania testów funkcjonalnych do Behata.

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

5580657696_bf70c5a0f3_b

Sortowanie tablic po danej wartości indeksu

Czasem mamy dane w tablicach zamiast w bazie danych.

Przykład

Mamy listę użytkowników w kolekcji tablic. Każda tablica składa się z par klucza i wartości

<?php
array(
  'name' => 'Jaś Kowalski',
  'points' => 10
);

Gdy chcemy wyswietlic ranking użytkowników, to chcemy posortować naszą kolekcję tablic z danymi użytkownikach według wartości indeksu ‚points’. 

Rozwiązanie

<?php
$sort = [];

foreach ($collection as $key => $row) {
  $sort[] = $row['points'];
}

array_multisort($sort, SORT_DESC, $collection);

foreach jest w php dość wolny, więc nie polecam tego rozwiązania dla kolekcji danych z 10 000+ elementów. Ale wtedy napewno dane macie już w bazie danych i zrobicie prostego SELECT * FROM users ORDER BY points

4440561067_ff472f723c_b

Jak sortować tablice UTF-8 w PHP

Jeżeli mamy tablicę której kluczami są nazwy miejscowości (zakodowanych w UTF-8) i chcemy ją posortować alfabetycznie po kluczach to mamy problem. Zwykłe ksort( $array ); nie wystarczy :(

Najpierw musimy upewnić się że jest załadowany odpowiedni locale, a następnie użyć metody sortowania za pomocą funkcji użytkownika. A oto pełne rozwiązanie w PHP:

<?php
setlocale( LC_ALL , 'pl_PL.UTF-8' );
uksort( $array , 'strcoll' );

Została tutaj użyta funckja strcoll która porównuje dwa stringi według collate aktualnego locale.

Autor zdjęcia: Jessica Wilson