Save entity with subresource into redis with Symfony & Api Platform - symfony

I have 2 entities, Event & User. So basically A user can have multiple event.
I have an EventItemDataProvider and in the getItem method I did something like that:
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?Event
{
if ($event = $this->cache->get($id, Event::class)) {
return $event;
}
$event = $this->em->getRepository(Event::class)->find($id);
if($event) {
$this->cache->set($id, $event, 120);
}
return $event;
}
When the data is returned without the cache I have this result that is correct with the right username:
{
"#context": "/contexts/Event",
"#id": "/events/20",
"#type": "Event",
"title": "new global event 3",
"description": "A big global event 3",
"createdAt": "2019-07-05T09:20:48+00:00",
"owner": {
"#id": "/users/3",
"#type": "User",
"username": "test3"
}
}
But if I hit the same method a second time, it will retrieve the data from the cache (redis) but this time username will be empty like that:
{
"#context": "/contexts/Event",
"#id": "/events/20",
"#type": "Event",
"title": "new global event 3",
"description": "A big global event 3",
"createdAt": "2019-07-05T09:20:48+00:00",
"owner": {
"#id": "/users/3",
"#type": "User",
"username": ""
}
}
Why is my username empty when I retrieve it from redis?
Basically my cache class has these methods:
public function get(string $uniq, string $className)
{
$value = $this->cache->getItem('test_key')->get();
return $value;
}
public function set(string $uniq, object $entity, int $expires = 60)
{
$save = $this->cache->getItem('test_key');
$save->set($entity);
$save->expiresAfter($expires);
$saved = $this->cache->save($save);
return $saved;
}
The owner field in my Event entity:
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="events")
* #ORM\JoinColumn(nullable=false)
* #Groups({"event:read", "event:write", "event:update"})
* #Assert\Valid()
*/
private $owner;
The username field in my User entity:
/**
* #ORM\Column(type="string", length=25, unique=true)
* #Groups({"user:read", "user:write", "user:update", "event:item:get", "event:update"})
* #Assert\NotBlank(message="Please provide a username")
*/
private $username;
My redis cache configuration:
cache:
app: cache.adapter.redis
default_redis_provider: "redis://redis"
pools:
custom.cache.entity:
adapter: cache.app
default_lifetime: 120
Why my username is filled when I retrieve it from Doctrine Repository but empty when I retrieve it from Redis?
With Xdebug I can see that if I retrieve the data from Doctrine I have something like that:
But if I retrieve the data from the cache I have something like that:
In this second case, initializer -> this seems infinite, so I think that the problem is probably here, but what should I do to solve this issue?

Related

API Platform: How to normalize a collection of embedded entities in GraphQL?

I'm trying to make a collection of subresources selectable in GraphQL (with pagination). I'd like to be able to query:
query {
getA(id: '/api/A/1') {
aId
subresources {
totalCount
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
edges {
node {
bId
}
}
}
}
}
and get the result:
{
aId: 1,
subresources: {
"totalCount": XX,
"pageInfo": {
"endCursor": "MQ==",
"startCursor": "MA==",
"hasNextPage": true,
"hasPreviousPage": false
},
edges: [
{
node: {
bId: 11
}
},
{
node: {
bId: 12
}
},
{
node: {
bId: 13
}
}
]
}
}
I'm not using Doctrine at all- I'm using custom data providers. The problem I'm encountering is that even when I return an A entity from DataProvider::getItem() that has an array of B subresources, I get an empty array for subresources in GraphQL. I get the correct data in REST though.
I'm following the instructions given in SymfonyCasts and I found a related API Platform issue, but I'm still having no luck.
I traced through API Platform core and I think it has to do with how the entity is normalized in GraphQL. Specifically, an empty array is returned in ItemNormalizer::normalizeCollectionOfRelations(). However, there's a comment saying "to-many are handled directly by the GraphQL resolver" but I'm not sure what that refers to.
Here's the entity code.
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
#[ApiResource(
graphql: ['item_query', 'collection_query', 'create', 'update', 'delete'],
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'patch', 'delete'],
normalizationContext: ['groups' => ['read']],
denormalizationContext: ['groups' => ['write']],
)]
class A {
#[ApiProperty(identifier: true)]
#[Groups(['read', 'write'])]
public ?int $aId = null,
/** #var B[] */
#[ApiProperty(readableLink: true, writableLink: true)]
#[Groups(['read', 'write'])]
public $subresources = []
}
And:
#[ApiResource(
graphql: ['item_query', 'collection_query', 'create', 'update', 'delete'],
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'patch', 'delete'],
normalizationContext: ['groups' => ['read']],
denormalizationContext: ['groups' => ['write']],
)]
class B {
#[ApiProperty(identifier: true)]
#[Groups(['read', 'write'])]
public ?int $bId = null,
}
My ADataProvider:
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): A {
$bs = $this->bDataProvider->getCollection(B::class, null, []);
return new A(123, $bs);
}
My BDataProvider:
/**
* #return ArrayPaginator<B>
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): ArrayPaginator {
return ArrayPaginator::fromList([new B(11), new B(12), new B(13)]);
}
ArrayPaginator implements IteratorAggregate and PaginatorInterface.
Specifically I see this error:
{
"errors": [
{
"debugMessage": "Collection returned by the collection data provider must implement ApiPlatform\\Core\\DataProvider\\PaginatorInterface or ApiPlatform\\Core\\DataProvider\\PartialPaginatorInterface.",
"message": "Internal server error",
"extensions": {
"category": "internal"
},
"locations": [
{
"line": 29,
"column": 5
}
],
"path": [
"a",
"b"
],
"trace": [
{
"file": "/homedir/core/src/GraphQl/Resolver/Stage/SerializeStage.php",
"line": 100,
"call": "ApiPlatform\\Core\\GraphQl\\Resolver\\Stage\\SerializeStage::serializeCursorBasedPaginatedCollection(array(0), array(5), array(6))"
},
TLDR: How does one use annotations (or YAML) to make attributes that are collections of subresources selectable in GraphQL?
Any help/ideas are appreciated, thanks for reading!
Found a solution: the ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface needs to be implemented by the BDataProvider.
It gets used in the ReadStage of api platform's graphql resolver. Surprisingly, it's found nowhere in the REST resolver, so this won't get called on a REST request.
The only method that needs to be implemented is getSubresource(). My basic first implementation looks like this:
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) {
if ($context['collection']) {
return $this->getCollection($resourceClass, $operationName, $context);
}
$id = // get your id from $identifiers;
return $this->getItem($resourceClass, $id, $operationName, $context);
}
This isn't found in the docs unfortunately, but there are a few pulls (1, 2) open to add it.

How to modify data before the denormalization and send It back?

I'd like to have advice on the best practice to achieve something.
I've a JSON output of an entity called "Session" with 3 related entities (platform, user, course ) and I'd like to use the nested way to create those 3 related if they don't exist.
But If they do exist I'd like to add their IRI (or ID) to the JSON output before API Platform does the magic. (or another way to achieve this behavior)
I naively thought that I should bind to the pre_denormalization event but I've no idea how to return the data to the event.
Here is what I've got so far.
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['onSessionDenormalize', EventPriorities::PRE_DESERIALIZE],
];
}
public function onSessionDenormalize(RequestEvent $event)
{
$data = $event->getRequest()->getContent();
}
public function modifyPayLoad($data) {
$dataObject = json_decode($data);
$platform = $dataObject->platform;
$user = $dataObject->user;
$course = $dataObject->course;
if($this->platformRepository->findOneBy(['slug' => $platform->slug])) {
$platformID = $this->courseRepository->findOneBy(['slug' => $platform->slug])->getId();
$dataObject->id = $platformID;
if($this->userRepository->findOneBy(['email' => $user->slug])) {
$dataObject->id = $this->userRepository->findOneBy(['email' => $user->slug])->getId();
$dataObject->user->platform->id = $platformID;
}
if($this->courseRepository->findOneBy(['slug' => $course->slug])) {
$dataObject->id = $this->courseRepository->findOneBy(['slug' => $course->slug])->getId();
$dataObject->course->platform->id = $platformID;
}
}
return json_encode($dataObject);
}
And the JSON:
{
"user": {
"firstname": "string",
"lastname": "string",
"address": "string",
"city": "string",
"email": "string",
"zipCode": int,
"hubspotID": int
},
"course": {
"id": "string",
"title": "string",
"platform": {
"slug": "string",
"name": "string"
}
},
"startDate": "2022-01-09T23:59:00.000Z",
"endDate": "2022-02-09T23:59:00.000Z",
"hubspotDealId": int
}
I can't get the ID in this JSON since those information are provided by a Puppeteer APP, or I should do 3 request to check if the related entity exist first, which is not adviced I think.
I also tried to change the identifier on the user, course and platform but In both cases, I've duplicate entries in database
I manage to do what I want with a custom denormalizer.
So I can post and update data comming from tierce source without ID.
class SessionDenormalizer implements DenormalizerAwareInterface, ContextAwareDenormalizerInterface
{
use DenormalizerAwareTrait;
public function __construct(
private UserRepository $userRepository,
private PlatformRepository $platformRepository,
private CourseRepository $courseRepository,
private SessionRepository $sessionRepository,
)
{
}
private const ALREADY_CALLED = 'SESSION_DENORMALIZER_ALREADY_CALLED';
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
{
if (isset($context[self::ALREADY_CALLED])) {
return false;
}
return $type === Session::class;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if (isset(
$data["user"]["email"],
$data["course"]["slug"],
$data["course"]["platform"]["slug"],
)) {
$user = $this->userRepository->findOneBy(["email" => $data["user"]["email"]]);
$course = $this->courseRepository->findOneBy(["slug" => $data["course"]["slug"]]);
$platform = $this->platformRepository->findOneBy(["slug" => $data["course"]["platform"]["slug"]]);
if ($user && $course && $platform) {
$data["user"]["#id"] = "/v1/users/" . $user?->getId();
$data["course"]["#id"] = "/v1/courses/" . $course?->getId();
$data["course"]["platform"]["#id"] = "/v1/platforms/" . $platform?->getId();
$session = $this->sessionRepository->findOneBy(["cpfID" => $data["cpfID"]]);
if($session) {
$data["#id"] = "/v1/sessions/" . $session->getId();
if(isset($context["collection_operation_name"])) {
$context["collection_operation_name"] = "put";
}
if(isset($context['api_allow_update'])) {
$context['api_allow_update'] = true;
}
}
}
}
$context[self::ALREADY_CALLED] = true;
return $this->denormalizer->denormalize($data , $type , $format , $context);
}
}
Services.yaml :
'app.session.denormalizer.json':
class: 'App\Serializer\Denormalizer\SessionDenormalizer'
tags:
- { name: 'serializer.normalizer', priority: 64 }

Enter a name instead of user id

I have 2 tables.
first table name is user.
User_id, User_name
Second table name is Question
Question_id, Question, User_id
I want to see the user name when I call the Question table.
table connection code:
public function user()
{
return $this->belongsTo(User::class);
}
And resource code is:
public function toArray($request)
{
return[
'Question' => $this->Question,
'created_at' => $this->created_at->diffForHumans(),
'user' => $this->user->name
];
}
Question controller show function:
public function show(Question $question)
{
return new QuestionResource($question);
}
The error during operation in the following code:
{
"message": "Class 'App\\Model\\User' not found",
"exception": "Symfony\\Component\\Debug\\Exception\\FatalThrowableError",
"file": "C:\\xampp\\htdocs\\forumapp\\vendor\\laravel\\framework\\src\\Illuminate\\Database\\Eloquent\\Concerns\\HasRelationships.php",
"line": 718,
"trace": [
{
"file": "C:\\xampp\\htdocs\\forumapp\\vendor\\laravel\\framework\\src\\Illuminate\\Database\\Eloquent\\Concerns\\HasRelationships.php",
"line": 179,
"function": "newRelatedInstance",
"class": "Illuminate\\Database\\Eloquent\\Model",
"type": "->"
},
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'user';
}
Create an Elquoent model class because you have used 'return $this->belongsTo(User::class);' User class here to define elquoent relation.
Hope it will work for you.

Symfony4 JMS\Serializer\SerializedName will be ignored on deserialize()

I have a JSON that will be deserialize() with JMS.
The JSON looks like
{
"creator": [
{
"value": 234,
"label": "Hello"
},
{
"value": 223,
"label": "World"
}
]
}
The JSON will be deserialized with my created Model
$this->serializer->deserialize($json, Model::class, 'json');
and my Model class has:
/**
* #Serializer\Type("array")
*/
private $creator;
This works perfectly fine, the deserializer convert the JSON into my Model and I receive creator with array-items.
I would like to change the variable name from creator to customer in my model. I was thinking it works with the annotation #Serializer\SerializedName().
But when im using this
/**
* #Serializer\Type("array")
* #Serializer\SerializedName("customer")
*/
private $creator;
the model will be not filled at all.
Do I fundamentally misunderstand this function?

JMS #Discriminator filed doesn't appear if specific group is serializing

I'm using Symfony 2.8, FOSRestBundle and JMSSerializerBundle.
Problem
Discriminator field type of Document entity doesn't apear in serialized model when I serialize specific group ("api" group in folowing example) of entity Citizen.
Doctrine Entities
Document:
namespace MyBundle\Entity;
use JMS\Serializer\Annotation as JMS;
…
/**
* #JMS\Discriminator(field = "type", map = {
* "doc1" = "MyBundle\Entity\Document1",
* "doc2" = "MyBundle\Entity\Document2"
* })
*/
class Document
…
Citizen:
class Citizen
{
…
/**
* #var ArrayCollection
*
* #ORM\OneToMany(
* targetEntity="MyBundle\Entity\Document",
* cascade={ "PERSIST", "REMOVE" },
* orphanRemoval=true,
* mappedBy="citizen"
* )
*
* #JMS\Groups({"api"})
*/
private $documents;
…
What I get
{
…
"documents": [
{
"number": "000000",
"date": "01.01.1970",
"serial": "0000",
"place": ""
}
],
…
}
What I need
{
…
"documents": [
{
"type": "doc1",
"number": "000000",
"date": "01.01.1970",
"serial": "0000",
"place": ""
}
],
…
}
If I remove specific serialization group, then type field is present in serialized output.
Thanks in advance
Just found issue on github.
Seems for now, workaround with Default group is needed, see lordelph's comment

Resources