Kdyby/Doctrine – ako na to?

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
duskohu
Člen | 778
+
0
-

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

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);
	}
}
duskohu
Člen | 778
+
0
-

@akadlec prepac ale nieco zrozumitelnejsie?, nepripada mi velmi vhodne len koli paginatoru vytvarat dalsi objekt.

akadlec
Člen | 1326
+
0
-

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.

duskohu
Člen | 778
+
0
-

Oki, ale ako by som vedel pouzit QueryObject na mojom priklade vo UsersFacade alebo v presentru, aby som mohol pouzit paginator?

Jiří Nápravník
Člen | 710
+
0
-

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

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

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

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();
	}
  1. 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
  2. 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
+
0
-

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

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

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

akadlec
Člen | 1326
+
0
-

@Jiří Nápravník: tak pokud už s daty nic neděláš tak si je rovnou vytáhni z fasády ne?

duskohu
Člen | 778
+
0
-

Krasne, dakujem pekne. Ked mozem mam dalsiu otazku. Do Facade mozem vlozit aj metodu save(), delete()?, alebo sa na to pouziva dalsi objekt?

Jiří Nápravník
Člen | 710
+
0
-

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

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

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

akadlec
Člen | 1326
+
0
-

mno pokud sem dobře odsledoval tak i když udělaš select dat tak dokud nezískáš iterátor tak se select nespustí

duskohu
Člen | 778
+
0
-

Ked mozem mam dalsiu ot. :-)

Mam:

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

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

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

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

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

duskohu
Člen | 778
+
0
-

Takze toto je uz tiez o Doctrine, dik, aspon viem kde mam hladat. Inak na roles mam cascade={„persist“, „remove“}

akadlec
Člen | 1326
+
0
-

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)

akadlec
Člen | 1326
+
0
-

a ještě doplním pokud chceš flushnout více entit najednou jako sem řešil třeba já když sem provedl změny v entitě usera která má relace na další entity a přes usera sem provedl změny i v nich tak jak radil @Filip Procházka, musíš flushnout celý EM

duskohu
Člen | 778
+
0
-

@akadlec dakujem za nakopnutie, uz sa do toho pomali dostavam ;-)

duskohu
Člen | 778
+
0
-

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?

akadlec
Člen | 1326
+
0
-

@dushoku: podle toho kde ty data sortuju. Buď přímo v anotaci když se jedná o kolekce a nebo jak to máš ty a nebo v konrétní find metodě. Btw co se ti na u.X nelibí? tak když si si udělal alias tak je snad logické že jej musíš použít ne?

duskohu
Člen | 778
+
0
-

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)

akadlec
Člen | 1326
+
0
-

wtf?

\Doctrine\ORM\Query\Expr\Join::WITH,
'c.client = cl.id'

proč to tam máš? Dyť to jak je vazba provedena máš definováno v entitě ne?

Filip Procházka
Moderator | 4668
+
0
-

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áš!

akadlec
Člen | 1326
+
0
-

@Filip Procházka: no právě, to se mě zdálo divné že tu vazbu tam má defakto dvakrát.

duskohu
Člen | 778
+
0
-

Aha, dakujem Join::WITH som dal prec. Ale stale neviem vyriesit tu kombinaciu where like. neviete poradit?

Filip Procházka
Moderator | 4668
+
0
-

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());
duskohu
Člen | 778
+
0
-

Dakujem velmi pekne, funguje :-), no ten pull request by asi nedopadol dobre :-(, nejako sa este necitim ze mam na to level.

Editoval duskohu (23. 3. 2014 16:06)