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
Related
I'm working on a Symfony 6 project and I'm using sqlite as db.
I have a ManyToOne relation between two entities: Neighborhood and PropertyForSale.
When I delete a Neighborhood I want the $Neighborhood field of PropertyForSale to be set to null so I added:
#[ORM\JoinColumn(onDelete: 'SET NULL')] to the property:
#[ORM\ManyToOne(inversedBy: 'propertiesForSale')]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Neighborhood $neighborhood = null;
Everything seems to work properly if I change the database to MySql but with Sqlite this attribute seems to be ignored. I know has something to do with the default foreign key behavior in sqlite and
PRAGMA foreign_keys = ON; should be executed but I canĀ“t find I way to make it work with Symfony and Doctrine; Any ideas?
I share a bigger portion of my code:
// PropertyForSale.php
#[ORM\Entity(repositoryClass: PropertyForSaleRepository::class)]
class PropertyForSale
{
// ...
#[ORM\ManyToOne(inversedBy: 'propertiesForSale')]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Neighborhood $neighborhood = null;
// ...
public function getNeighborhood(): ?Neighborhood
{
return $this->neighborhood;
}
public function setNeighborhood(?Neighborhood $neighborhood): self
{
$this->neighborhood = $neighborhood;
return $this;
}
}
// Neighborhood.php
#[ORM\Entity(repositoryClass: NeighborhoodRepository::class)]
class Neighborhood
{
// ...
#[ORM\OneToMany(mappedBy: 'neighborhood', targetEntity: PropertyForSale::class)]
private Collection $propertiesForSale;
// ...
public function getPropertiesForSale(): Collection
{
return $this->propertiesForSale;
}
public function addPropertiesForSale(PropertyForSale $propertiesForSale): self
{
if (!$this->propertiesForSale->contains($propertiesForSale)) {
$this->propertiesForSale->add($propertiesForSale);
$propertiesForSale->setNeighborhood($this);
}
return $this;
}
public function removePropertiesForSale(PropertyForSale $propertiesForSale): self
{
if ($this->propertiesForSale->removeElement($propertiesForSale)) {
// set the owning side to null (unless already changed)
if ($propertiesForSale->getNeighborhood() === $this) {
$propertiesForSale->setNeighborhood(null);
}
}
return $this;
}
}
The only workaround I found was to add an event listener on the entity preRemove event and manually set to null the relation:
// NeighborhoodListener
namespace App\EventListener;
use Doctrine\ORM\EntityManagerInterface;
class NeighborhoodListener
{
public function __construct(private EntityManagerInterface $entityManager) {}
public function preRemove($args) {
$properties = $args->getObject()->getPropertiesForSale();
foreach ($properties as $property) {
$property->setNeighborhood(null);
$this->entityManager->persist($property);
}
$this->entityManager->flush();
}
}
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');
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.
With symfony && doctrine 1.2 in an action, i try to display the top ranked website for a user.
I did :
public function executeShow(sfWebRequest $request)
{
$this->user = $this->getRoute()->getObject();
$this->websites = $this->user->Websites;
}
The only problem is that it returns a Doctrine collection with all the websites in it and not only the Top ranked ones.
I already setup a method (getTopRanked()) but if I do :
$this->user->Websites->getTopRanked()
It fails.
If anyone has an idea to alter the Doctrine collection to filter only the top ranked.
Thanks
PS: my method looks like (in websiteTable.class.php) :
public function getTopRanked()
{
$q = Doctrine_Query::create()
->from('Website')
->orderBy('nb_votes DESC')
->limit(5);
return $q->execute();
}
I'd rather pass Doctrine_Query between methods:
//action
public function executeShow(sfWebRequest $request)
{
$this->user = $this->getRoute()->getObject();
$this->websites = $this->getUser()->getWebsites(true);
}
//user
public function getWebsites($top_ranked = false)
{
$q = Doctrine_Query::create()
->from('Website w')
->where('w.user_id = ?', $this->getId());
if ($top_ranked)
{
$q = Doctrine::getTable('Website')->addTopRankedQuery($q);
}
return $q->execute();
}
//WebsiteTable
public function addTopRankedQuery(Doctrine_Query $q)
{
$alias = $q->getRootAlias();
$q->orderBy($alias'.nb_votes DESC')
->limit(5)
return $q
}
If getTopRanked() is a method in your user model, then you would access it with $this->user->getTopRanked()
In your case $this->user->Websites contains ALL user websites. As far as I know there's no way to filter existing doctrine collection (unless you will iterate through it and choose interesting elements).
I'd simply implement getTopRankedWebsites() method in the User class:
class User extends BaseUser
{
public function getTopRankedWebsites()
{
WebsiteTable::getTopRankedByUserId($this->getId());
}
}
And add appropriate query in the WebsiteTable:
class WebsiteTable extends Doctrine_Table
{
public function getTopRankedByUserId($userId)
{
return Doctrine_Query::create()
->from('Website w')
->where('w.user_id = ?', array($userId))
->orderBy('w.nb_votes DESC')
->limit(5)
->execute();
}
}
You can also use the getFirst() function
$this->user->Websites->getTopRanked()->getFirst()
http://www.doctrine-project.org/api/orm/1.2/doctrine/doctrine_collection.html#getFirst()