Přejít k navigační liště

Zdroják » Databáze » Architektura aplikace nad Doctrine 2

Architektura aplikace nad Doctrine 2

Články Databáze, PHP

O Doctrine 2 je na webu dostatek informací – i na Zdrojáku je poměrně podrobně popsáno, jak Doctrine používat, jak s ním pracovat a jak v něm psát i složitější úlohy. Tento článek ukáže nikoli samotný ORM, ale aplikaci, která jej používá, a možné problémy, na které při vývoji narazíte.

Ohledně toho, jak používat Doctrine 2 bylo napsáno již mnoho. Jak ze článků zde na Zdrojáku, tak z oficiální dokumentace se dozvíme, jak psát entity, jak používat mapování, pracovat s Entity Managerem, psát složité „selecty“, používat pokročilejší funkce v podobě práce s událostmi nebo psaní vlastních typů pro práci s daty. Přesně o všech těchto věcech tento článek nebude. Budeme se zabývat pouze tím, jak by mohla vypadat aplikace, která toto ORM používá.

S používáním Doctrine 2 začneme u velmi jednoduchého příkladu a postupně si budeme představovat problémy, na které je možné narazit (především s růstem aplikace), a ukážeme si, jak je možné reagovat. Určitě nebudu ukazovat nějaké všeléky nebo unikátní pravdy. Představím architekturu, ke které jsme v Medio Interactive dospěli při práci na našich projektech. Je velmi flexibilní a prakticky na všech místech si může vývojář v závislosti na složitosti aktuální aplikace vybrat, jak velkou míru dekompozice zvolí.

Příklady jsou silně zjednodušené, aby zbytečně neprotahovaly už tak obsáhlý článek. Vynechal jsem většinu validací, starosti o namespaces, vzorné vypisování PHPdoc bloků atp. Kód je tedy pouze ilustrativní, neklade si za cíl být funkčním. Změny v jednotlivých krocích možná budou lépe patrné při zobrazení diffů, je možné si je prohlédnout v repozitáři na GitHubu.

Jedoduchý příklad

Vzhledem k pohodlnosti vývoje je možné Doctrine 2 použít i na malé aplikace, kde si vystačíme s velmi přímočarým postupem. Začneme pouze s controllerem a entitou, která v sobě obsahuje Doctrine 2 anotace potřebné k mapování do databáze. V controlleru přímo pracujeme s entitou a s Entity Managerem.

<?php

/**
 * @Entity
 */
class Article {

    const STATUS_DRAFT = 1;
    const STATUS_PUBLISHED = 2;

    /** @Column(type="integer") */
    private $status;

    /** @Column(type="date") */
    private $publishDate;

    /** @Column(type="integer") */
    private $viewCount;

    // další properties

    public function __construct() {
        $this->status = static::STATUS_DRAFT;
        $this->viewCount = 0;
    }

    public function setStatus($status) {
        if ($status !== static::STATUS_DRAFT && $status !== static::STATUS_PUBLISHED) {
            throw new InvalidArgumentException('Invalid status value: ' . $status);
        }
        $this->status = $status;
    }

    public function setPublishDate(DateTime $date) {
        $this->publishDate = $date;
    }

    // další gettery a settery (settery by měly obsahovat validaci vstupů)

}
<?php

class ArticleController {

    /** @var DoctrineORMEntityManager */
    private $entityManager;

    /** @var int */
    private $topArticlesCount;

    public function __construct(DoctrineORMEntityManager $entityManager, $topArticlesCount) {
        // mapování do private proměnných
    }

    private function getTopArticles() {
        return $this->entityManager->createQueryBuilder()
            ->select('a')
            ->from('Article', 'a')
            ->where('a.status = ?1')
            ->orderBy('a.viewCount DESC')
            ->setMaxResults($this->topArticlesCount)
            ->getQuery()
            ->setParameter(1, Article::STATUS_PUBLISHED)
            ->getResult();
    }

    private function publish($id) {
        $article = $this->entityManager->find('Article', $id);
        $article->setPublishDate(new DateTime());
        $article->setStatus(Article::STATUS_PUBLISHED);
        $this->entityManager->flush();
    }

    // další obslužné metody

}

Entity

Způsobem, jakým je nyní napsána entita, jde vlastně jen o přepravku na data obohacenou pouze o validaci vstupních dat. Kontrolujeme jen, jestli do proměnných přichází data ve správné podobě.

Pokud bychom chtěli validovat stav celé entity, museli bychom v setterech kontrolovat čím dál tím větší množství věcí, aby se entita nedostala do nějakého nežádoucího stavu. Například zkontrolovat, že pokud chceme nastavit entitu do stavu published (podle kterého zřejmě budeme vybírat aktivní články), měl by již mít předem nastavené datum publikování, jinak se entita ocitne v nekonzistentním stavu. Momentálně toto nijak v kódu zachycené není a spoléháme tedy na to, že nikdo nezapomene zavolat obě metody a že je případně zavolá v zamýšleném pořadí.

Obranou proti tomu jsou zmiňované validace nebo dle mého lepší varianta – omezení rozhraní entity, aby nabízelo jen operace, které chceme zvenčí povolit (zapouzdření). Zavedeme tedy metodu setPublished, ve které budou všechny operace, které se musí provést v rámci entity, pokud má být připravena k publikování. V budoucnu sem můžeme bezpečně přidávat další změny, jak budou případně přibývat properties této třídy – a bude potřeba tuto změnu provést vždy jen na tomto jednom místě – viz DRY přístup. Díky tomu se vzdalujeme od anemického modelu a míříme k doménovému modelu.

<?php

/**
 * @Entity
 */
class Article {

    const STATUS_DRAFT = 1;
    const STATUS_PUBLISHED = 2;

    /** @Column(type="integer") */
    private $status;

    /** @Column(type="date") */
    private $publishDate;

    /** @Column(type="integer") */
    private $viewCount;

    // další properties

    public function __construct() {
        $this->status = static::STATUS_DRAFT;
        $this->viewCount = 0;
    }

    public function setPublished(DateTime $date) {
        $this->status = static::STATUS_PUBLISHED;
        $this->publishDate = $date;
    }

    // další gettery a settery (settery by měly obsahovat validaci vstupů)

}
<?php

class ArticleController {

    /** @var DoctrineORMEntityManager */
    private $entityManager;

    /** @var int */
    private $topArticlesCount;

    public function __construct(DoctrineORMEntityManager $entityManager, $topArticlesCount) {
        // mapování do private proměnných
    }

    private function getTopArticles() {
        return $this->entityManager->createQueryBuilder()
            ->select('a')
            ->from('Article', 'a')
            ->where('a.status = ?1')
            ->orderBy('a.viewCount DESC')
            ->setMaxResults($this->topArticlesCount)
            ->getQuery()
            ->setParameter(1, Article::STATUS_PUBLISHED)
            ->getResult();
    }

    private function publish($id) {
        $article = $this->entityManager->find('Article', $id);
        $article->setPublished(new DateTime());
        $this->entityManager->flush();
    }

    // další obslužné metody

}

Repository

V momentě, kdy budeme chtít používat některé dotazy na různých místech, začne opět být nepraktické je kopírovat. Bylo by lepší, kdybychom je měli někde znovupoužitelně připravené. Tímto místem může být například repository, kde můžeme shromažďovat dotazy, které se primárně týkají článků. Vlastní repository tak, aby s ním uměla Doctrine 2 efektivně pracovat, musíme podědit od DoctrineORMEn­tityRepository a uvést ho v anotaci @Entity pro Article.

<?php

/**
 * @Entity(repositoryClass="ArticleRepository")
 */
class Article {

    const STATUS_DRAFT = 1;
    const STATUS_PUBLISHED = 2;

    /** @Column(type="integer") */
    private $status;

    /** @Column(type="date") */
    private $publishDate;

    /** @Column(type="integer") */
    private $viewCount;

    // další properties

    public function __construct() {
        $this->status = static::STATUS_DRAFT;
        $this->viewCount = 0;
    }

    public function setPublished(DateTime $date) {
        $this->status = static::STATUS_PUBLISHED;
        $this->publishDate = $date;
    }

    // další gettery a settery (settery by měly obsahovat validaci vstupů)

}
<?php

class ArticleRepository extends DoctrineORMEntityRepository {

    public function findTopArticles($maxResultsCount) {
        return $this->getEntityManager()->createQueryBuilder()
            ->select('a')
            ->from('Article', 'a')
            ->where('a.status = ?1')
            ->orderBy('a.viewCount DESC')
            ->setMaxResults($maxResultsCount)
            ->getQuery()
            ->setParameter(1, Article::STATUS_PUBLISHED)
            ->getResult();
    }

}
<?php

class ArticleController {

    /** @var DoctrineORMEntityManager */
    private $entityManager;

    /** @var int */
    private $topArticlesCount;

    public function __construct(DoctrineORMEntityManager $entityManager, $topArticlesCount) {
        // mapování do private proměnných
    }

    private function getTopArticles() {
        return $this->entityManager->getRepository('Article')->findTopArticles($this->topArticlesCount);
    }

    private function publish($id) {
        $article = $this->entityManager->find('Article', $id);
        $article->setPublished(new DateTime());
        $this->entityManager->flush();
    }

    // další obslužné metody

}

Repository nám může vracet buď již hotové kolekce objektů, alternativou je ale i vracení nějak přednastaveného QueryBuilderu, který pak můžeme dál upravit.

Z repository vytvářené tímto způsobem se pravděpodobně časem stane obrovská třída s mnoha metodami, která dost silně porušuje Open/closed principle. Pokud bychom se tomu chtěli vyhnout, máme možnost zvolit odlišný přístup, jeden takový popisuje například Aleš Roubíček ve svém článku Doménové dotazy, kde vzniká defacto pro každý dotaz samostatná třída. Vše je tam velmi přehledně vysvětleno (ač v syntaxi C#), takže to zde nebudu opakovat.

Nicméně variantu repository bych určitě nezavrhoval – beru repository jako fasádu pro získávání dat a fasády dost často obsahují více (ale jednoduchých) metod. Další výhodou je, že pokud budeme chtít, aby dotazy sdílely určité části kódu, půjde to samozřejmě mnohem lépe v jedné třídě. V případě Doménových dotazů bychom museli přidat třídám další závislosti a jejich používání by již nebylo tak snadné.

Service

Aktuálně hlavní výkonná logika zůstává v controlleru. Jakmile budeme chtít určité postupy, ať už kompletně, nebo po částech používat v jiných controllerech, měli bychom tyto metody opět přesunout jinam – resp. na místo, kde by se měly ideálně nacházet od začátku – do modelu. Vzniknou nám tak service třídy. V nich by se měly objevit všechny metody, které souvisí s doménovým modelem, ale zároveň nejdou zapsat do samotných entit (tam jsme je přesouvali v prvním kroku). Můžou jimi být například metody, které pracují nad více objekty. Rozdělení, co ještě patří do entity a co už do service, velmi závisí na konkrétní situaci a kde daná logika dává větší smysl. Service třídy jsou tedy místem, kde by se měly objevit konkrétní ucelené postupy, jak řešit konkrétní věci (třeba nějaké složitější výpočty), abychom je dále mohli používat napříč aplikací. Je velmi důležité do services nemíchat žádnou práci s persistentní vrstvou, musí zde zůstat čistě doménová logika. Všechna data, která metody v service potřebují, musí získat prostřednictvím svých parametrů. Services dost možná vaše aplikace nevyužije, zkrátka proto, že v ní nejsou takové věci, které by stálo za to do ní psát.

Services by v zásadě měly být stateless – tzn. ve svých properties by měly mít pouze reference na své závislosti, které dostanou přes konstruktor pomocí Dependency Injection. Nic dalšího by si metody do properties ukládat neměly, což v praxi znamená, že nám stačí jedna instance pro mnoho použití (což skvěle funguje v kombinaci s DI kontejnerem, kde pak můžeme mít od každé service právě jednu instanci) a nebudou spolu nijak interferovat.

Vymyslet nějaký pěkný příklad na takto jednoduché ukázce je dost složité, nakonec jsem zvolil situaci, kdy bychom chtěli vylepšit původní vypisování top článků na základě více údajů a lepší logiky než jen prostého počtu zobrazení. Vytvořil jsem tedy ArticleService, který obsahuje metodu, která z předaných článků vybere požadovaný počet nejlepších. To je flexibilní, protože si pak zvenčí můžeme zvolit, kolik článků servise předáme. Může to být stejné množství, jaké ve výsledku požadujeme, může jich být ale více. Chtěl jsem tím ukázat to, že se občas nevyhneme stažení více dat, než budeme ve skutečnosti potřebovat, protože některé věci není vhodné řešit čistě pomocí selectů (netvrdím, že toto je nutně ten případ). Díky stateless povaze servis a oproštění od používání perzistence zásadně zjednodušíme testovatelnost a znovupoužitelnost kódu.

<?php

class ArticleService {

    public function getTopArticlesByMagic(array $articles, $maxResults) {
        // Zde je nějaký promyšlený algoritmus, který projde všechny předané články a vybere
        // nám z nich daný počet nejzajímavějších. Může k tomu používat metody entity, případně
        // přes asociace získávat další související entity a pracovat tak i s jejich daty.
    }

}
<?php

class ArticleRepository extends DoctrineORMEntityRepository {

    public function findArticlesSince(DateTime $date) {
        return $this->getEntityManager()->createQueryBuilder()
            ->select('a')
            ->from('Article', 'a')
            ->where('a.status = ?1 AND a.publishDate > ?2')
            ->getQuery()
            ->setParameter(1, Article::STATUS_PUBLISHED)
            ->setParameter(2, $date)
            ->getResult();
    }

    // findTopArticles a další metody ...

}
<?php

class ArticleController {

    /** @var DoctrineORMEntityManager */
    private $entityManager;

    /** @var ArticleService */
    private $articleService;

    /** @var int */
    private $topArticlesCount;

    public function __construct(DoctrineORMEntityManager $entityManager, ArticleService $articleService, $topArticlesCount) {
        // mapování do private proměnných
    }

    private function getTopArticles() {
        $now = new DateTime();
        $lastMonthArticles = $this->entityManager->getRepository('Article')->findArticlesSince($now->sub(new DateInterval('P1M')));
        return $this->articleService->getTopArticlesByMagic($lastMonthArticles, $this->topArticlesCount);
    }

    private function publish($id) {
        $article = $this->entityManager->find('Article', $id);
        $article->setPublished(new DateTime());
        $this->entityManager->flush();
    }

    // další obslužné metody

}

Facade

Ještě stále ale nemáme vyřešenou situaci, kdy budeme chtít některé akce (v našem případě publikování článků nebo vybrání těch nejlepších) používat na více místech v naší aplikaci. To může jednak znamenat použití ve více různých controllerech, ale zároveň jakmile přesuneme tyto metody do modelu (kam podle MVC rozdělení rozhodně patří), získáme možnost nad naším modelem stavět další aplikace. To znamená, že ve výsledku může být nad modelem postavený hlavní web, administrace, dále například REST API a koneckonců i když budeme psát CLI skripty (spouštěné třeba cronem), měly by tyto fasády využívat. Vrstva fasád nám tedy vytvoří přípustné API celého modelu.

V naší ukázce to tedy znamená, že pouze přesuneme metody z controlleru do nově vzniklé fasády, protože již nyní byly privátními a pouze volány dalšími obslužnými metodami. Díky tomu se ale controller konečně zbaví své závislosti na Entity Manageru.

Fasáda by měla být velmi přímočará a pouze využívat metody definované na ostatních částech systému a vhodným způsobem je spojovat dohromady. Každá public metoda ve fasádě by měla sloužit k jasně definovanému účelu a public rozhraní všech fasád popisuje případy užití, které naše aplikace pokrývá. Metody ve fasádách jsou popis jedné business transakce, takže by se pravděpodobně na konci operací, které v dané metodě provádíme, měla na EntityManageru zavolat metoda flush, která odešle provedené změny do databáze (do té doby se pouze hromadí v Unit of Work).

Pokud nastane situace, kdy bychom měli mít velmi podobný kód pro několik metod, nic nebrání vytvoření privátní metody, kam se tento společný kód umístí, a další metody jej budou pouze využívat (a flush tedy pravděpodobně budeme chtít použít až ve „vnějších“ metodách, aby nám nebránil v dalším používání).

<?php

class ArticleFacade {

    /** @var DoctrineORMEntityManager */
    private $entityManager;

    /** @var ArticleService */
    private $articleService;

    public function __construct(DoctrineORMEntityManager $entityManager, ArticleService $articleService) {
        // mapování do private proměnných
    }

    public function getTopArticles(DateTime $since, $maxResults) {
        $lastMonthArticles = $this->entityManager->getRepository('Article')->findArticlesSince($since);
        return $this->articleService->getTopArticlesByMagic($lastMonthArticles, $maxResults);
    }

    public function publish($id, DateTime $date) {
        $article = $this->entityManager->find('Article', $id);
        $article->setPublished($date);
        $this->entityManager->flush();
    }

}
<?php

class ArticleController {

    /** @var ArticleFacade */
    private $articleFacade;

    /** @var int */
    private $topArticlesCount;

    public function __construct(ArticleFacade $articleFacade, $topArticlesCount) {
        // mapování do private proměnných
    }

    private function getTopArticles() {
        $now = new DateTime();
        return $this->articleFacade->getTopArticles($now->sub(new DateInterval('P1M')), $this->topArticlesCount);
    }

    private function publish($id) {
        $this->articleFacade->publish($id, new DateTime());
    }

    // další obslužné metody

}

Zde se s rozšiřováním již zastavíme. Jen zopakuji, že z výše zobrazených tříd se práce s perzistencí týká Facade, EntityManager, Repository. Všechny ostatní části by měly být od perzistence kompletně oproštěny.

Mapovat, nebo nemapovat?

Z posledního obrázku je zřetelně vidět, že všechny části našeho systému jsou závislé na entitě a ta prostupuje všechny vrstvy, čímž se trochu nabourává izolovanost jednotlivých vrstev.

Pokud bychom ale chtěli tento problém vyřešit, museli bychom na několika místech sáhnout k mapování – například do controlleru bychom již neposílali entitu, ale vytvořili bychom speciální objekt, který by v sobě nesl jen potřebná data, a ty bychom do něj z entity překopírovali.

Tento přístup se nám v praxi při programování webových aplikací příliš neosvědčil, protože benefity, které bychom získali z izolovaných vrstev, zdaleka nedosahovaly overheadu, který byl způsoben přepisováním všech mapování a tříd reprezentujících data na několika úrovních. Toto přepisování se stávalo pokaždé, když se změnilo zadání nebo přišly nové požadavky.

Pokud entita prostupuje celým systémem, pak přidání nové property je potřeba reflektovat pouze na místech, kde chceme s novou hodnotou pracovat. Pokud má dojít ke změně stávajících evidovaných hodnot a je to změna, která vyžaduje modifikaci API dané entity, bude samozřejmě nutné upravit všechny části aplikace odpovídajícím způsobem, nicméně opět jen tam, kde se s nimi opravdu pracuje (oproti mapování, kde se data většinou čistě kopírují, popř. nějak agregují). Pokud pro prezentační účely potřebujeme entitě nějaké hodnoty přeci jen přidat, než ji předáme dále do view, může nám pomoci kompozice – vytvoříme objekt, který bude obsahovat referenci na danou entitu a dále v sobě bude obsahovat potřebné další properties určené k uchování prezentačních hodnot.

Závěr

Názorně jsem ukázal, jak se z několika málo tříd postupným refaktoringem může stát tříd poměrně hodně. Nicméně si rozhodně nemyslím, že by situace, kdy programátor využije všechny zde představené třídy, byla složitější, než ta původní. Kód jako takový prostě někde napsaný být musí a v momentě, kdy ho chceme používat na více místech, dochází buď k duplicitám, které velmi zvyšují riziko výskytu chyb a nekonzistencí, nebo jsme stejně donuceni refactoring udělat.

Pokud budeme architekturu členit od začátku, bude kód o hodně přehlednější – bude rozložený na více místech, ale jednotlivé metody nebudou tak dlouhé, a vždy by mělo být jasné, kde programátor najde funkcionalitu, kterou právě potřebuje (platí zvláště pro práci v týmu). Pokud ji nenajde na místě, kam logicky patří, lze usuzovat, že ještě nebyla naprogramována, a může ji sám na toto místo doplnit, což je situace diametrálně odlišná od toho, kdy by musel prohledat celou aplikaci (v úvodním příkladu všechny controllery) a zjistit, jestli požadovaná funkce již někde je, a případně ji refaktorovat, aby ji mohl použít i on sám.

Pokud vás článek zaujal a chcete se dozvědět více o tom, jak navrhovat aplikace a psát kód, aby byl lépe znovupoužitelný, udržovatelný a snadno testovatelný, přijďte se podívat na naše školení Pokročilý vývoj a testování aplikací.

Komentáře

Subscribe
Upozornit na
guest
28 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Eda

Opravdu skvělý článek. Díky.

Pokud by se našla chuť na nějaký další článek o praktickém postupu při používání Doctrine 2, budu minimálně já velmi rád :-)

Patrik Votoček

Za ten cca rok co řeším problém odstínění Presenteru (Controlleru) od EntityManageru mám konečně jasno jak na to. Za celou tu dobu, po spoustě pokusů mě totiž vůbec nenapadlo postavit nad Repository a Service ještě jednu vrstvu.

Zkusím to v praxi a uvidíme jak to dopadne (trochu se bojím aby to neskončilo u „God“ Facade objektů).

Škoda že se do článku nevešlo vytvoření nového článku z formuláře. I když to už je asi trochu nad rámec (mapování dat z formuláře na entitu s reakcí na chyby). (možná by se to mohlo objevit v nějaké branch na githubu? :-P).

manik.cze

ad „God“ Facade – ano, tam na to musíš dávat trochu pozor, nicméně Facade by měly být tak jednoduché, že u nich zas tak nevadí když je objekt větší. Nicméně nic tě nenutí pojmenovávat fasádu jako ArticleFacade a mít tam úplně všechno ohledně článků. Můžeš mít třeba ArticleAdminFacade, kde budou věcí pro editování a ArticleViewFacade, kde budeš mít věci okolo servírování článků apod.

Jo, to se nevešlo, především proto, že jsem se kód snažil držet maximálně krátký (i tak jsem v něm byl schopný udělat dvě chybky). Ale možná zkusím něco vytvořit, nebo další článek :)

Nox

Myslim že stejný problém je se service … ono už podle názvu ArticleService je jasné, že to asi bude mít na starosti milion různorodých činností s Articlem.

Pokud těch akcí bude víc než málo, tak místo naházení všech akcí s jednou entitou do jedné třídy bych udělal třídy, které mají jednu specifickou oblast činnosti – třeba ArticlePublisher, ArticleRevision … tím lépe, pokud to bude pracovat na 2+ entitami

manik.cze

Přesně tak.

Petr Procházka

Tak je to o tom jak facade budeš vytvářet. Když si z nich neuděláš god objekt tak nebudou :-). Když do nich přesouváš ‚modelovou‘ logiku z controlerů tak budou většinou menší než controlery.

Petr Procházka

Díky za dobrý článek.

Píšeš:
> „Jen zopakuji, že z výše zobrazených tříd se práce s perzistencí týká Facade, EntityManager, Repository.“

Co je tímhle myšleno? Asi vnímáme ‚práci s perzistencí‘ každý jinak.
Podle mě Facade by o ní neměli nic vědět a jen využívat api co nabízí EntityManager a Repository.

manik.cze

> Podle mě Facade by o ní neměli nic vědět a jen využívat api co nabízí EntityManager a Repository.

No a EM a Repository pracují s perzistencí, tzn, sami jsou perzistencí také. Jde mi o to, že ve všech vrstvách, kde pracuješ s těmihle věci ať už přímo nebo zprostřdedkovaně, tak se ti na to budou psát hůř testy (mockovat Doctrine věci je peklo). Kdežto když to budeš mít oddělené, tak v pohodě vystačíš s unit testy.

Petr Procházka

Nepoužívám Doctrine takže mockování vrstvy které se v doctrine jmenuje EntityManager mi nedělá problém. Ale většinou při testování fasád mockuju až vrstvu níže (blíže k uložišti).

Ale jestli tím bylo myšleno že fasáda pracuje z persitencí přes api EM/Repository tak je vše v pořádku a myslíme oba to samé :-)

Jan Tichý

Práce s persistencí je právě cokoliv, kde děláš s EntityManagerem nebo Repository. Pokud tedy někde potřebuješ zavolat find() nebo persist() nebo flush(), saháš si na persistenční vrstvu.

Což je něco, co se v těch jednodušších a systémově ne moc čistých přístupech na začátku článku dělá v controlleru.

V komplexním čistém přístupu na konci článku je to pak vyhozeno právě do fasády. Důležité pak v takovém případě je, že bu se žádný entity manager, flush, persist neměl objevit uvnitř žádného controlleru, entity ani servisy. Pokud například budeš v některých servisách flushovat, dříve či později ti to strašně nafackuje.

jirkakoutny

> Důležité pak v takovém případě je, že bu se žádný entity manager, flush, persist neměl objevit uvnitř žádného controlleru, entity ani servisy. Pokud například budeš v některých servisách flushovat, dříve či později ti to strašně nafackuje.

Honzo mohl bys prosím více rozvést, proč přístup k persistentní vrstvě ze Service považuješ za extrémně špatné řešení? Chápu, že odstínění od persistence může zjednodušit testování Service a její znovupoužitelnost. Je ještě něco zásadnějšího, o čem byl měl vědět? Díky

manik.cze

> proč přístup k persistentní vrstvě ze Service považuješ za extrémně špatné řešení? Chápu, že odstínění od persistence může zjednodušit testování Service a její znovupoužitelnost. Je ještě něco zásadnějšího, o čem byl měl vědět?

Podle mě je znovupoužitelnost (+ použitelnost) a testovatelnost alfa a omega OOP, takže je to ta nejzásadnější věc. Spíš je to tak, že pokud tyhle dvě věci (ona je to čistě teoreticky jen jedna, protože to spolu velmi úzce souvisí) nebudeš dodržovat, tak se může objevit celá řada dalších problémů, které by jinak vůbec nenastaly. Je to takový snowball effect – například zavedení jedné statické metody kdovíkde se dost často podepíše na x místech.

Opravdový odborník :-)

Neznám PHP a jeho knihovny, třeba je v tom nějaká záludnost, ale: Jaký je rozdíl mezi tím, jestli budu mockovat EM nebo jestli budu mockovat nějakou svoji třídu (dodatečno vrstvu)? Proč by jedno mělo být testovatelné a druhé ne?

A co se týče znovupoužitelnosti a přenositelnosti – EM by sám o sobě měl poskytovat dostatečnou abstrakci. Např. v Javě máme JPA a k němu různé implementace – můžu změnit implementaci ORM, ale rozhraní je stejné (tudíž není potřeba měnit aplikaci). To v PHP nemáte?

Nox

Máme, existuje interface ObjectManager, který má implementace EntityManager a DocumentManager – takže by to mělo jít

Teuzz

To new DateTime mě pěkně tahá za oči, parametrem nebo DI by to bylo hezčí.

Ale pěkný iterativní článek, který rozešlu na všechny strany, abych se nemusel dívat na to, na co se dívat musím :) Díky, Vašku!

manik.cze

Jo, jasně, new DateTime by tam napřímo být nemělo, nicméně jsem chtěl zdroják co nejvíc zjednodušit v ohledu k tématu článku. Už tak myslím, že to je bezkonkurenčně nejdelší článek, co tu je :)

Jakub Vrbas

Díky za článek.
Jen bych se chtěl zeptat, jak těsná je podle vás vazba na Doctrine, resp. na ORM obecně? Já tam tu vazbu totiž moc silnou nevidím a dokážu si dost dobře podobnou architekturu představit i bez ORM vrstvy.

manik.cze

Vazba nijak těsná není, naopak ta architektura je tak volná, že pokud nahradíte Entity Manager obdobnou částí jakékoli jiné perzistetntí vrstvy (pokud je napsaná alespoň trochu slušně), tak by absolutně neměl být problém. Nicméně nechtěl jsem článek pojmout takto obecně, ten kdo tuší, tak si to tam právě najde :)

jirkakoutny

Vašku, moc díky za supr článek. Přečetl jsem ho už 3x a pomohl mi domyslet naši vlastní architekturu (nepoužíváme Doctrine).

2 otázky:

1) Předpokládám, že v Mediu děláte spíše složitější aplikace. Oddělujete tedy vždy „povinně“ všechny vrstvy, tj. Facade, Service, Repository a Entity Manager? Nebo vrstvy přidáváte až „za běhu“ s tím, jak aplikace roste? Z článku pro mě vyplývá to první (možná až na nepovinné Service).

2) Do jaké vrstvy patří podle tebe Entity cache? Honzův 2 roky starý článek o pěti vrstvách modelu ji dával do Repository. Dáváte ji do Repository i nyní? Pro unit testy pak cache nějak centrálně vypínáte?

manik.cze

1) Všechny vrstvy nepoužíváme, resp. na poslední krok, tj. zavedení Facade právě u některých přecházíme. Vrstvy za běhu přidávat lze v pohodě, viz článek, nicméně se ukazuje, že to chce odhadnout víceméně správnou úroveň už na začátku…a vůbec není na škodu, pokud se zvolí vyšší dekompozice než nižší (protože projektové zadání spíš bobtná než naopak). Takže za mě viz shrnutí myšlenek v závěru článku – u projektů, které povedu já budu určitě vyžadovat spíše větší dekompozici – kódu se ve výsledku píše skoro stejně a přitom je mnohem lépe čitelný (a vzhledem k velikosti projektu je mi fakt jedno, jestli je na danou věc jedna třída nebo dvě – spíš naopak, stejně se to pak většinou dělí, aby nebyly tak obrovské, viz sousední diskuze s Patrikem).

2) Nevím, co přesně myslíš Entity cache – jestli je to Identity Map (tzn. aby se jedna entita během jedné transakce nestahovala z DB vícekrát), tak to u nás obhospodařuje Doctrine. A v jiných nástrojích persistenci bych to opravdu opět asi zařadil do Repository (resp. pro něco společného, co využívá jak Repository, tak EntityManager, protože to jsou informace, které potřebují obě tyhle věci). Pokud myslíš jinou cache, tak řekni přesněji kterou – cachovat se v téhle architektuře dá na hodně místech. Pro unit testy nic nevypínáme – unit testy jako takové nemají mít s perzistentní vrstvou nic společného (pak to jsou integrační testy) a unit testů se snažíme mít většinu – tzn. většina logiky by měla být v částech bez perzistence, právě proto, aby se dala dobře unit testovat. V integračních testech – tj. tam kde se mimo jiné používá databáze, tak to většinou používáme tak jak to je – v Identity Map by měl být čerstvý obraz databáze. Teoreticky v Identity Map může zůstat viset pozměněná instance z jiného testu, nicméně pokud se testy píší pořádně (tj. pracují jen s daty, které si samy zakládají), tak by s tím problém být neměl. Ono pokud by chtěl člověk opravdu zajistit správnou izolovanost testů, tak by měl pro každý jednotlivý test spouště úplně celou aplikaci znovu. Tak jak to máme my, tak je to spíš podobné reálnému využití těch tříd/metod, takže paradoxně to, že tam ta izolace v tomto kontextu není dokonalá, tak pokud by tam někdo nějaké to flush a podobně zapomněl, tak na to možná díky tomuhle spíš přijdeme – nebo že nejsou ideálně napsané ty testy.

> Pro unit testy pak cache nějak centrálně vypínáte?
V unit testech bys neměl mít potřebu nikdy nic vypínat/zapínat a už vůbec ne centrálně. V integračních viz výše.

jirkakoutny

Ad ta cache. Konkrétnější příklad:

Entity User má atribut např. hasVisitedDashbo­ard. Ten se nastaví na true poprvé, když si daný uživatel zobrazí stránku „Dashboard“. Podle tohoto příznaku pak uživateli něco skrýváme nebo naopak zobrazujeme a musíme tak znát hodnotu toho příznaku na několika dalších stránkách.

Hodnotu příznaku uchováváme v databázi. Samozřejmě se nám ji ale nechce načítat z databáze při každém načtení stránky, proto ji máme také v Memcached/File­cache.

V jaké vrstvě by se tedy tohle mělo podle tebe řešit? Díky

Michal Augustýn

Jestli do toho můžu vstoupit, tak dle mého názoru toto náleží do Repository. Otázka je, jak to implementovat.

1) Můžeš to naprasit přímo do originální Repository.

2) Uděláš navíc implementaci interfejsu Repository, říkejme ji „cache repository“. Ta bude zkoušet sahat do cache (třeba memcached) a pokud to v cache nebude, tak zavolá metodu na původní Repository.

3) V „ideálním“ řešení budou rovnou tři implementace rozhraní Repository. Jedna bude klasická implementace přímo přistupující do databáze (stejně jako v možnosti 2), druhá bude natvrdo přistupovat ke cache, a konečně třetí implementace bude komponovat chování předchozích dvou Repositories. Tj. zkusí zavolat GetById na druhé (cache) Repository a pokud nic nevrátí, zavolá tutéž metodu na první (přímé) Repository.

Volba záleží na konkrétní situaci, ale vždy bych se snažil dostat minimálně na úroveň možnosti 2.

manik.cze

Ohledně implementace a interfaces souhlasím s Augim. Akorát si nejsem jistý, jestli to vlastně má být až zas tak Repository – problém je ten, že ta cache se v té repository bude používat pro čtení, ovšem už si nemyslím, že je záležitostí Repository, aby tu cache nějak invalidovala. Takže jedině to dělat někde jinde (buď někde „automaticky“ na pozadí, což mi přijde wtf a implementace bude asi fuj) a nebo to musí někdo dělat.

S tím souvisí moje druhá poznámka a to, že podle toho co píšeš mi tahle úroveň cache moc nedává smysl. Tohle je informace, kterou předpokládám potřebujete právě jen když je uživatel přihlášený, proto dle mého patří do „CurrentUser“, resp. jeho Identity parametrů (nejmenuju nějaké konkrétní třídy, jen přibližně pro představu), kde bude dostupný společně s dalšími parametry podobného charakteru.

Osobně radši používám cache spíš na vyšších úrovních než na nižších. Čím nižší, tím více oblastí to ovlivní a tím větší problém je cache zpětně doimplementovat (pak se všude, kde se původně používala ta původní třída musí člověk zamyslet, jestli tam bude chtít ty cachované výsledky nebo normální apod.).

bene

ad1) fuj :-)

ad2) Dekorátor

ad3) Při „výcenásobné“ cache by skládací třída pouze „emulovala“ dekorátor. Řekněme že máme databázi, file cache, local variable cache.

Dekorátor:
$rep = new LocalVariable­ArticleReposi­tory(new FileArticleRe­pository(new DatabaseArticle­Repository());
$rep->find();

Skládací třída:
$rep= ArticleCacheRe­pository();
$rep->add(new DatabaseArticle­Repository());
$rep->add(new FileArticleRe­pository());
$acr->add(new LocalVariable­ArticleReposi­tory());
$rep->find(); // zde by musela byt logika, ktera prochazi jednotlive registrovane repository a vraci vysledek, uklada do cache, atp

jirkakoutny

Ještě jedna věc:

Má Facade povoleno pracovat jen s entitami, nebo může využívat i jiné třídy? Konkrétní příklad:

Z POSTu mi přijde URL stránky, kterou chci uložit do databáze. Před uložením si ale k ní chci stáhnout ještě její titulek (obsah HTML značky <title>). Patří stažení titulku do Facade nebo do Controlleru?

1. možnost http://pastebin.com/mD5RCc8P
2. možnost http://pastebin.com/MctbikJy

Díky

manik.cze

Preferoval bych asi variantu č.1. V té druhé zbytečně přenášíš konkrétní další zodpovědnosti na Controller, které tam imho být nemusí. Je to vždycky těžké, jestli více starostí nechat na „klientovi“ nebo toho víc udělat uvnitř metody, univrzální recept jsem na to ještě nenašel. Čím víc toho necháš na klientovi, tím máš samozřejmě obecnější metodu. Řešením může být nabídnout jak obecnější, tak konkrétnější metodu.

Ostatně tu tvojí metodu bych určitě taky rozdělil (i když je to dělení trochu v jiném smyslu než píšu v předchozím odstavci). V první bych nechal stahování a nalejvání dat do entity, vrátí neperzistovanou entitu. V druhé teprve práci s perzistencí a využití první metody. U té první metody bych právě uvažoval, jestli by se nehodila do té servisní vrstvy, o které píšu ve článku – záleželo by na konkrétní implementaci. Nicméně pokud ji nemáš, tak dvě metody ve fasádě jsou fajn.

manik.cze

Ještě jsem zapomněl na odpověd na původní dotaz:
> Má Facade povoleno pracovat jen s entitami, nebo může využívat i jiné třídy?

Rozhodně to má povolené, ostatně viz návrhový vzor Facade, který tuším v článku taky někde odkazuji.

http://en.wikipedia.org/wiki/Facade_pattern
Schovává používání složitějšího systému za volání jednoduchých metod.

JaSHin

Zdravím,

není mi moc jasné co vrací service. Kdybych nemapoval a nechával entity prosakovat až do presenteru, tak co by vracela konkrétní service, která najde TOP10 článku. Stejně to musí do něčeho přemapovat. Nebo se pletu ?

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.