Programmatically query Symfony / Doctrine ORM - symfony

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.

Related

How do I Create a ManyToOne Relationship Between a Doctrine Entity and a Custom Data Provider?

I'm currently building a web application, and went for Symfony 4 along with API Platform.
I built a custom data provider in order to pull data from a XML file, for an entity. Since it's all one-way operations, I only enabled GET operations for both items and collections.
I am trying to tie the entity served by this custom data provider to a Doctrine entity, but I'm getting an error saying that the entity is not a valid one or mapped super class.
How do I create a relationship between these two? Is it even possible?
Thanks!
This is a snippet from the aforementioned entity:
<?php
//src/Entity/Sst.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
/**
* #ApiResource(
* collectionOperations={"get"={"method"="GET"}},
* itemOperations={"get"={"method"="GET"}}
* )
*/
class Sst
{
public $code_urssaf;
/**
* #ApiProperty(identifier=true)
*/
public $code_sst;
// ...and a few others
public function getCodeUrssaf(): ?string
{
return $this->code_urssaf;
}
public function setCodeUrssaf(string $code_urssaf): self
{
$this->code_urssaf = $code_urssaf;
return $this;
}
public function getCodeSst(): ?string
{
return $this->code_sst;
}
public function setCodeSst(string $code_sst): self
{
$this->code_sst = $code_sst;
return $this;
}
// and so on; this is generated then tuned with Symfony's MakerBundle.
Here's a bit from the collection data provider, with imports omitted (but everything works when querying the API directly):
final class SstCollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
public function __construct(FilesystemInterface $extdataStorage, SerializerInterface $serializer)
{
$this->serializer = $serializer;
$this->storage = $extdataStorage;
}
public function supports(string $resourceClass, string $operationName = null, array $context = [] ): bool
{
return Sst::class === $resourceClass;
}
public function getCollection(string $resourceClass, string $operationName = null): \Generator
{
$sstfile = $this->storage->read('SST_29072019.xml');
$sstlist = $this->serializer->deserialize($sstfile, SstCollection::class, 'xml', array('object_to_populate' => $sstlist = new SstCollection()));
foreach($sstlist as $sstObject)
{
yield $sstObject;
}
}
}
The Doctrine entity has this, mirroring other relationships:
/**
* #var Sst[]
*
* #ORM\ManyToOne(targetEntity="App\Entity\Sst")
*/
private $sst;
I expect to be able to tie the custom entity to the Doctrine one, but I cannot even start the Symfony app, I'm getting:
In MappingException.php line 346:
Class "App\Entity\Sst" is not a valid entity or mapped super class.

How to use JMSSerializer with symfony 4.2

i am building an Api with symfony 4.2 and want to use jms-serializer to serialize my data in Json format, after installing it with
composer require jms/serializer-bundle
and when i try to use it this way :
``` demands = $demandRepo->findAll();
return $this->container->get('serializer')->serialize($demands,'json');```
it gives me this errur :
Service "serializer" not found, the container inside "App\Controller\DemandController" is a smaller service locator that only knows about the "doctrine", "http_kernel", "parameter_bag", "request_stack", "router" and "session" services. Try using dependency injection instead.
Finally i found the answer using the Symfony serializer
it's very easy:
first : istall symfony serialzer using the command:
composer require symfony/serializer
second : using the serializerInterface:
.....//
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
// .....
.... //
/**
* #Route("/demand", name="demand")
*/
public function index(SerializerInterface $serializer)
{
$demands = $this->getDoctrine()
->getRepository(Demand::class)
->findAll();
if($demands){
return new JsonResponse(
$serializer->serialize($demands, 'json'),
200,
[],
true
);
}else{
return '["message":"ooooops"]';
}
}
//......
and with it, i don't find any problems with dependencies or DateTime or other problems ;)
As I said in my comment, you could use the default serializer of Symfony and use it injecting it by the constructor.
//...
use Symfony\Component\Serializer\SerializerInterface;
//...
class whatever
{
private $serializer;
public function __constructor(SerializerInterface $serialzer)
{
$this->serializer = $serializer;
}
public function exampleFunction()
{
//...
$data = $this->serializer->serialize($demands, "json");
//...
}
}
Let's say that you have an entity called Foo.php that has id, name and description
And you would like to return only id, and name when consuming a particular API such as foo/summary/ in another situation need to return description as well foo/details
here's serializer is really helpful.
use JMS\Serializer\Annotation as Serializer;
/*
* #Serializer\ExclusionPolicy("all")
*/
class Foo {
/**
* #Serializer\Groups({"summary", "details"})
* #Serializer\Expose()
*/
private $id;
/**
* #Serializer\Groups({"summary"})
* #Serializer\Expose()
*/
private $title;
/**
* #Serializer\Groups({"details"})
* #Serializer\Expose()
*/
private $description;
}
let's use serializer to get data depends on the group
class FooController {
public function summary(Foo $foo, SerializerInterface $serialzer)
{
$context = SerializationContext::create()->setGroups('summary');
$data = $serialzer->serialize($foo, json, $context);
return new JsonResponse($data);
}
public function details(Foo $foo, SerializerInterface $serialzer)
{
$context = SerializationContext::create()->setGroups('details');
$data = $serialzer->serialize($foo, json, $context);
return new JsonResponse($data);
}
}

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.

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

How to set a default entity relation in a property?

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]

Resources