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.
Related
So far I have tried the following but I keep only getting the main Entity information joined entities do not make it to the result:
Option 1(Using ResultSetMapping Builder):
$rsm = new ResultSetMappingBuilder(
$this->_em,
ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT
);
$rsm->addRootEntityFromClassMetadata(
'CountryApp\StoreBundle\Entity\Product', 'p'
);
$rsm->addJoinedEntityFromClassMetadata(
'CountryApp\StoreBundle\Entity\Category', 'c', 'p', 'category'
);
$rsm->addJoinedEntityFromClassMetadata(
'CountryApp\StoreBundle\Entity\CustomerProductPrice', 'cpp', 'p', 'customerPrices'
);
$result = $this->_em
->createNativeQuery(
'
SELECT
p.id,
p.code,
p.name,
p.cost,
p.rrp,
p.status,
p.notes,
p.out_of_stock_since,
p.available_in,
c.id,
c.name,
c.code,
cpp.id,
cpp.price
FROM product as p
JOIN category as c ON c.id = p.category_id AND p.status != "DELETED"
LEFT JOIN customer_product_price as cpp ON cpp.product_id = p.id AND cpp.customer_id = :customer
', $rsm
)
->setParameter('customer', $customerId)
->getResult(Query::HYDRATE_ARRAY)
;
Option 2:(using QueryBuild and FetchMode)
$qb = $this->createQueryBuilder('p');
$result = $qb
->select('p')
->addSelect('c')
->addSelect('cpp')
->join(
'CountryApp\StoreBundle\Entity\Category',
'c',
Join::WITH,
$qb->expr()
->eq('c', 'p.category')
)
->leftJoin(
'CountryApp\StoreBundle\Entity\CustomerProductPrice',
'cpp',
Join::WITH,
$qb->expr()
->andX(
$qb->expr()
->eq('p', 'cpp.product'),
$qb->expr()
->eq('cpp.customer', ':customer')
)
)
->setParameter('customer', $customerId)
->getQuery()
->setFetchMode(
'CountryApp\StoreBundle\Entity\Category', 'product', ClassMetadata::FETCH_EAGER
)
->setFetchMode(
'CountryApp\StoreBundle\Entity\CustomerProductPrice', 'product', ClassMetadata::FETCH_EAGER
)
->getResult(Query::HYDRATE_ARRAY)
;
Please advise your thoughts as to what could make this work. I want to obtain the following structure:
[
0 => [
Product[
..
]
Category[
..
]
CustomerProductPrice[
..
]
],
1 => [
Product[
..
]
Category[
..
]
CustomerProductPrice[
..
]
],
..
.
]
While using Doctrine you define your relationships inside of your entity.
You can read more here https://symfony.com/doc/current/doctrine/associations.html always read the documentation and best practices. I dunno if you are using Symfony or not, but this is a great example and more understandable than Doctrine docs.
/**
* #ORM\Entity()
*/
class Product
{
// ...
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products")
*/
private $category;
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}
As you see here you define an entity that holds all associations and properties.
Normally association will be lazy loaded by default if you call $product->getCategory() you category will be lazy loaded. If you do not like lazy loading you can always eager fetch with
/**
* #ManyToOne(targetEntity="Category", cascade={"all"}, fetch="EAGER")
*/
And you will receive an array of products where each product will have a property named category and will contain Category entity inside it.
This is the main difference between CakePHP because in CakePHP you get all associations separately and in Symfony, you get a tree of associations.
And your queries seems too complex and in most cases, you do not have to modify queries like that at all. But be careful with lazy loads if you lazy load data on huge lists you will end up with poor performance.
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.
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()->getOneOrNullResult();
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,
<h1>{{product.productTitle}}</h1>
I guess issue is related to this line
$qb->select('p, pc.catTitle')
This is the error I get,
Key "productTitle" for array with keys "0, catTitle" does not exist in
EagleShopBundle:global:product.html.twig
You could try next query:
$qb->select('p, partial pc.{id, catTitle}')
// if you need full productCategory object then write just 'p, pc'
->from('EagleShopBundle:Products', 'p')
->leftJoin('p.category', 'pc')
//productCategory is the field
//in product entity which has relation to product category entity,
//paste your field (not column!) name here
//if it is not productCategory
->where('p.id = :productId')
->setParameter('productId', $id);
P.S.
It is better to move queries to entity repositories :)
P.P.S.
Doctrine partial objects
UPD
Fixed query - with right field name
I've a User entity and a Product entity.
class User{
/*
* #ORM\OneToMany(targetEntity="Product", mappedBy="User")
*/
private $Products;
}
class Product{
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="Products")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $User;
}
Now I'm trying to display a html table of users, but I want to show each user's product count too.
By using following code I'm able to obtain the users objects.
$qb = $this->_em->createQueryBuilder();
$qb->select('usr')
->from('User', 'usr');
$query = $qb->getQuery();
But I don't know how to get the products count in one shot. Any help?
First, you should really create a repository class for your entities if you want to create custom queries. Then you can simply run that query by injecting the entity repository as a service wherever you need it and then running the query method.
Second, you need to return a result from a doctrine query to retrieve anything from the database. If you want to determine the count of the objects returned, simply do this:
$qb = $this->_em->createQueryBuilder();
$qb->select('usr')
->from('User', 'usr');
$query = $qb->getQuery();
$count = count($query->getResult());
A doctrine query will return an array of objects matching your query. If you just want to return a count of the matching records, try something like this:
$qb = $this->_em->createQueryBuilder();
$qb->select('count(id)')
->from('User', 'usr');
$query = $qb->getQuery();
$count = $query->getSingleScalarResult();
Or to just get a count of Product objects for that user, from within the User repository class:
$qb = $this->_em->createQueryBuilder('usr');
$qb->select('count(p.id)')
->from('usr.Products', 'p');
return $qb->getQuery()->getSingleScalarResult();
I have a one to many relationship where one page has many versions.
**page**
id
parent_id
**page_version**
id
page_id
title
published
date
These are related by the foreign key page_id. So I have a Page entity and a PageVersion entity:
class Page
{
/**
* #ORM\OneToMany(targetEntity="PageVersion", mappedBy="page")
*/
private $pageversions;
}
class PageVersion
{
/**
* #var page
*
* #ORM\ManyToOne(targetEntity="Page", inversedBy="pageversions")
*
*/
private $page;
}
So I can get all page versions for each page. However, each page will only ever have one published page version. Therefore, when querying how can I get the latest version? I.e Where published = '1' rather than getting a list of all versions?
Here is an example of my query:
public function findAllPages()
{
$query = $this->getEntityManager()
->createQuery('
SELECT DISTINCT p,v
FROM XYZWebsiteBundle:Page p
JOIN p.pageversions v
WHERE v.published = :published
AND v.deleted = :deleted'
)->setParameters(array('published' => 1, 'deleted' => 0));
try {
return $query->getResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
However if I loop through the result I can only access the Page properties and not PageVersion properties e.g title, published, date etc.
$entities = $em->getRepository('XYZWebsiteBundle:Page')->findAllPages();
foreach($entities as $entity){
print($entity->getPageVersion()->getTitle());
}
The error returned is Fatal error: Call to undefined method Doctrine\ORM\PersistentCollection::getTitle().
$er = $this->getDoctrine()->getRepository('XYZWebsiteBundle:Page');
$qb = $er->createQueryBuilder('p')
->where('p.published = :published')
->setParameter('published', 1)
;
$entities=$qb->getQuery()->getResult();
If you are sure there will never be no more than 1 page with the published status, you can even use getOneOrNullResult() instead of getResult(). This returns null if there is no matching entity, the entity if there is one matching, and an exception if there is more than one.
Edit: ok, so you have to build the query on PageVersion rather than page then. Try:
$er = $this->getDoctrine()->getRepository('XYZWebsiteBundle:PageVersion');
$qb = $er->createQueryBuilder('p')
->where('p.page = :page')
->setParameter('page', $page)
->andWhere('p.published = :published')
->setParameter('published', 1)
;
$pageVersion=$qb->getQuery()->getResult();
with $page being the page you are trying to find the published version for.