I use Symfony 2 and the ORM Doctrine. I want to create and register a custom DQL function. In fact, I want to use the SQL function "CAST" in my request, like this :
$qb = $this->_em->createQueryBuilder();
$qb->select('d')
->from('\Test\MyBundle\Entity\MyEntity', 'd')
->orderBy('CAST(d.myField AS UNSIGNED)', 'ASC')
return $qb->getQuery()->getResult();
For this, I have created a "CastFunction" who extend "FunctionNode" :
namespace Test\MyBundle\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Parser;
class CastFunction extends FunctionNode
{
public $firstDateExpression = null;
public $secondDateExpression = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstDateExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_IDENTIFIER);
$this->secondDateExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return sprintf('CAST(%s AS %s)', $this->firstDateExpression->dispatch($sqlWalker), $this->secondDateExpression->dispatch($sqlWalker));
}
}
Of course, I have registered this class in my config.yml :
doctrine:
orm:
dql:
string_functions:
CAST: Test\MyBundle\DQL\CastFunction
Now, when I try my request, I obtain the following error:
"[Semantical Error] line 0, col 83 near 'UNSIGNED)': Error: 'UNSIGNED' is not defined."
I search but I don't where is the problem!
Have you got a idea?
After several search, I have finally found the solution. I had two problems: first my parse function was wrong, second, I called a SQL function in my orderBy (thank you Cerad).
So, here is my correct class:
namespace Ypok\YPoliceBundle\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Parser;
class CastFunction extends FunctionNode
{
public $firstDateExpression = null;
public $unit = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstDateExpression = $parser->StringPrimary();
$parser->match(Lexer::T_AS);
$parser->match(Lexer::T_IDENTIFIER);
$lexer = $parser->getLexer();
$this->unit = $lexer->token['value'];
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return sprintf('CAST(%s AS %s)', $this->firstDateExpression->dispatch($sqlWalker), $this->unit);
}
}
And now, I can use perfectly the SQL function 'CAST' in my repository:
$qb = $this->_em->createQueryBuilder();
$qb->select('d, CAST(d.myField AS UNSIGNED) AS sortx')
->from('\Test\MyBundle\Entity\MyEntity', 'd')
->orderBy('sortx', 'ASC')
return $qb->getQuery()->getResult();
Best regards
Can't find the reference but functions are not allowed in the order by clause. You need to cast your value in the select statement then sort by it.
Something like:
$qb->select('d, CAST(d.myField AS UNSIGNED) AS sortx)
->from('\Test\MyBundle\Entity\MyEntity', 'd')
->orderBy('sortx, 'ASC')
That is assuming your CAST function is written correctly.
Related
I do not understand how to properly connect the model and filter. I implemented this using Dependency Injection in the controller, but I don’t like the fact that it needs to be done in every method where filtering should be applied. It would be very convenient if the model itself understood which class with filters to use.
Tell me how to do better.
Filtration Class:
namespace App\Classes\Filter;
class QueryFilter
{
protected $query;
protected $params;
public function apply($query, $params)
{
$this->query = $query;
$this->params = $params;
foreach ($this->filters() as $filter => $value){
if(method_exists($this, $filter)){
$this->$filter($value);
}
}
return $this->query;
}
public function filters()
{
return $this->params;
}
}
Heirs implement filters for different models:
namespace App\Classes\Filter;
class PositionFilter extends QueryFilter
{
public function title($value)
{
$this->query->where('title', 'LIKE', "%$value%");
}
}
class GasStationFilter extends QueryFilter
{
public function number($value)
{
$this->query->where('number', 'LIKE', "%$value%");
}
public function region($value)
{
$this->query->whereHas('region', function ($query) use ($value){
$query->where('regions.id', $value);
});
}
}
In the controller, I inject the desired class with filters and apply filtering like this (I use scope in the model):
public function index(GasStationIndexRequest $request, GasStationFilter $filters)
{
$gasStations = GasStation::with('region')
->filter($request->validated(), $filters)
->take(10)
->get();
return GasStationSelect2Resource::collection($gasStations);
}
Model:
namespace App\Models;
class GasStation extends ListModel
{
public function region(): BelongsTo
{
return $this->belongsTo(Region::class);
}
public function scopeFilter($query, $params, $filters) : Builder
{
return $filters->apply($query, $params);
}
}
What is the best practice to chain repository methods to reuse query building logic?
Here is how I did it, but I doubt if this is the right way:
use Doctrine\ORM\Mapping;
use Doctrine\ORM\EntityManager;
class OrderRepository extends \Doctrine\ORM\EntityRepository
{
private $q;
public function __construct(EntityManager $em, Mapping\ClassMetadata $class)
{
parent::__construct($em, $class);
$this->q = $this->createQueryBuilder('o');
}
public function getOneResult()
{
return $this->q->getQuery()->getOneOrNullResult();
}
public function getResult()
{
return $this->q->getQuery()->getResult();
}
public function filterByStatus($status)
{
$this->q->andWhere('o.status = :status')->setParameter('status', $status);
return $this;
}
public function findNextForPackaging()
{
$this->q->leftjoin('o.orderProducts', 'p')
->orderBy('o.deliveryDate', 'ASC')
->andHaving('SUM(p.qtePacked) < SUM(p.qte)')
->groupBy('o.id')
->setMaxResults(1);
return $this;
}
}
This allows me to chain method like this:
$order = $em->getRepository('AppBundle:Order')->filterByStatus(10)->findNextForPackaging()->getOneResult();
This is of course just an example. In reality there are many more methods that can be chained.
One big problem with this is the fact that I need a join for some of the "filters", so I have to check if the join has already been set by some method/filter before I add it. ( I did not put it in the example, but I figured it out, but it is not very pretty )
The other problem is that I have to be careful when using the repository, as the query could already be set to something, so I would need to reset the query every time before using it.
I also understand that I could use the doctrine "matching" method with criteria, but as far as I understood, this is rather expensive, and also, I don't know how to solve the "join" Problem with that approach.
Any thoughts?
I made something similar to what you want:
Controller, this is how you use it. I am not returning Response instance but serialize the array in kernel.view listener but it is still valid example:
/**
* #Route("/root/pending_posts", name="root_pending_posts")
* #Method("GET")
*
* #return Post[]
*/
public function pendingPostsAction(PostRepository $postRepository, ?UserInterface $user): array
{
if (!$user) {
return [];
}
return $postRepository->begin()
->wherePublished(false)
->whereCreator($user)
->getResults();
}
PostRepository:
class PostRepository extends BaseRepository
{
public function whereCreator(User $user)
{
$this->qb()->andWhere('o.creator = :creator')->setParameter('creator', $user);
return $this;
}
public function leftJoinRecentComments(): self
{
$this->qb()
->leftJoin('o.recentCommentsReference', 'ref')->addSelect('ref')
->leftJoin('ref.comment', 'c')->addSelect('c');
return $this;
}
public function andAfter(\DateTime $after)
{
$this->qb()->andWhere('o.createdAt > :after')->setParameter('after', $after);
return $this;
}
public function andBefore(\DateTime $before)
{
$this->qb()->andWhere('o.createdAt < :before')->setParameter('before', $before);
return $this;
}
public function wherePublished(bool $bool)
{
$this->qb()->andWhere('o.isPending = :is_pending')->setParameter('is_pending', !$bool);
return $this;
}
}
and BaseRepository has most used stuff, still work in progress:
namespace wjb\CoreBundle\Model;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
abstract class BaseRepository extends EntityRepository
{
/** #var QueryBuilder */
private $qb;
public function begin()
{
$this->qb = $this->createQueryBuilder('o');
return $this;
}
public function qb(): QueryBuilder
{
return $this->qb;
}
public function none()
{
$this->qb()->where('o.id IS NULL');
return $this;
}
public function setMaxResults($maxResults)
{
$this->qb()->setMaxResults($maxResults);
return $this;
}
public function addOrderBy($sort, $order = null)
{
$this->qb()->addOrderBy($sort, $order);
return $this;
}
public function getResults()
{
return $this->qb()->getQuery()->getResult();
}
}
This helps me a lot in chaining calls like in controller example.
I'm writing a simple Custom Doctrine Function on Symfony that computes AGE given the bithdate of the entity. Here is my function:
class AgeFunction extends FunctionNode
{
private $birthDate;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->birthDate = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
$bday = $this->birthDate->dispatch($sqlWalker);
$currDate = DateFormatter::formatDate(new \DateTime());
return "TIMESTAMPDIFF(YEAR, {$bday}, '{$currDate}')";
}
}
And here is how i used it:
public function getAge()
{
$qb = $this->createQueryBuilder('s')
->select('AGE(s.dateOfBirth)')
->orderBy('s.id', 'DESC');
dump($qb->getQuery()->getResult());
}
This is the query produced:
SELECT TIMESTAMPDIFF(YEAR, s0_.date_of_birth, '2017-04-13') AS sclr_0 FROM suspect s0_ ORDER BY s0_.id DESC;
I think whats wrong here is s0_.date_of_birth never gets the actual value since when i replace it manually it works well.
So how can I do this? Thanks.
Maybe you're originally trying to do something else but the business requirement seems weird to me cos you're trying get just last person's age . Anyway let me just ignore it for now and focus on what you need. I've checked the example below and worked fine.
DQL
namespace My\Bundle\Product\APIBundle\Entity\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class TimestampDiff extends FunctionNode
{
public $value;
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->value = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker)
{
return sprintf(
'TIMESTAMPDIFF(YEAR, %s, %s)',
$this->value->dispatch($sqlWalker),
date('Y-m-d')
);
}
}
REPOSITORY
public function findAge()
{
$qb = $this->createQueryBuilder('s')
->select('TIMESTAMPDIFF(s.dateOfBirth) AS Age')
->orderBy('s.id', 'DESC')
->setMaxResults(1);
return $qb->getQuery()->getResult(Query::HYDRATE_SIMPLEOBJECT);
}
CALL
$p = $this->suspectRepository->findAge();
REGISTER (My setup is different so you can check links below to make it work for your setup)
# app/config.yml
doctrine:
dbal:
default_connection: hello
connections:
hello:
driver: "%database_driver%"
host: "%database_host%"
....
....
orm:
default_entity_manager: hello
entity_managers:
hello:
dql:
string_functions:
TIMESTAMPDIFF: My\Bundle\Product\APIBundle\Entity\DQL\TimestampDiff
connection: hello
....
How to Register custom DQL Functions
How to create and use custom built doctrine DQL function in symfony
RESULT
SELECT
TIMESTAMPDIFF(YEAR, s0_.date_of_birth, 2017-04-13) AS sclr_0
FROM suspect s0_
ORDER BY s0_.id DESC
LIMIT 1
How can I get a random result with an dql Query?
This is my query:
$firstCategoryId = 50;
$repository = $this->entityManager->getRepository(BaseProduct::class);
$products = $repository->createQueryBuilder('p')
->join('p.categories', 'c')
->where('c.id = :categoryId')
->setParameter('categoryId', $firstCategoryId)
->getQuery()
->setMaxResults(4)
->getResult();
This returns me always the first 4 products.
Lets say the category with ID 50 has over 100 products. And what I want is querying randomly 4 articles from category with ID 50. But how? Is this possible? Of course I can set no Max Result and than do it with PHP... but this is not a good solution because of performance.
You need to create dql function for that. https://gist.github.com/Ocramius/919465 you can check that.
namespace Acme\Bundle\DQL;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
class RandFunction extends FunctionNode
{
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker)
{
return 'RAND()';
}
}
After that open your config.yml file and add autoload that RandFunction.
orm:
dql:
numeric_functions:
Rand: Acme\Bundle\DQL\RandFunction
And your query must be like:
$firstCategoryId = 50;
$repository = $this->entityManager->getRepository(BaseProduct::class);
$products = $repository->createQueryBuilder('p')
->join('p.categories', 'c')
->addSelect('RAND() as HIDDEN rand')
->where('c.id = :categoryId')
->orderBy('rand')
->setParameter('categoryId', $firstCategoryId)
->getQuery()
->setMaxResults(4)
->getResult();
I try to get count of persons by age bracket.
AGEBRACKET | NBR
10 | 3
20 | 14
30 | 123
40 | 4
50 | 55
...
This is my code:
$qb = $em->createQueryBuilder();
$qb->select('FLOOR((YEAR(CURDATE())-YEAR(p.date_birth)) / 10) * 10 AS age, COUNT(p.id)');
$qb->from('MyBundle:Person', 'p');
$qb->groupBy('age');
$countByAge = $qb->getQuery()->execute();
I get this error:
[Syntax Error] line 0, col 7: Error: Expected known function, got
'FLOOR'
I look a little bit for a solution, and this is what have I found:
<?php
namespace MyProject\Query\AST;
use \Doctrine\ORM\Query\AST\Functions\FunctionNode;
use \Doctrine\ORM\Query\Lexer;
class MysqlFloor extends FunctionNode
{
public $simpleArithmeticExpression;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'FLOOR(' . $sqlWalker->walkSimpleArithmeticExpression(
$this->simpleArithmeticExpression
) . ')';
}
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$lexer = $parser->getLexer();
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}
<?php
\Doctrine\ORM\Query\Parser::registerNumericFunction('FLOOR', 'MyProject\Query\MysqlFloor');
$dql = "SELECT FLOOR(person.salary * 1.75) FROM CompanyPerson person";
And I get another error:
Attempted to call method "registerNumericFunction" on class "Doctrine\ORM\Query\Parser".
Have you any idea how I can do to have the desired result.
Thanks
There's an updated version in the Doctrine docs that should help you:
http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html#adding-your-own-functions-to-the-dql-language
If you want to add it to your Symfony config so it can be used everywhere in your project, see http://symfony.com/doc/current/cookbook/doctrine/custom_dql_functions.html for how you can do that.
The solution:
#config.yml
orm:
dql:
numeric_functions:
FLOOR: FrontBundle\DoctrineFunctions\FloorFunction
#FloorFunction.php
<?php
namespace MyBundle\DoctrineFunctions;
use \Doctrine\ORM\Query\AST\Functions\FunctionNode;
use \Doctrine\ORM\Query\Lexer;
class FloorFunction extends FunctionNode
{
public $simpleArithmeticExpression;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'FLOOR(' . $sqlWalker->walkSimpleArithmeticExpression(
$this->simpleArithmeticExpression
) . ')';
}
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}
$config = $em->getConfiguration();
$config->addCustomNumericFunction('FLOOR', 'MyBundle\DoctrineFunctions\FloorFunction');