Oznámení
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
další taková novinka se týká vylepšení chování příznaku
m:passThru
.
Nově se při čtení z property zaregistrovaná metoda volá
bezprostředně po získání hodnoty z Row
a při zapisování do
property se zaregistrovaná metoda volá těsně před zapsáním do hodnoty do
Row
. Důsledek to má takový, že všechny další kontroly (typ
property, nullable…) se aplikují až na hodnotu, která tou metodou
prošla.
To především rozvazuje ruce co se typů property týče. Řekněme
například, že bychom u entity Book
chtěli mít nějakou
informaci uloženou v databáze nestrukturovaně jako JSON v jednom sloupci,
ale v aplikaci bychom chtěli pracovat s dekódovanou hodnotou – při
čtení z property i při zápisu do ní.
/**
* @property int $id
* @property string $name
* @property array $data m:passThru(decode|encode)
*/
class Book extends \LeanMapper\Entity
{
/**
* @param string $value
*/
protected function decode(&$value)
{
$value = json_decode($value, true);
}
/**
* @param array $value
*/
protected function encode(&$value)
{
$value = json_encode($value);
}
}
Smysl výše uvedeného kódu by měl být jasný: v databázi mám hodnotu
uloženou jako JSON, ale v aplikaci při přístupu k položce
data
dostanu pole. Pole se do ní má také přiřazovat.
Možná jste si všimli, že v ukázce předávám parametr referencí. To
je změna oproti stable verzi a také BC break. Důvod je následující: jsem
přesvědčen, že příznak m:passThru
se tak v 95 % případů
používá pro validaci přiřazované/čtené hodnoty. A u takového
použití mi ve stable verzi přichází otravné, že na závěr metody je
nezbytně nutné volat return $value
. Nově Lean Mapper
s návratovou hodnotou nijak nepracuje a pokud ji v metodě zaregistrované
přes m:passThru
potřebujete změnit (jako v tom mém výše
uvedeném příkladě), předejte si parametr referencí.
Nápad, že by Lean Mapper nějakým způsobem přímo rozšiřoval typy,
které vrací dibi, zatím dávám k ledu. Také vznikl nápad, že by se v
m:passThru
volaly statické metody, ale tomu osobně nejsem moc
nakloněn. Jednak si vážím toho, že se Lean Mapper aktuálně bez
statických volání kompletně obejde (výjimkou je pár factory metod), a
také si to každý může naimplementovat sám v __call
v nějaké BaseEntity
.
před 6 lety
- Tharos
- Člen | 1042
Jenom ještě takový dovětek, co že je na tom typu property při použití
m:passThru
nového. :)
Ve stable verzi se kontrola hodnoty řeší ještě před voláním
zaregistrované metody, takže ve výše uvedeném případě by položka
data
musela mít typ string
, což by samozřejmě bylo
zavádějící v aplikaci (reálně by vracela pole). Že se takhle Lean Mapper
choval považuji de facto za bug.
před 6 lety
- Michal III
- Člen | 84
@Tharos: Ahoj. Když v metodě
Mapper::getImplicitFilters
předám anonymní filtr, tak v metodě
Repository::createFluent
je pak tento callback argumentem ve funkci
array_key_exists()
na řádku 95, která požaduje jen
string
nebo integer
.
Použil jsem to jako v téhle ukázce . Dělám něco špatně, nebo je to neošetřený vstup?
před 6 lety
- Michal III
- Člen | 84
Tharos napsal(a):
Jenom ještě takový dovětek, co že je na tom typu property při použití
m:passThru
nového. :)Ve stable verzi se kontrola hodnoty řeší ještě před voláním zaregistrované metody, takže ve výše uvedeném případě by položka
data
musela mít typstring
, což by samozřejmě bylo zavádějící v aplikaci (reálně by vracela pole). Že se takhle Lean Mapper choval považuji de facto za bug.
Na toto už jsem jednou narazil, když jsem potřeboval hodnotu z
enum('yes', 'no')
převést na TRUE, FALSE
. Tenkrát
jsem to obešel tím, že jsem to nakonec změnil v databázi na
tinyint
. Je fajn, že teď už to lze řešit :-). +1 pro
LeanMapper.
před 6 lety
- Casper
- Člen | 253
@Tharos:
To předávání referencí se mi nelíbí. Je to zbytečný
(a poměrně zásadní) BC break, který nepřidává nic nového. To, že se ti
to líbí více novým způsobem podle mě není moc důvod k takovému kroku.
A popravdě jsem podobný nápad nikde moc neviděl,
return
je prostě přímočařejší, častěji
používanější a intuitivnější (IMHO). A myslím, že to neposkytuje ani
výkonnostní výhody (a nebo neměřitelné). Představ si třeba, že by
(není nejlepší příklad)Nette\Forms\Container::getValues()
v Nette muselo fungovat přes
reference…
Editoval Casper (28. 11. 2013 14:04)
před 6 lety
- Tharos
- Člen | 1042
@Michal III:
Když v metodě
Mapper::getImplicitFilters
předám anonymní filtr, tak v metoděRepository::createFluent
je pak tento callback argumentem ve funkciarray_key_exists()
na řádku 95, která požaduje jenstring
nebointeger
.Použil jsem to jako v téhle ukázce . Dělám něco špatně, nebo je to neošetřený vstup?
To byl samozřejmě neošetřený vstup. Je to fixnuté v develop větvi, můžeš to vyzkoušet.
Díky moc za upozornění!
před 6 lety
- Tharos
- Člen | 1042
@Casper:
Určitě se nebráním hezčímu řešení, pokud nějaké vymyslíme.
Motivací k provedené úpravě mi bylo hlavně to, že třeba já osobně
příznak m:passThru
používám v naprosté většině případů
k validaci a psát return na konci takovýchto metod mě vcelku otravovalo:
protected function validateEmail($email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid e-mail address given: $email.");
}
return $email; // tohle je pruda
}
Napadly mě následující způsoby, jak ten return u čistě validačních funkcí odstranit:
1) Automaticky detekovat, jestli metoda něco vrátila nebo
ne. To je sice nejelegantnější, ale bohužel nemožné v PHP. :) Bohužel
v něm neexistuje nic jako void
nebo undefined
a
není způsob, jak odlišit, jestli metoda vrátila null
nebo nic…
2) Zavést nový příznak (snad m:validate
),
který by se volal těsně před m:passThru
a neočekávalo by se,
že zaregistrovaná metoda má vrátit nějakou hodnotu. Tohle by
z navržených řešení jako jediné nezpůsobilo BC
break.
3) Umožnit, aby v obsahu příznaku
m:passThru
šlo říct, zda se u dané metody má návratová
hodnota použít nebo ne. Napadlo mě následující řešení:
m:passThru(validateEmail) návratová hodnota se nepoužívá
m:passThru(encodeValue~) návratová hodnota se používá /tilda vyjadřuje, že hodnota má „protéct“ metodou :)/
4) Řešení skrze předání proměnné referencí.
Zatím jsem zvolil řešení 4, protože s sebou samozřejmě nenese žádný overhead (jedná se o využití něčeho, co v PHP už existuje).
Které řešení by se Ti nejvíce líbilo? Anebo Tě napadá ještě něco jiného? Díky!
Editoval Tharos (4. 12. 2013 8:00)
před 6 lety
- Casper
- Člen | 253
@Tharos:
Osobně mi ten řádek navíc nedělá problém, takže bych to vůbec neřešil
:) Ale pokud ti těch pár znaků v každé validaci vadí, byl bych pro
variantu 2. Jednička je nemožná, trojka je další divná
konvence/magie a čtyřka se mi nelíbí :) Dvojka je výstižná a navíc se
alespoň odliší validace od nějaké transformace, což je myslím správně.
Sice přibyde další příznak, kterých (pokud si dobře vzpomínám) nechceš
mít moc, ale přijde mi to jako nejlepší řešení – navíc bez BC
breaku.
před 6 lety
- Tharos
- Člen | 1042
@Casper: Tak fajn. :) Dneska jsem o tom ještě přemýšlel a aktuálně mi řešení přes nový příznak také přijde jako nejlepší.
On by to pro někoho BC break být mohl – je totiž možné, že se trefím do názvu příznaku, který někdo používá jako vlastní příznak, ale beztak bude verze 2.1 obsahovat hned několik BC breaků, takže tohle bych prostě podstoupil.
Co ještě řeším je, jak by se ten příznak měl jmenovat. Samozřejmě
se nabízí m:validate
, ale metody, které v něm kdy budu
registrovat, také většinou budou začínat slovem „validate“
(validateEmail
, validateAge
…). Není úplně hezké
mít to vše pak vedle sebe: m:validate(validateEmail)
,
m:validate(validateAge)
atp.
Jak ten příznak pojmenovat? Nebo to udělat tak, že příznak
m:validate
u položky email
bude automaticky volat
metodu validateEmail
? S tím, že pokud by byl uveden nějaký
název metody v závorkách, použila by se ta metoda? Velmi podobně funguje
m:useMethods
, jen zde bych asi nezaváděl to, že by se ta metoda
volala i bez uvedení příznaku (jak se děje u get<Name> a
set<Name> metod).
před 6 lety
- Michal III
- Člen | 84
Jestli tomu dobře rozumím, tak varianta 2 by
způsobovala, že by existovali 2 příznaky na tutéž věc, ovšem
s rozdílem reference/return… to se mi moc nelíbí. To už by se mi asi
více líbilo, ač možná trochu magické, něco jako
m:passThru(&validateEmail)
, kde by &
značilo očekávání reference v callbacku.
Pak mně ještě napadlo, že by se použila návratová hodnota jen
v případě, že by se nezměnila vstupní hodnota coby reference, což by
však bylo WTF chování, když by se dle nějaké podmínky hodnota nezměnila,
čímž by se hodnota property nastavila na NULL
.
před 6 lety
- Tharos
- Člen | 1042
@Michal III:
Jestli tomu dobře rozumím, tak varianta 2 by způsobovala, že by existovali 2 příznaky na tutéž věc, ovšem s rozdílem reference/return…
Víceméně ano. Mně se prostě jenom nelíbí psát na konec metody
validateEmail
konstrukci return $email
… nemá tam
žádnou logiku.
Pak mně ještě napadlo, že by se použila návratová hodnota jen v případě, že by se nezměnila vstupní hodnota coby reference, což by však bylo WTF chování
Co je ta vstupní hodnota coby reference? :)
Kdyby existovalo m:validate
tak, jak jsem ho popsal výše, tak
přidaná hodnota oproti použití m:passThru
by v případě
validace hodnoty byla, že ta validační metoda nemusí vracet hodnotu, a také
to, že název validační metody vůbec nemusí být uveden (ale můžu).
Tohle by se mi i vcelku líbilo:
/**
* @property int $id
* @property string $email m:validate
*/
class Author extends \LeanMapper\Entity
{
/**
* @param string $email
*/
protected function validateEmail($email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid e-mail address given: $email.");
}
}
}
Jak vám?
Editoval Tharos (28. 11. 2013 21:47)
před 6 lety
- Casper
- Člen | 253
@Tharos:
Rozhodně se mi to líbí víc než ty reference :) A ten automatický název metody vypadá fajn.
před 6 lety
- Michal III
- Člen | 84
@Tharos:
Víceméně ano. Mně se prostě jenom nelíbí psát na konec metody
validateEmail
konstrukcireturn $email
… nemá tam žádnou logiku.
Tomu rozumím. Já osobně s implementací 4 problém
nemám, zvykl bych jsi. Ale mám výhodu, že jsem zasvěcen. Noví uživatelé
by asi logicky předpokládali, že by se hodnota vracela přes return. Proto se
mi právě docela líbil ten návrh, že by se použilo.
m:passThru(&validateEmail)
, protože nezasvěcení by tam
& nedávali, kdežto zasvěcení by věděli, co
dělají.
Pak mně ještě napadlo, že by se použila návratová hodnota jen v případě, že by se nezměnila vstupní hodnota coby reference, což by však bylo WTF chování
Co je ta vstupní hodnota coby reference? :)
Tím jsem myslel tu property, která se předá jako parametr té
„passThru“ funkce. Tedy chtěl bych validovat property $email
,
tak bych si uložil její hodnotu, pak bych ji protáhl skrz passThru a potom
bych porovnal uloženou hodnotu s tou „protaženou“, jestli nedošlo ke
změně, ke které by mohlo dojít pouze, pokud by callback funkce přijímala
referenci. Nicméně opravdu se mi to nezdá jako šikovné řešení… (Je to
patrné už při Tvé ukázce, kdy se email validuje, ale nemění.)
Kdyby existovalo
m:validate
tak, jak jsem ho popsal výše, tak přidaná hodnota oproti použitím:passThru
by v případě validace hodnoty byla, že ta validační metoda nemusí vracet hodnotu, a také to, že název validační metody vůbec nemusí být uveden (ale můžu).Tohle by se mi i vcelku líbilo:
/** * @property int $id * @property string $email m:validate */ class Author extends \LeanMapper\Entity { /** * @param string $email */ protected function validateEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid e-mail address given: $email."); } } }
Jako přidanou hodnotu m:validate
tedy vidím, že se nemusí
uvádět jméno té funkce. Je to ale za cenu toho, že by pro téměř tutéž
věc existovaly 2 příznaky. Nebo by se příznak validate používal jen a
pouze na validaci, kdy by nedošlo k samotné změně hodnoty, kdežto passThru
by hodnotu měnila?
Ještě jsem se nedostal k tomu, že bych používal vlastní příznaky.
Zeptám se tedy, bylo by možné pomocí eventů nějak zapříčit, abych si
například vytvořil vlastní příznak m:email, který by způsoboval, že se
provede m:passThru(validateEmail)
(m:validate
)? Tohle
je zrovna asi špatný příklad, ale mohla by existovat často používaná
funkce v BaseEntity
, kterou bych potom volal takto snadno.
Dokonce mě napadlo, že i existující příznaky (alespoň některé) by byly nějaké protected metody, které by si mohl uživatel případně dle své potřeby předefinovat sám. Nevím, jestli už nezacházím příliš daleko…
před 6 lety
- Michal III
- Člen | 84
Z jiného soudku:
Mám aplikaci, která má spoustu pohledů, kde vypisuji v tabulkách
jednotlivá data (zaměstnanci, dokumenty, uživatelé, …). Nyní bych chtěl
umožnit filtrování těchto pohledů, ale chtěl bych to udělat nějak
inteligentně. Filtrování mohou být poměrně různorodá, kde je potřeba
dost změnit sql dotaz (např. filtrovat zaměstnance podle roku narození
znamená něco jako YEAR(birth_date) = ?
).
Jak to tedy udělat inteligentně a pokud možno co nejvíce věcí vyřešit nějak znovupoužitelně pro ostatní pohledy? Jak napsat metodu ve facade? Co všechno řešit v repozitáři? Jaký je best practise?
Co se tyče facade, napadlo mě několik možností:
- Jednotlivá kritéria jako parametry:
getUsers($username = NULL, $lastActivity = NULL..
- Dát kritéria do pole a to poté v metodě validovat
getUsers($restrictions)
A u repozitáře nevím, zda-li to mám řešit nějak globálně nějakou
hodně univerzální funkcí findBy
v BaseRepository
,
která by byla připravená na všemožné výjimky (např. onen
YEAR
či vyhledávání přes join v jiné tabulce…), nebo
vytvořit v každém repozitáři funkci „na míru“.
Zatím se mi mé nápady moc nelíbily, proto se obracím sem. Děkuji za nějaké rady.
před 6 lety
- Ripper
- Člen | 56
Zdravím,
lámu si tu hlavu s jednou věcí a stále ne a ne pochopit jak se k tomu dostat. Vím že to budu muset udělat v Entitě, ale netuším jak. Mám tabulku setting(id, collection_id), collection(id, name), collection_values(id, collection_id, value) a už je vám asi jasné co potřebuju. Chci vytáhnout řádek z setting a získat všechny možné values přiřazené ke kolekci. Tedy aby to pak bylo nějak takhle $setting->values ← tady potom budou řádky z collection_values.
Díky za nakopnutí.
před 6 lety
- Tharos
- Člen | 1042
@Ripper: Ahoj, to, co Ty potřebuješ, je zhruba následující konstrukce v entitě:
<?php
namespace Model\Entity;
/**
* @property int $id
* @property Collection $collection m:hasOne
* @property CollectionValue[] $collectionValues m:useMethods
*/
class Setting extends \LeanMapper\Entity
{
/**
* @return CollectionValue[]
*/
public function getCollectionValues()
{
$collectionValues = array();
foreach ($this->row->referenced('collection')->referencing('collectionvalue') as $collectionValue) {
$collectionValues[] = $entity = $this->entityFactory->createEntity('Model\Entity\CollectionValue', $collectionValue);
$entity->makeAlive($this->entityFactory);
}
return $collectionValues;
}
}
„Nudil jsem se“, a tak tady máš funkční ukázku. :)
před 6 lety
- Ripper
- Člen | 56
@Tharos: Wow, tak to ja paráda! Máš u mě velké díky.
před 6 lety
- Casper
- Člen | 253
@Tharos:
Neuvažoval jsi, že by metoda Repository::persist
mohla
přijímat pole entit? Jde mi o to, že nemám žádný efektivní
nástroj jak uložit vyšší množství entit najednou (s vygenerováním
jediného SQL update dotazu). Myslím, že by to bylo poměrně
užitečné, co myslíš?
Editoval Casper (30. 11. 2013 23:22)
před 6 lety
- Tharos
- Člen | 1042
@Ripper: Nemáš vůbec zač.
Ještě mě dneska napadlo, že Ty to vlastně vůbec nemusíš řešit takhle „low level“. :) Následující řešení je ekvivalentní a já osobně bych ho určitě upřednostnil:
<?php
namespace Model\Entity;
/**
* @property int $id
* @property Collection $collection m:hasOne
* @property CollectionValue[] $collectionValues m:useMethods
*/
class Setting extends \LeanMapper\Entity
{
/**
* @return CollectionValue[]
*/
public function getCollectionValues()
{
$collectionValues = array();
foreach ($this->collection->collectionValues as $collectionValue) {
$collectionValues[] = $collectionValue;
}
return $collectionValues;
}
}
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Já na tohle používám cosi, čemu říkám Query object, ale zjistil jsem, že to nazývám špatně :). Jedná se spíše o jakýsi Criteria object.
Vytvořím prázdnou instanci takového objektu, nastavím jej do nějakého
stavu (tzn. vložím do něj restrikce, které potřebuji) a pak to předám
repositáři, který má metodu findBy(Criteria $criteria)
, jenž
vrací požadované entity.
Je to velmi pohodlné, ale má
to i svá úskalí. „Plnokrevný“ Query object (podle Fowlera)
by dost možná fungoval lépe. Otázkou je, do čeho by se měl
překládat – já si dokáži představit řešení, že by se překládal do
sekvence volání nad Fluent
.
před 6 lety
- Tharos
- Člen | 1042
@Casper: Pro multi insert jisté řešení existuje. Co si mám představit pod „multi update“? :) Zajímalo by mě, jak vypadá takový jeden SQL dotaz.
Přiznám se, že se mi současná podoba té persist metody docela líbí.
Připadá mi hezky přehledá. Je z ní na první pohled jasné, co vše se
při persistování děje, k čemu a kdy se volají metody
Repository::insertIntoDatabase
,
Repository::updateInDatabase
atp. A bojím se, aby jí nějaký
„multi dotaz“ zbytečně neznepřehlednil.
před 6 lety
- Casper
- Člen | 253
@Tharos:
Multi-update je samozřejmě blbost :) A tím pádem je tak trochu i ten můj
dotaz blbost – jelikož lze zkonstruovat pouze ten multi-insert, tak to asi
do persist prostě nepatří. Nicméně díky za odkaz, tohle jsem už dávno
zapomněl, že tu někde je.
před 6 lety
- Michal III
- Člen | 84
@Tharos: Děkuji za navedení :-).
Jinak nestálo by za to dekomponovat __get
metodu
Entity
do více protected
či private
metod, je-li to možné, aby i nadále mohl uživatel používat některou
její funkcionalitu, ačkoli si get
metodu entity napíše sám a
nevyužije při tom anotace?
Já jsem zrovna narazil na to, že jsem potřeboval něco složitějšího a musel si tak napsat vlastní metodu v entitě, ale přitom jsem chtěl využít implicitní filtry.
před 6 lety
- Ripper
- Člen | 56
@Tharos: To vypadá suprově, ale hází mi to „Missing ‚collection‘ value for requested row.“, ale budu zkoušet. Jinak lze nějakým způsobem prohnat výstup přes fetchPairs? Elegantně přes LeanMapper.
před 6 lety
- Michal III
- Člen | 84
@Ripper: K tomu fetchPairs
: V
BaseRepository
si můžeš v metodě createCollection
transformovat pole do nějakého objektu (například ArrayHash
).
Když si vytvoříš vlastní objekt, který bude dědit třeba od toho
ArrayHash
, můžeš si tam vytvořit funkci
fetchPairs
sám.
Editoval Michal III (1. 12. 2013 15:22)
před 6 lety
- Ripper
- Člen | 56
@Michal III: Díky za odpověď. Moc jsem ale nepochopil jak to udělat, respektive jak to „transformovat“, udělal jsem to tedy takhle –
/**
* @param null $key
* @param null $value
*
* @return array
*/
public function fetchPairs($key = NULL, $value = NULL)
{
return $this->createStatement()->fetchPairs($key, $value);
}
Ale asi to nebude moc košér.
Edit – Ještě bych měl dotaz na všechny. Jak pojmenováváte tabulky? Používám „nazev_tabulky“, ale abych dodržel konvenci LeanMapperu, asi bych měl používat „nazevtabulky“ že? Ale to mi přijde takové suché. Nebo se mi to jen zdá? Jaká je v tom výhoda?
Editoval Ripper (1. 12. 2013 20:17)
před 6 lety
- Michal III
- Člen | 84
@Ripper: Můj objekt pro kolekci vypadá zhruba takto (respektive v současné době ho mám trochu komplikovanější, ale tahle triviální implementace postačí):
namespace Model;
use Nette\ArrayHash;
class Collection extends ArrayHash
{
/**
* Creates $key => $value pairs from entity collection.
*
* @param string|NULL $key
* @param string $value
* @return array
*/
public function fetchPairs($key, $value)
{
$pairs = array();
foreach ($this as $entity) {
if ($key === NULL) {
$pairs[] = $entity->$value;
} else {
$pairs[$entity->$key] = $entity->$value;
}
}
return $pairs;
}
}
V BaseRepository
mám potom toto:
protected function createCollection(array $entities)
{
return Collection::from($entities);
}
Co se týče konvencí pojmenování tabulek a sloupců, lze to vyřešit v mapperu takto:
/**
* Converts camelConvention to snake_convetion.
*
* @param string $str
* @return string
*/
public static function camel2Snake($str)
{
return strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $str));
}
/**
* Converts snake_convetion to camelConvention.
*
* @param string $str
* @return string
*/
public static function snake2Camel($str)
{
$words = explode('_', $str);
$return = $words[0];
unset($words[0]);
foreach ($words as $word) {
$return .= ucfirst($word);
}
return $return;
}
public function getColumn($entityClass, $field)
{
return self::camel2Snake($field);
}
public function getEntityField($table, $column)
{
return self::snake2Camel($column);
}
public function getEntityClass($table, \LeanMapper\Row $row = null)
{
$class = '';
switch ($table) {
default:
if (strpos($table, $this->defaultTablePrefix) === 0) {
$class = ucfirst(self::snake2Camel(substr($table, strlen($this->defaultTablePrefix))));
} else {
return parent::getEntityClass($table, $row);
}
}
return "$this->defaultEntityNamespace\\$class";
}
public function getTableByRepositoryClass($repositoryClass)
{
$matches = array();
if (preg_match('#([a-z0-9]+)repository$#i', $repositoryClass, $matches)) {
$table = self::camel2Snake($matches[1]);
switch ($table) {
default:
return $this->defaultTablePrefix . $table;
}
}
throw new InvalidStateException('Cannot determine table name.');
}
Toto by mělo zajišťovat převod název tabulek a sloupců z
foo_bar
na fooBar
a obráceně.
PS. omlouvám se za ten kód toho mapperu. Je to vytržené z jednoho
projektu, takže se ve Tvém případě nejspíše dá celkem obejít bez
operování s defaultTablePrefix
, sloužící pro nastavení
výchozího prefixu tabulek.
před 6 lety
- Ripper
- Člen | 56
@Michal III: Díky za odpověď. Ve všem hledám moc velké složitosti, převádění těch názvů tabulek je super. Jinak jak potom použiješ tu funkci fetchPairs? Jak se k ní dostaneš?
před 6 lety
- Michal III
- Člen | 84
@Ripper: Není zač :-). Pokud si definuješ vazební
property
v anotacích přes příznaky m:hasMany
nebo
m:belongsToMany
, tak se k ní dostaneš
$setting->collectionValues->fetchPairs('key', 'value')
(o to
se postará právě funkce createCollection
v
BaseRepository
. Pokud je definuješ pomocí metod, jako jsi to
řešil výše, musíš se o to postarat sám asi takto:
/**
* @return CollectionValue[]
*/
public function getCollectionValues()
{
$collectionValues = array();
foreach ($this->collection->collectionValues as $collectionValue) {
$collectionValues[] = $collectionValue;
}
// Právě zde převedeš pole $collectionValues na Tvou kolekci.
return \Model\Collection::from($collectionValues);
}
před 6 lety
- Ripper
- Člen | 56
@Michal III: Tak to je super! To nemá chybu :) Díky ještě jednou.
před 6 lety
- Tharos
- Člen | 1042
@Michal III:
Jinak nestálo by za to dekomponovat
__get
metoduEntity
do víceprotected
čiprivate
metod, je-li to možné, aby i nadále mohl uživatel používat některou její funkcionalitu, ačkoli siget
metodu entity napíše sám a nevyužije při tom anotace?Já jsem zrovna narazil na to, že jsem potřeboval něco složitějšího a musel si tak napsat vlastní metodu v entitě, ale přitom jsem chtěl využít implicitní filtry.
Sestavování implicitních filtrů by pravděpodobně dekomponovat šlo, podívám se na to.
Narazil jsi v praxi ještě na něco, co jsi musel překopírovávat z
Entity
?
Jsem nakloněn „dekomponování v rámci možností“, přičemž bych se ale rád primárně zaměřil na ty části, u kterých to má reálný smysl.
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Tak se na to můžeš podívat v této větvi. Je tahle metoda tím, co jsi měl na mysli?
Pokud k tomu nebudeš mít žádné výhrady, nejspíš to začlením.
před 6 lety
- Tharos
- Člen | 1042
Napadla mě jedna taková věc…
Nyní existují metody Repository::createCollection
a také
„duplicitní“ Entity::createCollection
, což není vůbec
hezké a už nějakou dobu mě to štve. Jednoduchým řešením by samozřejmě
bylo vytvoření nějaké ICollectionFactory
, kterou by si každý
naimplementoval podle gusta, ale vzniklou „službu“ by zase bylo zapotřebí
všude možně injektovat a ono je to bez DI kontejneru docela otrava.
Dlouho nebylo, kam tu metodu „šoupnout“, ale došlo mi, že teď už
přece je: co takhle rozšířit IEntityFactory
o metodu
createCollection
? Ta by fungovala identicky jako současné
Repository::createCollection
či
Entity::createCollection
(ty bych samozřejmě zrušil).
Výhody
- Nepovede to k duplicitám v kódu
- Výhodně se využije již hotového injektování závislostí
- Zmizí „ošlivá“ template method z
Entity
aRepository
(každý eliminovaný výskyt toho vzoru považuji za bod k dobru)
Nevýhody
- BC break :)
Osobně bych s přítomností takové metody v IEntityFactory
neměl nejmenší problém, protože tam má svou logiku.
Editoval Tharos (2. 12. 2013 20:46)
před 6 lety
- Michal III
- Člen | 84
@Tharos: Mohl bych Tě poprosit o ukázku, jak by měl uživatel postupovat, kdyby chtěl zapsat závislost pomocí metody, ale stále využít implicitních filtrů? Já narazil ve zkratce zhruba na toto:
- Nejprve jsem si vytvořil entitu (
ContactPerson
), jejíž data byly ve dvou tabulkách (protože ta entity byla potomkem nějaké jiné (Person
) a v té druhé tabulce byly informace pouze pro potomka). Toto jsem tedy řešil v mapperu pomocí implicitních filtru, protože mi přijde, že tam je pro tyto situace to správné místo – vždycky chci vytvářet entitu způsobem přijoinování té další tabulky, ať již z repozitáře nebo traverzováním z entity. - Posléze jsem vytvořil jinou entitu (
Organization
), která měla obsahovat kolekci té první těchContactPerson
. Jenže ta vazba byla trochu komplikovanější a já nemohl využít pouze anotací, ale musel jsem si napsatget
metodu sám. - V tu chvíli jsem se ale musel sám postarat o „natahání“
implicitních filtrů, tak jsem se podíval do metody
Entity::__get
, abych věděl, jak na to, ale trochu jsem se tam ztratil a nevěděl jsem, co přesně a jak bych z té metody měl využít.
před 6 lety
- Šaman
- Člen | 2275
BC breaky bych neřešil. Kromě pár nadšenců je aktuální LM těžko pokročile použitelné (dokud nebude dokumentace), takže verze 2.x se klidně ještě může dopilovávat.
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Tohle bych viděl jako takové „základní řešení“:
<?php
namespace Model\Entity;
use LeanMapper\Filtering;
/**
* @property int $id
* @property-read ContactPerson[] $contactPersons m:useMethods
*/
class Organization extends \LeanMapper\Entity
{
public function getContactPersons()
{
$entityClass = $this->getCurrentReflection()->getEntityProperty('contactPersons')->getType();
$implicitFilters = $this->createImplicitFilters($entityClass); // načteme implicitní filtry
// získáme kolekci low-level řádků
// Row::referencing očekává instanci Filtering, musíme ji tedy sestavit z $implicitFilters
$rows = $this->row->referencing('contactperson', null, new Filtering($implicitFilters->getFilters(), null, $this, null, $implicitFilters->getTargetedArgs()));
$value = array();
foreach ($rows as $row) {
// každý low-level řádek „obalíme“ entitní třídou
$entity = $this->entityFactory->createEntity($entityClass, $row);
$entity->makeAlive($this->entityFactory);
$value[] = $entity;
}
// return $this->entityFactory->createCollection($value); // nejspíš v budoucnu, prozatím $this->createCollection($value)
}
}
Tento základ lze různě rozvíjet:
- Úplně můžeš vynechat tu anotaci
@property-read ContactPerson[] $contactPersons m:useMethods
, můžeš napsat jenom tu metodu. Pak si můžeš (respektive musíš) v mém kódu do$entityClass
přiřadit rovnou název třídy cílové entity. - V mé ukázce je například název tabulky
contactperson
„hard coded“. Pokud bys chtěl mít i takovouto metodu dokonale abstraktní, všechny tyhle detaily bys měl číst z mapperu. Je to trochu upovídanější, ale zase pak nefušuješ mapperu do řemesla. :) - Metodě
createImplicitFilters
se nepředává instanceCaller
, protože není povinná. Pokud by ses v mapperu při vracení implicitních filtrů potřeboval rozhodovat podle toho, kdo se ptá, instanciCaller
si při tom volání klidně vyrob (new Caller($this)
). Pokud ji nepotřebuješ, neřešil bych ji. Druhým argumentem té třídy je také relevantní property – klidně si tam můžeš dosadit potřebnou reflexi. - Také v ukázce neřeším žádné dynamicky předané argumenty filtrům.
Zase, pokud bys potřeboval, dají se do
Filtering
snadno vměstnat.
Je to srozumitelné?
Editoval Tharos (3. 12. 2013 7:35)
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Nicméně, dám sem ještě jednu podobu té metody:
<?php
namespace Model\Entity;
use LeanMapper\Caller;
use LeanMapper\Filtering;
/**
* @property int $id
* @property-read ContactPerson[] $contactPersons m:belongsToMany m:useMethods
*/
class Organization extends \LeanMapper\Entity
{
public function getContactPersons()
{
$property = $this->getCurrentReflection()->getEntityProperty('contactPersons');
$entityClass = $property->getType();
$implicitFilters = $this->createImplicitFilters($entityClass, new Caller($this, $property));
$relationship = $property->getRelationship();
$rows = $this->row->referencing($relationship->getTargetTable(), $relationship->getColumnReferencingSourceTable(), new Filtering($implicitFilters->getFilters(), null, $this, $property, $implicitFilters->getTargetedArgs()));
$value = array();
foreach ($rows as $row) {
$entity = $this->entityFactory->createEntity($entityClass, $row);
$entity->makeAlive($this->entityFactory);
$value[] = $entity;
}
return $this->entityFactory->createCollection($value);
}
}
Tohle řešení super výhodně spojuje sílu anotací a metod. Ty si
nadefinuješ vazbu v anotaci, čímž za Tebe PropertyFactory
určí název tabulky, vazebního sloupce, název cílové entity… a pak to
v té metodě jenom zužitkuješ.
Tohle řešení netrpí nedostatky, kterými trpělo předchozí řešení
(„hard coded“ názvy). Jelikož máš v rukou reflexi property, velmi
snadno se sestaví plnohodnotný Caller
i Filtering
(které také o reflexi té property má zájem, protože se eventuálně
může auto-wirovat do nějakého filtru).
No, doufám, že jsem názorně předvedl další silné stránky Lean Mapperu. I v těchto věcech je totiž velmi flexibilní.
Edit: Opravil jsem tunu překlepů v obou mých posledních příspěvcích. :)
Editoval Tharos (2. 12. 2013 23:32)
před 6 lety
- Tharos
- Člen | 1042
V rámci „dekompozice“ Entity::__get
jsem povýšil na
protected
i ty metody Entity::getHasOneValue
,
Entity::getHasManyValue
atp.
Takže nyní lze probíranou metodu ještě zjednodušit (rozumějte ještě
více využít toho, co už je v Entity
hotové):
<?php
namespace Model\Entity;
use LeanMapper\Caller;
use LeanMapper\Filtering;
/**
* @property int $id
* @property-read ContactPerson[] $contactPersons m:belongsToMany m:useMethods
*/
class Organization extends \LeanMapper\Entity
{
public function getContactPersons()
{
$property = $this->getCurrentReflection()->getEntityProperty('contactPersons');
$entityClass = $property->getType();
$implicitFilters = $this->createImplicitFilters($entityClass, new Caller($this, $property));
$filtering = new Filtering($implicitFilters->getFilters(), null, $this, $property, $implicitFilters->getTargetedArgs());
return $this->getBelongsToManyValue($property, $filtering);
}
}
Tímto považuji rozčlenění té __get
metody za dokončené.
Nebo by ještě něco dalšího mělo smysl?
před 6 lety
- Michal III
- Člen | 84
@Tharos: Ano, je to srozumitelné :-). Mnohokrát
děkuji za vysvětlení. Myslím, že co se týče rozčlenění té
__get
metody, takto je to opravdu dostačující. Pokud ještě
někdy na něco podobného narazím, určitě se ozvu. Ono má opravdu smysl
dekomponovat pouze to, co má smysl, a je asi opravdu těžké uvědomit si to
bez nějakého use case. Ještě jednou děkuji.
Ještě se zeptám, jestli má smysl měnit obsah těch
get<Name>Value
metod? Jestli by neměly být
final
.
před 6 lety
- Casper
- Člen | 253
K té __get
metodě bych jen rád připomněl,
že je při jejím přepisování nutné nezapomenout na hidden parametr pro
filtry.
public function __get($name) {
if (...)
// some code
} else {
$funcArgs = func_get_args();
$filterArgs = isset($funcArgs[1]) ? $funcArgs[1] : array();
return parent::__get($name, $filterArgs);
}
}
před 6 lety
- Tharos
- Člen | 1042
@Michal III:
Ještě se zeptám, jestli má smysl měnit obsah těch get<Name>Value metod? Jestli by neměly být final.
U nich už si nedokážu moc dobře představit, k čemu by bylo jejich přetížení dobré.
Kolem poledne jsem ještě provedl drobnou revizi… API těch
get<RelationshipType>Value
metod nebylo uzpůsobené k tomu,
aby se staly protected
. Předávalo se jim pár parametrů
z jakýchsi mikro-optimalizačních důvodů – $targetTable
i
$relationship
jde zjistit z Property
, ale optimalizace
spočívá v tom, že se to nezjišťuje znovu, když už se to zjišťovalo v
Entity::__get
… Z toho důvodu také bylo možné těm metodám
dodat nesmyslná data, například zavolat getHasOneValue
s
Property
s vazbou M:N… Což samozřejmě u neprivátních už
metod vadí.
A proto vznikla metoda
Entity::getValueByPropertyWithRelationship
, která je
protected
a tohle optimálně řeší. Až tahle metoda volá
relevantní get<RelationshipType>Value
, které prostě
zůstávají privátní.
Diskutovanou entitu lze tedy finálně zapsat následovně:
<?php
namespace Model\Entity;
use LeanMapper\Caller;
use LeanMapper\Filtering;
/**
* @property int $id
* @property-read ContactPerson[] $contactPersons m:belongsToMany m:useMethods
*/
class Organization extends \LeanMapper\Entity
{
public function getContactPersons()
{
$property = $this->getCurrentReflection()->getEntityProperty('contactPersons');
$entityClass = $property->getType();
$implicitFilters = $this->createImplicitFilters($entityClass, new Caller($this, $property));
$filtering = new Filtering($implicitFilters->getFilters(), null, $this, $property, $implicitFilters->getTargetedArgs());
return $this->getValueByPropertyWithRelationship($property, $filtering);
}
}
Což považuji za optimální výsledek. Všimněte si, že jediná „hard coded“ informace v té metodě je název property při získávání reflexe.
Entity::__get
velmi prokoukla. Jsem s její podobou vcelku
spokojen a víc už do těchto magických metod vrtat nebudu (__set
jsem vylepšoval nedávno, o tom sem taky ještě něco napíšu). To, co je
teď v develop
větvi, je de facto ready
k testování/používání. :)
Editoval Tharos (3. 12. 2013 14:00)
před 6 lety
- Tharos
- Člen | 1042
@Casper:
K té
__get
metodě bych jen rád připomněl, že je při jejím přepisování nutné nezapomenout na hidden parametr pro filtry.public function __get($name) { if (...) // some code } else { $funcArgs = func_get_args(); $filterArgs = isset($funcArgs[1]) ? $funcArgs[1] : array(); return parent::__get($name, $filterArgs); } }
Samozřejmě, díky za poznámku. Nicméně to, že by někdo přepisoval
celou __get
, nepředpokládám (těžko by ji napsal nějak
zásadně lépe). O co tu teď běželo je, jak nejlépe recyklovat dílčí
záležitosti z té nativní __get
metody ve vlastních
getter/setter metodách.
před 6 lety
- Michal III
- Člen | 84
@Tharos: Paráda, jsem rovněž spokojen. Děkuji.
před 6 lety
- Michal III
- Člen | 84
@Tharos: Pokud mám špatně pojmenovaný „sloupec
odkazující na cílovou tabulku“ ve vazbě hasMany
, skončí mi
výjimka na Undefined index
na řádku 714
v
LeanMapper\Result.php
. Nestálo by za to vytvořit vlastní
výjimku lépe vypovídající o tom, co je špatně?
Už několikrát jsem narazil na to, že mi LeanMapper vyhodil podobně nízkoúrovňovou výjimku, ze které jsem potom musel po problému pátrat, nicméně tentokrát jsem si uvědomil, že by bylo asi vhodné Tě o tom informovat. Takže jestli nic nenamítáš, tak bych tak od teď činil. Případně, až se o trochu více zorientuji, bych mohl alternativně posílat pull requesty.
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Je super, že o tom píšeš. Já totiž zavádějící chybové hlášky beru skoro jako bug a snažím se je kontinuálně eliminovat. Takže na tohle se určitě podívám.
Jinak mi s tím pomáhá Casper, od kterého mám na toto téma otevřenou issue. Jakmile v praxi narazíš na jakoukoliv chybovou hlášku, která není na první pohled srozumitelná, klidně mi o tom dej vědět.
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Tak, mělo by to být ošetřené. Nyní bys měl dostat plně srozumitelnou hlášku podobného znění:
Cannot get value of property 'tags' in entity Model\Entity\Book due to low-level failure: missing row with id 1 or 'nejakablbost' column in that row.
Upravil jsem (snad) vše tak, aby se tyhle nízkoúrovňové chyby alespoň „obalily vysokoúrovňovými“ s úvodem, u jaké property a v jaké entitě k chybě došlo.
před 6 lety
- Michal III
- Člen | 84
@Tharos: Skvělé! Děkuji :-).
před 6 lety
- Tharos
- Člen | 1042
Tak tahle moje myšlenka se právě dočkala implementace. Pozor na to, že je to BC break.
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Není zač. :) Už jenom pár drobností
a konečně otevřu release větev 2.1. Vlastně už mám na seznamu jenom
jedinou věc, a tou je dopilování m:passThru
příznaku…
Plus kdybyste ještě někdo něco měli na srdci do verze 2.1, sem s tím… :)
před 6 lety
- Pavel Macháň
- Člen | 285
Tharos napsal(a):
@Michal III: Není zač. :) Už jenom pár drobností a konečně otevřu release větev 2.1. Vlastně už mám na seznamu jenom jedinou věc, a tou je dopilování
m:passThru
příznaku…Plus kdybyste ještě někdo něco měli na srdci do verze 2.1, sem s tím… :)
Už se těším na release 2.1 :) Do kdy to plánuješ vydat (±)? Tedle týden začínám dělat nový projekt a chci to postavit na LeanMapperu 2.1 a zatím sem si tam nalinkoval „dev-develop“ balík. Bude ještě nějaký zásadní BC break? Pokud bude už jen dopilování m:passThru příznaku tak bych měl být v klidu že? ;-) :)
Editoval EIFEL (3. 12. 2013 23:30)