diff --git a/Doctrine/AbstractListener.php b/Doctrine/AbstractListener.php index a7e8867..7a88e4b 100644 --- a/Doctrine/AbstractListener.php +++ b/Doctrine/AbstractListener.php @@ -6,6 +6,9 @@ use Doctrine\Common\EventSubscriber; use Doctrine\Common\Persistence\ObjectManager; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; use FOS\ElasticaBundle\Persister\ObjectPersister; +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\SyntaxError; abstract class AbstractListener implements EventSubscriber { @@ -51,6 +54,13 @@ abstract class AbstractListener implements EventSubscriber */ private $scheduledForRemoval = array(); + /** + * An instance of ExpressionLanguage + * + * @var ExpressionLanguage + */ + protected $expressionLanguage; + /** * Constructor. * @@ -89,8 +99,21 @@ abstract class AbstractListener implements EventSubscriber public function setIsIndexableCallback($callback) { if (is_string($callback)) { + if (!is_callable(array($this->objectClass, $callback))) { - throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable.', $this->objectClass, $callback)); + + if(false !== ($expression = $this->getExpressionLanguage())) { + + $callback = new Expression($callback); + + try { + $expression->compile($callback, array($this->getExpressionVar())); + } catch(SyntaxError $e) { + throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable or a valid expression.', $this->objectClass, $callback), 0, $e); + } + } else { + throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable.', $this->objectClass, $callback)); + } } } elseif (!is_callable($callback)) { if (is_array($callback)) { @@ -98,6 +121,7 @@ abstract class AbstractListener implements EventSubscriber if (is_object($class)) { $class = get_class($class); } + if ($class && $method) { throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable.', $class, $method)); } @@ -120,6 +144,10 @@ abstract class AbstractListener implements EventSubscriber return true; } + if($this->isIndexableCallback instanceof Expression) { + return $this->getExpressionLanguage()->evaluate($this->isIndexableCallback, array($this->getExpressionVar($object) => $object)); + } + return is_string($this->isIndexableCallback) ? call_user_func(array($object, $this->isIndexableCallback)) : call_user_func($this->isIndexableCallback, $object); @@ -155,4 +183,33 @@ abstract class AbstractListener implements EventSubscriber unset($this->scheduledForRemoval[$objectHash]); } } + + /** + * @param mixed $object + * @return string + */ + private function getExpressionVar($object = null) + { + $class = $object ?: $this->objectClass; + + $ref = new \ReflectionClass($class); + + return strtolower($ref->getShortName()); + } + + /** + * @return bool|ExpressionLanguage + */ + private function getExpressionLanguage() + { + if(null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + return false; + } + + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } } diff --git a/README.md b/README.md index 1f6cf06..6a9b899 100644 --- a/README.md +++ b/README.md @@ -630,6 +630,13 @@ In this case, the callback_class will be the `isIndexable()` method on the speci service and the object being considered for indexing will be passed as the only argument. This allows you to do more complex validation (e.g. ACL checks). +If you have the [Symfony ExpressionLanguage](https://github.com/symfony/expression-language) component installed, you can use expressions +to evaluate the callback: + + persistence: + listener: + is_indexable_callback: "user.isActive() && user.hasRole('ROLE_USER')" + As you might expect, new entities will only be indexed if the callback_class returns `true`. Additionally, modified entities will be updated or removed from the index depending on whether the callback_class returns `true` or `false`, respectively. diff --git a/Tests/Doctrine/AbstractListenerTest.php b/Tests/Doctrine/AbstractListenerTest.php index e3af609..e99e26d 100644 --- a/Tests/Doctrine/AbstractListenerTest.php +++ b/Tests/Doctrine/AbstractListenerTest.php @@ -164,6 +164,7 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase array('nonexistentEntityMethod'), array(array(new Listener\IndexableDecider(), 'internalMethod')), array(42), + array('entity.getIsIndexable() && nonexistentEntityFunction()'), ); } @@ -173,6 +174,7 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase array('getIsIndexable'), array(array(new Listener\IndexableDecider(), 'isIndexable')), array(function(Listener\Entity $entity) { return $entity->getIsIndexable(); }), + array('entity.getIsIndexable()') ); } diff --git a/composer.json b/composer.json index 099868b..094eba5 100644 --- a/composer.json +++ b/composer.json @@ -24,14 +24,16 @@ "doctrine/mongodb-odm": "1.0.*@dev", "propel/propel1": "1.6.*", "pagerfanta/pagerfanta": "1.0.*@dev", - "knplabs/knp-components": "1.2.*" + "knplabs/knp-components": "1.2.*", + "symfony/expression-language" : "2.4.*@dev" }, "suggest": { "doctrine/orm": ">=2.2,<2.5-dev", "doctrine/mongodb-odm": "1.0.*@dev", "propel/propel1": "1.6.*", "pagerfanta/pagerfanta": "1.0.*@dev", - "knplabs/knp-components": "1.2.*" + "knplabs/knp-components": "1.2.*", + "symfony/expression-language" : "2.4.*@dev" }, "autoload": { "psr-0": { "FOS\\ElasticaBundle": "" }