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;
}
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.
Voter
can decide about objects of certain class.Voter
can decide about this action.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