hide propery in nested self refenced entity in API Platform v3 - symfony

I have a self referenced entity where the $children property shows me which are the children of a specific category. But that property is also present for each $parent (and also children's children).
Is it possible to hide the $children for every nested relation, but show it for requested resource?
Category.php
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
new Get(normalizationContext: ["groups" => ["category", "category_item"]]),
new GetCollection(),
]
)]
#[ORM\Entity]
class Category
{
#[ORM\Id]
#[ORM\Column]
#[ORM\GeneratedValue]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Category::class)]
#[Groups(["category_item"])]
private $parent;
#[ORM\OneToMany(targetEntity: Category::class, mappedBy: "parent")]
#[Groups(["category_item"])]
private $children;
#[ORM\Column]
#[Assert\NotBlank()]
#[Groups(["category"])]
private $name;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function setParent(Category $parent = null)
{
$this->parent = $parent;
return $this;
}
public function getParent()
{
return $this->parent;
}
public function getChildren()
{
return $this->children;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
}
Sample data
ID
parent
name
1
NULL
root
2
1
child-1
3
1
child-2
4
1
child-3
5
2
child-1-1
6
2
child-1-2
7
5
child-1-1-1
8
7
child-1-1-1-1
If I do a GET request to /categories/5, I get this response
{
"#context": "/contexts/Category",
"#id": "/categories/5",
"#type": "Category",
"parent": {
"#id": "/categories/2",
"#type": "Category",
"parent": {
"#id": "/categories/1",
"#type": "Category",
"children": [
"/categories/2",
{
"#id": "/categories/3",
"#type": "Category",
"parent": "/categories/1",
"children": [],
"name": "child-2"
},
{
"#id": "/categories/4",
"#type": "Category",
"parent": "/categories/1",
"children": [],
"name": "child-3"
}
],
"name": "root"
},
"children": [
"/categories/5",
{
"#id": "/categories/6",
"#type": "Category",
"parent": "/categories/2",
"children": [],
"name": "child-1-2"
}
],
"name": "child-1"
},
"children": [
{
"#id": "/categories/7",
"#type": "Category",
"parent": "/categories/5",
"children": [
{
"#id": "/categories/8",
"#type": "Category",
"parent": "/categories/7",
"children": [],
"name": "child-1-1-1-1"
}
],
"name": "child-1-1-1"
}
],
"name": "child-1-1"
}
I would like to get a reponse like this where children is missing in every parent
{
"#context": "/contexts/Category",
"#id": "/categories/5",
"#type": "Category",
"parent": {
"#id": "/categories/2",
"#type": "Category",
"parent": {
"#id": "/categories/1",
"#type": "Category",
"name": "root"
},
"name": "child-1"
},
"children": [
{
"#id": "/categories/7",
"#type": "Category",
"parent": "/categories/5",
"name": "child-1-1-1"
}
],
"name": "child-1-1"
}
I've tryied using maxdepth annotation and it works for children's children (note the missing child-1-1-1-1 in the example above). But I need the parents to have no children at all.
#[ORM\OneToMany(targetEntity: Category::class, mappedBy: "parent")]
#[Groups(["category_item"])]
#[MaxDepth(1)]
private $children;

Related

API Platform 2.5.7 - Setting the returned #type when using a DTO

Using API Platform 2.5.7 and Symfony 4.4.
In order to handle some transformations when responding to a GET request for an entity, I implemented a DTO and an OutputDataTransformer.
https://api-platform.com/docs/core/dto/
The original API would return a JSON object such as:
{
"#id": "/api/records/1",
"#type": "Record",
"id": 1,
"content": "auth1.dns.mydomain.com hostmaster.mydomain.com 2004021303 3600 900 604800 3600",
"name": "mydomain.com",
"ttl": 3600,
"type": "SOA",
"zone": {
"#id": "/api/zones/1",
"#type": "Zone",
"id": 1,
"name": "mydomain.com",
"type": "NATIVE",
"recordCount": 7
},
"priority": 0
},
The entity now has an output config in the ApiResource annotation:
/**
* #ORM\Table(name="records")
* #ORM\Entity(repositoryClass="App\Repository\RecordRepository")
* #ApiResource(
* output=RecordOutput::CLASS,
* normalizationContext={"groups": {"records"}}
* )
* #ApiFilter(OrderFilter::class, properties={"id", "name", "content", "type", "disabled", "zone.name", "priority", "ttl"}, arguments={"orderParameterName": "order"})
* #Assert\GroupSequenceProvider
*/
class Record implements GroupSequenceProviderInterface
{
But after implementing the transformer such as:
<?php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use App\Dto\RecordOutput;
use App\Entity\Record;
class RecordOutputDataTransformer implements DataTransformerInterface
{
/**
* #param Record $record
*/
public function transform($record, string $to, array $context = [])
{
$output = new RecordOutput();
// transform the record type
if (Record::RECORD_TYPE_TXT == $record->getType()) {
// split on any double quotes with a space
$content = explode('" "', $record->getContent());
// recombine the string
$content = implode('', $content);
// remove any stray double quotes
$content = str_replace('"', '', $content);
$output->content = $content;
} else {
$output->content = $record->getContent();
}
$output->id = $record->getId();
$output->name = $record->getName();
$output->ttl = $record->getTtl();
$output->type = $record->getType();
$output->priority = $record->getPriority();
$output->zone = $record->getZone();
return $output;
}
The #type is no longer there when GETTING the resource:
{
"#context": "/api/contexts/Record",
"#id": "/api/records",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/records/1",
"id": 1,
"content": "auth1.dns.mydomain.com hostmaster.mydomain.com 2004021303 3600 900 604800 3600",
"name": "mydomain.com",
"ttl": 3600,
"type": "SOA",
"zone": {
"#id": "/api/zones/1",
"#type": "Zone",
"id": 1,
"name": "mydomain.com",
"type": "NATIVE",
"recordCount": 7
},
"priority": 0
},
Is there another configuration that needs to happen to specify the #type in the transformer or on the entity annotations?
Looks like this was a bug that was fixed in 2.5.8.
https://github.com/api-platform/core/pull/3699
Updating to 2.5.9 from 2.5.7 got this working (edited)

Aggregate values on API Platform response

Similar to this question, I am using API Platform with Doctrine entities - I have an Entity which contains a value:
/**
* #ApiResource()
*/
class Credit
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer")
*/
private $value;
}
I would like to retrieve the sum of this value and return it in the top level element of the response when querying for a collection:
{
"#context": "/api/contexts/Credit",
"#id": "/api/credits",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/credits/1",
"#type": "Credit",
"id": 1,
"value": 200,
"createdAt": "2019-03"
},
{
"#id": "/api/credits/2",
"#type": "Credit",
"id": 2,
"value": 200,
"createdAt": "2019-04"
}
],
"hydra:totalItems": 2,
"totalValues": 400
}
However, I would like to achieve this using a copy of the query instead of summing the values after the execution to maintain the same totalValues amount when pagination is applied - much the same way that hydra:totalItems will always return the total number of items.
What would be the best way to achieve this?
You can create a CreditCollectionNormalizer:
<?php
namespace App\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
class CreditCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
public const RESOURCE_CLASS = \App\Entity\Credit::class;
private const ALREADY_CALLED = 'CREDIT_COLLECTION_NORMALIZER_ALREADY_CALLED';
private NormalizerInterface $normalizer;
public function __construct(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}
public function normalize($object, string $format = null, array $context = []): array
{
$context[self::ALREADY_CALLED] = true;
$data = $this->normalizer->normalize($object, $format, $context);
if ('collection' === $context['operation_type'] &&
'get' === $context['collection_operation_name'] &&
self::RESOURCE_CLASS == $context['resource_class']) {
$balance = 0;
foreach ($object as $item) {
$balance += $item->amount;
}
$data['hydra:meta']['balance'] = $balance;
}
return $data;
}
public function supportsNormalization($data, string $format = null, array $context = []): bool
{
return $this->normalizer->supportsNormalization($data, $format, $context);
}
public function setNormalizer(NormalizerInterface $normalizer)
{
if ($this->normalizer instanceof NormalizerAwareInterface) {
$this->normalizer->setNormalizer($normalizer);
}
}
}
Then register it as a CollectionNormalizer decorator in services.yaml:
services:
App\Serializer\Normalizer\CreditCollectionNormalizer:
decorates: 'api_platform.hydra.normalizer.collection'
arguments: [ '#App\Serializer\Normalizer\CreditCollectionNormalizer.inner' ]
public: false
api/credits should now return the following:
{
"#context": "/api/contexts/Credit",
"#id": "/api/credits",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/credits/1",
"#type": "Credit",
"id": 1,
"value": 200,
"createdAt": "2019-03"
},
{
"#id": "/api/credits/2",
"#type": "Credit",
"id": 2,
"value": 200,
"createdAt": "2019-04"
}
],
"hydra:totalItems": 2,
"hydra:meta": {
"balance": 400
}
}

symfony fetchall with manyToOne relation returning same object multiple times

I am new to symfony so its likely I am doing something wrong...
This is my DB with relation
I am trying to create API that will return things back, but student controller will return "grade" table in some nested strange form
class StudentController extends AbstractController
{
/**
* #Route("/api/student", name="student")
*/
public function getSubjects()
{
$repository = $this->getDoctrine()->getManager()->getRepository(Student::class);
$result = $repository->findAll();
return $this->json($result, Response::HTTP_OK, [], [
ObjectNormalizer::ENABLE_MAX_DEPTH => false,
ObjectNormalizer::IGNORED_ATTRIBUTES => ['student'],
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}]
);
}
API will return:
{
"id": 1,
"OIB": 2147483647,
"name": "Amalia",
"surname": "Hill",
"address": "Derick Ports 82330",
"dateOfBirth": "2008-03-26T00:00:00+01:00",
"postalCode": {
"id": 241,
"name": "SOMEWHERE",
"postalCode": 31000,
"__initializer__": null,
"__cloner__": null,
"__isInitialized__": true
},
...
"scores": [
{
"id": 1,
"subject": {
"id": 3,
"code": "eng11",
"name": "ENGLISH",
"grade": {
"id": 1,
"grade": "1",
"__initializer__": null,
"__cloner__": null,
"__isInitialized__": true
},
"scores": [
{},
{
"id": 19,
"subject": {
"__initializer__": null,
"__cloner__": null,
"__isInitialized__": true
},
"score": 3,
"description": "Excepturi vitae ipsam sunt.",
"date": "2020-04-21T00:00:00+02:00"
},...
as you can see I have scores inside and that's OK, but inside score I have another score and I don't know how to get rid of it, any ideas?
// App\Entity\Student.php
/**
* #ORM\Entity(repositoryClass=StudentRepository::class)
*/
class Student
{
...
/**
* #ORM\OneToMany(targetEntity=Score::class, mappedBy="student")
*/
private $scores;
public function __construct()
{
$this->scores = new ArrayCollection();
}
/**
* #return Collection|Score[]
*/
public function getScores(): Collection
{
return $this->scores;
}
PS, that score with ID = 19 and rest, is all scores with same subject_id
Change the function name getScores() from Entity Subject, for sample getAllScores() , and add in the ignoring attributes :
return $this->json($result, Response::HTTP_OK, [], [
ObjectNormalizer::ENABLE_MAX_DEPTH => false,
ObjectNormalizer::IGNORED_ATTRIBUTES => ['student,allScores'],
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}]
);
more here

How to add an extra information on api-platform result

I'am using symfony and api platform.
I have a ressource with :
/**
* #ApiResource()
*/
class Credit
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer")
*/
private $value;
}
the result of /api/credits is :
{
"#context": "/api/contexts/Credit",
"#id": "/api/credits",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/credits/1",
"#type": "Credit",
"id": 1,
"value": 200,
"createdAt": "2019-03"
},
{
"#id": "/api/credits/2",
"#type": "Credit",
"id": 2,
"value": 200,
"createdAt": "2019-04"
}
],
"hydra:totalItems": 2
}
i want to add an extra information to my result like the totalValues :
400 ( sum of "value" of all results )
how can i do it
expected result :
{
"#context": "/api/contexts/Credit",
"#id": "/api/credits",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/credits/1",
"#type": "Credit",
"id": 1,
"value": 200,
"createdAt": "2019-03"
},
{
"#id": "/api/credits/2",
"#type": "Credit",
"id": 2,
"value": 200,
"createdAt": "2019-04"
}
],
"hydra:totalItems": 2,
"totalValues": 400
}
Solution is to implement NormalizerInterface and NormalizerAwareInterface like this :
ApiCollectionNormalizer :
namespace App\Serializer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class ApiCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
/**
* #var NormalizerInterface|NormalizerAwareInterface
*/
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
if (!$decorated instanceof NormalizerAwareInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', NormalizerAwareInterface::class)
);
}
$this->decorated = $decorated;
}
/**
* #inheritdoc
*/
public function normalize($object, $format = null, array $context = [])
{
$data = $this->decorated->normalize($object, $format, $context);
if ('collection' === $context['operation_type'] && 'get' === $context['collection_operation_name']) {
if ($data['#id'] === '/api/credits') {
$totalValues = 0;
foreach ($data['hydra:member'] as $credit) {
$totalValues += $credit['value'];
}
$data['totalValues'] = $totalValues;
}
}
return $data;
}
/**
* #inheritdoc
*/
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
/**
* #inheritdoc
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->decorated->setNormalizer($normalizer);
}
}
services.yaml :
'App\Serializer\ApiCollectionNormalizer':
decorates: 'api_platform.hydra.normalizer.collection'
arguments: [ '#App\Serializer\ApiCollectionNormalizer.inner' ]

Hydrate joined Entities with Api-Platform

I m building my first rest API with Api-Platform for an AngularJs app,
I want to get all my Project entity like juste here (with my url /projects):
{
"#context": "/contexts/Project",
"#id": "/projects",
"#type": "hydra:PagedCollection",
"hydra:totalItems": 19,
"hydra:itemsPerPage": 30,
"hydra:firstPage": "/projects",
"hydra:lastPage": "/projects",
"hydra:member": [
{
"#id": "/projects/1",
"#type": "Project",
"name": "test1",
"parent": null,
"createdAt": "2014-12-22T11:38:13+01:00",
"updatedAt": null,
"deletedAt": null
},
{
"#id": "/projects/2",
"#type": "Project",
"name": "test2",
"parent": null,
"createdAt": "2014-12-22T17:02:50+01:00",
"updatedAt": null,
"deletedAt": null
},
{
"#id": "/projects/3",
"#type": "Project",
"name": "test3",
"parent": "/projects/2",
"createdAt": "2014-12-22T18:28:50+01:00",
"updatedAt": null,
"deletedAt": null
}
]
}
But as you can see my Projects can have parent, so I got a reference of my parent Project ( like this /projects/2 )
Can I get directly a Project Object in Json instead of reference like this ?
{
"#id": "/projects/3",
"#type": "Project",
"name": "test3",
"parent": {
"#id": "/projects/2",
"#type": "Project",
"name": "test2",
"parent": null,
"createdAt": "2014-12-22T17:02:50+01:00",
"updatedAt": null,
"deletedAt": null
},
"createdAt": "2014-12-22T18:28:50+01:00",
"updatedAt": null,
"deletedAt": null
}
This is a good pratical of Rest APi ?
API Platform has a built-in function for embedding relations in the parent JSON document.
Your entity will look like:
namespace AppBundle\Entity;
use Symfony\Component\Serializer\Annotation\Groups;
class Project
{
private $id;
/** #Groups({"embed"}) */
private $parent;
/** #Groups({"embed"}) */
private $name;
/** #Groups({"embed"}) */
private $createdAt;
// ...
}
And the service definition:
# app/config/services.yml
services:
# ...
resource.offer:
parent: api.resource
arguments: [ 'AppBundle\Entity\Offer' ]
calls:
- method: initNormalizationContext
arguments: [ { groups: [ embed ] } ]
tags: [ { name: api.resource } ]
Be careful, it will embed the parent as well as the parent of the parent and so on. If you want to change this, you need to create a custom normalizer. It will be more straightforward when Symfony 3.1 will be released thanks to the new #MaxDepth annotation.

Resources