Symfony+JMS Serializer deserialize into existing object - symfony

I've been wondering if it's possible to use the JMS Serializer to deserialize JSON into an existing object.
Usually that would be useful for updating an existing object with new data that you have in a JSON format. Symfony's standard deserializer seems to offer that, but I can't seem to find anything about this with JMS. Have to use JMS though if I want the serializedName Annotation option.
The "workaround" is to deserialize and then use Doctrine's EntityManager to merge, but that only works so well, and you can't easily discern which fields are updated if the JSON doesn't contain every single field.

I have struggled to find the solution but finally found it and here we go:
your services.yaml
jms_serializer.object_constructor:
alias: jms_serializer.initialized_object_constructor
jms_serializer.initialized_object_constructor:
class: App\Service\InitializedObjectConstructor
arguments: ["#jms_serializer.unserialize_object_constructor"]
create class App\Service\InitializedObjectConstructor.php
<?php
declare(strict_types=1);
namespace App\Service;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
class InitializedObjectConstructor implements ObjectConstructorInterface
{
private $fallbackConstructor;
/**
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
*/
public function __construct(ObjectConstructorInterface $fallbackConstructor)
{
$this->fallbackConstructor = $fallbackConstructor;
}
/**
* {#inheritdoc}
*/
public function construct(
DeserializationVisitorInterface $visitor,
ClassMetadata $metadata,
$data,
array $type,
DeserializationContext $context
): ?object {
if ($context->hasAttribute('target') && 1 === $context->getDepth()) {
return $context->getAttribute('target');
}
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
}
in your controller or your service file
$object = $this->entityManager->find('YourEntityName', $id);
$context = new DeserializationContext();
$context->setAttribute('target', $object);
$data = $this->serializer->deserialize($request->getContent(), 'YourEntityClassName', 'json', $context);

So this can be done, I haven't fully worked out how, but I switched away from JMS again, so just for reference, since I guess it's better than keeping the question open for no reason:
https://github.com/schmittjoh/serializer/issues/79 and you might find more digging around the GitHub too.

Related

Symfony Serializer custom Denormalizer

I would like to decorate the ArrayDenormalizer for the Symfony serializer with a custom Denormalizer, but it seems it hits a Circular reference problem in the DependencyInjection since Nginx is crashing with a 502.
The custom Denormalizer implements DenormalizerAwareInterface so i actually expected that Symfony would handle the dependency injection automatically via Autowiring.
<?php
namespace App\Serializer;
use App\MyEntity;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class PreCheckRequestDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
if (in_array($data['locale'], ['de', 'en']) === false) {
$data['locale'] = 'en';
}
return $this->denormalizer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null)
{
return is_array($data) && $type === MyEntity::class;
}
}
What am i missing here?
Btw it is Symfony 6.1.
Seems this is a bug with autowiring of NormalizeAwareInterface in Symfony 6.1:
https://github.com/symfony/maker-bundle/issues/1252#issuecomment-1342478104
This bug led into a circular reference problem.
I solved it with not using the DenormalizerAwareInterface and DenormalizerAwareTrait and by disabling autowiring for the custom Denormalizer and declare the service explicitly:
App\Serializer\PreCheckRequestDenormalizer:
autowire: false
arguments:
$denormalizer: '#serializer.normalizer.object'
$allowedLocales: '%app.allowed_locales%'
$defaultLocale: '%env(string:DEFAULT_LOCALE)%'
The comments of the issue describe possible further other ways, but thsi solves it for me.

Symfony serialization: changing default DateTimeNormalizer format

I am trying to serialize an entity class which has some \DateTime fields. Everything works fine, but \DateTime objects are converted to string using the following format: "2019-10-21T01:05:12+00:00", while I would like to get just the date part: "2019-10-21".
Symfony documentation mentions default format but doesn't explain how to configure it:
DateTimeNormalizer This normalizer converts DateTimeInterface objects (e.g. DateTime and DateTimeImmutable) into strings. By default, it uses the RFC3339 format.
Is it possible to change the default DateTime normalization format and how?
Entity class:
class Fact
{
/**
* #ORM\Column(type="datetime", options={"default": "CURRENT_TIMESTAMP"})
* #Groups({"api"})
*/
private $created_on;
}
Normalization example:
use Symfony\Component\Serializer\SerializerInterface;
class FactController extends AbstractController
{
private $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
public function view($id)
{
....
$data = array(
'fact' => $this->serializer->normalize($fact, null, ['groups'=> 'api']),
);
...
}
}
It turns out that '$context' array is passed down to normalize() function of each supported Normalizer. Built-in Normalizers define array keys they accept and their default values.
The relevant key in my case is 'datetime_format', which defaults to \DateTime::RFC3339. Format must be the one accepted by \DateTime::format() and \DateTime::createFromFormat() methods - these functions are used for normalization / denormalization.
Correct usage in my case is:
public function view($id)
{
....
$data = array(
'fact' => $this->serializer->normalize($fact, null, ['groups'=> 'api',
'datetime_format' => 'Y-m-d']),
);
...
}
Here is an answer in another question. Solution is:
services:
serializer.normalizer.datetime:
class: ‘Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
arguments
-
!php/const Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::FORMAT_KEY: 'Y-m-d\TH:i:s.uP’
tags:
- { name: serializer.normalizer, priority: -910 }
Priority is taken from the original service, so, this solution will not have any side effects.

How can I make the entity field type available in silex?

I have been using Silex for my latest project and I was trying to follow along with the "How to Dynamically Modify Forms Using Form Events" in the Symfony cookbook. I got to the part that uses the entity field type and realized it is not available in Silex.
It looks like the symfony/doctrine-bridge can be added to my composer.json which contains the "EntityType". Has anyone successfully got entity type to work in Silex or run into this issue and found a workaround?
I was thinking something like this might work:
$builder
->add('myentity', new EntityType($objectManager, $queryBuilder, 'Path\To\Entity'), array(
))
;
I also found this answer which looks like it might do the trick by extending the form.factory but haven't attempted yet.
I use this Gist to add EntityType field in Silex.
But the trick is register the DoctrineOrmExtension form extension by extending form.extensions like FormServiceProvider doc says.
DoctrineOrmExtension expects an ManagerRegistry interface in its constructor, that can be implemented extending Doctrine\Common\Persistence\AbstractManagerRegistry as the follow:
<?php
namespace MyNamespace\Form\Extensions\Doctrine\Bridge;
use Doctrine\Common\Persistence\AbstractManagerRegistry;
use Silex\Application;
/**
* References Doctrine connections and entity/document managers.
*
* #author Саша Стаменковић <umpirsky#gmail.com>
*/
class ManagerRegistry extends AbstractManagerRegistry
{
/**
* #var Application
*/
protected $container;
protected function getService($name)
{
return $this->container[$name];
}
protected function resetService($name)
{
unset($this->container[$name]);
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported.');
}
public function setContainer(Application $container)
{
$this->container = $container['orm.ems'];
}
}
So, to register the form extension i use:
// Doctrine Brigde for form extension
$app['form.extensions'] = $app->share($app->extend('form.extensions', function ($extensions) use ($app) {
$manager = new MyNamespace\Form\Extensions\Doctrine\Bridge\ManagerRegistry(
null, array(), array('default'), null, null, '\Doctrine\ORM\Proxy\Proxy'
);
$manager->setContainer($app);
$extensions[] = new Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($manager);
return $extensions;
}));

How to use FOSRestBundle manual routes with CRUD from Symfony2?

I am using the FOSRestBundle to create a REST application but since REST features is only a part, I am also using some of Symfony2 built-in automation tools to generate my CRUD code. Everything works fine but I am unable to correctly map the route and I will appreciate some insight and example on how to do this manually. I have read the manual route definition in the FOS manual stating to use the given annotations but how do I do this since the CRUD code created by Symfony2 uses a different annotation?
Here is an example:
class UserController extends Controller
{
/**
* Lists all User entities.
*
* #Route("/", name="user")
* #Method("GET")
* #Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('CompanyWebServicesBundle:User')->findAll();
return array(
'entities' => $entities,
);
}
FOSRest manual gives the annotation for GET as
use FOS\RestBundle\Controller\Annotations\Get;
/**
* GET Route annotation.
* #Get("/likes/{type}/{typeId}")
*/
When I use the route as /index, it gives me an error and my route definition in config.yml is:
index:
type: rest
resource: Company\WebservicesBundle\Controller\UserController
How can I fix this problem?
If I were you, I would create separate bundles for your REST controllers and your generic CRUD controllers. This will make things easier to maintain (in my opinion). For example, create a AcmeMainBundle and a AcmeRestBundle, and then create a separate class to actually perform the actions that you will call from both bundles. Something like this:
// src/Acme/MainBundle/Crud/User.php (create this folder structure)
class User{
private $em;
public function __construct($em){
$this->em = $em;
}
public function getUser($id){
return $this->em->getRepository('AcmeMainBundle:User')->find($id);
}
}
Then:
// src/Acme/MainBundle/Controller/UserController.php
use Symfony\Component\HttpFoundation\Request;
use Acme\MainBundle\Crud\User;
class UserController extends Controller {
public function getAction($request){
$em = $this->getDoctrine()->getManager();
$getUser = new User($em);
$user = $getUser ->getUser($request->query->get('user_id'));
// return response
}
}
And then:
// src/Acme/RestBundle/Controller/UserController.php
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Routing\ClassResourceInterface;
use Symfony\Component\HttpFoundation\Request;
class UserController extends Controller implements ClassResourceInterface {
/**
* #Rest\View()
*/
public function getAction($id){
$em = $this->getDoctrine()->getManager();
$getUser = new User($em);
$user = $getUser ->getUser($id);
// return using the default format defined in config.yml
return array(
"success"=>'true',
"user" => $user
);
} // get_user [GET] /users/{id}
}
Please note that using the ClassResourceInterface means your method names will be used to generate the routes. see FOSRestBundle Docs for more info on that.
You can do something similar to this for all your CRUD, that way you keep your routes separate and maintainable, but still have a single code base to update.

Symfony2 / Doctrine - Modifying all queries

Is it possible to run all doctrine queries through a walker of some sort so that I can modify the query based on the current user's credentials? Ideally, I wouldn't have to explicitly call a setHint for a custom walker on every query, as that would restrict my ability to pass the current SecurityContext into the walker.
Also, I'd prefer not to use a Doctrine Filter, as I can't modify join conditions with filters, and I'd be forced to use an "IN" clause, which would severely affect performance
Currently, I'm using a service that modifies the QueryBuilder based on a user's credentials, but this becomes tedious, as I need to call the service every time I create a new QueryBuilder, and is even more of a pain when Repositories come into play (as I'd need to inject the service into every repository that needs to modify the query.
Hopefully I've explained this clearly enough. Appreciate any feedback!
I think I have solved my own issue. If someone else has a more elegant way of doing achieving these results, feel free to explain. In order to modify all of my queries, I have created a custom EntityManager and custom EntityRepository.
In my custom EntityManager, I have overwritten 2 methods. create() and getRepository()
public static function create($conn, Configuration $config, EventManager $eventManager = null)
{
if ( ! $config->getMetadataDriverImpl()) {
throw ORMException::missingMappingDriverImpl();
}
switch (true) {
case (is_array($conn)):
$conn = \Doctrine\DBAL\DriverManager::getConnection(
$conn, $config, ($eventManager ?: new EventManager())
);
break;
case ($conn instanceof Connection):
if ($eventManager !== null && $conn->getEventManager() !== $eventManager) {
throw ORMException::mismatchedEventManager();
}
break;
default:
throw new \InvalidArgumentException("Invalid argument: " . $conn);
}
return new MyCustomEntityManager($conn, $config, $conn->getEventManager());
}
The only thing that is changed in this method is that I am returning my own EntityManger(MyCustomEntityManager). Then, I overlaid the getRepository method as follows:
public function getRepository($entityName)
{
$entityName = ltrim($entityName, '\\');
if (isset($this->repositories[$entityName])) {
return $this->repositories[$entityName];
}
$metadata = $this->getClassMetadata($entityName);
$repositoryClassName = $metadata->customRepositoryClassName;
if ($repositoryClassName === null) {
$repositoryClassName = "Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository";
}
$repository = new $repositoryClassName($this, $metadata);
$this->repositories[$entityName] = $repository;
return $repository;
}
Here, I have only modified one line as well. Instead of relying on the DBAL Configuration to retreive the default $repositoryClassName, I have specified my own default repository Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository.
Once you have created your own custom EntityRepository, the sky is the limit. You can inject services into the repository(I currently use JMS Di annotations, described below), or perform custom actions against a QueryBuilder in the createQueryBuilder method, like so:
use JMS\DiExtraBundle\Annotation as DI;
class MyCustomEntityRepository extends EntityRepository
{
private $myService;
public function createQueryBuilder($alias)
{
$queryBuilder = parent::createQueryBuilder($alias);
/** INSERT CUSTOM CODE HERE **/
return $queryBuilder;
}
/**
* #DI\InjectParams({
* "myService" = #DI\Inject("my_service_id")
* })
*/
public function setMyService(MyServiceInterface $myService)
{
$this->myService = $myService;
}
}
Once you have created your own EntityRepository, you should have all of your repositories that need this custom functionality extend MyCustomEntityRepository. You could even take it a step further and create your own QueryBuilder to further extend this.
You can write a custom AST Walker and setup your application to use this walker for all queries with defaultQueryHint (Doctrine 2.5 new feature) configuration option:
<?php
/** #var \Doctrine\ORM\EntityManager $em */
$em->getConfiguration()->setDefaultQueryHint(
Query::HINT_CUSTOM_TREE_WALKERS,
['YourWalkerFQClassName']
)

Resources