Plánuje se Exploreru přidat podporu datových typů, jako to mají formuláře a latte?
- Bulldog
- Člen | 110
Ahoj,
chci se zeptat, jestli je v plánu do budoucna přidat i možnost zvolit si
datový typ, který vrátí Selection
podobně jako to mají
formuláře/latte.
Jde mi o to, že ORM, které existují mi přijdou pomalé/krkolomné atp. Většinou si tvořím vlastní řešení. No Nette databáze mi přijde hodně fajn, protože vlastně nejde o žádné sofistikované řešení, ale jen o jednoduchý ‚spojovník‘, nebo jak bych to řekl a můžu si upravit výpisy přímo na míru jak potřebuji.
Už dlouho ale přemýšlím, jestli když se Nette pohybuje ke striktnímu Objektovému návrhu a datovému typování, jestli by nebylo od věci, aby toto uměla i DB. Nemyslím tím tvořit z toho ORM, ale jen aby výsledná data nebyla reprezentována jen polem, Row, nebo nějakým obecným ActiveRow, ale abych mohl výsledky jednotlivých dotazů rovnou čerpat v mnou zvolené třídě a ne to později konvertovat.
Pokud nic takového v plánu není, tak se chci zeptat, jestli se toho mám ujmout a zkusit to do Nette databáze přidat?
- neznamy_uzivatel
- Člen | 115
Felix napsal(a):
Udelal jsem si misto active row vlastni “active row per tabulka”.
Nebyla by prosím malá ukázka jak na to? Vůbec si to nedovedu
představit..
Já si primitivně posílám ActiveRow do konstruktoru objektu, kde foreachem
projdu data a nasypu to do public properties, takže když potřebuju něco
upravit při výpisu, tak to dělám v getteru..
- Marek Znojil
- Člen | 80
neznamy_uzivatel napsal(a):
Felix napsal(a):
Udelal jsem si misto active row vlastni “active row per tabulka”.
Nebyla by prosím malá ukázka jak na to? Vůbec si to nedovedu představit..
Já si primitivně posílám ActiveRow do konstruktoru objektu, kde foreachem projdu data a nasypu to do public properties, takže když potřebuju něco upravit při výpisu, tak to dělám v getteru..
Dělám to stejně. :D
- Bulldog
- Člen | 110
No já to dělám tak, že si vytvořím třídu konkrétní entity,
například User
, která v konstruktoru bere ActiveRow, které
může být nullable a má privátní proměnnou typu array
s názvem třeba $data
Pak tato entita zprostředkovává všechny properties, co potřebuju.
Setter uloží novou hodnotu do $data
pod klíčem z db tabulky a
getter se podívá, jestli tato hodnota existuje v poli $data
a
pokud ano, tak ji vrátí. Pokud ne, tak se podívá jestli je nastavený
ActiveRow a pokud ano, tak vrátí hodnotu z něj a pokud ne, tak vrátí null,
nebo vyhodí error neinicializovaná proměnná.
Tím je to editovatelná entita, která umí pracovat s ActiveRow.
No a dále moje entita User
se chová jako takový osekaný
PROXY nad ActiveRow, takže má třeba metody ref
,
related
, update
, delete
, které jen
forwarduje na ten vnitřní ActiveRow.
Například metoda update
udělá to, že zavolá vnitřně:
$this->activeRow->update($this->data)
a následně:
$this->data = []
Samozřejmě tam probíhá nějaká kontrola, jestli je ActiveRow nastaven,
případně když je pole $data
prázdné, tak to dotaz vůbec
nedělá atp., ale princip fungování je podobný.
Akorát mi to přijde takové složité, takže bych uvítal něco podobného přímo v Exploreru, nebo jako doplněk, jak psal @Felix
Editoval Bulldog (12. 11. 2021 12:18)
- Kamil Valenta
- Člen | 799
Bulldog napsal(a):
No já to dělám tak, že si vytvořím třídu konkrétní entity, například
User
, která v konstruktoru bere ActiveRow, které může být nullable a má privátní proměnnou typuarray
s názvem třeba$data
Takže to není ani zapouzdřené, ani to nenapovídá atributy. K čemu je takový kontejner dobrý?
- Felix
- Nette Core | 1194
Ja to udelal takto. Rad bych to probral s @DavidGrudl a nebo pripadne hodil do contributte/database.
- Mrknul jsem, kde se tvori ActiveRow (
new ActiveRow
). A to je trida Selection (https://github.com/…election.php#L556). Metoda se jmenujecreateRow
a je protected. Lze pretizit. - Kouknul jsem se, kde se tvori trida Selection. Jedna se o Explorer (https://github.com/…Explorer.php#…).
Tak jsem si ho podedil. Tedka je moznost predelat metodu
table
a nebo si udelat svoji vlastnitableXyz()
. - Kdyz mame vlastni tridy, ktere umozni zmenu ActiveRow na vlastni tridu, tak
lze poresit jeste automatickou detekci podle tabulky, napriklad.
// Misto ActiveRow se pouzije nas objekt $context->setRowClass(CustomActiveRow::class); // Misto ActiveRow se pouzije nas objekt podle tabulky $context->setRowMapping([ 'user' => UserRow::class, 'address' => AddressRow::class, ]);
- Posledni skladacka je prepsani defaultniho Exploreru (ve starsich verzich se
jmenuje Context).
To se najde tak, ze je potreba dohledat registraci tridy Explorer
v DatabaseExtension (https://github.com/…xtension.php#L122).
Extension
se stara o to, aby se do Nette DI
containeru zaregistrovali vsechny potrebne sluzby a clovek si je nemusel
v souboru
config.neon
registrovat sam.# 1. Registrace nove tridy. Ktera se pak pouzije jako zavislost ve vasich tridach. services: - App\CustomExplorer(@database.default.connection, @database.default.structure, @database.default.conventions) # 2. Prepis originalnich exploreru z nette/database. V pripade vice connection zaregistrovat prepsat vice exploreru/contextu. services: database.default.context: App\CustomExplorer(@database.default.connection, @database.default.structure, @database.default.conventions)
Trida CustomContext
. Resi, jestli se pouzije vlastni trida pro
ActiveRow a nebo se pouzije trida podle nazvu tabulky.
use Nette\Caching\IStorage;
use Nette\Database\Connection;
use Nette\Database\Context;
use Nette\Database\Conventions\StaticConventions;
use Nette\Database\IConventions;
use Nette\Database\IStructure;
final class CustomContext extends Context
{
/** @var string */
private $rowClass;
/** @var string[] */
private $rowMapping = [];
public function setRowClass(string $class): void
{
$this->rowClass = $class;
}
/**
* @param string[] $mapping
*/
public function setRowMapping(array $mapping): void
{
$this->rowMapping = $mapping;
}
/**
* @param string $table
*/
public function table($table): CustomSelection
{
$selection = new CustomSelection($this, $this->conventions, $table, $this->cacheStorage);
if ($this->rowClass) {
$selection->setRowClass($this->rowClass);
}
if ($this->rowMapping) {
$selection->setRowMapping($this->rowMapping);
}
return $selection;
}
}
Trida CustomSelection
. Resi jaka trida se pouzije misto
ActiveRow.
use Nette\Database\Table\ActiveRow;
use Nette\Database\Table\Selection;
final class CustomSelection extends Selection
{
/** @var string */
private $rowClass;
/** @var string[] */
private $rowMapping = [];
public function setRowClass(string $rowClass): void
{
$this->rowClass = $rowClass;
}
/**
* @param string[] $mapping
*/
public function setRowMapping(array $mapping): void
{
$this->rowMapping = $mapping;
}
protected function createRow(array $row)
{
$className = $this->resolveClass();
return new $className($row, $this);
}
private function resolveClass(): string
{
if ($this->rowClass) {
return $this->rowClass;
}
if (isset($this->rowMapping[$this->getName()])) {
return $this->rowMapping[$this->getName()];
}
return ActiveRow::class;
}
}
Ukazkovy ActiveRow per tabulka.
/**
* @property int $id
* @property string $name
* @property string $surname
*/
class User extends ActiveRow
{
}
- Bulldog
- Člen | 110
@KamilValenta
Kamil Valenta napsal(a):
Bulldog napsal(a):
No já to dělám tak, že si vytvořím třídu konkrétní entity, například
User
, která v konstruktoru bere ActiveRow, které může být nullable a má privátní proměnnou typuarray
s názvem třeba$data
Takže to není ani zapouzdřené, ani to nenapovídá atributy. K čemu je takový kontejner dobrý?
Bulldog napsal(a):
Pak tato entita zprostředkovává všechny properties, co potřebuju.
Tím je myšleno, že entita vypadá například takto:
/**
* @property string $email
* @property string $password
*/
class User extends Entity
{
public function getEmail(): string
{
return $this->getColumn('email');
}
public function setEmail(string $email): void
{
$this->changedColumns['email'] = $email;
}
public function getPassword(): string
{
return $this->getColumn('password');
}
public function setPassword(string $password): void
{
if (empty($password)) {
return;
}
$this->changedColumns['password'] = $password;
}
}
a Entity.php:
/**
* @property-read int|string|null $id Primary key identifier. Null if key does not exists (not inserted into the database yet)
*/
abstract class Entity implements IteratorAggregate, ArrayAccess
{
use SmartObject;
protected array $changedColumns = [];
protected ActiveRow|null $row = null;
public function getIterator(): Traversable|Iterator
{
return new ArrayObject($this->toArray());
}
public function offsetExists($offset): bool
{
return (bool) $this->offsetGet($offset);
}
public function offsetGet($offset)
{
return ($this->$offset ?? null);
}
public function offsetSet($offset, $value)
{
$this->$offset = $value;
}
public function offsetUnset($offset)
{
unset($this->changedColumns[$offset]);
}
public function update(): int
{
// ověřování
return (int) $this->row->update($this->changedColumns);
}
public function related(BaseRepository $repository): DataCollection
{
// ověřování
return new Selection($this->row->related($repository::getTableName()), $repository::getEntityClass());
}
public function delete(): int
{
// ověřování
$retVal += $this->row->delete();
$this->changedColumns = [];
return $this->setActiveRow(null);
}
public function toArray(): array
{
// tady trošku pracuju s reflexí, aby jsem měl v návratové hodnotě klíče podle properties, ne podle sloupců v DB
}
public function __toArray(): array
{
return $this->toArray();
}
public function getId(): int|string|null
{
return $this->getColumn('id');
}
public function setActiveRow(ActiveRow|null $row): void
{
$this->row = $row;
}
public function getActiveRow(): ActiveRow|null
{
return $this->row;
}
protected function getColumn(string $columnName): mixed
{
return $this->changedColumns[$columnName] ?? ($this->row->$columnName ?? '');
}
}
Pro samotnou Selection to mám podobně, aby jsem forwardoval funkčnost
přes PROXY, jelikož chci využívat fičury lazy načítání atp.
Takže je to zapouzdřené i napovídá.
Editoval Bulldog (12. 11. 2021 12:38)
- Bulldog
- Člen | 110
@Felix
Tak jsem to zkoušel taky, ale zasekl jsem se na tom, že nevím,
jak zaregistrovat můj poděděný Explorer do DI, aby ho bralo klasické
nastavení připojení k databázi.
Můžu to udělat normální registrací jako službu, ale rád bych používal
i nadále výchozí nastavení připojení, což není moc
kompatibilní ne?
- Polki
- Člen | 553
Dobrá práce hoši.
Možná by to chtělo ještě nějak vymyslet, aby ty třídy byly volitelně
editovatelné co?
Když budu mít třídu Article
, kterou budu cpát do
formuláře, tak nechci mít druhou třídu Article
, kterou budu
cpát do databáze a mezi nima muset ručně přelévat data.
Představoval bych si to nějak takto:
public function articleFormSucceeded(Article $data): void
{
$this->articleRepository->save($data);
}
ArticleRepo:
public function save(Article $data): Article
{
if ($data->id) {
$data->update();
// Nebo
$this->db->table('article')->get($data->id)->update($data);
// Nebo
$this->db->table('article')->where('id', $data->id)->update($data);
// Nebooooooooooo jelikož $data obsahují už ID, tak
$this->db->table('article')->update($data);
} else {
$data = $this->db->table('article')->insert($data);
}
return $data;
}
Samozřejmě třída Article by se mohla vnitřně starat o validaci dat
Jo a taky místo $this->db->table('article')
by byla nějaká
metoda, která by nastavovala typ té třídy, případně by se dalo možná
zapsat něco jako $this->db->update($data)
a
$this->db->insert($data)
, přičemž to, do jaké tabulky se
to má uložit by se četlo podle typu třídy, co se tam pošle, jelikož
třída je svým způsobem identifikátor tabulky?
Editoval Polki (12. 11. 2021 13:29)
- Polki
- Člen | 553
Nápad na použití
Třída:
/**
* @property string $name name // $name je název property pro appku a name je název sloupce v DB
* @property string $content // pokud poslední parametr neuvedeme, tak je překlad 1:1, ale CamelCase vs snake_case
* @property int $authorId author_id // tady je jasně vidět překlad
*/
class Article implements ActiveRow // nebo jiný rozhraní případně dědit třídu. Důležité je, aby byla editovatelná
{ }
config:
database:
mapping:
article: App\Model\Entity\Article # formát nazev_tabulky: trida
user: App\Model\Entity\User
- App\Model\Entity\Comment # V případě, že neexistuje klíč, tak název tabulky je název entity ale v snake_case, tedy comment
Čtení:
$article = $this->db->table(Article::class)
->where() ... // tak jak to známe nad klasickou Selection
->fetch(); // Vrátí instanci třídy Article naplněnou daty z tabulky article
$author = $article->author; // Vrátí instanci třídy User, naplněnou daty z tabulky user
$comments = $article->rel(Comment::class); // Vrátí GrouppedSelection, která v případě fetch() bude vracet instance třídy Comment z tabulky comment
//////////////////////
$user = $this->db->table('user')->get(5); // Vrátí instanci třídy User
$setting = $this->db->table('setting')->get(5); // Vrátí instanci třídy ActiveRow, jelikož není v configu specifikováno mapování.
Zápis:
$this->db->insert($article); // provede insert dat z instance uložené v proměnné $article do databázové tabulky, kterou zjistí z configu podle $article::class
$this->db->update($article); // provede update...
$this->db->delete($article); // provede delete...
$this->db->save($article); // Provede insert nebo update na základě toho, jestli je v $article nastavena proměnná reprezentující PK v tabulce article
$this->db->insert($setting) // Error, setting je instance ActiveRow a neví se, do které tabulky se má uložit
$this->db->insert([
'name' => 'Robert',
'surname' => 'Pěšinka',
]) // Error posíláme pole, takže zase nevíme, kam data uložit
$this->db->table('setting')->insert($setting); // OK je specifikovaná i tabulka
$this->db->table('article')->insert($article); // OK sice je specifikovaná tabulka zbytečně, ale pro zpětnou kompatibilitu. :)
$thsi->db->table(Comment::class)->insert($article); // Error snažím se něco, co patří do tabulky article vložit do tabulky comment. Díky tomu není třeba ani dělat dotaz, jelikož tato chyba se dá odchytit již v insertu.
Editoval Polki (12. 11. 2021 21:15)
- Kamil Valenta
- Člen | 799
Bulldog napsal(a):
Takže je to zapouzdřené i napovídá.
Ano, tohle už ano. Já jsem vycházel z popisu „která v konstruktoru bere ActiveRow, které může být nullable a má privátní proměnnou typu array s názvem třeba $data“, protože zaslaná ukázka tomuto popisu neodpovídá.
- Polki
- Člen | 553
Já si myslím, že mu popis odpovídá docela dobře. To, že si místo do
konstruktoru udělal setter a getter pro ten ActiveRow a že se ta privátní
proměnná jmenuje changedColumns
místo data
a je
protected díky dědění základních fičur si myslím, že je jen detail.
Klidně si tam ten konstruktor, který očekává ActiveRow můžeš přidat a
proměnnou changedColumns
přejmenovat a bude to přesně
odpovídat.
A taky si myslím, že odpovídá i zbytek…
Pak tato entita zprostředkovává všechny properties, co potřebuju.
Z toho je podle mě jednoznačně jasný, že ta entita má v sobě properties, přes které se přistupuje k obsahu proměnné data, nebo toho ActiveRow, takže napovídání, integrita dat apod. je zajištěná ne?
Setter uloží novou hodnotu do $data pod klíčem z db tabulky a getter se podívá, jestli tato hodnota existuje v poli $data a pokud ano, tak ji vrátí. Pokud ne, tak se podívá jestli je nastavený ActiveRow a pokud ano, tak vrátí hodnotu z něj a pokud ne, tak vrátí null, nebo vyhodí error neinicializovaná proměnná.
Tady je to dokonce detailněji popsaný, díky čemuž už musí být tutově jasný, že ta třída vystavuje jako rozhraní gettery a settery nějakých properties a když je specifikováno, že data jsou privátní a AR se bere v konstruktoru, ale nepíše se, že by bylo zpřístupněno veřejně, tak to imho dává smysl.
No a dále moje entita User se chová jako takový osekaný PROXY nad ActiveRow, takže má třeba metody ref, related, update, delete, které jen forwarduje na ten vnitřní ActiveRow.
A tímhle dokonce říká, že se ta třída chová jako PROXY nad ActiveRow a doufám, že všichni, co se naučili s Nette umí i návrhové vzory, z čehož by taky mělo být jasný, že nejde přistupovat napřímo k ActiveRow uvnitř, takže nutně musí taky být privátní a musí se k němu (jeho hodnotám) přistupovat přes ty Gettery/Settery.
Za mě je to úplně v pohodě popis, který reflektuje zaslaný
příklad.
Schválně jsem podle popisu teď dal 2 kolegům napsat výsledek a vypadal
stejně jako na příkladu, jen místo getteru a setteru byl konstruktor a
vnitřní proměnná changedColumns
se samozřejmě
jmenovala data
Editoval Polki (12. 11. 2021 21:17)
- Kamil Valenta
- Člen | 799
Humorné je, Polki, jak Tvé ego dosud nerozdýchalo, jak Ti nikdo nedal za pravdu ve vláknu o db Exploreru a CRUD operacích a od té doby mínusuješ cokoliv, co napíšu :) Ale pokud to Tvé psychice pomůže, rád tuto úlohu na sobě ponechám.
Polki napsal(a):
Já si myslím, že mu popis odpovídá docela dobře.
Já myslím, že ne, a tak jsem se raději zeptal. Rozdíl mezi námi je, že já si za druhé nedomýšlím a raději se konkrétně ptám, aby si někdo nezvolil cestu antipatternu.
Z toho je podle mě jednoznačně jasný, že ta entita má v sobě properties, přes které se přistupuje k obsahu proměnné data, nebo toho ActiveRow, takže napovídání, integrita dat apod. je zajištěná ne?
Ne nutně, dokud jsme neviděli ukázku.
$entita->data->column
poskytuje všechny (pseudo)properties, ale nenapovídá.
O zajištění integrity nemůžeme vůbec hovořit, dokud jsme kód neviděli. Integritu nezajišťuje jen a pouze přítomnost setteru/getteru, ale jejich konkrétní podoba…
Setter uloží novou hodnotu do $data pod klíčem z db tabulky a getter se podívá, jestli tato hodnota existuje v poli $data a pokud ano, tak ji vrátí. Pokud ne, tak se podívá jestli je nastavený ActiveRow a pokud ano, tak vrátí hodnotu z něj a pokud ne, tak vrátí null, nebo vyhodí error neinicializovaná proměnná.
Tady je to dokonce detailněji popsaný, díky čemuž už musí být tutově jasný, že ta třída vystavuje jako rozhraní gettery a settery
Popisuje právě jeden setter a getter na data, z toho tutově nevyplývá, že ta classa má setterů / getterů více…
A tímhle dokonce říká, že se ta třída chová jako PROXY nad ActiveRow a doufám, že všichni, co se naučili s Nette umí i návrhové vzory
Pouč mne prosím, myslel jsem, že všechny návrhové vzory znám, ale „osekaná proxy“ mne minula.
Je to opět zbytečná diskuze, dal jsem dotaz, tazatel to upřesnil ukázkou. Tvá intervence je dost zbytečná :)
- Polki
- Člen | 553
@MarekBartoš no jo nekdy musis proste vyjadrit co myslis presne a
kvuli tomu to je pak dlouhe. Proto treba nektere matematicke dukazy, ktere
popisuji uplnou banalitu jsou nekolikastrankove.
Skoda je, jen to, ze i s detailnim popisem jsou lidi, kteri to i tak
nepochopi a nevidi souvislosti. To je pak zbytecne psat to no 😄
- Bulldog
- Člen | 110
Hoši hoši. To člověk chce úplně ráno zapnou práci, kouknout se jak
dopadlo vlákno na fóru a vidět tohle…
@Polki Vždyť je to jedno měl jsem za to sice, že jsem použil dost
implikace ve svých výrocích, ale může se stát, že jsem to napsal
špatně, nebo to někdo nevidí. Zeptat se je podle mě v pohodě. Jak se
říká líná huba holý neštěstí.
@KamilValenta Ohledně toho, co jsi napsal Polkimu, tak třeba my ve firmě jedem podle toho, jak to psal on v tom příspěvku už pěkně dlouho. Jediný, proč jsme mu nedali zapravdu je, že nám přišlo zbytečný se kvůli tomu registrovat na fóru. Ostatně si můžeš na netu sám dohledat, že stored procedures vznikly jen proto, že programátoři neumí psát efektivní SQL dotazy a díky tomu potom dotazy dost zhoršovaly performance aplikace, tak experti na databáze přidali do DB systémů procedury, díky kterým oni sami mohli napsat efektivní dotazy na straně DB a programátorům zprostředkovávali jen ‚API‘ s těmito procedurami, což je podle tvého popisu v tom vlákně i tvůj případ.
Každopádně bych ocenil, kdyby jsme řešili jen to, o čem téma je. Svoje problémy si můžete říct soukromě díky. :)
- Kamil Valenta
- Člen | 799
Bulldog napsal(a):
že stored procedures vznikly jen proto, že programátoři neumí psát efektivní SQL dotazy
Nebo chtěli snížit traffic, nebo chtěli cachovat prováděcí plán, nebo
chtěli grantovat oprávnění jen na část věty.
Každopádně to vlákno není jen o SP, je o tom, že DB má spoustu
„non-crud“ dotazů, kterých se musíš zříct jen proto, aby sis mohl
říct, že používáš jen Explorer a paranoidně se nebál poslat query
ad-hoc.
programátorům zprostředkovávali jen ‚API‘ s těmito procedurami, což je podle tvého popisu v tom vlákně i tvůj případ.
Což vůbec není můj případ, popisoval jsem API 3. stran, kdy prostě nemáš jinou možnost. Nebo ano, můžeš klienta odmítnout, ale stále nechápu proč.
- Klobás
- Člen | 113
MKI-Miro napsal(a):
Ahojte
Nasiel by sa teda nejaky konkretny/finalny/funkcny priklad ako to spravit?
vdaka
Ahoj, v podstatě vše finální od Milana Šulce. Může se lišit verzí
Nette.
Já jsem to teď testoval na svém starém, ale stále udržovaném soukromém
projektu.
V práci to za mě vyřešil šikovnější kolega, ale je to moloch a já jsem potřeboval vlastní ActiveRow pro každou tabulku a tam si případně doplnit nějaké pomocné metody, které mi nějakým způsobem doupraví data. A nechtěl jsem to mít stylem v každém modelu / repozitáři nad každým výběrem dat ty data procpat právě takovouhle Entitou / Přepravkou (obzvlášt když mám i dotazy co vrací jen Selection a ne rovnou ActiveRow).
Navíc je to nudné, otravné a člověk na to stejně může zapomenout.
Takže to řešení od Milana Šulce je defakto funkční s tím, že v mojí verzi Nette (2.4) je to takto:
- místo convetions, je to reflection.
- řve to vícenásobné služby Nette databa, proto je tam vyplé autowirování (autowired: false)
- v ukázce od MŠ je název třídy CustomContext, ale registruje to jako CustomExplorer (takže opravit/přepsat)
- pak to padá na 4 argumentu Contextu, jelikož je CacheStorage definováno jako privátní property, takže nutno předat ručně
- pak si naplníš pole kde tabulka ⇒ třída (já to nechci plnit ručně, ale mám to pres class_exists)
a pak skutečně můžeš na ActiveRow řádku (nyní už na tvém "Cosi"ActiveRow) volat metody z oné třídy.
<?php
foreach ($section as $row) {
\bdump($row->getCoolName()); // metoda z NoteActiveRow
}
class Note extends ActiveRow
{
public function getCoolName()
{
return $this->name . ' -> COOOOOL';
}
}
?>
<?php
services:
mydatabase:
factory: App\CustomContext(@database.default.connection, @database.default.structure, @database.default.reflection)
autowired: false
database.default.context: App\CustomContext(@database.default.connection, @database.default.structure, @database.default.reflection)
?>
Vím, že je to staré vlákno, ale třeba se to někomu bude hodit tak jako mně.
- m.brecher
- Generous Backer | 838
@Klobás
já jsem potřeboval vlastní ActiveRow pro každou tabulku a tam si případně doplnit nějaké pomocné metody, které mi nějakým způsobem doupraví data.
I já jsem řešil, jako asi každý kdo používá Nette Explorer jak přidat k ActiveRow metody, které by upravovaly surová data z databázové tabulky – úpravy, které není vhodné řešit v šabloně.
Já to nakonec vyřešil koncepcí stavby šablony a modelové třídy:
modelová třída:
final class ArticleModel
{
// .....
public string $basePath;
public const PhotoDir = '/photo';
public function getSrc(ActiveRow $photo): string
{
return $basePath.$this::PhotoDir.'/'.$photo->filename;
}
}
presenter:
final class ArticlePresenter extends BasePresenter
{
public function __construct(private ArticleModel $articleModel)
{}
public function actionView()
{
//....
$this->template->model = $this->articleModel;
}
}
šablona latte:
{varType App\Model\ArticleModel $model}
{var $article = $model->getOne() }
{var $photos = $model->related('photo')->order('photo.sort')}
{foreach $photos as $photo}
<img src="{$model->getSrc($photo)}">
{/foreach}
Tedy místo komplikovaného řešení jak doplnit do ActiveRow příslušné metody aplikuji na ActiveRow (obvykle v cyklu {foreach}) metodu modelové třídy. Tím, že do šablony nepředávám Selection v proměnné $photos, ale předávám celou modelovou třídu tak:
a) kód presenteru se zjednoduší a sjednotí
b) šablona je přehlednější, díky {varType} získáme nápovědu a časem
bude jistota, že je v šabloně správná modelová třída – až se
dotáhne typování šablon do konce
c) není nutné modifikovat ActiveRow samotný, což jsem pár pokusů dal, ale
nedopadlo to dobře
Místo $photo->src se použije o trochu, ale ne o moc složitější zápis $model->getSrc($photo).
Editoval m.brecher (10. 3. 3:34)
- Klobás
- Člen | 113
@mbrecher
Ahoj,
ač můžu ocenit snahu mi ukázat jiný způsob, tak tohle je přesně to, jak
to nechci používat.
Je to to samé o čem se tady psalo nahoře, v bledě modrém, ba co víc, je
to ještě horší.
V čem?
To čemu ty říkáš model a vkládáš ho do šablon, není defakto model, je spíš nějaká kolekce filtrů/helprů, navíc pojmenováno jako model – což je matoucí.
Jakmile budeš mít v šabloně referenci na vazební tabulky, musíš tam
navkládat další kolekce filtrů/helperů.
Nemáš to k dispozici automaticky jako v případě výše uvedéného
řešení.
Je to ještě aspon pro mě víc dummy řešení nebo spíš obcházení toho, že to NDB neumí než uplně první řešení a to je prohnat Entitou výpis v modelu, což nahoře padlo a já to taky tak někdy dřív dělal.
K napovídání, varType je jen Nettí věc, nikde jinde to nefunguje, resp. funguje to jen v PHP Stormu, ten nepoužívám.
Kdežto, když máš správný datový typ, ne jen prostou ActiveRow, tak každé normální hloupé IDE, ti metody z této „přepravky“ napoví, včetně properties, pokud je uvedeš nad třídou ve formátu phpdoc.
To co tady navrhl @Felix je fajn, jen chybí dopořešit ref, related a tuším ještě nějakou groupResult metodu a pak získáš luxusní možnost si případně doplnit všechny $tabulkaActiveRow a dořešit si případně věci tam a pokud pak někde vyzískáš data a předáš data do šablony, můžeš tam už všechno používat automaticky.
Možná to někdo v nějakém vlastním ORM dodělal, nezkoumal jsem. Mě se s nette database pracuje fajn, je to víceměnné jediná věco co mi tam chybí. Doctrinu nebo něco podobného používat nechci.
Jinak jak píšeš, není to snadné, ale to neznamená, že to budeme
obhácházet mnohem horší cestou :-)
Já jsem včera asi 2h zkoumal
<?php
Referencetable a referencedtable
?>
a taky jsem nevěděl jak to přesně upravit, holt jsou věci, které mohou dělat jen Ti nejlepší z nás ;-)
@felix nemáš to někde finálně dořešené?
PS. Jinak tvůj navržený kód je jak můj kod dřív mimo nette
<?php
(šablona)
foreach ($rows as $r){
$r = new Neco ($r)
echo $r->metoda z Neco
}
?>
pak jsem to vylepšil že model udělal transformaci dat přes new Neco a vrátil sadu takze už to bylo (a to tady padlo uplně na začátku)
<?php
(model, nějaká třída)
public function getRows()
{
$data = dibi:: ...
$rows = [];
foreach ($data as $row) {
$rows[] = new Neco($row);
}
return $rows
}
(šablona)
foreach ($rows as $r){
echo $r->metoda z Neco
}
?>
Obojí je prostě nic moc, 1 způsob je hrozný, 2 o něco lepší, ale oba trpí spousty nedokonalostmi.
Editoval Klobás (10. 3. 9:40)
- m.brecher
- Generous Backer | 838
@Klobás
K napovídání, varType je jen Nettí věc, nikde jinde to nefunguje, resp. funguje to jen v PHP Stormu, ten nepoužívám.
S řešením, které momentálně mám jsem maximálně spokojený, je určeno pouze pro Nette, PHPStorm a Latte.
To čemu ty říkáš model a vkládáš ho do šablon, není defakto model
Model dle definice MVC je ta část aplikace která reprezentuje business logiku. To je hodně obecná definice. V souladu s definicí MVC označuji třídy které zapouzdřují databázové tabulky suffixem Model. Ať si každý zvolí suffix jaký mu vyhovuje.
je spíš nějaká kolekce filtrů/helprů
Není, filtry a helpry mám bokem, v modelových třídách mám business logiku, příklad s getSrc() byl vyjímečný, byl to jen příklad.
Jakmile budeš mít v šabloně referenci na vazební tabulky, musíš tam navkládat další kolekce filtrů/helperů.
Vazbu na referenced table mám vyřešenou, ale ono to celé řešení je dost rozsáhlé a už OT.
NDB neumí než uplně první řešení a to je prohnat Entitou výpis v modelu
„prohnat Entitou výpis v modelu“ nevím přesně co máš na mysli?
Tvoje řešení nekritizuji, držím mu palce a jenom jsem napsal svoji zkušenost, že mě moje řešení maximálně vyhovuje.
Editoval m.brecher (10. 3. 16:47)
- fero.peterko
- Člen | 3
Ahoj, i já jsem dlouhodobě řešil problém s vlastním ActiveRow a vytvořením pseudo-ORM nad fantasticky jednoduchým Nette Database. Po určité době vývoje jsem zveřejnil https://github.com/varhall/dbino. Do verze v1 to byl celkem triviální wrapper nad Nette Database, po neuváženém updatu v Nette Database 3.2 jsem byl nucen to přepsat. Třeba v tom můžete nalézt inspiraci, já to aktivně využívám již roky.
Inspiraci jsem nacházel v Laravel nebo Yii, proto ten styl. Komentáře, že tento způsob použití je prasárna či jiné projevy nezkrotného programátorského ega ignoruji.
- David Grudl
- Nette Core | 8205
Nette Database s podporou datových typů a našeptáváním používám už dlouho.
Vygeneruju si entity, které odpovídají nejen tabulkám, ale i fungování ActiveRow. Příklad:
<?php
declare(strict_types=1);
namespace App\Entity;
use Nette\Database\Table;
/**
* @property-read int $id
* @property-read int $authorId
* @property-read string $title
* @property-read string $body
* @property-read ?\DateTimeImmutable $created
* @property-read bool $draft
* @property-read string $html
* @property-read UserRow $author
*/
final class PostRow extends Table\ActiveRow
{
}
/**
* @property-read int $id
* @property-read string $name
* @property-read string $password
* @property-read ?string $email
*/
final class UserRow extends Table\ActiveRow
{
}
Objekt PostRow
má property $post->authorId
,
která obsahuje ID autora, ale i $post->author
, ve které je
objekt UserRow
, takže lze psát
$post->author->name
.
Aby fungovalo napovídání, tak stačí přidat vhodné anotace k metodám, které vrací data z databáze. Příklad:
use App\Entity;
use Nette\Database\Table\Selection;
class Facade
{
/** @return Selection<Entity\UserRow> */
public function getUsers(): Selection
{
return $this->db->table('users');
}
}
a když napíšu kód:
foreach ($facade->getUsers() as $user) {
$user->... // IDE správně napovídá
}
Ve skutečnosti ale nepoužívám entity jenom takto na oko, nechávám si je
opravdu vracet. Tedy objekt $user
v ukázce nemá jen
napovídání pro UserRow, on opravdu UserRow je. Jediné, co je k tomu
potřeba, tak přepsat metodu Selection::createRow()
.
Chci do Nette Database přidat podporu pro vlastní mapper, který by určoval, pro jakou tabulku se má vytvořit jaká třída. Určitě to dám do verze 4.0.0