Doctrine Tree path iteration in Symfony 2 - symfony

I have variable $path which contains the array of the names of sibling categories. Using this variable I would like to create a new array $categories_children, that would contain the array of children of each category from $path array according to its title. I'm using Doctrine Tree-Nestedset extension by Gediminas Morkevicius and I've tried this:
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository('MyProjectAdminBundle:Category');
$category = $repo->findOneById($id);
$path = $repo->getPath($category);
$categories_children = array();
foreach($path as $node){
$parent = $repo->findOneByTitle($node);
$categories_children[] = $repo->getChildren($parent, true);
}
The problem is that method getChildren() with argument $parent returns the same array as like when the passed argument is NULL. That means it will return all nodes starting with root, instead of starting with selected category. Somehow method findOneByTitle(), which is being used in $parent variable, doesn't accept arguments from getPath() and behaves as NULL.

getChildren $parent argument specify only from which root element get the tree. You can see code of childrenQueryBuilder function in NestedTreeRepository.
To fetch all childs I use own function in repository.
public function getTreeAsFlatArray( AbstractTreeNode $parent = null, array $options = array() ) {
$qb = $this->createQueryBuilder('node');
$qb
->andWhere('node.lft > :parent_lft')
->setParameter('parent_lft', $parent->getLft() )
->andWhere('node.lft < :parent_rgt')
->setParameter('parent_rgt', $parent->getRgt() )
->andWhere('node.level <= :level')
->setParameter('level', $parent->getLevel() + $o['depth'])
->andWhere('node.root = :root')
->setParameter('root', $parent->getRoot())
;
...
}
If you only need direct childs simplets way to specify childs field in entity
/**
* #ORM\OneToMany(targetEntity="AbstractTreeNode", mappedBy="parent", fetch="EXTRA_LAZY")
* #ORM\OrderBy({"lft" = "ASC"})
*/
protected $childs;

Related

Is there a way to define first item of pagination results?

I'm setting up a Symfony project displaying paginated blogposts with an admin interface to manage all this stuff. On this admin it is also possible to "highlight" one of those public blogposts so that this highlighted one is displayed at first position only on the first page.
I need the same item count on each page and that is the problem I'm dealing with.
I'm using PagerFanta so I created an AbstractRepository with a "paginate" function.
protected function paginate(QueryBuilder $qb, $limit = 20, $offset = 0)
{
if ($limit == 0) {
throw new \LogicException('$limit must be greater than 0.');
}
//Instantiates the pagination object with the result of the query
$pager = new Pagerfanta(new DoctrineORMAdapter($qb));
//Sets max data per page
$pager->setMaxPerPage($limit);
//Sets the current page
$pager->setCurrentPage($offset);
return $pager;
}
In my blogpost repository I made a querybuilder to get all public blogpost excluding the highlighted one because I can get it in another way to display it on top of the first page.
public function findAllVisible($id, $limit = 3, $offset = 1, $order = 'DESC')
{
$qb = $this
->createQueryBuilder('a')
->select('a')
->where('a.website = :website')
->setParameter('website', 'blog')
->andWhere('a.public = :public')
->setParameter('public', true)
->andWhere('a.id != :id')
->setParameter('id', $id)
->orderBy('a.dateInsert', $order)
;
return $this->paginate($qb, $limit, $offset);
}
So I first tried to change the limit and the offset according to the current page but I logically lost one item between the first and the second page.
Then I tried to include the highlighted blogpost in querybuilder but I don't know how to define it as the first result if the current page is the first one.
Any idea of how to force the first result to be the highlighted blogpost only on first page? Or another clean and appropriate way to display results as expected?
I answer to myself because I managed to do what I needed to. In case of someone is dealing with the same issue, here is how I did.
I don't use PagerFanta anymore but Doctrine Paginator tool.
Instead of excluding my highlighted article from my query I replaced my initial ORDER BY by a.id = :highlightedId DESC, a.dateInsert DESC.
Now it's working as expected.
Here is my new repository function:
/**
* Finds all visible articles
*
* #param int $highlightedTipId the highlighted tip id
* #param int $page current page
* #param int $limit max items per page
*
* #throws InvalidArgumentException
* #throws NotFoundHttpException
*
* #return Paginator
*/
public function findAllVisible($highlightedTipId, $limit = 3, $page)
{
if (!is_numeric($page)) {
throw new InvalidArgumentException('$page value is incorrect (' . $page . ').');
}
if ($page < 1) {
throw new NotFoundHttpException('Page not found');
}
if (!is_numeric($limit)) {
throw new InvalidArgumentException('$limit value is incorrect (' . $limit . ').');
}
$entityManager = $this->getEntityManager();
$query = $entityManager->createQuery(
"SELECT
a,
CASE WHEN a.id = :id THEN 1 ELSE 0 END AS HIDDEN sortCondition
FROM App\Entity\Item a
WHERE
a INSTANCE OF App\Entity\TipArticle
AND
a.website = :website
AND
a.public = :public
ORDER BY
sortCondition DESC,
a.dateInsert DESC
"
);
$query->setParameter(':website', 'blog');
$query->setParameter(':public', true);
$query->setParameter(':id', $highlightedTipId);
$firstResult = ($page - 1) * $limit;
$query
->setFirstResult($firstResult)
->setMaxResults($limit);
$paginator = new Paginator($query);
if (($paginator->count() <= $firstResult) && $page != 1) {
throw new NotFoundHttpException('Page not found');
}
return $paginator;
}
A word about this line CASE WHEN a.id = :id THEN 1 ELSE 0 END AS HIDDEN sortCondition: it is the only way I found to do a ORDER BY a.id = :highlightedId DESC with Doctrine. As you can see I made a DQL but I is also possible with QueryBuilder.
Hope it will help! :)
Nice, well done. If I may offer some advice though. In a repo you shouldn't need to get the ObjectManager ($this->getEntityManager()) as the repo is already for a type of Entity, Item in this case. You should use Criteria instead. practical example docs
You should have the ObjectManager in whichever controller you've got, so then you'd do:
$items = $this-getObjectManager()->getRepository(Item::class)->findAllVisible($params)
For use of the Paginator you should use the QueryBuilder, with Expressions, like here
As a practical example, an indexAction of mine:
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator as OrmPaginator;
use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as OrmAdapter;
use Zend\Paginator\Paginator;
public function indexAction()
{
// get current page, defaults to 1
$page = $this->params()->fromQuery('page', 1);
// get current page size, defaults to 10
$pageSize = $this->params()->fromQuery('pageSize', 10);
// get ordering, defaults to 'createdAt'
$orderBy = $this->params()->fromQuery('orderBy', 'createdAt');
// get order direction, defaults to Criteria::DESC
$orderDirection = ($this->params()->fromQuery('orderDirection') === Criteria::ASC)
? Criteria::ASC
: Criteria::DESC;
$criteria = new Criteria();
$criteria->setFirstResult($page * $pageSize);
$criteria->setMaxResults($pageSize);
$criteria->orderBy([$orderBy => $orderDirection]);
/** #var QueryBuilder $queryBuilder */
$queryBuilder = $this->getObjectManager()->createQueryBuilder();
$queryBuilder->select('a')->from(Article::class, 'a');
$queryBuilder->addCriteria($criteria);
$paginator = new Paginator(new OrmAdapter(new OrmPaginator($queryBuilder)));
$paginator->setCurrentPageNumber($page); // set current page
$paginator->setItemCountPerPage($pageSize); // set item count per page
return [
'paginator' => $paginator,
'queryParams' => $this->params()->fromQuery(), // pass these for your pagination uri's
];
}
NOTE: In the above the $this is an instance of a Zend Framework controller, where ->fromQuery returns (if present) a given key from the Query bit if a URI, else return the 2nd param default (or null). You should do something similar.

Symfony3: Default parameter gets first entry even when defaulted to null

I am trying to create an action in my controller that handles a null value for category if it's not passed in.
I've tried annotating 2 routes (one without /{category}) but in every case, when I do not supply a category in the URL, Symfony retrieves the 1st category it can find.
#Route("/accounts/{id}/categories/{category}", defaults={"category" = null})
The action definition looks like this:
public function categoryAction(Request $request, Account $account, Category $category)
I have also tried $category = null in the action, but that does not make a difference.
How can I make this action have a $category with a value of null if the category is not defined in the url?
Update:
To be clear, here is the full annotation and function definition with comments on my xdebug results:
/**
* #Route("/accounts/{id}/categories")
* #Route("/accounts/{id}/categories/{category}, defaults={"category" = null}")
*/
public function categoryAction(Request $request, Account $account, Category $category = null)
{
// When I set a breakpoint here, $category is populated with
// the first category result in the database.
// This is when visiting: http://localhost:8000/accounts/1/categories
You can define multiple routes for same actions, maybe try something like :
/**
* #Route("/accounts/{id}/categories")
* #Route("/accounts/{id}/categories/{category}")
* #Template()
*/
public function categoryAction(Request $request, Account $account, Category $category)
{
And if you try to access /accounts/{id}/categories, $category will be null
I believe this is the change you should make:
/**
* #Route("/accounts/{id}/categories", defaults={"category" = null}")
* #Route("/accounts/{id}/categories/{category})
*/
public function categoryAction(Request $request, Account $account, Category $category)
{
...
Can you try it and let us know? I haven't tested it, but I think it's right.

Key "catTitle" for array with keys "0" does not exist in EagleShopBundle:global:product.html.twig

I'm trying to join two tables and print a value in twig template but I'm having this issue.
This is my Controller action.
/**
* #Route("products/display/{id}")
* #Template()
*/
public function displayAction($id) {
$em = $this->container->get('doctrine.orm.entity_manager');
$qb = $em->createQueryBuilder();
$qb->select('p', 'pc.catTitle')
->from('EagleShopBundle:Products', 'p')
->leftJoin('EagleShopBundle:ProductCategory', 'pc', \Doctrine\ORM\Query\Expr\Join::WITH, 'pc.id = p.category')
->where($qb->expr()->eq('p.id', '?5'))
->setParameter(5, $id);
$product = $qb->getQuery()->getResult();
return $this->render("EagleShopBundle:global:product.html.twig", array(
'product' => $product,
'image_path' => '/bundles/eagleshop/images/'
));
}
This is my twig file line related to the issue,
<small class="pr_type">{{product.catTitle}}</small>
But instead of printing 'catTitle' I'm having this issue,
Key "catTitle" for array with keys "0" does not exist in
EagleShopBundle:global:product.html.twig
It is simple: getResult() returns an array of objects, even if there is only one. If you are expecting that this query would return only one object use getOneOrNullResult(). But after that and before you are displaying results you need to check if smth is returned (for example: product instanceof Products) or null is returned.

Doctrine2 - Sub query join without a relationship

Ok so here's the issue.
I have a Entity named HelpDocuments and an Entity named LogEntry.
HelpDocuments can be dismissed by the user. When this happens I create a LogEntry with the following attributes:
event - eg: helpDocument.dismiss
entity_id - eg: 11
entityDiscriminator - eg: HelpDocument
There are no relationships created between HelpDocument and LogEntry as I'm implementing my own discriminator logic.
So what I'm trying to achieve is query for all HelpDocuments that have not been dismissed. I can do that with sql, left outer subquery join like so:
SELECT HelpDocument.*, temp.*
FROM HelpDocument
LEFT OUTER JOIN(
SELECT LogEntry.entity_id
FROM LogEntry
WHERE LogEntry.entityDiscriminator = 'HelpDocument'
AND LogEntry.event = 'helpDocument.dismiss'
AND LogEntry.entity_id = 11
) as temp ON HelpDocument.id = temp.entity_id
WHERE temp.entity_id IS NULL;
My issue is how do I turn this into DQL given that there is no relationship defined?
Updated Solution:
So the solution was to not use an LEFT OUTER JOIN because they don't exist / make sense in Doctrine2. In the end I had to do a subquery join:
/**
* Filter by User Dismissed
*
* #param $qb
* #param $route
* #return mixed
*/
public function filterQueryByUserDismissed(QueryBuilder $qb, $args)
{
$args = array_merge(array(
"user" => null,
"dismissed" => false
), $args);
/** #var $dismissedQB QueryBuilder */
$dismissedQB = $this->_em->createQueryBuilder();
/*
This line is important. We select an alternative attribute rather than
letting Doctrine select le.id
*/
$dismissedQB->select('le.entityId')
->from('\Mlf\AppBundle\Entity\UserEntityEventLog', 'le')
->where('le.entityDiscriminator = :entityDiscriminator')
->andWhere('le.event = :event')
->andWhere('le.user = :userId');
$function = (true === $args['dismissed']) ? "in" : "notIn";
$expr = $qb->expr()->$function($this->classAlias.'.id', $dismissedQB->getDQL());
/** #var $qb QueryBuilder */
$qb->andWhere($expr)
->setParameter("entityDiscriminator", HelpDocument::getDiscriminator())
->setParameter("event", HelpDocumentEvents::HELPDOCUMENT_DISMISS)
->setParameter("userId", $args["user"]);
// exit($result = $qb->getQuery()->getSQL());
return $qb;
}
This DQL query results in the following SQL:
SELECT h0_.id AS id0
FROM HelpDocument h0_
WHERE (
h0_.id NOT IN (
SELECT l1_.entity_id
FROM LogEntry l1_
WHERE l1_.entityDiscriminator = 'helpDocument'
AND l1_.event = 'helpDocument.dismiss'
AND l1_.user_id = 1
)
)
Yay!
I saw your solution and I have a minor change that will be a huge performance improvement. Especially if you have more then a couple of thousand rows.
public function filterQueryByUserDismissed(QueryBuilder $qb, $args)
{
$args = array_merge(array(
"user" => null,
"dismissed" => false
), $args);
/** #var $dismissedQB QueryBuilder */
$dismissedQB = $this->_em->createQueryBuilder();
/*
This line is important. We select an alternative attribute rather than
letting Doctrine select le.id
*/
$dismissedQB->select('le.entityId')
->from('\Mlf\AppBundle\Entity\UserEntityEventLog', 'le')
->where('le.entityDiscriminator = :entityDiscriminator')
->andWhere('le.event = :event')
->andWhere('le.user = :userId');
// ---- My changes below
// Get an array with the ids
$dismissedIdsMap = $dismissedQB->getQuery()->getResults();
$dismissedIds = array_map(
function($a){
return $a['entityId'];
},
$dismissedIdsMap);
$function = (true === $args['dismissed']) ? "in" : "notIn";
$expr = $qb->expr()->$function($this->classAlias.'.id', $dismissedIds);
// ---- My changes above
/** #var $qb QueryBuilder */
$qb->andWhere($expr)
->setParameter("entityDiscriminator", HelpDocument::getDiscriminator())
->setParameter("event", HelpDocumentEvents::HELPDOCUMENT_DISMISS)
->setParameter("userId", $args["user"]);
// exit($result = $qb->getQuery()->getSQL());
return $qb;
}
The code above is using two queries. If you are using one query, MySQL will create a temporary view from your subquery and then query the view with master query. It is a lot of overhead creating this view. With two queries you will be keeping the "view" in the PHP memory and this will reduce the overhead dramatically.

Count Rows in Doctrine QueryBuilder

I'm using Doctrine's QueryBuilder to build a query, and I want to get the total count of results from the query.
$repository = $em->getRepository('FooBundle:Foo');
$qb = $repository->createQueryBuilder('n')
->where('n.bar = :bar')
->setParameter('bar', $bar);
$query = $qb->getQuery();
//this doesn't work
$totalrows = $query->getResult()->count();
I just want to run a count on this query to get the total rows, but not return the actual results. (After this count query, I'm going to further modify the query with maxResults for pagination.)
Something like:
$qb = $entityManager->createQueryBuilder();
$qb->select('count(account.id)');
$qb->from('ZaysoCoreBundle:Account','account');
$count = $qb->getQuery()->getSingleScalarResult();
Some folks feel that expressions are somehow better than just using straight DQL. One even went so far as to edit a four year old answer. I rolled his edit back. Go figure.
Here is another way to format the query:
return $repository->createQueryBuilder('u')
->select('count(u.id)')
->getQuery()
->getSingleScalarResult();
It's better to move all logic of working with database to repositores.
So in controller you write
/* you can also inject "FooRepository $repository" using autowire */
$repository = $this->getDoctrine()->getRepository(Foo::class);
$count = $repository->count();
And in Repository/FooRepository.php
public function count()
{
$qb = $repository->createQueryBuilder('t');
return $qb
->select('count(t.id)')
->getQuery()
->getSingleScalarResult();
}
It's better to move $qb = ... to separate row in case you want to make complex expressions like
public function count()
{
$qb = $repository->createQueryBuilder('t');
return $qb
->select('count(t.id)')
->where($qb->expr()->isNotNull('t.fieldName'))
->andWhere($qb->expr()->orX(
$qb->expr()->in('t.fieldName2', 0),
$qb->expr()->isNull('t.fieldName2')
))
->getQuery()
->getSingleScalarResult();
}
Also think about caching your query result - http://symfony.com/doc/current/reference/configuration/doctrine.html#caching-drivers
public function count()
{
$qb = $repository->createQueryBuilder('t');
return $qb
->select('count(t.id)')
->getQuery()
->useQueryCache(true)
->useResultCache(true, 3600)
->getSingleScalarResult();
}
In some simple cases using EXTRA_LAZY entity relations is good
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html
If you need to count a more complex query, with groupBy, having etc... You can borrow from Doctrine\ORM\Tools\Pagination\Paginator:
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
$totalRows = count($paginator);
Since Doctrine 2.6 it is possible to use count() method directly from EntityRepository. For details see the link.
https://github.com/doctrine/doctrine2/blob/77e3e5c96c1beec7b28443c5b59145eeadbc0baf/lib/Doctrine/ORM/EntityRepository.php#L161
Example working with grouping, union and stuff.
Problem:
$qb = $em->createQueryBuilder()
->select('m.id', 'rm.id')
->from('Model', 'm')
->join('m.relatedModels', 'rm')
->groupBy('m.id');
For this to work possible solution is to use custom hydrator and this weird thing
called 'CUSTOM OUTPUT WALKER HINT':
class CountHydrator extends AbstractHydrator
{
const NAME = 'count_hydrator';
const FIELD = 'count';
/**
* {#inheritDoc}
*/
protected function hydrateAllData()
{
return (int)$this->_stmt->fetchColumn(0);
}
}
class CountSqlWalker extends SqlWalker
{
/**
* {#inheritDoc}
*/
public function walkSelectStatement(AST\SelectStatement $AST)
{
return sprintf("SELECT COUNT(*) AS %s FROM (%s) AS t", CountHydrator::FIELD, parent::walkSelectStatement($AST));
}
}
$doctrineConfig->addCustomHydrationMode(CountHydrator::NAME, CountHydrator::class);
// $qb from example above
$countQuery = clone $qb->getQuery();
// Doctrine bug ? Doesn't make a deep copy... (as of "doctrine/orm": "2.4.6")
$countQuery->setParameters($this->getQuery()->getParameters());
// set custom 'hint' stuff
$countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountSqlWalker::class);
$count = $countQuery->getResult(CountHydrator::NAME);
For people who are using only Doctrine DBAL and not the Doctrine ORM, they will not be able to access the getQuery() method because it doesn't exists. They need to do something like the following.
$qb = new QueryBuilder($conn);
$count = $qb->select("count(id)")->from($tableName)->execute()->fetchColumn(0);
To count items after some number of items (offset), $qb->setFirstResults() cannot be applied in this case, as it works not as a condition of query, but as an offset of query result for a range of items selected (i. e. setFirstResult cannot be used togather with COUNT at all). So to count items, which are left I simply did the following:
//in repository class:
$count = $qb->select('count(p.id)')
->from('Products', 'p')
->getQuery()
->getSingleScalarResult();
return $count;
//in controller class:
$count = $this->em->getRepository('RepositoryBundle')->...
return $count-$offset;
Anybody knows more clean way to do it?
Adding the following method to your repository should allow you to call $repo->getCourseCount() from your Controller.
/**
* #return array
*/
public function getCourseCount()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('count(course.id)')
->from('CRMPicco\Component\Course\Model\Course', 'course')
;
$query = $qb->getQuery();
return $query->getSingleScalarResult();
}
You can also get the number of data by using the count function.
$query = $this->dm->createQueryBuilder('AppBundle:Items')
->field('isDeleted')->equals(false)
->getQuery()->count();

Resources