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 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 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.

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 Nette\Forms\Container::getValues() v Nette muselo fungovat přes reference…(není nejlepší příklad)

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 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?

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 konstrukci return $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í:

  1. Jednotlivá kritéria jako parametry: getUsers($username = NULL, $lastActivity = NULL..
  2. 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 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.

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 a Repository (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ěch ContactPerson. Jenže ta vazba byla trochu komplikovanější a já nemohl využít pouze anotací, ale musel jsem si napsat get 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á instance Caller, protože není povinná. Pokud by ses v mapperu při vracení implicitních filtrů potřeboval rozhodovat podle toho, kdo se ptá, instanci Caller 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)

Stránky: Prev 1 … 15 16 17 18 19 … 23 Next