tiny ‘n’ smart
database layer

Odkazy: dibi | API reference

Oznámení

Omlouváme se, provoz fóra byl ukončen

Lean Mapper – tenké ORM nad dibi

před 5 lety

Michal III
Člen | 84
+
0
-

Glottis napsal(a):

jde to i takhle

static public function camelize($word) {
    return lcfirst(preg_replace('/(^|_)([a-z])/e', 'strtoupper("\\2")', $word));
}

Proč hledat místo pouhého ‚_‘ i začátek řetězce (^), zvětšit tak i počáteční písmeno a posléze ho zase zmenšit funkcí lcfirst?

před 5 lety

Casper
Člen | 253
+
0
-

Zdravím. Super update, teď už jenom ty vysněné defaultní hodnoty :)

Měl bych návrh na mírnou dekompozici metody createEntity. Její poslední řádek return new $entityClass($row); znemožňuje využít DI pro vytváření entit. Osobě využívám nové Nette továrničky i na vytváření entit a potřebuji tedy entitu vytvořit stylem $factory->create($row);. Myslím, že by bylo škoda, kdyby LM bránil plnému využití DI. Stačilo by prosté:

protected function createEntity(DibiRow $dibiRow, $entityClass = NULL, $table = NULL) {
        if ($table === null) {
                $table = $this->getTable();
        }
        $result = Result::getInstance($dibiRow, $table, $this->connection, $this->mapper);
        $primaryKey = $this->mapper->getPrimaryKey($this->getTable());

        $row = $result->getRow($dibiRow->$primaryKey);
        if ($entityClass === null) {
                $entityClass = $this->getEntityClass($row);
        }

        return $this->createEntityClass($row, $entityClass);
}

protected function createEntityClass(\LeanMapper\Row $row, $entityClass){
    return new $entityClass($row);
}

přičemž, pokud by někdo nevyužíval DI, nic by se pro něj nezměnilo a ti, kteří už bez něj nedají ani ránu (jako já), by si v každém repozitáři přetížili pouze createEntityClass s jedním řádkem:

protected function createEntityClass(\LeanMapper\Row $row, $entityClass = NULL) {
    return $this->myEntityFactory->create($row);
}

EDIT: tak jsem to trochu ozkoušel a mnohem větší problém by byl s vytvářením entit mimo repository, tedy například v Entity->getBelongsToManyValue. Nějaké chytré řešení mě moc nenapadá, ideální by bylo, kdyby veškeré vytváření entit probíhalo přes Repository, nicméně to by každá entita musela mít přístup do všech Repository a využívat jejich metody k vytváření potřebných entit. Myslíš, že by bylo možné tohoto nějak dosáhnout? Osobně si myslím, že by to rozhodně stálo za to.

Také moc nechápu proč vytváření entit v Entity dostává jako druhý argument Mapper, když v konstruktoru si mapper bereš z předaného Row (new $class($row, $this->mapper);). Předpokládám, že jde o zapomenutý argument z dříva :)

Editoval Casper (1. 8. 2013 14:55)

před 5 lety

Casper
Člen | 253
+
0
-

@Šaman:

P.S. Ještě by někdo mohl zveřejnit mapper, který ctí stejná pravidla, jako Ndb. (Je teď moc práce a příští týden odjíždím na tábor. Pokud nějakou dobu nebude, tak si ho napíšu a zveřejním sám.) Tento mapper by mohl být i součást jádra, protože řeší převod $fooBar property na foo_bar sloupce a to je dost potřeba.

Nevím přesně co ctí NDB, nikdy jsem jej nepoužíval, nicméně pro konvence názvů tabulek a sloupců foo_bar využívám tento mapper. Třeba je to alespoň z části to, co hledáš.

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Casper napsal(a):
EDIT: tak jsem to trochu ozkoušel a mnohem větší problém by byl s vytvářením entit mimo repository, tedy například v Entity->getBelongsToManyValue. Nějaké chytré řešení mě moc nenapadá, ideální by bylo, kdyby veškeré vytváření entit probíhalo přes Repository, nicméně to by každá entita musela mít přístup do všech Repository a využívat jejich metody k vytváření potřebných entit. Myslíš, že by bylo možné tohoto nějak dosáhnout? Osobně si myslím, že by to rozhodně stálo za to.

Entity by neměla správně vůbec o okolí nic vědět, tudíž nemá vědět ani o repository.

Proč si vlastně nenapíšeš BaseRepository class, od které všechny další repositories budou dědit? Tam si napíšeš vlastní createEntityClass, kde na základě předaného názvu classy zavoláš správnou továrničku, a je to. Ale spíše mi přijde trochu divné tohle řešit přes factories, to jak je to řešeno teď je z hlediska DI naprosto správně…

Také moc nechápu proč vytváření entit v Entity dostává jako druhý argument Mapper, když v konstruktoru si mapper bereš z předaného Row (new $class($row, $this->mapper);). Předpokládám, že jde o zapomenutý argument z dříva :)

To je tam právě k vazbám na další entity, bez zdroje dat by entita nevěděla, čím naplnit tuto navázanou entitu.

Editoval daniel.mejta (1. 8. 2013 17:25)

před 5 lety

Casper
Člen | 253
+
0
-

@daniel.mejta:

Entity by neměla správně vůbec o okolí nic vědět, tudíž nemá vědět ani o repository.

Ano to je hezké, ale jak vyřešíš jejich závislosti? Někdo jim je musí předat a pokud již LeanMapper\Entity musí jiné entity vytvářet, musí to být on, nebo se musí vytváření někam přesunout.

Proč si vlastně nenapíšeš BaseRepository class, od které všechny další repositories budou dědit? Tam si napíšeš vlastní createEntityClass, kde na základě předaného názvu classy zavoláš správnou továrničku, a je to. Ale spíše mi přijde trochu divné tohle řešit přes factories, to jak je to řešeno teď je z hlediska DI naprosto správně…

BaseRepository jsem si právě udělal, nicméně pokud jsi četl můj příspěvek, tak entity se právě nevytvářejí pouze v Repository, nicméně třída LeanMapper\Entity vytváří své „vztahové“ entity také a to je právě ten problém, o kterém jsem psal.

A to, že bych již v BaseRepository volal správnou továrničku je nesmysl, musela by mít závislost na továrničkách všech Entit, tedy každé další Repo by muselo předávat tisíc továrniček. Proto jsem psal o metodě, která by se přetížila, aby si každé Repo vyžádalo pouze továrničku své entity.

Co se ti zdá divné na využití factories? Je to naprosto nejpohodlnější. Uvedu závislost do konstruktoru, zaregistruju v configu a o víc se nestarám, Nette to vygeneruje za mě. Entita není jen schránka na data a často potřebuje jiné služby nebo parametry (i kdyby jen cestu k souborům). Tohle by naopak z LM vytvořilo nepřekonatelnou knihovnu, pokud by šlo využít nových Nette továrniček a plnohodnotného DI.

Momentálně to není z hlediska DI vůbec správně, jelikož to žádné DI nepodporuje. Nemám žádný způsob jak entitě (vytvořené na základě vztahové property) předat závislost.

To je tam právě k vazbám na další entity, bez zdroje dat by entita nevěděla, čím naplnit tuto navázanou entitu.

kde vidíš zpracování druhého argumentu?

Editoval Casper (1. 8. 2013 19:01)

před 5 lety

Tharos
Člen | 1036
+
0
-

Ad druhý argument: Jo, to je skutečně relikt z minulosti. :) Nedávno jsem trochu zjednodušoval API a skutečnost, že se ten druhý argument předává ještě v těchto metodách, mi unikl. Díky za upozornění. :)

před 5 lety

Tharos
Člen | 1036
+
0
-

@Casper, @daniel.mejta:

Ideální by bylo, kdyby veškeré vytváření entit probíhalo přes Repository, nicméně to by každá entita musela mít přístup do všech Repository a využívat jejich metody k vytváření potřebných entit. Myslíš, že by bylo možné tohoto nějak dosáhnout? Osobně si myslím, že by to rozhodně stálo za to.

Přesně tohle už jsem zkoušel. No, nedopadlo to moc slavně… ORM hodně bobtnalo, dost se komplikovalo a zároveň před očima i ztrácelo své možnosti. Někde na začátku tohohle vlákna jsem o tom svém experimentu i psal… Výsledek, který se rýsoval, z mého pohledu byl jen v málo věcech lepší a v mnoho věcech výrazně horší (například by to dost zkomplikovalo načítání entit v NotORM stylu).

Existují určité ideály, které se ale obtížně uvádějí do praxe. Bylo by hezké, kdyby entita nevěděla vůbec nic o svém okolí, ale pak by fakticky nemohlo být možné „traverzovat“ mezi entitami majícími mezi sebou vazby.

Po zkušenostech s Lean Mapperem tvrdím, že napsat ORM je vždy určitý kompromis mezi „akademickou čistotou“ návrhu, pragmatičností a tenkostí řešení. Myslím si, že nelze vynikat ve všech těchto oblastech zároveň. Lean Mapper si klade za cíl být pragmatický (dobře použitelný a s širokými možnostmi) a zároveň tenký (dá se v něm ještě snadno vyznat a díky jisté vnitřní jednoduchosti je myslím vcelku málo náchylný na chyby – od jeho vydání jsem musel opravovat v podstatě minimum chyb). Cenou za to je, že obsahuje zjednodušenou Identity Map, entity nevyužívají k načtení souvisejících entit repositáře a ještě pár dalších věcí.


S tím vytvářením entit přes factory je to zajímavý postřeh. Já si skoro myslím, že Lean Mapper sám je dostačující factory na entity. Jaký hlavní přínos by mělo podstrčení nějaké své vlastní továrny?

Jsem si vědom toho, že v současné době se entitám obtížně injektují nějaké služby – respektive nejde to přes konstruktor. Pokud entita pro realizaci svého API potřebuje ještě nějakou jinou službu, musí se jí předat přes nějaký setter. Jenomže je otázka, jestli by entity vůbec měly takhle s jinými službami pracovat. Mně přijde vhodné mít „nad tím“ ještě nějakou servisní vrstu (standardně zaregistrovanou v DI kontejneru) a podobnou vyšší logiku řešit/delegovat až v ní.

Debatu by určitě učinil přínosnější nějaký konkrétní use-case.

Editoval Tharos (1. 8. 2013 19:49)

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:

Jsem si vědom toho, že v současné době se entitám obtížně injektují nějaké služby – respektive nejde to přes konstruktor. Pokud entita pro realizaci svého API potřebuje ještě nějakou jinou službu, musí se jí předat přes nějaký setter. Jenomže je otázka, jestli by entity vůbec měly takhle s jinými službami pracovat. Mně přijde vhodné mít „nad tím“ ještě nějakou servisní vrstu (standardně zaregistrovanou v DI kontejneru) a podobnou vyšší logiku řešit/delegovat až v ní.

Injektování přes konstruktor je právě to, co by bylo super, kdyby fungovalo (nemusel bych se o nic starat). Zda by entity vůbec měly využívat jiné služby je asi trochu spekulativní, nicméně zkusím nastínít konkrétní use-case, na kterém jsem se zasekl a který ukazuje, že služby se mohou velice hodit i v entitách:

Načtu si entitu z databáze pomocí repository a potřebuji získat její soubor v PDF. Ten ale může a nemusí existovat a entita jej má pouze vrátit. V getteru by tedy bylo ověření zda soubor existuje a pokud ne, požádala by nějaký PdfCreator o vygenerování a následně by soubor vrátila. Tedy nějak takto:

public function getPdf(){
    $file = $this->pdfPath."/".$this->id.".pdf"; // pdfPath je injektovaný parametr

    if(is_file($file)){
        return $file;
    }else{
        return $this->pdfCreator->createPdf($this->a, $this->b); // požádám injektovanou službu o vytvoření
    }
}

Podobných scénářů lze vymyslet více a podstatné je, že já mám entitu a chci od ní něco získat. Ona to nemá a potřebuje požádat někoho dalšího. Nelze tedy postupovat opačně, že vezmu pdfCreator, předám mu entitu, ten si z ní vytahá data a vygeneruje potřebné, protože tohle je zodpovědnost entity, aby mi dodala něco svého a já ani nemusím potřebovat nic generovat (viz příklad).

Moc si nedokážu představit vyšší vrstvu postavenou nad entitami, aby mi zajistila stejnou funkcionalitu jakou popisuji (tedy, že závislost jednou deklaruji (config), jednou vyžádám (construct) a to je vše). Mohl bys nastínit o co by šlo? Pětivrstvý model jsem četl, nicméně zde je servisní vrstva zmíněna spíše kvůli validacím a podobným věcem, které již LM podporuje.

Editoval Casper (1. 8. 2013 20:14)

před 5 lety

Tharos
Člen | 1036
+
0
-

Jo, a to je přesně situace, kterou bych já řešil trochu jinak. :) Jenom v nástřelu:

<?php

namespace Model\Utility;

use Model\Entity\Book;

class PdfCreator
{

    private $pdfPath;


    public function __construct($pdfPath)
    {
        $this->pdfPath = $pdfPath;
    }

    public function getDocument(Book $book)
    {
        $document = $this->getDocumentName($book);
        if (!file_exists($document)) {
            $this->generateDocument($book);
        }
        return $document;
    }

    private function getDocumentName(Book $book)
    {
        return $this->pdfPath . '/' . $book->id . '.pdf';
    }

    private function generateDocument(Book $book)
    {
        $generatedPdfData = ... // here we generate document
        file_put_contents($this->getDocumentName($book), $generatedPdfData);
    }

}
namespace Model\Service;

use Model\Repository\BookRepository;
use Model\Utility\PdfCreator

class Books
{

    private $bookRepository;

    private $pdfCreator;


    public function __construct(BookRepository $bookRepository, PdfCreator $pdfCreator)
    {
        $this->bookRepository = $bookRepository;
        $this->pdfCreator = $pdfCreator;
    }

    public function getBookDocument($id)
    {
        return $this->pdfCreator->getDocument(
            $this->bookRepository->find($id)
        );
    }

}

Ta servisní vrstva by pak měla i další metody pro získávání knih a všeho možného s nimi souvisejícího a do presenterů bych si pak injektoval už jenom jí.

Rozdělení odpovědností mi zde přijde mnohem lepší:

Book – entita, která reprezentuje knihu
BookRepository – repositář, který umí načítat a persistovat knihy
PdfCreator – třída, která si řeší své PDF soubory (jako jediná řeší nějakou `$pdfPath`)
Books – fasáda, která zapouzdřuje určité scénáře a umožňuje velmi snadno používat celý „subsystém“
Presenter – řídí životní cyklus aplikace (a bohužel většinou řeší ještě řadu dalších věcí…)

U Tvého řešení se mi nelíbí, že entita nejen reprezentuje nějakou skutečnou knihu, ale zároveň i managuje někde nějaká PDFka. Tím zároveň vzniká ta její (IMHO nevhodná) závislost na $pdfPath a nějakém generátoru.

Editoval Tharos (1. 8. 2013 21:23)

před 5 lety

Casper
Člen | 253
+
0
-

Ta servisní vrstva by pak měla i další metody pro získávání knih a všeho možného s nimi souvisejícího a do presenterů bych si pak injektoval už jenom jí.

Tvá servisní vrstva poněkud přebírá zodpovědnost Repository, pokud by měla metody pro získávání knih ne? A dokonce z mého pohledu i napůl přebírá zodpovědnost Entit, tedy získávat nějaká jejich data (ten dokument se vztahuje k entitě). Přijde mi zvláštní, aby jedna třída měla metody jako getBook (získání entity) a getBookDocument (získání dat entity – už ten název naznačuje, že chceš Document entity Book). Chápu, že už je to trochu filozofie, nicméně jediný účel téhle vrstvy vidím v tom, že obcházím právě zmiňované injektování závislostí entitám a vzniká tím jakási (pro mě) zvláštní mezivrstva. Tu bych nemusel potřebovat pokud by se entity staraly o svá data (včetně validace, …) a repository (alias DAO) o práci s entitami :)

U Tvého řešení se mi nelíbí, že entita nejen reprezentuje nějakou skutečnou knihu, ale zároveň i managuje někde nějaká PDFka. Tím zároveň vzniká ta její (IMHO nevhodná) závislost na $pdfPath a nějakém generátoru.

Představit si můžeš klidně entitu faktura, u ní je PDF zřejmější :)


EDIT: jako zřejmější služba mě napadl například validátor. Validace nemusí být vždy pouze ověření hodnoty a rozhodně jí můžeš potřebovat na více místech aplikace (formulář, entita, …) což z ní dělá jasnou službu, kterou entita skutečně potřebuje a už se to moc nedá obejít přes jinou vrstvu. Leda by kompletně přebírala zodpovědnost ukládání entit a před persistencí dělala nějaké validace. To je možná lepší příklad, protože tímhle by tato vrstva nakynula na jakousi supervrstvu dělající úplně všechno (získávání entit, ukládání, validaci, získávání speciálních dat entity, …).

Editoval Casper (1. 8. 2013 21:45)

před 5 lety

Tharos
Člen | 1036
+
0
-

Tohle už je na debatu někde u piva. :)

Moje aplikace (tedy alespoň ty méně nabastlené) nastíněným způsobem fungují. Struktura se hodně podobá této (maximálně doporučuji – jak článek, tak i filozofii) jen s tím, že čemu Vašek v tom článku říká Facade (což je správný název toho vzoru), tomu já říkám Service. Je to trochu slovíčkaření, podstatné je, co ty třídy reálně dělají. Ono v tom názvosloví je obecně guláš (třeba jen pojem Repository má takových výkladů…).

Ta fasádní vrstva má hodně potenciálních přínosů. To, jak moc se projeví, ale záleží na složitosti té či oné aplikace. Já se teď třeba podílím na jedné, kde se data vypisují jednak na webu a jednak v RESTovém API. Některé informace se zjišťují poměrně složitě (košatá business logika) a je k nezaplacení, když je tahle košatá logika zabalená v nějaké fasádě a já ji mohu recyklovat jak v controllerech realizující web, tak i v controllerech realizujících API. Ty controllery jsou krásně malé třídy „jdoucí k věci“. Ale blbě se to takhle abstraktně popisuje…

Co když ve Tvém řešení přibude i XLS výstup? Budeš kvůli tomu muset změnit entitu. To já prostě nepovažuji za optimální a jsem přesvědčen, že existuje způsob, jak se tomu vyhnout. :)

Editoval Tharos (1. 8. 2013 23:54)

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:

Fasádní vrstvu samozřejmě znám, ale nikdy se mi právě nezdálo moc podstatné jí zavádět dokud nepotřebuji využívat API aplikace z více míst (jako REST). Na druhou stranu je pravda, že už jsem nějakou chvíli přemýšlel o ideálním místě pro umístění transakcí a tato mezivrstva se mi zdá jako ideální místo.

Chápu to tedy tak, že z mých argumentů necítíš potřebu se s tím dělat, takže to zkusím s nějakou tou mezivrstvou. Ať nezaplácáme diskusi o LM nějakou dohadou o ideální architektuře, tak se ještě připojím několik malých poznámek:

  • Bylo by vhodné vylepšit chybové hlášky. Minimálně v entitách neobsahují informaci o tom o kterou entitu se jedná a musím to hledat v call-stacku. Přidal bych tedy do chybových hlášek get_called_class.
  • Je opravdu žádoucí aby metoda getData vracela výsledky všech public getterů? Mohu si do BaseEntity nadefinovat všelijaké pomocné gettery pro speciální případy a tato metoda pak vrací i jejich výsledky. V mém případě mám implementovanou metodu getPreviewData a její výsledky to vrací také. Osobně metodu getData nepoužívám, ale chování mi nepřijde vhodné, myslím, že by tato metoda by měla vracet pouze ty hodnoty, které má entita uvedené v anotacích. (Ano lze to řešit přes nový whitelist…)

před 5 lety

Tharos
Člen | 1036
+
0
-

@Casper: Díky za podněty.

  • V úplně poslední fázi před vydáním 1.5 se chystám projít všechny chybové hlášky a učesat je. Začlenění get_called_class tedy určitě zvážím. Také bych chtěl zavést chybové kódy, které budou někde zdokumentované (pro maximální pohodlí při odchytávání chyb).
  • S tou getData je to taky docela dobrý nápad. Vím, že whitelist je v takových případech neohrabaný. Fajn – naimplementuji to. Po tom nedávném refaktoringu to navíc bude dost jednoduché. A jelikož teď máme příznak m:useMethods, nemělo by to být ani nijak limitující.

Pokud by někdo s bodem dva nesouhlasil, nechť se ozve :). Jinak to za pár dní provedu.

Editoval Tharos (2. 8. 2013 15:23)

před 5 lety

peter.z
Člen | 37
+
0
-

Chcel by som sa spytat, preco sa pri persist nepouzivaju nazvy stlpcov, ak su uvedene v property.

Priklad:

/**
 * @property int $itemsPerPage(items_per_page)
 */
class Blog extends Entity
{

}

Ak nastavim $blog->itemsPerPage = 10 a nasledne entitu persistujem do repozitara, tak sa namiesto stlpca items_per_page pouzije itemsPerPage.

Je to mozne nejakym sposobom fixnut alebo je to tak zamerne?

před 5 lety

Tharos
Člen | 1036
+
0
-

Pozor na drobný překlep, ta anotace by měla být:

@property int $itemsPerPage (items_per_page)

Důležitá je ta mezera mezi názvem property a tím mapováním.

Nedávno jsem parser dost zbenevolentněl, ale tahle mezera asi i nadále zůstane povinná, protože bez ní (mám dojem) i IDE špatně napovídá (rozuměj nenapovídá).

Editoval Tharos (2. 8. 2013 16:16)

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Musím pochválit filtry, je to jednoduché a velmi dobře využitelné :) Mám k tomu jeden návrh na změnu. Nejdříve use case:

Z databáze se žádné záznamy nemažou, jen se nastavuje příznak is_active. Ve výpisu chci defaultně vždy pouze aktivní záznamy a v současném stavu musím ve všech repository mít kód:

<?php
CommonFilter::active($rows);
?>

A v entitách musím mít:

<?php
/**
 * @property MyEntity[] $myEntity m:belongsToMany(my_entity_id:my_entity) m:filter(CommonFilter::active)
 */
?>

Navrhuji následující: U každé entity by se nadefinoval filtr, který by se vždy použil:

<?php
/**
 * @filter CommonFilter:active
 * ...
 */
?>

Je zde ale obtíž s repository, protože do metody createEntity|createEntities se předává DibiRow a aby to fungovalo, tak by bylo potřeba, aby se předávalo DibiFluent. V tom případě by šlo, aby createEntity volalo filter a pak $row->fetch(), createEntities volalo filter a pak $rows->fetchAll(). Je to BC break, napadá někoho nějaké jiné řešení? Stojí mnou navržené řešení za úvahu?

Otázka: jak mohu filtru přidat parametr? Něco jako toto:

<?php
/**
 * @property MyEntity[] $myEntity m:belongsToMany(my_entity_id:my_entity) m:filter(CommonFilter::active(false)|AnotherFilter::sort)
 */
?>

před 5 lety

Tharos
Člen | 1036
+
0
-

@daniel.mejta: Předně díky za inspiraci a za užitečný use case.

Právě dokončuji „vylepšené filtry“. Včera jsem pro zájemce pushnul na GitHub feature větvi, ve které je vyvíjím. Zatím je to bez dokumentace, ale jakmile to bude dokončené, sepíšu sem, v čem se nové filtry liší a co nového umožňují. Víceméně jedu podle RFC, které jsem sem nedávno dával, ale postupem času mě napadlo ještě pár hodně užitečných zlepšení.

Nové filtry budou právě řešit minimálně část Tvých problémů :). Mám na nich ještě tak odhadem tři čtyři hoďky práce, takže jejich dokončení už je za dveřmi.

Ideálně bych diskuzi rozvedl až po jejich vydání, protože teď bych musel psát, co přesně ty nové filtry vyřeší, a pak bych o tom psal ještě jednou po jejich vydání atp. Prosím tedy ještě o den či dva strpení – jsem přesvědčen, že se vyplatí. Nové filtry mají nejen mnohem více možností, ale jsou i mnohem elegantnější (ruší například povinně statická volání).

Editoval Tharos (5. 8. 2013 15:41)

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Tharos napsal(a):

@daniel.mejta: Předně díky za inspiraci a za užitečný use case.

Právě dokončuji …

Skvělé, rád počkám (stejně jdu místo večerního programování do hospody) a jsem zvědavý na API :)

před 5 lety

Tharos
Člen | 1036
+
0
-

Ahoj,

právě jsem mergnul do develop větve dlouho slibované vylepšené filtry. Trochu mi to trvalo, ale jsem přesvědčen, že výsledek čekání bohatě vynahradí. Protože jestli doteď byly filtry užitečné a dalo se s nimi dělat leccos, tak teď to teprve začíná být ta pravá zábava.

Postupoval jsem víceméně podle tohoto RFC, ale pár věcí je teď vyřešeno, troufám si tvrdit, ještě hezčeji.

Jaký je hlavní rozdíl

Filtry už nejsou staticky volané cosi, ale pojmenované callbacky a jako filtr lze zaregistrovat cokoliv, co projde podmínkou is_callable. Může jít tedy o volání metody nějaké služby, instanci Closure, ale klidně i o statické volání tak, jako doposud.

Důležité je, že každý filtr má nějaký název (pro názvy filtrů platí stejná pravidla, jako pro názvy funkcí v PHP), pomocí kterého se na něj odkazuje. Základní ukázka budiž následující:

class Limiter
{

    const DEFAULT_LIMIT = 100;

    public function limit(LeanMapper\Fluent $statement, $limit = null)
    {
        if ($limit === null) {
            $limit = self::DEFAULT_LIMIT;
        }
        $statement->limit($limit);
    }

}
$limiter = new Limiter;

$connection->registerFilter('limit', array($limiter, 'limit'));
/**
 * @property int $id
 * @property Book[] $books m:belongsToMany m:filter(limit)
 */
class Author extends LeanMapper\Entity
{
}
$author = $authorRepository->find($id);
$author->getBooks(10); // získá deset prvních knih

Samozřejmě při volání LeanMapper\Connection::registerFilter bychom mohli předat $callback jako anonymní funkci, string reprezentující statické volání atp.


Představme si nyní, že nějaký filtr zaregistrujeme následovně:

$connection->registerFilter('foo', 'fooFilter', 'ep');

Že se jako filtr bude volat globální funkce fooFilter je asi jasné, ale co znamená ten třetí argument? Znamená, že filtru se kromě Fluent statementu a dynamicky předaných parametrů má předat i entita (e), která má filtr nadefinovaný, a reflexe property (p), ke které se přistupuje. Jedná se o jakýsi jednoduchý „autowiring“. Lze použít následující řetězce:

'ep' – Filtr obdrží: $statement, $entity, $property, $arg1, $arg2, ...
'pe' – Filtr obdrží: $statement, $property, $entity, $arg1, $arg2, ...
'e' – Filtr obdrží: $statement, $entity, $arg1, $arg2, ...
'p' – Filtr obdrží: $statement, $property, $arg1, $arg2, ...
'' – Filtr obdrží: $statement, $arg1, $arg2, ...

Tohle má široké využití. Například lze z reflexe property číst vlastní příznaky (pomocí LeanMapper\Reflection\Property::getCustomFlagValue). Anebo je možné ve službě reprezentující filtr mít injectovaný mapper obohacený o vlastní metodu getTranslationTable() a podle kombinace entity/property zjistit, z jaké tabulky se má přijoinovat překlad. Víceméně optimální řešení překladů, ke kterému určitě připravím nějaký příklad. :)

A to je všechno?

Ne. V anotaci lze u každého filtru uvést argument, který se mu rovněž předá. Příklad bude opět nejvýřečnější:

$connection->registerFilter('foo', 'fooFilter', 'ep');
@property Book[] $books m:hasMany m:filter(foo#10)

V takovém případě při přístupu k položce books filtr obdrží parametry:

$statement, $entity, $property, '10', $arg1, $arg2, ...

Pořadí parametrů je: $statement, případná entita a property předané pomocí „autowiringu“, parametr z anotace a na závěr se připojí parametry z dynamického volání.

Tohoto parametru přímo v anotaci se dá velmi výhodně využít v situacích, kdy je hodnota nějakého parametru konstatní při přístupu přes určitou property a nechceme ji pořád dokola při každém přístupu k property ručně předávat.


I nadále je možné do příznaku m:filter zahrnout hned několik parametrů zároveň a u vazby typu M:N lze aplikovat nezávislé filtry při čtení ze spojovací i cílové tabulky. Tohle je patrné z existujícího testu: zaměřte se na to, jaké parametry filtry obdrží.


Pokud potřebujete nějaké položky definovat nízkoúrovňově a využíváte metod LeanMapper\Row::referencing a LeanMapper\Row::referenced, možná vás bude zajímat nová třída LeanMapper\Filtering, která zapouzdřuje volání určitých filtrů. Jako inspiraci/dokumentaci vám prozatím může sloužit to, jak s tou třídou pracuje Lean Mapper interně: zde a zde.

BC breaky

Jak už bylo naznačeno v RFC, Lean Mapper nyní nevyužívá DibiConnection, ale LeanMapper\Connection, které z DibiConnection dědí a přidává jen podporu pro práci s filtry. Filtry prostě patří do DBAL vrstvy.

Tohle by ale měl být BC break úplně kosmetický – stačí v DI kontejneru nahradit DibiConnection za zmíněné LeanMapper\Connection.

Co na to repositáře

Užitečné na filtrech je, že se dají použít i v repositářích. Nyní ještě pohodlněji:

$this->connection->select('*')->from($this->getTable())->applyFilter('limit', $count)->fetchAll();

Ptáte se, jak je možné, že fluent disponuje metodou applyFilter()? Je to díky LeanMapper\Fluent, které dědí z DibiFluent, přidává právě tu metodu applyFilter() a LeanMapper\Connection vytváří namísto DibiFluent právě tento fluent.


Tak doufám, že jsem na nic nezapomněl. Jako vždy jsem připraven zodpovídat dotazy a promtně opravovat případné bugy. :)

Editoval Tharos (7. 8. 2013 0:40)

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Tharos napsal(a):

Ahoj,

právě jsem mergnul do develop větve dlouho slibované vylepšené filtry…

Nový koncept filtrů je skvělý, žádné statické filtry, funkční, super :)

Mám k tomu dvě věci:

Počet dotazů
Mám v neonu zaregistrovaný filter:

common:
    services:
            mapper: App\Model\Mapper\AppMapper
            commonFilter: App\Model\Filter\CommonFilter
            connection:
                class: LeanMapper\Connection(%database%)
                setup:
                    - registerFilter('active', [@commonFilter, 'active'])
            eventRepository: App\Model\Repository\EventRepository(@connection, @mapper)

Common filter je class:

namespace App\Model\Filter;

use LeanMapper\Fluent;

class CommonFilter
{
    public function active(Fluent $statement)
    {
        $statement->where("is_active = %b", true);
    }
}

Mám repozitář, kde se standardně volá:

namespace App\Model\Repository;

/**
 * @table event
 * @entity \App\Model\Entity\Event
 */
class EventRepository extends BaseRepository
{
    public function findAll()
    {
        $rows = $this->connection->select('*')
            ->from($this->getTable())
            ->orderBy('[date_from] DESC');

        $rows->applyFilter('active');

        return $this->createEntities($rows->fetchAll());
    }
}

Mám nadefinovanou Entity:

namespace App\Model\Entity;

use LeanMapper\Entity;

/**
 * @property int $id
 * @property string $title
 * @property string $description
 * @property \Datetime $dateFrom (date_from)
 * @property \Datetime|null $dateTo (date_to)
 * @property User $createdBy m:hasOne(created_by:user) m:filter(active)
 * @property \Datetime $createdOn (created_on)
 * @property bool $isActive (is_active)
 */
class Event extends Entity
{
}

A tuto entity:

namespace App\Model\Entity;

use LeanMapper\Entity;

/**
 * @property int $id
 * @property string $email
 * ...
 */
class User extends Entity
{
    ...
}

Používám MySQL a problém je následující
Pokud iteruji eventy s aplikací filtrů, tak se mi volá dotaz na usery (createdBy) tolikrát, kolik je záznamů eventů. Pokud filtr u entity Event vymažu, tak se mi dotaz na entitu User (createdBy) zavolá jen jednou. Asi chyba, co? :/

Viz obr. pokud mám v entitě Event u createdBy filtr:
http://imageshack.us/…/35/6dix.png
… a pokud filtr vymažu:
http://imageshack.us/…826/0b70.png

Default mapper nebere v potaz vlastní namespace
Mám namespace App/Model/Entity a DefaultMapper se stále snaží o instancování classy v namespace Model/Entity. Abych uvedl příklad, tak např. mám entitu App/Model/Entity/User a namísto toho se snaží mapper instancovat Model/Entity/User. Jedná se o navázanou entitu, viz příklad výše u entity Event a tam navázaný User (createdBy). Šlo by to udělat tak, aby se instancovaly defaultně classy z namespace entity, kterou mám nadefinovanou? Zatím jsem to vyřešil vlastním mapperem:

namespace App\Model\Mapper;

use LeanMapper\DefaultMapper;

class AppMapper extends DefaultMapper
{
    protected $defaultEntityNamespace = 'App\Model\Entity';
}

Editoval daniel.mejta (7. 8. 2013 2:44)

před 5 lety

Casper
Člen | 253
+
0
-

Celý koncept se mi poměrně líbí až na třetí argument u registerFilter. Rozhodně je špatné, pokud tanto argument přijímá jakési konstanty, které nejdou nikde definované jako konstanty. Nějaká písmenka si nikdo pamatovat nebude byť jsou sebelogičtější. Zavedl bych něco jako Filtering::APPEND_ARG_ENTITY a Filtering::APPEND_ARG_PROPERTY s tím, že pokud bych chtěl předat oba, šlo by použít tradiční sčítání (+) či bitové OR (|).

před 5 lety

Tharos
Člen | 1036
+
0
-

@daniel.mejta:

Počet dotazů: Díky za upozornění. Aktualizuj prosím na aktuální develop. :) Musel jsem dočasně revertnout jednu optimalizaci, která tohle bohužel zapříčinila (dočasně = než ji doladím do důsledků).

Namespace: No, ona k tomu přesně ta protected proměnná slouží. Tohle není chyba. Někdy už jsem tu o tom psal – spolu s lepší podporou single table inheritance mapper nabral na významu a tady je prostě stěžejní, co tvrdí mapper.

Ještě jinými slovy: anotace @entity u repositáře se hodí v případě, kdy se název repositáře vymyká nějaké konvenci (a je zapotřebí z něj odvodit konvenční název entitní třídy). Ne v případě, kdy se těm konvencím vymyká název entitní třídy. Je to takhle řečeno srozumitelné? :)

Každopádně best practice je tohle všechno řešit pomocí mapperu.

před 5 lety

Tharos
Člen | 1036
+
0
-

@Casper: Good point. Já sice ten řetězec důkladně validuji, ale řešení přes konstanty by asi bylo lepší.

Jak je na tom nyní neon s konstantami? Lze v něm zapsat Filtering::ARG_PROPERTY | Filtering::ARG_ENTITY? Přemýšlím, jestli bych nezachoval obě možnosti (řetězec by mohl být užitečný právě v neonu).

Editoval Tharos (7. 8. 2013 8:37)

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos: Neon už konstanty umí, nicméně jejich součet nebo logické OR se mi vyvolat nepodařilo.

Obě možnosti zápisu zůstanou přece vždycky zachované, pokud konstantu doplníš napřímo, nicméně aby ti fungovalo +, budeš nejspíš potřebovat číselné konstanty. Pro | by šlo využít, že "e" | "p" = "u" :)

Editoval Casper (7. 8. 2013 9:10)

před 5 lety

Michal III
Člen | 84
+
0
-

@Tharos, @Casper:

Konstanty se mi líbí, ale přišlo mi, že záleží na pořadí argumentů:

'ep'
'pe'

Což IMHO podle logického či aritmetického sčítání určit nelze. Nebo pořadí zas tak důležité není? Každopádně i kdyby se skončilo se 4-mi konstantami, tak pořád by to bylo dobré.

před 5 lety

Tharos
Člen | 1036
+
0
-

Určitě je k diskuzi, jestli by čtyři konstanty pro všechny existující kombinace nebyly nejpoužitelnější:

Connection::ARG_ENTITY
Connection::ARG_PROPERTY
Connection::ARG_ENTITY_PROPERTY
Connection::ARG_PROPERTY_ENTITY

Více kombinací nebude ani v budoucnu. Ono totiž všechno ostatní se dá do služby už standardně nainjectovat (mapper, jiné služby atp.).

Jak je nejlépe pojmenovat? :) Zatím mi všechny varianty připadají takové nic moc…

Connection::WIRE_ENTITY
Connection::WIRE_PROPERTY
Connection::WIRE_ENTITY_AND_PROPERTY
Connection::WIRE_PROPERTY_AND_ENTITY

Jazykově by bylo nejpřesnější asi tohle. „Wire“ od „autowire“ (ono to tu neprobíhá úplně auto).

Edit: Plus by ty konstanty určitě měly být v LeanMapper\Connection, ve kterém se filtry registrují. Při vytváření instance LeanMapper\Filtering se už tohle dále nespecifikuje.

Ale zase mám dilema, protože Filtering::WIRE_ENTITY je mnohem srozumitelnější než Connection::WIRE_ENTITY

Editoval Tharos (7. 8. 2013 10:18)

před 5 lety

Casper
Člen | 253
+
0
-

@Michal:

Konstanty se mi líbí, ale přišlo mi, že záleží na pořadí argumentů, což IMHO podle logického či aritmetického sčítání určit nelze.

Ha, tohle jsem úplně minul. S konstantami by muselo být předem dáno pořadí nebo tebou zmíněné 4 konstanty, což je asi lepší i vzhledem k neonu.

Edit: Vlastně moc nechápu, proč bych měl potřebu odlišit pořadí těchto dvou argumentů. Pokud potřebuji oba, pak vím, že mi přijdou oba a je mi jedno v jakém pořadí ne? Potom by stačily pouze 3 konstanty. Jinak co se týče názvu, osobně bych zůstal u toho argumentu, tedy něco jako PUSH_ARG nebo APPEND_ARG. A nebo úplně nejjednodušeji jak je zmíněno níže: předávat vždy všechno.

Editoval Casper (7. 8. 2013 9:49)

před 5 lety

Jan Tvrdík
Nette guru | 2542
+
0
-

@Tharos: Můžeš mi vysvětlit, jak tě napadla taková blbost, jako míst zvlášť konstanty Filtering::ARG_ENTITY_PROPERTY a Filtering::ARG_PROPERTY_ENTITY? Nebylo by vůbec jednodušší předávat vždycky všechno?

před 5 lety

Tharos
Člen | 1036
+
0
-

Předávat vždycky všechno je nepraktické, protože u naprosté většiny filtrů s instancí entity ani reflexí property nepotřebuješ pracovat.

Takový filtr pro limitování typicky může mít definici limit($statement, $count) a bylo by krajně nepraktické, aby $count byl až čtvrtý argument: limit($statement, $entity, $property, $count).

Máš ale pravdu, že ty konstanty by bohatě stačily tři. Nedokážu si představit situaci, kdy by na pořadí $entity a $property nějak zvlášť záleželo…

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Tharos napsal(a):

Předávat vždycky všechno je nepraktické, protože u naprosté většiny filtrů s instancí entity ani reflexí property nepotřebuješ pracovat.

Takový filtr pro limitování typicky může mít definici limit($statement, $count) a bylo by krajně nepraktické, aby $count byl až čtvrtý argument: limit($statement, $entity, $property, $count).

Máš ale pravdu, že ty konstanty by bohatě stačily tři. Nedokážu si představit situaci, kdy by na pořadí $entity a $property nějak zvlášť záleželo…

A co takhle reflexí zjišťovat, zda callback funkce očekává entity nebo property a předat to jen v případě potřeby bez e, p, nebo ep parametru…

před 5 lety

Tharos
Člen | 1036
+
0
-

@daniel.mejta: Řešení přes reflexi má dvě úskalí.

1) Vytváření reflexí je poměrně drahé a snažím se jim vyhýbat všude tam, kde nejsou nezbytně nutné. Ale řekněme, že zde by byla přidaná hodnota taková, že by se to vyplatilo. Takže jde hlavně o bod 2).

2) Mohly by vzniknout různé matoucí situace. Představ si, že potřebuješ následující:

$book->getAuthors($tag);

Dejme tomu, že nad property $authors existuje filtr, který jako druhý parametr chce entitu, ale jinou, než tu, k jejíž property se zrovna přistupuje. Autowiring by pak předal filtru parametry $statement, $entity, $tag, což by bylo matoucí.

před 5 lety

Michal III
Člen | 84
+
0
-

Já bych byl asi pro pouze 2 konstanty, které by se kombinovaly pomocí |, jak původně navrhl @Casper, přijde mi to jako dostačující a tradiční řešení nějakých flagů. Třetí konstanta by mi přišla spíš jako takový workaround pro neon, který by nebyl nutný, pokud by byly zachovány i řetězce ‚pe‘ (‚ep‘). A třeba by se neon mohl časem | naučit.

Konstanty bych možná dal spíš do Connection, jestli se budou filtry registrovat takhle přímo nad objektem Connection:

$connection->registerFilter('foo', 'fooFilter', .....);

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Tharos napsal(a):

Ad 1) Reflexe by se dělala jen při registraci filtru, to nějakou režii sebere, ale snad ne tolik…
Ad 2) Mě rozdíl ve zmatečnosti nepřijde nějak zásadní, oproti současnému stavu by pouze zmizel e, p, ep, pe parametr, jinak by to fungovalo stejně … nebo se mýlím?

před 5 lety

Tharos
Člen | 1036
+
0
-

@daniel.mejta: Problém je ten, že ty můžeš mít ve filtru třeba hned jako druhý argument nějaký s typehintem Entity, ale nemusíš ho chtít autowirovat. Autowiringu to ale nezakážeš.

Všimni si toho mého stručného příkladu. Já filtru předávám instanci Tag (řekněme, že ji prostě ten filtr z nějakého důvodu potřebuje), kterou bych chtěl mít jako druhý parametr v tom filtru. Ale do něj mi mezitím autowiring nacpe instanci entity, k jejíž položce přistupuji.

Řešením by mohlo být použití autowiringu jenom volitelně:

$connection->registerFilter('limit', array($limiter, 'limit'));
$connection->registerAutowiredFilter('translate', array($translator, 'translate'));

Samozřejmě se zde nabízí logický parametr, ale jsem zastánce názoru, že ty je třeba vymýtit. :–P


Tohle by taky bylo řešení. Teď jde o to, jestli je to lepší, než dvě konstanty a možnost jejich kombinace pomocí bitového operátoru…

před 5 lety

Michal III
Člen | 84
+
0
-

Tharos napsal(a):

Řešením by mohlo být použití autowiringu jenom volitelně:

$connection->registerFilter('limit', array($limiter, 'limit'));
$connection->registerAutowiredFilter('translate', array($translator, 'translate'));

Nepřemýšlel jsem nad konkrétní situací, ale nemohla by nastat možnost, že bychom chtěli použít autowire jen na jeden a ten druhý ne?

před 5 lety

Tharos
Člen | 1036
+
0
-

Naimplementoval jsem tedy řešení s konstantami, které mi přijde jako relativně nejlepší. Použití je vidět v testu.

Změna je plně zpětně kompatibilní, i nadále je možné použít low-level řetězec. Mám totiž dojem, že aktuální stable Nette ještě zpracovávat konstanty v neonu neumí a nerad bych jeho uživatele ochudil o možnost definování filtry v configu.

před 5 lety

Michal III
Člen | 84
+
0
-

Navrhuji, aby příznak m:hasOne (a případně další) měl kromě ‚sloupec odkazující na cílovou tabulku:cílová tabulka‘ někde ještě další parametr, který by definoval název třídy entity a ideálně také nějaký magický znak či něco, co by znamenalo, že se má použít jako název typ té property, tedy něco jako:

@property DocumentCategory $category m:hasOne(category_id:do_categories:DocumentCategory)

a s magickým řetězcem, aby se název třídy nemusel uvádět explicitně, pokud by zrovna ten typ nebyl pouze abstraktním předkem:

@property DocumentCategory $category m:hasOne(category_id:do_categories:&)

či nějaký obdobný řetězec místo &. Protože jinak by mně osobně mapper hrozně nakynul, jelikož zrovna pracuji s db, kde se takhle vymyká konvencím vše (z důvodu použití prefixů, které oddělují jednotlivé skupiny tabulek – je jich hodně). Je to dobrý nápad, nebo to již nyní lze řešit jiným způsobem? Děkuji za reakci.

před 5 lety

Tharos
Člen | 1036
+
0
-

Podobné berličky bych vážně nerad přidával… Přehlednost Lean Mapperu by tím IMHO hodně utrpěla.

Mapper nemusí být jedna dlouhá třída, může být dekomponovaný do řady menších tříd, které drží pohromadě nějaká implementace IMapper. Myslím, že zrovna prefixy by měly jít řešit v mapperu poměrně obstojně, nebo ne?

To, že potřebuješ u nějaké položky vrátit instanci entity, která je úplně mimo dědičnou linii, bych asi řešil pomocí m:useMethods. Ve vlastních metodách pak můžeš vytvořit a vrátit instanci čehokoliv.

Editoval Tharos (8. 8. 2013 10:21)

před 5 lety

Michal III
Člen | 84
+
0
-

@Tharos:

Bohužel v současném návrhu by bylo poprání se s prefixy v mapperu dosti složité, nicméně to s tím m:useMethods beru, to bohatě stačí. Jen že jsem to ještě nepoužil, tak mě to ani nenapadlo.

Jenom ještě taková otázka, nevím, zda-li se to už řešilo, ale je toho zde hodně. Pokud použiju pouze getter nebo pouze setter, je lepší použít m:useMethods či nikoli?

před 5 lety

Tharos
Člen | 1036
+
0
-

Doporučené je příznak m:useMethods použít vždy, protože pak ti bude napovídat IDE (nejenom ty metody, ale i položku). A také jsou pak všechny položky entity jasně patrné z anotací. Nemusíš lovit v těle entity, jaké jsou tam ještě gettery a settery a které z nich by mohly reprezentovat položky…

před 5 lety

Tharos
Člen | 1036
+
0
-

Ahoj,

zatím pouze ve feature větvi jsem jemně pročistil a optimalizoval pár částí kódu knihovny a zároveň jsem provedl jeden takový BC break, který mi po důkladném zvážení přijde rozumný.

Odstranil jsem protected metody Repository::getEntityClass a obdobnou Entity::getEntityClass. Důvod je prostý – tyto metody byly víceméně pohrobky z doby, kdy ještě neexistoval mapper a Lean Mapper měl v sobě veškeré konvence zadrátované.

Důvod, proč jsem je zrušil, je, že řeší něco, co lze vyřešit jinak a lépe (pomocí mapperu), a také že to ty metody řeší vlastně nedostatečně. Všimněte si, že například na tomto řádku je už dlouhou dobu chyba. Aby ten řádek spolehlivě fungoval, měla by se v něm volat metoda $this->getTable(). A přítomnost těchto historických metod by mně osobně v chystané verzi 1.5 už přišla nevhodná.

Ponechal jsem jenom podporu anotaci @table u Repository, a to proto, protože pokud se odchýlí název repositáře od konvence, stačí to vyjádřit pouze na tomto jednom místě. U entit je díky vazbám mezi nimi prostě situace složitější a jediné spolehlivé všepojímající (doufám) řešení je vlastní mapper.

Editoval Tharos (8. 8. 2013 20:53)

před 5 lety

Tharos
Člen | 1036
+
0
-

Ahoj,

Lean Mapper má ode dnešního dne jednu ze slibovaných důležitých funkcionalit: podporu pro defaultní hodnoty (u položek typu int, bool, float, string a array).

API odpovídá bodu čtyři v tomto RFC.

Pevně věřím, že budete mile překvapeni, čemu všemu realizace rozumí a jak je robustní. Zde je pár ukázek:

zápis hodnota
@property bool $active = true true
@property bool $active = TrUe true
@property boolean $active =FALSE false
@property bool $active = foobar *vyhodí výjimku*
@property int $count = 10 10
@property int $count = 0x1F 31
@property integer $count = 0123 83
@property int $count = "12" 12
@property int $count = true *vyhodí výjimku*
@property int $count =-12 -12
@property float $count = 10.5 10.5
@property float $count = -2.2e-3 -0.0022
@property float $count = "-2.4e-3" -0.0024
@property string $title = test test
@property string $title = foo bar foo
@property string $title = "foo bar" foo bar
@property string $title = McDonald's McDonald's
@property string $title = 'McDonald\'s restaurant' McDonald's restaurant
@property string $title = "foo\"b'ar" foo"b'ar
@property array $list = array() array()
@property array $list = ARRAY() array()
@property array $list = ARRAY *vyhodí výjimku*

Parser rozumí číslům zapsaným v různých notacích, nerozhodí ho různé velikosti písmen, mezery navíc, umožňuje escapování uvozovek v textu… Zkrátka chová se téměř identicky, jako interpret PHP.


Inicializace defaultních hodnot se volá bezprostředně před protected metodou Entity::initDefaults, takže v ní lze defaultní hodnoty eventuálně doladit či zinicializovat ty komplexnější.

Připomínám, že defaultní hodnoty se berou potaz pouze u zbrusu nově vytvořených entit. Nikoliv v případě, kdy se konstruktoru entity předá LeanMapper\Row. Pokud by se defaultní hodnoty použily i v případě předání LeanMapper\Row, mohlo by dojít k tomu, že by se při volání:

/**
 * @property int $id
 * @property string $name
 * @property bool $active = true
 */
class Author extends Entity
{
}
$author = $authorRepository->find(1);
$authorRepository->persist($author);

změnily data v databázi, což je IMHO nepřijatelné (bylo by to totálně matoucí).

V následujícím případě se pak defaultní hodnoty uplatní:

$author = new Author(array(
    'name' => 'John Doe'
));
$authorRepository->persist($author); // do databáze se uloží mimo jiné active = 1

Tak ať to slouží. :)

Editoval Tharos (9. 8. 2013 23:52)

před 5 lety

daniel.mejta
Člen | 21
+
0
-

Tharos napsal(a):

Ahoj,

Lean Mapper má ode dnešního dne jednu ze slibovaných důležitých funkcionalit: podporu pro defaultní hodnoty (u položek typu int, bool, float, string a array).

Výborná práce, zase posun o kus dál :)
Mám dotaz:
Bude fungovat i zápis

@property array $whatever = []

?

Je plánována defaultní hodnota jakožto objekt? Např:

@property string $name
@property SomeObject $someObject = new SomeObject($name, 10)

?

Příklad č. 1 by měl být podle mě ok, příklad č. 2 může být komplikovaný :)

Díky!

před 5 lety

Casper
Člen | 253
+
0
-

@daniel.mejta:
Řekl bych, že na vytváření objektů je tu metoda initDefaults :) Uvažovatelná by asi byla podpora pro NOW(), tedy new \DateTime nebo něco podobného, nicméně ne každý využívá DateTime.

@Tharos:
Super update, a co třeba @property float $count = 10,5? Tam bych očekával asi nejvíce překlepů :) Jinak filtry jsem konečně pořádně otestoval a je to s nimi paráda.

P.S. a jak to vypadá s implementací kontroly validity entit zmíněné zde?

Editoval Casper (10. 8. 2013 9:33)

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:

Zkoušel jsem na poslední develop verzi použít vylepšenou metodu __set v BaseEntity dle tvého staršího příspěvku, nicméně místo sloupce "column_id" mi vrací vnitřní data ve tvaru "column{hasOne:FozcflAR7K}". Předpokládám, že se od doby toho příspěvku něco změnilo, jak mohu upravit zmíněnou metodu, aby fungovala tak jak jsi referoval v uvedeném příspěvku?

před 5 lety

davidm
Člen | 82
+
0
-

Ahoj,

celý LeanMapper je parádní počin a moc za něj děkuju!

Teď něco k zamyšlení. Možná je to jen subjektivní pocit, ale zdá se mi, že se poslední dobou jednoduché věci začínají řešit trochu wtf způsobama (defaultní hodnoty v anotacích, když stačí metoda initDefaults; třetí argument u filtrů, když mohu jednoduše předávat entity i property … to že musím v deklaraci funkce napsat o jednu proměnnou navíc není přece problém). Např. v případě těch anotací se přidává funkcionalita, která je podle mého názoru naprosto zbytečná (což odporuje slovu Lean v názvu ORM). Trochu se obávám, aby se z LeanMapperu ve výsledku nestalo něco jako „FatMapper“. Líbilo by se mi kdyby si LeanMapper udržel svoji jednoduchost.

Ještě jednou dík za skvělé ORM! Těším se až ho budu moct vyzkoušet na nějakém větším projektu!

před 5 lety

Tharos
Člen | 1036
+
0
-

@daniel.mejta:

Bude fungovat i zápis
@property array $whatever = []

Dobrý postřeh, podporu pro [] snadno doplním (trivialita).

Je plánována defaultní hodnota jakožto objekt? Např:

V plánu to není, tohle je zapotřebí řešit pomocí zmíněné metody initDefaults().

Editoval Tharos (10. 8. 2013 22:48)

před 5 lety

Tharos
Člen | 1036
+
0
-

@Casper:

Super update, a co třeba @property float $count = 10,5? Tam bych očekával asi nejvíce překlepů :)

Vtip je v tom, že zápis $count = 10,5 není validní ani v samotném PHP. Líbí se mi, že teď pro defaultní hodnoty platí víceméně stejná pravidla jako pro hodnoty v PHP. Vím, že se v tomhle udělá překlep snadno a výhoda takového překlepu mimo anotaci je, že na ni upozorní interpret, ale prostě se mi nechce takhle aktivně řešit za programátora jeho překlepy. Prostě desetinná část se odděluje tečkou – jasné pravidlo.

P.S. a jak to vypadá s implementací kontroly validity entit zmíněné zde?

Ještě to nechávám uzrát. Mně na tom vadí to, že bych tak trochu sám sobě podřízl větev. :–P Já totiž často využívám defaultních hodnot v databázi (typicky u boolean soupců) a nechce se mi je v mém případě stěhovat do entit…

V případě MySQL mi prostě mnohem lepší přijde zapnout striktní mód, což lze i za běhu: SET sql_mode = STRICT_ALL_TABLES.

Editoval Tharos (10. 8. 2013 22:28)

před 5 lety

Tharos
Člen | 1036
+
0
-

@Casper:

Zkoušel jsem na poslední develop verzi použít vylepšenou metodu __set v BaseEntity dle tvého staršího příspěvku, nicméně místo sloupce "column_id" mi vrací vnitřní data ve tvaru "column{hasOne:FozcflAR7K}". Předpokládám, že se od doby toho příspěvku něco změnilo, jak mohu upravit zmíněnou metodu, aby fungovala tak jak jsi referoval v uvedeném příspěvku?

Tohle je pořád funkční, ale rád vysvětlím, na jaký jev jsi právě narazil.

Entita vytvořená úplně „ze vzduchoprázdna“ (new Book) v sobě nemá mapper, a proto pořádně neví, co kam patří. Ona sice ze svých anotací ví, že má řekněme 1:N vazbu na entitu Author, ale už neví, v jakém vazebním sloupci má mít uložen cizí klíč. Proto si entita až do té doby, než se dostane k mapperu, udržuje cizí klíče v jakýchsi virtuálních low-level položkách (to je například to column{hasOne:FozcflAR7K}).

Řešením je entitě před tím, než budeš ta data číst, předat mapper. Ještě lépe to vysvětlím na ukázkách:

/**
 * @property int $id
 * @property string $name
 */
class Author extends Entity
{
}

/**
 * @property int $id
 * @property string $name
 * @property Author $author m:hasOne
 */
class Book extends Entity
{
}
$book = new Book;
$book->author = $author; // $author->id === 1

print_r($book->getModifiedRowData()); // Array ( [author{hasOne:OYEvwRJ1xV}] => 1)
$book = new Book;
$book->author = $author; // $author->id === 1
$book->useMapper($mapper);

print_r($book->getModifiedRowData()); // Array ( [author_id] => 1)
$book = new Book;
$book->useMapper($mapper);
$book->author = $author; // $author->id === 1

print_r($book->getModifiedRowData()); // Array ( [author_id] => 1)

Je z toho jasné, kam mířím? V době, kdy byl mapper zadrátován v jádru v podobě konvencí, Lean Mapper i bez mapperu věděl, kam má ten cizí klíč uložit – do author_id, pokud není v anotaci u vazby řečeno jinak. Nyní to tak není, mapper může ten cizí klíč nasměrovat třeba do sloupce authorid


Všimni si, že v repositáři se při persistování metoda Entity::useMapper volá před Entity::getModifiedRowData. Takže ten můj kód lze de facto použít beze změny i v develop verzi. Nenech se zmást, že ještě před persistencí ta nově vytvořená entita vrací nízkoúrovňová data uložená v jakýchsi virtuálních sloupcích – jakmile se v repositáři ta entita dostane k mapperu, vše si korektně přemapuje a do databáze už zapisuje podle pravidel mapperu.

Doufám, že jsem to vysvětlil srozumitelně. :) Pokud ne, neváhej se ještě zeptat.

Editoval Tharos (10. 8. 2013 22:51)

před 5 lety

Tharos
Člen | 1036
+
0
-

@davidm:

Teď něco k zamyšlení. Možná je to jen subjektivní pocit, ale zdá se mi, že se poslední dobou jednoduché věci začínají řešit trochu wtf způsobama (defaultní hodnoty v anotacích, když stačí metoda initDefaults; třetí argument u filtrů, když mohu jednoduše předávat entity i property … to že musím v deklaraci funkce napsat o jednu proměnnou navíc není přece problém). Např. v případě těch anotací se přidává funkcionalita, která je podle mého názoru naprosto zbytečná (což odporuje slovu Lean v názvu ORM). Trochu se obávám, aby se z LeanMapperu ve výsledku nestalo něco jako „FatMapper“. Líbilo by se mi kdyby si LeanMapper udržel svoji jednoduchost.

Díky za dobrý podnět k zamyšlení. Představa Fat Mapperu mě osobně přímo děsí :), a tak snad Tě potěší, že vizi máme určitě shodnou…

Od doby, kdy jsem Lean Mapper vypustil ven, v něm přibylo opravdu hodně funkcí, ale všechny byly podpořeny smysluplnými případy užití. Z mého pohledu šlo o věci, bez kterých by ta knihovna nebyla dospělá. V této věci je znát, že jsem knihovnu vypustil ven v dost rané fázi vývoje – bez mapperu, s filtry založenými na statických voláních, nešlo persistovat jednoduché M:N vazby…

Do verze 1.5 mi schází už jen naimplementovat události, které ale budou opravdu hodně lean (viz RFC v někde tomto vlákně). Ty se ještě vážně hodí pro různé vedlejší efekty persistence.

No a to je vše. Pak už jenom provedu pár kosmetických zásahů (chci projít a učesat všechny chybové hlášky, doplnit dokumentační komentáře…) a vypustím RC stable verze 1.5. Pak půjde z mého pohledu o vyzrálé ORM, ale stále ještě bude lean. Následně se plánuji zaměřit primárně na dokumentaci a podporu. Samotnou knihovnu budu od verze 1.5 rozšiřovat velmi konzervativně.

Všimni si, že z Lean Mapper ani nyní jen nebobtná, ale zároveň z něj i odstraňuji balast: před pár dny jsem odstranil pár protected metod, které už nebyly opodstatněné, nedávno jsem zrušil pár již překonaných příznaků v anotacích… plus jsem provedl množství jiných mikro-očištění, o kterých jsem na fórum ani nepsal.

Editoval Tharos (11. 8. 2013 0:00)

Stránky: Prev 1 … 7 8 9 10 11 … 23 Next RSS tématu