JMS Serializer: Serialize custom properties of entities - symfony

I want to add a custom property to the serialized entity's representation, which takes an existing entity property and formats it in a user friendly way by using an existing service.
I defined a subscriber class and injected the service used for formatting the existing entity property and subscribed to serializer.pre_serialize as follows:
class UserSerializationSubscriber implements EventSubscriberInterface
{
private $coreTwigExtension;
private $user;
public function setCoreTwigExtension(TwigExtension $coreTwigExtension)
{
$this->coreTwigExtension = $coreTwigExtension;
}
public function setUserService(UserService $user)
{
$this->user = $user;
}
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.pre_serialize',
'method' => 'onObjPreSerialize',
'class' => 'Some\Bundle\Entity\EntityClass',
'format' => 'json'
)
);
}
public function onObjPreSerialize(PreSerializeEvent $event)
{
$context = $event->getContext();
$context->attributes->get('groups')->map(
function(array $groups) use ($event) {
if (in_array('somegroup', $groups)) {
$obj= $event->getObject();
if ($obj->getConfirmedOn()) {
$contextualDate = $this->coreTwigExtension->getContextualDate($obj->getConfirmedOn());
$event->getVisitor()->addData('displayConfirmedOn', $contextualDate);
}
}
}
);
}
}
Subscriber registration:
some_bundle.handler.serialization:
class: Some\Bundle\Handler\ObjectSerializationSubscriber
calls:
- [setCoreTwigExtension, ['#bundle_core.twig.extension']]
- [setUserService, ['#some_bundle.service.user']]
tags:
- { name: jms_serializer.event_subscriber }
When I serialize an array/collection of entity Some\Bundle\Entity\EntityClass I get the following error:
There is already data for "displayConfirmedOn".
How do I resolve this? The only thing stopping me from using #VirtualProperty in the entity is that the virtual property output depends on a service, and no dependencies should be injected into an entity.

The error is due to the fact that the entity itself already exposes an attribute displayConfirmedOn for serialization. When your event listener runs it is not allowed to add an attribute with the same name to the output and you get this error.
Simply stop exposing the attribute in your entity and then the listener can add a property of the same name.

Related

Algolia search/Symfony :Too few arguments to function

When trying to do a simple search method with Algolia. The search method will query Algolia to get matching results and then will create a doctrine collection. I get this error:
I injected my indexManager in my controller :
class ProductController extends Controller
{
protected $indexManager;
public function __construct(IndexManagerInterface $indexingManager)
{
$this->indexManager = $indexingManager;
}
public function displayAction(Request $request) {
$em = $this->getDoctrine()->getManagerForClass(Product::class);
$posts = $this->indexManager->search('query', Product::class, $em);
return $this->render('ProductBundle:Default:postDisplay.html.twig', array(
'posts' => $posts));
}
}
Can you make sure the autowire feature is set to true?
https://symfony.com/doc/current/service_container/autowiring.html
Symfony should resolve the dependency automatically.

Symfony3 - Serializing nested entities

So I have a couple doctrine entities, a Subscription and a Subscriber. There are many Subscriptions to a single subscriber (manyToOne). I wrote custom normalizers for both entities, but am having trouble getting the Subscriber to show up in the Subscription once it has been normalized to JSON.
The only way I've been able to get it to work is by passing the 'Subscriber' normalizer to the 'Subscription' normailizer. It seems like I should just be able to use the SerializerAwareNormalizer Trait, or something like that, to have Symfony recursively normalize my related entities.
services:
acme.marketing.api.normalizer.subscription:
class: acme\MarketingBundle\Normalizer\SubscriptionNormalizer
arguments: ['#acme.marketing.api.normalizer.subscriber']
public: false
tags:
- { name: serializer.normalizer }
acme.marketing.api.normalizer.subscriber:
class: acme\MarketingBundle\Normalizer\SubscriberNormalizer
public: false
tags:
- { name: serializer.normalizer }
and the normalizer...
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface
{
private $subscriberNormalizer;
public function __construct($subscriberNormalizer)
{
$this->subscriberNormalizer = $subscriberNormalizer;
}
public function normalize($subscription, $format = null, array $context = [])
{
/* #var $subscription Subscription */
$subscriber = $subscription->getSubscriber();
return [
"id" => $subscription->getId(),
"subscriber" => $this->subscriberNormalizer->normalize($subscriber, $format)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}
Is there a better way to accomplish this?
Spent a few hours on google and couldn't figure it out. Post on SO and 5 minutes later hit the right google link :(. Answer seems to be to implement NormalizerAwareInterface on the custom normalizer, and then use the NormalizerAwareTrait to get access to the normalizer for nested entities.
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($subscription, $format = null, array $context = [])
{
return [
"id" => $subscription->getId(),
"subscriber" => $this->normalizer->normalize($subscription->getSubscriber())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}

Sysmfony REST API hash id of entities

I'm building a multitenancy backend using Symfony 2.7.9 with FOSRestBundle and JMSSerializerBundle.
When returning objects over the API, I'd like to hash all the id's of the returned objects, so instead of returning { id: 5 } it should become something like { id: 6uPQF1bVzPA } so I can work with the hashed id's in the frontend (maybe by using http://hashids.org)
I was thinking about configuring JMSSerializer to set a virtual property (e.g. '_id') on my entities with a custom getter-method that calculates the hash for the id, but I don't have access to the container / to any service.
How could I properly handle this?
You could use a Doctrine postLoad listener to generate a hash and set a hashId property in your class. Then you could call expose the property in the serializer but set the serialized_name as id (or you could just leave it at hash_id).
Due to the hashing taking place int the postLoad you would need to refresh your object if you have just created it using $manager->refresh($entity) for it take effect.
AppBundle\Doctrine\Listener\HashIdListener
class HashIdListsner
{
private $hashIdService;
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$reflectionClass = new \ReflectionClass($entity);
// Only hash the id if the class has a "hashId" property
if (!$reflectionClass->hasProperty('hashId')) {
return;
}
// Hash the id
$hashId = $this->hashIdService->encode($entity->getId());
// Set the property through reflection so no need for a setter
// that could be used incorrectly in future
$property = $reflectionClass->getProperty('hashId');
$property->setAccessible(true);
$property->setValue($entity, $hashId);
}
}
services.yml
services:
app.doctrine_listsner.hash_id:
class: AppBundle\Doctrine\Listener\HashIdListener
arguments:
# assuming your are using cayetanosoriano/hashids-bundle
- "#hashids"
tags:
- { name: doctrine.event_listener, event: postLoad }
AppBundle\Resources\config\serializer\Entity.User.yml
AppBundle\Entity\User:
exclusion_policy: ALL
properties:
# ...
hashId:
expose: true
serialized_name: id
# ...
Thanks a lot for your detailed answer qooplmao.
However, I don't particularly like this approach because I don't intend to store the hashed in the entity. I now ended up subscribing to the serializer's onPostSerialize event in which I can add the hashed id as follows:
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MySubscriber implements EventSubscriberInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public static function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'method' => 'onPostSerialize'),
);
}
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
$service = $this->container->get('myservice');
$event->getVisitor()->addData('_id', $service->hash($event->getObject()->getId()));
}
}

Symfony dependency injection in twig extension

Ok, I was trying to create twig extension with dependencies on other service (security.context) and got some troubles. So, here is my service declaration:
acme.twig.user_extension:
class: Acme\BaseBundle\Twig\UserExtension
arguments: ["#security.context"]
tags:
- { name: twig.extension }
and here's my class
// acme/basebundle/twig/userextension.php
namespace Acme\BaseBundle\Twig;
use Symfony\Component\Security\Core\SecurityContext;
use Acme\UserBundle\Entity\User;
class UserExtension extends \Twig_Extension
{
protected $context;
public function __construct(SecurityContext $context){
$this->context = $context;
}
public function getFunctions()
{
return array(
'getAbcData' => new \Twig_SimpleFunction('getAbcData', $this->getAbcData()),
);
}
public function getAbcData()
{
if ( !is_object($user = $this->context->getToken()->getUser()) || !$user instanceof User){ return null; }
return array(
'data_array' => $user->getData(),
);
}
public function getName()
{
return 'user_extension';
}
}
Finally, I have an error:
FatalErrorException: Error: Call to a member function getUser() on a non-object in \src\Acme\BaseBundle\Twig\UserExtension.php line 27
I guess that security.context service is not initialized yet, then i get an error.
Could anyone tell, please, is there are ways to load service manually, or any better solutions for an issue?
Thanks a lot.
I use Symfony 2.5.*
UPD:
I've also found this notice in symfony docs
Keep in mind that Twig Extensions are not lazily loaded. This means that there's a higher chance that you'll get a CircularReferenceException or a ScopeWideningInjectionException if any services (or your Twig Extension in this case) are dependent on the request service. For more information take a look at How to Work with Scopes.
Actually, I have no idea about how to do it correct..
You are calling $this->getAbcData() when constructing Twig_SimpleFilter. But you have to pass a callable as argument.
public function getFunctions() {
return array (
'getAbcData' => new \Twig_SimpleFunction( 'getAbcData', array( $this, 'getAbcData' ))
);
}
Leo is also right. You should check first if getToken() is returning an object before trying getToken()->getUser().
You can also pass the user to the function as a parameter in twig: {{ getAbcData(app.user) }}. This way the function is more generic and could be used for any user, not just the currently logged in one.
This should probably work. The error message means that getToken() is not an object so you have to test if getToken() is an object before testing if getUser() is also is an object.
public function getAbcData()
{
$token = $this->context->getToken();
if (!is_object($token) || !is_object($token->getUser())) {
return null;
}
return array(
'data_array' => $user->getData(),
);
}
You need to change your twig extension to have the container not the security context passed into the constructor.
Twig_Extensions are special in that the normal rule of don't pass in the container but instead pass in only what you need often doesn't apply as it causes problems due to scope issues.
So change your extension to be like this.
// acme/basebundle/twig/userextension.php
namespace Acme\BaseBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Acme\UserBundle\Entity\User;
class UserExtension extends \Twig_Extension
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container){
$this->container = $container;
}
public function getFunctions()
{
return array(
'getAbcData' => new \Twig_SimpleFunction('getAbcData', $this->getAbcData()),
);
}
public function getAbcData()
{
if ( !is_object($user = $this->container->get('security.context')->getToken()->getUser()) || !$user instanceof User){ return null; }
return array(
'data_array' => $user->getData(),
);
}
public function getName()
{
return 'user_extension';
}
}

Best way to develeop plugin compatible application. Dependency injection?

I'm wondering the best way to create fully compatible application to plug-ins.
I'm used to Wordpress plug-ins concept that you can define actions and filters and then use in your plug-ins. So others can define methods on their plug-ins that are executed when the action is called (or the filter).
My idea is create my app with some actions and filters and then other developers can build a Bundle that interfere in the "normal" app flow...
I was reading about Symfony2 Dependency Injection, but I didn’t found some comprehensive example to do something similar that I want.
Someone has a real example of something similar that I'm looking for?
Is the Dependency Injection the best solution or should I build my own plugin handler?
EDIT:
What I did to allow other bundles to add items to my knp-menu menu.
In my base bundle:
Defining the filter that allow subscribber to get and set menu data:
# BaseBundle/Event/FilterMenuEvent.php
class FilterMenuEvent extends Event
{
protected $menu;
public function __construct($menu)
{
$this->menu = $menu;
}
public function getMenu()
{
return $this->menu;
}
}
Defining the events of the menu:
# Event/MenuEvents.php
final class MenuEvents
{
const BEFORE_ITEMS = 'menu.before.items';
const AFTER_ITEMS = 'menu.after.items';
}
Setting up the subscriber:
# Event/MenuSubscriber.php
class MenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'menu.after.items' => array(
array('homeItems', 9000),
array('quickactionsItems', 80),
array('adminItems', 70),
...
array('logoutItems', -9000),
)
);
}
public function homeItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Home', array('route' => 'zashost_zaspanel_homepage'));
}
public function quickactionsItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Quick actions', array( 'route' => null));
$menu['Quick actions']->addChild('Add hosting', array( 'route' => 'zashost_zaspanel_register_host'));
}
}
Dispatching events in the generation of menu:
# Menu\Builder.php
class Builder extends ContainerAware
{
public function userMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$this->container->get('event_dispatcher')->dispatch(MenuEvents::AFTER_ITEMS , new FilterMenuEvent($menu));
return $menu;
}
}
Attach subscriber to kernel event subscriber:
# services.yml
services:
# Menu items added with event listener
base_menu_subscriber:
class: Acme\BaseBundle\Event\MenuSubscriber
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
Then in third party bundle:
Setting up my third party event subscriber:
class MenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'menu.after.items' => array('afterItems', 55)
);
}
public function afterItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Backups', array( 'route' => null));
$menu['Backups']->addChild('Create new backup', array( 'route' => null));
return $menu;
}
}
And attaching to kernel event subscriber:
# srevices.yml
services:
menu_subscriber:
class: Acme\ThirdPartyBundle\Event\MenuSubscriber
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
In that way I can use the priority of Event Dispatcher to set the position of each group of items of the menu.
A good starting point in providing extension points for your application, in which other developers can hook their custom behaviour, is to use the EventDispatcher component from Symfony - a implementation of the Observer Pattern.
Symfony already uses the component extensively in it's own core ( HttpKernel ) to allow other components (or plugins, if you will) to hook in various points in the http request -> response flow and handle everything from Request matching to Response generation.
For example you can hook to the kernel.request event and return a Response immediately if the Request is not valid or to the kernel.response event and change the response content.
See the full list of default KernelEvents.
By only using these (there are many others related to other components), you can create a plugin sytem that is more capable, more testable and more robust than that of the Wordpress "platform".
Of course, you can easily create and dispatch your own events that will suit your business logic (for example create events like post.created or comment.created) for a blog application.
Now, for the sake of an example, here is how you will configure a "plugin" that will do something with the generated Response and then will fire another event (that can be used by another plugin)
namespace Vendor;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class ResponseAlter implements EventSubscriberInterface
{
private $dispatcher;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function doSomethingWithResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
/**
* let other plugins hook to the provide.footer event and
* add the result to the response
*/
$footer = new ProvideFooterEvent();
$this->dispatcher->dispatch('provide.footer', $footer);
$this->addFooterProvidedByPluginToResponse($response, $footer->getProvidedFooter());
$event->setResponse($response);
}
static function getSubscribedEvents()
{
return array(
'kernel.response' => 'doSomethingWithResponse'
);
}
}
Now you will simply have to tag your service as a service subscriber and you're done. You've just plugged in the HttpKernel component:
services:
my_subscriber:
class: Vendor\ResponseAlter
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}

Resources