Plánuje se Exploreru přidat podporu datových typů, jako to mají formuláře a latte?

Bulldog
Člen | 110
+
+4
-

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?

Bulldog
Člen | 110
+
+1
-

Samozřejmě je tím myšlena i podpora poslat naplněný objekt do insertu/updatu, aby se nemuselo pracovat s obecnými poli, které je nutné explicitně konvertovat na objekty, nebo ověřovat integritu dat.

Editoval Bulldog (11. 11. 2021 22:02)

Felix
Nette Core | 1194
+
+6
-

Nedavno jsem resil neco podobne. Udelal jsem si misto active row vlastni “active row per tabulka”.

Urcite me to zajima. Pokud ne do nette/database. Tak klidne jako rozsireni do contributte/database.

Polki
Člen | 553
+
+1
-

Felix napsal(a):

Udelal jsem si misto active row vlastni “active row per tabulka”.

Tak se to právě aktuálně snažím řešit taky. ale přijde mi to takové krkolomné a radši bych to měl jako integrovanou podporu.

Editoval Polki (11. 11. 2021 23:44)

Bulldog
Člen | 110
+
0
-

Vidím, že to podobně řeší víc lidí.

Felix napsal(a):

klidne jako rozsireni do contributte/database.

Pokud to správně chápu, tak contributte/database není samostatná databázová knihovna, ale seznam knihoven nadstavujících/upravujících chování nette/database?

neznamy_uzivatel
Člen | 115
+
0
-

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
+
0
-

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
+
0
-

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
+
-3
-

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 typu array 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
+
+6
-

Ja to udelal takto. Rad bych to probral s @DavidGrudl a nebo pripadne hodil do contributte/database.

  1. Mrknul jsem, kde se tvori ActiveRow (new ActiveRow). A to je trida Selection (https://github.com/…election.php#L556). Metoda se jmenuje createRow a je protected. Lze pretizit.
  2. 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 vlastni tableXyz().
  3. 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,
    ]);
    
  4. 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
+
0
-

@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 typu array 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
+
+1
-

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

Felix
Nette Core | 1194
+
+3
-

Upravil jsem puvodni prispevek. Je to tam jako bod 4.

Polki
Člen | 553
+
+1
-

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
+
0
-

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
+
-1
-

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
+
0
-

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
+
+1
-

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á :)

Marek Bartoš
Nette Blogger | 1245
+
+7
-

Vás prostě baví si navzájem psát slohovky…

Polki
Člen | 553
+
0
-

@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
+
0
-

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
+
0
-

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

Bulldog
Člen | 110
+
-1
-

Možná jo, možná ne.
Bez urážení to zní jako konstruktivní debata, ke by se mohly obě strany něco přiučit a odnést si nové poznatky.
Ve všech případech to však není debata na toto vlákno a už vůbec ne na toto fórum. Vyřešte si to osobně v soukromí. Díky.

MKI-Miro
Člen | 275
+
0
-

Ahojte

Nasiel by sa teda nejaky konkretny/finalny/funkcny priklad ako to spravit?

vdaka

Klobás
Člen | 113
+
0
-

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

Klobás
Člen | 113
+
0
-

Funguje to jen na primární tabulku a její data, jakmile tam jsou navazující sloupečky na jiné tabulky (ref) nebo dokonce related, tak to nefunguje. Chjo.

m.brecher
Generous Backer | 838
+
-2
-

@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
+
0
-

@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
+
0
-

@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
+
+1
-

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
+
+8
-

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

MKI-Miro
Člen | 275
+
+1
-

Kedy sa planuje vydanie verzie 4.0.0?

David Grudl
Nette Core | 8205
+
+7
-

V říjnu

m.brecher
Generous Backer | 838
+
0
-

@feropeterko

Inspiraci jsem nacházel v Laravel nebo Yii, proto ten styl.

Díky za tip, zběžně jsem na Tvůj doplněk varhall/dbino nakouk a vypadá to zajímavě.

Editoval m.brecher (21. 9. 2:08)