tiny ‘n’ smart
database layer

Odkazy: dibi | API reference

Oznámení

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

Lean Mapper – tenké ORM nad dibi

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

@castamir: +

před 5 lety

David Ďurika
Člen | 341
+
0
-

Feature request
Zdravim da sa v LM spravit nieco podobne ako Doctrine 2, ze si definujem vlastne typy pre propery?

Napr by som si spravil property: @property json $images alebo @property string|null $images m:type(json) (je mi jedno ako by sa to zapisovalo ide mi len o princip)

a potom by som definoval triedu:

<?php
class Json extends Type {

    public function convertToPHPValue($value) {
        return Nette\Utils\Json::decode($value, NU\Json::FORCE_ARRAY);
    }

    public function convertToDatabaseValue($value) {
        return Nette\Utils\Json::encode($value);
    }
}
?>

dik

před 5 lety

Casper
Člen | 253
+
0
-

@achtan:

Property samozřejmě může být objekt. Konverzí tam a zpátky snadno docílíš pomocí m:passThru.

před 5 lety

David Ďurika
Člen | 341
+
0
-

Casper napsal(a):

super! to som si nevsimol. dik

vlastne to az tak super nieje :) lebo teraz musim tie metody davat do BaseEntity pricom to tam nepatry a zacina narastat. idealne by tolo definovat callbacky

m:passThru(Model\Filter\Json::decode|Model\Filter\Json::encode)

Editoval achtan (16. 10. 2013 16:34)

před 5 lety

castamir
Člen | 631
+
0
-

@achtan a jak to pak chceš volat?

Zkus třeba vzít v úvahu

m:passThru(Model\Filter\Json::decode)

a v jiné property

m:passThru(\Special::decode)

Řekl bych, že nějaká ta střední cesta bude asi nejrozumnější

// entity
function decode($arg) {
    return \Model\Filter\Json::decode($arg);
}
function encode($arg) {
    return \Model\Filter\Json::encode($arg);
}

a k tomu

m:passThru(decode|encode)

před 5 lety

David Ďurika
Člen | 341
+
0
-

@castamir nechapem v com je problem, ved to je obycajny callbatck…

před 5 lety

Tharos
Člen | 1039
+
0
-

Ahoj,

omlouvám se své prodlení v reakcích tady i na GitHubu. Zastihla mě jedna časově náročná záležitost – stěhování…

Co se sandboxu týče, bude skvělé, pokud zkušení uživatelé Lean Mapperu něco dají k nahlédnutí. Ono totiž koncepce aplikace se dá pojmout hodně různě a nahlédnout pod pokličku každé je minimálně inspirativní.

Já aktuálně víceméně vycházím ze skeletonu, který mám na GitHubu. Vcelku mi vyhovuje. Obsahuje mimochodem hodně robustní komponentu na tvorbu navigace webu. Tu jsem napsal už před nějakou dobou pro účely jednoho historického CMS a i když je staršího data a dneska bych v ní možná něco řešil lehce jinak, špatná rozhodně není a má výrazně větší možnosti, než třeba komponenta od Honzy Marka. Ale to jen tak na okraj. :)

Jelikož s dokumentací se vleču (prostě mi ji nabouralo dokončování RD a stěhování), přemýšlel jsem, že bych taky vypustil nějakou ukázkovou aplikaci… Příklad vydá za tisíc slov a vyrobit takovou aplikaci je skoro snazší, než napsat jednu kapitolu do dokumentace. Ta má by byla nad aktuálním stable Nette a nad tím mým skeletonem.


@achtan: Podpora pro něco podobného není špatný nápad a rád to zvážím.

Faktem je, že pomocí passThru to nyní nějak řešit lze, ale není to úplně pohodlné.

Dokážu si představit, že by existovalo nějaké rozhraní LeanMapper\IType, které by definovalo právě metody pro získání „PHP hodnoty“ a pro získání „databázové hodnoty“. Pokud by Lean Mapper u položky zjistil, že je takového typu (instanceof LeanMapper\IType), zapouzdřil by hodnotu do instance požadovaného typu a pak by automaticky používal odpovídající metody toho typu.

Co myslíte, bylo by to vhodné řešení? Implementačně by to mělo být triviální a myslím, že by to mohlo přinést významnou přidanou hodnotu. Ještě bych to ale prověřil z hlediska implementace, jestli v tom není nějaký koncepční problém…


Issues na GitHubu se budu také ASAP věnovat.

před 5 lety

David Ďurika
Člen | 341
+
0
-

@Tharos
LeanMapper\IType – ano
metody pro získání „PHP hodnoty“ a pro získání „databázové hodnoty“. – ano
zapouzdřil by hodnotu do instance… – nie

necha sa to sprava ako jednoduchy filter… uprava hodnoty pred vlozenim, a tesne po vitiahnuti…

ako som pisal ak pracujem s polom tak jedine co chcem je decode a encode ziedny specialny objekt.

a to co navrhujes ty si uz kazdy spravi podla potreby, cize ak chcem specialny objekt tak si spavim:

<?php
class FooType implements LM\IType

function toPhpValue($arg) {
    return new MySuperObject($args);
}
function toSqlValue($arg) {
    return "$args";
}
?>

Cela tato vec co tu riesim, je vlastne len akysi databazovy typ nie php (to tvoje riesenie je skor ten php typ)

před 5 lety

castamir
Člen | 631
+
0
-

@achtan ber to jako možné řešení bez zásahu do LM jako takového…

před 5 lety

David Ďurika
Člen | 341
+
0
-

@castamir ty si nedas povedat:) precitaj si tharosov komentar potom moj a potom…

Editoval achtan (17. 10. 2013 16:30)

před 5 lety

tsusanka
Člen | 23
+
0
-

@Tharos díky moc za hezkou knihovnu.

Mám dotaz na filtry. Mám entitu Post a u té vazbu na Language. Rád bych získal všechny Post, které mají vazbu na Language s ID 1. Pokud tomu dobře rozumím, toto není místo kde využiju filtry, neboť ty se používají u hasMany vazeb. Měl bych funkci definovat na repozitáři.

$posts = $this->posts->findByLanguage($language);
// PostRepository
public function findByLanguage(Language $language)
{
    $fluent = $this->connection
            ->select('*')
            ->from($this->getTable())
            ->where('[language_id] = %i', $language->id);

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

Tohle funguje správně, ale není to moc praktické. V momentě kdy bych rád Post, které mají Language ID 1 a zároveň je třeba seřadit, tak bych na to musel napsat celou novou funkci. Můžeš ukázat, jak bych tohle mohl udělat elegantně?

Také jsem si všiml, že @Šaman zmiňoval dvojici funkcí get a find a přesně něco takového by se mi líbilo. Je správné řešení, že bych definoval pouze funkci byLanguage, která by vracela DibiFluent (v PostRepository?) a pak měl na BaseRepository magii, která bude fetchovat a vytvářet entity. Rozumím tomu dobře? Díky moc za radu.

před 5 lety

Tharos
Člen | 1039
+
0
-

@tsusanka: Jenom Tě trochu opravím – filtry se dají použít nejen u hasMany vazeb, ale u jakýchkoliv vazeb. Nicméně to, co ty potřebuješ, je skutečně něco trochu jiného.

Pokud se nechceš v podobných situacích upsat, dají se velmi dobře využít query objekty. Použiješ-li podobné abstraktní Repository a podobný abstraktní Query objekt, Tvůj kód poté může vypadat zhruba následovně:

class PostRepository extends \Model\Repository\Repository
{
}
$posts = $postRepository->findBy(
    (new Query)->restrict(['language' => $language])
);

Pokud bys výsledek chtěl i nějak seřadit, není nic jednoduššího:

$posts = $postRepository->findBy(
    (new Query)->restrict(['language' => $language])
        ->order('published')
);

Určitě bych se vyvaroval „vynesení“ DibiFluent ven mimo repositář. To by bylo zbytečné a řekl bych ne úplně „bezpečné“.

Editoval Tharos (18. 10. 2013 14:54)

před 5 lety

Ripper
Člen | 56
+
0
-

Zdravím všechny,

potřeboval bych poradit s jedním dotazem, ale nevím zda je to možné udělat tak jak chci. Mám dvě tabulky, jedna tabulka se jmenuje ‚shop‘(id, name, …) a druhá ‚open_time‘(id, shop_id, from, to, day). V tabulce open_time je otevírací a zavírací doba v určitý den. Přes leanmapper to mám propojený tak že k open_time mohu přistupovat přes $data->opentimes->… a to mohu prohnat přes foreach protože je tam více záznamů, ale já bych chtěl udělat abych tam měl pouze aktuální den a mohl k tomu přistupovat přímo, tedy $data->opentime->from, doufám že je to aspoň trochu srozumitelné.

Děkuji za rady :)

před 5 lety

Casper
Člen | 253
+
0
-

@Ripper:

Využij filtru, nějak takto:

/**
 * @property-read OpenTime $todayOpenTime m:belongsToOne m:filter(todayOpenTime)
 */
class Shop extends BaseEntity{}

// filter (jeho registraci snad zvládneš)
class OpenTimeFilter {
    public function today($statement){
        $statement->where("[day] = %s", date("N"));
    }
}


// potom tedy:
echo $shop->todayOpenTime->from;

Psáno z hlavy, kdyžtak dolaď chybky.

Editoval Casper (18. 10. 2013 16:23)

před 5 lety

Ripper
Člen | 56
+
0
-

@Casper:
Parád, díky :)

před 5 lety

tsusanka
Člen | 23
+
0
-

@Tharos Díky za odpověď. Nevím, jestli se mi úplně líbí tohle psát na presenterech, asi bych to měl radši vše v repository. Mohu si to však lehce upravit a v repozitářích vždy definovat:

public function findAllByLanguage()
{
  $posts = $postRepository->findBy(
      (new Query)->restrict(['language' => $language])
  );
}

ale furt se mi moc nelíbí, že při volání findAllByLanguageAndUser nebudu volat findByLanguage, ale napíšu si to znovu. A trochu se tím pádem opakuji. Ještě se nad tím hluboce zamyslim :).

před 5 lety

Tharos
Člen | 1039
+
0
-

@tsusanka:

Ad presentery) Debatu o tom bys našel někde v tomhle vláknu, ale já třeba v presenterech Query objekty nevytvářím. Vše mi řeší fasády nad repositáři, přičemž až sada těch fasádních tříd vytváří veřejné API modelu. Instance Query vyrábím právě v těch fasádních třídách.

Ad psaní metod) Pokud chceš mít jasně dané veřejné API modelu, nejpřímočařejší je ty metody napsat… Pokud nemáš problém s magickými metodami, lze napsat takovou implementaci __call, aby se Ti při volání findAllByLanguage automaticky vytvářel potřebný Query a předal se relevantnímu repositáři.

Každopádně… Ono by to šlo udělat to i tak, že by Ti nějaká modelová třída nevracela přímo kolekci (pole) entit, ale nějakou lazy záležitost, která by šla ještě zvenčí nějak doupravit a pak teprve z ní vytáhnout výsledek. Mohlo by to jít používat následovně:

$entities = $posts->findByLanguage($language)->orderBy('created')->limit(10)->fetchEntities(); // findByLanguage může být vyhodnoceno v __call, ta metoda nemusí vůbec existovat

Implementace by měla být velmi jednoduchá.

Je to fakt jenom otázka chuti a více možných přístupů, které lze nad Lean Mapperem aplikovat.


Zkusím i o tomhle něco málo říct na sobotní PoSobotě. :)

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

@Tharos: Tzn rekurzivní výpis stromu je nejlepší udělat fasadou, které předám $breadcumbs (drobečky) a sestavit strom ve fasadě?

Budeš na PS mluvit i o filtrech?

před 5 lety

Tharos
Člen | 1039
+
0
-

@jenicek: Určitě o nich mluvit budu, ale jelikož mám tak 40 minut prostoru, asi to bude fakt jenom úvod do problematiky… Ale koho by zajímalo cokoliv víc, určitě se večer zdržím.

Co se práce se stromovými daty týče, nedá se na to obecně odpovědět. Je X možností, jak taková data uchovávat v úložišti, a Y možností, jak je mapovat na objekty…

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

@Tharos: OK, udělám si čas a dorazím. Také by bylo zajímave zda jde s LM nějak lehce kešovat.

před 5 lety

Šaman
Člen | 2253
+
0
-

Nooou, tuhle sobotu mám asi poslední akci sezóny, na další už bych přišel, pokud by se tam probíral LM… Jestli bude možnost, tak se přimlouvám za nějaký záznam. Díky.

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

@Šaman: jestli nebudou mít lepší zkusim MobilCAM, bude ti stačit?

Editoval jenicek (22. 10. 2013 16:05)

před 5 lety

Jan Tvrdík
Nette guru | 2541
+
0
-

@jenicek: Ideální je záznam obrazovky přímo z toho notebooku, ze kterého se přednáší. Tomu se žádná kamera nevyrovná. Ta se hodí leda tak na dotazy diváků.

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

@Jan Tvrdík: OK

Editoval jenicek (22. 10. 2013 16:23)

před 5 lety

tsusanka
Člen | 23
+
0
-

@Tharos

Ještě otázečka, dá se nějak jednoduše z Repository zjistit o jakou entitu se jedná? Lépe: mám v baseRepository magické findBySomething (moc díky @Šaman za inspiraci) a rád bych zjistil zda-li ono Something je na entitě, které se to týká, definováno. Abych vyhodil nějakou výjimku pokud ne, případně s tím nějak dále pracoval. Je mi rozumět?

před 5 lety

Šaman
Člen | 2253
+
0
-

Do takto nízkoúrovňových funkcí nevidím, ale odmagičtění magických funkcí uděláš nejlépe přes ty anotace.
Samozřejmě, že když budeš hledat podle neexistující property, že ti to zařve, ale nenapadá mě co na tom ošetřovat. Je to chyba v kódu a vzhledem k závažnosti situace (načítání dat) bych ji považoval za non-recoverable error.

před 5 lety

Tharos
Člen | 1039
+
0
-

@tsusanka: Jde to docela snadno. Posílám superhrubý nástřel, jak to může fungovat:

class Mapper extends DefaultMapper
{

    protected $defaultEntityNamespace = null;

}

/**
 * @property int $id
 * @property string $name
 */
class Book extends LeanMapper\Entity
{
}

class BaseRepository extends LeanMapper\Repository
{

    public function __call($name, array $arguments)
    {
        $matches = array();
        if (preg_match('#^findBy([a-zA-Z]+)$#', $name, $matches)) {
            $field = lcfirst($matches[1]);
            $entityClass = $this->mapper->getEntityClass($this->getTable());
            $entityReflection = $entityClass::getReflection($this->mapper);
            $property = $entityReflection->getEntityProperty($field);
            if ($property === null) {
                throw new InvalidMethodCallException("Entity $entityClass doesn't have $field field.");
            }
            return $this->createEntities(
                $this->connection->select('*')
                        ->from($this->getTable())
                        ->where($property->getColumn() . ' = ?', reset($arguments))
                        ->fetchAll()
            );
        }
    }

}

class BookRepository extends BaseRepository
{
}

//////////

$bookRepository = new BookRepository($connection, new Mapper);

$books = $bookRepository->findByName('test');

Samozřejmě by to chtělo umět rozlišit findBy, findOneBy, findByNameAndAuthor atp., ale to vše je úplně přímočaře řešitelné.

před 5 lety

Šaman
Člen | 2253
+
0
-

Tak ukázkový projekt s využitím LM je zveřejněný, diskuzní vlákno je zde, ať to tu nespamujeme.

před 5 lety

David Ďurika
Člen | 341
+
0
-

Zdravim, Tharos v jeho prednaske spominal ze sa riesilo to ako rozdelit entitu do viac tabuliek, vie ma na to niekto odkazat, dakujem

před 5 lety

Pavel Macháň
Člen | 285
+
0
-

achtan napsal(a):

Zdravim, Tharos v jeho prednaske spominal ze sa riesilo to ako rozdelit entitu do viac tabuliek, vie ma na to niekto odkazat, dakujem

Viz https://forum.dibiphp.com/…orm-nad-dibi?p=11 Rozděluje se to tam kvůli překladům

před 5 lety

Tharos
Člen | 1039
+
0
-

Ahoj,

moc rád bych vám představil dvě zajímavé novinky v Lean Mapperu.

Zrevidované chybové hlášky

Na Casperův podnět jsem ještě jednou prošel všechny výjimky vyhazované na vyšší úrovni a zrevidoval jejich znění tak, aby bylo maximálně popisné. Pokud se nově pokusíte například přistoupit k neexistující položce published v entitě Model\Entity\Book, Lean Mapper vám sdělí:

Cannot access undefined property 'published' in entity Model\Entity\Book.

Anebo pokud se u stejné entity do položky author, která reprezentuje vazbu na autora knihy, pokusíte přiřadit textovou hodnotu „John Doe“, Lean Mapper vám oznámí:

Unexpected value type given in property 'author' in entity Model\Entity\Book, Model\Entity\Author expected, string given.

Pevně věřím, že odteď vás každá chybová hláška okamžitě zavede na problematické místo ve vašem kódu. Pokud byste ale přece jen na nějakou špatně srozumitelnou výjimku narazili, klidně mi dejte vědět a já ji mile rád opravím. Píšu opravím, protože špatně srozumitelné chybové hlášky považuji za bug. :)

IEntityFactory

Nápad nastíněný zde se dočkal implementace a světlo světa spatřilo rozhraní LeanMapper\IEntityFactory a jeho triviální defaultní implementace LeanMapper\DefaultEntityFactory.

Co to znamená?

Odteď Lean Mapper nikde nevytváří instance entit přímo (tj. zmizely všechny new $entityClass($row) atp.), nýbrž pomocí továrny. Zásadní přidanou hodnotou je, že v továrně lze entitě velmi elegantně (a díky Nette DI kontejneru i plně automatizovaně) předat různé závislosti.

Příklad místo slov
/**
 * Entity factory, která využívá Nette DI kontejneru k předávání závislostí pomocí auto wiringu
 */
class EntityFactory implements IEntityFactory
{

    /** @var Container */
    private $container;


    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * @param string $entityClass
     * @param Row|Traversable|array|null $arg
     * @return Entity
     */
    public function getEntity($entityClass, $arg = null)
    {
        $entity = $this->container->createInstance($entityClass, array($arg));
        $this->container->callInjects($entity);
        return $entity;
    }

}
/**
 * Triviální služba zaregistrovaná v DI kontejneru
 */
class Mailer
{

    public function sendHelloMail($recipient)
    {
        echo "Hello to $recipient!";
    }

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

/**
 * @property int $id
 * @property string $name
 * @property string $pubdate
 * @property Author|null $author m:hasOne
 */
class Book extends Entity
{

    /** @var Mailer */
    private $mailer;


    /**
     * @param Mailer $mailer
     */
    public function inject(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function sayHelloToAuthor()
    {
        if ($this->author !== null) {
            $this->mailer->sendHelloMail($this->author->email);
        }
    }

}
$container = $configurator->createContainer();

$entityFactory = new EntityFactory($container);

$bookRepository = new BookRepository($connection, $mapper, $entityFactory);

$book = $bookRepository->find(1);

$book->sayHelloToAuthor(); // vypíše „Hello to john@doe.com!“

Určitě si domyslíte, že závislost může být předána i standardně pomocí konstruktoru. Vlastně lze konstruktor úplně uvolnit jen pro předávání závislostí… Zkrátka fantazii se meze nekladou. :)

Možná si říkáte, k čemu je tohle dobré. Pokud se snažíte nemít úplně hloupé entity, k vyřešení řady záležitostí vám sice stačí pouhé traverzování po souvisejících entitách, ale dostanete se i do situace, kdy zkrátka potřebujete mít v entitě k ruce i nějakou službu. Neřešme teď prosím, jestli to je nebo není „správně“ a „co by entita měla/neměla“ – existuje více přístupů a jejich výhodnost záleží na kontextu.

BC break

Tahle novinka se neobešla bez BC breaku. Při „běžném použití“ Lean Mapperu kosmetického – repositáře kromě Connection a IMapper nově potřebují i IEntityFactory. Tak, jako exisutje DefaultMapper, existuje i DefaultEntityFactory, která je úplně triviální a kterou můžete bez jakýchkoliv okolků používat, pokud žádné závislosti do entity předávat nepotřebujete.

Pokud pracujete s Lean Mapperem na nižší úrovni, změn je trochu více (odstraněná metoda Entity::useMapper, nová metoda Entity::makeAlive…), nicméně pro pokročilejší uživatele by měly být snadno pochopitelné přímo ze změn v kódu. Kdyžtak ale rád cokoliv zodpovím.


Jak je vidět z posledních commitů, v souvislosti s touhle změnou jsem lehce zrefaktoroval i něco málo ve vnitřnostech knihovny. Šlo to velmi dobře a díky tomu jsou ony interní záležitosti Lean Mapperu zase ještě menší, přehlednější a paradoxně toho víc umožňují. :)


Mám teď v hlavě ještě jednu implementačně triviální, ale velmi praktickou záležitost, pak bych prošel issues na GitHubu… a asi vydal verzi 2.1.0.

Enjoy!

Editoval Tharos (9. 11. 2013 12:59)

před 5 lety

Marek Šneberger
Člen | 131
+
0
-

Ahoj, mějme 2 entity Order a Customer. každá objednávka patří k nějakému zákazníkovi (customer_id). V repository jsem si udělal metodu na vrácení seznamu objednávek (a k tomu jméno a příjmení zákazníka):
OrderRepository::getOrders

$orders = $this->connection->select('[order.*]')
            ->select('[customer.*]')
            ->from($this->getTable())
            ->join('customer')->on('[order.customer_id] = [customer.id]')
            ->join('tariff')->on('[order.tariff_id] = [tariff.id]')
            ->join('product')->on('[tariff.product_id] = [product.id]')
            ->where($search)
            ->fetchAll();
        return $orders ? $this->createEntities($orders) : false;
    }

Jenže LeanMapper mi dělá SQL dotaz navíc:

SELECT `customer`.*
FROM `customer`
WHERE `customer`.`id` IN (3)

Je nějak možné mu říct, aby to nedělal, když už ty data má v getOrders()? :D Není to zas tak akutní, nicméně raději bych si to sám najoinoval a nechal LeanMapper vytvořit entity, než pro každou vazbu přidal další dotaz. Těch vazeb je tam docela dost:)

Díky!

před 5 lety

Šaman
Člen | 2253
+
0
-

@Tharos: Super, čím dál lepší. Jen škoda, že na větší práci v týmu se mi zatím LM asi nepodaří prosadit. Jinak jak jsi mluvil o těch queryObjectech, můžeš někam nahodit ukázku? Rád bych to zpracoval a vytvořil ukázku s nějakým datagridem (případně máš už nějaký odzkoušený?).

Editoval Šaman (9. 11. 2013 14:30)

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:

  1. Implementaci IEntityFactory chválím, nicméně jak zmínil Jan Tvrdík, myslím, že metodě by více seděl název createEntity, jak už to tak u factories bývá.
  2. A jak to vypadá s dokumentací? Ne že bych jí osobně potřeboval, ale aby z toho projektu nevzniklo nezdokumentované cosi :)
  3. A ještě mám dotaz k tomu předávání závislostí přes konstruktor, které mám osobně raději: skutečně to bude fungovat? V konstruktoru Entity pořád vidím předávání Row, takže pokud nadefinuji ve své entitě závislosti v konstruktoru, interní vytváření entity s předáním Row mi neprojde přes typehint ne?
/**
 * @property int $id
 * @property string $name
 */
class Book extends Entity {

    private $mailer;

    public function __construct(Mailer $mailer) { // what about passing Row?
        $this->mailer = $mailer;
    }
}

před 5 lety

Tharos
Člen | 1039
+
0
-

@Casper:

Ad 1) Snad si to tu přečte i Honza Tvrdík. :)

Důvodů, proč jsem prozatím zvolil název getInstance, je víc. Předesílám ale, že na něm nijak netrvám.

  • Je to trochu věc konvencí. Třeba v Javě se lze na mnoha místech setkat s pojmenováním tovární metody getInstance (nikoliv createInstance). getInstance osobně hodně používám a v Lean Mapperu už takhle existuje Result::getInstance a Result::getDetachedInstance. S tím je konzistentní IEntityFactory::getEntity spíše než IEntityFactory::createEntity. Přiznám se, že pokud bych IEntityFactory upravil, asi bych upravil i Result, protože konzistence pro mě má velkou váhu.
  • Dokážu si představit situaci, kdy by IEntityFactory nevytvářela novou instanci, ale vrátila již nějakou existující. Třeba v případě Identity Map nadstavby. Pak by byl název createEntity zavádějící a obecnější getEntity by byl IMHO lepší. Faktem je, že pak by byl zavádějící i název IEntityFactory – co spíše přejmenovat tohle? :)

Ad 2) Vím, že už by Lean Mapper vážně chtělo povýšit na zdokumentované cosi. :) Nevíte někdo, jak se nafukuje čas? Je k tomu zapotřebí nějaké speciální vybavení?

Ad 3) Existuje více způsobů, jak se s Row (pozor, jde o Row|Traversable|array|null) vypořádat:

/**
 * @property int $id
 * @property string $name
 * @property string $pubdate
 * @property Author|null $author m:hasOne
 */
class Book extends Entity
{

    /** @var Mailer */
    private $mailer;


    /**
     * @param Mailer $mailer
     */
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    /**
     * @param Row|Traversable|array|null $arg
     */
    public function loadState($arg = null)
    {
        parent::__construct($arg);
    }

    public function sayHelloToAuthor()
    {
        if ($this->author !== null) {
            $this->mailer->sendHelloMail($this->author->email);
        }
    }

}
class EntityFactory implements IEntityFactory
{

    /** @var Container */
    private $container;


    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * @param string $entityClass
     * @param Row|Traversable|array|null $arg
     * @return Entity
     */
    public function getEntity($entityClass, $arg = null)
    {
        $entity = $this->container->createInstance($entityClass);
        $this->container->callInjects($entity);
        $entity->loadState($arg);
        return $entity;
    }

}

Anebo:

/**
 * @property int $id
 * @property string $name
 * @property string $pubdate
 * @property Author|null $author m:hasOne
 */
class Book extends Entity
{

    /** @var Mailer */
    private $mailer;


    /**
     * @param Row|Traversable|array|null $arg
     * @param Mailer $mailer
     */
    public function __construct($arg, Mailer $mailer)
    {
        parent::__construct($arg);
        $this->mailer = $mailer;
    }

    public function sayHelloToAuthor()
    {
        if ($this->author !== null) {
            $this->mailer->sendHelloMail($this->author->name);
        }
    }

}
class EntityFactory implements IEntityFactory
{

    /** @var Container */
    private $container;


    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * @param string $entityClass
     * @param Row|Traversable|array|null $arg
     * @return Entity
     */
    public function getEntity($entityClass, $arg = null)
    {
        $entity = $this->container->createInstance($entityClass, array($arg));
        $this->container->callInjects($entity);
        return $entity;
    }

}

U druhého řešení se mi moc nelíbí, že $arg zůstává jako první parametr, ale možná by se Nette DI kontejner vypořádal i s jiným pořadím? Z hlavy netuším… Spíš bych zvolil první řešení.

před 5 lety

Casper
Člen | 253
+
0
-

Díky za ukázku, trochu jsem to zvážil a nakonec se mi nejvíc líbí používání inject metod, abych nemusel při vytváření nových (detached) entit vkládat závislosti (tohle je blbost, stejně je třeba vytvářet nové entity pomocí factory, abych měl závislosti vždy k dispozici).


Nicméně narazil jsem na problém a nevím jestli je úmyslný nebo ne. Pokud traverzuji nad entitami, nepředává se jim automaticky IEntityFactory. Tedy v metodách getHasOneValue atd chybí volání makeAlive. Je to záměr a musím to provést uvnitř své EntityFactory, nebo jde o chybku? Ve tvém ukázkovém EntityFactory toto volání není.

$payment->user->/* zde nemá user IEntityFactory */address->city;

Editoval Casper (12. 11. 2013 21:59)

před 5 lety

Tharos
Člen | 1039
+
0
-

@Casper: To je samozřejmě moje chybka. IEntityFactory se o žádné makeAlive starat nemá.

Ještě dneska večer to fixnu. Díky za upozornění!

před 5 lety

castamir
Člen | 631
+
0
-

@Tharos sice nechceš, aby tu padla debata na téma, zda má mít entita závislosti, ale mě to prostě nedá. Tímto už jsi podle mě přestřelil…

Úkolem entity je abstrahovat data a ulehčit práci nad nimi a ne dělat proxy pro další služby…
Entita by rozhodně neměla umět odesílat emaily…

Pokud chceš posílat maily na základě dat entity, pak si udělej metodu ve službě na odesílání emailů, dej si tam správný typ parametru a pořeš si detaily tam…

Editoval castamir (13. 11. 2013 9:50)

před 5 lety

Casper
Člen | 253
+
0
-

@castamir

Řekl bych, že Mailer je špatný příklad, ale předávání závislostí entitám je dost užitečná věc (ač se o to tady lidi rádi přou zda je to správně či ne). Samozřejmě je na každém, co tam kdo narve, ale spousta věcí mi dává smysl: například cesta k souborům daných entit – podle mě by měla entita vracet své soubory (faktura své PDF atd).

Pokud tuhle cestu entita nezná, je dost opruz, když si musíš do šablony (nejen tam, prostě všude kde s tím souborem dané entity potřebuješ pracovat) předávat cestu k souboru, popřípadě ještě hůř tam předáš nějakou tu třídu, která cestu vrací. Srovnej:

{foreach $articles as $article}
  <img src="{$facade->getMainImagePath($article)}" /> {* fasáda zde nemá co dělat *}
  {$article->content}
{/foreach}
{foreach $articles as $article}
  <img src="{$article->mainImagePath}" />
  {$article->content}
{/foreach}

Tharos se o anemickém vs DDD modelu trochu rozepisoval v tomto vláknu.

Editoval Casper (13. 11. 2013 10:11)

před 5 lety

Michal III
Člen | 84
+
0
-

Ahoj, jak by se prosím dalo elegantně vyřešit následující:

V tabulkách v db by existoval sloupec deleted, který by určoval, že položka je smazaná. Nebyla by tedy smazaná kvůli tomu, že nějaké položky by na ní byly závislé. Řekněme třeba, že by to byla adresa, na kterou ukazují nějaké dopisy z tabulky dopisů, nicméně adresa již není platná, tak by jí byl nastaven příznak deleted.

A teď tedy jak elegantně vyřešit, aby jak repozitář těchto „adres“ vracel jen aktuální adresy (tomu samozřejmě rozumím, jak si upravit vlastní funkci v repozitáře), tak stejným způsobem i entity, které jsou vázané na tyto adresy, vracely také jen aktuální. Například entita Person by vracela v položce $person->addresses jen ty aktuální.

Řešit to také s přihlédnutím na to, že takovýchto tabulek se sloupcem deleted bude více.

Děkuji.

před 5 lety

Casper
Člen | 253
+
0
-

@Michal III:

Použij filtry:

Entita:

/**
 * @property Address[] $addesses m:belongsToMany m:filter(notDeleted)
 */
class Person extends \LeanMapper\Entity {}

Filtr:

class AddressFilter {
    public function notDeleted(\LeanMapper\Fluent $statement){
        $statement->where("deleted = FALSE");
    }
}

Repository:

class AddressRepository extends \LeanMapper\Repository {
    public function getAll(){
        return $this->createEntities(
            $this->connection->select('*')
                    ->from($this->getTable())
                    ->applyFilter("notDeleted")
                    ->fetchAll()
            );
    }
}

Editoval Casper (13. 11. 2013 11:16)

před 5 lety

Tharos
Člen | 1039
+
0
-

@Michal III: Ha, úplně ses trefil do featury, na které právě pracuji. :) Obrysy už jsou na GitHubu. Jenom ale ještě potřebuji pár věcí poladit… Dej mi prosím ještě den dva času.

Pak Tvůj problém (mimo jiné) půjde vyřešit elegantně a úplně triviálně (jak je tomu v Lean Mapperu zvykem). :)

Jsem rád, že jsi přímo na tohle natrefil, protože aspoň budu mít pro tu novou funkcionalitu hezkou ukázku. :)

před 5 lety

Tharos
Člen | 1039
+
0
-

@Michal III, @Casper: Přesně tak, vede to k použití filtrů. Jenom ale v současné době se pak použití toho filtru musí uvádět na mnoha místech – v repositáři a ve všech entitách, které mají na tu „filtrovanou“ vazbu.

No a to se právě změní. Nově půjde v mapperu říct, že při načítání určité entity se vždy mají aplikovat vybrané filtry, a pak už jej nebude nikde zapotřebí explicitně uvádět. Samozřejmě v anotacích atp. k tomu půjde přidávat další filtry a tak dále.

před 5 lety

Tharos
Člen | 1039
+
0
-

@castamir:

Úkolem entity je abstrahovat data a ulehčit práci nad nimi a ne dělat proxy pro další služby…
Entita by rozhodně neměla umět odesílat emaily…

Přímo odesílat určitě ne, ale podle DDD se takový use case může odehrávat přes API entity (entita pak samozřejmě to odeslání deleguje).

Pokud chceš posílat maily na základě dat entity, pak si udělej metodu ve službě na odesílání emailů, dej si tam správný typ parametru a pořeš si detaily tam…

Metodu ve službě na odesílání mailů s parametrem typu Entity rozhodně budeš mít, ale klíčové je, kdo takovou metodu bude volat. Jestli entita, anebo nějaká servisní třída.

Odkážu i tady na jeden hezký článek. Takovou servisní třídu by Ti autor toho článku označil za nosy class (it occupies itself too much with the activities of another class). Proti jeho argumentu se dá těžko něco namítnout :), je to pravda.

Jak už jsem psal, to, co by entita měla a neměla záleží na přístupu. V DDD entita může řadu věcí, které v jiných přístupech nemůže

Já se nechci stavět na žádnou stranu, protože mi přijde, že oba přístupy mají svá pro i proti. Sympatizuji ale s tím nemít entity úplně hloupé, byť nic se nemá přehánět.


Důležité je pro mě to, aby Lean Mapper pohodlně umožnil implementaci obou přístupů.

Editoval Tharos (13. 11. 2013 11:30)

před 5 lety

Michal III
Člen | 84
+
0
-

@Casper, @Tharos: Děkuji za rychlé odezvy. (…tak na tohle jsou ty filtry…). Tak na ten nový koncept už se těším :-).

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:

Měl bych ještě jeden dotaz k těm DI EntityFactory. Jelikož by se nová entita po perzistenci měla chovat totožně jako při získání z databáze, je potřeba při využívání továren nějak vyřešit závilosti. Napadají mě tři řešení, nicméně každé má své úskalí:

  1. Vytvářet vše pomocí univerzální továrny:
$user = $factory->getEntity("\Model\Entity\User");
$userRepo->persist($user);

Což mi přijde rozhodně nejsprávnější řešení, nicméně osobně považuji napovídání od IDE za hodně důležité a v tomto případě o něj přicházím. Můžu si pomoct pomocí /* @var $user User */, nicméně to je docela otrava tohle psát u každé vytvářené entity. A navíc např. NetBeans upřednostňuje return anotaci u metody getEntity, takže s ní stále napovídá pouze pro Entity.

  1. Vytvářet vše pomocí specifických továren typu UserFactory. Jenže to je zbytečené psaní kódu jen proto, aby mi IDE napovídalo. Výhoda je ovšem v tom, že pokud bych v budoucnu potřeboval diverzifikovat vytváření různých entit různými factory, mám vše připravené (problém by asi byl u vytváření vztahových entit).
  2. Vytvářet vše pomocí new a nějak zajistit, aby se po persistenci předaly závislosti. Jenže to mi nepřijde moc čisté, abych si do BaseRepository dal něco ve stylu:
public function persist(\LeanMapper\Entity $entity) {
    parent::persist($entity);
    $this->entityFactory->injectDependencies($entity);
}

Jelikož repository se nemá starat o nějaké závislosti, od toho tu je moje EntityFactory. Navíc by se čistě teoreticky mohlo stát, že závislosti budu potřebovat dříve.


Možná je to relativně malichernost, ale jaké řešení bys volil ty? Bohužel mi jako nejlepší pořád vychází první možnost s psaním napovídacích anotací.

Editoval Casper (13. 11. 2013 12:44)

před 5 lety

castamir
Člen | 631
+
0
-

Casper napsal(a):

{foreach $articles as $article}
  <img src="{$facade->getMainImagePath($article)}" /> {* fasáda zde nemá co dělat *}
  {$article->content}
{/foreach}
{foreach $articles as $article}
  <img src="{$article->mainImagePath}" />
  {$article->content}
{/foreach}

Tharos se o anemickém vs DDD modelu trochu rozepisoval v tomto vláknu.

Na tohle používám vlastní makra

{foreach $articles as $article}
    <img ai:src="{$article->id}" />
    <!-- ai je prefix pro article, ale muze tam byt i obecnejsi -->

    {$article->content}
{/foreach}

Makro si nadefinuju v/poblíž služby a jen zaregistruju

Editoval castamir (13. 11. 2013 13:00)

před 5 lety

Tharos
Člen | 1039
+
0
-

@Casper: Přesně tohle jsem dneska v noci v jednom svém projektu řešil. :)

Zvolil jsem řešení 3), jen s tím rozdílem, že předání závislostí neřeším přímo v repositáři, ale využívám k tomu událost EVENT_AFTER_CREATE. Přesně v té události se dají entitě krásně předat závislosti, aniž by se tím musel zabývat přímo repositář (entita se předává jako parametr handleru).

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:
A kde ten callback napojíš? Ideálně by se mi to líbilo v configu. Rozchodil jsem pouze nepěkné:

userRepo:
    class: Model\Repository\UserRepository
    setup:
        - "$service->onAfterCreate[] = ?"([@container, "callInjects"])

Což by navíc vyžadovalo dědičnost služeb. Předpokládám tedy, že máš nějaké hezčí řešení :) Nebo máš prostě ten můj kód v initEvents?

protected function initEvents() {
    parent::initEvents();

    $this->onAfterCreate[] = function($entity) {
        $this->entityFactory->injectDependencies($entity);
    };
}

@castamir:
Pořád to neřeší ten obecný problém, že data entity (soubor) spravuješ jinde. Stejně musíš ten soubor všude tahat s sebou:

public function orderFinished(Order $order){
    $invoice = $this->getInvoicePath($order);
    $this->mailer->sendInvoice($order, $invoice);
}

vs.

public function orderFinished(Order $order){
    $this->mailer->sendInvoice($order);
}

Osobně mi více vyhovuje mít tohle v entitách, pak mám vždy jistotu, že se k souborům dostanu snadno a rychle a nemusím kvůli tomu předávat závislosti / parametry / vytvářet makra.

před 5 lety

castamir
Člen | 631
+
0
-

@Casper to, co děláš ty, způsobuje ještě víc problémů… krom toho, že to narušuje single responsibility principle, tak se

  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?)
  2. musíš u mnoha služeb testovat, zda pracuješ s persistovanou entitou, protože ti volání metod těchto služeb může spadnout… příkladem může být entita na správu dokumentu, která má property user jako klíč do tabulky uživatelů… tato entita ti mimo jiné vycucá z prstu (čti z jiné služby) získá např. nějakou šablonu PDF. Ty pak zavoláš „tisk“ PDF, aniž bys měl uloženou tuhle entitu a nedejbože se ještě dožadoval získání properties z entity uživatele… ošetření těchto případů je pak mnohem robustnější – ničeho tím neušetříš, protože tohle ošetření musíš stejně zduplikovat u oné služby… je to tedy i porušení DRY
  3. to, že si Tharos a možná pár dalších 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…

Editoval castamir (13. 11. 2013 14:36)

Stránky: Prev 1 … 13 14 15 16 17 … 23 Next RSS tématu