Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save technetium/0c62164400a411e9ffc3713260448b25 to your computer and use it in GitHub Desktop.
Save technetium/0c62164400a411e9ffc3713260448b25 to your computer and use it in GitHub Desktop.
Propagating awareness via ManyToOne associations

Propagating awareness via ManyToOne associations

Introduction

Michaël Perrin has written an article about using annotation and filters improve security.

With a more complex model, for example an order that contains products, you want also to filter on the associations of the filtered entity. In this case the products that the orders contain.

You can solve this by adding a user property to the product entity, but this risks compromising you're database integrity. Better is to make the ManyToOne assiciation from product to order also UserAware

Insprered by Steve's stackoverflow question, I've created a filter that recursively adds subqueries to the where clause until (an assiciation with) a user is found.

Implementation

The magic happens in the UserFilter class. The magic happens in the UserFilter class. More specifically the buildQuery method. This method recursively transverses the UserAware properties, until the the UserAware property is of type User, or is a reference to the User class. With each function call a sub query is padded to the basic query.

Configuration

The configuration is almost the same as in Michaël Perrin's piece. The only difference is an extra line is added in the Configurator.php to inject an ObjectManager into the UserFilter class.

$filter->setObjectManager($this->em);

Usage

When all in configurated the usage is very simple. Just add the UserAware annotation to the class. See the Product class for an example.

Remarks

In the UserAware class, I've also added the annotation userPropertyName. Since in the UserFilter class userPropertyName is now also needed next to userFieldName, It doesn't make sence to prefere to use the latter. In fact it's more convinient to use the first one userPropertyName, this way the impelmentator of the entitie classes doesn't have to know about the mapping of property names to field names.

<?php
namespace AppBundle\Filter;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Annotations\Reader;
class Configurator
{
protected $em;
protected $tokenStorage;
protected $reader;
public function __construct(ObjectManager $em, TokenStorageInterface $tokenStorage, Reader $reader)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
$this->reader = $reader;
}
public function onKernelRequest()
{
if ($user = $this->getUser()) {
$filter = $this->em->getFilters()->enable('user_filter');
$filter->setParameter('id', $user->getId());
$filter->setAnnotationReader($this->reader);
// The filter needs an Object manager, set it here
$filter->setObjectManager($this->em);
}
}
private function getUser()
{
$token = $this->tokenStorage->getToken();
if (!$token) {
return null;
}
$user = $token->getUser();
if (!($user instanceof UserInterface)) {
return null;
}
return $user;
}
}
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Annotations\UserAware;
/**
*
* @ORM\Entity
* @UserAware(userPropertyFieldName="order")
*/
class product {
...
/**
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
* @ORM\JoinColumn(nullable=false)
*/
private $order;
...
}
<?php
namespace AppBundle\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* @Annotation
* @Target("CLASS")
*/
final class UserAware
{
public $userPropertyName;
public $userFieldName; // Kept for backwards compatibility reasons
}
<?php
// Propagating awareness via ManyToOne associations
// Based on Michaël Perrin's filter
// @see http://www.michaelperrin.fr/2014/07/25/doctrine-filters/
// And inspred by Steve's idea of using subqueries
// @see http://stackoverflow.com/questions/12354285/how-can-i-implemented-a-doctrine2-filter-based-on-a-relation
namespace AppBundle\Filter;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use AppBundle\Annotations\UserAware as UserAware;
define('USER_ENTITY', 'AppBundle\Entity\User');
define('USER_AWARE', 'AppBundle\\Annotations\\Userware');
class UserFilter extends SQLFilter
{
public $reader; // The annotation reader
public $manager; // The entity manager
private function getPropertyAndFieldName(
ClassMetadata $targetEntity,
UserAware $userAware)
{
$propertyName = $userAware->userPropertyName;
if ($propertyName)
{
$fieldName =
$targetEntity->isIdentifier($propertyName) ?
$targetEntity->getColumnName($propertyName) :
$targetEntity->getSingleAssociationJoinColumnName($propertyName);
}
else
{
// Try to get the information via the fieldname,
// for backwards compatability reasons only
$fieldName = $userAware->userFieldName;
$propertyName = $targetEntity->getFieldForColumn($fieldName);
}
return array($propertyName, $fieldName);
}
private function buildBasicQuery($targetTableAlias, $fieldName)
{
try
{
// Don't worry, getParameter automatically quotes parameters
$user_id = $this->getParameter('user_id');
}
catch (\InvalidArgumentException $e) {
// No user id has been defined
return '';
}
// Something went wrong
if (empty($fieldName) || empty($user_id)) {
// just to be sure make sure nothing gets returned
return '0'; // false;
}
$query = sprintf(
'(%1$s.%2$s = %3$s)',
$targetTableAlias, $fieldName, $user_id
);
return $query;
}
private function buildQuery(ClassMetadata $targetEntity, $targetTableAlias, UserAware $userAware)
{
list($propertyName, $fieldName) =
$this->getPropertyAndFieldName($targetEntity, $userAware);
$targetClassName =
$targetEntity->hasAssociation($propertyName) ?
$targetEntity->getAssociationTargetClass($propertyName) :
USER_ENTITY;
if (USER_ENTITY === $targetClassName)
{
return $this->buildBasicQuery($targetTableAlias, $fieldName);
}
$reflClass = new \ReflectionClass($targetClassName);
$userAware = $this->reader->getClassAnnotation($reflClass, USER_AWARE);
if (!$userAware)
{
throw new \Exception('Association of UserAware entity isn\'t UserAware Exception');
}
$classMetaData = $this->manager->getClassMetadata($targetClassName);
$tableName = $classMetaData->getTableName();
$identifier = $classMetaData->getSingleIdentifierFieldName();
$query =
sprintf('(%s.%s IN (SELECT %s FROM %s WHERE ', $targetTableAlias, $fieldName, $identifier, $tableName) .
$this->buildQuery($classMetaData, $tableName, $userAware) . '))';
return $query;
}
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (!$this->reader)
{
return '';
}
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "user aware" (marked with an annotation)
$userAware = $this->reader->getClassAnnotation(
$targetEntity->getReflectionClass(),
USER_AWARE
);
if (!$userAware)
{
return '';
}
return $this->buildQuery($targetEntity, $targetTableAlias, $userAware);
}
public function setAnnotationReader(Reader $reader)
{
$this->reader = $reader;
}
// The EntityManager (em) in the parent is private,
// we have to add it to this class another way :-(
public function setObjectManager(ObjectManager $manager)
{
$this->manager = $manager;
}
}
@ldesmeules
Copy link

Thanks you so much!
But have you any solution with ManyToMany relation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment