How to use JMSSerializer with symfony 4.2 - symfony

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

Related

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'

API Platform documentation for custom normalizer

I am using API Platform and I followed this tutorial to add a custom serialized field which relies on an external service. The avatar property needs to be exposed using the Packages class.
<?php
namespace App\Serializer;
use App\Entity\User;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\UrlHelper;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class UserNormalizer implements ContextAwareNormalizerInterface
{
/**
* #var Packages
*/
private $packages;
/**
* #var UrlHelper
*/
private $urlHelper;
/**
* #var ObjectNormalizer
*/
private $normalizer;
public function __construct(Packages $packages, UrlHelper $urlHelper, ObjectNormalizer $normalizer)
{
$this->packages = $packages;
$this->normalizer = $normalizer;
$this->urlHelper = $urlHelper;
}
public function normalize($user, $format = null, array $context = [])
{
/** #var array */
$data = $this->normalizer->normalize($user, $format, $context);
$avatar = null;
if ($user->getAvatarFilename()) {
$path = $this->packages->getUrl('uploads/avatars/'.$user->getAvatarFilename());
$avatar = $this->urlHelper->getAbsoluteUrl($path);
}
$data['avatar'] = $avatar;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof User;
}
}
The problem is that this property doesn't appear in the documentation as it's added by the custom normalizer. How can I add documentation for it (eg. type, example etc...)?
if this still relevant for you or somebody else:
You can add a custom field to the openapi model with this: https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification
You need to add $avatar field to your entity.
Like you suggest in the comments you could add a not mapped property to your entity and document it in the annotations, and yes it's hacky like they said here!... it is suggested in the SymfonyCast tutorials
Just remember the downside to this approach: our documentation has no
idea that this isMe field exists. If we refresh this page and open the
docs for fetching a single User... yep! There's no mention of isMe. Of
course, you could add a public function isMe() in User, put it in the
user:read group, always return false, then override the isMe key in
your normalizer with the real value. That would give you the custom
field and the docs. But sheesh... that's... getting kinda hacky.

symfony no supporting normalizer found while normalizing an entity

I'm working on symfony 4.1.
I defined two normalizer in my service.yml.
api.tone_normalizer:
class: App\Serializer\Normalizer\JnToneNormalizer
tags: [serializer.normalizer]
and
api.wskeytone_normalizer:
class: App\Serializer\Normalizer\ApiWsKeyToneToneNormalizer
tags: [serializer.normalizer]
Here the first normalizer. Is aware about JnTone entities.
<?php
namespace App\Serializer\Normalizer;
use App\Entity\JnTone;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* JnTone normalizer
*/
class JnToneNormalizer implements NormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'id' => $object->getId(),
'name' => $object->getName(),
];
}
/**
* {#inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof JnTone;
}
}
And the normalizer where I want to call the first one. rootTone is an instance of JnTone entity so I want to call my JnTone normalizer.
<?php
namespace App\Serializer\Normalizer;
use App\Entity\JnWsKey;
use App\Entity\JnTone;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
use Symfony\Component\Serializer\SerializerAwareTrait;
use Symfony\Component\Serializer\SerializerAwareInterface;
/**
* JnWsKey normalizer
*/
class ApiWsKeyNormalizer implements NormalizerInterface, SerializerAwareInterface
{
use NormalizerAwareTrait;
use SerializerAwareTrait;
private $tones;
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'id'=>$object->getId(),
'name'=>$object->getName(),
'rootTone'=>$this->serializer->normalize($object->getRootTone(),$format,$context)
];
}
/**
* {#inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof JnWsKey ;
}
}
I can't get this working. The first normalizer isn't find
Could not normalize object of type App\Entity\JnTone, no supporting normalizer found.
What I am doing wrong?
I just did not realize that I have to declare all needed normailizer in the serializer definition. I solved it doing:
$encoder = new JsonEncoder();
$serializer = new Serializer(array(
new JnToneNormalizer(),
new JnWsKeyToneNormalizer()
), array($encoder));
Symfony seems to have ObjectNormalizer. I think you may take advantage of it. Check the installation and usage. I think there is also a way to perform serialization of complex nested objects using annotations and groups.

Testion controler method in symfony (phpUnit)

i need some help i want to write a unit test about a controler method , i have searched for examples and tested a lot of method's but none of them has worked:
Here is my controller:
class ComputerController extends Controller
{
/**
* #Route("/list-computers.html", name="back_computer_list")
* #return RedirectResponse|Response
*/
function listComputerAction()
{
$ad = $this->get("ldap_service");
$computers = $ad->getAllComputer();
return $this->render('BackBundle:Computer:list.html.twig', array(
"computers" => $computers,
));
}
I have tried to test it with mock like this:
class ComputerController extends Controller
{
/**
* #var EngineInterface
*/
private $templating;
public function setTemplating($templating)
{
$this->templating = $templating;
}
and i have created a test method:
class ComputerControllerTest extends TestCase {
public function testlistComputerAction(){
$templating = $this->getMockBuilder('BackBundle\Controller\ComputerController')->getMock();
$computers = [1,2];
$templating->expects($this->once())
->method('render')
->with('BackBundle:Computer:list.html.twig', array(
"computers" => $computers))
->will($this->returnValue( $computers));
$controller = new ComputerController();
$controller->setTemplating($templating);
$this->assertEquals('success', $controller->listComputerAction());
}
When i start executing phpunit , i have this warning"Trying to configure method "render" which cannot be configured because it does not exist, has not been specified, is final, or is static"
I would be thankful if someone has an idea about this
I tried to Test a method in ldapService : Here is the method's of the service that i want to test
/**
* #return bool|resource
*/
public function getLdapBind()
{
if (!$this->ldapBind) {
if ($this->getLdapConnect()) {
$this->ldapBind = #ldap_bind($this->ldapConnect, $this->ldapUser, $this->ldapPass);
}
}
return $this->ldapBind;
}
/**
* #param $ldapUser
* #param $password
* #return bool
*/
function isAuthorized($ldapUser, $password)
{
$result = false;
if ($this->ldapConnect) {
$result = #ldap_bind($this->ldapConnect, $ldapUser, $password);
}
return $result;
}
Here is the test (using Mock):
<?php
namespace BackBundle\Tests\Service;
use PHPUnit\Framework\TestCase;
use BackBundle\Service\LdapService;
use PHPUnit_Framework_MockObject_InvocationMocker;
class LdapServiceTest extends TestCase {
public function testgetLdapConnect()
{
// $LdapService = new LdapService();
$ldapMock = $this->getMockBuilder( 'LdapService')->setMethods(['getLdapBind'])->disableOriginalConstructor()->getMock();
$ldapMock->expects($this->once())
// ->method()
->with(array('ldap_bind', 'mike', 'password'))
->will($this->returnValue(true));
$ldapMock->isAuthorized('mike', 'password');
}
}
But i have a warning that i can't resolve : "Method name matcher is not defined, cannot define parameter matcher without one"
If someone , has an idea about that please
Honestly, there is nothing useful to test in that three-line controller. #1 is the service container, and #3 is the Twig subsystem. Line #2 can be unit tested on it's own.
With more complex controllers, I have found that making them a service where all the dependencies are passed in, either by constructor, or into the action itself does make slightly more complex controllers quite easy, but very few need that anyway.

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