symfony fetchall with manyToOne relation returning same object multiple times - symfony

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

Related

hide propery in nested self refenced entity in API Platform v3

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;

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
}
}

Laravel return encrypted id, the id will be overrided with 0

What I am trying to achieve is that I want to return the encrypted id, but the function will change it to 0 instead of the encrypted value. I made a helper function for encrypting the id, pass by reference.
if (! function_exists('encryptID')) {
/**
* Generate the URL to a controller action.
*
* #param $data
* #param array $keys
*/
function encryptID(&$data = [], $keys = [])
{
// if(empty($keys)) $keys = ['id'];
foreach($data as &$aData) {
foreach ($keys as $aKey){
if(isset($aData[$aKey])){
$aData[$aKey] = encrypt($aData[$aKey]);
$aData['_' . $aKey] = encrypt($aData[$aKey]);
}
}
}
}
}
In the controller
/**
* #return mixed
*/
public function index()
{
$tasks = $this->user->tasks()->get();
encryptID($tasks, ['id']);
return response()->apiSuccess($tasks);
}
apiSuccess function:
Response::macro('apiSuccess', function ($responseData) {
return Response::json([
'success' => true,
'body' => $responseData
]);
});
The result the api returns:
[
{
"id": 0,
"title": "aaa",
"description": "bbbbbb",
"created_at": "2019-11-16 04:13:21",
"updated_at": "2019-11-16 04:13:21",
"_id": "eyJpdiI6InFweGtISm1jaE9vOFRuZDBuSUt5WVE9PSIsInZhbHVlIjoiOUQzSE1nWTc4MXM2UnRZQ3BScXNCQT09IiwibWFjIjoiNzM1YWU0Y2UwZjdkN2ZmNDM5MmYzYTRhNTI0YTI4ZDdjNTU5N2M1M2E4NWQyZGRiMzI4MzVjNGFiMjUxMmU4NiJ9"
},
{
"id": 0,
"title": "aaa",
"description": "bbbbbb",
"created_at": "2019-11-16 04:14:53",
"updated_at": "2019-11-16 04:14:53",
"_id": "eyJpdiI6ImtBeU92cWhuT05FS3NSYXErOCtKWUE9PSIsInZhbHVlIjoiMTRMN2tTV3Q3SGFzVWE0Q2ZOUXJlQT09IiwibWFjIjoiNzZjMThkMzViMDg4ODllNzk3ZTc3MWMzN2FiYzhmZTg2ZGI2MmM2Y2IzOWM5ZGQ4NTJiMDMwMTZjOTBjN2ZlMiJ9"
}
]
If I change the key = ['title'], it works as expected.
[
{
"id": 1,
"title": "eyJpdiI6Ikc5bjNWc0pmd2Y2b1lvTFo3M25sVlE9PSIsInZhbHVlIjoieEpLRFwvTVUwckZkbjVCVGwrZ3pMMUE9PSIsIm1hYyI6IjA2NTJhYzNjMjBiMzliYmMyYTYxMjU4N2VmOGFhZTVmMGUzZjBhNzdlMTFjYTQ2YTFkNDA0ODVmMTljZmIzZTMifQ==",
"description": "bbbbbb",
"created_at": "2019-11-16 04:13:21",
"updated_at": "2019-11-16 04:13:21",
"_title": "eyJpdiI6Im5UYXErNFMzQjY3c3lHSzJ3eGJcLzFBPT0iLCJ2YWx1ZSI6ImpLc1dGWThBSUpKMllEb0VyY0RVcjdIZ3Y1OEZUdld1d3dUYmJmXC9wWkZjUWFyWDlqUnNvMVwvRDlDSjU4QkY2MFJtcGYzbFM0alJ0Z0NoQnZrU3pJRGc0cU1ta2lSaEl3VFk0QlZPSXFZTkVIdDd0ZGNKaGVkb3FmV3dOUlJyYlB4OEV0QlM3RXE0aDVYTFlxWGFWYmZ4UVNkUnBsTmo4eDdNSjloRUhSSUxkZEZoR3VMcldvZHJuczRkVWVrS0NOQWVCOXgwRnJFNjZjU2R4b3VWYUZyZUtkc2dtc0xoNFZzejdFcFpuTHBua1BSa2NMblJoZ2VUWnZhakk2UUNoWkNNZGlRTnpHMHZpcDVqMGg2QmNudWc9PSIsIm1hYyI6IjBiM2QyMTNlNDY4ZGEyMjA1MTVlNWZmMWRkY2IzZjU0MzIxYjVhZmQ4MTQ1OGQzYzkxZmMwMWFkYzg5MmQ4NTYifQ=="
},
{
"id": 3,
"title": "eyJpdiI6IkVIbVBGcWhCanA1UzBPRnZ1S2RXY1E9PSIsInZhbHVlIjoiN2JSWG1nV1B1Z0lFWEJHOVBPaDh5dz09IiwibWFjIjoiYWQwMzA0NmRhZDc4MzEwNTRhZTFhZWI2MThjYzAzZTg2ZWEzOTAyNzhmNTkwNDU3ZTA0ZWIzYjdhOTM3NWFlNSJ9",
"description": "bbbbbb",
"created_at": "2019-11-16 04:14:53",
"updated_at": "2019-11-16 04:14:53",
"_title": "eyJpdiI6InowVzM4WE9YaGdvZG9kY1Frdm9Jb1E9PSIsInZhbHVlIjoiaUN4UnVNcVVwQVNSSGYxbHFtWHMzeUhUNUJBTytwQXVDaUhqanJ6ZXJJa0NaV01CYzNcLzZKdFJHazdsQzNnSElnWnZIb0lQUzFlZ24zemZCWk54TmRrR0Q4NTlOV04zNnR5ZmptSXR0aWlKVEU0dGNNUFFHc1Q3NU4ybE5lOVZ3V245UTVYdDRWdHRac29XbmluUW9YckdpNzU2WmRGY2luMjVRN0xrRFkrODVTT0lrR1hIaVd2ZDNGak1MWDJwUTczRlwvYVE3RWZ5YVwvdE9BU3pGTjVndlNINlwvbUtGMmRLcU13ZUdHMlVHcUhjcTk2dlNIdVkwZlRXOWh0ZWpBMzdzb29DS2Y1WjF3ZWNFRmJQa205THJBPT0iLCJtYWMiOiJiZGU3ZjMwZmU5Zjk4YTE4MmNjODVhNTI1NWU1MTEyNGQxMDA3YjM5ZjMyM2FiY2VhOTVhZDViZTBkNmZmNzhiIn0="
}
]
As you can see all ids will be changed to 0, instead of the encrypted value. I am confused, why would that happen. I made a new key "_id" to test if the encryption is successful, it turns out that the encryption works, but somehow reassign the value to id does not work.
Laravel version: 6.2,
PHP version: 7.2.
Anyone knows the reason? Thanks!
The id field is set as the primary key and 'incrementing'. When you get this attribute it will be cast to an integer because it is 'incrementing' and set as integer by default.
You can set $incrementing = false; on the model to stop this particular cast.
0 === (int) "some string"

API Platform filter entity data

I just start to use Api platform and immediately stuck with problem how to filter data.
I have entity User and i want to filter data that are present in response ( JSON API format)
{
"links": {
"self": "/api/users"
},
"meta": {
"totalItems": 2,
"itemsPerPage": 30,
"currentPage": 1
},
"data": [
{
"id": "/api/users/1",
"type": "User",
"attributes": {
"_id": 1,
"username": "jonhdoe",
"isActive": true,
"address": null
}
},
{
"id": "/api/users/3",
"type": "User",
"attributes": {
"_id": 3,
"username": "test",
"isActive": true,
"address": null
}
}
]
}
so I want to remove e.g. User with id 3, but not use filters sent via request. I just want to set filter that will be always run when someone go to /api/users.
I look to api-platform extensions but this will be applied on each request e.g. /api/trucks. So at end I just want to get something like
{
"links": {
"self": "/api/users"
},
"meta": {
"totalItems": 1,
"itemsPerPage": 30,
"currentPage": 1
},
"data": [
{
"id": "/api/users/1",
"type": "User",
"attributes": {
"_id": 1,
"username": "jonhdoe",
"isActive": true,
"address": null
}
}
]
}
As you pointed out, extensions are the way to go.
The applyToCollection method gets a $resourceClass parameter containing the current resource class.
So you can apply the WHERE clause only for a specific class in this method like:
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if (User::class === $resourceClass) {
$queryBuilder->doSomething();
}
// Do nothing for other classes
}

Resources