Oznámení
před 6 lety
- Casper
- Člen | 253
@castamir:
- Zrovna tohle tu teď řeším, jestli jsi mé příspěvky četl. Pokud bych se dal na zmíněnou první metodu, tenhle problém kompletně odpadá. Nicméně dá se říci, že ano, vytváří se hůře.
- Tohle je obecně problém ORM typu Lean Mapper. Tak je prostě nastavená jeho architektura, že neperzistovaná entita se nemůže dožadovat vztahových entit. Stejně tak to teď bude s těmi závislostmi, pokud nebudeš vše vytvářet přes faktory.
- Tohle není ale problém vývojáře. To, že někdo nechápe principy architektury kterou potažmo využívá je jeho problém. Rozhodně je podle mě blbost si říct: „tuhle skvělou feature raději nebudu vyvíjet, protože neznalí programátoři s tím napáchají akorát škody“. To bych podle tvé logiky mohl říct, že DI je taky špatná. Vždyť přeci neznalý programátor pak může kombinovat DI se statickým voláním a to už je teprve peklo, kdyby to po něm někdo přebral a domníval se, že předáním závislostí je vše vyřešené.
Editoval Casper (13. 11. 2013 14:46)
před 6 lety
- Michal III
- Člen | 84
Ještě bych se chtěl zeptat, jak nejlépe přistoupit k vysokoúrovňovým datům tak, aby se vazební property nenahrazovaly za instance Tříd, ale zůstaly jen jejich syrové hodnoty. Typicky při naplňování formuláře pro úpravu.
Tedy například entita Book by měla property author odkazující na Author a já bych chtěl naplnit formulář pro úpravu knihy tak, aby na pozici author nebyla instance Author, nýbrž jen jeho id, které pak můžu dát jako defaultní hodnotu selectu?
Nebo takovéhle věci řešíte úplně nějak jinak?
před 6 lety
- Casper
- Člen | 253
@Michal III:
Tharos má ve svém skeletonu na githubu metodu pro naplnění formuláře v BaseEntity. Osobně se mi více líbí si napsat podobnou, ale obecnější metodu getPreviewData, která jen potřebná data získá. Tu pak můžu využít nejen pro fomuláře.
před 6 lety
- Michal III
- Člen | 84
@Casper: Děkuji.
před 6 lety
- Tharos
- Člen | 1042
@Casper, @Michal III: Mně se ta má metoda v
BaseEntity
vůbec nelíbí, sám bych ji určitě nikde
neventiloval. :)
Nejde ani tak o její vnitřnosti, ty nejsou špatné, ale hlavně o její umístění – entita by neměla o žádných formulářích nic vědět. To mé řešení ve skeletonu je prostě dirty ad-hoc solution.
V jednom projektu mám rozpracované řešení, které mi přijde mnohem
lepší – potřebnou logiku zapouzdřuje formulář a já pak jenom volám
něco jako $form->setDefaults($entity)
. Akorát to taky ještě
není v takovém stavu, abych to mohl s čistým svědomím pustit
ven… :(
Editoval Tharos (14. 11. 2013 2:00)
před 6 lety
- Jan Suchánek
- Backer | 403
@Tharos: Ahoj, mimo Translating a skeletonu máš ještě najaké další příklady řešení ze kterých se dá učit, nebo spíš alespoň nad nimi bádat? Translating je moc pěkný.
před 6 lety
- Tharos
- Člen | 1042
@castamir:
krom toho, že to narušuje single responsibility principle
Vezmeme-li v úvahu tuto definici SRP:
A class or module should have one, and only one, reason to change.
pak neplatí, že existence metody Entity\Order::notifyAdmin
je
automaticky porušením SRP. Záleží na tom, jestli ta entita tu práci
sama dělá, anebo ji jen deleguje. Moc doporučuji tento článek od René Steina, se kterým osobně naprosto
souhlasím.
1. mnohem hůř vytváří vytváří instance (rozumněj, stále bude možné vytvořit instanci pouze přes new a kdo to pak ošetří, když ta instance nebude mít připojené patřičné služby?)
To je prostě cena dependency injection…
to, že si pár lidí nejspíš bude schopno pohlídat, co za závislosti s entitou propojí, aby z toho nevznikla totální prasečina je jedna věc, druhá ale je, že spoustu dalších vyloženě naočujete tímto použitím a oni si to pak budou uplatňovat za všech okolností a ten guláš jim v tom vznikne…
Tohle je přesně důvod, proč mi anémický model u data-centric aplikací nepřijde jako vůbec špatný. Je prostě blbuvzdorný, neklade vysoké nároky na programátora. Jasně separuje data a práci s nimi, takže programátor hned ví „co kam šoupnout“. Nevyužívá se naplno možností OOP, ale, mezi námi… u většiny webových aplikací je to víceméně fuk.
Rich-domain model je rozhodně těžší navrhnout a pokud to programátor udělá špatně, výsledek bude pravděpodobně hůře použitelný a udržovatelný než anémický model řešící stejné zadání.
před 6 lety
- Tharos
- Člen | 1042
@jenicek: Bohužel nemám… Zatím v nějaké hezké ucelené podobě neexistuje nic jiného, než části kódu, které se tady povalují na fóru…
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
Lean Mapper má od včerejšího dne jednu zajímavou novou funkcionalitu, které říkám implicitní filtry.
Vezměme si například toto
zadání. Lze to velmi přímočaře vyřešit pomocí filtru, který do SQL
dotazu doplní restrikci deleted = 0
. Problémem ale je, že
takový filtr musíme explicitně použít všude tam, kde se daná entita
načítá – v repositáři a ve všech entitách, které se na tu zmíněnou
odkazují (mají na ní vazbu). Jinak bychom se někde mohli například
dotraverzovat i k odstraněným entitám. Obrovskou nevýhodou je, že
zapomenout někde ten filtr uvést je velmi snadné.
No a tohle řeší ony implicitní filtry. Stačí v mapperu říct, že
při načítání té či oné entity se vždy mají použít vybrané
filtry. Slouží k tomu nová metoda IMapper::getImplicitFilters
,
která vrací buďto pole s názvy těch filtrů, anebo instanci
ImplicitFilters
která kromě těch názvů obsahuje i parametry,
které se jim mají předat. Fungují podobně jako pátý parametr konstruktoru
třídy Filtering
.
Ukažme si, jak lze pomocí tohoto konceptu vyřešit výše odkázané zadání:
/**
* @property int $id
*
* @property Category $category m:hasOne
*
* @property string $title
* @property DateTime $published
* @property string $text
*/
class Article extends \LeanMapper\Entity
{
}
/**
* @property int $id
*
* @property Article[] $articles m:belongsToMany
*
* @property string $name
*/
class Category extends \LeanMapper\Entity
{
}
class Repository extends \LeanMapper\Repository
{
public function findAll()
{
return $this->createEntities(
$this->createFluent()->fetchAll()
);
}
}
class ArticleRepository extends Repository
{
protected function deleteFromDatabase($arg)
{
$primaryKey = $this->mapper->getPrimaryKey($this->getTable());
$idField = $this->mapper->getEntityField($this->getTable(), $primaryKey);
$id = ($arg instanceof Entity) ? $arg->$idField : $arg;
$this->connection->query(
'UPDATE %n SET [deleted] = 1 WHERE %n = ?', $this->getTable(), $primaryKey, $id
);
}
}
class CategoryRepository extends Repository
{
}
class Mapper extends TestMapper
{
public function getImplicitFilters($entityClass, $caller = null)
{
if ($entityClass === 'Article') {
return new ImplicitFilters(array('living'), array(
'living' => array($this->getTable($entityClass))
));
}
return parent::getImplicitFilters($entityClass, $caller);
}
}
////////////////////
$mapper = new Mapper;
$connection->registerFilter('living', function ($statement, $table) {
$statement->where('%n.[deleted] = 0', $table);
});
To je v podstatě vše. Všimněte si následujících záležitostí v kódu:
1. Repository obsahuje novou protected metodu
createFluent
, jejíž implementace
je myslím snadno pochopitelná. Jednak zase o něco usnadňuje život
(nemusíte datlovat ani obligátní
$this->connection->select(...)->from($this->getTable())->fetchAll()
,
ale hlavně hlídá, aby byly ihned aplikovány všechny potřebné implicitní
filtry, takže na ně nelze zapomenout.
2. Při definici vazby z entity Category
na
entitu Article
není nutné uvádět žádné filtry, protože
díky implicitnímu filtru je už zaručené, že se vždy načtou pouze
nesmazané články. Takže jsme si zase ušetřili nějaké psaní a také jsme
eliminovali riziko, že nějaký filtr uvést zapomeneme.
3. Repositář ArticleRepository
velmi
jednoduše namísto trvalého odstraňování záznamů jen nastavuje
deleted = 1
.
Zavoláme-li pak v aplikaci $articleRepository->findAll()
,
položí se dotaz:
SELECT * FROM `article` WHERE `article`.`deleted` = 0
A provedeme-li pak v aplikaci následující aktivitu:
foreach ($categoryRepository->findAll() as $category) {
echo $category->name, "\n";
foreach ($category->articles as $article) {
echo " ", $article->title, "\n";
}
}
Položí se dotazy:
SELECT * FROM `category`
SELECT `article`.* FROM `article` WHERE `article`.`category_id` IN (1, 2) AND `article`.`deleted` = 0
Všimněte si, že se korektně načítají pouze neodstraněné články a nemusíme na to téměř nijak myslet.
Celý tento koncept má velmi široké uplatnění. Například ten systém překladů, který jsem zde na fórum už kdysi dával, by se mohl ještě zjednodušit: ty filtry m:filter(translate) by se vůbec nemusely uvádět, čímž by zároveň odpadlo riziko, že takový filtr někde uvést zapomenete. Asi ten příklad ještě zaktualizuji. :)
Enjoy!
Editoval Tharos (15. 11. 2013 15:00)
před 6 lety
- Pavel Macháň
- Člen | 285
Kde registrujete filtry pro leanmapper v nette?
@Tharos: implicitní filtry vypadají dobře :)
Editoval EIFEL (15. 11. 2013 13:44)
před 6 lety
- Michal III
- Člen | 84
@Tharos: Paráda! Děkuji.
Editoval Michal III (15. 11. 2013 12:45)
před 6 lety
- llook
- Člen | 412
Nad soft deletem jsem se už něco nanadával… Mám k této implementaci dva dotazy:
- Dají se ty implicitní filtry přebít explicitními? Pokud bych někde použil takovýto soft delete, je nějak možné přimět LeanMapper, aby vracel i ty smazané položky?
- Používají se implicitní filtry u vazby hasOne? Pak by bylo by možné smazat položku, na které jiná položka závisí a tím narušit integritu dat.
před 6 lety
- Tharos
- Člen | 1042
@llook: Samozřejmě, že lze takové filtry „přebíjet“. Ony se kombinují s těmi explicitně uvedenými. Platí, že se nejprve volají ty implicitní a pak ty explicitní, ale s tím, že pokud je uveden v explicitních filtrech (třeba v příznaku m:filter) nějaký filtr, který je obsažen i v implicitních, tak se v těch implicitních ignoruje a volá se až později (v rámci zpracování těch explicitních). Tak lze efektivně ovlivnit pořadí volání filtrů a také lze v tom příznaku explicitně přetížit „fixní parametry“, které se filtru předají.
Ono se to fakt kostrbatě popisuje, v dokumentaci tohle asi budu muset polopaticky ukázat na nějakých příkladech… Ale mělo by to být velmi pružné.
Pokud bys chtěl implicitně ten filtr living používat, ale zároveň bys chtěl mít možnost vypisovat i smazané záznamy, je hned několik možností, jak to vyřešit. Já bych asi nepohrdl následujícím řešení (i když by záleželo na kontextu):
Upravuji výše uvedený příklad.
const EXCLUDE_DELETED = 0; // tyhle mrchy bych určitě nenechal v globálním prostoru…
const INCLUDE_DELETED = 1;
$connection->registerFilter('living', function ($statement, $table, $mode = EXCLUDE_DELETED) {
if ($mode === EXCLUDE_DELETED) {
$statement->where('%n.[deleted] = 0', $table);
}
});
// kdesi v relevantním repository
public function findAllIncludingDeleted()
{
return $this->createEntities(
$this->createFluent(INCLUDE_DELETED)->fetchAll() // zde lze předat filtrům dynamický parametr tak, jako při volání Entity::getXyz($arg1, $arg2, ...)
);
}
foreach ($articleRepository->findAllIncludingDeleted() as $article) {
echo $article->title, "\n";
}
// SELECT `article`.* FROM `article`
foreach ($categoryRepository->findAll() as $category) {
echo $category->name, "\n";
foreach ($category->getArticles(INCLUDE_DELETED) as $article) {
echo " ", $article->title, "\n";
}
}
// SELECT `category`.* FROM `category`
// SELECT `article`.* FROM `article` WHERE `article`.`category_id` IN (...)
Samozřejmě by bylo možné i nadefinovat přímo getter
getArticlesIncludingDeleted
bez potřeby předávání parametru
atp. Ale jak říkám, ono by to šlo řešit hned více způsoby. Pokud by Ti
například createFluent(INCLUDE_DELETED)
přišlo příliš
magické, šlo by tu část nahradit ručním vytvořením dotazu.
Co se té hasOne vazby týče, při traverzování přes ní se implicitní
filtry samozřejmě aplikují. Jak přesně bys chtěl smazat položku, na
které je nějaká jiná závislá? Máš na mysli situaci, kdy bys měl třeba
načtený článek, který je navázaný na smazaného autora, a k tomu
autorovi traverzoval? Lean Mapper jej z databáze nenačte a vrátí pro tu
property hodnotu null (musí tedy být typu Author|null
, ale to
myslím dává smysl a je žádoucí).
Anebo jsi měl na mysli ještě něco jiného? :)
Editoval Tharos (15. 11. 2013 22:44)
před 6 lety
- Tharos
- Člen | 1042
Casper napsal(a):
Předpokládám tedy, že máš nějaké hezčí řešení :) Nebo máš prostě ten můj kód v initEvents?
Já ho mám opravdu v initEvents
. Popravdě jsem se s tím moc
nedělal, možnosti neonu jsem v téhle věci zatím nezkoumal…
před 6 lety
- Michal III
- Člen | 84
@Tharos: Myslím, že u toho traverzovaní přes
vazvu hasOne
je mnohdy právě žádoucí, aby se vrátil i soft
smazaný autor – tedy situace, kdy bychom „living“ autory používali
jako autory, se kterými můžeme dál pracovat (přidávat jim knihy), ale
zároveň bychom pro knihy se smazanými autory stále chtěli autora vypisovat.
(Tohle je zrovna trochu nešikovný příklad, ale představme si tedy něco
podobného s mobilními operátory a tarify. Tarify bychom časem chtěli
odstranit z nabídky, přičemž by ale ještě existovali lidé, kteří by ho
měli aktivovaný…
před 6 lety
- llook
- Člen | 412
Když mažeš položku, na které je jiná položka závislá, tak se musíš nejdřív té závislosti nějak zbavit.
Například smazání uživatele na fóru – měl bys smazat i všechny jeho příspěvky, nebo je převést pod nějaký anonymní účet. Když to zapomeneš ošetřit, tak ti databáze nedovolí toho uživatele smazat. Ale soft delete klidně provést můžeš a potom každá stránka, kde je nějaký příspěvek od smazaného uživatele, bude házet server error.
Soft delete nese podobná rizika, jako
SET foreign_key_checks = 0;
.
před 6 lety
- Tharos
- Člen | 1042
@Michal III, @llook: Jasně, už to chápu. :)
V podstatě jde o to, jak nasimulovat to, co za nás u standardního delete
běžně řeší cizí klíče: SET NULL
, RESTRICT
,
CASCADE
…
Troufám si tvrdit, že to lze v Lean Mapperu vcelku přímočaře vyřešit. To, jak lze na vyžádání načíst i záznamy označené jako smazané, jsem nastínil. Pokud jde o zachování konzistence při označování záznamů jako smazaných, šlo by postupovat následovně:
- Navěsil bych si handler na událost AFTER_DELETE (pro entitu, která se reálně nemaže).
- V tom handleru bych řešil ony závislé entity. Ty bych našel třeba pomocí RobotLoaderu – naindexoval bych si všechny entity a za použití EntityReflection bych vyfiltroval ty, které potenciálně mají na mazanou entitu vazbu.
- Načetl bych ty, které tu vazbu reálně mají (podle ID mazané entity), a podle potřeby bych se s tou vazbou vypořádal.
Vznikl by tak plně automatizovaný systém bez potřeby dodatečné konfigurace po přidání nových entit nebo vazeb mezi nimi.
před 6 lety
- Tharos
- Člen | 1042
Jak jsem naznačil, aktualizoval jsem ukázku s překlady tak, aby naplno využívala nejnovějších schopností Lean Mapperu, zejména implicitních filtrů.
Řekl bych, že se celá věc zase zjednodušila. Máme-li takovouto
implementaci getImplicitFilters
:
public function getImplicitFilters($entityClass, $caller = null)
{
if (is_subclass_of($entityClass, 'Model\Entity\TranslatableEntity')) {
if ($caller instanceof Entity) {
return array('translateFromEntity');
} else {
return new ImplicitFilters(array('translate'), array(
'translate' => array($this->getTable($entityClass)),
));
}
}
return parent::getImplicitFilters($entityClass, $caller);
}
Naše entity pak mohou vypadat následovně:
namespace Model\Entity;
use DateTime;
/**
* @property string $id
*/
class Lang extends \LeanMapper\Entity
{
}
/**
* @property Lang $lang m:hasOne m:translatable
*/
abstract class TranslatableEntity extends \LeanMapper\Entity
{
}
/**
* @property int $id
* @property Content[] $contents m:belongsToMany
*
* @property DateTime $created
* @property bool $active = true
* @property string|null $name m:translatable
* @property string|null $description m:translatable
*/
class Page extends TranslatableEntity
{
}
/**
* @property int $id
* @property Page $page m:hasOne
*
* @property string $code
* @property string|null $content m:translateble
*/
class Content extends TranslatableEntity
{
}
Všimněte si, že úplně zmizely příznaky m:filter
–
filtry pro přijoinování překladů už není vůbec zapotřebí explicitně
uvádět.
Použití v aplikaci zůstává nezměněné:
$csLang = $langRepository->find('cs');
$enLang = $langRepository->find('en');
foreach ($pageRepository->findAll($csLang) as $page) {
echo $page->name; // vypíše český název stránky
foreach ($page->contents as $content) {
echo $content->content; // vypíše český obsah stránky (jazyk převzat z entity $page)
}
}
foreach ($pageRepository->findAll($csLang) as $page) {
echo $page->name; // vypíše český název stránky
foreach ($page->getContents($enLang) as $content) {
echo $content->content; // vypíše anglický obsah stránky (jazyk explicitně uveden při volání getContents)
}
}
Editoval Tharos (18. 11. 2013 2:34)
před 6 lety
- Pavel Macháň
- Člen | 285
Tharos napsal(a):
Jak jsem naznačil, aktualizoval jsem ukázku s překlady tak, aby naplno využívala nejnovějších schopností Lean Mapperu, zejména implicitních filtrů.
\--
@Tharos Super :) zrovna nedávno sem se do těch
překladů pustil a todle ušetří další čas :)
btw kdy plánuješ vydat další Releases?
Editoval EIFEL (18. 11. 2013 15:48)
před 6 lety
- Michal III
- Člen | 84
@Tharos: Možná došlo k nedorozumění v mém
příspěvku, ale mně by opravdu šlo o to (a také by se mi to zrovna
hodilo), aby se implicitní filtry aplikovaly jen na repozitáře a jen určité
druhy vazeb při traverzování – tedy vyjma hasOne, u které bych chtěl,
aby filtry použity nebyly a nebylo mi navráceno NULL
, nýbrž
soft smazaná položka. Lze tohoto nějak docílit?
před 6 lety
- llook
- Člen | 412
Michal III napsal(a):
@Tharos: Možná došlo k nedorozumění v mém příspěvku, ale mně by opravdu šlo o to (a také by se mi to zrovna hodilo), aby se implicitní filtry aplikovaly jen na repozitáře a jen určité druhy vazeb při traverzování – tedy vyjma hasOne, u které bych chtěl, aby filtry použity nebyly a nebylo mi navráceno
NULL
, nýbrž soft smazaná položka. Lze tohoto nějak docílit?
Tohle chceš u filtrů, které nějak filtrují výsledek. Ale taky můžou být filtry, které třeba přidávají vypočítané hodnoty, nebo joinují další tabulku a takové filtry chceš aplikovat vždycky.
Vidím dvě možnosti – buďto ty filtrovací implicitní filtry budeš u každé hasOne vazby přebíjet explicitním filtrem, nebo naopak ty filtrovací filtry nebudou implicitní, ale explicitně uvedené u každé vazby, na kterou se mají aplikovat.
před 6 lety
- Michal III
- Člen | 84
llook napsal(a):
Michal III napsal(a):
@Tharos: Možná došlo k nedorozumění v mém příspěvku, ale mně by opravdu šlo o to (a také by se mi to zrovna hodilo), aby se implicitní filtry aplikovaly jen na repozitáře a jen určité druhy vazeb při traverzování – tedy vyjma hasOne, u které bych chtěl, aby filtry použity nebyly a nebylo mi navráceno
NULL
, nýbrž soft smazaná položka. Lze tohoto nějak docílit?Tohle chceš u filtrů, které nějak filtrují výsledek. Ale taky můžou být filtry, které třeba přidávají vypočítané hodnoty, nebo joinují další tabulku a takové filtry chceš aplikovat vždycky.
Vidím dvě možnosti – buďto ty filtrovací implicitní filtry budeš u každé hasOne vazby přebíjet explicitním filtrem, nebo naopak ty filtrovací filtry nebudou implicitní, ale explicitně uvedené u každé vazby, na kterou se mají aplikovat.
Asi ano. Ještě mě napadla možnost, že by funkce
getImplicitFilters
dostávala více informací, aby se to dalo
vyřešit přímo v mapperu, pokud by to tedy bylo možné a žádoucí. Tolik
jsem se do jádra fungování této věci neponořoval.
před 6 lety
- Tharos
- Člen | 1042
@EIFEL: Z featur byly ty implicitní filtry asi tím posledním, co jsem ve verzi 2.1 ještě vyloženě chtěl mít. Takže cca do konce týdne otevřu release větev. Finální verze 2.1 by tedy měla spatřit světa velmi brzy.
Editoval Tharos (18. 11. 2013 21:26)
před 6 lety
- Tharos
- Člen | 1042
@Michal III, @llook: Llook to popsal úplně přesně.
Už nyní z toho lze vybruslit ven – explicitním uváděním filtrů, vlastními gettery/settery… Ale faktem je, že to nejsou kdo ví jak pohodlné způsoby.
IMapper::getImplicitFilters
dostává jako druhý parametr
($caller
) instanci čehosi, co se chystá načíst pro cílovou
entitu ($entityClass
) data z databáze. Nyní jde buďto
o instanci Repository
, anebo o instanci Entity
.
To má svůj význam – pokud je tím volajícím entita, lze se spolehnout
na to, že Entity
i Reflection\Property
budou filtru
k dispozici (samozřejmě pokud je filtr zaregistrován s patřičným
autowiring schématem). Využívá se toho například v té mé ukázce
s překlady – pokud je původcem entita, lze jazyk přebírat z ní, pokud
je původcem repositář, musí se jazyk uvést explicitně.
Nabízí se tedy jedno možné řešení, a to jest, že by „caller“ byl zapouzdřen do instance následující třídy:
namespace LeanMapper;
/**
* @author Vojtěch Kohout
*/
class Caller
{
/** @var mixed */
private $instance;
/** @var mixed */
private $complement;
/**
* @param mixed $instance
* @param mixed|null $complement
*/
public function __construct($instance, $complement = null)
{
$this->instance = $instance;
$this->complement = $complement;
}
/**
* @return mixed
*/
public function getInstance()
{
return $this->instance;
}
/**
* @return mixed|null
*/
public function getComplement()
{
return $this->complement;
}
/**
* @return bool
*/
public function hasComplement()
{
return $this->complement !== null;
}
}
S tím, že pokud by byla „caller“ entita, oním „doplňkem“
(complement) by byla Reflection\Property
, přes kterou se
traverzuje. U repositáře by ten doplněk nebyl využit.
Pak by šlo velmi jednoduše docílit přesně toho, co Michal III chce – implicitní filtr vybírající pouze nesmazané záznamy by se mohl používat všude, kromě hasOne vazeb.
Co si myslíte o takovém řešení? Já si nejsem jist s jednou věcí. Ono to dává do ruky poměrně hodně možností – ono totiž s pomocí tohoto (a pořádné dávky ifů) by někdo mohl úplně všechny filtry (rozumějte i „explicitní“) nadefinovat přes mapper, což bych viděl jako bad practice a nerad bych k tomu někoho vybízel.
Editoval Tharos (19. 11. 2013 9:21)
před 6 lety
- Pavel Macháň
- Člen | 285
Tharos napsal(a):
Jak jsem naznačil, aktualizoval jsem ukázku s překlady tak, aby naplno využívala nejnovějších schopností Lean Mapperu, zejména implicitních filtrů.
Řekl bych, že se celá věc zase zjednodušila. Máme-li takovouto implementaci
getImplicitFilters
:public function getImplicitFilters($entityClass, $caller = null) { if (is_subclass_of($entityClass, 'Model\Entity\TranslatableEntity')) { if ($caller instanceof Entity) { return array('translateFromEntity'); } else { return new ImplicitFilters(array('translate'), array( 'translate' => array($this->getTable($entityClass)), )); } } return parent::getImplicitFilters($entityClass, $caller); }
Nebylo by lepší používat defaultEntityNamespace než mít natvrdo
nastaven namespace entit?
Beru zpět :)
if (is_subclass_of($entityClass, $this->defaultEntityNamespace . '\TranslatableEntity')) {...}
Tharos napsal(a):
@EIFEL: Z featur byly ty implicitní filtry asi tím posledním, co jsem ve verzi 2.1 ještě vyloženě chtěl mít. Takže cca do konce týdne otevřu release větev. Finální verze 2.1 by tedy měla spatřit světa velmi brzy.
Super :)
Editoval EIFEL (20. 11. 2013 0:14)
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
tak jsem v boční větvi
vyzkoušel rozšířit ten parametr $caller
v
IMapper::getImplicitFilters
– nyní se tedy předává instance
třídy LeanMapper\Caller
.
Abych ukázal, k čemu je to tedy dobré… Nahraďme mapper z této mé ukázky tímto:
class Mapper extends TestMapper
{
public function getImplicitFilters($entityClass, Caller $caller = null)
{
if ($entityClass === 'Article' and ($caller->isRepository() or !($caller->getComplement()->getRelationship() instanceof BelongsToMany))) {
return new ImplicitFilters(array('living'), array(
'living' => array($this->getTable($entityClass))
));
}
return parent::getImplicitFilters($entityClass, $caller);
}
}
Výsledek bude takový, že při načítání entit Article
se
vždy (ať už z repositáře nebo nějaké související entity) aplikuje
filtr living
, s výjimkou přístupu přes property nesoucí
belongs to many vazbu. Celé jsem to přizpůsobil té své již
existující ukázce, analogicky by samozřejmě šel vyřešit požadavek
Michala III odlišující vazbu has one.
Pokud proti tomuto řešení nikdo nic nenamítne /anebo to někdo nevymyslí lépe :)/, mám v plánu to brzy mergnout.
Editoval Tharos (20. 11. 2013 13:49)
před 6 lety
- Michal III
- Člen | 84
@Tharos: +1
před 6 lety
- Pavel Macháň
- Člen | 285
Hrál sem si s překlady a narazil sem na jednu věc co se mě u toho moc
nelíbí(tedle pidi SQL dotaz bude hned proveden, ale vnitřně mě štve, že
se volá i když nemusí) a to neustálé dotazování databáze na jazyk
(SELECT language
.* FROM language
WHERE
language
.id_language
IN (xzy)) pro každý
překlad.
V presenteru si načtu po startu všechny jazyky pomocí LanguageFacade a uložím si je do privátní proměné této fasády. Pokud chci najít jazyk dle jeho kodu (cs,en, atd) sahnu do fasády a najdu si je v již načtených jazycích (je tam možnost si vyžádat i aktuální data z DB). Pokud persistuju data byl bych rád kdyby bylo možné použít data z LanguageFacade aby odpadlo dotazování, které už by nebylo potřeba.
Jde mě o to hlavně u jazyků, přeci jen, jak často přidáváte nebo mažete jazyky za životní cyklus presenteru na frontendu.
Lze toto chování změnit bez zásahu do Leanmapperu?
před 6 lety
- Tharos
- Člen | 1042
@EIFEL: Samozřejmě, že je to možné. :) Není to ani nic složitého.
Upravil jsem Ti tu ukázku tak, aby v po prvotním načtení všech jazyků nedošlo už k žádnému dalšímu dotazu do tabulky s jazyky: http://www.leanmapper.com/…lations2.zip
Pointou je, že ty musíš do všech míst, kde jsou zapotřebí, předat
kolekci načtených jazyků (v praxi se jedná pouze o Translator
a o potomky TranslatingRepository
). Já ji v té upravené
ukázce předávám jako asociativní pole, ale v reálu bych si z toho
samozřejmě udělal službu s nějakým jasně daným API a ve výsledku by
veškeré předávání vyřešil auto wiring.
Mimochodem s tímto řešením ty jazyky klidně ani nemusí být v databázi, mohou existovat třeba jen v nějakém konfiguračním souboru.
před 6 lety
- Pavel Macháň
- Člen | 285
Tharos napsal(a):
@EIFEL: Samozřejmě, že je to možné. :) Není to ani nic složitého.
Upravil jsem Ti tu ukázku tak, aby v po prvotním načtení všech jazyků nedošlo už k žádnému dalšímu dotazu do tabulky s jazyky: http://www.leanmapper.com/…lations2.zip
Pointou je, že ty musíš do všech míst, kde jsou zapotřebí, předat kolekci načtených jazyků (v praxi se jedná pouze o
Translator
a o potomkyTranslatingRepository
). Já ji v té upravené ukázce předávám jako asociativní pole, ale v reálu bych si z toho samozřejmě udělal službu s nějakým jasně daným API a ve výsledku by veškeré předávání vyřešil auto wiring.Mimochodem s tímto řešením ty jazyky klidně ani nemusí být v databázi, mohou existovat třeba jen v nějakém konfiguračním souboru.
@Tharos Super, díky moc :). Mě nějak nedocvaklo, že je ten dotaz kladen v Translatoru (a kde jinde že :D )
Editoval EIFEL (21. 11. 2013 16:08)
před 6 lety
- mrtnzlml
- Člen | 143
Ahoj, na stránkách jsem se dočetl, že „Název tabulky lze upřesnit pomocí anotace @table a třídu entity lze upřesnit pomocí anotace @entity (obě anotace patří nad třídu repositáře).“ a protože nedodržuji konvence, tak bych konkrétně anotaci @entity potřeboval použít. To však nefunguje a nikde jsem se nedočetl, že by fungovat neměla. Musím ručně používat $entityClass v createEntity() což se mi ale nelíbí. Jak je to tedy?
Druhá věc. Do teď jsem používal Nette\Database a po přechodu na Dibi se mi nelíbí to, že neumí predikci SELECTů, ale všude v příkladech se používá SELECT *, což se mi nelíbí. Dá se toto v Leanu nějak pěkně vyřešit?
Díky.
před 6 lety
- Pavel Macháň
- Člen | 285
mrtnzlml napsal(a):
Ahoj, na stránkách jsem se dočetl, že „Název tabulky lze upřesnit pomocí anotace @table a třídu entity lze upřesnit pomocí anotace @entity (obě anotace patří nad třídu repositáře).“ a protože nedodržuji konvence, tak bych konkrétně anotaci @entity potřeboval použít. To však nefunguje a nikde jsem se nedočetl, že by fungovat neměla. Musím ručně používat $entityClass v createEntity() což se mi ale nelíbí. Jak je to tedy?
Druhá věc. Do teď jsem používal Nette\Database a po přechodu na Dibi se mi nelíbí to, že neumí predikci SELECTů, ale všude v příkladech se používá SELECT *, což se mi nelíbí. Dá se toto v Leanu nějak pěkně vyřešit?
Díky.
@mrtnzlml:
- Mám takovej dojem (nejsem si tím opravdu jistej), že se anotace @entity a @table odstranila. Toto se řeší pomocí mapperu, takže pokud nedodržuješ konvence základního mapperu tak si vytvoř svůj, který ti bude vyhovovat
- Todle je zase otázka repozitářů. Jak si složíš SQL dotaz takovej ho máš, jen nevím jak se bude chovat persistování entity.
Editoval EIFEL (22. 11. 2013 0:33)
před 6 lety
- mrtnzlml
- Člen | 143
EIFEL napsal(a):
- Mám takovej dojem (nejsem si tím opravdu jistej), že se anotace @entity a @table odstranila. Toto se řeší pomocí mapperu, takže pokud nedodržuješ konvence základního mapperu tak si vytvoř svůj, který ti bude vyhovovat
- Todle je zase otázka repozitářů. Jak si složíš SQL dotaz takovej ho máš, jen nevím jak se bude chovat persistování entity.
Tak to by bylo matoucí, protože @table funguje, to také využívám. Na mapper jsem úplně zapomněl. To by asi šlo. Díky.
U Nette\Database jsem si právě zvykl nepoužívat SELECT vůbec, což je maximálně pohodlné. Každopádně zatím se do tohoto návrhu dostávám, takže to asi nebudu teď řešit…
před 6 lety
- Pavel Macháň
- Člen | 285
mrtnzlml napsal(a):
Tak to by bylo matoucí, protože @table funguje, to také využívám. Na mapper jsem úplně zapomněl. To by asi šlo. Díky.
U Nette\Database jsem si právě zvykl nepoužívat SELECT vůbec, což je maximálně pohodlné. Každopádně zatím se do tohoto návrhu dostávám, takže to asi nebudu teď řešit…
@mrtnzlml: Asi sem si to splet s nějakou jinou anotací, už vtom mám zmatek jak je to roztroušené na 16 stránkách vlákna.
Select nemusíš používat. Stačí nově(dev verze) zavolat v repozitáři
$this->createFluent();
// což je toto + aplikace implicitních filtrů
$this->connection->select('%n.*', $table)->from($table);
Editoval EIFEL (22. 11. 2013 0:54)
před 6 lety
- Michal III
- Člen | 84
Umí si LeanMapper poradit s use statementy předků? Tj:
namespace Model\Entity;
use DateTime;
/**
* @property DateTime $date
*/
class Predek extends \LeanMapper\Entity
{
}
//v jiném souboru
namespace Model\Entity;
class Potomek extends Predek
{
}
Totiž mně to při používání tímto způsobem pak hází:
LeanMapper\Exception\InvalidValueException
Property ‚date‘ is expected to contain an instance of Model\Entity\DateTime, instance of DibiDateTime given.
pokud přistupuji k date instance Potomek ($potomek->date).
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Už by měl. Prosím o otestování. Stačilo jenon změnit hodnotu jednoho parametru. :)
Díky za upozornění!
Editoval Tharos (24. 11. 2013 0:23)
před 6 lety
- Tharos
- Člen | 1042
@mrtnzlml:
Ahoj, na stránkách jsem se dočetl, že „Název tabulky lze upřesnit pomocí anotace @table a třídu entity lze upřesnit pomocí anotace @entity (obě anotace patří nad třídu repositáře).“ a protože nedodržuji konvence, tak bych konkrétně anotaci @entity potřeboval použít. To však nefunguje a nikde jsem se nedočetl, že by fungovat neměla. Musím ručně používat $entityClass v createEntity() což se mi ale nelíbí. Jak je to tedy?
Je to tak, že ty anotace byly tak trochu reliktem z doby, kdy ještě neexistoval samostatně stojící mapper. Chvíli jsem je udržoval i po představení mapperu, ale jakmile Lean Mapper začal podporovat single table inheritance, ty anotace přestaly fungovat dobře. Uměly se s mapperem „tlouct“ (mohly tvrdit něco jiného než mapper), a proto jsem je odstranil. Respektivě odstranil jsem anotaci @entity, která skrývala více potenciálních problémů. Anotace @table se stále ještě bere v potaz.
Mapper by je měl plně nahrazovat. Vím, že mi píšeš na Gitu – odpovím i tam. :)
Druhá věc. Do teď jsem používal Nette\Database a po přechodu na Dibi se mi nelíbí to, že neumí predikci SELECTů, ale všude v příkladech se používá SELECT *, což se mi nelíbí. Dá se toto v Leanu nějak pěkně vyřešit?
V ORM je obecně žádoucí mít entity plně inicializované, byť to s sebou nese nějakou režii. Pokud bys ale někde opravdu chtěl načíst jenom „část entity“, můžeš se podívat na tento můj prastarý příspěvek. Dnes by to díky implicitním filtrům mělo být ještě snazší.
Přímo koncept „předbíhání budoucnosti“ není v Lean Mapperu naimplementován.
před 6 lety
- mrtnzlml
- Člen | 143
Tharos napsal(a):
Je to tak, že ty anotace byly tak trochu reliktem z doby, kdy ještě neexistoval samostatně stojící mapper. Chvíli jsem je udržoval i po představení mapperu, ale jakmile Lean Mapper začal podporovat single table inheritance, ty anotace přestaly fungovat dobře. Uměly se s mapperem „tlouct“ (mohly tvrdit něco jiného než mapper), a proto jsem je odstranil. Respektivě odstranil jsem anotaci @entity, která skrývala více potenciálních problémů. Anotace @table se stále ještě bere v potaz.
Jak už psal EIFEL, pokoušel jsem se napsal v mapperu metodu getEntityClass tak, aby uměla přistoupit k dokumentačnímu komentáři repozitáře, ale to neumím. Jak na to, když mám BlogArticleRepository (obecně jakýkoliv repozitář), který dědí od ARepository, který dědí od LeanMapper\Repository, který volá metodu getEntityClass z mapperu? Pokud se dostanu nějakým rozumným způsobem k tomu komentáři, tak napsat si vlastní anotaci už není problém…
V ORM je obecně žádoucí mít entity plně inicializované, byť to s sebou nese nějakou režii. Pokud bys ale někde opravdu chtěl načíst jenom „část entity“, můžeš se podívat na tento můj prastarý příspěvek. Dnes by to díky implicitním filtrům mělo být ještě snazší.
Přímo koncept „předbíhání budoucnosti“ není v Lean Mapperu naimplementován.
Díky, to že je běžné entity plně inicializovat jsem nevěděl.
před 6 lety
- Pavel Macháň
- Člen | 285
mrtnzlml napsal(a):
Jak už psal EIFEL, pokoušel jsem se napsal v mapperu metodu getEntityClass tak, aby uměla přistoupit k dokumentačnímu komentáři repozitáře, ale to neumím. Jak na to, když mám BlogArticleRepository (obecně jakýkoliv repozitář), který dědí od ARepository, který dědí od LeanMapper\Repository, který volá metodu getEntityClass z mapperu? Pokud se dostanu nějakým rozumným způsobem k tomu komentáři, tak napsat si vlastní anotaci už není problém…
Nepřijde mě to jako moc dobrej nápad zatahovat do mapperu repozitáře.
Editoval EIFEL (24. 11. 2013 12:11)
před 6 lety
- mrtnzlml
- Člen | 143
EIFEL napsal(a):
Nepřijde mě to jako moc dobrej nápad zatahovat do mapperu repozitáře.
Ok, pak se ale vracím k problému, který ještě nikdo nedokázal konkrétně zodpovědět. Tím co jsem zde psal jsem navazoval na https://github.com/…per/issues/8#…, konkrétně na „you can even parse @entity annotation above repository class from your custom mapper“…
Nebráním se jinému řešení, ale zatím opravdu jako nejlepší vychází při psaní repozitáře specifikovat jaká entita se má používat. Jenže když bych neměl zatahovat repozitář do mapperu, pak padla také myšlenka, že by mělo existovat v mapperu něco jako překladová tabulka table ⇒ entity a zase mi přijde dost nelogické při psaní repozitáře zasahovat do nějakého mapperu, který ani není vidět, že se používá a přidávat novou hodnotu do slovníku…
Nebo existuje ještě jiné maximálně elegantní řešení, jak specifikovat se kterou entitou a tabulkou má repozitář pracovat?
před 6 lety
- Pavel Macháň
- Člen | 285
mrtnzlml napsal(a):
EIFEL napsal(a):
Nepřijde mě to jako moc dobrej nápad zatahovat do mapperu repozitáře.
Ok, pak se ale vracím k problému, který ještě nikdo nedokázal konkrétně zodpovědět. Tím co jsem zde psal jsem navazoval na https://github.com/…per/issues/8#…, konkrétně na „you can even parse @entity annotation above repository class from your custom mapper“…
Nebráním se jinému řešení, ale zatím opravdu jako nejlepší vychází při psaní repozitáře specifikovat jaká entita se má používat. Jenže když bych neměl zatahovat repozitář do mapperu, pak padla také myšlenka, že by mělo existovat v mapperu něco jako překladová tabulka table ⇒ entity a zase mi přijde dost nelogické při psaní repozitáře zasahovat do nějakého mapperu, který ani není vidět, že se používá a přidávat novou hodnotu do slovníku…
Nebo existuje ještě jiné maximálně elegantní řešení, jak specifikovat se kterou entitou a tabulkou má repozitář pracovat?
Udělal bych si překladovej slovník v mapperu. Sice to nebudeš mít ihned po ruce, když budeš psát repozitář ale budeš to mít všechno na jednom místě v mapperu a nic tě nebrání si ho rozšířit (pokud implementujes IMapper) o ten překladovej slovník.
Editoval EIFEL (24. 11. 2013 13:30)
před 6 lety
- jannek19
- Člen | 47
ahoj, zkouším teď LeanMapper na jednom projektu, chystám se implementovat jednu konkrétní funkcionalitu v mém systému, jedná se o ukládání jednotlivých revizí pro každý článek. Mám určitou představu, jak bych to v LeanMapperu udělal, ale zajímalo by mě, jestli na danou situaci nenapadne někoho něco lepšího, nějaké best practise. Pokud se tu něco takového už řešilo, tak se omlouvám, tohle vlákno je fakt hrozně dlouhé, je možné že jsem to přehlédl (fakt by chtělo zpracovat jednotlivá řešení z tohoto topicu do dokumentace na webu). Takže, mám to zatím navrženo následovně:
- DB tabulka
article
uchovává společné informace o jednotlivých článcích, přes sloupecrevision_id
odkazuje na aktuální revizi - DB tabulka
revision
uchovává jednotlivé revize článků – data, která se revizi od revize liší – text článku, titulek, autor revize, datum vytvoření revize, komentář k revizi, atd. Zároveň má sloupecarticle_id
, ve kterém je informace, ke kterému článku vlastně tato revize patří a sloupecparent_id
, který se buď odkazuje na rodičovskou revizi, nebo je nastaven naNULL
.
Ukládání a publikování jednotlivých revizí má na starosti DB
procedura saveArticle
, které se jen předají data z formuláře
(text, titulek, autor,…) a ona se automaticky na základě předaných dat
(autor, typ uložení (autosave, draft, final version), atd.) rozhodne, jestli
přepíše některou dřívější revizi, nebo jestli vytvoří zcela novou
revizi a také jestli má danou revizi publikovat (jestli má nastavit
article.revision_id
na ID právě uložené revize). Zároveň taky
kontroluje, jestli nedošlo k potenciální kolizi, jestli někdo jiný
mezitím nepublikoval, nějakou jinou revizi. Informaci o možné kolizi
předává aplikaci pomocí DB erroru, který nám do aplikace díky dibi
probublá ve formě exception. Pokud k chybě nedojde, vrátí ta DB procedura
do aplikace odpovídající article_id
a revision_id
,
co s nimi aplikace udělá je na ní (přesměruje na detail dané revize, na
detail článku, cokoli). Co jsem si s tím tak různě při vývoji hrál,
fungovalo to ukládání podle mě moc pěkně, ale teď to nějak potřebuju
napasovat na entity a repozitáře v LeanMapperu :)
Co se entit týče napadlo mě, udělat v DB view
(articleview
), který by dával dohromady informace z tabulky
article
a odpovídající záznamy z tabulky
revision
, a v aplikaci entitu ArticleviewEntity
(a
k ní odpovídající repositář). V repozitáři bych pak přepsal metodu
persist
, aby místo INSERT
/UPDATE
volal
tu proceduru saveArticle
, možný problém, který tam vidím, je
to automatické vytváření nových revizí (nekonzistence dat v entitě?) a
to vyhazování té Exception při kolizi. Jsou to ale reálné problémy, nebo
si jen vytvářím zbytečná strašidla?
Časem taky budu potřebovat navázat na jednotlivé revize další data (tagy, fotografie, …). To jsem ale zatím nepromýšlel.
Tak co, jdu na to dobře, nebo by to šlo lépe? Nějaké rady, poznámky, nápady?
Díky.
před 6 lety
- Tharos
- Člen | 1042
@mrtnzlml:
Ještě jednou jsem jsi přečetl to Tvé zadání:
I have database, with namespaced tables (blog_articles, set_options, sys_issues, sys_logins and so on). But I want to specify, that entities are BlogArticle, Issue, Login…
Neřešme nyní repositáře. Máme tedy prefixované tabulky
blog_articles
, set_options
, sys_issues
atp. a potřebujeme vyjádřit, že v tabulce blog_articles
jsou
data pro entity BlogArticle
, v sys_issues
jsou data
pro Issue
a tak dále.
Přesně k tomu slouží metody
IMapper::getEntityClass($table, Row $row = null)
a
IMapper::getTable($entityclass)
– tyto metody prostě musí
vracet správné hodnoty pro různé vstupy. Jestli jejich implementace bude
využívat nějakého výčtu přímo v kódu, nějakého konfiguračního
souboru, nějaké speciální tabulky v databázi… už je vedlejší.
Pak zde máme repositáře. Řekněme, že budeme mít repositáře
BlogArticleRepository
, IssueRepository
a tak podobně.
Ty lze „namapovat“ buďto pomocí anotace @table
(@table blog_articles
nad BlogArticleRepository
,
@table sys_issues
nad IssueRepository
atp.), anebo
implementací metody IMapper::getTableByRepositoryClass
. Já bych
v tomto případě spíše využil anotaci @table
– na tohle se
přesně hodí a takové řešení neskrývá žádná skrytá rizika.
Je to řešení Tvého problému? :)
Důvodem, proč mapování sys_issues
→ Issue
a
podobné musí být v mapperu a ne jen v nějaké anotaci nad repositářem,
je to, že tyhle detaily nezajímají jenom repositáře. Architektura Lean
Mapperu je taková, že entita pro načtení těch entit, na které má vazbu,
nepotřebuje odpovídající repositáře, nýbrž se k nim umí dotraverzovat
„svépomocí“. K tomu ale potřebuje znát detaily O/R mapování a pokud
by byly zapsané jen někde v anotaci nad nějakým repositářem, nebyly by
jí nic platné (entity nemají na repositářích vůbec žádnou závislost,
vůbec o nich neví). Ona se ptá mapperu.
Tohle je přesně důvod, proč anotace @entity
nefungovala
dobře a proč byla zrušena.
Editoval Tharos (25. 11. 2013 0:37)
před 6 lety
- mrtnzlml
- Člen | 143
Tharos napsal(a):
…
Přesně k tomu slouží metodyIMapper::getEntityClass($table, Row $row = null)
aIMapper::getTable($entityclass)
– tyto metody prostě musí vracet správné hodnoty pro různé vstupy. Jestli jejich implementace bude využívat nějakého výčtu přímo v kódu, nějakého konfiguračního souboru, nějaké speciální tabulky v databázi… už je vedlejší.
…
Je to řešení Tvého problému? :)
…
Díky, tomuto všemu myslím rozumím, ale možná jsem to celou dobu popisoval blbě, takže mi všichni odpovídají na něco jiného. Za to se omlouvám. Mě právě spíš zajímala implementace toho co je vedlejší, tedy jak správně vracet hodnoty pro různé vstupy. Přivedlo mě k tomu to, že napsat to do repozitáře pomocí anotace @entity bylo dost elegantní, například oproti využívání konfiguračního souboru, spec. tabulky v DB, atd… Proto jsem se snažil v mapperu doprogramovat reflexi repozitáře, abych si tam tu anotaci dodělal sám, ačkoliv v Leanu tato funkce již není. Každopádně to neumím udělat, takže zřejmě zatím zůstanu u nějakého méně elegantního řešení, jako je třeba:
/** @var array */
protected $translate = array( //FIXME: stupid solution
//table_name => entity
'blog_articles' => 'BlogArticle',
'odr_orders' => 'Order',
'examplehashedname' => 'Author', //cannot resolve entity name from table_name
);
//...
public function getEntityClass($table, LeanMapper\Row $row = null) {
//zde bych rád provedl reflexi nad repozitářem, který je aktuální, ale to neumím
if(array_key_exists($table, $this->translate)) {
return $this->defaultEntityNamespace . '\\' . $this->translate[$table];
} else {
return $this->defaultEntityNamespace . '\\' . ucfirst($table);
}
}
Každopádně díky za tvůj čas, nesmírně si toho vážím. Třeba časem přijdu na to jak elegantně rovnou při psaní repozitáře specifikovat entitu a nebudu muset zasahovat do mapperu, nebo konfiguračního souboru. Nebo možná řeším blbosti a opravdu se to takto normálně řeší. Pak se již není o čem bavit… (-:
před 6 lety
- Tharos
- Člen | 1042
@jannek19: V Tvém případě se budeš muset
rozhodnout, co necháš na databázi (v podobě pohledu, uložených procedur a
tak podobně) a co necháš v „objektech“. Pravděpodobně by to s Lean
Mapperem celé šlo vyřešit v „objektech“, jsem přesvědčen, že by ses
obešel i bez té uložené metody saveArticle
. Ale vůbec není
na překážku – lze ji využít z repositáře při persitenci, přesně,
jak píšeš.
Osobně bych asi vynechal ten pohled pro join dat z article
a
revision
, protože to lze v Lean Mapperu velmi snadno propojit
pomocí filtru. Psáno z hlavy by to mohlo vypadat nějak takhle:
// CustomMapper.php
public function getImplicitFilters($entityClass, Caller $caller = null)
{
if ($entityClass === 'Model\Entity\Article') {
return new ImplicitFilters(function ($statement) { // tzv. anonymní filtr, novinka, o které se akorát chystám napsat na fórum :)
$statement->select('[revision].]')->join('revision')->on('[task.revision_id] = [revision.id]')
});
}
}
Entita Model\Entity\Article
pak bude zapoudřovat už ta
spojená data. Úplně stejně, jako by zapouzdřovala data z Tvého view.
Problémy s automatickým vytvářením nových revizí bys mít neměl,
protože ty můžeš v repositáři v rámci persistence libovolně upravit
data entity. Prostě když zjistíš, že update vedl například k vytvoření
nového záznamu v tabulce revision
, tak jen nastavíš jeho ID do
entity Article
a na závěr zavoláš ještě v repositáři
$article->markAsUpdated()
, aby se při příští persistenci
neměl tendenci volat update toho sloupce s cizím klíčem.
Osobně v tom pak už nevidím žádné zádrhely. Jinými slovy: pokud ti
při persistenci uložená procedura saveArticle
změní stav dat,
není problém ty změny reflektovat do entity ještě před tím, než ji
vrátíš z repositáře ven.
Mám můj popis hlavu a patu a je srozumitelný? :)
před 6 lety
- Tharos
- Člen | 1042
@mrtnzlml: No, hele, popíšu Ti jeden trochu bláznivý způsob, jak toho docílit. :) Byl jsem línej se o tom rozepisovat, ale noc je ještě mladá…
Ty si tu překladovou tabulku můžeš totiž vygenerovat právě z těch anotací:
- Vytvoř si vlastní mapper (ten už asi máš)
- Vytvoř si repositáře a využij v nich anotace
@table
i@entity
- Do konstruktoru mapperu si předej RobotLoader a vytáhni si z něj hned všechny repositáře, které má naskenované
- Ty projdi a vytáhni si z každého (třeba pomocí parseru anotací
z Nette) hodnotu anotace
@table
a@entity
- No a výsledkem bude přesně ta překladová tabulka, kterou bys jinak psal a udržoval ručně :)
Důležité je tuhle analýzu provádět maximálně jednou (při
vytváření instance mapperu) a ne při každém dotazu na
IMapper::getEntityClass
nebo IMapper::getTable
,
protože jinak by to mohlo být hodně líné.
Robot loader je na takovéto hrátky super věc. Já takhle skenuji entity, z jejichž reflexí pak generuji schéma databáze a v jednom projektu částečně i sestavuji formuláře.
Editoval Tharos (25. 11. 2013 0:35)
před 6 lety
- mrtnzlml
- Člen | 143
Díky, takto mi to stačí. Vždy jsem se pokoušel v mapperu získat aktuálně používaný repozitář, což se ale ukázalo jako docela náročný úkol. Toto řešení mě nenapadlo a je to konečně něco u čeho již mohu říct ano, to jsem hledal. (-: Díky za pomoc a inspiraci…
před 6 lety
- Michal III
- Člen | 84
Tharos napsal(a):
@Michal III: Už by měl. Prosím o otestování. Stačilo jenon změnit hodnotu jednoho parametru. :)
Díky za upozornění!
Otestováno, funguje. Díky za rychlou opravu :-).
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
píšu nový quick start (který bude součástí nové dokumentace) a vymyslel jsem si zadání s „nenulovou“ doménovou logikou. Představený model bude někde na pomezí mezi anémickým a rich domain.
Jak tak quick start píšu, napadají mě různé nápady, které se většinou ihned snažím převést v realitu. Na GitHubu teď přibylo pár novinek, které bych zde postupně rád představil. A začnu tím, čemu říkám:
Anonymní filtry
Jde o úplně jednoduchý koncept. Mějme nějaký absolutně jednoúčelný
filtr, který má v celém modelu smysl použít jen na jednom jediném
místě. Například máme knihu s přiřazenými tagy a chceme mít v entitě
Book read only položku isEbook
, která vrátí boolean hodnotu
vyjadřující, zda kniha má přiřazen jeden vybraný tag (třeba s názvem
„ebook“).
Lze na to napsat krásný stručný filtr. Doteď ale bylo nutné ten filtr
pojmenovat (otrava), zaregistrovat v Connection
(další otrava) a
teprve pak jej bylo možné využít.
Nově instance Filtering
a ImplicitFilters
přijímají jako první argument nejen pole s názvy filtrů, ale
i s instancemi Closure
, kterým se předává Fluent
a dynamicky předané parametry. Pole může obsahovat klidně i mix názvů
filtrů s instancemi Closure
a dále také platí, že pokud
chcete použít jenom jeden filtr (ať už daný jako název nebo
Closure
), může se předat přímo (nemusí se předávat
v rámci pole s jedním prvkem), viz ukázka v tomto postu.
Těmto anonymním filtrům nejde autowirovat entitu ani property a nejde jim
ani předat žádné targeted args (což je pochopitelné, nelze na ně
odkazovat názvem), ale to vůbec nevadí, protože vše lze do filtru dostat
pomocí use
při vytváření Closure
.
Takto tedy může takový anonymní filtr vypadat v praxi:
/**
* @property int $id
* @property Tag[] $tags m:hasMany
* @property string $name
* @property string|null $description
* @property string|null $website
* @property-read bool $isEbook m:useMethods(isEbook)
*/
class Book extends \LeanMapper\Entity
{
/**
* @return bool
*/
public function isEbook()
{
$filtering = new Filtering(function (Fluent $statement) {
$statement->join('tag')->on('[book_tag.tag_id] = [tag.id]')
->where('[tag.name] = "ebook"');
});
$rows = $this->row->referencing('book_tag', null, $filtering);
return !empty($rows);
}
}
Enjoy!
Editoval Tharos (25. 11. 2013 14:26)
před 6 lety
- jannek19
- Člen | 47
@Tharos: Díky za rady, tohle se hodí, zkusím to tak udělat a uvidím.