Kdyby/Doctrine – ako na to?
- duskohu
- Člen | 778
Caute, po precitani par calnakov:
Rozbehal som si Kdyby/Doctrine, Vytvoril som si Entitu:
namespace Nas\UsersModule\Model;
use Doctrine\ORM\Mapping as ORM;
use Nette\Object;
/**
* @ORM\Entity
* @ORM\Table(name="users")
*/
class User extends Object
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
* @var int
*/
protected $id;
/**
* @ORM\Column
* @var string
*/
protected $name;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name
* @return User
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
}
Na zaklade Kdyby/Doctrine best practice aj UsersFacade, ktoru som si zaregistroval ako sluzbu a injectujem si ju kde potrebujem.
namespace Nas\UsersModule\Model;
use Kdyby\Doctrine\EntityDao;
use Kdyby\Doctrine\EntityManager;
use Nette\Object;
class UsersFacade extends Object
{
/** @var EntityDao */
private $users;
public function __construct(EntityManager $em)
{
$this->users = $em->getDao('\Nas\UsersModule\Model\User');
}
public function findAll()
{
return $this->users->findAll();
}
}
Popripade ked nechcem pouzivat Facade tak si injectnem do presentru EntityManager a userov si vytiahnem:
$users = $this->em->getDao('\Nas\UsersModule\Model\User');
$userList = $users->findAll();
A ako to cele pouzivat?
1. Ako pouzit VisualPaginator?
Editoval duskohu (6. 3. 2014 11:34)
- akadlec
- Člen | 1326
Třeba takto:
// Get all available user emails
$messages = $this->messagesSearching->findAllByUserInFolder($this->userEntity, $this->folderEntity);
if ( $messages instanceof \Kdyby\Doctrine\ResultSet) {
// Get messages paginator
$paginator = $this['paginator']->getPaginator();
// Set max items per page
$paginator->itemsPerPage = $this->itemsPerPage;
$messages = $messages
->applySorting(array('m.created DESC'))
->applyPaginator($paginator)
->getIterator(\Doctrine\ORM\AbstractQuery::HYDRATE_OBJECT);
} else {
// Store info message
$this->flashMessage("mailbox.emails.messages.addressCouldNotBeLoaded", "warning");
throw new BadRequestException;
}
kde search funkce je cca takto:
public function findAllByUserInFolder(IUser $user, IFolder $folder)
{
return $this->dao->fetch(new FindMessagesByUserInFolderQuery($user, $folder));
}
a využívá querybuilder kdyby:
class FindMessagesByUserInFolderQuery extends \Kdyby\Doctrine\QueryObject
{
/**
* @var IUser
*/
private $user;
/**
* @var IFolder
*/
private $folder;
/**
* @param IUser $user
* @param IFolder $folder
*/
function __construct(IUser $user, IFolder $folder)
{
$this->user = $user;
$this->folder = $folder;
}
/**
* @param \Kdyby\Persistence\Queryable $repository
*
* @return \Doctrine\ORM\Query|\Doctrine\ORM\QueryBuilder
*/
protected function doCreateQuery(Kdyby\Persistence\Queryable $repository)
{
return $repository->createQueryBuilder()
->select('m')
->from(Message::getClassName(), 'm')
->where('m.user = :user')
->andWhere('m.folder = :folder')
->orderBy('m.created', 'DESC')
->setParameter('user', $this->user)
->setParameter('folder', $this->folder);
}
}
- akadlec
- Člen | 1326
to není objekt kvůli paginatoru ale jednoduše mám fasády rozděleny podle typu na:
- NecoFacade – pro práci s konkrétní jednou entitou
- NecoSearching – pro práci se sadou entit, třeba pro načtení sady uživatelů kteří vyhovují konkrétní podmínce atd
- NecoStats – pro zjištování nějakých statistických dat, např. počet uživatelů pro danou podmínku
QueryObject je objekt pro vytváření samotných custom query
co jsem se ti snažil ukázat je jak získáš ResultSet kterému pak stačí předat paginator a on ti vrátí potřebná data.
- Jiří Nápravník
- Člen | 710
QueryObjectr je fajn, ale pokud se ti nechce delat na kazdou query dalsi objekt jako me, tka to muzes mit nejak takto:
public function actionDefault(){
$paginator = $this['categoryPaginator']->getPaginator();
$this->template->articles = $doctrinePaginator = $this->articleFacade->getArticlesForCategoryPaginator($category, $paginator->getOffset());
$paginator->setItemCount(count($doctrinePaginator));
}
protected function createComponentCategoryPaginator()
{
$vp = $this->visualPaginatorFactory->create();
$vp->getPaginator()->setItemsPerPage(4);
return $vp;
}
v te fasade samotne potom jenom vratis Doctrine Paginator:
public function getPublishedArticlesPaginator($firstResult, $maxResults = 50)
{
$qb = $this->articleDao->createQueryBuilder();
....
$paginator = new Paginator($qb);
return $paginator;
}
- David Matějka
- Moderator | 6445
je taky mozny pouzit Kdyby\Doctrine\ResultSet bez query objectu
public function getPublishedArticles()
{
$qb = ....
return new ResultSet($qb->getQuery());
}
na ResultSet-u muzes pak volat applyPaginator atd
- Jiří Nápravník
- Člen | 710
matej21: díky za tip! s tí mto vypadá mnohem hezčeji
Editoval Jiří Nápravník (7. 3. 2014 0:24)
- duskohu
- Člen | 778
Pani dakujem, este mi niesu jasne 2 veci
mam teda hore spomenutu userEntitu
, fasadu som urobil takto:
class UsersFacade extends Object
{
/** @var EntityDao */
private $usersDao;
public function __construct(EntityManager $em)
{
$this->usersDao = $em->getDao('\Nas\UsersModule\Model\User');
}
/**
* @return ResultSet
*/
public function findAll()
{
$qb = $this->usersDao->createQueryBuilder()
->from('\Nas\UsersModule\Model\User', 'u')
->select('u');
return new ResultSet($qb->getQuery());
}
}
fasadu si injectnem do presentra a pouzijem applyPaginator:
/**
* @autowire
* @var \Nas\UsersModule\Model\UsersFacade
*/
protected $usersFacade;
...
public function renderDefault()
{
$itemsPerPage = 10;
$users = $this->usersFacade->findAll();
// Pagination
/** @var VisualPaginator $vp */
$vp = $this['vp'];
$paginator = $vp->getPaginator();
$paginator->itemsPerPage = $itemsPerPage;
$users->applyPaginator($paginator);
$this->getTemplate()->users = $users->getIterator();
}
- je nutne v UsersFacade vytvarat userDao ked nasledne vo findAll zase musim definovat ->from(‚\Nas\UsersModule\Model\User‘, ‚u‘), alebo co je na tomto zle, lebo nejako sa mi nezda ze to mam dobre
- Ak by som chcel v presentru tieto data este nejako filtrovat (where, in …), popripade triedit, ako by sa to dalo?
- Filip Procházka
- Moderator | 4668
Když se koukneš na api třídy EntityDao
, tak zjistíš, že
createQueryBuilder má dva argumenty. Tím že tam předáš
to „u
“, tak se ti tam základní select
i
from
automaticky doplní
/**
* @return ResultSet
*/
public function findAll()
{
$qb = $this->usersDao->createQueryBuilder('u')
return new ResultSet($qb->getQuery());
}
Je taky potřeba připomenout, že Query objekty a ResultSet jsou určené pro data, která chcete vypisovat někde v nějaké šabloně a je jich neurčité množství – kdekoliv nepotřebujete stránkovat, tak píšete Query objekt zbytečně.
ResultSet je také elegantní způsob, jak oddálit vykonání dotazu do databáze až do šablony, kde si výsledek můžete nacachovat makrem. Protože DQL objekt obaluje a neumožňuje jeho další modifikaci (například v presenteru).
Ohledně třídění…
Mějme třeba komponentu FiltersControl
, která se nějak
vykreslí do šablony, jako jsou třeba filtry pod hlavičkou na damejidlo.
A takový control ten má různé persistentní parametry a může klidně
obsahovat i formulář. Má metodu getFilters()
, která vrací
nějaký náš objekt, třeba RestaurantFilters
class RestaurantFilters
{
public $deliveryPrice = 0;
public $onlyCardPayment = FALSE;
// ..
}
ten si v té getFilters()
naplním
public function getFilters()
{
$filters = new RestaurantFilters;
$filters->deliveryPrice = $this->deliveryPrice;
// ...
return $filters;
}
A pak si ho předám třeba v render mětodě, třeba do facade
public function renderDefault()
{
$this->template->restaurants = $this->restaurantsFacade
->filter($this['filters']->getFilters());
}
No a ta metoda filter má za úkol poskládat takovou query (nebo několik, je-li to třeba), která vrátí jenom ty restaurace, které odpovídají filtrům. A pokud bych chtěl, tak si můžu vrátit třeba ResultSet a stránkovat si ho.
Co se snažím říct, tak že ResultSet není určen na další filtrování, filtrování je doménová logika kterou bych doporučil dělat v modelu. Zato stránkování, to je věc čistě uživatelského rozhranní a nevidím důvod proč by o tom měl model vůbec něco vědět, že něco takového existuje :)
- akadlec
- Člen | 1326
@Jiří Nápravník: nemyslím si že je vhodné dávat do fasády stránkovadlo, dle mě by se fasáda měla postarat o to jaké data vybrat a to jestli zobrazíš všechny nebo jen konkrétní stránky by měl rozhodnout kontroler.
@duskohu: tak query object nemusíš dělat pokud nechceš, je to jen jedna z možností kde si můžeš oddělit skládání vlastních query mimo fasádu. Jak jsem psal výše, já jsem si rozdělil fasády na to podle toho co se s nimi dělá, zda se zpracovává jen jedna entita a nebo jejich sada a nebo jestli tahám nějaké statistické údaje. V podstatě to můžeš mít vše v jednom, ale pak se ti může stát že máš milion metod v jednom objektu a je to nepřehledné, takto si připojím konkrétní fasádu podle toho co chci dělat s daty.
Jinak mou inspirací bylo řešení od jsifaldy
- Jiří Nápravník
- Člen | 710
akadlec: stránkovadlo ve fasádě se mi taky moc nelíbilo. ale ted jsem to v podstatě vyřešil tím resultsetem, který mi fasáda vrací a pagiantor tam šoupnu v presenteru
Filip Procházka: pokud jen vytáhnu data z fasaády a posunu do view, v presenteru s nimi nic nedělám. Je dobý nápad obalit to do ResultSetu? Nebo to jej už spíše zneužívám, pro co určen není?
- Jiří Nápravník
- Člen | 710
akadlec eh, sorry, zapomnel jsem napsat to podstatné. Chtěl bych to kvůli tomu, abych to pak mohl cachovat přímo v latte, jak ostatně sám Filip uvedl, že to je dobré řešení
dushoku: jj, muzes, v podstate v té fasade mas vsechnu business logiku, coz je i mazani a ukladani. Jen pravdepodobne bude lepsi asi nazev saveArticle() nez save(), protoze muzes mi facadu i nad vice entitami, DAOs, a pak nemusíš příliš vidět, co ten save vlastně ukládá
- akadlec
- Člen | 1326
@duskohu: no můžeš ale tuším sem někde čet že to není příliš košér. Já přešel na entity crud a entity manager (ne manager z doctrine) a tento manager má v sobě tři metody, create/update/remove a dělá to že mu předám parametry v arrayhash (form nebo custom data) a můžu si tam zadefinovat before a after akce, takže třeba při vytvoření nového uživatele tam navážu akci co mě pošle notifikaci že se někdo regnul atd.
@Jiří Nápravník: jako že si do šablony předáš ResultSet a až tam budeš dělat načtení dat?
Editoval akadlec (8. 3. 2014 11:37)
- Jiří Nápravník
- Člen | 710
akadlec: kal úsaů Filip výše:
ResultSet je také elegantní způsob, jak oddálit vykonání dotazu do databáze až do šablony, kde si výsledek můžete nacachovat makrem. Protože DQL objekt obaluje a neumožňuje jeho další modifikaci (například v presenteru).
- duskohu
- Člen | 778
Ked mozem mam dalsiu ot. :-)
Mam:
- User entitu
- UsersFasade
- Role entitu
- RoleFasade
- Vsatah je @OneToMany
Niekde si vytiahnem UserFasade vytiahnem User entitu, a ako user entite vlozim, vymazem, editujem Role?
$userEntity = $this->usersFacade->getUserDao()->find($id);
$userRoles= $userEntity->getRoles()->getIterator();
/** @var Role $role */
foreach ($userRoles as $role) {
// Ako editujem, vymazem rolu na tejto user entite?
}
// Ako pridam rolu tejto entite?
$role = new Role();
$role->setSlug('myslug');
- Filip Procházka
- Moderator | 4668
Tady máš dokumentaci doctrine, nemá žádný význam ji přepisovat po kouskách sem do fóra. Prva si ji přečti pak se ptej dál :)
- akadlec
- Člen | 1326
podle mě je tohle zbytečné:
$userRoles= $userEntity->getRoles()->getIterator();
mělo by přece stačit:
$userRoles= $userEntity->getRoles();
a pokud se to hodí do cyklu tak se getIterator zavolá sám nebo se pletu?
Přidej si do user entit seter a getter na role případně adder a remover:
/**
* @param array $roles
*
* @return $this
*/
public function setRoles(array $roles)
{
$this->roles = $roles;
return $this;
}
/**
* @param IRole $role
*
* @return $this
*/
public function addRole(IRole $role)
{
if ( !$this->roles->contains($role) ) {
$this->roles[] = $role;
}
return $this;
}
/**
* @return array
*/
public function getRoles()
{
return $this->roles->toArray();
}
/**
* @param IRole $role
*
* @return $this
*/
public function removeRole(IRole $role)
{
if ( $this->roles->contains($role) ) {
$this->roles->removeElement($role);
}
return $this;
}
- duskohu
- Člen | 778
@Filip Procházka studujem :-)
Napriek tomu robim nieco zle, @akadlec dik takto som to
riesil, ale remove mi nefunguje
predpokladal som ze treba zavolat $em->flush(); ktore vola sa vola v Kdyby.Doctrine.EntityDao::save()
cize predpokladam ze to mam urobit takto:
// vytiahnem entitu
$userEntity = $this->usersFacade->getUserDao()->find($id);
// vytvorim rolu
foreach ($this->userEntity->getRoles() as $role) {
$userEntity->removeRole($role);
}
a kedze Kdyby.Doctrine.EntityDao save() vola flush predpokladam ze zavolam nad entitou save Lenze predane role entity nevymaze, ale nevrati ani error.
$this->usersFacade->save($userEntity);
- Jiří Nápravník
- Člen | 710
Pokud se nemýlím, tak OneToMany je inverzní nikoli vlastnící strana. A flushnuté jsou změny provedené na vlastnící straně.
Či-li musíš provést změny na strane Role, případně využít cascade attribut v anotaci
- akadlec
- Člen | 1326
a ještě si nastuduj něco o orphanRemoval=true pokud tě zajímají one-to-many
Jinak u tvé definice roles pro user vidím OneToMany takže chceš jednomu userovi přiřadit více rolí? Pokud ano tak bych na to šel přes vazební tabulku a použil relaci ManyToMany protože to vystihuje ten stav co chceš dělat nějak takto:
(jeden uživatel může mít x roli a jedna role může náležet x uživatelům)
/**
* @var \Doctrine\Common\Collections\Collection
* @writable
*
* @ORM\ManyToMany(targetEntity="\Entities\Roles\Role")
* @ORM\JoinTable(name="ipub_account_roles",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="user_id")},
* inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="role_id", unique=TRUE)}
* )
*/
protected $roles;
pokud to máš jako jeden user jedna role a tudíž pro jednu roli může být X userů tak by to mělo být ManyToOne ne?
(jeden uživatel může mít jednu roli a jedna role může mít x uživatelů)
/**
* @var \Entities\Roles\Role
* @writable
*
* @ORM\ManyToOne(targetEntity="Entities\Roles\Role")
* @ORM\JoinColumn(name="role_id", referencedColumnName="role_id", onDelete="SET NULL")
**/
protected $roles;
Editoval akadlec (12. 3. 2014 12:54)
- duskohu
- Člen | 778
Caute, tak sa mi uz podarilo skoro vsetko rozbehat. Ale mam v jednej veci este nejasno. Konkretne ide o sorting. Samozrejme viem ze sa to pouziva:
/** @var ResultSet $users */
$users = $this->usersFacade->findFiltered($this->getFilter());
$users->applySorting(array('u.name ASC', 'u.surname ASC'));
Co sa mi nepaci? To ze musim pisat to u.name, u.surname, bez u mi to nejde, a ked v fasade zmenim to u, tak samozrejme to uz vobec. A moja otazka znie, ako riesite sorting, aby to bolo nejak ciste?
- duskohu
- Člen | 778
Dakujem pani konecne som sa dostal dalej. Teraz som narazil na dalsi problem. Mam QueryBuilder:
$qb = $this->dao->createQueryBuilder('c')
->select('c', 'cl', 'm')
->leftJoin('c.client', 'cl',
\Doctrine\ORM\Query\Expr\Join::WITH,
'c.client = cl.id'
)
->leftJoin('c.model', 'm',
\Doctrine\ORM\Query\Expr\Join::WITH,
'c.model = m.id'
);
A neviem ako pomocou whereCriteria() postupne nahadzat kombinacie eq a like.
$whereCriteria = array();
if ($filter->id !== NULL) {
$whereCriteria['c.id'] = $filter->id;
}
if ($filter->clientName !== NULL) {
$whereCriteria['cl.name LIKE'] = '%' . $filter->clientName . '%';
}
if ($filter->modelName !== NULL) {
$whereCriteria['m.name LIKE'] = '%' . $filter->modelName . '%';
}
$qb->whereCriteria($whereCriteria);
return new ResultSet($qb->getQuery());
Toto mi vygeneruje:
SELECT c, cl, m
FROM Nas\ServiceModule\Model\Contract c
LEFT JOIN c.client cl WITH c.client = cl.id
LEFT JOIN c.model m WITH c.model = m.id
INNER JOIN c.cl c
INNER JOIN c.m m
WHERE c.id = :param_1 AND c.name LIKE :param_2 AND m.name LIKE :param_3
ORDER BY c.id DESC
a padne to na: applyPaginator()
Kdyby\Doctrine\QueryException
[Semantical Error] line 0, col 156 near 'c INNER JOIN': Error: Class Nas\ServiceModule\Model\Contract has no association named cl
Neviete mi poradit co robim zle?
Editoval duskohu (20. 3. 2014 23:25)
- Filip Procházka
- Moderator | 4668
Join::WITH
se používá pokud chceš přidat další omezení
na filtrování relací přímo v DQL, ale tu základní vazbu kterou máš už
přímo v metadatech entity tam psát už nemáš!
- Filip Procházka
- Moderator | 4668
Na LIKE nemám test case :) Nechceš poslat pullrequest?
Tohle by mělo fungovat určitě:
$qb = $this->dao->createQueryBuilder('c')
->select('c', 'cl', 'm')
->leftJoin('c.client', 'cl')
->leftJoin('c.model', 'm');
if ($filter->id !== NULL) {
$qb->andWhere('c.id = :id')->setParameter('id', $filter->id);
}
if ($filter->clientName !== NULL) {
$qb->andWhere('cl.name LIKE :clientName')
->setParameter('clientName', $filter->clientName);
}
if ($filter->modelName !== NULL) {
$qb->andWhere('m.name LIKE :clientName')
->setParameter('modelName', $filter->modelName);
}
return new ResultSet($qb->getQuery());