Mike
Mike

Reputation: 2374

Symfony2 / Doctrine - Modifying all queries

Is it possible to run all doctrine queries through a walker of some sort so that I can modify the query based on the current user's credentials? Ideally, I wouldn't have to explicitly call a setHint for a custom walker on every query, as that would restrict my ability to pass the current SecurityContext into the walker.

Also, I'd prefer not to use a Doctrine Filter, as I can't modify join conditions with filters, and I'd be forced to use an "IN" clause, which would severely affect performance

Currently, I'm using a service that modifies the QueryBuilder based on a user's credentials, but this becomes tedious, as I need to call the service every time I create a new QueryBuilder, and is even more of a pain when Repositories come into play (as I'd need to inject the service into every repository that needs to modify the query.

Hopefully I've explained this clearly enough. Appreciate any feedback!

Upvotes: 4

Views: 3017

Answers (2)

Artur Eshenbrener
Artur Eshenbrener

Reputation: 2020

You can write a custom AST Walker and setup your application to use this walker for all queries with defaultQueryHint (Doctrine 2.5 new feature) configuration option:

<?php
/** @var \Doctrine\ORM\EntityManager $em */
$em->getConfiguration()->setDefaultQueryHint(
    Query::HINT_CUSTOM_TREE_WALKERS,
    ['YourWalkerFQClassName']
)

Upvotes: 1

Mike
Mike

Reputation: 2374

I think I have solved my own issue. If someone else has a more elegant way of doing achieving these results, feel free to explain. In order to modify all of my queries, I have created a custom EntityManager and custom EntityRepository.

In my custom EntityManager, I have overwritten 2 methods. create() and getRepository()

public static function create($conn, Configuration $config, EventManager $eventManager = null)
{
    if ( ! $config->getMetadataDriverImpl()) {
        throw ORMException::missingMappingDriverImpl();
    }

    switch (true) {
        case (is_array($conn)):
            $conn = \Doctrine\DBAL\DriverManager::getConnection(
                $conn, $config, ($eventManager ?: new EventManager())
            );
            break;

        case ($conn instanceof Connection):
            if ($eventManager !== null && $conn->getEventManager() !== $eventManager) {
                 throw ORMException::mismatchedEventManager();
            }
            break;

        default:
            throw new \InvalidArgumentException("Invalid argument: " . $conn);
    }

    return new MyCustomEntityManager($conn, $config, $conn->getEventManager());
}

The only thing that is changed in this method is that I am returning my own EntityManger(MyCustomEntityManager). Then, I overlaid the getRepository method as follows:

public function getRepository($entityName)
{
    $entityName = ltrim($entityName, '\\');

    if (isset($this->repositories[$entityName])) {
        return $this->repositories[$entityName];
    }

    $metadata = $this->getClassMetadata($entityName);
    $repositoryClassName = $metadata->customRepositoryClassName;

    if ($repositoryClassName === null) {
        $repositoryClassName = "Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository";
    }

    $repository = new $repositoryClassName($this, $metadata);

    $this->repositories[$entityName] = $repository;

    return $repository;
}

Here, I have only modified one line as well. Instead of relying on the DBAL Configuration to retreive the default $repositoryClassName, I have specified my own default repository Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository.

Once you have created your own custom EntityRepository, the sky is the limit. You can inject services into the repository(I currently use JMS Di annotations, described below), or perform custom actions against a QueryBuilder in the createQueryBuilder method, like so:

use JMS\DiExtraBundle\Annotation as DI;

class MyCustomEntityRepository extends EntityRepository
{
    private $myService;

    public function createQueryBuilder($alias)
    {
         $queryBuilder = parent::createQueryBuilder($alias);

          /** INSERT CUSTOM CODE HERE **/

          return $queryBuilder;
    }

    /**
    * @DI\InjectParams({
    *     "myService" = @DI\Inject("my_service_id")
    * })
    */
    public function setMyService(MyServiceInterface $myService)
    {
        $this->myService = $myService;
    }
}

Once you have created your own EntityRepository, you should have all of your repositories that need this custom functionality extend MyCustomEntityRepository. You could even take it a step further and create your own QueryBuilder to further extend this.

Upvotes: 1

Related Questions