Add dynamic property on entity to be serialized - symfony

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);

Related

How to set validation order in symfony, field validation depends on another field?

For example, I have an entity with two fields:
/**
* #Assert\Range(min=1, max=self::SPEND_MAX)
*/
public ?int $spendMax = null;
/**
* #Assert\NotBlank()
* #Assert\Length(max=255)
*/
public string $name;
How I can set for spendMax field, that first of all I need to validate if field name is not null, if it's true, that I can start validation Range of spendMax.
If name is null, then validation of spendMax is false.
You can make your own custom method to do the validation and arrange for it to be called before persisting or updating the entity.
Something along these lines:
abstract class ValidatableEntity
{
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function validate(): void
{
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator()
;
$violations = $validator->validate($this);
if (0 !== $violations->count()) {
throw new ValidationFailedException($this, $violations);
}
}
/**
* Intended to be called from the prePersist Event from EntityPersistenceEventSubscriber service
* when this object is to be persisted.
* The method is intended to be overridden in derived classes and does nothing here.
* If the validation in the derived class is unsuccessful, the method should throw
* a ValidationFailedException.
* Note that since the object has not yet been persisted, its ID field will not be defined in this method.
*/
public function prePersistValidation(EntityManager $entityManager): void
{
return;
}
/**
* Intended to be called from the preUpdate Event from EntityPersistenceEventSubscriber service
* when this object is to be updated.
* The method is intended to be overridden in derived classes and does nothing here.
* If the validation in the derived class is unsuccessful, the method should throw
* a ValidationFailedException.
*/
public function preUpdateValidation(PreUpdateEventArgs $args): void
{
// How to get an entity manager here:
// $entityManager = $args->getObjectManager();
return;
}
To use it, derive your entity class from this one and override the preUpdateValidation and prePersistValidation methods.

How to filter custom property on api-platform entity

I have an entity in Api Platform such as a team:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\Basketball\TeamRepository")
* #ApiResource(
* routePrefix="/v2",
* normalizationContext={"groups"={"public:read"}, "enable_max_depth"=true},
* iri="http://schema.org/Team",
* collectionOperations={
* "get",
* },
* itemOperations={
* "get",
* },
* )
*/
class Team implements ObjectManagerAware
{
use \App\Entity\Traits\UUIDTrait;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Basketball\Event", mappedBy="teams")
*/
private $events;
public function __construct()
{
$this->events = new ArrayCollection();
}
/**
* #return Collection|Event[]
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->addTeam($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->contains($event)) {
$this->events->removeElement($event);
$event->removeTeam($this);
}
return $this;
}
}
This all works and I can load teams and events. What I'd like to do is have a property on the API result that returns only upcoming events (not past events). What is the "proper" way to do that? I've tried adding a custom method such as
/**
* #return Collection|Event
* #Groups({"public:read"})
*/
public function getRemainingEvents(): ?Collection
{
$qb = $this->em->createQueryBuilder();
$qb->select('e')->from(Event::class, 'e');
$qb->where(
$qb->expr()->orX($qb->expr()->eq('IDENTITY(e.home)', $this->getId()), $qb->expr()->eq('IDENTITY(e.away)', $this->getId()))
);
$qb->andWhere('e.startDate >= :d');
$qb->andWhere('e.startDate >= :d');
$qb->setParameter('d', (new \DateTime())->format('Y-m-d'));
$qb->orderBy('e.startDate', 'ASC');
$query = $qb->getQuery();
return new ArrayCollection($query->getResult());
}
which does do sort of what I want but the api result using application/vnd.api+json doesn't list the events in the "relationships" the same way that the "events" property does. I've tried looking at the custom filters (https://api-platform.com/docs/core/filters/#creating-custom-filters) but don't understand how that would work for a custom method on the entity.
I'd like the remainingEvents to act the same as events and list the related items in the "relationships" of the response.
Is there some way to annotate it for the serializer or use the filter?
I think you realize that using that approach (a method within an Entity retrieving data from Database) is wrong. Entities should be dummy.
About what are you trying to do, I think your best solution should be adding a DateFilter (https://api-platform.com/docs/core/filters/#date-filter) to the "Events" Resource, allowing you to do a GET events?startDate[after]=2019-09-18
And if you want to filter Events from a specific "Team" Resource, simply add another filter to the Event resource, a Search Filter (https://api-platform.com/docs/core/filters/#search-filter) to specify the Team, allowing you to do a:
GET events?startDate[after]=2019-09-18&team=IRIofteam_OR_ID

Symfony Entity load calculated field but not always

I have a symfony entity that has a not mapped calculated field
namespace AppBundle\Entity;
class Page
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Page count. Non-mapped
*
* #var integer
*/
protected $pageCount;
}
The $pageCount value is obtainable by consuming a remote service that will provide the value for use in the application.
I figured the best way is to use the postLoad event to handle this.
class PageListener
{
/**
* #ORM\PostLoad
*/
public function postLoad(LifecycleEventArgs $eventArgs)
{
// ...
}
}
I need to retrieve this value when loading values.
public function indexAction()
{
// I want to fetch the pageHits here
$pagesListing = $this->getDoctrine()
->getRepository('AppBundle:Pages')
->findAll();
// I don't want to fetch the pageHits here
$pagesListing2 = $this->getDoctrine()
->getRepository('AppBundle:Pages')
->findAll();
}
However, this will ALWAYS result in a call to a remote service.
There may be cases where I do not want the service to be invoked, so that it reduced a performance load on the application.
How can I fetch the remote values automatically, but only when I want to.
Your "problem" is pretty common and one of the reasons I never use Doctrine repositories directly.
Solution I would recommend
Always make custom repository services and inject Doctrine into them.
That way, if you want to merge some data from some other data source (eg. Redis, filesystem, some remote API), you have complete control over it and process is encapsulated.
Example:
class PageRepository
{
private $em;
private $api;
public function __construct(EntityManagerInterface $em, MyAwesomeApi $api)
{
$this->em = $em;
$this->api = $api;
}
public function find($id)
{
return $em->getRepository(Page::class)->find($id);
}
public function findAll()
{
return $em->getRepository(Page::class)->findAll();
}
public function findWithCount($id)
{
$page = $this->find($id);
$count = $this->myAwesomeApi->getPageCount($id);
return new PageWithCount($page, $count);
}
}
Solution I wouldn't recommend, but works :)
If you don't want to change your code structure and want to keep it as it is, you could make a really simple change that will make your pageCount be loaded only when it is necessary:
Move code from Page::postLoad method into Page::getPageCount()
Example:
public function getPageCount()
{
if (null === $this->pageCount) {
$this->pageCount = MyAwesomeApi::getPageCount($this->id);
}
return $this->pageCount;
}
This way, pageCount will only be loaded if something tries to access it.

Doctrine one to many - persisting multiple files from owning side not working

I have 2 entities Submission and Documents. 1 Submission can have Multiple documents.
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $document;
/**
* #return mixed
*/
public function getDocument()
{
return $this->document->toArray();
}
public function setDocument(Document $document)
{
$this->document[] = $document;
return $this;
}
Document Entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="document")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id",onDelete="cascade", nullable=true)
*/
protected $submission;
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
After receiving files dropzonejs - I'm saving them into Document object, and then, i'm try to save this object into Submission, and persist.
$document = new Document();
$em = $this->getDoctrine()->getManager();
$media = $request->files->get('file');
foreach($media as $req){
$document->setFile($req);
$document->setPath($req->getPathName());
$document->setName($req->getClientOriginalName());
$em->persist($document);
}
$submission->setSubmissionStatus(true);
foreach($document as $item){
$submission->setDocument($item);
}
$submission->setUser($user);
$em = $this->getDoctrine()->getManager();
$em->persist($submission);
$em->flush();
Problem is that all the time, i'm receiving error that submission_title is not set, but that's not true, because i have set this field before. I haven't got idea, what is wrong.
I think you'll get some mileage out of following the tutorial over at http://symfony.com/doc/current/doctrine/associations.html, if you haven't already.
I can see that your getters / setters aren't optimal for associating more than one Document with your Submission.
As they write in the Symfony docs, where they want to associate one category with many products, they have the following code:
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
}
From the docs:
The code in the constructor is important. Rather than being
instantiated as a traditional array, the $products property must be of
a type that implements Doctrine's Collection interface. In this case,
an ArrayCollection object is used. This object looks and acts almost
exactly like an array, but has some added flexibility. If this makes
you uncomfortable, don't worry. Just imagine that it's an array and
you'll be in good shape.
So, you'll want to be sure the constructor for your Document entity has something like $this->submissions = new ArrayCollection();. I've changed the property to a plural name, because I think it's more semantically correct. But you can keep your $submission property name, if you like.
Next is to add a addSubmission, removeSubmission, and a getSubmissions method.
Then, your class might end up looking like this:
<?php
// src/AppBundle/Entity/Submission.php
namespace AppBundle\Entity
use Doctrine\Common\Collections\ArrayCollection;
class Submission
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*
* #var ArrayCollection()
*/
protected $documents;
...
/**
* Instantiates the Submission Entity
*
* #return void
*/
public function __construct()
{
$this->documents = new ArrayCollection();
}
/**
* Returns all documents on the Submission
*
* #return mixed
*/
public function getDocuments()
{
return $this->documents;
}
/**
* Add document to this Submission
*
* #param Document $document The object to add to the $documents collection.
*
* #return Submission
*/
public function setDocument(Document $document)
{
$this->documents[] = $document;
return $this;
}
/**
* Remove a document from this Submission
*
* #param Document $document The object to remove from the $documents collection.
*
* #return Submission
*/
public function removeDocument(Document $document)
{
$this->documents->removeElement($document);
return $this;
}
}

Programmatically query Symfony / Doctrine ORM

I'm using Doctrine-defined Entity schema as a reference to create a C++/Qt API to access such data.
Is there a way to programmatically iterate through all the fields and their parameters such that there is only one master schema (and the C++ boilerplate headers are generated from it?)
To get entities information, such as attributes and their properties you can use Doctrine Metadata Drivers. You just need your entities namespaces, once you have it you can use Doctrine to get their metadata.
I've created a service for this in my application (Posting here just as a usage example):
namespace Acme\Project\ProjectUserBundle\Service\Mapping;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityNotFoundException;
/**
* Class EntityService
*/
class EntityService
{
private $em;
public function __construct(EntityManager $entityManager)
{
$this->em = $entityManager;
}
/**
* Return all Doctrine entities namespaces
*
* #return array
*/
public function getAllEntityClasses()
{
$doctrineEntities = array();
$allEntitiesMetadata = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($allEntitiesMetadata as $entityMetadata) {
$doctrineEntities[] = $entityMetadata->getName();
}
return $doctrineEntities;
}
/**
* Return all Doctrine entities namespaces in a given base namespace
*
* #param $namespace
* #return array
* #throws \Doctrine\ORM\EntityNotFoundException
*/
public function getAllEntityClassesInNamespace($namespace)
{
$allEntityClasses = $this->getAllEntityClasses();
foreach ($allEntityClasses as $entity) {
preg_match($namespace, $entity, $haveFound);
if (!empty($haveFound)) {
$entitiesInNamespace[] = $entity;
}
}
if (!isset ($entitiesInNamespace)) {
throw new EntityNotFoundException("No entities found in $namespace namespace");
}
return $entitiesInNamespace;
}
/**
* Receives a entity namespace and return all entity metadata
*
* #param $entityNamespace
* #return \Doctrine\Common\Persistence\Mapping\ClassMetadata
*/
public function getEntityMetadata($entityNamespace)
{
$metadataFactory = $this->em->getMetadataFactory();
$entityMetadata = $metadataFactory->getMetadataFor($entityNamespace);
//Check out, all entity information here;
var_dump($entityMetadata);
foreach ($entityMetadata->fieldMappings as $fieldMapping) {
//Each attribute info;
var_dump($fieldMapping);
}
return $entityMetadata;
}
}
Register it on your services.yml/xml, it uses Doctrine entity manager as dependency.
services:
Mapping.EntityService:
class: Acme\Project\ProjectUserBundle\Service\Mapping\EntityService
arguments: [ #doctrine.orm.entity_manager ]
Than just use the service on your controller (In my case I was using this service to get entities and apply permissions for users over them (read/write/view):
class UserPermissionController extends Controller
{
public function yourAction()
{
$entityService = $this->get('Mapping.EntityService');
$entitiesToApplyPermissions = $entityService->getAllEntityClassesInNamespace('/Acme/');
foreach ($entitiesToApplyPermissions as $entity) {
$entityService->getEntityMetadata($entity);
//do whatever you want here
}
}
}
Note that this is just an example of usage, take a look at the documentation for more info:
http://doctrine-orm.readthedocs.org/en/latest/reference/metadata-drivers.html
To generate the C++ headers you likely could create another service to handle, which btw could use this Mapping.EntityService as dependency.

Resources