Nextras\ORM – ORM nad Nextras\Dbal

goood
Člen | 26
+
0
-

hrach napsal(a):

@goood sic nevim, v cem je problem, na to mrknu pozdeji, tak upozornuji, ze delas neuveritelnou bezpecnostni chybu a prasarnicku! ma to byt

->where('[a.event_id] = %i', $eventId)

Ano ano, vim, diky :) Opravim

hrach
Člen | 1834
+
0
-

@goood je to strasne dulezite :-) i pro ostatni, aby nahodou nekopirovali nebezpecny kod…

goood
Člen | 26
+
0
-

@hrach máš pravdu, opravil jsem to. Díky

Ještě jedna technická. Zkoumal jsem ještě dokumentaci k dbal a narazil jsem na špatné odkazy na stránce http://nextras.org/…s/2.0/mapper odkazy místo na dbal vedou na orm

Editoval goood (22. 3. 2016 22:51)

pfilipek
Člen | 25
+
0
-

Zdravím,
chtěl jsem se zeptat jak to je s Nextras/ORM. Někde jsem se dočetl, že pracuje nad Nette/Database, ale v praxi mi to s ním nefunguje (je to tedy možné zprovoznit s NDtb?). Zjistil jsem že to funguje s Nextras/Dbal, ale vyhazuje mi to vyjímku Unknown or incorrect time zone: ‚Europe/Prague‘. V tomto vlákně jsem se dočetl, že se to má řešit importem názvů časových pásem do databáze, ale na produkci to bohužel nemůžeme ovlivni, jelikož máme externí správu databázového serveru. Lze to řešit i nějak jinak?

Felix
Nette Core | 1186
+
+2
-

pfilipek napsal(a):

Zdravím,
chtěl jsem se zeptat jak to je s Nextras/ORM. Někde jsem se dočetl, že pracuje nad Nette/Database, ale v praxi mi to s ním nefunguje (je to tedy možné zprovoznit s NDtb?). Zjistil jsem že to funguje s Nextras/Dbal, ale vyhazuje mi to vyjímku Unknown or incorrect time zone: ‚Europe/Prague‘. V tomto vlákně jsem se dočetl, že se to má řešit importem názvů časových pásem do databáze, ale na produkci to bohužel nemůžeme ovlivni, jelikož máme externí správu databázového serveru. Lze to řešit i nějak jinak?

Jde to vyresit napevno pres konfigurace.

applicationTz: time zone for returned DateTime objects
connectionTz: time zone for connection
simpleStorageTz: time zone for simple time stamp type

Problematika casovy zon a dalsich je popsana zde: https://nextras.org/…2.0/datetime

Muzes mrknout na Componette, kde je vyresene napevno: https://github.com/…xt/dbal.neon#L10

hrach
Člen | 1834
+
0
-

Někde jsem se dočetl, že pracuje nad Nette/Database, ale v praxi mi to s ním nefunguje

takto to bylo jeste pred stable 1.0. tj. hodne davno, vic jak pise Felix :-)

hrach
Člen | 1834
+
0
-

@pfilipek Jinak idealne nastavit auto-offset, a jeste idealni si naimportovat jmenne zony. https://nextras.org/…ysql-support

pfilipek
Člen | 25
+
0
-

@Felix: Děkuji za radu. Je to funkční.

Ještě jsem se chtěl zeptat, zda se nedá náhodou řešit přes config nastavení prefixu tabulek nebo jestli to někdo neřešil nějakým způsobem. Díky za každou radu ;)

Editoval pfilipek (5. 5. 2016 17:56)

pfilipek
Člen | 25
+
0
-

hrach napsal(a):

Někde jsem se dočetl, že pracuje nad Nette/Database, ale v praxi mi to s ním nefunguje

takto to bylo jeste pred stable 1.0. tj. hodne davno, vic jak pise Felix :-)

Díky za objasnění ;)

pfilipek
Člen | 25
+
0
-

Zdravím, chtěl jsem se zeptat na to když mám u entity property například @property array $test, tak jestli ORM umí a případně jak ukládat do databáze (např pomocí serializace) a při dotázání z DB zase vrátit pole. Pokud ne tak posílám řešení které mě zatím funguje:

<?php
  namespace app\model\orm\entities;

  use pf\datatypes\JSON;

  /**
   * Třída TestEntity.
   *
   * @property JSON $test
   */
  class TestEntity extends Entity
  {
    /**
     * Nastaví test property na objekt JSON.
     *
     * @param mixed $data
     */
    protected function setterUserSettings($data) {
      if (!is_a($data, JSON::class)) {
        $data = JSON::create($data);
      }

      return $data;
    }
  }
?>

objekt JSON je třída, která je potomkem ArrayObject, takže se s ní pracuje jako s polem a má právě magickou metodu __toString(), která zajistí že při ukládání do databáze se pole uloží v JSONu a při načtení z DB se z něj stane opět JSON object.

Více viz. zdrojový kód:

<?php
  namespace prosys\datatypes;

  use ArrayObject;
  use Tracy\Debugger;

  /**
   * Objekt reprezentujici datovy typ JSON.
   */
  class JSON extends ArrayObject implements \JsonSerializable{
    private $_json;

    public function __construct($json = NULL, $flags = 0, $iterator_class = 'ArrayIterator') {
      if (!is_array($json)) {
        $json = ((($decoded = @json_decode($json, TRUE))) ? $decoded : array());
      }

      $this->_json = (($json) ? $json : array());

      parent::__construct($json, $flags, $iterator_class);
    }

    public function jsonSerialize() {
      return $this->_json;
    }

    public function offsetExists($offset) {
      return array_key_exists($offset, $this->_json);
    }

    public function offsetGet($offset) {
      return $this->_json[$offset];
    }

    public function offsetSet($offset, $value) {
      $this->_json[$offset] = $value;
    }

    public function offsetUnset($offset) {
      unset($this->_json[$offset]);
    }

    public function append($value) {
      $this->_json[] = $value;
    }

    public function count($mode = 'COUNT_NORMAL') {
      return count($this->_json, $mode);
    }

    public function getIterator() {
      return new \ArrayIterator($this->_json);
    }

    public function __toString() {
      return json_encode($this->_json, JSON_PRETTY_PRINT);
    }

    public static function create($json) {
      return new static($json);
    }
  }
?>

Editoval pfilipek (7. 5. 2016 14:53)

Felix
Nette Core | 1186
+
0
-

pfilipek napsal(a):

Zdravím, chtěl jsem se zeptat na to když mám u entity property například @property array $test, tak jestli ORM umí a případně jak ukládat do databáze (např pomocí serializace) a při dotázání z DB zase vrátit pole. Pokud ne tak posílám řešení které mě zatím funguje:

Mrkni do dokumentace: https://nextras.org/…/conventions#…

Mohlo by se ti to hodit, presne na co potrebujes.

Pripadne muzes mrknout jeste na tohle rozsireni, je tam par zajimavych feature.

https://github.com/…xtras-ormext


Jenom takova rada, priste zkus vkladat zdrojaky bez tech phpDocu, usetrime tim hodne mista. Treba na mobilu to skoro neslo precist. xD

pfilipek
Člen | 25
+
0
-

pfilipek napsal(a):

@Felix: Děkuji za radu. Je to funkční.

Ještě jsem se chtěl zeptat, zda se nedá náhodou řešit přes config nastavení prefixu tabulek nebo jestli to někdo neřešil nějakým způsobem. Díky za každou radu ;)

Zdravím, tak jsem implementoval použití prefixu tabulek definovaného v config.neon. V první řadě jsem si vytvořil MyMapper, který dědí od \Nextras\Orm\Mapper\Mapper a prefix jsem si ošetřil v něm viz následující ukázka:

<?php
  namespace app\orm;

  use Nette\Caching\Cache;
  use Nextras\Dbal\Connection;
  use Nextras\Dbal\InvalidArgumentException;
  use Nextras\Orm\Mapper\Mapper;
  use pf\common\Functions;

  /**
   * Hlavní mapper ORM systému.
   */
  abstract class MyMapper extends Mapper
  {
    private $prefix = NULL;
    protected $tableName = NULL;

    public function __construct(Connection $connection, Cache $cache) {
      parent::__construct($connection, $cache);

      $this->prefix = Functions::item($connection->getConfig(), 'prefix');
    }

    /**
     * Vrátí název tabulky.
     *
     * @return string
     */
    public function getTableName() {
      if (is_null($this->tableName) || !$this->tableName) {
        throw new InvalidArgumentException('Musí být zadán název tabulky pro mapper: ' . get_called_class());
      }
      return $this->prefix . $this->tableName;
    }
  }
?>

Třída Functions sdružuje užitečné funkce a zde použitá funkce item jen ošetřuje to, že pokud se požadovaný klíč v předaném poli nenachází, tak vrátí výchozí hodnotu (v default NULL, ale jinak se dá předa jako třetí parametr funkce).
config.neon potom použijete klíč prefix v sekci dbal.

Editoval pfilipek (7. 5. 2016 14:53)

Paradiso
Člen | 101
+
0
-

Ahoj, chtěl bych se zeptat, jaká je best practice, v případě že mám entitu user a potřeboval bych jí považovat na customer a seller

v entitě order chci odkazovat na customer a seller.

oboje se bere ze stejné tabulky

napadlo mě podětit si user a udělat entity customer a seller, ale to mi stejně zařvalo, že mám duplicitní entitu

doufám, že jsem to nějak tak rozumně popsal, děkuji předem za odpověď

pfilipek
Člen | 25
+
0
-

Zdravím,

chtěl jsem se zeptat, jak vyřešit tento problém:
Mám entitu Country a CountryTranslation. Problém je v tom, že potřebuju nyní řadit entitu Country podle property name z entity CountryTranslation. Entity mám nadefinované následovně:

Country:

/**
 * @property-read int $id {primary}
 * @property string                                  $isoCode
 * @property string                                  $isoCode2
 * @property JSON                                    $addressFormat
 * @property bool                                    $mandatoryZipcode
 * @property bool                                    $disabled
 *
 * @property ManyHasOne|CountryTranslation[]   $translations {1:m CountryTranslation::$country}
 */
class Country extends TranslationEntity
{
}

CountryTranslation:

/**
 * @property-read int       $id        {primary}
 * @property      Language  $language  {m:1 Language, oneSided=TRUE}
 * @property      Country   $country   {m:1 Country::$translations}
 */
class CountryTranslation extends MainEntity
{
}

Nevím si už rady jak docílit toho řazení. Někde jsem četl že přes Mapper ale netuším zatím jak na to. Můžete někdo aspoň trochu naznačit řešení? Díky moc.

Jan Tvrdík
Nette guru | 2595
+
0
-

Rád bych ti pomohl, ale bohužel nechápu kde a podle čeho to chceš řadit.

potřebuju nyní řadit entitu Country podle property name z entity CountryTranslation.

Přečetl jsem to 10× a pořád to nechápu. Každá země má několik překladů a ty chceš seřadit ty země podle názvu (jednoho?) jejich překladu? Myslíš, že bys mi to mohl vysvětlit na příkladu?

pfilipek
Člen | 25
+
0
-

chceš seřadit ty země podle názvu (jednoho?) jejich překladu?

Ahoj,
pochopil jsi to správně, každá země má n překladů podle jazyků v DB. Takže moje otázka zní když budu mít aktivní jazyk čeština tak potřebuji řadit podle názvu země pro jazyk čeština a když slovenština, tak zase řadit podle názvu překladu slovenština.

Díky moc

Jan Tvrdík
Nette guru | 2595
+
0
-

Teď jsem to teprve (snad) pochopil – ty chceš řadit názvy zemí podle jejich jména v aktivním jazyce, tj. např. seřadit země podle jejich českého nebo slovenského názvu.

Pak by mělo stačit

$countries = [];
foreach ($countryTranslations->findBy(['language' => $activeLanguage])->orderBy('name') as $ct) {
	$countries[] = $ct->country;
}
pfilipek
Člen | 25
+
0
-

a jak to zakomponovat do Nextras/Datagrid, protože tam to dělám tak, že poskytuji data jako kolekci entit Country a ty vypisuji v šabloně, kde se odkazuji na překlad dle aktuálního jazyka. Sloupec mám pojmenovaný name, takže když mi přijde do sourceCallbacku order s nazvem sloupce name, tak to musim poskytnout jinym zpusobem ta data než když to dělám obvykle že? A ještě mi teď došlo co když budu chtít řadit podle názvu z překladu (CountryTranslation) a zároveň podle isoCode (Country) to už nebude tak easy že? Mě napadlo to nechat na mapperu a vyřešit to nějak přes join ale zatím v tom tápu a nevím jak a co předat.

hrach
Člen | 1834
+
0
-

pripadne:

$countries = $orm->countries->findBy(['this->translation->lang' => $activeLanguage])
     ->orderBy('this->translation->name');
Jan Tvrdík
Nette guru | 2595
+
0
-

kolekci entit Country

Můžeš to pole obalit objektem ArrayCollection. Nebo (pokud bys to potřeboval třeba dynamicky stránkovat, tak) to můžeš samozřejmě přepsat do SQL a dát do mapperu.

A ještě mi teď došlo co když budu chtít řadit podle názvu z překladu (CountryTranslation) a zároveň podle isoCode (Country)

Nechápu, to ty přeložené názvy nejsou unikátní? Slovo „zároveň“ používáš ve významu „sekundárně“? Mělo by snad fungovat i něco jako

$countryTranslations
	->findBy(['language' => $activeLanguage])
	->orderBy(['name', 'this->country->isoCode']);
Jan Tvrdík
Nette guru | 2595
+
0
-

@hrach To funguje i když ta země má těch překladů n? Tj. ORM pak nad tím udělá DISTINCT?

pfilipek
Člen | 25
+
0
-

Zdravim,
tak jsem zkoušel to co napsal @hrach

pripadne:

$countries = $orm->countries->findBy(['this->translation->lang' => $activeLanguage])
     ->orderBy('this->translation->name');

a nefunguje. Píše to následující chybu Cannot order by ‚this->translations->name‘ expression, includes has many relationship, což je logické, protože ORM neví podle kterého překladu to řadit.

to @Jan Tvrdík:
no ty překlady nemusí být unikátní, tady u překladu státu nejspíš ano, ale co když se to bude týkat překladu tabulky výrobků tam už jeden výrobek může být pro dva jazyky přeložen stejně, například čeština a slovenština mají některá slova totožná.

Editoval pfilipek (23. 5. 2016 8:46)

pfilipek
Člen | 25
+
0
-

Tak jsme to teďka ještě s kolegou konzultovali a mě napadlo po přečtení příspěvku od @Jan Tvrdík, že i když chci vypisovat na výstup země, tak bude lepší k tomu přistupovat ze strany překladů zemí (CountryTranslation), protože každý překlad vidí právě na svoji konkrétní zemi a přes ni můžu jak filtrovat, tak i řadit pokud se nepletu:

$countryTranslations = $orm->countryTranslations->findBy(['language' => $activeLanguage, 'this->country->isoCode' => $filter['isoCode']])
     ->orderBy('name')
     ->orderBy('this->country->isoCode', ICollection::DESC);

Editoval pfilipek (23. 5. 2016 9:13)

pfilipek
Člen | 25
+
0
-

pfilipek napsal(a):

Tak jsme to teďka ještě s kolegou konzultovali a mě napadlo po přečtení příspěvku od @Jan Tvrdík, že i když chci vypisovat na výstup země, tak bude lepší k tomu přistupovat ze strany překladů zemí (CountryTranslation), protože každý překlad vidí právě na svoji konkrétní zemi a přes ni můžu jak filtrovat, tak i řadit pokud se nepletu:

$countryTranslations = $orm->countryTranslations->findBy(['language' => $activeLanguage, 'this->country->isoCode' => $filter['isoCode']])
     ->orderBy('name')
     ->orderBy('this->country->isoCode', ICollection::DESC);

problém u tohoto řešení je, že pokud některá země nebude mít překlad pro aktuální jazyk tak se mi nezobrazí. Jak na to? Chtěl bych, aby se to chovalo následovně:

Výchozí parametry:

  • Výchozí jazyk systému je: čeština
  • Aktuální jazyk je: sloveština
  • Země mají pro výchozí jazyk překlady

Měly by se zobrazovat všechny země tak, že pokud nebude existovat překlad pro aktuální jazyk vezme se překlad pro výchozí jazyk.

Editoval pfilipek (23. 5. 2016 9:53)

hrach
Člen | 1834
+
0
-

Sorry za zmatky, uz chapu, proc to nejede :/ Namet na vylepseni ORM.

pfilipek
Člen | 25
+
0
-

hrach napsal(a):

Sorry za zmatky, uz chapu, proc to nejede :/ Namet na vylepseni ORM.

mohl bych poprosit o nastínění (nakopnutí) jak by se to dalo řešit, já bych to zkusil naimplementovat a hodil bych potom pullrequest na git, abych aspoň trochu pomohl.

Jan Tvrdík
Nette guru | 2595
+
0
-

@pfilipek Myslím, že to co chceš, je na natolik složité, že buď musíš načíst zároveň překlady pro výchozí i aktivní jazyk a v PHP si to pak proiterovat a vzít si z toho překlad pro aktivní jazyk, pokud je k dispozici, a jinak vzít překlad pro výchozí jazyk.

$defaultLanguage = 'cs';
$activeLanguage = 'sk';

$countryTranslations = $orm->countryTranslations
	->findBy([
		'language' => [$activeLanguage, $defaultLanguage],
		'this->country->isoCode' => $filter['isoCode']]
	)
	->orderBy('name')
	->orderBy('this->country->isoCode', ICollection::DESC);

Nebo bys musel položit dotazy dva a načíst v prvním překlady pro aktivní jazyk a v druhém překlady pro výchozí jazyk, které jsi ještě nenačetl v prvním dotazu.

Nebo, což mi připadá asi jako nejlepší možnost, je napsat si to v SQL v mapperu, kde to jde udělat jedním (byť netriviálním) dotazem.

pfilipek
Člen | 25
+
0
-

Díky moc za návod, tvůj návod

$defaultLanguage = 'cs';
$activeLanguage = 'sk';

$countryTranslations = $orm->countryTranslations
	->findBy([
		'language' => [$activeLanguage, $defaultLanguage],
		'this->country->isoCode' => $filter['isoCode']]
	)
	->orderBy('name')
	->orderBy('this->country->isoCode', ICollection::DESC);

je super, akorát se mi dublují záznamy což je logické když udělám language IN (‚cs‘, ‚sk‘), ale potřeboval bych tím pádem udělat nějké GROUP BY pro seskupení těch záznamů.

SQL dotaz v Mapperu není problém, ale nevím co má mapper vracet, tak aby se mi to chovalo korektně a hlavně když budu chtít použít nějaké aliasy, tak se s tím už bude hůře pracovat v řazení atd. Nebude to vůbec použitelné pro napojení na Nextras\Datagrid.

Ještě dotaz na metody v mapperu:
Někde jsem četl, že když udělám metodu v Mapperu, tak stačí potom v repozitáři dát metodu do anotace třídy (předpokládám, že jen kvůli nápovědě) a mělo by být vše funkční, ale u mě není → vyhodí to chybu Call to undefined method prosys\model\orm\CountryRepository::test().

Jan Tvrdík
Nette guru | 2595
+
0
-

Nebude to vůbec použitelné pro napojení na Nextras\Datagrid.

Nesmysl, z pohledu datagridu nejde poznat, jestli se pro sestavené té kolekce použil dotaz z mapperu nebo ne. Pokud ho dobře napíšeš.

stačí potom v repozitáři dát metodu do anotace třídy

Mám dojem, že musí začínat ne get nebo find.

pfilipek
Člen | 25
+
0
-

@JanTvrdík Vyzkoušel jsem tvoji radu

Mám dojem, že musí začínat ne get nebo find.

ale bohužel je nefunkční. Zkusil jsem si pojmenovat vlastni metodu getMyfindMy a pokaždé mi to zahlásilo chybu, že volám neznámou metodu na repozitář.

goood
Člen | 26
+
+1
-

pfilipek napsal(a):

@JanTvrdík Vyzkoušel jsem tvoji radu

Mám dojem, že musí začínat ne get nebo find.

ale bohužel je nefunkční. Zkusil jsem si pojmenovat vlastni metodu getMyfindMy a pokaždé mi to zahlásilo chybu, že volám neznámou metodu na repozitář.

Tady je to popsano https://nextras.org/…0/repository . Me to teda funguje dobre

<?php
/**
 * @method ICollection|Book[] findBooksWithEvenId()
 */
final class BooksRepository extends Repository
{
    // ...
}


final class BooksMapper extends Mapper
{
    public function findBooksWithEvenId()
    {
        return $this->builder()->where('id % 2 = 0');
    }
}
?>
pfilipek
Člen | 25
+
0
-

@goood Díky, tak už jsem pochopil použití a funguje.

Akorát pořád stojím na tom mém problému s překlady. Nevím si s tím rady. To co mi poradil @JanTvrdík (přistupovat ze strany překladů) je nepoužitelné, pokud daná entita nemá překlad pro aktuální jazyk. Když totiž použiji podmínku ...->findBy(['language' => [$activeLanguage, $defaultLanguage]); tak se mi vrátí překlady pro oba jazyky – logicky ;). Já bych ovšem potřeboval vrátit vždycky všechny entity, a ke každé entitě mít překlad aktuálního jazyka a pokud neexistuje, tak se vezme výchozí překlad (ten musí vždycky existovat).

Zkuste mě aspoň někdo nakopnout :) Díky moc.

Editoval pfilipek (24. 5. 2016 13:44)

Jan Tvrdík
Nette guru | 2595
+
0
-

@pfilipek Řešíš tady X dní jak napsat velmi složitý dotaz pomocí ORM místo toho, abys ho během 15 minut napsal v SQL.

goood
Člen | 26
+
0
-

pfilipek napsal(a):

@goood Díky, tak už jsem pochopil použití a funguje.

Akorát pořád stojím na tom mém problému s překlady. Nevím si s tím rady. To co mi poradil @JanTvrdík (přistupovat ze strany překladů) je nepoužitelné, pokud daná entita nemá překlad pro aktuální jazyk. Když totiž použiji podmínku ...->findBy(['language' => [$activeLanguage, $defaultLanguage]); tak se mi vrátí překlady pro oba jazyky – logicky ;). Já bych ovšem potřeboval vrátit vždycky všechny entity, a ke každé entitě mít překlad aktuálního jazyka a pokud neexistuje, tak se vezme výchozí překlad (ten musí vždycky existovat).

Zkuste mě aspoň někdo nakopnout :) Díky moc.

Tak si to napiš přímo v SQL, to asi bude nejjednodušší

pfilipek
Člen | 25
+
0
-

@JanTvrdík Díky za názor, ale pro mě v tu chvíli nemá význam asi používat ORM, protože v systému budu mít cca 70% entit, které mají k sobě překladové entity. Chtěl jsem využívat ORM, protože mi to přišlo lepší než obyčejné SQL, nebo Nette Database, ale jak se tak dívám, tak mi asi nic jiného nezbývá, protože jak vidno můj problém je v ORM neřešitelný.

hrach
Člen | 1834
+
+2
-

@pfilipek

Díky za názor, ale pro mě v tu chvíli nemá význam asi používat ORM

zpusob vytazeni dat prece nic nerika o tom, ze nemuzes vyuzivat benefitu orm;
to ale porad nemeni nic na tom, ze zrejme muzes vyuzit QueryBuilder, ktery poskytuje dostatecnou abstrakci a posleze se porad da prevest na klasicke ICollection.

Gappa
Nette Blogger | 199
+
+1
-

Ahoj,

mám dotaz ohledně jazykových mutací. Aktuálně je mám řešené obyčejným dotazem pomocí dvou tabulek – jedna obsahuje „společné věci“, druhá pouze věci, které se musí překládat a přes ID z „nejazykové“ tabulky jsou oba záznamy provázané.

Ideální by bylo, kdyby se pak celek choval jako jedna entita – tedy např. aby $article->image byl z nejazykové tabulky, $article->title z jazykové.

  1. Je to vůbec v ORM možné udělat? Protože nějak by se musel předávat jazyk a na jeho základě automaticky vytáhnout z druhé tabulky překlad (pokud v požadovaném jazyku existuje) a „jedna entita byla ze dvou tabulek“.
  2. Pokud to možné je – je to vůbec dobrý nápad? :)
  3. Existuje nějaký lepší způsob, jak řešit v DB překlady?

Díky za jakékoliv postřehy :)

(Debatu výše jsem sledoval, ale přijde mi, že se řešilo trošku něco jiného.)

pfilipek
Člen | 25
+
+1
-

@Gappa Něco co potřebuješ mám již vyřešeno. Udělal jsem si obecnou TranslationEntity, od které dědí všechny entity, které mají překladovou entitu více napoví níže uvedený kód:

namespace prosys\model\orm\entities;

  use Nextras\Orm\InvalidArgumentException;
  use Nextras\Orm\InvalidStateException;
  use prosys\services\LanguageService;

  abstract class TranslationEntity extends MyEntity
  {
    /** @var LanguageService */
    protected $languageService;

    /**
     * Injectnutí LanguageService.
     * @param LanguageService $languageService
     */
    public function injectTranslator(LanguageService $languageService) {
      $this->languageService = $languageService;
    }

    /**
     * Vrátí překlad podle jazyka.<br />
     * Pokud jazyk není předán, vezme se výchozí.
     *
     * @param string $code
     * @return ProSYSEntity
     */
    public function getTranslation($code = NULL) {
      $language = NULL;
      if (!isset($this->translations)) {
        throw new InvalidArgumentException('Entita ' . get_called_class() . ' nemá definovanou property translations s vazbou na překladovou tabulku.');
      }

      if (!is_null($code)) {
        try {
          $language = $this->languageService->getLanguageByCode($code);
        } catch (InvalidArgumentException $e) {
          throw new InvalidArgumentException($e->getMessage());
        }
      }

      if (is_null($language)) {
        $language = $this->languageService->getCurrentLanguage();
      }

      $translation = $this->translations->get()->findBy(['language' => $language->id])->fetch();
      if (!$translation) {
        throw new InvalidArgumentException('<b>' . get_called_class() . ' (id: ' . $this->id . ')</b>', 'Následující entity nemají překlad pro jazyk: <b>' . $language->name . '</b>');
        $translation = $this->translations->get()->findBy(['language' => $this->languageService->getDefaultLanguage()])->fetch();
      }

      return $translation;
    }

    /**
     * Vyzkouší získat property způsobem, který definuje rodič, v případě výjimky vyzkouší, zda se nejedná o překladovou property v aktuálním jazyku a jinak vyhodí obě výjimky.
     *
     * @param $name
     * @return mixed
     */
    public function &__get($name) {
      $translate = function($name, $msgs = []) {
        try {
          return $this->getTranslation()->$name;
        } catch (InvalidArgumentException $e) {
          $msgs[] = $e->getMessage();
          throw new InvalidArgumentException(implode(' --> ', $msgs));
        }
      };

      try {
        $value = parent::__get($name);
      } catch (InvalidArgumentException $e) {             // neni-li uvedena v anotaci entity
        $value = $translate($name, [$e->getMessage()]);
      } catch (InvalidStateException $e) {                // je-li v anotaci entity jako virtual
        $value = $translate($name, [$e->getMessage()]);
      }

      return $value;
    }

A tady je použití v překladatelné entitě:

namespace prosys\model\orm\entities;

use prosys\datatypes\JSON;

/**
 * Třída CountryEntity - objektová reprezentace země systému.
 *
 * @property-read int $id {primary}
 * @property string                                  $isoCode
 * @property string                                  $isoCode2
 * @property JSON                                    $addressFormat
 * @property bool                                    $mandatoryZipcode
 * @property bool                                    $disabled
 *
 * @property ManyHasOne|CountryTranslationEntity[]   $translations {1:m CountryTranslationEntity::$country}
 *
 * @property-read string $name         {virtual}
 * @property-read string $description  {virtual}
 * @property-read string $help         {virtual}
 */
class CountryEntity extends ProSYSTranslationEntity
{
}

LanguageService je služba, která se mi stará o poskytování jazyků.

Funguje to tak, že překladatelná entita musí mít property $translations a potom se s tím pracuje následovně:

	$country = $orm->countries->getById(2);
	$country->getTrasnlation()->name; // vrátí název pro aktuální jazyk
	$country->getTrasnlation('en')->name; // vrátí název pro anglický jazyk

Editoval pfilipek (25. 5. 2016 8:31)

hrach
Člen | 1834
+
+1
-

@Gappa :

  1. ano da, napriklad jak psal pfilipek; nebo tak, ze si napises gettery, ktere budou slouzit jako proxy na subentitu.
  2. imo tezko rict, sam s preklady tak velke zkusenosti nemam, ale nevidim tam zadny zasadni problem.
  3. treba si muzes udelat jen „readonly“ entity, ktere budes tahat z view, ktere ti sestavi db.

@pfilipek urcite neni dobre ridit beznou funkcionalitu pomoci vyjimek

pfilipek
Člen | 25
+
0
-

@hrach Tak jsem si udělal dotaz do DB v maperru:

public function findByLanguage(LanguageEntity $language) {
  $query = $this->builder()->from($this->getTableName(), 'c')
                         ->addSelect('c.*')
                         ->addSelect('CASE WHEN t.`name` IS NULL THEN d.`name` ELSE t.`name` END AS name')
                         ->leftJoin('c', 'country_translations', 't', 't.country_id = c.id AND t.language_id = %i', $language->id)
                         ->leftJoin('c', 'country_translations', 'd', "d.country_id = c.id AND d.language_id = (SELECT id FROM languages WHERE is_default = 1)");

  return $query;
}

vrátí mi to ICollection, ale problém nastane když chci na to zavolat orderBy('name') (sloupec, který mi vzniká pomocí CASE). Jak jsem zjistil, tak Dbal automaticky přidá k požadovanému sloupci alias z from tabulky a to vyhodí vyjímku Unknown column ‚c.name‘ in ‚order clause‘ což chápu, protože takový sloupec v tabulce countries c opravdu neexistuje.

Ještě se zeptám, kdybych to chtěl řešit pomocí DB view (pohledu), tak jak to mám udělat abych mohl i ukládat data, protože nad view samozřejmě nemůžu provádět INSERT, UPDATE a DELETE?

Editoval pfilipek (25. 5. 2016 12:33)

pfilipek
Člen | 25
+
0
-

@hrach Ještě bych se chtěl zeptat na to, jak správně řídit běžnou funkcionalitu jak zmiňuješ zde ?

hrach
Člen | 1834
+
0
-

@pfilipek

  • case & name: no a nemuzes tam dat as c.name? ICollection umi pracovat jen se strukturou entity, tzn. jen diky tomu, ze tam je takovy sloupec ti to funguje, ale tim hackem by to mohlo fungovat.
  • view: tam je to prave ten vtip, ze by to bylo jen read-only. byla to jen idea, nevim, jestli je to dobry napad.
  • rizeni flow – pres navratove hodnoty, nebo argumenty pasnuty jako reference. uplne presne jsem nezkoumal tvuj priklad, ale urcite to nejak pujde.
Jan Tvrdík
Nette guru | 2595
+
+1
-

@pfilipek Nepomohlo by to obalit dalším selectem, tj. něco jako

	public function findByLanguage(LanguageEntity $language)
	{
		$query = $this->builder()->from($this->getTableName(), 'c')
			->addSelect('c.*')
			->addSelect('CASE WHEN t.`name` IS NULL THEN d.`name` ELSE t.`name` END AS name')
			->leftJoin('c', 'country_translations', 't', 't.country_id = c.id AND t.language_id = %i', $language->id)
			->leftJoin('c', 'country_translations', 'd', "d.country_id = c.id AND d.language_id = (SELECT id FROM languages WHERE is_default = 1)");

		return $this->builder()->from($query->getQuerySql(), 'x', ...$query->getQueryParameters());
	}
pfilipek
Člen | 25
+
0
-

@hrach

  • case & name: Tak jsem vyzkoušel doporučený hack ale stále to nefunguje. Stále to vyhazuje stejnou chybu.
  • view: a můžu se zeptat jestli máš nápad? protože mě přijde blbost dělat kvůli tomu dvě entity, dva repositáře a dva mappery (jede pro obsluhu view – SELECT a druhý pro zbylé operace INSERT, UPDATE a DELETE)
pfilipek
Člen | 25
+
0
-

@JanTvrdík tak to taky bohužel nefunguje. Vyhazuje to Error: Object of class Nextras\Dbal\QueryBuilder\QueryBuilder could not be converted to string. což chápu → asi není implementována metoda __toString(). Nicméně jsem zkusil i toto

	$this->builder()->from($query->getQuerySql(), 'x');

a vyhodí to že není předán parametr pro zástupný znak %i Missing query parameter for modifier %i. search.

Edit: jo díky jsem si nevšim toho třetího parametru.

Editoval pfilipek (25. 5. 2016 13:04)

pfilipek
Člen | 25
+
0
-

@JanTvrdík Tak díky tvoje řešení je funkční. Jdu to testnout jak se to bude chovat dál v systému. Díky

Gappa
Nette Blogger | 199
+
0
-

@pfilipek – díky, prozkoumám to.
@hrach – díky za odpovědi.

Ideální stav by byl, že by entita řešila jak načítání, tak ukládání dat, aby nebylo poznat, že se reálně pracuje se dvěma tabulkami.

pfilipek
Člen | 25
+
0
-

@Gappa

Ideální stav by byl, že by entita řešila jak načítání, tak ukládání dat, aby nebylo poznat, že se reálně pracuje se dvěma tabulkami.

No úplně ideální to není, protože je lepší mít pro překlady zvlášť entitu, protože překladů máš většinou víc (podle počtu jazyků). Je to lepší z toho důvodu, že přes vazbu ManyHasOne si to pohodlně napáruješ a potom už můžeš jednoduše vypisovat v šabloně např:

<div id="someEntity_{$someEntity->id}">
	ID: {$someEntity->id}
	Překlady:
	<ul n:inner-foreach="$someEntity->translations as $translation">
		<li>Jazyk: {$translation->language->name}: {$translation->name}</li>
	</ul>
</div>

při definici entit:

/**
 * @property      string                                  $id           {primary}
 * @property      string                                  $version
 * @property      bool                                    $installed
 *
 * @property      ManyHasOne|SomeTranslationEntity[]      $translations {1:m SomeTranslationEntity::$some, cascade=[persist, remove]}
 *
 * @property-read string                                  $name         {virtual}
 * @property-read string                                  $description  {virtual}
 * @property-read string                                  $help         {virtual}
 */
class SomeEntity extends Entity
{
  /**
   * @param string    $id
   * @param string    $version
   * @param bool      $installed
   *
   * @return SomeEntity
   * @throws InvalidArgumentException
   */
  public static function create(string $id = '', string $version = '', bool $installed = FALSE): Entity {
    if (!$id || !$version) {
      throw new InvalidArgumentException('id a verze jsou povinné parametry');
    }

    $entity = new SomeEntity();

    $entity->id = $id;
    $entity->version = $version;
    $entity->installed = $installed;

    return $entity;
  }
}

/**
 * @property-read int             $id        {primary}
 * @property      ExtensionEntity $extension {m:1 SomeEntity::$translations}
 * @property      LanguageEntity  $language  {m:1 LanguageEntity, oneSided=TRUE}
 * @property      string          $name
 */
class SomeTranslationEntity extends TranslationEntity
{
  /**
   * @param LanguageEntity $code
   * @param string $name
   *
   * @return SomeTranslationEntity
   * @throws InvalidArgumentException
   */
  public static function create(LanguageEntity $language = NULL, string $name = ''): Entity {
    if (!$language || !$name) {
      throw new InvalidArgumentException('Entita jazyka a překlad názvu rozšíření jsou povinné parametry');
    }

    $entity = new SomeTranslationEntity();

    $entity->language = $language;
    $entity->name = $name;

    return $entity;
  }
}

V entitách mám vytvořené i továrničky, aby se to lépe používalo potom při ukládání atd …

a naopak zase při ukládání je to taky triviální a ještě navíc uzavřené v transakci, takže když by se něco pokazilo, tak se defacto nic nestane více příklad

	$someEntity = new SomeEntity();
	$someEntity->version = '1.0';
	$someEntity->installed = TRUE;

	$someEntity->translations->add(SomeTranslationEntity::create($currentLanguageEntity, 'Název nějaké entity'));

	// ulozeni hlavni entity vč. překladu
	$orm->someRepository->persistAndFlush($someEntity);
Hanz25
Člen | 38
+
0
-

Ahoj,

zkouším 2.0.2. a zatím to vypadá dobře. Jen mi skáče exception

Nextras\Dbal\InvalidArgumentException
Modifier %dts does not allow NULL value, use modifier %?dts instead.

data vybírám takto

$this->findBy(["published!=" => NULL, "published<" => new \DateTime]);

Definice entity

/**
 * Blog
 * @property int $id {primary}
 * @property string $title
 * @property string $nameClean
 * @property string $text
 * @property int $views
 * @property User|NULL $author {m:1 User::$blogs}
 * @property DateTime|NULL $published
 */
class Blog extends Entity
{
}

cache smazaná, když si dumpnu výsledek, a proklikám se k informacím o propertě published, tak je tam nullable ⇒ true

Editoval Hanz25 (9. 6. 2016 12:31)

Hanz25
Člen | 38
+
+1
-

@Hanz25
Aha, už jsem to našel. Hledal jsem, kde se ten modifier tvoří a pak jsem zjistil, že se vybírá oproti databázi. A měl jsem hloupou chybu v tom, že ten sloupec nebyl v databázi nastavený na nullable.

Editoval Hanz25 (15. 6. 2016 9:45)