Symfony2 OneToMany does not use custom repository class - symfony

I have to tables
preset_item
id
and
preset_item_element
preset_item_id -> reference to prese_item
element_type
element_id
in PresetItemElement entity there is:
/**
* #var \GGG\ManagerBundle\Entity\PresetItem
*
* #ORM\ManyToOne(targetEntity="GGG\ManagerBundle\Entity\PresetItem", inversedBy="elements")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="preset_item_id", referencedColumnName="id")
* })
*/
private $presetItem;
and in PresetItem
/**
* #ORM\OneToMany(targetEntity="PresetItemElement", mappedBy="presetItem")
*/
private $elements;
public function getElements()
{
return $this->elements;
}
And preset Item have custom Repository class:
/**
* PresetItem
*
* #ORM\Table(name="preset_item")
* #ORM\Entity(repositoryClass="GGG\ManagerBundle\Entity\PresetItemRepository")
*/
class PresetItem
it's looks like that:
<?php
namespace GGG\ManagerBundle\Entity;
use Doctrine\ORM\EntityRepository;
use Doctrine\DBAL\LockMode;
class PresetItemElementRepository extends EntityRepository
{
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) {
return $this->decorate(parent::findBy($criteria, $orderBy, $limit, $offset));
}
public function find($id, $lockMode = LockMode::NONE, $lockVersion = null) {
return $this->decorateElement(parent::find($id, $lockMode, $lockVersion));
}
public function findOneBy(array $criteria, array $orderBy = null) {
return $this->decorateElement(parent::findOneBy($criteria, $orderBy));
}
private function decorateElement($element) {
$object = $this->getEntityManager()
->getRepository(
'GGGManagerBundle:'.$element
->getPresetItemElementType()
->getRepresentationObject()
)->findOneBy(array('id' => $element->getElementId()));
$element->setObject($object);
}
private function decorate($elements) {
foreach($elements as $element) {
$this->decorateElement($element);
}
return $elements;
}
}
So i'm decorating each PresetItemElement with some additional data
and it's work when i;m getting single PresetItemElement object but when i'm getting PresetItem and try iterate getElements()
$entity = $em->getRepository('GGGManagerBundle:PresetItem')->find($id);
foreach($entity->getElements() as $a) {
var_dump($a->getObject());
}
i have null here, it looks like custom PresetItemElementRepository was not executed
What i do wrong?

Doctrine doesn't use repositories internally when loading related entities. Also, when using for example a query or the query builder to retrieve entities, the repository wouldn't be used either.
To do something every time an entity is loaded from the database, you should register an event handler for the postLoad event. See http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html for how to write and register event listeners, and http://docs.doctrine-project.org/en/latest/reference/events.html#lifecycle-events for the available lifecycle events.

Related

symfony many normalizers for one entity

I have several user roles with access to orders and controllers for each of them. Are there ways to change the normalizer for one entity, for example...
in this action i need to get the normalizer for the courier:
## CourierController
/**
* #Rest\Get()
*/
public function orders()
{
$serializer = $this->get('serializer');
$orders = $this->getDoctrine()
->getRepository(Order::class)
->findBy(['courier' => $this->getUser()->getCourierAccount()]);
$data = $serializer->normalize($orders); // <--------- 1) how to choose the right normalizer?
return $this->json($data);
}
But in this i need for something like 'ClientOrderNormalizer'
## ClientController
/**
* #Rest\Get()
*/
public function orders()
{
$serializer = $this->get('serializer');
$orders = $this->getDoctrine()
->getRepository(Order::class)
->findBy(['client' => $this->getUser()->getClientAccount()]);
$data = $serializer->normalize($orders); // <--------- 2) how to choose the right normalizer?
return $this->json($data);
}
1) Using Serialization Groups Annotations -> look here
2) Create custom DTO then serialize this object for a response.
Example of how to create custom DTO.
final class SignInResponse implements ResponseInterface
{
/**
* #var string
*
* #SWG\Property(type="string", description="Token.")
*/
private string $token;
public function __construct(UserSession $userSession)
{
$this->token = $userSession->getToken();
}
/**
* Get Token
*
* #return string
*/
public function getToken(): string
{
return $this->token;
}
}

How can i limit the number of nested entities in API Platform?

Having two related entities, let's say Author and Book, I can limit (or paginate) the results of Authors but not the number of results of its related entity Books which always shows the whole collection.
The issue is that Authors may have hundreds of Books making the resulting JSON huge and heavy to parse so I'm trying to get, for example, only the last 5 books.
I'm sure I'm missing something since I think this is probably a common scenario but I can't find anything on the docs nor here in StackOverflow.
I'm starting with Api Platform, any hint would be appreciated!
I finally solved it creating a normalizer for the entity but I still think that it has to be a simpler solution.
Here's what I had to do, following the Authors / Books example:
Add a setter to the Author entity to override the Author's Book collection:
// src/Entity/Author.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
// ...
/**
* #ApiResource
* #ORM\Entity(repositoryClass="App\Repository\AuthorRepository")
*/
class Author
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Book", mappedBy="author", orphanRemoval=true)
*/
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// Getters and setters
//...
public function setBooks($books): self
{
$this->books = $books;
return $this;
}
}
Create a normalizer for the Author's entity:
// App/Serializer/Normalizer/AuthorNormalizer.php
<?php
namespace App\Serializer\Normalizer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
class AuthorNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
private $normalizer;
public function __construct(
NormalizerInterface $normalizer,
IriConverterInterface $iriConverter
) {
if (!$normalizer instanceof DenormalizerInterface) {
throw new \InvalidArgumentException('The normalizer must implement the DenormalizerInterface');
}
if (!$normalizer instanceof AbstractItemNormalizer) {
throw new \InvalidArgumentException('The normalizer must be an instance of AbstractItemNormalizer');
}
$handler = function ($entity) use ($iriConverter) {
return $iriConverter->getIriFromItem($entity);
};
$normalizer->setMaxDepthHandler($handler);
$normalizer->setCircularReferenceHandler($handler);
$this->normalizer = $normalizer;
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->normalizer->denormalize($data, $class, $format, $context);
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->normalizer->supportsDenormalization($data, $type, $format);
}
public function normalize($object, $format = null, array $context = [])
{
// Number of desired Books to list
$limit = 2;
$newBooksCollection = new ArrayCollection();
$books = $object->getBooks();
$booksCount = count($books);
if ($booksCount > $limit) {
// Reverse iterate the original Book collection as I just want the last ones
for ($i = $booksCount; $i > $booksCount - $limit; $i--) {
$newBooksCollection->add($books->get($i - 1));
}
}
// Setter previously added to the Author entity to override its related Books
$object->setBooks($newBooksCollection);
$data = $this->normalizer->normalize($object, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof \App\Entity\Author;
}
}
And finally register the normalizer as a service manually (using autowire led me to circular reference issues):
services:
App\Serializer\Normalizer\AuthorNormalizer:
autowire: false
autoconfigure: true
arguments:
$normalizer: '#api_platform.jsonld.normalizer.item'
$iriConverter: '#ApiPlatform\Core\Api\IriConverterInterface'

Drupal 8.x + GraphQL Module Custom Type/Field Plugin Receiving Error "fields must be an object with field names as keys"

I am attempting to create a module which defines it's own custom Type and associated Field plugins.
When installed, GraphQLi reports the following error in the console:
Uncaught Error: CustomTypeInterface fields must be an object with field names as keys or a function which returns such an object.
Drupal 8.61. I have tried on both GraphQL 3.0-RC2 and 3.x-Dev. Any help would be much appreciated. Thanks.
My code is as follows:
/graphql_custom.info.yml
name: GraphQL Custom Type Example
type: module
description: ''
package: GraphQL
core: 8.x
dependencies:
- graphql_core
/src/CustomObject.php
namespace Drupal\graphql_custom;
class CustomObject {
protected $data;
function __construct(String $data) {
$this->data = $data;
}
function getData() {
return $this->data;
}
}
/src/Plugin/GraphQL/Fields/CustomField.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Fields;
use Drupal\graphql_custom\CustomObject;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use GraphQL\Type\Definition\ResolveInfo;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Created Custom Object with argument as data.
*
* #GraphQLField(
* id = "custom_field",
* secure = true,
* name = "customfield",
* type = "CustomType",
* nullable = true,
* arguments = {
* "argument" = "String!"
* }
* )
*/
class CustomField extends FieldPluginBase implements ContainerFactoryPluginInterface {
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {#inheritdoc}
*/
protected function isLanguageAwareField() {
return FALSE;
}
/**
* {#inheritdoc}
*/
public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
return parent::resolve($value, $args, $context, $info);
}
/**
* {#inheritdoc}
*/
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
$arg = $args['argument'];
$object = new CustomObject($arg);
yield $object;
}
}
/src/Plugin/GraphQL/Fields/CustomFieldData.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Fields;
use Drupal\graphql_custom\CustomObject;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Custom Type Data Field
*
* #GraphQLField(
* id = "custom_field_data",
* secure = true,
* name = "data",
* type = "String",
* parents = {"CustomType"}
* )
*/
class CustomFieldData extends FieldPluginBase {
/**
* {#inheritdoc}
*/
protected function resolveValues($value, array $args, $context, $info) {
if ($value instanceOf CustomObject) {
yield (string) $value->getData();
} else {
yield (string) "Empty";
}
}
}
/src/Plugin/GraphQL/Interfaces/CustomTypeInterface.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Interfaces;
use Drupal\graphql_custom\CustomObject;
use Drupal\graphql\Annotation\GraphQLInterface;
use Drupal\graphql\Plugin\GraphQL\Interfaces\InterfacePluginBase;
/**
* Interface for Custom Type.
*
* For simplicity reasons, this example does not utilize dependency injection.
*
* #GraphQLInterface(
* id = "custom_type_interface",
* name = "CustomTypeInterface"
* )
*/
class CustomTypeInterface extends InterfacePluginBase {
/**
* {#inheritdoc}
*/
public function resolveType($object) {
if ($object instanceof CustomObject) {
$schemaManager = \Drupal::service('graphql_core.schema_manager');
return $schemaManager->findByName('CustomType', [
GRAPHQL_CORE_TYPE_PLUGIN,
]);
}
}
}
/src/Plugin/GraphQL/Types/CustomType.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Types;
use Drupal\graphql_custom\CustomObject;
use Drupal\graphql\Plugin\GraphQL\Types\TypePluginBase;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use GraphQL\Type\Definition\ResolveInfo;
/**
* GraphQL Custom Type.
*
* #GraphQLType(
* id = "custom_type",
* name = "CustomType",
* interfaces = {"CustomTypeInterface"}
* )
*/
class CustomType extends TypePluginBase {
/**
* {#inheritdoc}
*/
public function applies($object, ResolveContext $context, ResolveInfo $info) {
return $object instanceof CustomObject;
}
}
With your fields you should use interface reference in parents instead of type:
parents = {"CustomTypeInterface"}
Another way is to remove interface and use direct type reference as mentioned in your example.

Symfony Doctrine Hydrator - With custom hydrator I lose my ManyToOne relation

I would like to extends ObjectHydrator to benefit of the hydration of my ManyToOne relation and add extra field to the Entity.
Here is my hydrator: StatisticsDataHydrator.php
namespace AppBundle\Hydrator\ProjectAssignment;
use AppBundle\Entity\ProjectAssignment;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
class StatisticsDataHydrator extends ObjectHydrator
{
/**
* {#inheritdoc}
*/
protected function hydrateRowData(array $data, array &$result)
{
$hydrated_result = array();
parent::hydrateRowData($data, $hydrated_result);
/** #var ProjectAssignment $project_assignment */
$project_assignment = $hydrated_result[0][0];
$result[] = $project_assignment;
}
}
Here is my config: config.yml
doctrine:
orm:
hydrators:
project_assignment_statisticsdata_hydrator: AppBundle\Hydrator\ProjectAssignment\StatisticsDataHydrator
Where I don't use the hydrator I have no problem:
/**
* #param ProjectStage $stage
* #return array
*/
public function findByStageWithStatisticsData(ProjectStage $stage){
$qb = $this->createQueryBuilder('pa');
$qb
//->addSelect('44')
->where($qb->expr()->eq('pa.project_stage', ':stage'))
->setParameter('stage', $stage);
return $qb->getQuery()->getResult();
}
But when I use my hydrator:
/**
* #param ProjectStage $stage
* #return array
*/
public function findByStageWithStatisticsData(ProjectStage $stage){
$qb = $this->createQueryBuilder('pa');
$qb
->addSelect('1234') // referencial value
->where($qb->expr()->eq('pa.project_stage', ':stage'))
->setParameter('stage', $stage);
return $qb->getQuery()->getResult('project_assignment_statisticsdata_hydrator');
}
The strangest behavior is that the same occure with this config: config.yml
doctrine:
orm:
hydrators:
project_assignment_statisticsdata_hydrator: Doctrine\ORM\Internal\Hydration\ObjectHydrator
I have tried all kind of fetch on relation with no success:
#ORM\ManyToOne(... , fetch="EAGER")
#ORM\ManyToOne(... , fetch="LAZY")
...
Maybe I have to use a Proxy on my Entity, I really don't know :(
Thank you for any help!
Great! I found the problem, it was with my query builder. I had to manually add the joins and the select of related objects.
/**
* #param ProjectStage $stage
* #return array
*/
public function findByStageWithStatisticsData(ProjectStage $stage){
$qb = $this->createQueryBuilder('pa');
$qb
->addSelect('e') // added
->addSelect('r') // added
->addSelect('1234')
->leftJoin('pa.employee', 'e') // added
->leftJoin('pa.role', 'r') // added
->where($qb->expr()->eq('pa.project_stage', ':stage'))
->setParameter('stage', $stage);
return $qb->getQuery()->getResult('project_assignment_statisticsdata_hydrator');
}
Bonus, here is my Hydrator (it can help someone):
namespace AppBundle\Hydrator\ProjectAssignment;
use AppBundle\Entity\Hydrator\ProjectAssignment\StatisticsData;
use AppBundle\Entity\ProjectAssignment;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
class StatisticsDataHydrator extends ObjectHydrator
{
/**
* {#inheritdoc}
*/
protected function hydrateRowData(array $data, array &$result)
{
$hydrated_result = array();
parent::hydrateRowData($data, $hydrated_result);
/** #var ProjectAssignment $project_assignment */
$project_assignment = $hydrated_result[0][0];
$keys = array_keys($hydrated_result); $key = end($keys);
$statistics_data = new StatisticsData($project_assignment);
$statistics_data->setTotalWorkedTime((int)$hydrated_result[$key][1]);
$project_assignment->setStatisticsData($statistics_data);
$result[] = $project_assignment;
}
}
In my Entity I have the folowing attribute/getter/setter
/********** NON SYNCED FIELDS **********/
/** #var StatisticsData $statistics_data */
private $statistics_data;
/**
* #return StatisticsData
*/
public function getStatisticsData()
{
return $this->statistics_data;
}
/**
* #param StatisticsData $statistics_data
*/
public function setStatisticsData($statistics_data)
{
$this->statistics_data = $statistics_data;
}
/***************************************/
The problem is that the Doctrine SqlWalker will not load meta columns, which include subclasses and associations, if the query Hydration Mode is not HYDRATE_OBJECT or if the query hint HINT_INCLUDE_META_COLUMNS is not set to true:
$addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
$this->query->getHydrationMode() == Query::HYDRATE_OBJECT
||
$this->query->getHydrationMode() != Query::HYDRATE_OBJECT &&
$this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
The problem has already been reported in this issue.
As mentioned by the author of the issue, you can either implement the suggested fix, or set the query hint HINT_INCLUDE_META_COLUMNS to true:
$query = $queryBuilder->getQuery();
$query->setHint(Query::HINT_INCLUDE_META_COLUMNS, true);
$result = $query->getResult('CustomHydrator');

Add dynamic property on entity to be serialized

I have this REST API. Whenever request comes to get a resource by id ( /resource/{id}) I want to add a permissions array on that object on the fly (entity itself does not have that field).
What I came up with is this event listener. It checks the result the controller has returned:
class PermissionFinderListener {
...
public function onKernelView(GetResponseForControllerResultEvent $event) {
$object = $event->getControllerResult();
if (!is_object($object) || !$this->isSupportedClass($object)) {
return;
}
$permissions = $this->permissionFinder->getPermissions($object);
$object->permissions = $permissions;
$event->setControllerResult($object);
}
....
}
The problem is that the JMS Serializer opts out this dynamic property on serialization. I tried making the onPostSerialize event subscriber on JMS serializer, but then there are no clear way to check if this is a GET ONE or GET COLLECTION request. I don't need this behaviour on GET COLLECTION and also it results a huge performance hit on collection serialization. Also I don't want to create any base entity class with permission property.
Maybe there is some other way to deal with this scenario?
What I could imagine is a combination of Virtual Property and Serialization Group:
Add a property to your entity like:
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"}) */
*
* #return string
*/
public function getPermissions()
{
return $permissionFinder->getPermissions($this);
}
Only thing you need to do then is to serialize 'includePermissions' group only in your special case (see http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies)
If you don't have access to $permissionFinder from your entity you could as well set the permission attribute of an entity from a Controller/Service before serializing it.
EDIT:
This is a bit more code to demonstrate what I mean by wrapping your entity and using VirtualProperty together with SerializationGroups. This code is not tested at all - it's basically a manually copied and stripped version of what we're using. So please use it just as an idea!
1) Create something like a wrapping class for your entity:
<?php
namespace Acquaim\ArcticBundle\Api;
use JMS\Serializer\Annotation as JMS;
/**
* Class MyEntityApi
*
* #package My\Package\Api
*/
class MyEntityApi
{
/**
* The entity which is wrapped
*
* #var MyEntity
* #JMS\Include()
*/
protected $entity;
protected $permissions;
/**
* #param MyEntity $entity
* #param Permission[] $permissions
*/
public function __construct(
MyEntity $entity,
$permissions = null)
{
$this->entity = $entity;
$this->permissions = $permissions;
}
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"})
*
* #return string
*/
public function getPermissions()
{
if ($this->permissions !== null && count($this->permissions) > 0) {
return $this->permissions;
} else {
return null;
}
}
/**
* #return object
*/
public function getEntity()
{
return $this->entity;
}
}
2) In your controller don't return your original Entity, but get your permissions and create your wrapped class with entity and permissions.
Set your Serialization Context to include permissions and let the ViewHandler return your serialized object.
If you don't set Serialization Context to includePermissions it will be excluded from the serialized result.
YourController:
$myEntity = new Entity();
$permissions = $this->get('permission_service')->getPermissions();
$context = SerializationContext::create()->setGroups(array('includePermissions'));
$myEntityApi = new MyEntityApi($myEntity,$permissions);
$view = $this->view($myEntityApi, 200);
$view->setSerializationContext($context);
return $this->handleView($view);

Resources