In my Symfony 6 project I use the symfony Serializer with this configuration.
# config/packages/framework.yaml
framework:
serializer:
name_converter: 'serializer.name_converter.camel_case_to_snake_case'
enable_annotations: true
default_context:
datetime_format: Y-m-d
annotations:
enabled: true
I was expecting that #[Ignore] attribute excludes the fields from json. But it has no effect.
class Event {
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
#[Ignore]
private $id;
...
}
I have also tried to define the serializer in the controller based on attributes-groups and ignoring-attributes but still the result is the same.
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$serializer = new Serializer([new ObjectNormalizer($classMetadataFactory)],[new JsonEncoder()]);
return new JsonResponse($serializer->serialize($events,'json'));
What did i miss, or why it does not exlude the fields based on Ignore attribute?
Is it possible to only use the yml configuration to activate the Ignore attribute?
The "datetime_format" changes the output datetime format, but "name_converter" also does not change the field names from camelCase to sanke_case.
I guess the reason could be that GetSetMethodNormalizer has higher priority compare to ObjectNormalizer and it does not use the serilizer configuration.
your configuration is correct, you can use this command to verify your configuration:
php bin/console debug:config framework serializer
if use serializer by youself please set nameConverter in second argumant of ObjectNormalizer. see code withoutContainer function`
but if you want use symfony configuration you should use service in container so use this style see code withContainer function.
<?php
namespace App\Controller;
use App\Entity\Event;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
class TestController extends AbstractController
{
#[Route('/without-container')]
public function withoutContainer(): Response
{
$event = new Event();
$event->setName('Published Email');
$event->setIsPublished(true);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
$serializer = new Serializer([$normalizer],[new JsonEncoder()]);
return new JsonResponse($serializer->serialize($event,'json'));
//"{\"name\":\"Published Email\",\"is_published\":true}"
}
#[Route('/with-container', name: 'app_home')]
public function withContainer(SerializerInterface $serializer): Response
{
$event = new Event();
$event->setName('Published Email');
$event->setIsPublished(true);
return new JsonResponse($serializer->serialize($event,'json'));
// "{\"name\":\"Published Email\",\"is_published\":true}"
}
}
Related
I can't for the life of me figure out how to handle a regular deserialization. I've read dozens of SO questions and also the official doc and it seems to be easy. Seems.
I've got a simple JSON, like:
[{"id":"00112063002463454431","first_name":"John","last_name":"Doe","date_of_birth":"2006-09-28"}]
Now I'd like to map it to my class Person. No matter what I've tried, it always complains about date_of_birth to be string. It is expected to be DateTimeInterface when the routine internally calls setDateOfBirth(?DateTimeInterface $dateOfBirth) inside the Person class. But in my understanding DateTimeNormalizer's denormalize() should've already converted it to a DateTime object before it hydrates the Person object, shouldn't it?
Inside my class the field is defined as follows:
#[ORM\Column(type: Types::DATE_MUTABLE)]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeInterface $dateOfBirth = null;
Deserializing process:
$serializer = new Serializer(
[new DateTimeNormalizer(), new GetSetMethodNormalizer(), new ArrayDenormalizer()],
[new JsonEncoder()]
);
$personsFromJson = $serializer->deserialize($requestContent, 'App\Entity\Person[]', 'json');
Is there anything else to do?!
Edit
One-class example:
<?php
namespace App\Controller;
use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
class TestController extends AbstractController
{
#[Route(path: '/test', name: 'app_test')]
public function index(): Response
{
$json = '{"dateOfBirth":"2023-01-31"}';
$serializer = new Serializer(
[new DateTimeNormalizer(), new GetSetMethodNormalizer()],
[new JsonEncoder()]
);
$testPersonFromJson = $serializer->deserialize($json, TestPerson::class, 'json');
return $this->json($testPersonFromJson);
}
}
class TestPerson {
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeInterface $dateOfBirth = null;
public function getDateOfBirth(): ?DateTimeInterface {
return $this->dateOfBirth;
}
public function setDateOfBirth(?DateTimeInterface $dateOfBirth): self {
$this->dateOfBirth = $dateOfBirth;
return $this;
}
}
App\Controller\TestPerson::setDateOfBirth(): Argument #1
($dateOfBirth) must be of type ?DateTimeInterface, string given,
called in
[...]\vendor\symfony\serializer\Normalizer\GetSetMethodNormalizer.php
on line 163
The DateTimeNormalizer() gets instantiated, but indeed, denormalize() gets never called.
I had to add a ReflectionExtractor to my ObjectNormalizer. I didn't notice this anywhere.
new ObjectNormalizer(null, null, null, new ReflectionExtractor())
https://symfony.com/doc/current/components/property_info.html#reflectionextractor
Found via the comments after my 18567th search at: Symfony Serialize Entity with Datetime / Deserialize fails
Coming from a NodeJS environment, this seems like a nobrainer but I somehow did not figured it out.
given the function:
/**
* #Route("/", name="create_stuff", methods={"POST"})
*/
public function createTouristAttraction($futureEntity): JsonResponse
{
...
}
Let futureEntity have the same structure as my PersonEntity.
What is the best way of mapping that $futureEntity to a PersonEntity?
I tried to assign it manually, and then run my validations which seems to work, but i think this is cumbersome if a model has more than 30 fields...
Hint: Im on Symfony 4.4
Thank you!
Doc: How to process forms in Symfony
You need to install the Form bundle: composer require symfony/form (or composer require form if you have the Flex bundle installed)
Create a new App\Form\PersonType class to set the fields of your form and more: doc
In App\Controller\PersonController, when you instanciate the Form, just pass PersonType::class as a first parameter, and an new empty Person entity as a second one (the Form bundle will take care of the rest):
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
The whole controller code:
<?php
namespace App\Controller;
use App\Entity\Person;
use App\Form\PersonType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class PersonController extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
}
/**
* #Route("/person/new", name="person_new")
*/
public function new(Request $request): Response
{
$person = new Person(); // <- new empty entity
$form = $this->createForm(PersonType::class, $person);
// handle request (check if the form has been submited and is valid)
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$person = $form->getData(); // <- fill the entity with the form data
// persist entity
$this->entityManager->persist($person);
$this->entityManager->flush();
// (optional) success notification
$this->addFlash('success', 'New person saved!');
// (optional) redirect
return $this->redirectToRoute('person_success');
}
return $this->renderForm('person/new.html.twig', [
'personForm' => $form->createView(),
]);
}
}
The minimum to display your form in templates/person/new.html.twig: just add {{ form(personForm) }} where you want.
i have this controller
namespace InicioBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use EntidadesBundle\Entity\Usuarios;
use Symfony\Component\HttpFoundation\Session\Session;
class DefaultController extends Controller
{
private $session;
public function __construct(){
$this->session = new Session();
}
.....
public function ver_rol($rol){
if($this->sacarRol() === $rol){
return true;
}else{
return false;
}
}
}
and in the services.yml , i got this:
parameters:
#parameter_name: value
services:
app.rolSession:
class: InicioBundle\Controller\DefaultController
arguments: ["i dont know how get paramets"]
the problem is that it doesnt work, symfony return an error FileLoaderLoadException, that the services.yml does not caontain valid YAML
There is a space before parameters: in your services.yml file, maybe remove that and your yaml should be valid.
Also if you are passing no arguments to constructor you can just delete arguments: ["null"]
One more thing, IIRC you need to add FQCN as class name, so class: InicioBundle\Controller\Default => class: InicioBundle\Controller\DefaultController
While we are at the subject, you can type hint Request in your action and use it to getSession() or maybe inject #session service to your controller
While using Symfony 3.3, I am declaring a service like this:
class TheService implements ContainerAwareInterface
{
use ContainerAwareTrait;
...
}
Inside each action where I need the EntityManager, I get it from the container:
$em = $this->container->get('doctrine.orm.entity_manager');
This is a bit annoying, so I'm curious whether Symfony has something that acts like EntityManagerAwareInterface.
Traditionally, you would have created a new service definition in your services.yml file set the entity manager as argument to your constructor
app.the_service:
class: AppBundle\Services\TheService
arguments: ['#doctrine.orm.entity_manager']
More recently, with the release of Symfony 3.3, the default symfony-standard-edition changed their default services.yml file to default to using autowire and add all classes in the AppBundle to be services. This removes the need for adding the custom service and using a type hint in your constructor will automatically inject the right service.
Your service class would then look like the following:
use Doctrine\ORM\EntityManagerInterface;
class TheService
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
// ...
}
For more information about automatically defining service dependencies, see https://symfony.com/doc/current/service_container/autowiring.html
The new default services.yml configuration file is available here: https://github.com/symfony/symfony-standard/blob/3.3/app/config/services.yml
Sometimes I inject the EM into a service on the container like this in services.yml:
application.the.service:
class: path\to\te\Service
arguments:
entityManager: '#doctrine.orm.entity_manager'
And then on the service class get it on the __construct method.
Hope it helps.
I ran into the same issue and solved it by editing the migration code.
I replaced
$this->addSql('ALTER TABLE user ADD COLUMN name VARCHAR(255) NOT NULL');
by
$this->addSql('ALTER TABLE user ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT "-"');
I don't know why bin/console make:entity doesn't prompt us to provide a default in those cases. Django does it and it works well.
So I wanted to answer your subquestion:
This is a bit annoying, so I'm curious whether Symfony has something
that acts like EntityManagerAwareInterface.
And I think there is a solution to do so (I use it myself).
The idea is that you slightly change your kernel so tha it checks for all services which implement the EntityManagerAwareInterface and injects it for them.
You can also add write an EntityManagerAwareTrait that implements the $entityManager property and the setEntityManager()setter. The only thing left after that is to implement/use the interface/trait couple the way you would do for the Logger for example.
(you could have done this through a compiler pass as well).
<?php
// src/Kernel.php
namespace App;
use App\Entity\EntityManagerAwareInterface;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use function array_key_exists;
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container): void
{
$definitions = $container->getDefinitions();
foreach ($definitions as $definition) {
if (!$this->isAware($definition, EntityManagerAwareInterface::class)) {
continue;
}
$definition->addMethodCall('setEntityManager', [$container->getDefinition('doctrine.orm.default_entity_manager')]);
}
}
private function isAware(Definition $definition, string $awarenessClass): bool
{
$serviceClass = $definition->getClass();
if ($serviceClass === null) {
return false;
}
$implementedClasses = #class_implements($serviceClass, false);
if (empty($implementedClasses)) {
return false;
}
if (array_key_exists($awarenessClass, $implementedClasses)) {
return true;
}
return false;
}
}
The interface:
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\EntityManagerInterface;
interface EntityManagerAwareInterface
{
public function setEntityManager(EntityManagerInterface $entityManager): void;
}
The trait:
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\EntityManagerInterface;
trait EntityManagerAwareTrait
{
/** #var EntityManagerInterface */
protected $entityManager;
public function setEntityManager(EntityManagerInterface $entityManager): void
{
$this->entityManager = $entityManager;
}
}
And now you can use it:
<?php
// src/SomeService.php
declare(strict_types=1);
namespace App;
use Exception;
use App\Entity\EntityManagerAwareInterface;
use App\Entity\Entity\EntityManagerAwareTrait;
use App\Entity\Entity\User;
class SomeService implements EntityManagerAwareInterface
{
use EntityManagerAwareTrait;
public function someMethod()
{
$users = $this->entityManager->getRepository(User::Class)->findAll();
// ...
}
}
I'm starting with FOSRestBundle. I have added this routing configuration:
//Sermovi/Bundle/APIBundle/Resources/config/routing.yml
sermovi_api_homepage:
type: rest
resource: Sermovi\Bundle\APIBundle\Controller\DefaultController
//app/config/routing.yml
sermovi_api:
type: rest
prefix: /api
resource: "#SermoviAPIBundle/Resources/config/routing.yml"
And this config.yml
fos_rest:
routing_loader:
default_format: json
view:
view_response_listener: true
sensio_framework_extra:
view:
annotations: false
And this controller:
namespace Sermovi\Bundle\APIBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Response;
class DefaultController extends FOSRestController
{
public function getArticlesAction()
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('SermoviManagementBundle:Transaction')->find(776);
return array(
'entity' => $entity
);
}
}
And I'm getting this error:
[{"message":"The controller must return a response (Array(entity =>
Object(Sermovi\Bundle\ManagementBundle\Entity\Transaction))
given).","class":"LogicException","trace":[{"namespace":"","short_class":"","class":"","type":"","function":"","file":"/home/tirengarfio/workspace/sermovi/app/bootstrap.php.cache","line":2855,"args":[]},{"namespace":"Symfony\Component\HttpKernel","short_class":"HttpKernel","class":"Symfony\Component\HttpKernel\HttpKernel","type":"->","function":"handleRaw","file":"/home/tirengarfio/workspace/sermovi/app/bootstrap.php.cache","line":2817,"args":[["object","Symfony\Component\HttpFoundation\Request"],["string","1"]]},{"namespace":"Symfony\Component\HttpKernel","short_class":"HttpKernel","class":"Symfony\Component\HttpKernel\HttpKernel","type":"->","function":"handle","file":"/home/tirengarfio/workspace/sermovi/app/bootstrap.php.cache","line":2946,"args":[["object","Symfony\Component\HttpFoundation\Request"],["string","1"],["boolean",true]]},{"namespace":"Symfony\Component\HttpKernel\DependencyInjection","short_class":"ContainerAwareHttpKernel","class":"Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel","type":"->","function":"handle","file":"/home/tirengarfio/workspace/sermovi/app/bootstrap.php.cache","line":2247,"args":[["object","Symfony\Component\HttpFoundation\Request"],["string","1"],["boolean",true]]},{"namespace":"Symfony\Component\HttpKernel","short_class":"Kernel","class":"Symfony\Component\HttpKernel\Kernel","type":"->","function":"handle","file":"/home/tirengarfio/workspace/sermovi/web/app_dev.php","line":28,"args":[["object","Symfony\Component\HttpFoundation\Request"]]}]}]
EDIT:
"Could" I do something like this below? Or since FOSRestBundle is using JMSSerializerBundle I should not do it?
$serializedEntity = $this->container->get('serializer')->serialize($entity, 'json');
return new Response($serializedEntity);
There are several ways to setup your Controllers with FOSRestBundle. The way you are doing it, you must return a view. Here is a link to a rest controller on github that may help you out a bit. LiipHelloBundle has an example.
Also, I found it easiest to use the ClassResourceInterface in my controllers. This way, I return an array, and it handles all the serialization itself. It also uses your Controller name to generate the routes that are necessary, so I don't have to manually define any routes. It is my preferred way of setting up the Controller. See the doc entry here for how that works.
If you do end up using the ClassResourceInterface, be sure to include the following annotation for each action, it will make it so your returned array is serialized properly:
use FOS\RestBundle\Controller\Annotations as Rest;
//.....
/**
* #Rest\View()
*/
public function cgetAction() {}
You might even be able to do that with the way you are setting up the controller, but I haven't tried that before. Let us know if you go that way and it works.
UPDATE
For those who may be interested in using the FOSRestBundle without using the ClassResourceInterface, the problem with the controller action in the question is that it does not return a view. This should work in the action:
class DefaultController extends FOSRestController
{
public function getArticlesAction()
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('SermoviManagementBundle:Transaction')->find(776);
$statusCode = 200;
$view = $this->view($entity, $statusCode);
return $this->handleView($view);
}
}