Pětivrstvý model postavený na NotORM

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Vyki
Člen | 388
+
0
-

Zdravím, dělám nyní na jednom projektu, kde používým pro práci s DB NotORM a mám několik technických dotazů jakým způsobem správně rozdělit vrstvy modelu. Jak poznamenal Jakub Vrána, s NotORM se dá vystavět pětivrstvý model. Jelikož tutoriál jakým způsobem to udělat ještě není na světě rozhodl jsem se vyrobit si takový model sám. Jak jsem začal? Nejprve jsem přemýšlel jak celou strukturu vytvořit s ohledem na následující:

  • Service: vlastní třídy
  • Entita: NotORM_Row
  • Repository: BaseModel
  • Mapper: NotORM_Result
  • Úložiště: PDO

Nejprve jsem si vytvořil modelLoader podobný tomu, který najdete v kuchařce.

[...]
    public function getModel($name) {
	// zde by mohlo být načítání tříd, které od Repository dědí a rozšiřují jeho fce
        // většinou jsem si však vystačil se základními funkcemi find, findBy atd takže jsem
        // to vypustil
        $lname = strtolower($name);

        if (!isset($this->models[$lname])) {
            $this->models[$lname] = new Repository($lname, $this->getNotORM());
        }

        return $this->models[$lname];
    }

    // slouží k načítání services, které jsou uloženy každá ve své třídě
    public function getService($name, $params) {
        $class = 'Projekt\\Service\\' . ucfirst($name);

        if (class_exists($class) === FALSE)
            throw new Nette\InvalidStateException("Service $class was not found.");

        return new $class($this->container);
    }
[...]

Repository jsem v podstatě vpřevzal z Doctrine 2 a lehce upravil. Jakko mapper tam vkládám NotORM.

<?php
class Repository extends Nette\Object
{
    private $mapper;

    private $tableName;

    public function __construct($tableName, $mapper)
    {
        $this->mapper = $mapper;
        $this->tableName = $tableName;
    }

    public function find($id)
    {
        if(is_integer($var) === FALSE)
            throw new Nette\InvalidArgumentException('Identifikátor tabulky musí být číslo!');

        return $this->mapper->{$this->tableName}[$id];
    }

    public function findAll()
    {
        return $this->findBy(array());
    }

    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
    {
        $ret = $this->mapper->{$this->tableName}($criteria);

        if($limit !== NULL)
            $ret->limit($limit, $offset);

        if($orderBy !== NULL)
            $ret->order($orderBy);

        return $ret;
    }

    public function insert(array $data)
    {
        return $this->mapper->{$this->tableName}()->insert($data);
    }

    public function findOneBy(array $criteria)
    {
        return $this->mapper->{$this->tableName}[$criteria];
    }

    public function __call($method, $arguments)
    {
        if (substr($method, 0, 6) == 'findBy') {
            $by = substr($method, 6, strlen($method));
            $method = 'findBy';
        } else if (substr($method, 0, 9) == 'findOneBy') {
            $by = substr($method, 9, strlen($method));
            $method = 'findOneBy';
        } else {
            return parent::__call($method, $arguments);
        }

        if (!isset($arguments[0])) {
             throw new Nette\InvalidArgumentException('Neplatný argument pro výběr z databáze');
        }

        return $this->$method(array($by => $arguments[0]));
    }
}

Funguje to dobře, v presenteru to používám díky properties a trochu magie v loaderu takto:

	$this->model->gallery->find(19);

S čím mám ovšem trochu problém je používání vrstvy service. Pro čtění dat si vystačím s klasickým modelem, tak jak jsem naznačil. Při operacích CREATE, UPDATE, DELETE však nepracuji pouze s databází, ale když například tvořím nové album, chci vytvořit i složky kam bude ukládat fotky. Když budu album mazat, chci za sebou zamést – fotky a adresář alba smazat. Operace jako CREATE, UPDATE, DELETE tedy vyčlením do samostatné třídy service. Jak s tím ale pak pracovat? Jaké mají být mezi service a ostatními vrstvami vazby? V prsenteru by mi pak asi vznikalo něco takového:

	$gallery = $this->model->gallery->find(19);

	/** vráti instanci třídy GalleryService */
	$service = $this->service->gallery->delete($gallery);

Uvedená metoda v GalleryService by mohla vypadat takto:

class GalleryService extends BaseService {
	public function delete(NotORM_Row $row) {
		$path = $this->context->expand($row['path']);
		@unlink($path);
		$row->delete();
	}
}

Jaký máte názor na práci s NotORM modelem tímto stylem? Děkuji moc za každou připomínku či kritiku.

Editoval Vyki (18. 8. 2011 17:55)

Filip Procházka
Moderator | 4668
+
0
-

Já bych ti k tomu dodal, že pokud se nebojíš odklonit od „čistého návrhu Repository“ a povolíš si DAO (Data-Access-Object). Pak můžeš mít v „repozitáři“ i metody save (save zastupuje i update) a delete. Služby pak budou obsahovat pouze a jenom logiku práce s daty. Což je podle mě ideální stav.

Vyki
Člen | 388
+
0
-

Takže když si v repozitáři vytvořím ještě SAVE a DELETE metody tak je budu používat v případě prostého přidání / úpravy / smazání řádku. Když by se při mazání dělo něco náročnějšího budu tyto operace provádět ve službě?

Filip Procházka
Moderator | 4668
+
0
-

Přesně tak. A služba může volitelně používat repozitáře :)

Editoval HosipLan (18. 8. 2011 14:51)

Tharos
Člen | 1030
+
0
-

@Vyki: Chceš názor, máš jej mít ;). Napíšu Ti k tomu pár postřehů, se kterými možná nebudeš souhlasit /a možná to bude i dobře :)/. Ale všechny své výtky se pokusím jasně zdůvodnit a výměnu názorů upřímně vítám :).

  1. ModelLoader. Teď se s nimi zde na fóru úplně roztrhl pytel… Co řeší takto napsaný ModelLoader lépe, než DI kontejner? Proč nevyužít přímo jej? Z mého pohledu jsou všechny zde uvedené ModelLoadery jen chudými příbuznými DI kontejneru a tak nějak nerozumím motivům k jejich dopisování do Nette 2.0.
  2. Takto napsané repository nad NotORMem mi přijde jako věc sama pro sebe (je to spíš takový adaptér nad NotORMem). Přínos repository by IMHO měl být v tom, že umožní snadno vyměnit použitý mapper. To ale tak nějak tiše předpokládá, že ty mappery mají stejné (a nějaké standarní) API. A zde vidím u NotORMu velkou záludnost. NotORM má API velmi svérázné a jsem přesvědčen, že nenajdeš žádný další mapper se stejnými API. Tím pádem se z toho repositáře stává jenom obálka nad NotORMem a v podstatě samoúčelá (funguje opravdu jen jako adaptér API, kdy nepíšeš $notorm->gallery[$id] ale $repository->find($id)).

Prostě mi připadá, že NotORM je ideální používat v presenterch napřímo (jako se používá i Nette\Database v examples). Pokud by repository nad ním mělo mít nějaký smysl, bylo by dobré napsat nad NotORM ještě nějaký adaptér, aby repository vnitřně komunikovalo s mapperem přes nějaké „normální“ API a aby bylo například možné NotORM nahradit řekněme nějakým mapperem postaveným nad Dibi nebo tak. Pak by to IMHO teprve mělo smysl. Ale ruku na srdce, chtělo by se Ti s tím psát :)? Já osobně bych nejprve chtěl mít vážný důvod.

3. Servisní vrstvu bych Ti opět doporučit konfigurovat a používat přes DI kontejner. Sepsal jsi krásný příklad využití takové vrstvy. Já bych jej řešil následovně:

  • Připravit bych si třídu, řekněme Model\Service\Gallery. Ta by obsahovala zmíněné metody pro vytvoření, úpravu a odstranění galerií a v konstruktoru by přijímala vše potřebné pro realizaci toho. Tím určitě bude nějaký NotORM_Result (abys měl jak vymazat požadovaný záznam z databáze) a také nějaká třída zapouzdřující diskové operace nad fotkami na disku (nazývejme ji například jednoduše Model\FileRepository\Gallery – ta by se „správně“ také měla zaregistrovat jako služba).
  • Tu servisní třídu bych si zaregistroval jako službu do DI kontejneru, přičemž bych ji vytvářel nějakou tovární metodou, která by jí předala právě ten NotORM_Result a tu Model\FileRepository\Gallery. Samotné vytvoření té instance by v tovární metodě mohlo vypadat zhruba následovně:
return new Model\Service\Gallery($context->notorm->gallery(), $context->galleryFileRepository);

Důležité je, že parametrem těch CRUD metod by pak už bylo jenom ID galerie, se kterou se má pracovat. Takže ukázka použití v presenteru by mohla vypadat následovně:

$this->context->galleryService->create($data);
// nebo
$this->context->galleryService->update($id, $data);
// nebo
$this->context->galleryService->delete($id);

Tak co si o tom myslíš :)?

Editoval Tharos (19. 8. 2011 0:00)

Filip Procházka
Moderator | 4668
+
0
-

Já bych řekl, že @**Tharos** má s těmi repozitáři pravdu. :))

Vyki
Člen | 388
+
0
-

@Tharos: Moc děkuji za názor. 1) Včera jsem nad tím také přemýšlel zda modely neregistrovat jako prosté služby, ale potřebuji ty modely načítat lazzy. V takovém případě stejně skončím u loaderu, který se mi o ten lazzy loading bude starat. Pokud bych si ty modely chtěl tahat přímod z DI kontejneru, musel bych již při startu aplikace znát jejich názvy. Uměle bych plnil DI kontejner službami, které třeba ani nevyužiji v případě, že po některém z modelů nesáhnu.

  1. Když jsem začínal NotORM používat, tak implementace těch funkcí findBy, findOneBy… byla vlastně první věcí, kterou jsem si dopsal. Jsem zvyklí pracovat s DB pomocí těchto zkratek, do kterých se vkládají podmínky. Když jsem se začal pídit po tom, jak nějak lidsky vytvořit fungující model, až pak jsem si uvědomil, že ty zkratky patří do vrstvy Repository. Kdybych se nyní rozhodl vyměnit mapper, problém by nastal možná u asociací typu $shop->photo['path'], ale jinak mi tento způsob práce s modelem nebrání vyměnit mapper a to je přesně to co od toho očekávám.

Souhlasím s tím, že ten Repozitář není napsaný úplně šťastně, ale plní právě onen účel adaptéru pro komunikaci s NotORM pomocí zkratek. Je jasné, že kdybych měnil někdy mapper, musím si pro něj napsat i nový repozitář, ale to už by bylo minimum práce.

  1. U té servisní vrstvy pro model sdílím stejný názor. Přesně takto jsem to plánoval dneska začít u té galerie tvořit. Ale narážím na to samé jako u načítání modelu. Koukal jsem že třeba Patrik to v Nella frameworku, řeší v podstatě také lazzy loadingem akorát on žádný loader nepotřebuje neboť jej s lazzy loadingem integroval přímo do svého containeru.

Editoval Vyki (19. 8. 2011 9:01)

Filip Procházka
Moderator | 4668
+
0
-

A jak jsi přišel na to, že registrovat službu tě nějak zásadně stojí výkon? DI Container podporuje továrničky a úplně celý je lazy. Pokud službu nepoužiješ, zkrátka se nevytvoří. Za předpokladu, že do Containeru nedáš rovnou instanci. Což se ti s configem nepovede a ani v když budeš služby mít v poděděném Configuratoru, tak se nebudou vytvářet, dokud nebudou potřeba.

Vyki
Člen | 388
+
0
-

Že DI kontajner načítá služby lazy to vím, ale to mi stále neřeší tu situaci, že při startu je musím zaregistrovat a znát jejich jména, která se shodují s názvy tabulek. Jedině, že bych si vytvořil nějaký nástroj, který by se mi o registraci těch služeb staral automaticky a do cache si ukládal názvy těch tabulek v DB a pak by mi ten DI container sám plnil. Je pravda, že to by bylo asi docela dobré. Myslíte, že by se pro tento účel vyplatilo vyhradit samostatný container?

Filip Procházka
Moderator | 4668
+
0
-

Můžeš si přece container upravit, aby když službu nezná, zkusil vytvořit repozitář z NotORM a pak teprve umřel :)

//edit: Pro zájemce naznačím:

public function getService($name)
{
	if (!$this->hasService($name)) {
		try {
			$this->addService($name, $this->repositoryFactory->create($name));
		} catch (\Exception $e){  }
	}

	return parent::getService($name);
}

Editoval HosipLan (19. 8. 2011 19:29)

Tharos
Člen | 1030
+
0
-

Jak bych řešil to s těmi tabulkami. Jako služby bych si neregistroval jednotlivá repository, ale zaregistroval bych si nějakou repositoryFactory. Ta by uměla vyrobit instanci konkrétního repository a ano, zde by byl vhodný prostor pro tu „cache instancí“ (prostě pokud by repository vytvořené pro danou tabulku a mapper už měla, nevytvářela by novou instanci a vrátila by již existující).

Úplné použití v presenteru by poté bylo:

$galleryRepo = $this->context->repositoryFacotry->getInstance('gallery');

na což bych si asi v nějakém BasePresenteru vyrobil zkratku, aby šlo psát pouze:

$galleryRepo = $this->getRepository('gallery');

Mimochodem ta služba repositoryFactory by byla velmi jednoduchá a snadno by se nadefinovala v config.neonu (v podstatě by jako argument jen přijímala další službu, a to něco ala dbMapper – více toho opravdu nepotřebuje), takže by Ti odpadla jakákoliv starost s vlastním kontejnerem či konfigurátorem.


EDIT: V tomto případě by bylo vhodnější tu službu/třídu repositoryFactory nazvat spíše repositoryManager, protože factory v názvu IMHO až příliš evokuje, že se pokaždé vytváří nová instance. Volání $repositoryManager->getInstance(...) mi přijde lépe vypovídající o tom, co se děje („dej mi instanci a je mi jedno, jestli mi vrátíš již existující, anebo vyrobíš novou“). Pro SSRPP by to mohlo být ještě rozdělené na dvě třídy, kdy by RepositoryManager obsahovala instanci RepositoryFactory a rozhodovala by se, jestli už instanci má, anebo zda požádá RepositoryFactory o vytvoření nové. :)

Editoval Tharos (19. 8. 2011 11:12)

Patrik Votoček
Člen | 2221
+
0
-

Vyki napsal(a):

  1. … Koukal jsem že třeba Patrik to v Nella frameworku, řeší v podstatě také lazzy loadingem akorát on žádný loader nepotřebuje neboť jej s lazzy loadingem integroval přímo do svého containeru.

Což už pár dní neplatí protože u Nella Frameworku 2.0 Alpha 2 jsem přesedlal na klasický DI kontejner.

gawan
Člen | 110
+
0
-

Tharos napsal(a):

1. ModelLoader. Teď se s nimi zde na fóru úplně roztrhl pytel… Co řeší takto napsaný ModelLoader lépe, než DI kontejner? Proč nevyužít přímo jej? Z mého pohledu jsou všechny zde uvedené ModelLoadery jen chudými příbuznými DI kontejneru a tak nějak nerozumím motivům k jejich dopisování do Nette 2.0.

nie som si istý, či tejto pripomienke dobre rozumiem. ModelLoader tiež obsahuje DI\Container, ak to správne chápem, potom „best practice“ je používať len jeden systémový kontainer? Tak si to myslel?

Tharos
Člen | 1030
+
0
-

@gawan: Primární problém vidím v tom, že „ModelLoader“ vůbec není nástrojem DI. Nejblíže má k service locatoru. A když už se programátor rozhodně řídit závislosti v aplikaci pomocí DI, nepovažuji „ModelLoader“ ze své podstaty za vhodný nástroj. Mnohem lépe vyhoví DI kontejner.

Obzvláště pak konstrukce v Presenteru typu:

$this->context->modelLoader->...

považuji za pikantní, protože DI se zde mění na v podstatě zřetězené volání dvou service locatorů :). Pokud programátor chce prohlásit, že jeho kód využívá DI, tak tohle určitě není konstrukce, kterou by v něm chtěl mít.

Editoval Tharos (3. 9. 2011 11:56)

hAssassin
Člen | 293
+
0
-

jen takovy dotaz: Mapper ma pracovat s konkretnim ulozistem (DB, pevny disk, memcache, apod.) a repozitar by mel byt most mezi aplikaci (nebo mozna tomu muzeme rikat i model) a tim ulozistem. Cili by mel byt CRUD metody, ktery jen vezmou pozadovany uloziste a predaji data jemu (cili mapperu). A to vse proto, aby, pokud bude nutny pouzit jiny uloziste, stacilo v repozitari zmenit jen jmeno mapperu.

Ale nebylo by mozny, aby jeden repozitar mohl mit najednou mapperu vice a napr u tech obrazku u galerie, v metode create() by se nejprve zavolal DbMapper, ktery by vlozil zaznam do DB a nasledne FsMapper (file system), ktery by ulozil obrazek na disk. Stejne tak pro medoty update() a delete(). Tim padem by pak odpadla service trida a hlavne dalsi repozitar (FileRepozitory). Co si o tom myslite? Muze to tak byt, nebo ne?

@Vyki > mohl bys trochu nastinit trochu ty magie v presenteru nebo modelLoaderu pro vyreseni toho, abys mohl v presenteru volat:

$this->model->gallery->find(19);

Mam to ted podobne ale musim to volat takhle (coz se mi nelibi):

$this->getModel('gallery')->find(19);
Filip Procházka
Moderator | 4668
+
0
-

Mapper ti, jak říkáš, převádí data z databáze na objekty.
Repozitář ti pak dovoluje číst data konkrétního typu, nebo tabulky.
DAO, neboli Data-Access-Object je vzor, který ti rozšiřuje funkce repozitáře o možnosti editace, mazání a vkládání objektů toho jednoho typu (tím nemyslím klasické extends, prostě to je v té stejné třídě).

Služba je pak třída, která ti obaluje logiku souvisejících operací do metod. Třeba že když smažeš téma ve fóru, tak smaže i jeho příspěvky, hodnocení příspěvků, …

hAssassin
Člen | 293
+
0
-

@HosipLan > takze bys to spis videl tak, ze napr. smazani obrazku z DB a posleze z disku videl tak, ze to bude v service, ktera nejdriv zavola repozitar a pak unlink? Nez to co sem psal ja, ze by se v repozitari (resp. DAO) prepnul mapper z jednoho uloziste na jiny?

Jinak slozitejsi veci, jako jsou treba mazani temat a pripadne smazani jejich prispevku a hodnoceni (kdyz pomineme fakt, ze se to vetsinou deje automaticky v DB) by do service jednoznacne patrily. Slo mi jen o tom pristupu k ruznym ulozistim v ramci jednoho repozitare (viz zaznam s galerii a jejich obrazky).

Filip Procházka
Moderator | 4668
+
0
-

Konkrétně obrázky bych řešil takto nějak :)

class Gallery extends Nette\Object
{
	public function __construct($galleryRepository, $galleryStorage)
	{
		$this->... //
	}

	public function deleteImage($image)
	{
		if ($this->galleryRepository->delete($image)) {
			$this->galleryStorage->delete($image);
		}
		// ...
	}
}

Ten muj příklad s fórem měl ilustrovat, jaké věci tam patří. Samozřejmě kaskádové smazání v databázi je s notorm mnohem lepší řešení :)

hAssassin
Člen | 293
+
0
-

@HosipLan > zajimavy reseni, diky. Ale mel bych stejne jeste par dotazu:

  1. ta trida Gallery, kterou si nastinil nahore, predstavuje entitu? Pokud ano, tak se mi tam nezda ta metoda deleteImage(). Nemela by byt entita jen obalkou nad daty? Cili metody pro delete nebo create/update by tam byt vubec nemely, ne?
  2. predstavoval jsem si to tak, ze z prezenteru se budou instaciovat pouze repozitare (DAO) a pripadne servisy (ktery v sobe budou opet volat repozitar) a z nich ziskaji jiz vytvorene entity – viz tvuj tutorial v kucharce.
  3. vytvareni vlastni entity/ziskani dat z entity pred ulozenim je starost mapperu nebo repozitare? Puvodne jsem to mel v repozitari, ktery mapperu predaval pouze pole s daty, nebo z mapperu ziskal pole dat a to prevedl na entitu. Z toho co pises vyse ale vyplyva, ze toto by mela byt spise starost mapperu, ktery by uz mel do repozitare vracet primo instanci entity, ze?
  4. a pokud to chapu uplne spravne, tak service jsou pouze volitelne a nemusi byt implementovany pro kazdy model, ale pouze tam, kde maji smysl (nejaka slozitejsi operace napric i vice „modely“). Primarne se pracuje vzdy s repozitarem (DAO).

Snad vse, ale tohle tema je dost osemetny, takze kdyz uz mam neco vymyslenyho, stejne to po case zase zmenim, takze me mozna jeste neco napadne :-)

Filip Procházka
Moderator | 4668
+
0
-

ta trida Gallery, kterou si nastinil nahore, predstavuje entitu? Pokud ano, tak se mi tam nezda ta metoda deleteImage().

Třída Gallery měla představovat službu s doménovou logikou pro práci s obrázky, nikoliv entitu. Možná jsem ji nevhodně pojmenoval.

Nemela by byt entita jen obalkou nad daty? Cili metody pro delete nebo create/update by tam byt vubec nemely, ne?

To co popisuješ se jmenuje „Anémický model“, jehož podstatou jsou „hloupé“ entity. Není to dobré řešení, ale je to dobrý prototyp, než do entit dostaneš doménovou logiku.

http://martinfowler.com/…inModel.html

predstavoval jsem si to tak, ze z prezenteru se budou instaciovat pouze repozitare (DAO) a pripadne servisy (ktery v sobe budou opet volat repozitar) a z nich ziskaji jiz vytvorene entity – viz tvuj tutorial v kucharce.

Možná už jsi si všiml, že nerad píšu služby do konfiguráku, takže tě zkusím nasměrovat takto

V první řadě přečíst, jak chápe Jakub pětivrstvý model s NotORM

// repozitáře, nebo DAO (podle toho jak chceš pracovat s "entitami" z NotORM)
class GalleryRepository extends BaseRepository {
	public function __construct($notOrmNecoNemapamatujuseAnebuduZjistovat) { $this->...; }
}
class ArticlesRepository extends ...
class FormTopicsRepository extends ...

// služba
class Galleries extends BaseService { ...

// definice containeru
/**
 * Tyto annotace mi umožňují krásné napovídání od IDE
 * @property-read GalleryRepository $galleryRepository
 */
class MyAppContainer extends Nette\DI\Container
{
	public function __construct(Nette\DI\Container $container) {
		// kdyby nějaká služba potřebovala parametry, cache, session, ...
		$this->addService('container', $container);
	}

	protected function createServiceDb() {
		return new NotORM; // konfigurace a nastavení zde
	}

	protected function createServiceGalleryRepository() {
		// jenom ilustrace
		return new GalleryRepository($this->db->tabulka());
	}

	protected function createServiceArticlesRepository() { ...
	protected function createServiceFormTopicsRepository() { ...

	protected function createServiceGalleryStorage() { ...

	protected function createServiceGalleries() {
		return new Galleries($this->galleryRepository, $this->galleryStorage);
	}
}

Ano, všechny služby si registruji ručně a ručně konfiguruji. Takový hotový aplikační Container pak můžu zaregistrovat jako službu v configu, tam mě to už nebolí. Dodělám si do presenteru zkratku

/**
 * Opět díky anotaci získám vazbu na našeptávání a odtud už můžu všechno
 * @return MyAppContainer
 */
public function getApp()
{
	return $this->getService('myApp'); // facebook, google, seznam.cz, ...
}

Ještě čistější by bylo implementovat si IPresenterFactory, který naplní presenter jenom službami které ten potřebuje. Ale to se nevylučuje s tím, co jsem do teď napsal.

vytvareni vlastni entity/ziskani dat z entity pred ulozenim je starost mapperu nebo repozitare?

vytvářet entitu můžeš v kódu $entity = new Entity, nebo pomocí factory $entity = EntityFactory::create();, nebo v mapperu, pokud ji sestavuješ po načtení z databáze. Ovšem o tohle se za tebe stará NotORM. Ty žádné entity s ním ani nemáš, všechny mají stejný typ.

Puvodne jsem to mel v repozitari, ktery mapperu predaval pouze pole s daty, nebo z mapperu ziskal pole dat a to prevedl na entitu. Z toho co pises vyse ale vyplyva, ze toto by mela byt spise starost mapperu, ktery by uz mel do repozitare vracet primo instanci entity, ze?

Ano, repozitář by o databázi vůbec neměl vědět a měl by pracovat s hotovými entitami, nebo parametry pro mapper, který je bude podle nich načítat.

a pokud to chapu uplne spravne, tak service jsou pouze volitelne a nemusi byt implementovany pro kazdy model, ale pouze tam, kde maji smysl (nejaka slozitejsi operace napric i vice „modely“). Primarne se pracuje vzdy s repozitarem (DAO).

Ano, s tímto řešením se ztotožňuji.

Snad vse, ale tohle tema je dost osemetny, takze kdyz uz mam neco vymyslenyho, stejne to po case zase zmenim, takze me mozna jeste neco napadne :-)

Hlavně si přečti co psal Jakub. Protože s NotORM máš o 4 vrstvy postaráno, v některých případech nemusíš ani vytvářet repozitáře, ale rovnou můžeš mít jako služby ty objekty reprezentující tabulku.

hAssassin
Člen | 293
+
0
-

@HosipLan > diky za vycerpavajici odpoved :-) Velmi pekne napsane. Par postrehu:

Protože s NotORM máš o 4 vrstvy postaráno, v některých případech nemusíš ani vytvářet repozitáře, ale rovnou můžeš mít jako služby ty objekty reprezentující tabulku.

sice jsem to vubec neuvadel, ale NotORM nepouzivam, jedu na dibi, ptal jsem se spis obecne na model (vlaken o modelu a jeho vrstvach je tady vic nez dost a nechtel sem zakladat nove, jen me nenapadlo ze to mozna sklouzne do OT, co se nazvu tohoto vlakna tyce, ale ono to je vlastne jedno jestli pouziju NotORM, Nete\Database `nebo `dibi, model je porad model).

Možná už jsi si všiml, že nerad píšu služby do konfiguráku, takže tě zkusím nasměrovat takto

ano, uvadel jsem to jako priklad, taky nedavam vsechno jako sluzby do configu, ale mam na to vlastni loader.

…vytvářet entitu můžeš v kódu $entity = new Entity, nebo pomocí factory $entity = EntityFactory::create();, nebo v mapperu…

slo mi hlavne o to, jestli je spravny to, ze mapper muze vubec instanciovat novou entitu, ze bude sam napr v metode find() volat new Entity, kterou naplni daty. Proto to mam zatim v repozitari. Asi uplne nejlepsi(/nejcistejsi) by bylo mit tu tovarnicku na entity jak pises a ta by se mohla volat odkudkoliv.

V první řadě přečíst, jak chápe Jakub pětivrstvý model s NotORM

cetl jsem nekolikrat, petivrstvy model je skvely clanek a snazim se podle neho ten model nejak dat dokupy (jednu funkcni verzi uz mam hotovou, ale delam refactoring, nepracuje se mi s tim uplne pohodlne :-)

To co popisuješ se jmenuje „Anémický model“, jehož podstatou jsou „hloupé“ entity. Není to dobré řešení, ale je to dobrý prototyp, než do entit dostaneš doménovou logiku.

diky za link na clanek, cetl jsem ho, ale nejsem si jisty jestli ho chapu uplne spravne. nicmene se mi „hloupe entity“ taky uplne nelibi. Gettery a hlavne settery by meli byt urcite pro typovou kontrolu. Ale metody pro update/create a delete se mi v entite libi, s tim, ze by nevolaly primo mapper (a uz vubec ne dibi) ale volaly by repozitar (v tomto pripade uz DAO), ktery by sam volal prislusny mapper.

Jediny co se mi porad nejak nelibi, je mit pro kazdy konkretni typ uloziste (pro mapper) i spesl repozitar (viz tvuj prispevek vyse s galerii a smazanim obrazku). Nejak mi to do toho nechce zapadnout :-) ale to uz je asi muj problem…

Filip Procházka
Moderator | 4668
+
0
-

Jediny co se mi porad nejak nelibi, je mit pro kazdy konkretni typ uloziste (pro mapper) i spesl repozitar (viz tvuj prispevek vyse s galerii a smazanim obrazku). Nejak mi to do toho nechce zapadnout :-) ale to uz je asi muj problem…

Ale to nemusíš, tady jde o to, že obrázky jsou v databázi i na disku. Všimni si, že třídě co pracuje s diskem neříkám repozitář, ale storage :)

Ono je to v podstatě jedno. Mně se líbí tato implementace. Samozřejmě by jsi to mohl udělat i tak, že práce s obrázky by se kompletně schovala za repozitář a ten by se o to postaral, nebo klidně i mapper.

Každý píše „změním mapper a ukládám třeba na disk, nebo do session“ tak to je situace, kterou prostě nikdy potřebovat nebudeš. Jde jen o to lépe ilustrovat oddělení zodpovědností. Když začátečníkovy řekneš, že by to někdy mohl potřebovat vyměnit, více se bude snažit aby to bylo čisté a dodržovalo nějaký interface. Když pochopíš, že to nepotřebuješ získáš více volnosti a můžeš to pak napsat o něco lépe, s tím že už víš, jak neporušit zodpovědnosti.

hAssassin
Člen | 293
+
0
-

@HosipLan > aha, to mi nejak nedoslo, sice jsem si nazvu Storage vsimnul, ale myslel jsem si ze to je specialni repozitar, ktery pak pracuje se specialnim mapperem. Coz ale vlastne ani neni nutny, jelikoz by to mohlo pracovat primo se soubory na disku a nad nimi provadet operace a to uz mi tak spatny neprijde a celkem se mi to líbí.

Kazdopadne diky za rozvinuti myslenek a nakopnuti k novym napadum :-)

EDIT: jeste me napadla jedna vec, ktera me taky trochu trapi. hodnekrat jsem se setkal s resenim, ktery rozdeli model na „dve“ casti – prvni, ktera se pouziva pouze na frontu ma pouze metody pro ziskani dat, druha, ktera se pouziva v administraci, ma metody i pro ukladani/vytvareni a mazani zaznamu. Jak na tohle nejlip jit? Rozdelit repozitare na dva, pricemz ten druhy by dedil od prvniho, nebo na to prdet uplne?

Editoval hAssassin (5. 9. 2011 22:24)

hAssassin
Člen | 293
+
0
-

@HosipLan > napadla me jeste jedna vec. Jde o ty „hloupy“/„chytry“ entity. Jak jsi psal, dostat do entity domenovou logiku, jako priklad me napadlo toto:

Mejme entitu, ktera reprezentuje nejaky strom. Pak pravdepodobne bude v DB sloupcek parent_id a entita bude mit property $parentId. Dale by mohla mit property $parent coz by byl objekt rodicovske entity stejne tridy (s id $parentId). V entite by mohla byt metoda getParent(), ktera by zavolala repozitar, jeho metodu findSingle($parentId) a ziskala entitu rodice. Samo ze by to bylo lazy, cili rodic by se zjistoval jen jednou a to az na pozadani, napr. v sablone. Je toto dobry priklad jak z entity udelat „chytrou“ entitu nebo by bylo lepsi to resit jinak? Jak jsem psal vyse, pouzivam dibi, ne NotORM.

A jeste jedna vec me trapi. Jde o to, ze z prezenteru (at uz by to bylo reseno napr. jak naznacil Vyki ve svem prvnim prispevku) se bude volat bud $this->repozitory (ziska repozitar) nebo $this->service (ziska sluzbu modelu). Potom ale prezenter musi vedet co ma volat, jestli staci volat repozitar nebo servicu. Coz je takovy divny. Lepsi by bylo volat vzdycky jen jedno z prezenteru (proste $this->model) a uvnitr by se to uz zaridilo podle aktualni situace. Nejlepsi by bylo asi volat vzdy sluzbu, ktera by bud jen predala rizeni repozitari, nebo provedla neco slozityho. Co vy na to?

Filip Procházka
Moderator | 4668
+
0
-

hodnekrat jsem se setkal s resenim, ktery rozdeli model na „dve“ casti – prvni, ktera se pouziva pouze na frontu ma pouze metody pro ziskani dat, druha, ktera se pouziva v administraci, ma metody i pro ukladani/vytvareni a mazani zaznamu.

Tohle je hodně dobrá prasárna, která nemá absolutně žádné opodstatnění. Je to pokus o „jakože dobrej nápad“, který je zcela zcestný.

Mejme entitu, ktera reprezentuje nejaky strom…

Mít tam parentId i parent je zbytečné, protože když tam máš jenom parent, tak si z něj můžeš vždy získat jeho id. Jinak, tomuto se říká návrhový vzor „proxy“. V podstatě podědíš třídu entity a když přistoupíš k nějaké vlastnosti spustíš tím načtení hodnot.

Takže pak máš třeba třídu

class Leaf extends Nette\Object
{
	private $parent;
	private $children = array();
	public function setParent(Leaf $parent) { $this->parent = $parent; }
	public function getParent() { return $this->parent; }
	public function getChildren() { return $this->children; }
}

Když bys teď načetl jeden „lísteček“ z databáze, který má rodiče a děti, tak se ti vytvoří jeho objekt, a proxy objekty rodiče a dětí. Ty proxy objekty by měly být ideálně generované a vypadaly by takto nějak:

class LeafProxy extends Leaf
{
	private $__initializator;
	protected function __initialize() {
		$this->__initializator->initialize($this); // třeba
	}

	public function setParent(Leaf $parent) {
		$this->__initialize();
		return parent::setParent($parent);
	}

	public function getParent() {
		$this->__initialize();
		return parent::getParent();
	}

	public function getChildren() {
		$this->__initialize();
		return parent::getChildren();
	}
}

Takže si načtu lísteček

$leaf = $model->find($id);

Ten má nějakého rodiče a nějaké děti

$parent = $leaf->getParent(); // tady se nic nenačítá
$children = $leaf->getChildren(); // tady take ne

Objekt v $parent i všechny objekty v $children byly vytvořeny mapperem z proxy objektu Leaf a umí sebe načíst z databáze, znají svoje id, ale neobsahují žádná data.

Díky tomu můžu lazy přistupovat k dalším „sousedním“ vlastnostem.

$parentOfParent = $parent->getParent(); // až teď se naplní daty a položí dotaz

Kolik ale myslíš, že by tohle vytvořilo dotazů na databázi?

foreach ($leaf->getChildren() as $childLeaf) {
	echo $childLeaf->name;
}

Ano správně, Jeden pro $leaf a jeden pro jméno každého potomka. Říká se tomu „N+1 problem“. NotORM tohle dělá ještě chytřeji. Dokáže odhadnout, když načítáš závislé entity a všechny je načte najednou podle IDček. Takže položí jeden dotaz pro $leaf a jediným dalším načte všechny data těch $childLeaf. Efektivněji to ani nejde :)

Opravdu chceš něco takového psát sám? Nebylo by lepší použití hotová řešení, jako je Doctrine 2 nebo NotORM?

A jeste jedna vec me trapi…

To se ti možná zdá jako dobrý nápad, ale ve výsledku by to bylo něco obludného a jak by k tomu přišel presenter, který potřebuje pracovat s více modely?

Opravdu je $this->galeriesRepository a $this->galleries správné řešení.