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

Bulldog
Člen | 110
+
+3
-

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 | 1173
+
+5
-

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 | 70
+
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 | 695
+
-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 | 1173
+
+4
-

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 | 1173
+
+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 | 695
+
-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 | 695
+
+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 | 1072
+
+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 | 695
+
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 | 257
+
0
-

Ahojte

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

vdaka