Complex serialization - symfony

For example, I have two entities: main (parent) entity (for example User) and dependent entity (Post). I want to serialize User entity using JMS Serializer with additional information of its first post date. The post date stored in DB is timestamp int, but I want to serialize it with my helper (just a service) which converts int to sting with some formatting.
Trying to create a virtual_property with method at entity class, but failed to inject my helper into entity class. The only way to solve it for me is to serialize by myself into controller:
public function someAction()
{
$serializedUser = $this->serializeEntity($user);
}
public function serializeEntity(User $user)
{
// JMS serialization
$user = $this->get('jms_serializer')->serialize($user, 'array');
if ($user->getPosts()->count()) {
$post = $user->getPosts()->first();
$user['first_post_date'] = $this->get('my_helper_service')->dateFormat($post->getDate());
}
return $user;
}
NB: this example is synthetic, in the real world I have more complex methods, not just date formatter. But the main idea is the same.
I feel there should be better way to do this.

Dmtry's solution should work perfectly for your case.
An event listener/subscriber is the best solution in this case.
A bit more general solution, that works even with objects and will trigger the whole event system part of the JMS serializer (Dmtry's solution works only with primitives and only for JSON/YAML, not XML), is:
class MyFormatter implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
'class' => 'YourEntity'
),
);
}
public function __construct(MyFormatter $foermatter)
{
$this->formatter = $formatter;
}
public function onPostSerialize(ObjectEvent $event)
{
$visitor = $event->getVisitor();
$context = $event->getContext();
$timestamp = $event->getObject()->getTimestamp();
$formattedTime = $this->formatter->format($timestamp);
$metadata = new StaticPropertyMetadata('stdClass', 'first_post_date', $formattedTime);
$visitor->visitProperty($metadata, $formattedTime, $context);
}
}
stdClass is ignored by the serializer...

Of course, there is a better way. It's called serialization events.
http://jmsyst.com/libs/serializer/master/event_system
You create event subscriber
my_bundle.serializer_subscriber:
class: MyBundle\Serializer\MyEntitySerializerSubscriber
arguments:
- #bundle.time_formatter
tags:
- { name: jms_serializer.event_subscriber }
And then just add the data you need in your listener
public function myOnPostSerializeMethod(ObjectEvent $event)
{
if (!($event->getObject() instance of YourEntity)) {
return;
}
$timestamp = $event->getObject()->getTimestamp();
$visitor = $event->getVisitor();
$visitor->addData('date', $this->formatter->format($timestamp));
}
P.S. I didn't check the code, so maybe I'm mistaken somewhere with name of methods, but the idea is clear, I hope

Related

Prevent the doctrine postLoad event for some cases

I have an entity BlogPost with a status property. This status property depends on an external API call which is handled via the doctrine postLoad event. All other properties are stored in the local database.
public function postLoad(BlogPost $post)
{
$this->postHandler->calculateStatus($post);
}
The problem is, in some cases i don't want to calculate the status at all. For example if i want to get only the description of all blogposts.
With the code above, all blog entities being loaded will trigger the postLoad event even if i just want to have values from a local database. That is very expensive and not acceptable.
So for example in my repository class i want to get all BlogPosts having a website without invoking the postLoad event.
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getResult();
}
Is there a way to say "Yes, load the BlogPost collection, but do not fire event!" ???
Any other approaches? Custom event?
Thanks
Why don't just move this logic outside the post entity and event listener? If you know when you need to calculate the status you can do it explicitly.
For example
$post = $this->entityManager->find(BlogPost::class, $postId);
$status = $this->postHandler->calculateStatus($post);
The other approach I could suggest is not good but works. You could use lazy calculation and instead of calling $this->postHandler->calculateStatus($this) in postLoad event listener you could inject postHandler service into entity and perform the calculation in the moment you actually need it.
For example if you need calculation when calling $blogPost->getStatus() method, you could do it this way:
interface PostHandlerAwareInterface
{
public function setPostHandler(PostHandlerInterface $postHandler): void;
}
class EntityServiceInjectorEventSubscriber implements EventSubscriber
{
/** #var PostHandlerInterface */
private $postHandler;
public function postLoad($entity): void
{
$this->injectServices($entity);
}
public function postPersist($entity): void
{
$this->injectServices($entity);
}
private function injectServices($entity): void
{
if ($entity instanceof PostHandlerAwareInterface) {
$entity->setPostHandler($this->postHandler);
}
}
}
class BlogPost extends PostHandlerAwareInterface
{
/** #var PostHandlerInterface */
private $postHandler;
private $status;
public function setPostHandler(PostHandlerInterface $postHandler): void
{
$this->postHandler = $postHandler;
}
public function getStatus()
{
if (null === $this->status) {
$this->postHandler->calculateStatus($this);
}
return $this->status;
}
}
If you don't like this idea you still could manage it via (BUT I STRONGLY DO NOT RECOMMEND DO THIS DIRTY HACK) setting the flag to your entity event listener.
You could inject your entity event listener to the code and set flag before fetching data:
class BlogPostCalculateStatusListener
{
/** #var bool */
private $calculationEnabled = true;
public function suspendCalculation(): void
{
$this->calculationEnabled = false;
}
public function resumeCalculation(): void
{
$this->calculationEnabled = true;
}
public function postLoad(BlogPost $post): void
{
if ($this->calculationEnabled) {
$this->postHandler->calculateStatus($post);
}
}
}
$this->calculateStatusListener->suspendCalculation();
$blogPosts = $blogPostRepository->findBlogPosts();
$this->calculateStatusListener->resumeCalculation();
Hope this helps.
PS. If you want to get only the descriptions of all blog posts you can do this way:
class BlogPostRepository
{
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp.description')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getArrayResult();
}
}
getArrayResult does not invoke lifecycle callbacks.
Since i haven't found a real similar use case on the internet, i'll go for the following solution which seems the easiest and most acceptable cleanest to me. Maybe someone else could find this useful.
Implement a TransientLoadable Interface
interface TransientLoadable
{
public function isLoaded() : bool;
public function setLoaded(bool $loaded) : TransientLoadable;
public function setTransientLoadingFunction(\Closure $loadingFunction) :
TransientLoadable;
}
Implement the entity
class BlogPost implements TransientLoadable
{
...
}
Setup Loading function on postLoad Event
public function postLoad(BlogPost $post)
{
$func = function() use ($postHandler, $post)
{
//Since there may be another fields being loaded from the same API, catch them also since data is anyway in the same request
$postHandler->setAllDataFromAPI($post)
//Set the loading state to true to prevent calling the API again for the next property which may also be transient
$post->setLoaded(true);
}
$post->setTransientLoadingFunction($func)
}
Use the built-in lazy loading mechanism to get the property from the API only when it's needed
class BlogPost implements TransientLoadable
{
private function getStatus() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->status;
}
private function getVisitorCount() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->visitorCount;
}
}
So what's happening? Let's imagine we want to get the status and the visitor count, both are loaded via a single external API call.
If some api-dependent property of the entity is needed, all other properties gets loaded too (since we don't want to have for each property another call). This in ensured through the loaded function of the TransientLoadable interface. All data gets loaded by the setAllDataFromAPI function which is injected as a closure function.
I think that is not the cleanest solution. The loading stuf should be done by an extra layer on top of the entity class. Since sonata admin does not deal with such an layer, i think that this solution is cleaner than writing the loading mechanism directly to the entity class.
I am open to another suggestions or feedback
Thanks

Testing arguments of the method tested

is it possible or good idea to test arguments of the class that has the method mocked ?
Example :
class CarType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventSubscriber(new User('argument example')
}
}
This is my test:
public function testFormBuilder()
{
$builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder')
->disableOriginalConstructor()
->getMock();
// Test
$type = new CarType();
$this->builder->expects($this->once())
->method('addEventSubscriber')
->with(
$this->isInstanceOf(User::class)
);
$type->buildForm($this->builder, []);
}
This test work fine, But...
I want to know if the first argument of the User class is a string, but can I do in this same test?
You are not able to check directly because the User is being created directly in your code that you are testing.
However depending if the string can be retrieved via a public method of the User class, then you could do something like this:
$this->builder->expects($this->once())
->method('addEventSubscriber')
->with(
$this->callback(function($user) {
if (! $user instanceof User) {
return False;
}
if ($user->getName() != <Expected String>) {
return False;
}
return True;
})
);
You can use a callback check further details of User instance. You are not verifying that the string was passed to the constructor but you can check that the object is properly created.
Though this isn't the best way to go about this. Your code is not extensible because you are not able to change the User class. It would be better to pass in a User object to the method that gets passed on. This would allow you have different types of User objects in the future.

Symfony2 : Doctrine : PHPUnit : Set entity Id during flushing with mocked entity manager in unit tests

Symfony 2.8.13 / Doctrine ORM 2.5.5 / PHPUnit 5.7.5
I want to test a method of a class that makes use of the doctrine entity manager. This public method calls a private one that instantiates a Bookmark entity, flushes it and returns this entity. Then later, in the tested method I need to access the entity Id. Everything is mocked excepted the Bookmark entity itself. The main problem is that there is no setId() method in my entity. Here is the code and my main idea to solve this issue but I don't know if it is correct ?
Tested class and method
class BookmarkManager
{
//...
public function __construct(TokenStorageInterface $tokenStorage, ObjectManager $em, Session $session)
{
//...
}
public function manage($bookmarkAction, $bookmarkId, $bookmarkEntity, $bookmarkEntityId)
{
//...
$bookmark = $this->add($bookmarkEntity, $bookmarkEntityId);
//...
$bookmarkId = $bookmark->getId();
//...
}
private function add($entity, $entityId)
{
//...
$bookmark = new Bookmark();
//...
$this->em->persist($bookmark);
$this->em->flush();
return $bookmark;
}
}
Test
class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
public function testThatRestaurantAdditionToBookmarksIsWellManaged()
{
//...
// THIS WON'T WORK AS NO setId() METHOD EXISTS
$entityManagerMock->expects($this->once())
->method('persist')
->will($this->returnCallback(function ($bookmark) {
if ($bookmark instanceof Bookmark) {
$bookmark->setId(1);
}
}));
//...
$bookManager = new BookmarkManager($tokenStorageMock, $entityManagerMock, $sessionMock);
//...
}
}
Solutions ?
1- Make usage of reflection class as proposed here :
$entityManagerMock->expects($this->once())
->method('persist')
->will($this->returnCallback(function ($bookmark) {
if ($bookmark instanceof Bookmark) {
$class = new \ReflectionClass($bookmark);
$property = $class->getProperty('id');
$property->setAccessible(true);
$property->setValue($bookmark, 1);
//$bookmark->setId(1);
}
}));
2- Create a test Boookmark entity that extends from the real one and add a setId() method. Then create a mock of this class and replace and customize the one got from the ReturnCallback method with this one ? It seems crappy...
Any thoughts ? Thanks for your help.
The reflection looks interesting but it decreases readability of tests (mixing with mocks makes the situation tough).
I would create a fake for entity manager and implements there setting id based on reflection:
class MyEntityManager implements ObjectManager
{
private $primaryIdForPersitingObject;
public function __construct($primaryIdForPersitingObject)
{
$this->primaryIdForPersitingObject = $primaryIdForPersitingObject;
}
...
public function persist($object)
{
$reflectionClass = new ReflectionClass(get_class($object));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($object, $this->primaryIdForPersitingObject);
}
public function flush() { }
...
}
Once you implemented this, you can inject the instance of MyEntityManager and make your tests small and easier to maintain.
You test would look like
<?php
class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
public function testThatRestaurantAdditionToBookmarksIsWellManaged()
{
// ...
$entityManager = MyEntityManager(1);
//...
$bookManager = new BookmarkManager($tokenStorageMock, $entityManager, $sessionMock);
//...
}
}
Of course, a situation may be harder if there is a need of setting different ids for many persisting objects. Then you can, for example, increase $primaryIdForPersitingObject on persist call
public function persist($object)
{
$reflectionClass = new ReflectionClass(get_class($object));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($object, $this->primaryIdForPersitingObject);
$this->primaryIdForPersitingObject++;
}
It may be extended even further to have separate primaryIdForPersitingObject each entity class, and your tests will be still clean.

How return an array from a symfony2 controller action?

in my symfony2 project i need call the same action in many controllers and this action should return a very simple php array that then will be passed to a twig template by these controllers. How can i do it?
A pratical example can explain my situation better.
1.shared controller
// Acme/DemoBundle/Controller/MetasController
class MetasController extends Controller {
public function metasAction() {
$myArray= array();
return $myAarray;
}
}
page render controller
// Acme/DemoBundle/Controller/PageController
class PageController extends Controller {
protected $property = "test";
public function indexAction() {
$metas= $this->forward('AcmeDemoBundle:Metas:metas');
return $this->render('AcmeDemoBundle:Page:index.html.twig', array('property'=>property, 'metas'=>$metas));
}
}
when i do this i get an error: the controller must be a response array given.
You should create a service
// Acme/DemoBundle/Controller/MetasController
class MetasController {
public function metasAction() {
$myArray= array();
return $myAarray;
}
}
declare as service in Acme\DemoBundle\Resources\config\services.yml
services:
demo.metas:
class: "Acme\DemoBundle\Controller\MetasController"
Then you can use it in any other controller
// Acme/DemoBundle/Controller/PageController
class PageController extends Controller {
protected $property = "test";
public function indexAction() {
$metas= $this->get('demo.metas')->metas();
return $this->render('AcmeDemoBundle:Page:index.html.twig', array('property'=>property, 'metas'=>$metas));
}
}
In your action controller :
<?php
...
$arrayExample = array();
return $this->render('ExampleBundle:ExampleFolder:exampleTemplate', array('myArray' => $arrayExample));
And in your twig template now you have access to your array using myArray
Example :
{% for data in myArray %}
...
{% endfor %}
Try this :
use Symfony\Component\HttpFoundation\Response;
public function indexAction()
{
...
$content = $this->renderView(
'AcmeDemoBundle:Page:index.html.twig',
array('property'=> $property,
'metas' => $metas
));
return new Response($content);
}
Yes, you can register your controller as a service as it said above but I would recommend to isolate this logic in a different place. It might be a service but not controller.
As I understand you need the same array in several places. So, it might be some class registered as service or some simple class with static method providing this array. In this case your code will be much cleaner.
If you need this array only in view you can define custom twig method which will return array you need. If this array might be different time to time (if it might depend on some data) you can pass entity manager to the service providing this array or to the twig extension.
(The best use of controllers is to be just a proxy between view and data layer. It's not a good idea to use it for such purposes as you described (in my opinion of course).)

How can I bind a Symfony config tree to an object

How would I go about binding a Symfony config tree to a class rather than returning an array?
Using Symfony\Component\Config\Definition\Processor returns an array.
In my case I want the config to be bound to a class so I can use methods to combine parts of the data.
Here is a simple example of my use case. I want the config bound to a class so I can use a method to join table.name and table.version together (my actual use case is more complex, but this is a simple example)
config.yml
db:
table:
name: some_table
version: v2
ConfigurationInterface
class DBConfiguration implements ConfigurationInterface
{
/**
* {#inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('db');
$rootNode
->children()
->arrayNode('table')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('version')->end()
->end()
->end()
;
return $treeBuilder;
}
}
Class I want to bind the config to
class DB
{
public $table;
public function __construct()
{
$this->table = new Table();
}
}
class Table
{
public $name;
public $version;
/**
* #return string
* Calculate the full table name.
*/
public function getTableName()
{
return $this->name.'-'.$this->version;
}
}
The Symfony Config component doesn't support that.
However, in a Symfony project, this is usually done at the container compile phase. In your bundle's Extension class, you will have access to the configuration tree of your bundle in array form.
You can then take this array and assign it to a service defined in the service container that will create your config object.
This is exactly how DoctrineBundle's configuration class is built:
Abstract services (for the configuration and the factory) are defined in dbal.xml
When loading DoctrineBundle's extension, an instance of the abstract config service is created for each defined connection.
An instance of the abstract factory service is created for each defined connection.
The options array is then passed to the abstract factory service along with the configuration
When creating an instance, the factory then does the necessary transformations.
As far as I know, Symfony has no native support for this, however, you could implement it yourself. You could use subset of Symfony Serializer Component in charge of deserialization, but I think it would be an overkill. Especially since I don't see any PublicPropertyDenormalizer, only GetSetMethodNormalizer (which is denormalizer too). Therefor you would have to either make your config objects have get/set methods or roll PublicPropertyDenormalizer on your own. Possible but it really seems like an overkill and doesn't look like helping much:
Symfony Serializer Component
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
class MyConfigObject implements Symfony\Component\Serializer\Normalizer\DenormalizableInterface
{
private $field1;
private $subobject;
public function getField1()
{
return $this->field1;
}
public function setField1($field1)
{
$this->field1 = $field1;
}
public function getSubobject()
{
return $this->subobject;
}
public function setSubobject(SubObject $subobject)
{
$this->subobject = $subobject;
}
public function denormalize(\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $denormalizer, $data, $format = null, array $context = array())
{
$obj = new static();
$obj->setField1($data['field1']);
$obj->setSubobject($denormalizer->denormalize($data['subobject'], 'SubObject'));
return $obj;
}
}
class SubObject
{
private $subfield1;
public function getSubfield1()
{
return $this->subfield1;
}
public function setSubfield1($subfield1)
{
$this->subfield1 = $subfield1;
}
}
$normalizer = new \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer();
$obj = (new MyConfigObject())->denormalize($normalizer, $array);
Native PHP Way
Imo this is a lot easier than above as Symfony Serializer wasn't really ment for that.
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
trait Denormalizable
{
public function fromArray($array)
{
foreach ($array as $property => $value) {
if (is_array($value)) {
if ($this->$property instanceof ArrayDenormalizableInterface) {
$this->$property->fromArray($value);
} else {
$this->$property = $value;
}
} else {
$this->$property = $value;
}
}
}
}
interface ArrayDenormalizableInterface
{
public function fromArray($array);
}
class MyConfigObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $field1;
public $subobject;
public function __construct()
{
$this->subobject = new SubObject();
}
}
class SubObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $subfield1;
}
$myConf = new MyConfigObject();
$myConf->fromArray($array);
Whatever way you choose, you can now just take array returned from symfony processor and turn it into a config object you need.

Resources