I am creating a normalizer and im not sure now it should be applied
"Custom normalizers and/or encoders can also be loaded by tagging them as serializer.normalizer and serializer.encoder. It's also possible to set the priority of the tag in order to decide the matching order."
https://symfony.com/doc/current/serializer.html#adding-normalizers-and-encoders
services.yml
datetime_normalizer:
class: App\Normalizer\DateTimeNormalizer
public: true
tags: [serializer.normalizer]
class
<?php
namespace App\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class DateTimeNormalizer
*/
class DateTimeNormalizer implements NormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return $object->format(\DateTime::ISO8601);
}
/**
* {#inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof \DateTime;
}
}
call
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory);
$serializer = new Serializer([$normalizer]);
$user = $serializer->normalize($token->getUser());
output
"datetime":{"timezone":{"name":"UTC","transitions":[{"ts":-9223372036854775808,"time":"-292277022657-01-27T08:29:52+0000","offset":0,"isdst":false,"abbr":"UTC"}],"location":{"country_code":"??","latitude":0,"longitude":0,"comments":""}},"offset":0,"timestamp":1527033600}
full code at github
https://github.com/ricardosaracino/symfony-pull-list/blob/master/config/services.yaml
Instead of creating your own serializer, you need to rely on the serializer created by Symfony by injecting it where you need. An example of this is in the doc: https://symfony.com/doc/current/serializer.html#using-the-serializer-service.
Have a look at https://symfony.com/doc/current/controller.html#fetching-services for controllers to learn more.
Related
I will start saying I am using Symfony 4.3.4 and Api Platform (called AP from now on). Having said that this how my custom controller (used for AP) looks like:
declare(strict_types=1);
namespace App\Controller\CaseWork\Pend;
use App\Request\PendCaseRequest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route;
class PendCaseController
{
/**
* #Route("/myroute/{id}/pend", name="routeName")
* #ParamConverter("case", class="App\Entity\Cases")
*/
public function __invoke(PendCaseRequest $request, int $id)
{
// do something with the $request
}
}
As you may notice I also have a Request Data Transformer Object and here is a code snippet for it:
declare(strict_types=1);
namespace App\Request;
use App\Interfaces\RequestDTOInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints as Assert;
class PendCaseRequest implements RequestDTOInterface
{
/**
* #var int
*
* #Assert\NotBlank()
* #Assert\NotNull()
* #Assert\Type("integer")
*/
private $param;
public function __construct(Request $request)
{
$data = json_decode($request->getContent(), true);
$this->param = (int) $data['param'];
// ...
}
}
It's suppose (as per docs here) that when the request comes in and an id matching a App\Entity\Cases is found a new attribute named case should be append to my $request object but in my scenario is not happening and I am not sure why or what I am missing.
While debugging and setting a break point at this line $this->param = (int) $data['param']; in my DTO, if I print out $this->attributes I got the following output:
‌Symfony\Component\HttpFoundation\ParameterBag::__set_state(array(
'parameters' =>
array (
),
))
What I am missing here? What is wrong with my approach?
I have found a "solution" here. I end up using a Decorator as suggested by the answer on that post.
My main controller changed into this:
declare(strict_types=1);
namespace App\Controller\CaseWork\Pend;
use App\Request\PendCaseRequest;
use App\Entity\Cases;
class PendCaseController
{
public function __invoke(PendCaseRequest $request, Cases $case)
{
// do something with the $request
}
}
A decorator was created:
declare(strict_types=1);
namespace App\Decorator;
use App\Controller\CaseWork\Pend\PendCaseController;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Cases;
use App\Request\PendCaseRequest;
class PendCaseDecorator
{
/** #var PendCaseController */
protected $decoratedController;
/** #var EntityManagerInterface */
protected $entityManager;
public function __construct(PendCaseController $controller, EntityManagerInterface $entityManager)
{
$this->decoratedController = $controller;
$this->entityManager = $entityManager;
}
public function __invoke(PendCaseRequest $request, int $id)
{
$object = $this->entityManager->getRepository(Cases::class)->find($id);
if (!$object instanceof Cases) {
throw new NotFoundHttpException('Entity with '.$id.' not found');
}
return $this->decoratedController($request, $object);
}
}
And I had registered it at services.yml:
services:
App\Controller\CaseWork\Pend\PendCaseController: ~
App\Decorator\PendCaseDecorator:
decorates: App\Controller\CaseWork\Pend\PendCaseController
That way I keep using my DTO and pass back a Cases entity object.
I'm studying Symfony and I'm trying with Dispatch Event Component.
I Understand that there are
Dispatcher (component)
Event ( that pass information to Listener )
Listener ( the logic )
I created a simple example in my route
/**
* #Route("/evento", name="evento")
*/
public function eventoAction()
{
$dispatcher = new EventDispatcher();
$listener = new StoreListener($this->get('mailer'));
$dispatcher->addListener(StoreEvents::STORE_ORDER,array($listener,'onFooAction'),25);
$order = new Ordini();
$order->setName('ordine nuovo -'.rand(0,100));
$em = $this->getDoctrine()->getManager();
$em->persist($order);
$em->flush();
$event = new FilterOrderEvent($order);
$dispatcher->dispatch(StoreEvents::STORE_ORDER,$event);
return $this->render('::event.html.twig');
}
StoreEvent with Contain the Const String
namespace AppBundle\Events;
final class StoreEvents
{
/**
* The store.order event is thrown each time an order is created
* in the system.
*
* The event listener receives an
* Acme\StoreBundle\Event\FilterOrderEvent instance.
*
* #var string
*/
const STORE_ORDER = 'store.order';
}
The Event that I pass to Listener
<?php
namespace AppBundle\Events;
use AppBundle\Entity\Ordini;
use Symfony\Component\EventDispatcher\Event;
class FilterOrderEvent extends Event
{
protected $order;
public function __construct(Ordini $order)
{
$this->order = $order;
}
/**
* #return Ordini
*/
public function getOrder()
{
return $this->order;
}
}
And The Listener
<?php
namespace AppBundle\Events;
use Symfony\Component\EventDispatcher\Event;
class StoreListener
{
protected $mail;
/**
* StoreListener constructor.
* #param $mail
*/
public function __construct($mail)
{
$this->mail = $mail;
}
public function onFooAction(Event $event)
{
var_dump($event);
var_dump($event->getOrder());
echo "fooAction";
$message = \Swift_Message::newInstance()
->setSubject($event->getOrder()->getName())
->setFrom('send#example.com')
->setTo('send#example2.com)
->setBody('Test')
;
$this->mail->send($message);
}
}
all these stuff Work but I want to Understand where I have to declare the dispatcher.
I have to declare for each controller ?
$dispatcher = new EventDispatcher();
even the add listener ( or addSubscriber)
$dispatcher->addListener(StoreEvents::STORE_ORDER,array($listener,'onFooAction'),25);
I don't think this is the best practice to Declare every Time the new Dispatcher
or new addListener
UPDATE
I created another Action
/**
* #Route("/evento2", name="evento2")
*/
public function evento2Action()
{
$order = new Ordini();
$order->setName('ordine nuovo -'.rand(0,100));
$em = $this->getDoctrine()->getManager();
$em->persist($order);
$em->flush();
$event = new FilterOrderEvent($order);
$this->get('event_dispatcher')->dispatch(StoreEvents::STORE_ORDER,$event);
return $this->render('::event.html.twig');
}
And create my service:
services:
create_order_listener:
class: AppBundle\Events\StoreListener
arguments: ["#swiftmailer.mailer"]
tags:
- { name: kernel.event_listener, event: store.order, method: onFooAction
all work. But now I'd like to know the best practice or when Create listener by service and when I have to use addListener or addSubscriber
I'm trying to get Entity Listeners work with ODM in Symfony 2.7 but to no avail.
a51.document.listener.store:
class: A51\FilesystemBundle\EventListener\StoreEntityListener
tags:
- { name: doctrine.odm.mongodb.document_manager, event: postLoad, method: onPostLoad }
arguments: [#a51.repo.file]
and:
<?php
namespace A51\FilesystemBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use A51\FilesystemBundle\Document\Store;
use A51\FilesystemBundle\Repository\FileRepositoryInterface;
class StoreEntityListener
{
/**
* #var FileRepositoryInterface
*/
private $fileRepository;
public function __construct(FileRepositoryInterface $fileRepository)
{
$this->fileRepository = $fileRepository;
}
public function onPostLoad(LifecycleEventArgs $args)
{
$this->index($args);
}
public function index(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Store)
{
$entity->setTotalByteSize($this->fileRepository->findSumFilesSizeByStore($entity));
}
}
}
I've tried pretty much everything I could find in docs but for some reason onPostLoad method does not get called.
Store document gets loaded with ParamConverter:
* #ParamConverter("store", class="A51FilesystemBundle:Store")
Any help would be welcome.
I have a MongoDB listener in my project, but my code it's so different of yours. There's a simpler way, all you must to do it's import DocumentManager and then you can call it from construct to use it on all you listener. I'm gonna show you my code and tell me if this help you ;)
namespace AppBundle\OdmListener;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ODM\MongoDB\DocumentManager;
class RedundancyListener
{
/**
* #var DocumentManager
*/
private $dm;
/**
* #param DocumentManager $odm
*/
function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
Then inside you can do any queries or updates as you do it in you controller. Also you can use ORM or ODM CycleEvents if you import them, like I do in the example.
/**
* #param LifecycleEventArgs $eventArgs
*/
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof \AppBundle\Entity\Example) {
$subscriptionHash = $this->getSubscription($entity);
$this->dm->createQueryBuilder('AppBundle\Document\Example')
->update()
//->multiple(true)
->field('name')->set($entity->getName())
->field('last_name')->set($entity->getLastName())
->field('mail')->set($entity->getMail())
->getQuery()
->execute();
}
}
}
I'm trying to inject my repository service into EventListener but that leads me to following exception, which, with my basic knowledge of Symfony2, I have no idea how to resolve. Exception is:
ServiceCircularReferenceException in bootstrap.php.cache line 2129:
Circular reference detected for service "doctrine.orm.default_entity_manager", path: "doctrine.orm.default_entity_manager -> doctrine.dbal.default_connection -> person.connect.listener -> tag.repository.service".
And here is how I've declared repository and listener:
tag.repository.service:
class: Application\Bundle\PersonBundle\Entity\TagRepository
factory: ["#doctrine", getRepository]
arguments: [ Application\Bundle\PersonBundle\Entity\Tag ]
person.connect.listener:
class: Application\Bundle\PersonBundle\EventListener\ConnectListener
arguments:
tokenStorage: "#security.token_storage"
tagRepo: "#tag.repository.service"
tags:
- { name: doctrine.event_listener, event: postPersist, connection: default }
Most answers, that I've able to find, suggest injecting service container, but I really don't want do that. Is there any way to resolve this properly?
UPD: Here is the code of the listener. Everything worked fine until I've tried to inject TagRepository
class ConnectListener
{
/**
* #var TokenStorage
*/
private $tokenStorage;
/**
* #var TagRepository
*/
private $tagRepo;
/**
* #param TokenStorage $tokenStorage
* #param TagRepository $tagRepo
*/
public function __construct(TokenStorage $tokenStorage, TagRepository $tagRepo)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param LifecycleEventArgs $args
* #return void
*/
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Person) {
$user = $this->tokenStorage->getToken()->getUser();
$visibility = new PersonVisibility($entity, $user);
$visibility->setVisibilityType(PersonVisibility::VT_CREATED);
$entityManager->persist($visibility);
$entityManager->flush();
}
}
}
As far as TagRepository is descendant of EntityRepository try obtaining its instance in postPersist event. Like this:
// using full classname:
$tagRepo = $entityManager->getRepository("Application\Bundle\PersonBundle\Entity\TagRepository");
// alternatively:
$tagRepo = $entityManager->getRepository("ApplicationPersonBundle:Tag");
Yo can also change your declaration of your repository, don't use the factory and use one of these 2 methods.
This will avoid the circular reference and will be cleaner than use the EntityManager class.
I need to do a custom isGranted method (not using Rbac or acl module from community). So I have a service which provides the functionality. But this code:
if (!$this->userService->isGrantedCustom($this->session->offsetGet('cod_lvl'), 'ZF_INV_HOM')) {
throw new \Exception("you_are_not_allowed", 1);
}
...is duplicated in each controller and each action I have. Parameters are changing of course depends on the permission ('ZF_INV_HOM', 'ZF_TODO_DELETE' ...).
I think it's not a bad idea to do this code before the controller is called, but I can't figure what is the best solution (best architecture), and how to pass those parameters to it (I thought about annotation on controllers but how to handle this ?).
The point is, if I have to modify this code I can't imagine to do that hundreds of times, for each controllers, each action I have I need to have this code in one place.
If you don't want to pollute your Module with all this code you can also make a listener class and attach only the listener in your bootstrap method:
<?php
namespace Application\Listener;
use Application\Service\UserService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\SharedEventManagerInterface;
use Zend\EventManager\SharedListenerAggregateInterface;
use Zend\Authentication\AuthenticationServiceInterface;
class IsAllowedListener implements SharedListenerAggregateInterface
{
/**
* #var AuthenticationServiceInterface
*/
protected $authService;
/**
* #var UserService
*/
protected $userService;
/**
* #var \Zend\Stdlib\CallbackHandler[]
*/
protected $sharedListeners = array();
/**
* #param SharedEventManagerInterface $events
*/
public function attachShared(SharedEventManagerInterface $events)
{
$this->sharedListeners[] = $events->attach(AbstractActionController::class, MvcEvent::EVENT_DISPATCH, array($this, 'isAllowed'), 1000);
}
public function __construct(AuthenticationServiceInterface $authService, UserService $userService ){
$this->authService = $authService;
$this->userService = $userService;
}
/**
* #param MvcEvent $event
*/
protected function isAllowed(MvcEvent $event)
{
$authService = $this->getAuthService();
$identity = $authService->getIdentity();
$userService = $this->getUserService();
if($userService->isGrantedCustom()){
// User is granted we can return
return;
}
// Return not allowed response
}
/**
* #return AuthenticationServiceInterface
*/
public function getAuthService()
{
return $this->authService;
}
/**
* #param AuthenticationServiceInterface $authService
*/
public function setAuthService(AuthenticationServiceInterface $authService)
{
$this->authService = $authService;
}
/**
* #return UserService
*/
public function getUserService()
{
return $this->userService;
}
/**
* #param UserService $userService
*/
public function setUserService(AuthenticationServiceInterface $userService)
{
$this->userService = $userService;
}
}
You need to setup a factory to inject your dependencies:
<?php
namespace Application\Listener;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* Factory for creating the IsAllowedListener
*/
class IsAllowedListenerFactory implements FactoryInterface
{
/**
* Create the IsAllowedListener
*
* #param ServiceLocatorInterface $serviceLocator
* #return RenderLinksListener
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$userService = $serviceLocator->get('Application\Service\UserService');
return new IsAllowedListener($authService, $userService );
}
}
And register all this in config:
'service_manager' => array(
'factories' => array(
'Application\Listener\IsAllowedListener' => 'Application\Listener\IsAllowedListenerFactory'
)
)
And then in bootstrap:
public function onBootstrap(EventInterface $event)
{
$application = $event->getTarget();
$serviceManager = $application->getServiceManager();
$eventManager = $application->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
$isAllowedListener = $serviceManager->get('Application\Listener\IsAllowedListener')
$sharedEventManager->attachAggregate($isAllowedListener);
}
Instead of using AbstractActionController::class, you could also make a specific class, so you will only listen to instances of that class.
So for example AbstractIsAllowedActionController::class or something like that.
By attaching an event listener to the SharedEventManager you can target all controllers and have the authorization check in just one place.
In this case the target is Zend\Mvc\Controller\AbstractActionController which means any controller extending it will execute the listener. The high priority of this listener will mean that it is executed prior to the target controller action, giving you the chance to handle any requests that have not been authorized.
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager()->getSharedManager();
$eventManager->attach(
\Zend\Mvc\Controller\AbstractActionController::class, // Identity of the target controller
MvcEvent::EVENT_DISPATCH,
[$this, 'isAllowed'],
1000 // high priority
);
}
In each controller there would need to be some way that you can determine which 'resource' is being accessed.
As an example it could implement this interface
interface ResourceInterface
{
// Return a unique key representing the resource
public function getResourceId();
}
The listener could then look like this.
public function isAllowed(MvcEvent $event)
{
$serviceManager = $event->getApplication()->getServiceManager();
// We need the 'current' user identity
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$identity = $authService->getIdentity();
// The service that performs the authorization
$userService = $serviceManager->get('MyModule\Service\UserService');
// The target controller is itself a resource (the thing we want to access)
// in this example it returns an resource id so we know what we want to access
// but you could also get this 'id' from the request or config etc
$controller = $event->getTarget();
if ($controller instanceof ResourceInterface) {
$resourceName = $controller->getResourceId();
// Test the authorization, is UserX allowed resource ID Y
if (empty($resourceName) || $userService->isGrantedCustom($identity, $resourceName)) {
// early exit for success
return;
} else {
// Denied; perhaps trigger a new custom event or return a response
}
}
}