Doctrine a service vrstva aneb takto mi to dává smysl

Webexpo 2011 bylo velice inspirativní. S Patrikem jsme si v sobotu večer otevřeli notebooky a konečně jsme se odhodlali převést myšlenky v realitu. Měl jsem vcelku jasnou vizi, kterou mi Patrik pomohl dopilovat k dokonalosti.

Repozitáře nestačí

Repozitář je, podle definice, jenom taková chytřejší kolekce. Je potřeba entity i ukládat a mazat, nevidím důvod, proč by to nemohl dělat ten stejný objekt. Vznikl tedy Dao, nebo-li Data-Access-Object.

Dao implementuje několik samostatných rozhraní a rozšiřuje repozitář

class Dao extends Doctrine\ORM\EntityRepository
implements IDao, IQueryable, IObjectFactory

V Doctrine\ORM\EntityRepository je základní funkčnost jako findAll(), find(), … vždyť to znáte.

IDao definuje metody save() a delete(), které umí pojmout i pole nebo kolekce, pro snadnější manipulaci s entitami. Vychází z rozhraní IQueryExecutor, ale o tom až za chvíli

IQueryable definuje metody createQueryBuilder() a createQuery(). Ta první již je v repozitáři, ale ta druhá je jenom v EntityManageru. Potřebné jsou ale obě dvě.

A IObjectFactory definuje jednoduchou metodu createNew(), pro vytváření nových instancí entit.

Zatím vcelku jednoduché, že? Pár metod navíc pro repozitář, aby se s ním lépe pracovalo. Tuto třídu Dao nastavuji a vynucuji pomocí listeneru při načítání metadat. Takže všechny entity ji mají v základu místo obyčejného repozitáře.

Zde bych doporučil článek Honzy Tichého Pět vrstev modelu a související diskuzi.

Tolik metod musí stačit

Dao třída nám pěkně nabobtnala a umí toho tak akorát. Kdybych se měl držet myšlenek svého článku v Nette kuchařce ERM, tak bych nyní, pro specifické dotazy, třídu Dao dědil a přidával jí metody pro jednotlivé DQL. Metody jako findBarByBazAndOrderItByFoo() rychle přibývají a objekt těžkne, ztrácí řád a vůbec toho umí nějak moc.

Zde přichází na řadu Aleš Roubíček s článkem Doménové dotazy, který mi připomněl dávno zapomenuté články od Fowlera. V podstatě je to kuchařka na samostatné třídy pro DQL. Možná je to v jiném jazyce, ale je to snadno pochopitelné, takže tuto část do hloubky rozebírat nebudu a poprosím Vás odskočit si na jeho článek pro detaily a chybějící souvislosti.

Definoval jsem si tedy rozhraní IQueryObject a IQueryExecutor, kterému Query objekty předávám a získávám tak výsledek. Když teď chci zapsat dotaz, tak si podědím abstraktní QueryObjectBase, která už rozhraní IQueryObject implementuje a implementuji metodu doCreateQuery(), kterou vyžaduje abstraktní předek.

class RolePermissionsQuery extends QueryObjectBase
{
    /** @var Role */
    private $role;

    /**
     * @param Role $role
     */
    public function __construct(Role $role)
    {
        $this->role = $role;
    }

    /**
     * @param IQueryable $dao
     * @return Doctrine\ORM\QueryBuilder
     */
    protected function doCreateQuery(IQueryable $dao)
    {
        return $dao->createQueryBuilder('perm')->select('perm', 'priv', 'act', 'res')
            ->innerJoin('perm.privilege', 'priv')
            ->innerJoin('perm.role', 'role')
            ->innerJoin('priv.action', 'act')
            ->innerJoin('priv.resource', 'res')
            ->where('role = :role')->setParameter('role', $this->role);
    }
}

A získávám výsledek zjednodušeně takto

$role = $em->getRepository('Role')->find(1);
$result = $em->getRepository('Permission')
    ->fetch(new RolePermissionsQuery($role));

Nemusím tedy znečišťovat Dao dalšími metodami, protože všechno mám rozdělené na třídy.

A ještě bych upozornil, že Doctrine mám obaleno v samostatném DI Containeru (subcontainer), takže na EntityManager se snažím nesahat, volám si přes zkratku v něm jenom jednotlivé Dao.

Kam se poděly service?

Někdo možná špatně pochopil, že ty service, to jsou vlastně třídy, které mají metody save(), delete() a dáváme jim repozitář. Taky jsem to tak chvilku zkoušel, ale to mi prostě nedává smysl.

Dao a Query objekty perfektně obslouží persistenci a načítání entit a service už je všechno ostatní. Validační pravidla, počítaní, nějaké managery, … Zkrátka všechny třídy, které nezajímá, že někde je nějaká databáze a operují s logikou, kterou je naučíte.

Co jsem tím získal?

  • Zvrácenou radost, že skoro vůbec nepotřebuji EntityManager. Pouze na získávání metadat.
  • Nemusím rozšiřovat Dao, abych jim vnucoval metody na složité dotazy.
Mám tu chybu? Fix me

Autor:

comments powered by Disqus