Using DQL functions inside Doctrine 2 ORDER BY - php

I'm doing a project in Symfony 2.3 with Doctrine 2.4 using MySQL database.
I have an Entity of FieldValue (simplified):
class FieldValue
{
/**
* The ID
*
* #var integer
*/
protected $fieldValueId;
/**
* Id of associated Field entity
*
* #var integer
*/
protected $fieldId;
/**
* Id of associated user
*
* #var integer
*/
protected $userId;
/**
* The value for the Field that user provided
*
* #var string
*/
protected $userValue;
/**
* #var \MyProjectBundle\Entity\Field
*/
protected $field;
/**
* #var \MyProjectBundle\Entity\User
*/
protected $user;
The problem I have is the fact that $userValue, while it's LONGTEXT in my database, can represent either actual text value , date or number, depending in the type of the Field.
The Field can be dynamically added. After adding a one to any of the users every other user can also fill it's own value for that Field.
While querying the database I use orderBy to sort on a certain column, which also can be one of those Fields. In that case I need to sort on $userValue. This is problematic when I need to have number fields sorted as numbers, and not as strings ('123' is less than '9' in that case...).
The solution for it (I thought) is to CAST the $sort, so I would get SQL like:
ORDER BY CAST(age AS SIGNED INTEGER) ASC
Since Doctrine does not have a built-in DQL function for that, I took the liberty of adding that to my project as INT DQL function (thanks to Jasper N. Brouwer):
class CastAsInteger extends FunctionNode
{
public $stringPrimary;
public function getSql(SqlWalker $sqlWalker)
{
return 'CAST(' . $this->stringPrimary->dispatch($sqlWalker) . ' AS SIGNED INTEGER)';
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->stringPrimary = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}
So happy with myself finding an easy solution I did that:
$sort = "INT(".$sort.")";
$queryBuilder->orderBy($sort, $dir);
which produced expected DQL:
ORDER BY INT(age) ASC
But also produced an exception:
An exception has been thrown during the rendering of a template ("[Syntax Error] line 0, col 12272: Error: Expected end of string, got '('") in MyProject...
So I've tried to find out what is going on and got into this in Doctrine\ORM\Query\Parser.php:
/**
* OrderByItem ::= (
* SimpleArithmeticExpression | SingleValuedPathExpression |
* ScalarExpression | ResultVariable
* ) ["ASC" | "DESC"]
*
* #return \Doctrine\ORM\Query\AST\OrderByItem
*/
public function OrderByItem()
{
...
}
Does that mean that there is no possibility to use DQL functions inside ORDER BY?
And if this is the case - is there any other way to achieve this?
UPDATE
I actually already have INT used in my select query, inside CASE WHEN:
if ($field->getFieldType() == 'number') {
$valueThen = "INT(".$valueThen.")";
}
$newFieldAlias = array("
(CASE
WHEN ...
THEN ".$valueThen."
ELSE ...
END
) as ".$field->getFieldKey());
Later on the $newFieldAlias is being added to the query.
Doesn't change anything...
UPDATE 2
Even when I add an extra select to the query, which will result in this DQL:
SELECT age, INT(age) as int_age
and then sort like that:
ORDER BY int_age ASC
I still don't het the correct result.
I've checked var_dump from $query->getResult(), and this is what I got:
'age' => string '57' (length=2)
'int_age' => string '57' (length=2)
Like CAST does not matter. I'm clueless...

Doctrine DQL does not accept functions as sort criteria but it does accept a "result variable". It means that you can do the following:
$q = $this->createQueryBuilder('e')
->addSelect('INT(age) as HIDDEN int_age')
->orderBy('int_age');

Doctrine 2 does not support INT by default, but you can use age+0.
$q = $this->createQueryBuilder('e')
->addSelect('age+0 as HIDDEN int_age')
->orderBy('int_age');

It is problem in your parser.php file. I have similar kind of issue and I solve this issue to replace below code in my parser file.
/**
* OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
*
* #return \Doctrine\ORM\Query\AST\OrderByClause
*/
public function OrderByClause()
{
$this->match(Lexer::T_ORDER);
$this->match(Lexer::T_BY);
$orderByItems = array();
$orderByItems[] = $this->OrderByItem();
while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
$this->match(Lexer::T_COMMA);
$orderByItems[] = $this->OrderByItem();
}
return new AST\OrderByClause($orderByItems);
}

Just use this:
->orderBy('u.age + 0', 'ASC');

Related

how to sort using KnpPaginator paginate() method

I'm trying to sort some data using KnpPaginator, but there is no example of how to use "options" array.
For now I sort data, by passing it to the findBy itself, but I can't sort by subobject attribute this way.
$this->limit = $request->query->getInt('limit', $container->getParameter('pagination.default_limit'));
$this->page = $request->query->getInt('page', 1);
$sortColumn = $this->camelcase($request->query->get('sort', 'id'));
$sortDirection = $request->query->get('direction', 'asc');
$this->sort = array($sortColumn => $sortDirection);
$this->list = $this->getRepo(Service::class)->findBy(
array('user' => $this->getUser()->getId()),
$this->sort
);
$pagination = $this->get('knp_paginator')->paginate(
$this->list,
$this->page,
$this->limit
);
It works totally fine when I want to sort by one of Service columns but I want to sort it by Service->getType()->getName() property.
Service has type property which is ManyToOne relation to ServiceType.
ServiceType uses DoctrineBehaviors Translatable, so column name belongs to other object named ServiceTypeTranslation.
I'd like to use its full functionality without having to write DQL myself, but when I try to name sort like "type.name" I get error that this column doesn't exist.
Eager loading wouldn't help as it only joins 1 table and translation for "name" property resides 2 tables deep from Service.
I know that I could try using $options array as 4th argument for paginate() but I can't get it to work.
Any ideas? Below some parts of Entities for visualisation.
Service.php
/**
* #ORM\Table
* #ORM\Entity
*/
class Service
{
...
/**
* #ORM\ManyToOne(targetEntity="DictServiceType", fetch="EAGER")
* #ORM\JoinColumn(referencedColumnName="id", nullable=false)
*/
private $type;
...
ServiceType.php
/**
* #ORM\Table
* #ORM\Entity
*/
class DictServiceType
{
use ORMBehaviors\Translatable\Translatable;
...
/**
* #return mixed
*/
public function getName()
{
return $this->translate(null, false)->getName();
}
...
ServiceTypeTranslation.php
/**
* #ORM\Table
* #ORM\Entity
*/
class DictServiceTypeTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* #var string
*
* #ORM\Column(type="string", length=255)
*/
private $name;
...
For the record, I tried to use paginator like this, based on what I read in the paginate() method description, but it's not even close to working.
There are no examples of how to use the $whitelist array anywhere, at least I couldn't find them.
$pagination = $this->get('knp_paginator')->paginate(
$this->list,
$this->page,
$this->limit,
array(
array(
$this->sort
)
)
);
And the phpdoc for this 4th argument says:
* #param array $options - less used options:
* boolean $distinct - default true for distinction of results
* string $alias - pagination alias, default none
* array $whitelist - sortable whitelist for target fields being paginated

Doctrine2 join column

I have defined the follow entity in doctrine2 (with symfony).
/**
*
* #ORM\Table(name="order")
* #ORM\Entity
*/
class Order
/**
* #var integer
*
* #ORM\Column(name="personid", type="integer", nullable=false)
*/
private $personid;
/**
* #ORM\OneToOne(targetEntity="People")
* #ORM\JoinColumn(name="personid", referencedColumnName="personid")
*/
private $person;
public function getPersonId()
{
return $this->personid;
}
public function getPerson()
{
return $this->person;
}
}
I realize that if I call $order->getPersonId() it return always an empty value and I have to call the getPerson()->getId() method to get the correct personid.
Could anyone explain me why the variable $personid is not filled?
Should I to delete the column id used for the join if I defined one?
Thanks
Gisella
You should remove private $personid;, it's better to work with objects only in an ORM.
It's not a problem if you get the ID with $order->getPerson()->getId(), because Doctrine won't load the complete entity. The People entity will only be loaded if you call an other field than the join key.
You can still have a getter shortcut like this :
public function getPersonId()
{
return $this->getPerson()->getId();
}
Edit :
You can also still work with "ID" if you use Doctrine references, like this :
$order->setPerson($em->getReference('YourBundle:People', $personId));
With this way, Doctrine won't perform a SELECT query to load data of the person.
You don't need to have the $personid field when you already have the $person field.
$people contains the People object (with all People's attributes including the id).
Moreover, when doctrine translate your object into sql tables, he knows that he have to join with th id so it will create a field (in database) named personid. (It's the name that you defined in your ORM)
/**
* #ORM\OneToOne(targetEntity="People")
* #ORM\JoinColumn(name="personid", referencedColumnName="personid")
*/
private $person;
Sorry for bad english :p

Get The Time From Database in Symfony 2

I have a field in database (Time) with this value 09:00:00.
I created the Entity and with Time Field
/**
* #var \DateTime
*
* #ORM\Column(name="m_01_ch", type="time")
*/
private $m_01_ch;
In my controller I retrieve the element and when I do:
$val = $myentity->getM01Ch();
My value is (in XDebug)
$val = {DateTime}[3]
date= "2015-07-08 09:00:00.000000"
timezone_type = 3
timezone "Europe/Rome"
If I get $val->date I have All the Date, but I want to get only 09:00:00
Can I take my "original" value without use Regex etc?
You can modify your entity where you are returning your value. In your function getM01Ch() do something like this
/**
* #return \DateTime
*/
public function getM01Ch()
{
$returnValue = $this->m_01_ch->format('h:i:s')
return $returnValue
}
Other than that I don't know if any better approach exists. More info

Symfony2/Doctrine2 - Issue when getting datetime from database

As I was working on my Symfony2 project a strange bug savagely appeared (again).
I created an entity Check containing a dateCreated attribute and some others attributes so I can link Check to different entities that are extending a ProductBase. Here are samples of Check and a AProduct :
/**
* Check
*
* #ORM\Table(name="check")
* #ORM\Entity
*/
class Check
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="date_created", type="datetime")
*/
private $dateCreated;
[...]
/**
* #ORM\ManyToOne(targetEntity="Acme\BlogBundle\Entity\AProduct", inversedBy="checks")
* #ORM\JoinColumn(name="aproduct_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $aproduct;
[...]
}
/**
* AProduct
*
* #ORM\Table(name="aproduct")
* #ORM\Entity
*/
class AProduct extends ProductBase
{
[...]
/**
* #ORM\OneToMany(targetEntity="Acme\BlogBundle\Entity\Check", mappedBy="product")
* #ORM\OrderBy({"dateCreated" = "DESC"})
*/
protected $checks;
[...]
}
So my problem is that when I am trying to display the dateCreated attribute in one of my controller, see code below, Symfony2 (or Doctrine2) is adding exactly one month to the date stored in the database and I don't know why it's happening :
[...]
$aproduct = $aproducts[0];
$checks = $aproduct->getChecks();
$lastCheck = $checks->toArray()[0]; //I know it's not 'safe' but it's shorter to expose my problem
var_dump($lastCheck->getDateCreated());
Result :
object(DateTime)[854]
public 'date' => string '2014-01-20 16:21:41' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
Value stored in database :
2013-12-20 16:21:41
What I don't understand the most is that in another controller, with the exact same methods but on a different product (BProduct for instance), I get the correct date...
Has anyone already faced to this problem or have any clue of what to do to solve it?
Thank you again. If you need more information just ask and I will try to help as most as I can.
Edit : The others aproduct stored in aproducts are displaying the correct date...
I see you are using:
#ORM\OrderBy({"dateCreated" = "DESC"})
Might be silly but check the id of returned Check instance.
Well I finally found my problem and my post here was really silly and completely unrelated to Symfony or Doctrine, sorry about that.
I was performing some "tests" on the last instance of my aproduct checks before displaying them and those "tests" were affecting the dateCreated value.
Here is what I was doing :
public static function updateAProductStatus(AProduct $product){
if(($check = $product->getChecks()->first()) instanceof Check){
$date = $check->getDateCreated();
$expiracyDate = $date->add(new \DateInterval('P1M')); //this line is the problem
$status = self::getStatus($expiracyDate); //this is only returning 'expired', 'expiring' or 'good' based on the difference between today's date and dateCreated + 1 month
} else {
$status = 'expired';
}
return $status;
}
So, as written in the code, $date->add(new \DateInterval('P1M')); is changing the stored value of Checks's dateCreated attribute. I don't understand exactly why it's affecting it because I'm not working directly on the Check instance.
The quick way to solve it was to explicitly clone the DateTime instance before adding a DateInterval :
$date = clone $date;
But I will add a new field in my Check or AProduct entity to store the expiracy date instead of calculating it on every update.
Update:
I read that PHP passes objects and arrays as reference and not as value. That's why I was having a problem here. I didn't know PHP was acting like that. I will be more careful in the future then!

How to specify several join conditions for 1:1 relationship in Doctrine 2

Documentation states:
class Cart
{
// ...
/**
* #OneToOne(targetEntity="Customer", inversedBy="cart")
* #JoinColumn(name="customer_id", referencedColumnName="id")
*/
private $customer;
// ...
}
This annotation represents such sql:
JOIN Customer c ON c.id = cart.customer_id
And the issue is that I need to add additional comparison there, like:
JOIN Customer c ON c.id = cart.customer_id AND c.anotherField = <constant>
Any solutions for that?
UPD:
the real additional condition I need for now is <const> BETWEEN c.f1 AND c.f2
There doesn't seem to be any solution to your problem that Doctrine could do auto-magically.
Since #ficuscr already gave you a solution for queries, there's only one more thing to handle - check your additional criteria and return the Customer instance in your getter on success and NULL on failure to meet additional criteria.
class Cart
{
const VALUE = '<some_constant_value>';
/**
* #OneToOne(targetEntity="Customer", inversedBy="cart")
* #JoinColumn(name="customer_id", referencedColumnName="id")
*/
private $customer;
// ...
/**
* #return Customer|null
*/
public function getCustomer()
{
if ($this->customer->getField1() <= self::VALUE
&& $this->customer->getField2() >= self::VALUE
) {
return $this->customer;
}
return null;
}
}
If this was a One-To-Many relation, Collection Filtering API (a.k.a. Criteria) could be used to filter the collection created by the mappings:
use Doctrine\Common\Collections\Criteria;
class Cart
{
const VALUE = '<some_constant_value>';
/**
* #OneToMany(targetEntity="Customer", mappedBy="cart")
*/
private $customers;
// ...
/**
* #return Customer[]|ArrayCollection
*/
public function getCustomers()
{
$expr = Criteria::expr();
$criteria = Criteria::create()
->where($expr->andX(
$expr->lte('field1', self::VALUE),
$expr->gte('field2', self::VALUE)
));
return $this->patientProblems->matching($criteria);
}
}
you can use the WITH keyword to specify additional join conditions, as you can see in some of the examples.
i think this should get you going:
SELECT l, c FROM location
INNER JOIN Customer c
WITH CURRENT_TIMESTAMP() BETWEEN c.f1 AND c.f2
WHERE CURRENT_TIMESTAMP() BETWEEN l.f1 AND l.f2
i removed the ON clause because i think there's no need to explicitly specify the join's ON fields unless they are not the "standard" ones (id of each entity)
also notice the call to CURRENT_TIMESTAMP() which translates into MySQL's NOW(). check out a list of other pretty useful aggregate functions and expresions here

Categories