Search code examples
phpsymfonysymfony-security

Using one class Voter for many entities


This is the function supportsClass in class Voter

http://symfony.com/doc/2.5/cookbook/security/voters_data_permission.html

 public function supportsClass($class)
{
    $supportedClass = 'AppBundle\Entity\Post';

    return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}

I'd like to know if it is possible to use one class voter for many entities in the same bundle or I have to create a Voter for each entity ?

EDIT I have found this solution:

 public function supportsClass($class)
{
    $classes = array(
        'Project\AgenceBundle\Entity\Agence',
        'Project\AgenceBundle\Entity\SubAgence',
        'Project\AgenceBundle\Entity\Subscription'
    );
    $ok = false;
    foreach($classes as $supportedClass)
    $ok = $ok || $supportedClass === $class || is_subclass_of($class, $supportedClass);

    return $ok;
}

Solution

  • In short yes, you can reuse your Voter as much as you want. For example your voter can work against interface.

    However you shouldn't use voter to judge too many things just to save few lines of code. Probably if voter can judge some set of objects that are not derivate class but they have something in common. Which in turn is good place for interface and trait. And so voter should work against that interface. Thats what interfaces are there for, to give you contract.

    If you have array of classes in supportsClass and than in future you change something in one of them. You might break Voter for that class, but since it is not bound by interface, no static analysis or PHP interpretr will catch it. And that is quite a problem.

    As you can see Voter is build from 3 parts.

    • supportsClass which tells Symfony whether this Voter can decide about objects of certain class.
    • supportsAttribute which tells Symfony whether this Voter can decide about this action.
    • vote Based on passed object it decides if it yes/no/dunno

    This is not exactly how it works. But it should give you idea what it is voter is there for.

    You:

    //You in controller
     if (!$this->get('security.context')->isGranted('edit', $object)) {
         throw new AuthenticationException('Not a step furher chap!');
     }
    

    Framework:

    //security.context
    //again it is rough idea what it does for real implementation check Symfoy github
    public function isGranted($action, $object) {
      //There it goes trough all voters from all bundles!
      foreach ($this->voters as $voter) {
          if (!$voter->supportsClass(get_class($object))) {
              //this voter doesn't care about this object
              continue;
          }
    
          if (!$voter->supportsAttribute($action)) {
              //this voter does care about this object but not about this action on it
              continue;
          }
    
          //This voter is there to handle this object and action, so lest se what it has to say about it
          $answer = $voter->vote(..);
          ...some more logic
      }
    }
    

    Weird example from top of my head:

    interface Owneable {
        public function getOwnerId();
    }
    
    trait Owned {
    
        /**
         * @ORM....
         */ 
        protected $ownerId;
    
        public function getOwnerId() {
            return $this->ownerId;
        }
    
        public function setOwnerId($id) {
            $this->ownerId = $id;
        }
    }
    
    class Post implements Owneable {
       use Owned;
    }
    
    class Comment implements Owneable {
       use Owned;
    }
    
    class OwnedVoter implements VoterInterface
    {
    
        public function supportsAttribute($attribute)
        {
            return $attribute === 'edit';
        }
    
        public function supportsClass($class)
        {
            //same as return is_subclass_of($class, 'Owneable');
            $interfaces = class_implements($class);
            return in_array('Owneable' , $interfaces);
        }
    
        public function vote(TokenInterface $token, $ownedObject, array $attributes)
        {
            if (!$this->supportsClass(get_class($ownedObject))) {
                return VoterInterface::ACCESS_ABSTAIN;
            }
    
    
            if (!$this->supportsAttribute($attributes[0])) {
                return VoterInterface::ACCESS_ABSTAIN;
            }
    
    
            $user = $token->getUser();
            if (!$user instanceof UserInterface) {
                return VoterInterface::ACCESS_DENIED;
            }
    
    
    
    
            $userOwnsObject = $user->getId() === $ownedObject->getOwnerId();
            if ($userOwnsObject) {
                return VoterInterface::ACCESS_GRANTED;
            }
    
    
            return VoterInterface::ACCESS_DENIED;
        }
    }
    

    TIP: Voter is just class as any other, things like inheritance and abstract class work here too!

    TIP2: Voter is registered as service you can pass security.context or any other service to it. So you can reuse your code pretty well