Symfony Restful API - Expose virtual property isLiked by current logged in user - php

There are two entities Restaurant and Users. Restaurant entity has many-to-many relation with user, field name favoriteBy.
<many-to-many field="favoriteBy" target-entity="UserBundle\Entity\Users" mapped-by="favoriteRestaurants"/>
I am using JMS Serializer along with FOSRestfulAPI. In restaurant listing API I have to expose one extra boolean field "isFavorited", which will be true if current logged in user has in array collection favoriteBy.
How I can find whether current user has favorited the restaurant or not within entity?
/**
* Get is favorited
* #JMS\VirtualProperty()
* #JMS\Groups({"listing", "details"})
*/
public function isFavorited()
{
// some logic in entity
return false;
}
One way I am thinking is to inject current user object to entity and user contains method to find out, but its look like not good approach.
Please suggest me some method, or guide me to right direction.

You could implments an EventSubscriberInterface as described here in the doc.
As Example:
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
...
class RestaurantSerializerSubscriber implements EventSubscriberInterface
{
protected $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return [
[
'event' => 'serializer.post_serialize',
'class' => Restaurant::class,
'method' => 'onPostSerialize',
],
];
}
public function onPostSerialize(ObjectEvent $event)
{
$visitor = $event->getVisitor();
$restaurant = $event->getObject();
// your custom logic
$isFavourite = $this->getCurrentUser()->isFavourite($restaurant);
$visitor->addData('isFavorited', $isFavourite);
}
/**
* Return the logged user.
*
* #return User
*/
protected function getCurrentUser()
{
return $this->tokenStorage->getToken()->getUser();
}
And register, as YML example:
acme.restaurant_serializer_subscriber:
class: Acme\DemoBundle\Subscriber\RestaurantSerializerSubscriber
arguments: ["#security.token_storage"]
tags:
- { name: "jms_serializer.event_subscriber" }
Hope this help
PS: You could also intercept the serialization group selected, let me know if you neet that code.

Entity should know nothing about current logged in user so injecting user into entity is not a good idea.
Solution 1:
This can be done with custom serialization:
// serialize single entity or collection
$data = $this->serializer->serialize($restaurant);
// extra logic
$data['is_favourited'] = // logic to check if it's favourited by current user
// return serialized data
Solution 2
This can be also achieved by adding Doctrine2->postLoad listener or subscriber after loading Restaurant entity. You can add dependency for current authenticated token to such listener and set there Restaurant->is_favorited virtual property that will be next serialized with JMS.

Related

Doctrine preUpdate event shows no changeset for associated collections

This is a question about the event system in Doctrine (within a Symfony project).
I have two classes, User and Visit, that are associated via a many-to-many relationship. I.e. a user can have many visits and a visit can have many users (that attend the visit).
class Visit
{
#[ORM\Column]
protected string $Date;
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: "Visits")]
#[ORM\JoinTable(name: "users_visits")]
protected Collection $Users;
public function __construct()
{
$this->Users = new ArrayCollection();
}
//... other properties and methods omitted
}
class User
{
#[ORM\Column]
protected string $Name;
#[ORM\ManyToMany(targetEntity: Visit::class, inversedBy: "Users")]
#[ORM\JoinTable(name: "users_visits")]
protected Collection $Visits;
public function __construct()
{
$this->Visits = new ArrayCollection();
}
//... other properties and methods omitted
}
I also have an UpdateSubscriber that is supposed to record certain inserts, updates or removals, in a separate sql-table to create an overview over all relevant changes later on.
class UpdateSubscriber implements EventSubscriberInterface
{
public function __construct(private LoggerInterface $logger)
{
}
public function getSubscribedEvents(): array
{
return [
Events::preUpdate,
Events::postPersist,
Events::postRemove,
Events::postFlush
];
}
public function preUpdate(PreUpdateEventArgs $args): void
{
$this->logger->debug('Something has been updated');
if($args->hasChangedField('Date')){
$this->logger->debug('The date has been changed.');
}
if($args->hasChangedField('Users')){
$this->logger->debug('It was the Users field');
}
}
// ... other methods emitted
I have gotten this system to work, but when I run this test code
$visitRepo = $this->om->getRepository(Visit::class);
$userRepo = $this->om->getRepository(User::class);
// you can assume that visit 7 and user 8 already exist in the database
$v = $visitRepo->find(7);
$u = $userRepo->find(8);
$v->setDate('2022-01-05');
$this->om->flush();
$v->addUser($u);
$this->om->flush();
The test code works without errors and I can see a new row in the sql-table "users_visits". I can also see the date for visit 7 has been changed to 2022-01-05.
BUT: Checking the log I can only find
Something has been updated.
The date has been changed.
Something has been updated.
There is no "It was the Users field". Using my debugging tools I can see that the EntityChangeSet is empty during preUpdate() during the $v->addUser($u), which is weird and unexpected.
I have extensively been reading the docs for the event PreUpdate but there is no mentioning of why changes to associated collections are not shown in the EntityChangeSet or how I could track those changes in an EventSubscriber.
Do I have to go via the rather cumbersome UnitOfWork during the onFlushEvent? Or this there something I have been missing?
In case I myself or somebody else reads this, my pretty dirty solution was to collect all currently updated collections and then check if the property ("Users" in this case) matched on of these.
$updatedCollectionNames = array_map(
function (PersistentCollection $update) {
return $update->getMapping()['fieldName'];
},
$args->getEntityManager()->getUnitOfWork()->getScheduledCollectionUpdates()
);
// and later
if (in_array('Users', $updatedCollectionNames, true)){
$this->logger->debug('It was the Users field');
}
This seems dirty and contrived, but it will do for now until somebody has a better proposal.

FOSUserBundle - perform function upon initial login

I have a Symfony 3 application with user management via FOSUserBundle. I want to implement a Listener class that checks for an initial user login, by checking the user's "last_login" value. While I did read the article on event subscribers, I struggle to find out when exactly the last_login value is updated.
Is there anyone with such knowledge, or does anyone know a better approach regarding a user's initial login?
I just had a look at this and it seems you can use an eventSubscriber to subscribe to the SecurityEvents::INTERACTIVE_LOGIN event to get where you need to be.
class RegistrationSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => [
['lastLogin', 150],
],
];
}
public function lastLogin(InteractiveLoginEvent $event){
$user = $this->tokenStorage->getToken()->getUser();
if($user->getLastLogin() == null){
//Do something
}
}
In my testing on the first login $user->getLastLogin() is null so you can put any logic you need here.
Symfony and FOSUserBundle have events for that. Even FOSUserBundle has an event subscriber for those events
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::SECURITY_IMPLICIT_LOGIN => 'onImplicitLogin',
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
);
}
you could subscribe for those events and make your own logic. There is no such thibg as initial login in symfony, you need to manage by your self.
Subscribe to the events and try something like this:
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event){
$user = $event->getAuthenticationToken()->getUser();
//some logic to check the user
if(!$user->getLastLogin()){
//its my first login!!! do what ever you want here
}else{
//I have already logged before so just pass
}
}
Also you need to change your subscriber priority to -10 or something like that to catch the event before FOSUserBundle or you will be passing always.
Hope it helps

Verify that the user can do an action

I have two entites Person and Nursery and a ManyToMany association between them.
A user can have the role ROLE_MANAGER and be a manager for several nurseries.
For that in every action on his dashboard I need to verify if he's linked to the nursery if I don't do it he can modify the nursery slug in the url and have access to a nursery that he is not linked with.
Is there a way to check that on every action in the nursery manager dashboard without copy/paste a verification code in every action ?
As I understood Symfony Events (or Voters ?) can do that but I've never used them before ...
EDIT : Maybe it's easier to understand with a little bit of code !
So my nursery dashboard function is :
public function dashboardAction($nursery_slug)
{
//$currentUser = $this->get('security.token_storage')->getToken()->getUser();
$nurseryRepo = $this->getDoctrine()->getRepository('VSCrmBundle:Nursery');
$nursery = $nurseryRepo->findOneBy(array('slug' => $nursery_slug));
// Sometimes may help
if(!$nursery)
{
throw $this->createNotFoundException("The nursery has not been found or you are not allowed to access it.");
}
return $this->render("VSCrmBundle:Manager:dashboard.html.twig", array(
'nursery' => $nursery
));
}
To protect this dashboard I need to verify if the current user is linked to the nursery, somethink like :
$verification = $nurseryRepo->findOneBy(array('person' => $currentUser));
if(!$verification){throw accessDeniedException();}
But at the moment I'm obliged to do this test on every action in the manager dashboard ....
There are two things you need to implement to make this work smoothly.
First off, you need a NurseryVoter: http://symfony.com/doc/current/security/voters.html
Something like:
class NurseryVoter extends Voter
{
const MANAGE = 'manage';
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::MANAGE))) {
return false;
}
if (!$subject instanceof Nursery) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $nursery, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// Check the role and do your query to verify user can manage specific nursery
Wire everything up per the link. And at this point your controller code is reduces to:
$this->denyAccessUnlessGranted('manage', $nursery);
Get all that working first. After that, use a Kernel:Controller event to move the deny access code from the controller to a listener. Follow the docs: http://symfony.com/doc/current/event_dispatcher.html
Your controller listener gets called after the controller is assigned but before the controller action is actually called. The trick here is how to determine which action actually needs the check to be done. There are a couple of approaches. Some folks like to flag the actual controller class perhaps by adding a NurseryManagerInterface. The listeners check the controller to see if it has the interface. But I don't really care for that.
I like to add this sort of stuff directly to the route. So I might have:
// routes.yml
manage_nursery:
path: /manage/{nursery}
defaults:
_controller: manage_nursery_action
_permission: CAN_MANAGE_NURSERY
Your listener would then check the permission.
Updated with a few more details on the kernel listener. Basically you inject the authorization checker and pull _permission from the request object.
class KernelListener implements EventSubscriberInterface
{
// #security.authorization_checker service
private $authorizationChecker;
public function __construct($authorizationChecker,$nuseryRepository)
{
$this->authorizationChecker = $authorizationChecker;
$this->nurseryRepository = $nuseryRepository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => [['onController']],
];
}
public function onController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$permission = $request->attributes->get('_permission');
if ($permission !== 'CAN_MANAGE_NURSERY') {
return;
}
$nursery = $this->nurseryRepository->find($request->attributes->get('nursery');
if ($this->authorizationChecker->isGranted('MANAGE',$nursery) {
return;
}
throw new AccessDeniedException('Some message');
}

Pass parameter to Entity

I am new to symfony. I want to be able to cofigure administrator role name to my application. I need to do something like: (in controller)
if($this->getUser()->isAdmin()) {
//..
}
In User Entity I could define isAdmin as:
function isAdmin()
{
$this->hasRole('ROLE_ADMIN');
}
but that way, ROLE_ADMIN can't be configured. Note that I don't want to pass 'a role name' as param (or default param) to isAdmin function. I want it like i can pass object to User Entity:
public function __construct(AuthConfiguration $config)
{
$this->config = $config;
}
public function isAdmin()
{
return $this->hasRole($this->config->getAdminRoleName());
}
But how can I pass object to user entity since user creation is handled by the repository ?
You can set up custom Doctrine DBAL ENUM Type for roles using this bundle: https://github.com/fre5h/DoctrineEnumBundle
<?php
namespace AppBundle\DBAL\Types;
use Fresh\Bundle\DoctrineEnumBundle\DBAL\Types\AbstractEnumType;
class RoleType extends AbstractEnumType
{
const ROLE_USER = 'ROLE_USER';
const ROLE_ADMIN = 'ROLE_ADMIN';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
const ROLE_PROJECT_OWNER = 'ROLE_PROJECT_OWNER';
/**
* #var array Readable choices
* #static
*/
protected static $choices = [
self::ROLE_USER => 'role.user',
self::ROLE_ADMIN => 'role.administrator',
self::ROLE_SUPER_ADMIN => 'role.super_administrator',
self::ROLE_PROJECT_OWNER => 'role.project_owner',
];
}
Register new type in config.yml:
doctrine:
dbal:
mapping_types:
enum: string
types:
RoleType: AppBundle\DBAL\Types\RoleType
Configure your user's role field as ENUM RoleType type:
use Fresh\Bundle\DoctrineEnumBundle\Validator\Constraints as DoctrineAssert;
...
/**
* #DoctrineAssert\Enum(entity="AppBundle\DBAL\Types\RoleType")
* #ORM\Column(name="role", type="RoleType")
*/
protected $role = RoleType::ROLE_USER;
And use it in your entity or repository or anywhere else this way:
use AppBundle\DBAL\Types\RoleType;
...
public function isAdmin()
{
$this->hasRole(RoleType::ROLE_ADMIN);
}
The constructor is only called when you create a new instance of the object with the keyword new. Doctrine does not call the constructor even when it hydrates entities.
You could potentially create your own entity hydrator and call the entity's constructor however I haven't tried this solution. It may not be as maintainable.
I want to provide an alternative which I prefer (you may not).
On all my projects, the architecture is as follow:
Controller <-> Service <-> Repository <-> Entity.
The advantage of this architecture is the use of dependency injection with services.
In your services.yml
services:
my.user:
class: Acme\HelloBundle\Service\MyUserService
arguments:
# First argument
# You could also define another service that returns
# a list of roles.
0:
admin: ROLE_ADMIN
user: ROLE_USER
In your service:
namespace Acme\HelloBundle\Service;
use Symfony\Component\Security\Core\User\UserInterface;
class MyUserService {
protected $roles = array();
public function __constructor($roles)
{
$this->roles = $roles;
}
public function isAdmin(UserInterface $user = null)
{
if ($user === null) {
// return current logged-in user
}
return $user->hasRole($this->roles['admin']);
}
}
In your controller:
// Pass a user
$this->get('my.user')->isAdmin($this->getUser());
// Use current logged-in user
$this->get('my.user')->isAdmin();
It's away from the solution you are looking for but in my opinion it seems more inline with what Symfony2 provides.
Another advantage is that you can extend the definition of an admin.
For example in my project, my user service has a isAdmin() method that has extra logic.

Sonata Admin Bundle: possible to add a child admin object that can have different parents?

I'm using doctrine inheritance mapping to enable various objects to be linked to a comment entity. This is achieved through various concrete "Threads", which have a one-to-many relationship with comments. So taking a 'Story' element as an example, there would be a related 'StoryThread' entity, which can have many comments.
That is all working fine, but I'm having troubles trying to define a CommentAdmin class for the SonataAdminBundle that can be used as a child of the parent entities. For example, I'd want to be able to use routes such as:
/admin/bundle/story/story/1/comment/list
/admin/bundle/media/gallery/1/comment/list
Does anyone have any pointers about how I can go about achieving this? I'd love to post some code extracts but I haven't managed to find any related documentation so don't really know the best place to start.
I've been trying to use the SonataNewsBundle as a reference because they've implemented a similar parent/child admin relationship between posts and comments, but it appears as though this relies on the 'comment' (child) admin class to be hardcoded to know that it belongs to posts, and it also seems as though it needs to have a direct many-to-one relationship with the parent object, whereas mine is through a separate "Thread" entity.
I hope this makes sense! Thanks for any help.
Ok I managed to get this working eventually. I wasn't able to benefit from using the $parentAssociationMapping property of the CommentAdmin class, as the parent entity of a comment is a concrete instance of the Thread entity whereas the parent 'admin' class in this case is a Story (which is linked via the StoryThread). Plus this will need to remain dynamic for when I implement comments on other types of entity.
First of all, I had to configure my StoryAdmin (and any other admin classes that will have CommentAdmin as a child) to call the addChild method:
acme_story.admin.story:
class: Acme\Bundle\StoryBundle\Admin\StoryAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: content, label: Stories }
arguments: [null, Acme\Bundle\StoryBundle\Entity\Story, AcmeStoryBundle:StoryAdmin]
calls:
- [ addChild, [ #acme_comment.admin.comment ] ]
- [ setSecurityContext, [ #security.context ] ]
This allowed me to link to the child admin section from the story admin, in my case from a side menu, like so:
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
// ...other side menu stuff
$menu->addChild(
'comments',
array('uri' => $admin->generateUrl('acme_comment.admin.comment.list', array('id' => $id)))
);
}
Then, in my CommentAdmin class, I had to access the relevant Thread entity based on the parent object (e.g a StoryThread in this case) and set this as a filter parameter. This is essentially what is done automatically using the $parentAssociationMapping property if the parent entity is the same as the parent admin, which it most likely will be if you aren't using inheritance mapping. Here is the required code from CommentAdmin:
/**
* #param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter
*/
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add('thread');
}
/**
* #return array
*/
public function getFilterParameters()
{
$parameters = parent::getFilterParameters();
return array_merge($parameters, array(
'thread' => array('value' => $this->getThread()->getId())
));
}
public function getNewInstance()
{
$comment = parent::getNewInstance();
$comment->setThread($this->getThread());
$comment->setAuthor($this->securityContext->getToken()->getUser());
return $comment;
}
/**
* #return CommentableInterface
*/
protected function getParentObject()
{
return $this->getParent()->getObject($this->getParent()->getRequest()->get('id'));
}
/**
* #return object Thread
*/
protected function getThread()
{
/** #var $threadRepository ThreadRepository */
$threadRepository = $this->em->getRepository($this->getParentObject()->getThreadEntityName());
return $threadRepository->findOneBy(array(
$threadRepository->getObjectColumn() => $this->getParentObject()->getId()
));
}
/**
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager($em)
{
$this->em = $em;
}
/**
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
*/
public function setSecurityContext(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
An alternative to your code for direct related entities :
public function getParentAssociationMapping()
{
// we grab our entity manager
$em = $this->modelManager->getEntityManager('acme\Bundle\Entity\acme');
// we get our parent object table name
$className = $em->getClassMetadata(get_class($this->getParent()->getObject($this->getParent()->getRequest()->get('id'))))->getTableName();
// we return our class name ( i lower it because my tables first characted uppercased )
return strtolower( $className );
}
be sure to have your inversedBy variable matching the $className in order to properly work

Categories