An application has a page of statistics representing dozens of calculations. To avoid duplicating code in a repository the error
Error: 'c' is used outside the scope of its declaration
occurs when attempting to insert DQL with conditions into a QueryBuilder.
The basic entities include Household and Contact. Calculations are based on contact date range, site (location of contact), and type (type of contact). There is a service that creates an array of where clauses and query parameters, as will be evident in the code below.
I know the calculation works if all the code occurs in a single function. It seems the problem arises from the join with the Contact entity and its necessary constraints. Can DRY be accomplished in this scenario?
All of the following appear in the Household entity's repository.
The DQL is:
private function reportHousehold($criteria)
{
return $this->createQueryBuilder('i')
->select('i.id')
->join('TruckeeProjectmanaBundle:Contact', 'c', 'WITH',
'c.household = i')
->where($criteria['betweenWhereClause'])
->andWhere($criteria['siteWhereClause'])
->andWhere($criteria['contactWhereClause'])
->getDQL()
;
}
Example of $criteria: $criteria['betweenWhereClause'] = 'c.contactDate BETWEEN :startDate AND :endDate'
One of the calculations on Household:
public function res($criteria)
{
$parameters = array_merge(
$criteria['betweenParameters'], $criteria['siteParameters'],
$criteria['startParameters'], $criteria['startParameters'],
$criteria['contactParameters']);
$qb = $this->getEntityManager()->createQueryBuilder();
return $this->getEntityManager()->createQueryBuilder()
->select('h.id, 12*(YEAR(:startDate) - h.arrivalyear) + (MONTH(:startDate) - h.arrivalmonth) Mos')
->from('TruckeeProjectmanaBundle:Household', 'h')
->distinct()
//DQL inserted here:
->where($qb->expr()->in('h.id', $this->reportHousehold($criteria)))
->andWhere($qb->expr()->isNotNull('h.arrivalyear'))
->andWhere($qb->expr()->isNotNull('h.arrivalmonth'))
->andWhere($criteria['startWhereClause'])
->setParameters($parameters)
->getQuery()->getResult()
;
}
You're either missing getRepository() or from()
Try this (my prefered choice) :
private function reportHousehold($criteria) {
return $this->getEntityManager
->createQueryBuilder()
->select("i.id")
->from(YourEntity::class, "i")
->join("TruckeeProjectmanaBundle:Contact", "c", "WITH", "c.household=i.id")
->where($criteria['betweenWhereClause'])
->andWhere($criteria['siteWhereClause'])
->andWhere($criteria['contactWhereClause'])
->getQuery()
->execute();
}
Or this
private function reportHousehold($criteria) {
return $this->getEntityManager
->getRepository(YourEntity::class)
->createQueryBuilder("i")
->select("i.id")
->join("TruckeeProjectmanaBundle:Contact", "c", "WITH", "c.household=i.id")
->where($criteria['betweenWhereClause'])
->andWhere($criteria['siteWhereClause'])
->andWhere($criteria['contactWhereClause'])
->getQuery()
->execute();
}
Careful, I'm assuming you're on Symfony 3 or above.
If not, replace YourEntity::class by Symfony 2 syntax which is "YourBundle:YourEntity"
In a sense Preciel is correct: the solution does require the use of $this->getEntityManager()->createQueryBuilder(). Instead of injecting DQL as a subquery, the trick is to return an array of ids and use the array in an IN clause. The effect is to remove any consideration of entities other than the Household entity from the calculation. Here's the result:
public function res($criteria)
{
$parameters = array_merge($criteria['startParameters'], $criteria['startParameters'], ['hArray' => $this->reportHousehold($criteria)]);
$qb = $this->getEntityManager()->createQueryBuilder();
return $this->getEntityManager()->createQueryBuilder()
->select('h.id, 12*(YEAR(:startDate) - h.arrivalyear) + (MONTH(:startDate) - h.arrivalmonth) Mos')
->from('TruckeeProjectmanaBundle:Household', 'h')
->distinct()
->where('h.id IN (:hArray)')
->andWhere($qb->expr()->isNotNull('h.arrivalyear'))
->andWhere($qb->expr()->isNotNull('h.arrivalmonth'))
->setParameters($parameters)
->getQuery()->getResult()
;
}
private function reportHousehold($criteria)
{
$parameters = array_merge($criteria['betweenParameters'], $criteria['siteParameters'], $criteria['contactParameters']);
return $this->createQueryBuilder('i')
->select('i.id')
->join('TruckeeProjectmanaBundle:Contact', 'c', 'WITH', 'c.household = i')
->where($criteria['betweenWhereClause'])
->andWhere($criteria['siteWhereClause'])
->andWhere($criteria['contactWhereClause'])
->setParameters($parameters)
->getQuery()->getResult()
;
}
Related
Good afternoon, please tell me how to get the column data:
{#ORM\Index(name="localities_names_idx", columns={"name_ru", "name_cn", "name_en"})
I tried using queryBuilder :
$qb
->select('a')
->from(Locality::class, 'a')
->where('a.name_ru = :name_ru')
->andWhere('a.area is null')
->setParameter('name', 'Moscow');
$query = $qb->getQuery();
Without success
I need it for:
$em = $this->entityManager;
$qb = $em->createQueryBuilder();
$qb
->select('a')
->from(Locality::class, 'a')
->where('a.name_ru = :name_ru')
->andWhere('a.area is null')
->setParameter('name', 'Москва');
$query = $qb->getQuery();
$result = $query->getResult(Query::HYDRATE_SCALAR);
$location = new Location();
$location->setLocality($result[0]['a_id']);
$location->setAddress($address);
$em->persist($location);
$em->flush();
return $location->getId();
Edit: This turned into a review, but might as well keep it here
You're using index wrong.
I'm guessing you're using it incorrectly. I'm assuming you want an index for faster search. However, the way you do now you've created a combined index. Example:
{#ORM\Index(name="thing_colour_idx", columns={"thing", "colour"})
Thing | Colour
--------------
Car Blue
Car Red
Bike Green
Bike Yellow
If you always (or at least most of the time) select by both columns, eg you always search for a BLUE+CAR, or a GREEN+BIKE, than this is the way to go.
However, if you to select all Thing=Car, without colour, then this index does nothing. You want this:
indexes={
#ORM\Index(name="thing_idx", columns={"thing"}),
#ORM\Index(name="colour_idx", columns={"colour"})
}
You're not using getResult as intended
You do ->getResult(Query::HYDRATE_SCALAR), but then follow it up with a ->setLocality($result[0]['a_id']). Unless you have performace issues, you dont work with IDs, thats a problem for Doctrine, not you. You should only care about objects:
$locality = $em
->createQueryBuilder()
->from(Locality::class, 'a')
->where('a.name_ru = :name_ru')
->andWhere('a.area is null')
->setParameter('name', 'Москва') // <- btw, this should be 'name_ru', same as two lines heigher
->getQuery()
->getSingleResult();
$location = new Location();
$location->setLocality($locality);
Dont use the querybuilder like that, use a repository
You're now placing service logic and query logic in one code. That is incorrect. An example of the way Symfony is intented:
class LocalityRepository extends ServiceEntityRepository{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Locality::class);
}
public function getLocalityByRuName(string $name): Locality
{
return $this->createQueryBuilder('a')
->where('a.name_ru = :name_ru')
->andWhere('a.area is null')
->setParameter('name_ru', $name);
->getQuery();
->getSingleResult();
}
}
class YourService
{
public function __construct(
private LocalityRepository $localityRepository
){}
public function somethingSomething()
{
$locality = $this->localityRepository->getLocalityByRuName('Москва');
$location = new Location();
$location->setLocality($locality);
}
}
Final note: Try not to use a ->flush() in a service. The job of a service is to do work, not to decide what to do it. A controller decides when something is done. Suppose you have another service which in which you alter things, but DONT want to flush them. You can now no longer use any service method that flushes.
The error was that there were no name properties Ru
I have 2 Entities
User
Article
and a “likedByUsers” Many To Many relationship between both.
When I show an article, I want to know if the user has liked it so a heart icon is shown.
I've got this in the ArticleRepository:
public function findOneBySlug($slug,$userId): ?Pack
{
return $this->createQueryBuilder('p')
->andWhere('p.slug = :val')
->setParameter('val', $slug)
->addSelect('COUNT(u) AS userLike', 'p')
->leftJoin("p.users", 'u', 'WITH', 'u.id = :userId')
->setParameter('userId', $userId)
->getQuery()
->getOneOrNullResult()
;
}
But it throws an error:
Return value of App\Repository\ArticleRepository::findOneBySlug() must be
an instance of App\Entity\Article or null, array returned
I want to add "userLike" (bool) to the Article returned entity. Anyone can help me out?
calling addSelect(...) on a query builder might change the return type / format.
in your particular case, the former db result was something like [... all the article properties ...] which hydration and the getOneOrNullResult turns into one Article or null.
the new format looks like
[... all the article properties ..., userlike], which hydration turns into [Article, userlike] which can't possibly turned into one Article or a null result, because it's a "more complex" array.
So you have to use a different result fetcher. Depending on what the caller of your function expects as a return value (I would expect an article ^^) you maybe should rename the function or add a virtual property on article to hide the userlike or something, so you can return just the Article or null.
So the solution that I would choose:
$result = $this->createQueryBuilder(...)
//...
->getSingleResult();
if(!$result) {
// empty result, obviously
return $result;
}
// $result[0] is usually the object.
$result[0]->userLike = $result['userLike'];
// or $result[0]->setUserLike($result['userLike'])
return $result[0];
btw: $this->createQueryBuilder($alias) in a repository automatically calls ->select($alias), so you don't have to addSelect('... userLike', 'p') and just do addSelect('... userLike')
I want to create a query that values more precise search terms, e.g. search for "Essen" should return Essen currently it returns Evessen as this is a valid value as well.
My current function:
public function findCities($city){
$qb = $this->createQueryBuilder('z');
$qb
->select('z')
->where($qb->expr()->like('z.city', ':city'))
->orderBy('z.code')
->setParameter('city', '%'.$city . '%');
return $qb->getQuery()->getResult();
}
Based on THIS advice I created a repository function:
public function findCities($city){
$qb = $this->createQueryBuilder('z');
$qb
->select('z')
->where($qb->expr()->like('z.city', ':city'))
->orderBy('INSTR(z.city, '.$city.'), z.city')
->setParameter('city', '%'.$city . '%');
return $qb->getQuery()->getResult();
}
Unfortunately it returns [Syntax Error] line 0, col 70: Error: Expected known function, got 'INSTR'
Any other approach (that does NOT return an array, as there is a function that needs heavy altering if the output is an array, I'd like to avoid that) maybe?
There is no INSTR function in DQL, that's why you get this error see docs
instead you can make NativeQuery see docs
something like this
$rsm = new \Doctrine\ORM\Query\ResultSetMapping();
$rsm->addEntityResult('City', 'c');
// for every selected field you should do this
$rsm->addFieldResult('c', 'id', 'id');
$rsm->addFieldResult('c', 'name', 'name');
$em = $this->getEntityManager()
->createNativeQuery('
SELECT
id, name
FROM cities WHERE city LIKE '%:city%'
ORDER BY INSTR(city, ':city'), city',
$rsm
)->setParameter('city', $city);
return $em->getResult();
Is there any way to alias fields when using partial object syntax in Doctrine 2?
I know I can do this:
$this->createQueryBuilder('user')->select([
'user.id AS id',
'user.firstName AS first_name',
'user.lastName AS last_name',
'user.email AS email',
'user.dateCreated AS date_created'
])->getQuery()->getArrayResult();
However I need to use the partial object syntax in order for doctrine to retrieve the result in a nested relational heirarchy:
$this->createQueryBuilder('team')
->select('PARTIAL team.{id, name, dateCreated}, s, PARTIAL e.{id, name}')
->innerJoin('team.session', 's')
->innerJoin('s.event', 'e')
->getQuery()->getArrayResult();
I dug around in Doctrine\ORM\Internal\Hydration\ArrayHydrator but didn't see any hooks or anything, and it doesn't look like Doctrine has a postSelect event or something that would allow me to implement my own mutation.
Thanks for any help!
Not very efficient, but I ended up subclassing the ArrayHydrator and mutating the keys myself.
Hopefully there is a better way, if not I hope this helps someone
The problem is not just about alias, it's also about bracket. Basically, Partial object syntax is very poor and does not allow aliases or brackets. It expects for a coma or the end of the list, and everything else will throw a syntax error.
I wanted to retrieve a partial object collection using a SUM() function like this
public function findByCompetition(array $competition)
{
$competitionField = $competition['field'] ?? 'Id';
$competitionValue = $competition['value'] ?? 0;
$teamsCompStatsByComp = $this->createQueryBuilder('t')
->select('partial t.{Id, competitionId, competitionOldId, teamId, teamOldId, SUM(goalsAttempted) goalsAttempted}')
->where('t.'.$competitionField.' = ?1')
->groupBy('t.teamId')
->orderBy('t.Id', 'ASC')
->setParameter('1', $competitionValue)
->getQuery()
->getResult()
;
return new ArrayCollection($teamsCompStatsByComp);
}
But got the same error
[Syntax Error] line 0, col 85: Error: Expected Doctrine\ORM\Query\Lexer::T_CLOSE_CURLY_BRACE, got '('
I had to retrieve data as an array, use IDENTITY() on Foreign Key, then manually hydrate my entities
public function findByCompetition(array $competition)
{
$competitionField = $competition['field'] ?? 'Id';
$competitionValue = $competition['value'] ?? 0;
$teamsCompStatsByComp = $this->createQueryBuilder('t')
->select('t.Id, IDENTITY(t.competitionId) competitionId, t.competitionOldId, IDENTITY(t.teamId) teamId, t.teamOldId, SUM(goalsAttempted) goalsAttempted')
->where('t.'.$competitionField.' = ?1')
->groupBy('t.teamId')
->orderBy('t.Id', 'ASC')
->setParameter('1', $competitionValue)
->getQuery()
->getResult()
;
foreach ($teamsCompStatsByComp as $key => $teamCompStats) {
$teamsCompStatsByComp[$key] = new TeamStatistics($teamCompStats);
}
return new ArrayCollection($teamsCompStatsByComp);
}
I think we should open an issue on Github to improve partial syntax behavior.
I have this query with a subquery:
$query = $this->getEntityManager()->createQueryBuilder();
$subquery = $query;
$subquery
->select('f.following')
->from('ApiBundle:Follow', 'f')
->where('f.follower = :follower_id')
->setParameter('follower_id', $id)
;
$query
->select('c')
->from('ApiBundle:Chef', 'c')
->where('c.id <> :id')
->setParameter('id', $id)
;
$query
->andWhere(
$query->expr()->notIn('c.id', $subquery->getDQL())
);
return $query->getQuery()->getResult();
And I get this error:
[Semantical Error] line 0, col 116 near 'f, ApiBundle:Chef': Error: 'f' is already defined.
I can't find the cause of the error, the alias f is defined only one time. Any suggestions?
This issue is about objects and references in PHP.
When you do $subquery = $query;, $query being an object, you simply have $subquery pointing to the same value.
A PHP reference is an alias, which allows two different variables to
write to the same value. As of PHP 5, an object variable doesn't
contain the object itself as value anymore. It only contains an object
identifier which allows object accessors to find the actual object.
When an object is [...] assigned to another
variable, the different variables are not aliases: they hold a copy of
the identifier, which points to the same object.
Reference: http://us1.php.net/manual/en/language.oop5.references.php
It means in your code that when you write this:
$subquery
->select('f.following')
->from('ApiBundle:Follow', 'f')
->where('f.follower = :follower_id')
->setParameter('follower_id', $id)
;
This is equivalent to:
$query
->select('f.following')
->from('ApiBundle:Follow', 'f')
->where('f.follower = :follower_id')
->setParameter('follower_id', $id)
;
So when at the end you call:
$query->andWhere(
$query->expr()->notIn('c.id', $subquery->getDQL())
);
You are using 2 times the same object pointed by 2 different variables ($query === $subquery).
To solve this issue, you can either use:
$query = $this->getEntityManager()->createQueryBuilder();
$subquery = $this->getEntityManager()->createQueryBuilder();
Or the clone keyword:
$query = $this->getEntityManager()->createQueryBuilder();
$subquery = clone $query;
I would like to share my solution which requires ORM mapping:
Following entities are mapped like this:
Event 1:M Participant
Participant class
/**
* #ORM\ManyToOne(targetEntity="KKB\TestBundle\Entity\Event", inversedBy="participants")
* #ORM\JoinColumn(name="event_id", referencedColumnName="id", nullable=false)
*/
private $event;
Event class
/**
* #ORM\OneToMany(targetEntity="KKB\TestBundle\Entity\Participant", mappedBy="event", cascade={"persist"})
*/
private $participants;
class EventRepository extends \Doctrine\ORM\EntityRepository
{
public function getEventList($userId)
{
$query = $this->createQueryBuilder('e');
$subquery = $this->createQueryBuilder('se');
$subquery
->leftJoin('se.participants', 'p')
->where('p.user = :userId')
;
return $query->where($query->expr()->notIn('e.id', $subquery->getDQL()))
->setParameter('userId', $userId)
;
}
}