Oznámení
před 6 lety
- Šaman
- Člen | 2275
Souhlasím s tím, že entita funguje jako rozhraní.
Nechci po ní, aby mi nějak složitě zjišťovala název sloupce, jen aby mi
vrátila nízkoúrovňový název property (tedy ten, který je uveden za ní
v závorce a pokud není vyplněn, tak originální název po průchodu
případnými konvencemi).
Tedy stejně jako navrhuješ $property->getExtra()
, tak by mohlo
existovat $property->getRowName()
, resp.
$entity->getRowName('author')
(vrátí
'author_id'
).
před 6 lety
- Tharos
- Člen | 1042
Šaman napsal(a):
Tedy stejně jako navrhuješ
$property->getExtra()
, tak by mohlo existovat$property->getRowName()
To už existuje, použití viz tady.
OK, tímhle tuhle věc považuji za aktuálně vyřešenou.
před 6 lety
- Tharos
- Člen | 1042
@Filip111: Tak jsem ten příznak
m:extra
zavedl.
Hodnotou toho příznaku může být cokoliv, kromě pravé závorky (nechce se mi složitě parsovat úroveň zanoření závorek ve výrazu… snad časem, kdyby někdo cítil potřebu).
Nyní si můžeš vyrobit pro lokalizované entity něco na způsob následujícího základu:
abstract class BaseEntity extends \LeanMapper\Entity
{
const LANG_CS = 'cs';
const LANG_EN = 'en';
const TRANSLATE = 'translate';
private $translatableColumns;
public function readTranslatableColumns()
{
if ($this->translatableColumns === null) {
$this->translatableColumns = array();
foreach ($this->getReflection()->getEntityProperties() as $entityProperty) {
if ($entityProperty->getExtra() === self::TRANSLATE) {
$this->translatableColumns[] = $entityProperty->getColumn();
}
}
}
return $this->translatableColumns;
}
}
Konkrétní entita pak může vypadat následovně:
/**
* @property int $id
* @property string $lang m:enum(parent::LANG_*)
* @property string $name m:extra(translate)
* @property string|null $description m:extra(translate)
* @property string $isbn
*/
class Book extends BaseEntity
{
}
A v repositáři pak stačí při persistenci použít už jen
$book->readTranslatableColumns()
.
Teď už by mělo jít vytvořit základ pro ty lokalizovatelné entity opravdu snadno a stručně.
Editoval Tharos (19. 6. 2013 22:47)
před 6 lety
- Filip111
- Člen | 244
@Tharos:
snažm se to vyzkoušet, ale mezitím jsem narazil na další potíž – chci
entitu propojit na číselník jazyků.
Ty jsi to naznačil zjednodušeně pomocí konstatnt (mj. moc pěkná konstrukce
LANG_*
), já to mám v tabulce languages a primárním klíčem je
code
. (u všech číselníků, kde je primárním klíčem text to
označuji code, takže se nejedná jen o jednu tabulku).
No a to je problém, protože ty to spojuješ pokaždé přes id.
https://github.com/…r/Result.php#L407
Ještě doplním definici property:
* @property Language $lang m:hasOne(lang:languages) m:extra(translatable)
Mj. mě zajímá jak se zachová propojení na číselník jazyků v kombinaci s m:extra(translatable)..snad dobře.
Editoval Filip111 (20. 6. 2013 9:22)
před 6 lety
- Tharos
- Člen | 1042
@Filip111: Tohle vyřeší vlastní konvence, které
jsou v plánu.
Bohužel to, že tabulka uchovávající data nějaké entity musí mít
primární klíč id
, je nyní jediná zadrátovaná věc
z konvencí, kterou nelze změnit.
Je to víceméně relikt z doby, kdy jsem ještě nezamýšlel ORMko uvolnit
a bylo v podstatě na míru mým konvencím. Já totiž i v číselnících
používám jako primární klíč id
. Dočasně je tedy nutné tu
tabulku upravit… Anebo mít přechodně ten číselník redundantně ještě
v nějaké BaseEntity
v podobě konstant. Nebo snad přes
nějaký dočasný pohled…
Dobrou zprávou je, že tohle ty konvence elegantně vyřeší a v noci na dnešek v mé hlavě vzaly za své poslední drobné implementační otazníky. Takže by měly spatřit světlo světa brzy.
Editoval Tharos (20. 6. 2013 9:57)
před 6 lety
- bauer01
- Člen | 31
Ahoj,
moc pěkná práce, klobouk dolů, zkusil jsem jej jen tak letmo použít na
něčem menším a pracuje se s ním fakt pěkně. Co takhle využít
milestones a issues na githubu a přehodit tam věci z roadmapy? Lépe by se
sledovalo dění kolem vývoje ;-)
před 6 lety
- Tharos
- Člen | 1042
Ahoj, díky za odezvu. Na milestones na GitHubu se rád podívám. Aspoň bych se s nimi naučil pracovat. ;)
Co nevidět mě zase čekají práce na webu a dokumentaci, a tak určitě zvážím, jak nejlépe by šel GitHub využít.
před 6 lety
- Filip111
- Člen | 244
@Tharos:
Ok, budu se těšit na vlastní konvence.
Sorry, ale začal jsem s tím trochu pracovat, takže jsem narazil na
další..dotaz/chybu?
Zkoušel jsem to i na tvém příkladu s překlady http://www.leanmapper.com/…lip111_2.zip
$page = new Page;
$page->dateCreated = '2012-01-01 12:00:00';
echo $page->lang;
$page->lang = 'sk';
$page->text = 'slovenský obsah stránky';
Vytvořím novou entitu a aniž bych nastavil property lang, chci z ní
číst. Předpokládal bych, že se mi vrátí null, ale skončí to
výjimkou Missing 'lang' value for requested row.
To je podle mě docela vážná chyba – entita by se měla tvářit jako konzistentní objekt, který má vždy k dispozici všechny svoje property, bez ohledu na to jestli jsou prázné či naplněné.
před 6 lety
- Tharos
- Člen | 1042
@Filip111: Tohle je záměr. Silně si za tímhle chováním stojím a rád to vysvětlím.
Pokud entita obsahuje nějakou neinicializovanou položku, pokus o její
přečtení IMHO má skončit chybou. Pokud by taková akce
vrátila null
, je nemožné poznat, jestli je v položce uložená
skutečně hodnota null
, anebo jestli jen položka není
zinicializovaná.
Představ si, že se například překlepneš v nějakém mapování a
Row
, který entitě předáš, bude namísto
description
obsahovat decsription
. Pak by volání
$book->description
vracelo null
a to by nesmírně
mátlo (protože v databázi by description
bylo).
Já také chci, aby entita byla za všech okolností konzistentní objekt.
A proto vyžaduji, aby všechny položky, které se někdo snaží číst, byly
zinicializované a nedocházelo k různým překvapením. Kdo potřebuje
pracovat jen s nějakou podmnožinou entity, lze k tomu využít dědičnost
(jako jsem tu uváděl
příklad s Article
a ArticlePreface
).
Kdyby PHP znalo undefined
, tady by pro něj bylo ideální
využití…
Edit: Ještě přihodím jeden argument pro současné chování :).
/**
* @property string $name
*/
class Book extends Entity
{
}
$book = new Book;
$name = $book->name;
Co by měla obsahovat proměnná $name
? null
?
A není to logický nesmysl, když přece položka $book->name
podle definice null
obsahovat nemůže (není typu
string|null
)? :)
Editoval Tharos (20. 6. 2013 14:25)
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
právě jsem otevřel release větev v1.4.0.
Přehled všech změn, které se odehrály od poslední stable verze 1.3.0, jsou na webu knihovny.
Z webu je patrné, že jsem ty persistence M:N vazeb posunul až za podporu vlastních konvencí, protože udělat ty konvence jako první dává lepší smysl (protože ta persistence také bude konvence respektovat).
Kdybyste kdokoliv měli v rukávu nějaký bug nebo chtěli ještě něco drobného do verze 1.4.0 začlenit, teď je chvíli prostor. Prostě než to mergnu do masteru a otaguju, čímž verzi 1.4.0 „formálně“ vydám.
před 6 lety
- Filip111
- Člen | 244
@Tharos:
Toho jsem se přesně bál…že si to zase odůvodníš :)
Co by měla obsahovat proměnná $name
? null…nemám s tím
problém, to že podle definice nesmí obsahovat null vnímám tak, že mi
nepůjde nad touto entitou persist, protože name
nesmí obsahovat
null (a pravděpodobně i v DB je definice sloupce NOT NULL
)
Nedalo mi to a zkusil jsem Doctrine2. Vytvořím entitu, všechny její prvky
se mi vrátí jako null, pokud nejsou nastavené. Zkoušel jsem ještě nastavit
jednomu prvku aby byl not null, ale úplě to ignoruje. Když mu nastavím
nullable=false
tak mi stejně dovolí vložit do něj null
i uložit (padne to až na vyjímku při ukládání, protože v DB je sloupec
nastaven na not null). Nejsem v Doctrine2 tak zdatnej, možná dělám
něco blbě.
Já bych tedy preferoval, aby se vracelo u neinicializovaných prvků null. V případě property, kde null není povoleno jen zkontrolovat před uložením do DB, zda je null.
Každopádně null hodnota u neinicializovaných prvků mi přijde
praktická (a člověk je na to celkem zvyklý). Běžně si vytvořím ve
fasádě entitu (a je úplně jedno jak vznikla jestli načtením z DB nebo
vytvořením z formuláře) a pošlu ji dál ke zpracování. Následné funkce
vůbec neřeší, jak vznikla a nějak s ní pracují dál.
Jednoduše stylem:
if ($entity->cat) ...
Pokud se ale bude jednat o novou entitu s některými neinicializovanými prvky, bude to peklo a každou chvíli někde vyskočí výjimka. A místo jednoduchých ifů dávat všude catch mi přijde docela nesmysl.
před 6 lety
- Michal Vyšinský
- Člen | 614
Ahoj,
Když mu nastavím nullable=false tak mi stejně dovolí vložit do něj null i uložit (padne to až na vyjímku > při ukládání, protože v DB je sloupec nastaven na not null). Nejsem v Doctrine2 tak zdatnej, možná dělám > něco blbě.
Atribut „nullable“ v anotaci @Column se bere v potaz pouze při vytváření schématu (schémata ?) databáze z entity. Jakoukoliv následnou vstupní validaci si musíš už napsat sám.
před 6 lety
- Tharos
- Člen | 1042
Filip111 napsal(a):
Toho jsem se přesně bál…že si to zase odůvodníš :)
:)
Co by měla obsahovat proměnná
$name
? null…nemám s tím problém, to že podle definice nesmí obsahovat null vnímám tak, že mi nepůjde nad touto entitou persist, protožename
nesmí obsahovat null (a pravděpodobně i v DB je definice sloupceNOT NULL
)
Není to bohužel tak bezproblémové, jak se na první pohled zdá. Já osobně entity hojně využívám k vyjádření nějaké složitější business logiky. A považ třeba jen následující kód:
/**
* @property bool $banned
* @property bool $hasPaid
*/
class Visitor extends Entity
{
public function canViewArticle()
{
return !$this->banned and $this->hasPaid;
}
}
$visitor = new Visitor;
if ($visitor->canViewArticle()) {
// Kdo ví, jestli může! S neinicializovanými položkami je loterie, kdo touhle podmínkou projde.
}
Tohle je dokonalé podhoubí pro velmi zákeřné chyby. Nehledě na to, že ta logika může být mnohem spletitější.
Nedalo mi to a zkusil jsem Doctrine2. Vytvořím entitu, všechny její prvky se mi vrátí jako null, pokud nejsou nastavené. Zkoušel jsem ještě nastavit jednomu prvku aby byl not null, ale úplě to ignoruje. Když mu nastavím
nullable=false
tak mi stejně dovolí vložit do něj null i uložit (padne to až na vyjímku při ukládání, protože v DB je sloupec nastaven na not null). Nejsem v Doctrine2 tak zdatnej, možná dělám něco blbě.
To je ale z mého pohledu nedostatek v Doctrine (i když tohle mé
hodnocení je povrchní – určitě to tam půjde nějak hezky řešit).
Prostě když mám property, která nesmí být null
, nesmím při
jejím čtení dostat null
.
Tady jde i o kód, který s tou získanou hodnotou dále pracuje a třeba
na deklarovaný typ spoléhá. To mám pak u položek, které mají v definici
řečeno, že nemohou obsahovat null
, ještě testovat, jestli ho
náhodou přece jenom neobsahují?
/**
* @property Author $author
*/
class Book extends Entity
{
}
$book = $bookRepository->find(1);
echo $book->author->name; // Trying to get property of non-object in…
Já bych tedy preferoval, aby se vracelo u neinicializovaných prvků null. V případě property, kde null není povoleno jen zkontrolovat před uložením do DB, zda je null.
Každopádně null hodnota u neinicializovaných prvků mi přijde praktická (a člověk je na to celkem zvyklý).
Tohle je ale podle mě prostě špatný návyk.
Běžně si vytvořím ve fasádě entitu (a je úplně jedno jak vznikla jestli načtením z DB nebo vytvořením z formuláře) a pošlu ji dál ke zpracování. Následné funkce vůbec neřeší, jak vznikla a nějak s ní pracují dál.
Jednoduše stylem:if ($entity->cat) ...
Pokud se ale bude jednat o novou entitu s některými neinicializovanými prvky, bude to peklo a každou chvíli někde vyskočí výjimka. A místo jednoduchých ifů dávat všude catch mi přijde docela nesmysl.
Na místě, kde vytváříš entitu, je Tvou zodpovědností dostat ji do takového stavu, kdy je potřebně zinicializovaná. Obzvláště pokud ji pak předáváš někam dál.
Nemyslím si, že by to bylo až tolik práce navíc. A pořád jsem přesvědčen, že to je jediný způsob, jak mít entity skutečně spolehlivé a chovající se podle očekávání.
Editoval Tharos (20. 6. 2013 16:10)
před 6 lety
- Vojtěch Dobeš
- Člen | 1317
@Tharos +1, tenhle přístup se mi líbí :).
před 6 lety
- Jan Tvrdík
- Nette guru | 2550
Možná by stálo za to případ podporu pro něco jako
m:default
. Viz také https://github.com/…oty-v-entitě
před 6 lety
- Tharos
- Člen | 1042
@Jan Tvrdík: Dobrý nápad, to by leccos vyřešilo.
:) Kromě m:default
mě napadá i následující možnost
zápisu:
@property bool $name = 'Default name'
@property bool $isPublished = 0
@property string|null $description = null
Plus mě napadá, že by v entitě mohlo být možné přepsat nějakou
protected
metodu initialize()
, kterou by volal
konstruktor a pomocí které by také bylo možné výchozí hodnoty
zinicializovat:
/**
* @property int $id
* @property string $name
* @property bool $published
*/
class Book
{
protected function initialize()
{
$this->assign(array(
'name' => 'Default name',
'published' => true,
));
}
}
Editoval Tharos (20. 6. 2013 17:43)
před 6 lety
- tomas.lang
- Člen | 54
@Tharos +1 co se týče inicializace hodnot :-)
před 6 lety
- Filip111
- Člen | 244
@CherryBoss:
ok, to jsem nevěděl – hledal jsem v dokumentaci, ale tam nullable moc
detailně neřeší. Každopádně jsem si říkal, že dělam něco blbě,
protože by to už dávno někdo řešil vzhledem k rozšířenosti
Doctrine.
@Tharos:
Nedovedu si představit inicializovat entitu, kde je spoustu properties a ke
každé explicitně uvádět že má hodnotu null nebo prázdný řetězec.
Akorát to natáhne/znepřehlední kód a budu pořád otrocky něco
opisovat.
Nicméně defaulní hodnota tento problém (vlastně feauture :) řeší
k mé plné spokojenosti. Varianta $name = 'Default name'
mi
přijde přirozenější, ale pokud bys upřednostňoval m:
anotace, je mi to jedno.
Jsem rád, že se do diskuse přidal vojtech.dobes a Jan Tvrdík. Považuju to trochu za záruku kvality – určitě budou mít přínosnější návrhy a komentáře než já ;)
Editoval Filip111 (20. 6. 2013 21:00)
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
tak jsem dneska dopoledne vydal verzi 1.4.0. Přehled všech změn je v changelogu na webu.
Aktualizoval jsem i roadmap, aby bylo zřejmé, jakým směrem chci vývoj dále směřovat.
Než se ale do těch bodů pustím, chci zase pohnout s dokumentací. Aktuální dokumentaci upravím a doplním pro řadu 1.4.x, přičemž od řady 1.5.x bude existovat více dokumentací vedle sebe. Infrastrukturu už pro to mám víceméně hotovou…
Díky vám všem za odezvu, kterou jste mi zde poskytli!
před 6 lety
- Tharos
- Člen | 1042
@Filip111: Přidal jsem v develop větvi
protected
metodu initDefaults()
, kterou volá
konstruktor a pomocí které se dají defaultní
hodnoty nastavit.
Každopádně je to ale jen první část řešení těch defaultních hodnot. Přibude ještě podpora v anotacích:
@property string $name = 'Default name'
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
od dnešního dne Lean Mapper disponuje v develop větvi velmi zajímavou novou funkconalitou, a tou je podpora pro vlastní konvence (a nejen databázové).
Považuji tuhle vrstvu za výrazný posun vpřed. Když jsem develop verzi nasadil na jednu svojí aplikaci, ve které mám databázi navrženou podle „nestandardních“ konvencí, anotace se nádherně zjednodušily a také se mapování přesunulo na jedno místo. Zkrátka mi hned došlo, že tohle byl krok tím správným směrem. A že jsem ani pořádně netušil, že tohle v Lean Mapperu skutečně chybělo, byť jde jen o pohodlí :).
Mapper (vlastní konvence, chcete-li) stojí na tomto jednoduchém rozhraní:
namespace LeanMapper;
interface IMapper
{
/**
* Returns primary key name from given table name
*
* @param string $table
* @return string
*/
public function getPrimaryKey($table);
/**
* Returns table name from given fully qualified entity class name
*
* @param string $entityClass
* @return string
*/
public function getTable($entityClass);
/**
* Returns fully qualified entity class name from given table name
*
* @param string $table
* @return string
*/
public function getEntityClass($table);
/**
* Returns table column name from given fully qualified entity class name and entity field name
*
* @param string $entityClass
* @param string $field
* @return string
*/
public function getColumn($entityClass, $field);
/**
* Returns entity field name from given table name and table column
*
* @param string $table
* @param string $column
* @return string
*/
public function getEntityField($table, $column);
/**
* Returns relationship table name from given source table name and target table name
*
* @param string $sourceTable
* @param string $targetTable
* @return string
*/
public function getRelationshipTable($sourceTable, $targetTable);
/**
* Returns name of column that contains foreign key from given source table name and target table name
*
* @param string $sourceTable
* @param string $targetTable
* @return string
*/
public function getRelationshipColumn($sourceTable, $targetTable);
/**
* Returns table name from repository class name
*
* @param string $repositoryClass
* @return string
*/
public function getTableByRepositoryClass($repositoryClass);
}
Všimněte si, jak triviální je výchozí implementace vyjadřující preferované konvence.
Možnosti mapperu jsou velké. Umožňuje elegantně vyřešit převod
$authorName
na author_name
, vyřešit rozdělení
entit do více jmenných prostorů, vypořádat se s primárními klíčem nad
textovým sloupcem code
… a dalo by se pokračovat. Většinu
záležitostí lze také vyjádřit velmi snadno. Pokud například spojovací
tabulky pojmenováváte stylem book_x_tag
, stačí mít takovýto
vlastní mapper:
class CustomMapper extends LeanMapper\DefaultMapper
{
protected $relationshipTableGlue = '_x_';
}
Možná se ptáte, jak moc velký BC break zavedení něčeho podobného přináší. Bylo výzvou tohle naimplementovat tak, aby dopady na „vysokoúrovňové“ API byly minimální. Myslím ale, že se to povedlo. :)
Nově stačí jen předávat repositářům kromě instance
DibiConnection
i nějakou implementaci IMapper
.
To je vše. Takže v typické Nette aplikaci, kde jsou
repositáře zaregistrované jako služby v DI kontejneru, stačí jen přidat
službu implementující IMapper
a auto-wiring už se o vše
postará. To, že se pak mapper uvnitř repositářů, entit, resultů atp.
dostane všude tam, kde je ho zapotřebí, už si řeší Lean Mapper
vnitřně sám.
Za zmíňku stojí následující chování:
$book = new Book;
$book->title = 'Nová kniha';
$book->description = 'lorem isupm';
$bookRepository->persist($book);
Pokud budeme chtít položku description
persistovat do sloupce
custom_description
, bude to fungovat? Ano, protože entita přejme
mapper z repositáře a persituje se už podle jeho pravidel.
API tedy zůstalo stejně přívětivé.
Pro úplnost uvedu, že spolu se zavedením mapperu vzala za své proměnná
$defaultEntityNamespace
v repositářích, protože ta je nově
součástí mapperu (kam samozřejmě patří). A na závěr už bych jen rád
dodal, že možnost doupřesnit mapování v anotacích zůstala zachována a
má i nadále nevyšší prioritu.
Editoval Tharos (24. 6. 2013 22:49)
před 6 lety
- Šaman
- Člen | 2275
Super! Člověk si odjede na prodloužený víkend trochu zašermovat a jeho
oblíbené ORM je zase o kus dál :)
Jdu si s tím pohrát a zjistit, jak co nejjednodušeji zařídit, aby mi
metoda $bookRepository->findByDescription($description);
vyhledávala nad custom_description
s tím, že si tento název
sloupce zjistí sama. Pokud bych narazil na nějaké neočekávané chování,
dám vědět.
Ještě mě napadla jedna (a asi poslední) věc, kterou jsem kdy v ORM
využil a tady nevím, jak toho dosáhnout. I když by asi šla přetížit
metoda $repository->getEntityClass()
, ale potřeboval bych jí
nejprve předat načtená data.
Jde o to, že mám například několik typů výrobků
(computer
, television
) v tabulce product
(obstarávanou pomocí ProductRepository
).
A jeden sloupec bude type
a já bych rád podle tohoto sloupce
změnil třídu, která se mi má z načtených dat vytvořit (např.
class Computer extends Product
). Tedy
$productRepository->getById(10)
mi vrátí nějakou třídu
dědící z Product
podle toho, co je vlastné pod tímto id
uložené.
Doufám, že jsem to vysvětlil srozumitelně :)
Editoval Šaman (24. 6. 2013 16:44)
před 6 lety
- Tharos
- Člen | 1042
@Šaman: Oba Tvé problémy mají jednoduché řešení. :)
Ad findByDescription: Při implementaci mapperu jsem už tenhle Tvůj požadavek měl na paměti ;), takže tam, kde jsi doposud volal:
$property = $this->getReflection()->getEntityProperty($property);
stačí nově jen volat:
$property = $this->getReflection($this->mapper)->getEntityProperty($property);
Property, která se Ti vrátí, již mapperu využívá a při volání
$property->getColumn()
Ti vrátí ve výše uvedeném případě
požadované custom_description
.
Jak jsme se spolu bavili po Jabberu, tohle není problém mít statické… Pak jen ale musíš mapper předat z repositáře, protože staticky se nikde neudržuje. Určitě víš, jak to myslím. :) Volání pak bude vypadat zhruba:
$property = static::getReflection($mapper)->getEntityProperty($property);
Ad typ entity: Metoda createEntity
v abstraktním repositáři disponuje
užitečným parametrem $entityClass
.
V Tvém případě by tedy mělo stačit následující:
public function getById($id) // or find($id)
{
$row = $this->connection->select('*')->from($this->getTable())->where('id = %i', $id)->fetch();
if ($row === false) {
throw new \Exception('Entity was not found.');
}
$entityClass = $this->translateTypeToEntityClass($row['type']); // TODO: implement helper method
return $this->createEntity($row, $entityClass);
}
Editoval Tharos (24. 6. 2013 17:25)
před 6 lety
- besanek
- Člen | 128
Takže už zase budu přesouvat $defaultEntityNamespace ? :D
Rozhodně dobrá práce! Byť konvence momentálně nevyužiji, tak v nich
vidím velký potenciál. Hlavně co se týče při nasazování na již
existující databáze.
Pokud bych si troufl Lean Mapper hodnotit po týdnu práce, určitě bych
použil slovní spojení “nehází klacky pod nohy “. Používal jsem
NotORM, Doctrine. Super nástroje, ale často jsem narazil na problém, který
jsem neuměl čistě vyřešit.
Když jsem si chtěl v Lean Mapperu udělat jednoduchou cache, stačilo
několik jednoduchých řádků do base repository.
Rozhodně pokračuj v tom co děláš :)
@Šaman [OT]: Zašermovat? Snad jsi nebyl v Budyni na Koruně? :)
před 6 lety
- Tharos
- Člen | 1042
besanek napsal(a):
Takže už zase budu přesouvat $defaultEntityNamespace ? :D
Naposledy. :)
Pokud bych si troufl Lean Mapper hodnotit po týdnu práce, určitě bych použil slovní spojení “nehází klacky pod nohy “. Používal jsem NotORM, Doctrine. Super nástroje, ale často jsem narazil na problém, který jsem neuměl čistě vyřešit.
Když jsem si chtěl v Lean Mapperu udělat jednoduchou cache, stačilo několik jednoduchých řádků do base repository.
Rozhodně pokračuj v tom co děláš :)
Díky za zhodnocení. :) Pokračovat rozhodně budu. Dorazilo mi i pár reakcí přes soukromé zprávy a můžu říct, že zatím přijetí Lean Mapperu překonalo má očekávání (přece jenom konkurence mezi podobnými nástroji je nemalá). Nad má původní očekávání je i to, jak se umí vypořádat se záludnostmi, které zde zazněly. Pořád čekám, až někdo přijde s něčím, na čem si vyláme zuby. :)
Mile mě překvapilo také včerejší vydání Dibi 2.1, což znamená, že ani ta „stará dobrá“ knihovna, nad kterou je Lean Mapper postaven, není úplně opuštěná a bez perspektivy. :)
Editoval Tharos (24. 6. 2013 21:00)
před 6 lety
- Šaman
- Člen | 2275
@Tharos: Ten popsaný způsob jak zajistit
polymorfismus produktů je nepraktický v tom, že bych musel stejnou rutinu
řešit v každé dotazovací metodě (findByPrice()
,
getByName()
, ..). Ale navedl jsi mě ke správné metodě,
konkrétně createEntity
, kterou by mělo stačit mírně
přetížit, aby se toto chování projevilo všude. Díky.
@besanek: [OT] Nikoliv, tentokrát to nebyla historická akce, ale fantasy LARP Zlenice.
Editoval Šaman (24. 6. 2013 21:06)
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
Lean Mapper v develop větvi disponuje ode dnešního rána jednou užitečnou novou funkcionalitou – podporou správy a persistence jednoduchých M:N vazeb.
Jednoduchá M:N vazba je taková vazba, která je v relační databázi reprezentována spojovací tabulkou, která neobsahuje nic jiného, než odkazy do spojovaných tabulek a volitelně umělý primární klíč. Zpravidla má tedy taková tabulka dva nebo tři sloupce. Pokud potřebujete v Lean Mapperu pracovat se spojovací tabulkou, která pro každou vazbu obsahuje i nějaké další doplňující informace (zda je vazba aktivní, datum jejího vzniku…), best practice je mít pro takovou tabulku samostatnou entitu (a podle potřeby i repositář).
Lean Mapper nativně vrací kolekce entit v polích
($repository->findAll()
, $book->tags
…),
přičemž každý si může tohle chování velmi
snadno upravit a získávat kolekci plně podle svých potřeb. Tohle
řešení mi přijde ideální, ale bohužel neumožňuje zavést API pro
správu vazeb v plném rozsahu tak, jaké je popsané
zde.
Současné API vypadá následovně:
// Nějaká úvodní kaše
class Mapper extends LeanMapper\DefaultMapper
{
protected $defaultEntityNamespace = null;
public function getPrimaryKey($table)
{
if ($table === 'tag') {
return 'code';
}
return parent::getPrimaryKey($table);
}
}
/**
* @property string $code
* @property string $name
*/
class Tag extends LeanMapper\Entity
{
}
/**
* @property int $id
* @property Tag[] $tags m:hasMany
* @property DateTime|null $released
* @property string $title
* @property string|null $web
* @property string $slogan
*/
class Application extends LeanMapper\Entity
{
}
$mapper = new Mapper;
$applicationRepository = new ApplicationRepository($connection, $mapper);
$tagRepository = new TagRepository($connection, $mapper);
$application = $applicationRepository->find(1);
$tag = $tagRepository->find('JavaScript'); // this is our primary key...
$application->addToTags($tag);
$application->removeFromTags($tag);
$application->addToTags('JavaScript');
$application->removeFromTags('JavaScript');
$applicationRepository->persist($application);
Jak vidno, tagy lze přidávat a odebírat buďto na základě instance
Tag
, anebo na základě ID tagu (v našem příkladu kódu).
Co je asi samozřejmé je, že pokud přistoupíte i před persistencí ke
kolekci $tags
(například tagy vypíšete), přidané a odebrané
tagy se již projeví. V paměti tyto změny provedené už jsou, jen ještě
nejsou persistované v databázi.
Co ale osobně považuji za velkou vychytávku je, jaké dotazy se
vygenerují při volání persist()
. Lean Mapper si inteligentně
hlídá, co je zapotřebí v databázi přidat a odstranit a stav
sesynchronizuje velmi malým počtem dotazů:
- Jedním multi-insertem hromadně vkládajícím potřebné nové vazby
- Voláním DELETE pro jednotlivé vazby s tím, že pokud například
aplikace má tři vazby na tag JavaScript a dvě se mají odstranit, využije se
LIMIT
(
DELETE ... WHERE `
code` = 'JavaScript' LIMIT 2
).
Také platí, že pokud například desetkrát odeberete vazbu na tag ‚SQL‘, který ale aplikace nemá ani jednou přiřazený, žádné dotazy se negenerují.
Aby vše takhle hezky mohlo fungovat, existuje jedno drobné omezení. Pokud
vytváříte například novou aplikaci (new Application
), je
zapotřebí ji před tím, než jí začnete přiřazovat tagy, persistovat.
$application = new Application(array(
'title' => 'New application',
'slogan' => 'lorem ipsum',
));
$applicationRepository->persist($application);
$application->addToTags('PHP');
$application->addToTags('JavaScript');
$applicationRepository->persist($application);
Časem možná tohle omezení odstraním, zatím se za něj omlouvám. :) Při úpravě již existují entity načtené z repositáře takovéto persistování nadvakrát samozřejmě zapotřebí není:
$application = $applicationRepository->find(1);
$application->title = 'New title';
$application->addToTags('PHP');
$application->addToTags('JavaScript');
$applicationRepository->persist($application);
V plánu mám ještě metodu pro zjištění, zda $tags
obsahují nějaký tag nebo ne (syntaxe bude asi
$application->hasInTags($tag)
).
Ono se bez toho dá ale ve spoustě případů žít. Pokud například máte formulář pro vytvoření aplikace obsahující mimo jiné i pole checkboxů pro výběr, které tagy se mají aplikaci rovnou přiřadit, lze pak jednoduše volat:
foreach ($checkboxes as $checkbox) {
if ($checkbox->isChecked()) {
$application->addToTags($checkbox->tagId);
}
}
a při následné úpravě takové entity lze použít konstrukci:
foreach ($checkboxes as $checkbox) {
$application->removeFromTags($checkbox->tagId);
if ($checkbox->isChecked()) {
$application->addToTags($checkbox->tagId);
}
}
Je to takový hodně pseudokód :), ale myslím, že princip je z toho
zřejmý. Vůbec nevadí, že se při update pro každý tag zavolá nejprve
removeFromTags
, protože vnitřní inteligence zajistí, že se
při persistování odeberou přesně ty tagy, které je odebrat zapotřebí, a
nápodobně se přidají i nové.
Mimochodem, implementace téhle správy byla překvapivě snadná. Celá tak nějak „zaplula“ do koncepce Lean Mapperu bez jakýchkoliv hacků nebo berliček
A když se tak koukám na roadmap, říkám si, verze 1.5 je v dohlednu. :)
Editoval Tharos (26. 6. 2013 15:30)
před 6 lety
- castamir
- Člen | 631
Takovej drobnej dotaz – proč se v getRowData automaticky mění
personalId na personal_id když jsem žádné explicitní mapování v anotaci
neuváděl? Je to matoucí, protože pokud neuvedu v anotaci tohle mapování,
tak metoda getData zařve na neznámém personalId, protože má k dispozici
informaci o personal_id, ale personalId už ne.
takhle je to duplicitní. A co se stane, když tam nedám v té
anotaci mapování personalId ⇒ personal_id ale např.
personal_number?
Editoval castamir (27. 6. 2013 11:37)
před 6 lety
- Tharos
- Člen | 1042
Neměl bys kus kódu k nahlédnutí? Abych to mohl reprodukovat… Stačí jen nástřel.
Rád se na to podívám a případný bug opravím. Anebo si chování obhájím ;), možná jde jen o nějaké nedorozumění. Žádné takové mapování by se samovolně odehrávat nemělo.
Editoval Tharos (27. 6. 2013 11:25)
před 6 lety
- castamir
- Člen | 631
už nic
Editoval castamir (27. 6. 2013 11:37)
před 6 lety
- castamir
- Člen | 631
Další problém mi tu přidělává nenašeptávání =/
Ad předchozí příspěvek – já jsem fakt blbec. Promiň :D
Editoval castamir (27. 6. 2013 11:36)
před 6 lety
- Tharos
- Člen | 1042
Super, díky za kód. Ani ho nemusím spouštět :). „Problém“ je
v tom, že ty máš hodnotu položky personalId
persistovanou
v databázovém sloupci personal_id
. To není v souladu
s výchozími konvencemi (podle nich by se ten sloupec v databázi měl také
jmenovat personalId
), a tak je zapotřebí Lean Mapperu
napovědět. Napovědět lze buďto přes explicitní uvedení sloupce
v závorce v definici entity, anebo na úrovni vlastního mapperu:
class Mapper extends DefaultMapper
{
public function getColumn($entityClass, $field)
{
if ($entityClass === 'Users' and $field === 'personalId') {
return 'personal_id';
}
return parent::getColumn($entityClass, $field);
}
public function getEntityField($table, $column)
{
if ($table === 'users' and $column === 'personal_id') {
return 'personalId';
}
return parent::getEntityField($table, $column);
}
}
Tohle je samozřejmě totálně ad-hoc, typicky se dají konvence v mapperu zobecnit. Pak už Lean Mapper ví, co k čemu patří.
Entita zkrátka při „nedodržení konvencí“ bez navedení neví, že
hodnotu pro svou položku personalId
má hledat v
„nízkoúrovňových datech“ ve sloupci personal_id
(a mohlo by
to být třeba i my_fancy_personal_id
…).
Edit: vyřešeno, tak bezva. :)
Editoval Tharos (27. 6. 2013 11:50)
před 6 lety
- Tharos
- Člen | 1042
Ahoj,
mám takový malý RFC. Rád bych nyní významně vylepšil parser anotací a rád bych se dopředu svěřil se svými úmysly.
1) Oddělení komentáře
Stávající parser se snaží ponechat prostor pro komentář, takže anotace může vypadat například takto:
@property DateTime $created Date and time of creation
Problémem je, že pokud například napíšete tohle:
@property Author $maintainer m:hasOne (maintainer_id)
parser si myslí, že (maintainer_id)
už je komentář. Je sice
hezké stanovit, že mezi příznakem a závorkami za ním nesmí být mezera,
ale v tomhle se překlepnout je raz dva a problémem je, že to tiše projde.
Jenomže co kdyby v těch závorkách skutečně byl komentář… Parser to
prostě nemá šanci jednoznačně poznat.
Navrhuji tedy komentář uvádět následovně:
@property Author $maintainer m:hasOne(maintainer_id) # Person who cares about the application
Parser by pak mohl být tolerantní k různým mezerám mezi příznaky a závorkami a hlavně by parsování skončilo výjimkou, pokud by se objevil zjevný problém.
2) Nové příznaky
Nyní Lean Mapper zná následující příznaky:
m:hasOne
m:hasMany
m:belongsToOne
m:belongsToMany
m:enum
m:filter
m:extra
Mně by přišlo ideální, aby rozumněl těmto příznakům:
m:hasOne
m:hasMany
m:belongsToOne
m:belongsToMany
m:enum
m:filter
m:passthru
m:hintOnly
Příznak m:hintOnly
říká, že položka je nadefinovaná
pomocí metod (existují například metody getName()
a
setName($name)
), ale anotace je přítomná kvůli napovídání
v IDE. V rámci anotace by už pak nešlo použít nic ve smyslu
m:hasOne
atp., protože to už si řeší metody, které položku
vyjadřují. Jde skutečně jenom o napovídání, a proto název
hintOnly
. Příklad za sto slov:
/**
* @property string $name m:hintOnly
*/
class Author extends Entity
{
public function getName()
{
return strtoupper($this->row->name);
}
public function setName($name)
{
$this->row->name = strtolower($name);
}
}
$author = new Author;
$author-> // here comes the IDE hint
Příznak m:passthru
má jednoduchý význam. Umožňuje prohnat
hodnotu těsně před tím, než se přiřadí do patřičné položky v
LeanMapper\Row
(respektive těsně poté, co se z
LeanMapper\Row
přečte) skrze nějakou uživatelskou funkci.
Užití je jednoduché:
/**
* @property string $email m:passthru(checkEmail)
*/
class Author extends Entity
{
private function checkEmail($email)
{
if (!Validator::isEmail($email)) {
throw new Exception('Invalid e-mail address.');
}
return $email;
}
}
$author = new Author;
$author->email = 'test'; // throws exception
Navrhuji, že by se detail příznaku mohl volitelně sestávat z jedné nebo ze dvou částí:
m:passthru(checkEmail) – checkEmail se použije při zápisu do položky i při čtení z položky
m:passthru(localizeEmail|checkEmail) – localizeEmail se použije při čtení položky, checkEmail při zápisu do položky
m:passthru(localizeEmail|) – použije se pouze localizeEmail při čtení položky, zápis do položky probíhá standardně
m:passthru(|checkEmail) – použije se pouze checkEmail při zápisu do položky, čtení z položky probíhá standardně
Přijde mi, že tohle je velmi efektivní způsob, jak stručně řešit všemožné specifické validace.
3) Vlastní příznaky
No a pak mě napadla ještě jedna taková věc. Zrušil bych příznak
m:extra a namísto toho bych zavedl, že jakýkoliv jiný příznak (tj. cokoliv
ve tvaru m:<nazev>(volitelné parametry)) by se nezahodilo a šlo by to
pak v entitě číst z LeanMapper\Reflection\Property
. Existovaly
by metody ve smyslu hasCustomFlag($name)
a
readCustomFlag($name)
.
Mělo by to velmi široké využití. Například zde probírané překlady by pak mohly vypadat následovně:
/**
* @property int $id
* @property Lang $lang m:hasOne
* @property string $name m:translate
* @property string $description m:translate
* @property string $keywords m:searchable(fulltext,catalogue)
*/
class Page extends Entity
{
}
Přidal jsem do ukázky i vlastní příznak m:searchable
s parametry, který by mohl například řídit, zda a jakým způsobem lze
v obsahu stránky vyhledávat.
4) Defaultní hodnoty
U položek typu boolean
(bool
),
integer
(int
), float
a
string
bych umožnil definici výchozí hodnoty v anotaci
následujícím způsobem:
@property int $limit = 5
@property string $name = 'Default name'
@property string $description = "Lorem ipsum dolor sit amet"
@property bool $published = true
Samozřejmě v entitě zůstane metoda initDefaults()
umožňující zinicializovat třeba i položky typu DateTime
atp.
Navrhuji, aby nastavování defaultní hodnoty stejné položky z anotace i z
initDefaults()
metody zároveň vedlo k výjimce.
Připadají vám tyto návrhy dobré a užitečné? Chybí tomu něco? Anebo tomu něco přebývá? Předem moc díky za názory.
Editoval Tharos (28. 6. 2013 8:35)
před 6 lety
- Šaman
- Člen | 2275
Mě se to líbí.
před 6 lety
- Michal III
- Člen | 84
Mně se to líbí rovněž. Co se týče výchozích hodnot objektů, nefungovalo by následující?
<?php
/**
@property DateTime $date = new DateTime('yesterday')
*/
?>
Každopádně se mi líbí každičký jednotlivý návrh a celá knihovna se mi velice zamlouvá víc a víc.
před 6 lety
- Tharos
- Člen | 1042
@Michal III: Mít možnost takto zinicializovat
například DateTime
by bylo super, ale otázkou je, co
s případnými složitějšími konstrukcemi. Když bude umožněno
přiřadit:
new DateTime('yesterday')
někdo se toho chytne a pokusí se přiřadit:
new MyAuthor(array('id' => GlobalCounter::makeId(), "name" => strtoupper("Default \"name\""))
:) Psát parser, který by něco takového zchroustal, by bylo strašné…
Anebo by to vedlo na nějaký ošklivý eval
.
Ještě se ale podívám, co umožňuje zapsat například Neon. Pravdou je, že v omezené míře by to možná umožnit šlo (v duchu „název třídy a nějaké skalární atributy“ nebo volání nějaké továrny).
Tohle parsování není kešované na disku, ale jen v paměti. Zatím to není vůbec žádný problém, parsování je jednoduché, provede se pro každý typ entity jenom jednou a co Lean Mapper poctivě profiluji, úzká hrdla jsou pořád v Dibi. :) Nerad bych parsování zesložitil natolik, aby se vyplatilo kešovat na disk.
Určitě to ale ještě zvážím, díky za nakopnutí. :)
před 6 lety
- Michal III
- Člen | 84
@Tharos: Asi bych to řešil způsobem, který by byl
opravdu značně omezený, tedy jen na skalární typy a případně pole se
skalárními typy (array(1, 3)
či [1, 3]
). Na ony
složitější konstrukce by tu opravdu byla metoda initDefaults()
,
jelikož ty „zrůdnosti“ by v anotacích ani nevypadaly moc přehledně.
Každopádně pro jednoduché použití by to možná v rámci přehlednosti
stálo za to.
Pokud by výchozí přiřazování objektů bylo z větší části (> 70%) používáno jednoduchým způsobem, tedy se skalárními typy, stálo by to nejspíš za implementaci, jinak bych to asi neřešil a ponechal Tvůj původní návrh :-).
před 6 lety
- Tharos
- Člen | 1042
Jenom taková technická… Odjíždím na dovolenou a až do 7. července budu mít velmi omezený přístup k internetu. Na případné bug reporty|dotazy|nápady budu moci reagovat až po svém návratu. Díky. :)
před 6 lety
- castamir
- Člen | 631
hlavně ať to není case sensitive ať půjde udělat oba zápisy ;o)
@property bool $published = true
@property bool $published = TRUE
před 6 lety
- Šaman
- Člen | 2275
Taky píšu TRUE a FALSE jako konstanty.
OT: @castamir: Nesouvisí nějak tvůj nick s (Rains of) Castamere? :)
před 6 lety
- Filip111
- Člen | 244
@Tharos:
mě se to líbí – vlastní m:příznaky jsou fajn,
možná k defaultním hodnotám bych přidal ještě možnost v anotaci
definovat jako defautlní hodnotu array(). Stačí prázdné, bez složitých
konstrukcí.
před 6 lety
- besanek
- Člen | 128
Moc pěkný RFC, hlavně velké + za m:passthru.
K tomu přiřazování defaultních objektů. Zápis
@property DateTime $date = new DateTime('yesterday')
se mi moc nelíbí. „new DateTime“ je redundantní, protože nic jiného než objekt DateTime by v $date vůbec být neměl. Navíc se mi to zdá jako cpaní PHP do komentářů.
Osobně bych viděl raději něco takového.
@property DateTime|null // bude výchozí hodnota NULL
@property DateTime // bude přiřazen DateTime s konstruktorem bez parametrů
@property DateTime $date=['yesterday'] //bude vytvořen DateTime('yesterday')
před 6 lety
- Filip111
- Člen | 244
@besanek
Já bych to nekomplikoval – $date=['yesterday']
je absolutně
neprůhledný. Je zbytečný vymejšlet další jazyk nebo pseudozápisy. Omezil
bych to na elementární typy včetně array(). Pro cokoliv složitějšího je
tu initDefaults()
před 6 lety
- Šaman
- Člen | 2275
S defautlní hodnotou NULL u nullable položek souhlasím (pokud
explicitně neuvedu jinou defaultní hodnotu).
A s tím DateTimem bych možná (nebude-li to na úkor rychlé a čisté
implementaci) akceptoval všechno, co skousne konstruktor DateTimu (‚now‘,
‚20.1.2011‘, ‚2011/20/1‘, ‚-1 day‘) s tím, že pokud není nic
uvedeno, vytvoří se DateTime bez parametrů, tedy ‚now‘. Nebude to o nic
magičtější, než samotný DateTime.
před 6 lety
- besanek
- Člen | 128
Teď si nejsem jist, že jsme se zcela pochopili.
Souhlasím, že pro cokoli složitějšího je initDefaults()
,
ovšem složitější si představuji volání funkcí či jiné objekty.
Předávání základních datových typů by šlo pomocí anotací.
@property Object $foo=['foo', 'bar', 123] // zavolá new Object('foo', 'bar', 123)
@property Object $foo=('foo', 'bar', 123) // popř. kulaté závorky :)
Trochu jsem se inspiroval v c++ seznamem inicializací. :) Ale jen jsem se pokusil rozvinout nápad s tvorbou objektu. Pokud by to mělo nějaké výkonnostní dopady, tak to nemá smysl implementovat.
před 6 lety
- Jirda
- Člen | 111
Jsem taky pro to, aby se ta syntaxe zbytecne nekomplikovala. Taky bych to omezil pouze na ty elementarni typy a pro slozitejsi proste initDefaults().
Editoval Jirda (29. 6. 2013 22:35)
před 6 lety
- Michal III
- Člen | 84
@Jirda:
Myslím si, že syntaxe se zavedením neelementárních typů nekomplikuje,
komplikuje se spíše parser (tedy při použití té
standardní @property DateTime $date = new DateTime
syntaxe).
@besanek:
besanek napsal(a):
@property Object $foo=['foo', 'bar', 123] // zavolá new Object('foo', 'bar', 123) @property Object $foo=('foo', 'bar', 123) // popř. kulaté závorky :)
Řekl bych, že tímto bychom se zbavili redundantnosti za cenu nepřehlednosti a magické syntaxe. Hranaté závorky by ve mně evokovaly spíše pole a je zbytečné si zapamatovávat novou syntaxi.
Mimochodem nemůže nastat situace, že bychom jako výchozí hodnotu chtěli
objekt potomka? Špatný příklad:
@property Date $date = new DateTime
.
před 6 lety
- Etch
- Člen | 404
Takovej zcela triviální dotaz:
Jde nějak jednoduše dosáhnout následujícího:
$params['limit'] = null;
$count = count($this->booksRepository->filterBooks($params));
$params['limit'] = 50;
$books = $this->booksRepository->filterBooks($params);
filterBooks($params)
nedělá nic jiného, než že aplikuje na
výsledek filtry podle parametrů. Jde to udělat nějak jednodušeji, než za
pomocí nějaké specializované count metody v repository??
před 6 lety
- Tharos
- Člen | 1042
@Etch: Pokud nejdu z křížkem po funuse (byl jsem teď pár dní bez přístupu k internetu), tak já tohle řeším zhruba následovně:
class Fragment
{
/** @var int */
private $overallCount;
/** @var int */
private $fragmentCount;
/** @var int|null */
private $offset;
/** @var array */
private $data;
/**
* @param int $overalCount
* @param int $fragmentCount
* @param int|null $offset
* @param array $data
*/
public function __construct($overalCount, $fragmentCount, $offset, $data)
{
$this->overallCount = $overalCount;
$this->fragmentCount = $fragmentCount;
$this->offset = $offset;
$this->data = $data;
}
/**
* @return int
*/
public function getOverallCount()
{
return $this->overallCount;
}
/**
* @return int
*/
public function getFragmentCount()
{
return $this->fragmentCount;
}
/**
* @return int|null
*/
public function getOffset()
{
return $this->offset;
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
}
/**
* @property int $id
* @property string $name
*/
class Book extends Entity
{
}
class BookRepository extends Repository
{
/**
* @param array $filter
* @return Fragment
*/
public function findByFilter(array $filter)
{
$limit = array_key_exists('limit', $filter) ? $filter['limit'] : null;
$offset = array_key_exists('offset', $filter) ? $filter['offset'] : null;
$statement = $this->connection->select('COUNT(*)')
->from($this->getTable());
$this->filterStatement($statement, $filter);
$overallCount = $statement->fetchSingle();
$statement = $this->connection->select('*')
->from($this->getTable());
$this->filterStatement($statement, $filter);
$data = $this->createEntities($statement->fetchAll($offset, $limit));
return new Fragment($overallCount, count($data), $offset, $data);
}
/**
* @param DibiFluent $statement
* @param array $filter
*/
private function filterStatement(DibiFluent $statement, array $filter)
{
// do something useful here
}
}
//////////
$filter = array(
'limit' => 1,
'offset' => 2,
);
$booksFragment = $bookRepository->findByFilter($filter);
echo $booksFragment->getOverallCount() . "\n";
foreach ($booksFragment->getData() as $book) {
echo $book->name . "\n";
}
Uvedený kód by ještě snesl učesání a zobecnění podle konkrétních potřeb, ale myšlenka by z něj měla být myslím dobře patrná.
Editoval Tharos (6. 7. 2013 23:13)