Aggregate values on API Platform response - symfony

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

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)

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' ]

Symfony 4.2/API-platform: non-deterministic security related errors

We're developing a REST API using symfony 4.2 and API-platform and we're experience a strange behavior. When we issue e.g. GET HTTP requests on the generated endpoints : we either get the expected JSON response or a 500 error for the very same request, randomly as it seems. Interestingly enough:
the random errors are experienced using our angular client
the errors NEVER show when issuing curl commands on the very same URLs
Here is the beginning of the returned JSON stack trace when an error occurs:
{
"type": "https://tools.ietf.org/html/rfc2616#section-10",
"title": "An error occurred",
"detail": "The class 'App\\Security\\User\\AppUser' was not found in the chain configured namespaces App\\Entity, Vich\\UploaderBundle\\Entity",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/MappingException.php",
"line": 22,
"args": []
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping",
"short_class": "MappingException",
"class": "Doctrine\\Common\\Persistence\\Mapping\\MappingException",
"type": "::",
"function": "classNotFoundInNamespaces",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php",
"line": 87,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
],
[
"array",
[
[
"string",
"App\\Entity"
],
[
"string",
"Vich\\UploaderBundle\\Entity"
]
]
]
]
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping\\Driver",
"short_class": "MappingDriverChain",
"class": "Doctrine\\Common\\Persistence\\Mapping\\Driver\\MappingDriverChain",
"type": "->",
"function": "loadMetadataForClass",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php",
"line": 151,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
],
[
"object",
"Doctrine\\ORM\\Mapping\\ClassMetadata"
]
]
},
{
"namespace": "Doctrine\\ORM\\Mapping",
"short_class": "ClassMetadataFactory",
"class": "Doctrine\\ORM\\Mapping\\ClassMetadataFactory",
"type": "->",
"function": "doLoadMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php",
"line": 305,
"args": [
[
"object",
"Doctrine\\ORM\\Mapping\\ClassMetadata"
],
[
"null",
null
],
[
"boolean",
false
],
[
"array",
[]
]
]
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping",
"short_class": "AbstractClassMetadataFactory",
"class": "Doctrine\\Common\\Persistence\\Mapping\\AbstractClassMetadataFactory",
"type": "->",
"function": "loadMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php",
"line": 78,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\ORM\\Mapping",
"short_class": "ClassMetadataFactory",
"class": "Doctrine\\ORM\\Mapping\\ClassMetadataFactory",
"type": "->",
"function": "loadMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php",
"line": 183,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping",
"short_class": "AbstractClassMetadataFactory",
"class": "Doctrine\\Common\\Persistence\\Mapping\\AbstractClassMetadataFactory",
"type": "->",
"function": "getMetadataFor",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php",
"line": 283,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\ORM",
"short_class": "EntityManager",
"class": "Doctrine\\ORM\\EntityManager",
"type": "->",
"function": "getClassMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/doctrine-bundle/Repository/ContainerRepositoryFactory.php",
"line": 45,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\Bundle\\DoctrineBundle\\Repository",
"short_class": "ContainerRepositoryFactory",
"class": "Doctrine\\Bundle\\DoctrineBundle\\Repository\\ContainerRepositoryFactory",
"type": "->",
"function": "getRepository",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php",
"line": 713,
"args": [
[
"object",
"Doctrine\\ORM\\EntityManager"
],
[
"string",
"App\\Security\\User\\AppUser"
]
]
}
...
}
For the sake of simplicity, we implemented a dummy AbstractGuardAuthenticator which always returns a hard coded user and which exhibits the very same behavior. Here follows its code, the code of the user entity class along with the interesting bits of the security configuration:
<?php
namespace App\Security;
use App\Security\User\AppUser;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class YesAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
return true;
}
public function getCredentials(Request $request)
{
return array(
'token' => 'sdflsdklfjsdlkfjslkdfjsldk46541qsdf',
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = new AppUser(22, 'toto#wanadoo.fr', 'toto', 'litoto', 'tl', 'totoL', '', array(), null);
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
The user entity class:
<?php
namespace App\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
/**
* Represents a user logged in the SSO authentication system.
*
* #package App\Security\User
*/
class AppUser implements UserInterface, EquatableInterface {
private $id;
private $email;
private $pseudo;
private $avatar;
private $surname;
private $lastName;
private $usePseudo;
private $administeredProjectId;
private $roles;
const ADMIN_ROLE = "administrator";
public function __construct(
$id, $email, $surname, $lastName, $pseudo, $usePseudo, $avatar,
array $roles, $administeredProjectId) {
$this->id = $id;
$this->email = $email;
$this->surname = $surname;
$this->lastName = $lastName;
$this->pseudo = $pseudo;
$this->usePseudo = $usePseudo;
$this->avatar = $avatar;
$this->administeredProjectId = $administeredProjectId;
$this->roles = $roles;
}
public function setId($idd) {
$this->id = $idd;
}
public function getId() {
return $this->id;
}
public function isAdmin() {
return in_array(AppUser::ADMIN_ROLE, $this->roles);
}
public function isProjectAdmin() {
return (!is_null($this->administeredProjectId));
}
public function isLuser() {
return (
!( $this->isAdmin() ) ||
( $this->isProjectAdmin() ) );
}
public function getRoles() {
return $this->roles;
}
public function getSurname() {
return $this->surname;
}
public function getLastName() {
return $this->lastName;
}
public function getAvatar() {
return $this->avatar;
}
public function getAdministeredProjectId() {
return $this->administeredProjectId;
}
public function getPassword() {
return null;
}
public function getSalt() {
return null;
}
public function getEmail() {
return $this->email;
}
public function getUsername() {
return $this->usePseudo ? $this->pseudo : ($this->surname . ' ' . $this->lastName);
}
public function getPseudo() {
return $this->pseudo;
}
public function eraseCredentials() {
}
public function isEqualTo(UserInterface $user) {
if (!$user instanceof AppUser) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}
}
Extract of the security.yaml file:
security:
providers:
user:
entity:
class: App\Security\User\AppUser
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: true
guard:
authenticators:
- App\Security\YesAuthenticator
main:
anonymous: ~
logout: ~
guard:
authenticators:
- App\Security\YesAuthenticator
Here is an example of an entity/resource for which Web services are exhibiting the behavior. Nothing fancy...
<?php
namespace App\Entity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
/**
* #ORM\Entity
* #ORM\Table(name="tb_project")
* #ApiResource(attributes={
* "normalization_context"={"groups"={"read"}},
* "formats"={"jsonld", "json"},
* "denormalization_context"={"groups"={"write"}}},
* collectionOperations={"get"},
* itemOperations={"get"}
* )
*/
class Project
{
/**
* #Groups({"read"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
private $id = null;
/**
* #Groups({"read"})
* #ORM\OneToOne(targetEntity="Project")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
/**
* #Assert\NotNull
* #Groups({"read"})
* #ORM\Column(type="string", nullable=false)
*/
private $label = null;
/**
*
* #Assert\NotNull
* #Groups({"read"})
* #ORM\Column(name="is_private", type="boolean", nullable=false)
*/
private $isPrivate = true;
public function getId(): ?int {
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getIsPrivate(): ?bool
{
return $this->isPrivate;
}
public function setIsPrivate(bool $isPrivate): self
{
$this->isPrivate = $isPrivate;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
return $this;
}
public function __clone() {
if ($this->id) {
$this->id = null;
}
}
public function __toString()
{
return $this->getLabel();
}
}
Please note we have other Web services with custom data providers filtering returned data based upon the current user. In a non-deterministic manner, requests to these also sometimes fail because the current user is null (while a user is indeed logged).

Resources