How to set a default entity relation in a property? - symfony

I have 2 entities, Contact and ContactType.
The owner entity is Contact, with a property $type :
/**
* #ORM\ManyToOne(targetEntity="Evo\BackendBundle\Entity\ContactType")
* #ORM\JoinColumn(nullable=true)
*/
protected $type = null;
I now have to set this relation to be mandatory. I tried the following :
/**
* #ORM\ManyToOne(targetEntity="Evo\BackendBundle\Entity\ContactType")
* #ORM\JoinColumn(nullable=false)
*/
protected $type = 2;
But I get an error, which is pretty logic. I should set an entity (with id 2) as default, not a integer. But I have no idea how to do this. I previously read I shouldn't do any query to DB or any use of EntityManager inside an entity. So how can I set a default ContactType ?

A better solution probably would be to put this logic in some kind of "manager" service, for example a ContactManager.
<?php
use Doctrine\ORM\EntityManagerInterface;
class ContactManager
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function createContact(ContactType $type = null)
{
if (!$type instanceof ContactType) {
$type = $this->manager->getReference('ContactType', 2);
}
return new Contact($type);
}
}
Then define your service (for example in services.yml):
contact_manager:
class: ContactManager
arguments: [#doctrine.orm.entity_manager]

Related

Symfony: how to add a serializer group on a Route depending on logged in user's roles

I'm using JMSSerializerBundle in my entities definition, and RestBundle's annotations in controllers.
I have an entity with public and admin-protected attributes, let's say
use JMS\Serializer\Annotation as Serializer;
class UserAddress {
/**
* #Serializer\Expose
* #Serializer\Groups(groups={"address:read"})
*/
private $nonSecretAttribute;
/**
* #Serializer\Expose
* #Serializer\Groups(groups={"address:admin-read"})
*/
private $secretAttribute;
}
and a User like :
class User {
// ...
/**
* #ORM\OneToMany(targetEntity="UserAddress", mappedBy="user")
*/
private $addresses;
and my controller looks like
use FOS\RestBundle\Controller\Annotations as Rest;
class UsersController {
/**
* #Rest\Get("/users/{user}/addresses", requirements={"user"="\d+"})
* #Rest\View(serializerGroups={"address:read"})
* #IsGranted("user_read", subject="user")
*/
public function getUsersAddresses(User $user)
{
return $user->getAddresses();
}
}
but how could I add the address:admin-read to the serializer groups here if the logged in user happens to have the ADMIN_ROLE role ?
Is that possible in the #Rest\View annotation ?
Do I have a way to modify the groups in the controller's method, inside a conditional loop verifying my logged in user's roles ?
You should instantiate the fos-rest View manually and add a Context. On that Context you can set the serialization groups at runtime by evaluating the users roles.
Something like this should work:
use FOS\RestBundle\View\View;
use FOS\RestBundle\Context\Context;
class UsersController
{
public function getUsersAddresses(User $user): View
{
$isAdmin = in_array('ROLE_ADMIN', $user->getRoles());
$context = new Context();
$context->setGroups($isAdmin ? ['address:admin-read'] : ['address:read']);
$view = VVV::create()->setContext($context);
return $view
->setContext($context)
->setData($user->getAddresses());
}
}

How can i limit the number of nested entities in API Platform?

Having two related entities, let's say Author and Book, I can limit (or paginate) the results of Authors but not the number of results of its related entity Books which always shows the whole collection.
The issue is that Authors may have hundreds of Books making the resulting JSON huge and heavy to parse so I'm trying to get, for example, only the last 5 books.
I'm sure I'm missing something since I think this is probably a common scenario but I can't find anything on the docs nor here in StackOverflow.
I'm starting with Api Platform, any hint would be appreciated!
I finally solved it creating a normalizer for the entity but I still think that it has to be a simpler solution.
Here's what I had to do, following the Authors / Books example:
Add a setter to the Author entity to override the Author's Book collection:
// src/Entity/Author.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
// ...
/**
* #ApiResource
* #ORM\Entity(repositoryClass="App\Repository\AuthorRepository")
*/
class Author
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Book", mappedBy="author", orphanRemoval=true)
*/
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// Getters and setters
//...
public function setBooks($books): self
{
$this->books = $books;
return $this;
}
}
Create a normalizer for the Author's entity:
// App/Serializer/Normalizer/AuthorNormalizer.php
<?php
namespace App\Serializer\Normalizer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
class AuthorNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
private $normalizer;
public function __construct(
NormalizerInterface $normalizer,
IriConverterInterface $iriConverter
) {
if (!$normalizer instanceof DenormalizerInterface) {
throw new \InvalidArgumentException('The normalizer must implement the DenormalizerInterface');
}
if (!$normalizer instanceof AbstractItemNormalizer) {
throw new \InvalidArgumentException('The normalizer must be an instance of AbstractItemNormalizer');
}
$handler = function ($entity) use ($iriConverter) {
return $iriConverter->getIriFromItem($entity);
};
$normalizer->setMaxDepthHandler($handler);
$normalizer->setCircularReferenceHandler($handler);
$this->normalizer = $normalizer;
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->normalizer->denormalize($data, $class, $format, $context);
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->normalizer->supportsDenormalization($data, $type, $format);
}
public function normalize($object, $format = null, array $context = [])
{
// Number of desired Books to list
$limit = 2;
$newBooksCollection = new ArrayCollection();
$books = $object->getBooks();
$booksCount = count($books);
if ($booksCount > $limit) {
// Reverse iterate the original Book collection as I just want the last ones
for ($i = $booksCount; $i > $booksCount - $limit; $i--) {
$newBooksCollection->add($books->get($i - 1));
}
}
// Setter previously added to the Author entity to override its related Books
$object->setBooks($newBooksCollection);
$data = $this->normalizer->normalize($object, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof \App\Entity\Author;
}
}
And finally register the normalizer as a service manually (using autowire led me to circular reference issues):
services:
App\Serializer\Normalizer\AuthorNormalizer:
autowire: false
autoconfigure: true
arguments:
$normalizer: '#api_platform.jsonld.normalizer.item'
$iriConverter: '#ApiPlatform\Core\Api\IriConverterInterface'

Reference setting value in entity symfony

I have a doubt about code organization using symfony3 and doctrine: I'll try to explain as clear as I can. Let's say I have a FootballClub entity:
class FootballClub
{
// other code
private $memberships;
public function addMembership(Membership $membership) : FootballClub
{
$this->memberships[] = $membership;
return $this;
}
public function removeMembership(Membership $membership) : bool
{
return $this->memberships->removeElement($membership);
}
}
The entity is in a many-to-one relationship with another entity, Membership, which represents the contract a player has with the club. Let's say each club
has only a limited number of membership it can acquire, number that is represented as a setting, for example, as a property in a Setting entity.
The question is: how should I reference that setting when removing a membership from the club and check that is respected? Entities should not have any dependency, so what would be the correct way to implement this? A service? can you provide an example? Thank you for your time.
You could create a Settings entity, linked in OneToOne relation with FootballCluc entity.
Define Settings like this and instanciate it in the FootballClub's constructor
Settings entity
/** #Entity */
class Settings
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer")
*/
private $maxMembership;
// Other configurable properties ...
__constructor($maxMembership = 50)
{
$this->maxMembership = $maxMembership;
}
public function getMaxMembership()
{
return $this->maxMembership;
}
public function setMaxMembership($maxMembership)
{
$this->maxMembership = $maxMembership;
}
}
Football Entity
class FootballClub
{
/**
* One FootballClub has One Settings.
* #OneToOne(targetEntity="Settings")
* #JoinColumn(name="settings_id", referencedColumnName="id")
*/
private $settings;
// other code
private $memberships;
__constructor(Settings $settings = null)
{
if (null === $settings) {
$settings = new Settings();
}
$this->settings = $settings;
}
public function addMembership(Membership $membership) : FootballClub
{
if ($this->settings->getMaxMembership() <= count($this->memberships)) {
// throw new Exception("Max number of membership reached"); Strict mode
// return false // soft mode
}
$this->memberships-> = $membership;
return $this;
}
public function removeMembership(Membership $membership) : bool
{
return $this->memberships->removeElement($membership);
}
}

Add dynamic property on entity to be serialized

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

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