{"id":17312263,"url":"https://github.com/halaxa/elnino-domain-query","last_synced_at":"2025-07-15T06:36:56.664Z","repository":{"id":194522738,"uuid":"441275588","full_name":"halaxa/elnino-domain-query","owner":"halaxa","description":null,"archived":false,"fork":false,"pushed_at":"2023-03-16T12:56:00.000Z","size":84,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-01T06:27:43.631Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/halaxa.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-12-23T19:20:51.000Z","updated_at":"2023-03-16T12:56:58.000Z","dependencies_parsed_at":"2023-09-13T20:29:31.623Z","dependency_job_id":null,"html_url":"https://github.com/halaxa/elnino-domain-query","commit_stats":null,"previous_names":["halaxa/elnino-domain-query"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halaxa%2Felnino-domain-query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halaxa%2Felnino-domain-query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halaxa%2Felnino-domain-query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halaxa%2Felnino-domain-query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/halaxa","download_url":"https://codeload.github.com/halaxa/elnino-domain-query/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245761301,"owners_count":20667895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T12:42:54.547Z","updated_at":"2025-03-27T01:14:51.599Z","avatar_url":"https://github.com/halaxa.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"## _Čistě open sourcovaná knihovna zatím bez dalších úprav._\nDíky firmě elnino za opensourcování 23.12.2021\n\n# Doménové dotazy nad Doctrine 2\n\n## Myšlenka\n\nDoménovým dotazem máme na mysli znovupoužitelný kousek doménové logiky sloužící pro\ndotazování se nad databází pro data. Cílem je skrýt konkrétní implementaci dotazování\nv pojmenované a komponovatelné celky. Důvodem je redukce opakování stejných DQL podmínek,\nkteré vyjadřují určitý stav v doméně a také redukce chyby, která by mohla vzniknout\nnepřesným formulováním takové podmínky. Také docílíme vytknutí podmínek z repository metod\ndo samostatných komponovatelných objektů a repository tak nebude kynout jednoúčelovými metodami.\n\n## Motivace\n\nCílem je místo něčeho takového:\n\n```\nSELECT user\nFROM App\\Entity\\User user\nINNER JOIN user.todos todo\nWHERE user.active = 1\n    AND todo.done = 0\n```\n\nPoužívat něco takového:\n\n```php\n$userRepo-\u003ematch(new UsersWithPendingTodos);\n```\n\nA být třeba schopní kombinace logickými operátory:\n\n```php\n$userRepo-\u003ematch(\n    new AndX(\n        new NotX(new UsersWithPendingTodos),\n        new NotX(new UsersWithPendingProjects)\n    )\n);\n```\n\n## Jak na to\n\nZákladním stavebním kamenem tohoto systému jsou:\n\n 1. **vlastní specifikace**\n 2. **DefaultSpecificationRepository::match()**\n\n### DefaultSpecificationRepository\nRepository v názvu je jen zpola nesprávným označením toho, že tento objekt je naším výchozím bodem\npro komunikaci s databází. **Nedědí od `EntityRepository`**. Naopak zavádí koncept služby,\nkterá kromě vyzobávání objektů z úložiště umožňuje i jejich ukládání a mazání (obsahuje třeba `persist()`,\n`flush()`, `remove()` apod.). Podobně jako `EntityRepository` má nastavenou výchozí entitu,\nnad kterou pracuje. Poskytnout ji ovšem nemusíme:\n\n```php\n$userRepo = new DefaultSpecificationRepository($entityManager, User::class);\n$userRepo2 = new DefaultSpecificationRepository($entityManager);\n```\n\nO tom, co se stane, když ji neposkytneme, se dozvíme [dále](#entityclassproviderinterface).\n\nNení tedy třeba sahat si pro `EntityManager` kvůli persistenci entit a pro repository kvůli\njejich získávání (varianta tahání repository z `EntityManager` ani není hodná zmínění).\nStačí jedna služba na obojí. Přesto je tato vlastnost jen zpříjemněním práce a s\ndoménovými dotazy souvisí.\n\n### Píšeme vlastní specifikace\nVlastní specifikací označujeme implementaci `SpecInterface`. Jedná se o objekt, který\nv sobě zapouzdřuje podmínku doménové logiky a tu zveřejňuje pomocí jediné metody\n`expression()`. Ta vrací buď `SpecExpr` (jádro doménového výrazu) nebo opět specifikaci `SpecInterface`.\nSpecifikace by mohla vypadat nějak takto:\n\n```php\nuse Doctrine\\ORM\\Query\\Expr;\n\nclass ActiveUser implements SpecInterface\n{\n    function expression($alias = null)\n    {\n        $e = new Expr;\n        return new SpecExpr(\n            $e-\u003eeq(\"user.active\", ':active'),\n            [':active' =\u003e 1]\n        );\n    }\n}\n```\n\nObjekt `SpecExpr` zde voláme se dvěma parametry. Prvním je doctrinní výraz a druhým je mapa\n`:parametr =\u003e 'hodnota'`. Parametry bychom používat měli a to jak kvůli escapování tak kvůli\ncachování SQL dotazů, které Doctrine provádí. Specifikaci pak\npoužijeme v metodě `match()`, která má jako parametry DQL SELECT clause a specifikace. Protože jsme\nuvnitř naší specifikace použili natvrdo alias `'user'` musíme tuto specifikaci použít v metodě `match()`\nse stejným aliasem:\n\n```php\n$result = $userRepo-\u003ematch('user', new ActiveUser);\n```\n\nTento způsob je možný, avšak spíše nepohodlný. Museli bychom vědět jaké aliasy specifikace na své\npodmínky používá a ty v selectu použít. Řešením je parametr `$alias`, který je do metody `expression()`\npředáván a možná jste si ho v příkladu již všimli. Je to právě ten alias, který předáváme do metody `match()`.\nTa alias dále předává specifikacím právě jako argument do metody `expression()`. Pokud tedy použijeme volání,\njak je ukázáno v posledním příkladě, můžeme specifikaci upravit a použít tak alias, o kterém\nse rozhodne až v době volání `match()`:\n\n```php\nuse Doctrine\\ORM\\Query\\Expr;\n\nclass ActiveUser implements SpecInterface\n{\n    function expression($alias = null)\n    {\n        $e = new Expr;\n        return new SpecExpr(\n            $e-\u003eeq(\"$alias.active\", ':active'),\n            [':active' =\u003e 1]\n        );\n    }\n}\n```\nSpecifikace si tak nevynucuje alias, se kterým ji musíme použít a je univerzálnější. Alias v metodě `match()`\nje navíc nepovinný, takže specku v tomto příkladu můžeme klidně zavolat i následovně:\n\n```php\n$result = $userRepo-\u003ematch(new ActiveUser);\n```\n\nMetoda `match()` si v takovém případě alias vygeneruje z názvu třídy. Například z `\\App\\Entity\\User`\nudělá `user_`, což v rámci jednoho dotazu postačuje.\nKe každému automaticky vygenerovanému aliasu je přidán na konec symbol `_`, aby nedocházelo ke kolizi názvů\ns DQL operátory nebo funkcemi (třeba `order`), protože DQL neumožňuje escapování symbolů na úrovni jazyka.\n\nNakonec, pokud přímo nevyžadujeme názvy vlastních parametrů v DQL dotazu, je možné jako návratovou hodnotu\nve vlastní specifikaci použít specifikaci `Params`, která parametry generuje za nás a zápis dále zjednodušuje.\nNaše specifikace by pak mohla vypadat následovně:\n\n```php\nclass ActiveUser implements SpecInterface\n{\n    function expression($alias = null)\n    {\n        return new Params([\"$alias.active\" =\u003e 1]);\n    }\n}\n```\n\nProměnnou `$alias` nakonec nemusíme používat vůbec, protože se umí přidat sám. Stačí tedy:\n\n```php\nclass ActiveUser implements SpecInterface\n{\n    function expression($alias = null)\n    {\n        return new Params([\"active\" =\u003e 1]);\n    }\n}\n```\n\n### Generování aliasů\nAbychom mohli řadit přímo v metodě `match`, třeba specifikací `OrderBy` jako třeba v jednoduchém\npříkladu takto:\n\n```php\n$repo-\u003ematch('person', new PersonSpec, new OrderBy('person.age'))\n```\n\nmusíme znát alias cílové entity. Pokud je tako entita ale najoinovaná někde v naší spece a alias má vygenerovaný,\nmohl by to být problém. Tím víc, když se k kednomu aliacu může dojít dvěma cestami:\n\n`SELECT person FROM Person person JOIN p.ratings rating`\nnebo\n`SELECT person FROM Person person JOIN person.comments comment JOIN comment.ratings rating`\n\nPokud nechceme, nemusíme v `match` metodě ani v `Join` specifikaci žádné aliasy nikde specifikovat\na vždy se vygenerují samy. Aliasy přijoinovaných entit se generují tak, aby reflektovaly cestu,\nkterou k nim bylo dospěno a byly tak jednoznačné. Pokud bychom tedy měli svoji specifikaci `WithWellRatedComments`,\nkterá v sobě bude joinovat tak jak máme ve druhém příkladu, vygenerovaný alias bude `person_comments_ratings_` \n(z prvního příkladu by byl `person_comments_`).\nTen pak snadno použijeme, pokud budeme podle `Rating` chtít řadit:\n\n```php\n$personRepo-\u003ematch(new WithLastRatedComments, OrderBy('person_comments_ratings_.date DESC'))\n```\n\n### Join specifikace\nSíla a znovupoužitelnost specifikací je patrná z toho, že je můžeme použít jak pro omezení výběru\nprimární entity, tak pro omezení podle joinované entity aniž by o tom joinovaná specifikace věděla. Řekněme, že\nentita User kvůli separaci modulů neví o článcích. Když budu chtít vybrat články aktivních uživatelů a půjdu na to\ntím pádem ze strany článku, mohu přesto specifikaci `ActiveUser` použít. Pomůže nám dvojice specifikací\n`Join` a `LeftJoin`. Api mají stejné, rozdíl je jen ten, který je patrný z názvu:\n\n```php\n$articleRepo-\u003ematch('article'\n    new Join('article.user u',\n        new ActiveUser;\n    );\n);\n```\n\nPrvním parametrem je join řetětec nebo `JoinExpr`, ve kterém stačí jen property. Druhým\nparametrem (nepovinným) je `SpecInterface`, který chceme přijoinovat. Tím se nám otevírá cesta ke stromovému joinování\npřes více entit, neboť `Join` samozřejmě implementuje `SpecInterface`:\n\n```php\n$ratingRepo-\u003ematch('rating'\n    new Join('rating.article a',\n        new Join('a.user u'\n            new ActiveUser;\n        )\n    )\n);\n\nKvůli zjevnosti celého procesu je zde použit způsob, kdy můžeme aliasy předávat explicitně a naše specifikace\njej umí přijmout v konstruktoru. Každopádně joiny nám umožňují zjednodušení a do join řetězce aliasy nemusíme psát\naliasy částěčně nebo vůbec, protože se opět mohou vygenerovat. Tím, že zde stavíme dotaz stromově, nedojde ke kolizi\ni když budou všechny generované. Jediné, co join znát musí, je vlastnost entity, na kterou chceme joinovat.\nCelé se to pak dá zapsat i takto:\n\n```php\n$ratingRepo-\u003ematch(\n    new Join('article',\n        new Join('user'\n            new ActiveUser;\n        )\n    )\n);\n```\n\nS případným OrderBy podle vygenerovaného aliasu:\n\n```php\n$ratingRepo-\u003ematch(\n    new Join('article',\n        new Join('user'\n            new ActiveUser;\n        )\n    ),\n    new OrderBy('rating_article_user_.karma')\n);\n```\n\nVhodné je pak tento výraz zabalit do samostatné jedné specifikace, abychom si neznečisťovali uživatelský kód,\nmohli ji znovupoužít a třeba pomocí ní ovlivňovat i počet vrácených výsledků, způsob hydratace nebo fetch join.\nProstě stejně jako bychom dříve pro tento use case vytvořili metodu na repository:\n\n```php\nclass RatingsOfActiveUsers implements\n    SpecInterface,\n    QueryModifierInterface\n{\n    public function expression($ratingAlias = null)\n    {\n        return new Join('article',\n            new Join('user',\n                new ActiveUser\n            )\n        );\n    }\n\n    public function modifyQuery(Query $query)\n    {\n        $query-\u003esetHydrationMode(Query::HYDRATE_ARRAY);\n    }\n}\n```\n\nV metodě match se pak mohu rozhodnout o fetch joinu takto:\n\n```php\n$ratingRepo-\u003ematch('rating_, rating_article_', new RatingsOfActiveUsers);\n```\n\n### Operátory\nSpecifikace implementující `SpecInterface` můžeme z vnějšku kombinovat pomocí logických operátorů, které jsou také\nimplementacemi `SpecInterface`. Můžeme tak samozřejmě kombinovat jak naše specifikace tak hotové `Join`y nebo jiné\noperátory.\n\n```php\n$ratingRepo-\u003ematch(\n    new Join('article',\n        new Join('user'\n            new OrX(\n                new ActiveUser,\n                new RichUser\n            )\n        )\n    )\n);\n```\n\nnebo\n\n```php\n$ratingRepo-\u003ematch(\n    new AndX(\n        new WellRated,\n        new Join('article',\n            new Join('user'\n                new ActiveUser,\n            )\n        )\n    )\n);\n```\na tak dále ...\n\n\nPokud budeme ve specifikaci implementující `SpecInterface` implementovat navíc třeba `QueryBuilderModifier`,\nnesmíme spoléhat na správné vyhodnocení operátoru nad výrazy, které v naší specifikaci navěsíme na query builder\nručně a nevrátíme je pomocí `expression()`. Proto se také tento postup nedoporučuje.\n\n### Více o metodě `match()`\nJe to jediná metoda, kterou bychom se pro získávání dat měli snažit používat jak je vidět\nv příkladech výše. Parametry jsou nepovinné a při zavolání naprázdno (bez argumentů) vrátí metoda\nkolekci všech entit o které se daný repository stará (obdobně jako `findAll()`). První nepovinný\nparametr je seznam aliasů, které chceme načíst, stejně jako v klauzuli `SELECT` v DQL.\nPrvní z těchto aliasů je zároveň brán jako primární `FROM` alias. Dále je již počet argumentů\nvariabilní a sestává z vlastních specifikací, kterými chceme dotaz formovat. Pokud je\nspecifikací jako argumentů více, je mezi ně implicitně položen operátor `AND`.\n\nMetoda `match()` na našich specifikacích rozpoznává následující rozhranní:\n\n1. `EntityClassProviderInterface` Poskytuje FQCN entity, ke které se implementující specifikace váže.\n2. `SpecInterface` To už známe, vyjadřuje výraz identifikující doménový stav/podmínku\n3. `QueryBuilderModifierInterface` Bude mu předán přímo QueryBuilder k modifikaci (opatrně!)\n4. `QueryModifierInterface` Bude mu předán Query k modifikaci\n5. `ResultFetcherInterface` Zajistí získání dat z Query\n6. `ResultModifierInterface` Dostane výsledek dotazu (např. kolekci entit) opět k dodatečné modifikaci.\n\njejich klíčové metody jsou v tomto pořadí také volány.\n\n#### EntityClassProviderInterface\nPokud je implementováno, je třídě dána prioritra před tou, která je v Repository jako primární. Pokud Repository entity\ntřídu nemá, je implementování tohoto interface nutné.\n\n#### ResultFetcherInterface\nSlouží k získání výsledku/dat z Query. Jeho metodě `fetchResult()` je předán Query a její návratová hodnota je brána\nza výsledek dotazu. Odkaz na tento výsledek je pak dodatečně předán případné implementaci `ResultModifierInterface`.\n\n#### Explicitní aliasy\nExplicitně předávané aliasy do `match()` metody lze kombinovat se specifikacemi, které nastaví SCALAR nebo ARRAY mód\nhydratace a dosáhnout tím optimalizovaných read-only dotazů typu:\n\n```php\nclass HydrateArray implements ResultFetcherInterface {\n\n    public function fetchResult(Query $query)\n    {\n        return $query-\u003egetArrayResult();\n    }\n}\n\n$result = $userRepo-\u003ematch('user.id, user.name', new ActiveUser, new HydrateArray);\n```\n\n### Vestavěné specifikace\nNacházejí se v `Elnino\\DomainQuery\\Spec` a jsou standardními implementacemi výše uvedených rozhranní. Často se\npoužívají, případně ro bez nich ani pořádně nejde a proto si našly místo přímo v knihovně.\n\n#### IndexBy\nNastaví `INDEX BY` DQL klauzuli. Použití:\n\n```php\n$result = $repo-\u003ematch('order', new SomeOrderSpec, new IndexBy('order.id'));\n```\n\nPokud nebude result array jinak modifikován, budou výsledky v něm indexovány pod klíčem, který odpovídá hodnotě\nv IndexBy. Je to standardní chování Doctrine 2. Viz http://doctrine-orm.readthedocs.org/en/latest/tutorials/working-with-indexed-associations.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhalaxa%2Felnino-domain-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhalaxa%2Felnino-domain-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhalaxa%2Felnino-domain-query/lists"}