[Listener] Support additional indexable callback types

Support service/method tuples as indexable callbacks. Closures are also supported, although they cannot be injected by the service container.

The indexable callback is now injected via a setter and validated when set (instead of during event processing).
This commit is contained in:
Jeremy Mikola 2012-04-10 17:23:09 -04:00
parent 78a36c196d
commit 86cdaa7c3c
6 changed files with 215 additions and 70 deletions

View file

@ -91,7 +91,6 @@ class Configuration
->thenInvalid('The driver %s is not supported. Please choose one of '.json_encode($this->supportedDrivers))
->end()
->end()
->scalarNode('is_indexable_callback')->defaultNull()->end()
->scalarNode('identifier')->defaultValue('id')->end()
->arrayNode('provider')
->children()
@ -107,6 +106,7 @@ class Configuration
->scalarNode('update')->defaultTrue()->end()
->scalarNode('delete')->defaultTrue()->end()
->scalarNode('service')->end()
->scalarNode('is_indexable_callback')->defaultNull()->end()
->end()
->end()
->arrayNode('finder')
@ -167,7 +167,6 @@ class Configuration
->thenInvalid('The driver %s is not supported. Please choose one of '.json_encode($this->supportedDrivers))
->end()
->end()
->scalarNode('is_indexable_callback')->defaultNull()->end()
->scalarNode('model')->end()
->scalarNode('repository')->end()
->scalarNode('identifier')->defaultValue('id')->end()
@ -185,6 +184,7 @@ class Configuration
->scalarNode('update')->defaultTrue()->end()
->scalarNode('delete')->defaultTrue()->end()
->scalarNode('service')->end()
->scalarNode('is_indexable_callback')->defaultNull()->end()
->end()
->end()
->arrayNode('finder')

View file

@ -305,12 +305,14 @@ class FOQElasticaExtension extends Extension
$listenerDef->replaceArgument(0, new Reference($objectPersisterId));
$listenerDef->replaceArgument(1, $typeConfig['model']);
$listenerDef->replaceArgument(3, $typeConfig['identifier']);
$listenerDef->replaceArgument(4, $typeConfig['is_indexable_callback']);
$listenerDef->replaceArgument(2, $this->getDoctrineEvents($typeConfig));
switch ($typeConfig['driver']) {
case 'orm': $listenerDef->addTag('doctrine.event_subscriber'); break;
case 'mongodb': $listenerDef->addTag('doctrine.odm.mongodb.event_subscriber'); break;
}
if (isset($typeConfig['listener']['is_indexable_callback'])) {
$listenerDef->addMethodCall('setIsIndexableCallback', array($typeConfig['listener']['is_indexable_callback']));
}
$container->setDefinition($listenerId, $listenerDef);
return $listenerId;

View file

@ -2,10 +2,11 @@
namespace FOQ\ElasticaBundle\Doctrine;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\ObjectManager;
use FOQ\ElasticaBundle\Persister\ObjectPersisterInterface;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
abstract class AbstractListener
abstract class AbstractListener implements EventSubscriber
{
/**
* Object persister
@ -28,39 +29,123 @@ abstract class AbstractListener
*/
protected $events;
/**
* Name of domain model field used as the ES identifier
*
* @var string
*/
protected $esIdentifierField;
protected $scheduledForRemoval;
/**
* Callback for determining if an object should be indexed
*
* @var mixed
*/
protected $isIndexableCallback;
/**
* Constructor
**/
public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $events, $esIdentifierField = 'id', $isIndexableCallback = null)
* Objects scheduled for removal
*
* @var array
*/
private $scheduledForRemoval = array();
/**
* Constructor.
*
* @param ObjectPersisterInterface $objectPersister
* @param string $objectClass
* @param array $events
* @param string $esIdentifierField
*/
public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $events, $esIdentifierField = 'id')
{
$this->objectPersister = $objectPersister;
$this->objectClass = $objectClass;
$this->events = $events;
$this->esIdentifierField = $esIdentifierField;
$this->scheduledForRemoval = array();
$this->isIndexableCallback = $isIndexableCallback;
}
/**
* @return array
* @see Doctrine\Common\EventSubscriber::getSubscribedEvents()
*/
public function getSubscribedEvents()
{
return $this->events;
}
protected function scheduleForRemoval($object, $objectManager)
/**
* Set the callback for determining object index eligibility.
*
* If callback is a string, it must be public method on the object class
* that expects no arguments and returns a boolean. Otherwise, the callback
* should expect the object for consideration as its only argument and
* return a boolean.
*
* @param callback $callback
* @throw RuntimeException if the callback is not callable
*/
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));
}
} elseif (!is_callable($callback)) {
if (is_array($callback)) {
list($class, $method) = $callback + array(null, null);
if (is_object($class)) {
$class = get_class($class);
}
if ($class && $method) {
throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable.', $class, $method));
}
}
throw new \RuntimeException('Indexable callback is not callable.');
}
$this->isIndexableCallback = $callback;
}
/**
* Return whether the object is indexable with respect to the callback.
*
* @param object $object
* @return boolean
*/
protected function isObjectIndexable($object)
{
if (!$this->isIndexableCallback) {
return true;
}
return is_string($this->isIndexableCallback)
? call_user_func(array($object, $this->isIndexableCallback))
: call_user_func($this->isIndexableCallback, $object);
}
/**
* Schedules the object for removal.
*
* This is usually called during the pre-remove event.
*
* @param object $object
* @param ObjectManager $objectManager
*/
protected function scheduleForRemoval($object, ObjectManager $objectManager)
{
$metadata = $objectManager->getClassMetadata($this->objectClass);
$esId = $metadata->getFieldValue($object, $this->esIdentifierField);
$this->scheduledForRemoval[spl_object_hash($object)] = $esId;
}
/**
* Removes the object if it was scheduled for removal.
*
* This is usually called during the post-remove event.
*
* @param object $object
*/
protected function removeIfScheduled($object)
{
$objectHash = spl_object_hash($object);

View file

@ -2,29 +2,17 @@
namespace FOQ\ElasticaBundle\Doctrine\MongoDB;
use FOQ\ElasticaBundle\Doctrine\AbstractListener;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
use Doctrine\Common\EventSubscriber;
use FOQ\ElasticaBundle\Doctrine\AbstractListener;
class Listener extends AbstractListener implements EventSubscriber
class Listener extends AbstractListener
{
public function postPersist(LifecycleEventArgs $eventArgs)
{
$document = $eventArgs->getDocument();
if ($document instanceof $this->objectClass) {
if ($this->isIndexableCallback && !is_callable(array($document, $this->isIndexableCallback))) {
if (method_exists($document, $this->isIndexableCallback)) {
$exception = sprintf('The specified check method %s::%s is out of scope.', $this->objectClass, $this->isIndexableCallback);
} else {
$exception = sprintf('The specified check method %s::%s does not exist', $this->objectClass, $this->isIndexableCallback);
}
throw new \RuntimeException($exception);
}
if (($this->isIndexableCallback && call_user_func(array($document, $this->isIndexableCallback))) || !$this->isIndexableCallback) {
$this->objectPersister->insertOne($document);
}
if ($document instanceof $this->objectClass && $this->isObjectIndexable($document)) {
$this->objectPersister->insertOne($document);
}
}
@ -33,17 +21,7 @@ class Listener extends AbstractListener implements EventSubscriber
$document = $eventArgs->getDocument();
if ($document instanceof $this->objectClass) {
if ($this->isIndexableCallback && !is_callable(array($document, $this->isIndexableCallback))) {
if (method_exists($document, $this->isIndexableCallback)) {
$exception = sprintf('The specified check method %s::%s is out of scope.', $this->objectClass, $this->isIndexableCallback);
} else {
$exception = sprintf('The specified check method %s::%s does not exist', $this->objectClass, $this->isIndexableCallback);
}
throw new \RuntimeException($exception);
}
if (($this->isIndexableCallback && call_user_func(array($document, $this->isIndexableCallback))) || !$this->isIndexableCallback) {
if ($this->isObjectIndexable($document)) {
$this->objectPersister->replaceOne($document);
} else {
$this->scheduleForRemoval($document, $eventArgs->getDocumentManager());

View file

@ -2,29 +2,17 @@
namespace FOQ\ElasticaBundle\Doctrine\ORM;
use FOQ\ElasticaBundle\Doctrine\AbstractListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\Common\EventSubscriber;
use FOQ\ElasticaBundle\Doctrine\AbstractListener;
class Listener extends AbstractListener implements EventSubscriber
class Listener extends AbstractListener
{
public function postPersist(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof $this->objectClass) {
if ($this->isIndexableCallback && !is_callable(array($entity, $this->isIndexableCallback))) {
if (method_exists($entity, $this->isIndexableCallback)) {
$exception = sprintf('The specified check method %s::%s is out of scope.', $this->objectClass, $this->isIndexableCallback);
} else {
$exception = sprintf('The specified check method %s::%s does not exist', $this->objectClass, $this->isIndexableCallback);
}
throw new \RuntimeException($exception);
}
if (($this->isIndexableCallback && call_user_func(array($entity, $this->isIndexableCallback))) || !$this->isIndexableCallback) {
$this->objectPersister->insertOne($entity);
}
if ($entity instanceof $this->objectClass && $this->isObjectIndexable($entity)) {
$this->objectPersister->insertOne($entity);
}
}
@ -33,17 +21,7 @@ class Listener extends AbstractListener implements EventSubscriber
$entity = $eventArgs->getEntity();
if ($entity instanceof $this->objectClass) {
if ($this->isIndexableCallback && !is_callable(array($entity, $this->isIndexableCallback))) {
if (method_exists($entity, $this->isIndexableCallback)) {
$exception = sprintf('The specified check method %s::%s is out of scope.', $this->objectClass, $this->isIndexableCallback);
} else {
$exception = sprintf('The specified check method %s::%s does not exist', $this->objectClass, $this->isIndexableCallback);
}
throw new \RuntimeException($exception);
}
if (($this->isIndexableCallback && call_user_func(array($entity, $this->isIndexableCallback))) || !$this->isIndexableCallback) {
if ($this->isObjectIndexable($entity)) {
$this->objectPersister->replaceOne($entity);
} else {
$this->scheduleForRemoval($entity, $eventArgs->getEntityManager());

View file

@ -22,6 +22,24 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase
$listener->postPersist($eventArgs);
}
/**
* @dataProvider provideIsIndexableCallbacks
*/
public function testNonIndexableObjectNotInsertedOnPersist($isIndexableCallback)
{
$persister = $this->getMockPersister();
$entity = new Listener\Entity(1, false);
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
$persister->expects($this->never())
->method('insertOne');
$listener = $this->createListener($persister, get_class($entity), array());
$listener->setIsIndexableCallback($isIndexableCallback);
$listener->postPersist($eventArgs);
}
public function testObjectReplacedOnUpdate()
{
$persister = $this->getMockPersister();
@ -33,17 +51,54 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase
->method('replaceOne')
->with($entity);
$persister->expects($this->never())
->method('deleteById');
$listener = $this->createListener($persister, get_class($entity), array());
$listener->postUpdate($eventArgs);
}
/**
* @dataProvider provideIsIndexableCallbacks
*/
public function testNonIndexableObjectRemovedOnUpdate($isIndexableCallback)
{
$classMetadata = $this->getMockClassMetadata();
$objectManager = $this->getMockObjectManager();
$persister = $this->getMockPersister();
$entity = new Listener\Entity(1, false);
$eventArgs = $this->createLifecycleEventArgs($entity, $objectManager);
$objectManager->expects($this->any())
->method('getClassMetadata')
->with(get_class($entity))
->will($this->returnValue($classMetadata));
$classMetadata->expects($this->any())
->method('getFieldValue')
->with($entity, 'id')
->will($this->returnValue($entity->getId()));
$persister->expects($this->never())
->method('replaceOne');
$persister->expects($this->once())
->method('deleteById')
->with($entity->getId());
$listener = $this->createListener($persister, get_class($entity), array());
$listener->setIsIndexableCallback($isIndexableCallback);
$listener->postUpdate($eventArgs);
}
public function testObjectDeletedOnRemove()
{
$classMetadata = $this->getMockClassMetadata();
$objectManager = $this->getMockObjectManager();
$persister = $this->getMockPersister();
$entity = new Listener\Entity(78);
$entity = new Listener\Entity(1);
$eventArgs = $this->createLifecycleEventArgs($entity, $objectManager);
$objectManager->expects($this->any())
@ -71,7 +126,7 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase
$objectManager = $this->getMockObjectManager();
$persister = $this->getMockPersister();
$entity = new Listener\Entity(826);
$entity = new Listener\Entity(1);
$eventArgs = $this->createLifecycleEventArgs($entity, $objectManager);
$objectManager->expects($this->any())
@ -93,6 +148,34 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase
$listener->postRemove($eventArgs);
}
/**
* @dataProvider provideInvalidIsIndexableCallbacks
* @expectedException RuntimeException
*/
public function testInvalidIsIndexableCallbacks($isIndexableCallback)
{
$listener = $this->createListener($this->getMockPersister(), 'FOQ\ElasticaBundle\Tests\Doctrine\Listener\Entity', array());
$listener->setIsIndexableCallback($isIndexableCallback);
}
public function provideInvalidIsIndexableCallbacks()
{
return array(
array('nonexistentEntityMethod'),
array(array(new Listener\IndexableDecider(), 'internalMethod')),
array(42),
);
}
public function provideIsIndexableCallbacks()
{
return array(
array('getIsIndexable'),
array(array(new Listener\IndexableDecider(), 'isIndexable')),
array(function(Listener\Entity $entity) { return $entity->getIsIndexable(); }),
);
}
abstract protected function getLifecycleEventArgsClass();
abstract protected function getListenerClass();
@ -140,14 +223,33 @@ namespace FOQ\ElasticaBundle\Tests\Doctrine\Listener;
class Entity
{
private $id;
private $isIndexable;
public function __construct($id)
public function __construct($id, $isIndexable = true)
{
$this->id = $id;
$this->isIndexable = $isIndexable;
}
public function getId()
{
return $this->id;
}
public function getIsIndexable()
{
return $this->isIndexable;
}
}
class IndexableDecider
{
public function isIndexable(Entity $entity)
{
return $entity->getIsIndexable();
}
protected function internalMethod()
{
}
}