In a Doctrine Criteria instance, I have some static parameters, that I wrote directly in the query as below :
/**
* #return \Doctrine\Common\Collections\Criteria
*/
public function lastContratCriteria()
{
$criteria = Criteria::create();
$baseCondition = $criteria->expr()->andX(
$criteria->expr()->eq('BaseType', 1), // STATIC VALUE
$criteria->expr()->orX(
$criteria->expr()->gt('dateFinContractuellePrevue','CURRENT_DATE()'), // STATIC VALUE
$criteria->expr()->isNull('dateFinContractuellePrevue')
)
);
$criteria
->andWhere($baseCondition)
->orderBy(['dateDebut' => 'DESC'])
->setMaxResults(1);
return $criteria;
}
But this throws:
QueryException : Too few parameters: the query defines 2 parameters but you bound 0.
This is the DQL output, where you can see bound params based on property name (which I did not defined) :
WHERE contrat.BaseType = :BaseType AND (contrat.dateFinContractuellePrevue > :dateFinContractuellePrevue OR contrat.dateFinContractuellePrevue IS NULL)
I could not find in the docs why my static params were not used. To debug , I changed the static values to bound parameters :
$baseCondition = $criteria->expr()->andX(
$criteria->expr()->eq('BaseType', ':bType'),
$criteria->expr()->orX(
$criteria->expr()->gt('dateFinContractuellePrevue',':dateFinCP'),
$criteria->expr()->isNull('dateFinContractuellePrevue')
)
);
But No matter how I named the parameters, to make it work I had to call each one in setParameter() with its correspondent property name instead of the custom name I choose :
->setParameter('BaseType', 1) // Should be bType as above, not BaseType ?
->setParameter('dateFinContractuellePrevue', 'CURRENT_DATE()') // Should be :dateFinCP ?
Here's the complete query :
/**
* #return \Doctrine\Common\Collections\Criteria
*/
public function lastContratCriteria()
{
$criteria = Criteria::create();
$baseCondition = $criteria->expr()->andX(
$criteria->expr()->eq('BaseType', ':bType'),
$criteria->expr()->orX(
$criteria->expr()->gt('dateFinContractuellePrevue',':dateFinCP'),
$criteria->expr()->isNull('dateFinContractuellePrevue')
)
);
}
/**
* #return \Doctrine\ORM\QueryBuilder
* #throws \Doctrine\ORM\Query\QueryException
*/
public function getCurrentContratQB()
{
return $this->createQueryBuilder('contrat')
->addCriteria($this->lastContratCriteria());
}
/**
* #return \Doctrine\Common\Collections\Collection
* #throws \Doctrine\ORM\Query\QueryException
*/
public function getSeminaire()
{
$qb = $this->createQueryBuilder('salarie')
->andWhere('salarie.archive = FALSE')
->leftJoin('salarie.contrats', 'c')
->addSelect('c')
->andWhere("c.id IN (". $this->contratRepository->getCurrentContratQB() .")")
// ONLY ORIGINAL FIELD NAME IS RECOGNIZED HERE, not the bound parameter name
->setParameter('BaseType', 1) // Should be bType, not BaseType
->setParameter('dateFinContractuellePrevue', 'CURRENT_DATE()') // Should be :dateFinCP
//dump($qb->getDQL());
return $qb->getQuery()
->getResult();
}
Why can't I pass static values in $criteria->expr()->eq() ? Why is Doctrine demanding me to set parameters with the exact name of the queried property instead of taking care of the parameters I named ?
You need the DQL of the subquery to add it inside another querybuilder and expose the original parameter names.
Try replacing
->andWhere("c.id IN (". $this->contratRepository->getCurrentContratQB() .")")
with
->andWhere("c.id IN (". $this->contratRepository->getCurrentContratQB()->getDQL() .")")
or
; $qb->andWhere($qb->expr()->in("c.id", $this->contratRepository->getCurrentContratQB()->getDQL())
Related
Using:
PostgreSQL 11 with uuid_generate_v4 type
Symfony 4.4.11
Api Platform 2.5.6
I have an Entity with the following Id :
/**
* #ORM\Entity(repositoryClass="App\Repository\ContractRepository")
* #ORM\HasLifecycleCallbacks
*/
class Contract
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="UUID")
* #ORM\Column(name="id", type="guid", unique=true)
*/
private $id;
[...]
I generate the following route with Api Platform :
App\Entity\Contract:
itemOperations:
get:
So I get a generated route like /contracts/{id}
Currently, if I do /contracts/TEST, it will try to do the SQL request with "TEST" in a where clause and so will fail as a 500.
I would like to prevent this behaviour by asserting that the {id} parameter is a UUID_v4 and return a 400 if not.
This behaviour is DBMS specific, so you have to add your own logic.
The API-Platform component which retrieve an entity given an ID is the ItemDataProviderInterface.
First, I will declare a new exception MalformedUuidException.
Next, I will convert this exception to a 400 error.
Finally, I will create a new ItemDataProviderInterface implementation, wrapping the ORM one and adding some checks to the ID:
class ContractDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface
{
/** #var ItemDataProviderInterface */
private $realDataProvider;
public function __construct(ItemDataProviderInterface $realDataProvider)
{
$this->realDataProvider = $realDataProvider;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
$uuidPattern = '/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i';
if (preg_match($uuidPattern, $id) === 1) {
return $this->realDataProvider->getItem($resourceClass, ['id' => $id], $operationName, $context);
} else {
throw new MalformedUuidException("the given ID \"$id\" is not a valid UUID.");
}
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return $resourceClass === Contract::class;
}
}
# config/services.yaml
App\DataProvider\ContractDataProvider:
arguments:
$realDataProvider: '#api_platform.doctrine.orm.default.item_data_provider'
However, note that the getItem() method's contract does not specify the MalformedUuidException exception, so this implementation breaks the Liskov substitution principle.
Consider returning null instead and be satisfied with a 404 error.
I am making a query with Doctrine which calculates a custom field using a CASE WHEN like this:
public function findLatestPaginator($page = 1, $itemsPerPage)
{
$qb = $this->createQueryBuilder('n');
$qb = $qb
->select(['n AS news', 'CASE WHEN lu.id IS NOT NULL THEN 1 ELSE 0 END AS n.liked'])
->leftJoin('n.likingUsers', 'lu')
;
$qb = $qb
->orderBy('n.date', 'DESC')
->setFirstResult($itemsPerPage * ($page - 1))
->setMaxResults($itemsPerPage)
->getQuery()
;
return $qb->getResult();
}
In my entity I have a field called $liked which is not mapped. Is it possible to make the query (or the hydrator?) automatically set the field on the resulting entity?
Right now I am making a foreach loop and manually setting the property:
/**
* #return News[]
*/
private function convertNews(array $records)
{
$newsList = [];
foreach ($records as $record) {
if (isset($record['liked'], $record['news'])) {
/** #var News */
$news = $record['news'];
$news->liked = boolval($record['liked']);
$newsList[] = $news;
}
}
return $newsList;
}
Maybe a DTO could be useful here : doctrine documentation
Basically, you define a PHP class which is not mapped to your model.
You can then select what you need and trigger the instantiation of an object of this PHP class from the DQL query.
Hope this helps !
Default hydrator does not include unmapped properties (wow, right?).
You want to override/extend doctrine/orm/lib/Docrtrine/ORM/Internal/Hydration/AbstractHydrator
line 386 where it only checks for $classMetadata fieldMappings
Or you can hack around by using DTO and query in transformer, if performance is not needed
I am pretty new to Symfony and hope someone can help me. I have an entity called Material and an associated entity called MaterialKeyword, which are basically tags. I am displaying the keywords comma delimited as a string in a text field on a form. I created a data transformer to do that. Pulling the keywords from the database and displaying them is no problem, but I have a problem with the reversTransform function when I want to submit existing or new keywords to the database.
Material class (MaterialKeyword):
/**
* #Assert\Type(type="AppBundle\Entity\MaterialKeyword")
* #Assert\Valid()
* #ORM\ManyToMany(targetEntity="MaterialKeyword", inversedBy="material")
* #ORM\JoinTable(name="materials_keyword_map",
* joinColumns={#ORM\JoinColumn(name="materialID", referencedColumnName="materialID", nullable=false)},
* inverseJoinColumns={#ORM\JoinColumn(name="keywordID", referencedColumnName="id", nullable=false)})
*/
public $materialkeyword;
/**
* Constructor
*/
public function __construct()
{
$this->MaterialKeyword = new ArrayCollection();
}
/**
* Set materialkeyword
*
* #param array $materialkeyword
*
*/
public function setMaterialkeyword(MaterialKeyword $materialkeyword=null)
{
$this->materialkeyword = $materialkeyword;
}
/**
* Get materialkeyword
*
* #Assert\Type("\array")
* #return array
*/
public function getMaterialkeyword()
{
return $this->materialkeyword;
}
Here is my code from the data transformer:
This part is working:
class MaterialKeywordTransformer implements DataTransformerInterface
{
/**
* #var EntityManagerInterface
*/
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms an object (materialkeyword) to a string.
*
* #param MaterialKeyword|null $materialkeyword
* #return string
*/
public function transform($material)
{
$result = array();
if (null === $material) {
return '';
}
foreach ($material as $materialkeyword) {
$result[] = $materialkeyword->getKeyword();
}
return implode(", ", $result);
}
This part is not working:
/**
* Transforms a string (keyword) to an object (materialkeyword).
*
* #param string $materialkeyword
* #return MaterialKeyword|null
* #throws TransformationFailedException if object (materialkeyword) is not found.
*/
public function reverseTransform($keywords)
{
// no keyword? It's optional, so that's ok
if (!$keywords) {
return;
}
$repository = $this->manager
->getRepository('AppBundle:MaterialKeyword');
$keyword_array = explode(", ", $keywords);
foreach($keyword_array as $keyword){
$materialkeyword = new MaterialKeyword();
$keyword_entry = $repository->findBy(array('keyword' => $keyword));
if(array_key_exists(0, $keyword_entry)){
$keyword_entry_first = $keyword_entry[0];
}else{
$keyword_entry_first = $keyword_entry;
}
if (null === $keyword_entry_first) {
throw new TransformationFailedException(sprintf('There is no "%s" exists',
$keywords
));
}
$materialkeyword->setKeyword($keyword_entry_first);
}
return $materialkeyword;
}
There will be several keywords, so how do I store them. I tried Arrays and ArrayCollections (new ArrayCollection()) without any success.
The error that I am getting currently with the code above:
Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /.../vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 605 and defined
TL;DR;
Your reverseTransform function should return an array containing zero or n MaterialKeyword.
It should not return a single MaterialKeyword object because the reverse transformation of MaterialKeyord[] --> string is not string --> MaterialKeyword, it is string --> MaterialKeyword[].
Thinking about this, the doctrine ArrayCollection exception you have make sense as it is trying to do new ArrayCollection(/** Single MaterialKeyword object */) instead of new ArrayCollection(/** Array of MaterialKeyword objects */).
From what you're telling I assume that Material and MaterialKeyword are connected by a ManyToMany association, in which case each Material has an array of MaterialKeyword objects associated to it.
Which means, that your Data Transformer should work with arrays as well, but you're only working with single objects.
Specifically, reverseTransform should return an array of MaterialKeyword objects, whereas you're only returning one (the last one handled in the loop.)
Another issue is that your method created new objects every time, even though $repository->findBy(...) would already return a MaterialKeyword instance. Creating a new object would cause that entry to be copied instead of simply used.
So the correct method might look like this:
public function reverseTransform($keywords)
{
// no keyword? It's optional, so that's ok
if (!$keywords) {
return array();
}
$repository = $this->manager
->getRepository('AppBundle:MaterialKeyword');
$keyword_array = explode(", ", $keywords);
$result_list = array(); // This will contain the MaterialKeyword objects
foreach($keyword_array as $keyword){
$keyword_entry = $repository->findOneBy(array('keyword' => $keyword));
if (null === $keyword_entry) {
throw new TransformationFailedException(sprintf('There is no "%s" exists',
$keyword
));
}
$result_list[] = $keyword_entry;
}
return $result_list;
}
#Hanzi put me on the correct track. It has to be an array of MaterialKeywords objects.
Here is my final working code in class MaterialKeywordTransformer:
/**
* Transforms a string (keyword) to an object (materialkeyword).
*
* #param string $materialkeyword
* #return MaterialKeyword|null
* #throws TransformationFailedException if object (materialkeyword) is not found.
*/
public function reverseTransform($keywords)
{
// keyword are optional, so that's ok
if (!$keywords) {
return;
}
$repository = $this->manager
->getRepository('AppBundle:MaterialKeyword');
$repository_m = $this->manager
->getRepository('AppBundle:Material');
$keyword_array = explode(", ", $keywords);
foreach($keyword_array as $keyword){
$materialkeyword = new MaterialKeyword();
$materialkeyword->setKeyword($keyword);
if($this->opt["data"]->getMaterialID() !== null) {
$materialkeyword->setMaterialID($this->opt["data"]->getMaterialID());
} else {
$material = $repository_m->findOne();
$materialID = $material[0]->getMaterialID();
$materialkeyword->setMaterialID($materialID);
}
$materialkeywords[] = $materialkeyword;
if (null === $keywords) {
throw new TransformationFailedException(sprintf('There is no "%s" exists',
$keywords
));
}
}
return $materialkeywords;
}
I have an entity NewsVersion with ManyToMany :
class NewsVersion
{
...
/**
* #var NewsCategory[]|ArrayCollection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\NewsCategory",
* cascade={"persist"}
* )
* #ORM\JoinTable(name="news_version_categories")
*/
private $categories;
...
In my repository, when I call this :
$qb = $this->createQueryBuilder('nv')
->select('nv')
->innerJoin('nv.categories', 'nvc')
->addSelect('nvc');
return $qb->getQuery()->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
I have :
But When I call this :
$qb = $this->createQueryBuilder('nv')
->select('nv')
->innerJoin('nv.categories', 'nvc')
->addSelect('nvc.id');
I have :
Why nvc.id don't return id in categories array ? I want return only id from my category, but in categories array in NewsVersion entity (same as first screen)
You should remove the ->addSelect('nvc.id')
and add the id of category in the select statement
$qb = $this->createQueryBuilder('nv')
->select('nv', 'nvc.id')
->innerJoin('nv.categories', 'nvc');
You're making two separate queries for data; $queryBuilder->addSelect() creates a new one alongside the first.
It might make sense in plain SQL.
/* ->select('nv') */
SELECT *
FROM `nv`
/* all of nv's fields are returned in this set */
/* ->addSelect('nvc.id') */
SELECT `id`
FROM `nvc`
/* only nvc.id is returned in this set */
Doctrine wraps all of the queries' resultsets in an array, for your convenience, but doesn't combine them because they did not come from the same query.
Relevant source:
// class QueryBuilder
public function addSelect($select = null)
{
// ...
return $this->add('select', new Expr\Select($selects), true);
}
Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values