how to sort using KnpPaginator paginate() method - php

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

Related

Symfony 5 / Doctrine: Sorting entity collection by creation DateTime

I am developing a project using Symfony 5. One of my use-cases involves reading a collection from the Database, with the items sorted by creation order, descending (newest first). I am using the "Timestampable" extension from "stof/doctrine-extensions-bundle" to save the createdAt and updatedAt timestamps in my entity.
According to Doctrine documentation, I can sort items ussing the Repository methods:
$sortedEntities = $repository->findBy(array('createdAt' => 'DESC'));
This is the attribute in question:
/**
* #var \DateTime $createdAt
*
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
private $createdAt;
However, using 'ASC' or 'DESC' seems to have no impact on the ordering of the list.
You are not reading the documentation correctly. The orderBy is the second argument, not the first.
The example given in the docs is
$tenUsers = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20), array('name' => 'ASC'), 10, 0);
Here, you can see the orderBy (name, ASC) is the second arg. The first arg is a where arg - in this case, WHERE age = 20.
Here is the full signature from Doctrine\Persistence\ObjectRepository
/**
* Finds objects by a set of criteria.
*
* Optionally sorting and limiting details can be passed. An implementation may throw
* an UnexpectedValueException if certain values of the sorting or limiting details are
* not supported.
*
* #param array<string, mixed> $criteria
* #param string[]|null $orderBy
* #param int|null $limit
* #param int|null $offset
* #psalm-param array<string, 'asc'|'desc'|'ASC'|'DESC'> $orderBy
*
* #return object[] The objects.
* #psalm-return T[]
*
* #throws UnexpectedValueException
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null);
I hope that clarifies it for you. :-)
[EDIT] In response to your comment, you cannot use true as a value for the first argument. Look at the signature I posted. The first argument is #param array<string, mixed>, so it needs an array. Try this then:
sortedEntities = $repository->findBy(array(), array('createdAt' => 'DESC'));

Doctrine Constraint on a Many-to-Many Relation

We have an Entity 'User' that has a Many-to-Many Relation to different 'Types'. The MySQL Relation is handled via a Cross Table.
class User {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Type")
* #ORM\JoinTable(name="user_type",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="type_id", referencedColumnName="id")}
* )
*/
protected $types;
}
class Type {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\User")
* #ORM\JoinTable(name="user_type",
* joinColumns={#ORM\JoinColumn(name="type_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")}
* )
*/
protected $users;
/**
* #ORM\Column(type="datetime")
*/
protected $publishedAt;
}
How can I limit the Responses in this many-to-many relation to only show Types that have already been published?
That is the factual SQL should include a WHERE that filters the corresponding items that have not been published yet. Can I do that with an annotation or something like that?
I know I can do this by filtering the returned collection. But is there anything more efficient than that?
This question is kind of a douplicate.
It has been answered here: php - Doctrine2 association mapping with conditions - Stack Overflow
One of the comments tells, that this results in an error for Many-to-Many Relations. As of Doctrine 2.5 this no longer is the case.
So what you can do in doctrine is hand over a query condition when you request the entities of the relation:
So you do not change the Annotation, but the getter for the Entity:
class User {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Type")
* #ORM\JoinTable(name="user_type",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="type_id", referencedColumnName="id")}
* )
*/
protected $types;
public function getTypes()
{
$criteria = Criteria::create()->where(Criteria::expr()->lte('publishedAt', date('Y-m-d')));
return $this->types->matching($criteria);
}
}
This will result in a Lazy-Fetch (depending on your settings) of the required items. All the doctrine magic still works, like caching and the like. So the collection will only be fetched, if it has not been fetched...
You can use Criteria.
Add a function to your User class called eg getPublishedTypes.
public function getPublishedTypes()
{
return $this->getTypes()->matching(TypeRepository::createPublishedCriteria());
}
And in your TypeRepository add the function createPublishedCriteria.
static public function createPublishedCriteria()
{
return Criteria::create()
->andWhere(Criteria::expr()->lte('publishedAt', date('Y-m-d')); //your logic
}
Note: function has to be static
You can also use that criteria later on in your query builder with following line
$qb->addCriteria(self::createPublishedCriteria()).
Another solution with bad practise could be collection filtering. Add in your User class:
public function getPublishedTypes()
{
return $this->getTypes()->filter(function(Type $type) {
return $type->getPublishedAt < date('Y-m-d');
}
This version is not that great, because it produces way more queries (bad with large data in your database).

Change Entity assert constrains dynamically without FormType

So the problem is like this:
I am trying to save some data from API and I need to validate them with Symfony validation ex:
private $id;
/**
* #var
* #Assert\Length(max="255")
* #CustomAssert\OrderExternalCode()
* #CustomAssert\OrderShipNoExternalCode()
*/
private $code;
private $someId;
/**
* #var
* #Assert\NotBlank()
* #Assert\Length(max="255")
*/
private $number;
this works well but now I need to add some Assert Constrains dynamically from the controller and that is where I am stuck!
Does anyone knows how to do that or any suggestion that might help?
Currently I did an extra constraint which does extra query in the DB and I don't want to do that and I am not using FormType.
You can use groups and use (or leave out) the extra group you're talking about.
Using the CallbackConstraint should help I think, in your case :
use My\Custom\MyConstraint;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
// This is not tested !
class MyEntity
{
/**
* #Assert\Callback()
*/
public function validateSomeId(ExecutionContextInterface $context)
{
$constraint = new MyConstraint(['code' => $this->code]);
$violations = $context->getValidator()->validate($this->number, $constraint);
foreach ($violations as $violation) {
$context->getViolations()->add($violation);
}
}
}
See https://symfony.com/doc/current/reference/constraints/Callback.html
EDIT : I don't know what you're trying to validate so I just put some random params of your entity in there
So I wanted to dynamically validate the request data based on a condition in the controller.
I specified an extra group for that in the entity like so:
/**
* #var
* #Assert\NotBlank(groups={"extra_check"})
* #Assert\Length(max="255")
*/
private $externalId;
Then in the controller I just did the condition to validate with the extra group or not.
$groups = $order->getExternalCode() != null ? ['Default'] : ['Default', 'extra_check'];
$this->validateRequest($request, null, $groups);
The Default group is the one without group specified and the other one is the group I specified in the field

Symfony2: How to manage lists of things to show in form and use in PHP classes

Having a list of "things" (for example, a list of cities) to display in a select option form field type, how should I manage it?
For example, take the following very short list of cities:
[0] Salerno;
[1] New York;
[2] Paris;
[3] Chicago;
[4] New Delhi;
I would like to do the following things:
Show them in the form select field type;
Store them in the database using their index (0 for Salerno, 1 for New York and so on);
Transform them into strings from the index when I retrieve the information from the database through Doctrine.
One alternative could be the use of a class full of constants, but maybe there is a better solution, so I'm asking here for it! :)
Thank you!
Super easy, use the entity field type. This allows you to use an entity to list the Choices, it even handles mapping the indexes for you.
http://symfony.com/doc/current/reference/forms/types/entity.html
$builder->add('users', 'entity', array(
'class' => 'AcmeHelloBundle:User',
'property' => 'username',
));
The class maps to your entity class. the property tells it which property on the class to show to the user in the select.
A hopefully more direct answer: the most straightforward technique would be to create a City entity. It would look like this:
use Doctrine\ORM\Mapping as ORM;
/**
* City
*
* #ORM\Table(name="city")
* #ORM\Entity
*/
class City
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="city", type="string", length=15, nullable=false)
*/
protected $city;
/**
* Set city
*
* #param string $city
* #return City
*/
public function setCity($city)
{
$this->city = $city;
return $this;
}
/**
* Get city
*
* #return string
*/
public function getCity()
{
return $this->city;
}
}
In a form class you could add City as an entity type (see Chausser's answer for documentation reference).
The city's name could be returned in a controller with something like
$em = $this->getDoctrine()->getManager();
$city = $em->getRepository("YourBundle:City")->find($id);
$cityName = $city->getCity();

Using DQL functions inside Doctrine 2 ORDER BY

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');

Categories