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.
Related
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.
is it possible to create a relation to a generic table/class whith Doctrine?
Here is some code to make it easier to understand:
// class Log...
// TODO:
// It could be useful to have a reference to
// the element mentioned by the log, the issue is
// we don't know what kind of entity it is.
/**
* #ORM\ManyToOne(targetEntity="???")
*/
private $elementId
Maybe instead of using targetEntity I could just use an int that is the id of the element located in the unknow table.
There is no built-in possibility now.
Let me propose a work around using Doctrine Lifecycle Events :
Create 3 properties :
/*
* #ORM\Column(name="element_class", type="string")
*/
private $elementClass
/*
* #ORM\Column(name="element_id", type="integer")
*/
private $elementId
// Not mapped
private $element
public function setElement($element)
{
$this->element = $element;
$this->elementClass = get_class($element);
$this->elementId = $element->getId();
}
public function getElement()
{
return $this->element;
}
// You need these for the PostLoad event listener :
public function hydrateElementPostLoad($element)
{
$this->element = $element;
}
public function getElementClass()
{
return $this->elementClass;
}
public function getElementId()
{
return $this->elementId;
}
Then create a PostLoadListener able to hydrate the element property :
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Log;
class PostLoadListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if($entity instanceOf Log){
$em = $args->getEntityManager();
$entity->hydrateElementPostLoad(
$this->em->getRepository($entity->getElementClass())->findOneById($entity->getElementId())
);
}
}
}
And register this event in your services.yml :
services:
places.listener:
class: AppBundle\EventListener\PostLoadListener
tags:
- { name: doctrine.event_listener, event: postLoad }
That's also how the most famous Bundle for logging works (The Gedmo DoctrineExtensions Logger)
To retrieve all logs for an entity, create a repository method for your Log entity :
getLogs($entity)
{
return $this->_em->findBy(array(
'element_id'=>$entity->getId(),
'element_class'=>get_class($entity)
));
}
You are trying to manage some abstraction of one or more of your entities in the database level which is a headache,
Doctrine already has proposed Somme solutions to manage this kind of abstractions by using Inheritance Mapping
A mapped superclass is an abstract or concrete class that provides persistent entity state and mapping information for its subclasses, but which is not itself an entity. Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.
For more information check this
In a Symfony2 application, I have an entity that needs to be populated on pre-persist with various context properties (like user id, what page it was called from, etc.)
I figured that to do this, I need to add a doctrine event listener that has access to "service_container", and the best way to give such access is to pass "service_container" as an argument to this listener.
I have a specific entity that I want to listen to, and I do not want to trigger the listener to events with any other entity.
We can add an entity-specific listener, documentation is found here:
http://docs.doctrine-project.org/en/latest/reference/events.html#entity-listeners
- but this does not provide example of how to pass an argument (I use PHP annotations to declare the listener).
I also tried to use JMSDiExtraBundle annotations, like in the example below:
http://jmsyst.com/bundles/JMSDiExtraBundle/master/annotations#doctrinelistener-or-doctrinemongodblistener
- but this way requires to declare the listener as non-entity-specific
Is there any way to make a listener for one entity only, and have it have access to container?
One of the ways similar to doctrine docs through dependency injection:
<?php
namespace AppBundle\EntityListener;
use AppBundle\Entity\User;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouterInterface;
class UserListener {
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function postPersist(User $user, LifecycleEventArgs $args)
{
$logger = $this->logger;
$logger->info('Event triggered');
//Do something
}
}
services:
user.listener:
class: AppBundle\EntityListener\UserListener
arguments: [#logger]
tags:
- { name: doctrine.orm.entity_listener }
And dont forget add listener to entity mapping:
AppBundle\Entity\User:
type: entity
table: null
repositoryClass: AppBundle\Entity\UserRepository
entityListeners:
AppBundle\EntityListener\UserListener: ~
I would simply check entity type from the event. If you check type inside or outside the subscriber, it has the same performance cost. And simple type condition is fast enough.
namespace App\Modules\CoreModule\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
class SetCountryToTaxSubscriber implements EventSubscriber
{
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [Events::prePersist];
}
public function prePersist(LifecycleEventArgs $lifecycleEventArgs)
{
$entity = $lifecycleEventArgs->getEntity();
if ( ! $entity instanceof Tax) {
return;
}
$entity->setCountry('myCountry');
}
}
I found this piece of code shared in a Gist (somewhere I lost the link) and I needed something like that so I started to use in my application but I have not yet fully understood and therefore I am having some problems.
I'm trying to create dynamic menus with KnpMenuBundle and dynamic means, at some point I must verify access permissions via database and would be ideal if I could read the routes from controllers but this is another task, perhaps creating an annotation I can do it but I will open another topic when that time comes.
Right now I need to access the SecurityContext to check if the user is logged or not but not know how.
I'm render the menu though RequestVoter (I think) and this is the code:
namespace PlantillaBundle\Menu;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
class RequestVoter implements VoterInterface {
private $container;
private $securityContext;
public function __construct(ContainerInterface $container, SecurityContextInterface $securityContext)
{
$this->container = $container;
$this->securityContext = $securityContext;
}
public function matchItem(ItemInterface $item)
{
if ($item->getUri() === $this->container->get('request')->getRequestUri())
{
// URL's completely match
return true;
}
else if ($item->getUri() !== $this->container->get('request')->getBaseUrl() . '/' && (substr($this->container->get('request')->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri()))
{
// URL isn't just "/" and the first part of the URL match
return true;
}
return null;
}
}
All the code related to securityContext was added by me in a attempt to work with it from the menuBuilder. Now this is the code where I'm making the menu:
namespace PlantillaBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAware;
class MenuBuilder extends ContainerAware {
public function mainMenu(FactoryInterface $factory, array $options)
{
// and here is where I need to access securityContext
// and in the near future EntityManger
$user = $this->securityContext->getToken()->getUser();
$logged_in = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav');
if ($logged_in)
{
$menu->addChild('Home', array('route' => 'home'))->setAttribute('icon', 'fa fa-list');
}
else
{
$menu->addChild('Some Menu');
}
return $menu;
}
}
But this is complete wrong since I'm not passing securityContext to the method and I don't know how to and I'm getting this error:
An exception has been thrown during the rendering of a template
("Notice: Undefined property:
PlantillaBundle\Menu\MenuBuilder::$securityContext in
/var/www/html/src/PlantillaBundle/Menu/MenuBuilder.php line 12") in
/var/www/html/src/PlantillaBundle/Resources/views/menu.html.twig at
line 2.
The voter is defined in services.yml as follow:
plantilla.menu.voter.request:
class: PlantillaBundle\Menu\RequestVoter
arguments:
- #service_container
- #security.context
tags:
- { name: knp_menu.voter }
So, how I inject securityContext (I'll not ask for EntityManager since I asume will be the same procedure) and access it from the menuBuilder?
Update: refactorizing code
So, following #Cerad suggestion I made this changes:
services.yml
services:
plantilla.menu_builder:
class: PlantillaBundle\Menu\MenuBuilder
arguments: ["#knp_menu.factory", "#security.context"]
plantilla.frontend_menu_builder:
class: Knp\Menu\MenuItem # the service definition requires setting the class
factory_service: plantilla.menu_builder
factory_method: createMainMenu
arguments: ["#request_stack"]
tags:
- { name: knp_menu.menu, alias: frontend_menu }
MenuBuilder.php
namespace PlantillaBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class MenuBuilder {
/**
* #var Symfony\Component\Form\FormFactory $factory
*/
private $factory;
/**
* #var Symfony\Component\Security\Core\SecurityContext $securityContext
*/
private $securityContext;
/**
* #param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory, $securityContext)
{
$this->factory = $factory;
$this->securityContext = $securityContext;
}
public function createMainMenu(RequestStack $request)
{
$user = $this->securityContext->getToken()->getUser();
$logged_in = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');
$menu = $this->factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav');
if ($logged_in)
{
$menu->addChild('Home', array('route' => 'home'))->setAttribute('icon', 'fa fa-list');
}
else
{
$menu->addChild('Some Menu');
}
return $menu;
}
}
Abd ib my template just render the menu {{ knp_menu_render('frontend_menu') }} but now I loose the FontAwesome part and before it works, why?
Your menu builder is ContainerAware, so I guess that in it you should access the SecurityContext via $this->getContainer()->get('security.context').
And you haven't supplied any use cases for the voter class, so I'm guessing you're not using the matchItem method.
You should definitely try to restructure your services so that the dependencies are obvious.
Per your comment request, here is what your menu builder might look like:
namespace PlantillaBundle\Menu;
use Knp\Menu\FactoryInterface;
class MenuBuilder {
protected $securityContext;
public function __construct($securityContext)
{
$this->securityContext = $securityContext;
}
public function mainMenu(FactoryInterface $factory, array $options)
{
// and here is where I need to access securityContext
// and in the near future EntityManger
$user = $this->securityContext->getToken()->getUser();
...
// services.yml
plantilla.menu.builder:
class: PlantillaBundle\Menu\MenuBuilder
arguments:
- '#security.context'
// controller
$menuBuilder = $this->container->get('plantilla.menu.builder');
Notice that there is no need to make the builder container aware since you only need the security context service. You can of course inject the entity manager as well.
================================
With respect to the voter stuff, right now you are only checking to see if a user is logged in. So no real need for voters. But suppose that certain users (administrators etc) had access to additional menu items. You can move all the security checking logic to the voter. Your menu builder code might then look like:
if ($this->securityContext->isGranted('view','homeMenuItem')
{
$menu->addChild('Home', array('route' ...
In other words, you can get finer controller over who gets what menu item.
But get your MenuBuilder working first then add the voter stuff if needed.
Im seraching over and cannot find answer.
I have database role model in my application. User can have a role but this role must be stored into database.
But then user needs to have default role added from database. So i created a service:
<?php
namespace Alef\UserBundle\Service;
use Alef\UserBundle\Entity\Role;
/**
* Description of RoleService
*
* #author oracle
*/
class RoleService {
const ENTITY_NAME = 'AlefUserBundle:Role';
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findAll()
{
return $this->em->getRepository(self::ENTITY_NAME)->findAll();
}
public function create(User $user)
{
// possibly validation here
$this->em->persist($user);
$this->em->flush($user);
}
public function addRole($name, $role) {
if (($newrole = findRoleByRole($role)) != null)
return $newrole;
if (($newrole = findRoleByName($name)) != null)
return $newrole;
//there is no existing role
$newrole = new Role();
$newrole->setName($name);
$newrole->setRole($role);
$em->persist($newrole);
$em->flush();
return $newrole;
}
public function getRoleByName($name) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('name' => $name));
}
public function getRoleByRole($role) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('role' => $role));
}
}
my services.yml is:
alef.role_service:
class: Alef\UserBundle\Service\RoleService
arguments: [%doctrine.orm.entity_manager%]
And now I want to use it in two places:
UserController and User entity. How can i get them inside entity?
As for controller i think i just need to:
$this->get('alef.role_service');
But how to get service inside entity?
You don't. This is a very common question. Entities should only know about other entities and not about the entity manager or other high level services. It can be a bit of a challenge to make the transition to this way of developing but it's usually worth it.
What you want to do is to load the role when you load the user. Typically you will end up with a UserProvider which does this sort of thing. Have you read through the sections on security? That should be your starting point:
http://symfony.com/doc/current/book/security.html
The reason why it's so difficult to get services into entities in the first place is that Symfony was explicitly designed with the intent that services should never be used inside entities. Therefore, the best practice answer is to redesign your application to not need to use services in entities.
However, I have found there is a way to do it that does not involve messing with the global kernel.
Doctrine entities have lifeCycle events which you can hook an event listener to, see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events For the sake of the example, I'll use postLoad, which triggers soon after the Entity is created.
EventListeners can be made as services which you inject other services into.
Add to app/config/config.yml:
services:
example.listener:
class: Alef\UserBundle\EventListener\ExampleListener
arguments:
- '#alef.role_service'
tags:
- { name: doctrine.event_listener, event: postLoad }
Add to your Entity:
use Alef\UserBundle\Service\RoleService;
private $roleService;
public function setRoleService(RoleService $roleService) {
$this->roleService = $roleService;
}
And add the new EventListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Service\RoleService;
class ExampleListener
{
private $roleService;
public function __construct(RoleService $roleService) {
$this->roleService = $roleService;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(method_exists($entity, 'setRoleService')) {
$entity->setRoleService($this->roleService);
}
}
}
Just keep in mind this solution comes with the caveat that this is still the quick and dirty way, and really you should consider redesigning your application the proper way.
Thanks to Kai's answer above which answer to the question, but it's not compatible with symfony 5.x .
It's good to precise it's a bad practice, but required in some special case like legacy code or a bad DB design (as a temporary solution before schema migration)
As in my case, I use this code with a mailer and translator, which introduce an issue with the private property if Symfony >= 5.3 , so here the solution for recent version of symfony:
in config/services.yaml:
services:
Alef\UserBundle\EventListener\ExampleListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
ExampleListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Entity\Role;
class ExampleListener
{
public function postLoad(LifecycleEventArgs $postLoad): void
{
$entity = $postLoad->getEntity();
if ($entity instanceof User) {
$repository = ;
$entity->roleRepository(
$postLoad->getEntityManager()->getRepository(Role::class)
);
}
}
}
And in your Entity (or in a trait if you use it in more than one entity):
use Alef\UserBundle\Service\RoleService;
/** #internal bridge for legacy schema */
public function roleRepository(?RoleRepository $repository = null) {
static $roleRepository;
if (null !== $repository) {
$roleRepository = $repository;
}
return $roleRepository;
}
public function getRoleByName($name) {
return $this->roleRepository()->findBy(array('name' => $name));
}