diff --git a/CHANGELOG-3.0.md b/CHANGELOG-3.0.md index 534a25e..a80d09d 100644 --- a/CHANGELOG-3.0.md +++ b/CHANGELOG-3.0.md @@ -12,6 +12,15 @@ https://github.com/FriendsOfSymfony/FOSElasticaBundle/compare/v3.0.0...v3.0.1 To generate a changelog summary since the last version, run `git log --no-merges --oneline v3.0.0...3.0.x` -* 3.0.0-ALPHA2 (2014-xx-xx) +* 3.0.0-ALPHA3 (xxxx-xx-xx) + + * #463: allowing hot swappable reindexing + * #415: BC BREAK: document indexing occurs in postFlush rather than the pre* events previously. + * 7d13823: Dropped (broken) support for Symfony <2.3 + +* 3.0.0-ALPHA2 (2014-03-17) * 41bf07e: Renamed the `no-stop-on-error` option in PopulateCommand to `ignore-errors` + * 418b9d7: Fixed validation of url configuration + * 726892c: Ignore TypeMissingException when resetting a single type. This allows to create new types without having to recreate the whole index. + * 7f53bad Add support for include_in_{parent,root} for nested and objects diff --git a/Client.php b/Client.php index 601fc77..98cdeae 100644 --- a/Client.php +++ b/Client.php @@ -33,4 +33,9 @@ class Client extends ElasticaClient return $response; } + + public function getIndex($name) + { + return new DynamicIndex($this, $name); + } } diff --git a/Command/PopulateCommand.php b/Command/PopulateCommand.php index a67df94..98834c7 100644 --- a/Command/PopulateCommand.php +++ b/Command/PopulateCommand.php @@ -126,6 +126,7 @@ class PopulateCommand extends ContainerAwareCommand } $output->writeln(sprintf('Refreshing %s', $index)); + $this->resetter->postPopulate($index); $this->indexManager->getIndex($index)->refresh(); } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index d629ea6..0445748 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -146,6 +146,7 @@ class Configuration implements ConfigurationInterface ->prototype('array') ->children() ->scalarNode('index_name')->end() + ->booleanNode('use_alias')->defaultValue(false)->end() ->scalarNode('client')->end() ->scalarNode('finder') ->treatNullLike(true) @@ -183,6 +184,7 @@ class Configuration implements ConfigurationInterface ->scalarNode('insert')->defaultTrue()->end() ->scalarNode('update')->defaultTrue()->end() ->scalarNode('delete')->defaultTrue()->end() + ->scalarNode('persist')->defaultValue('postFlush')->end() ->scalarNode('service')->end() ->variableNode('is_indexable_callback')->defaultNull()->end() ->end() @@ -275,6 +277,7 @@ class Configuration implements ConfigurationInterface ->scalarNode('insert')->defaultTrue()->end() ->scalarNode('update')->defaultTrue()->end() ->scalarNode('delete')->defaultTrue()->end() + ->booleanNode('immediate')->defaultFalse()->end() ->scalarNode('service')->end() ->variableNode('is_indexable_callback')->defaultNull()->end() ->end() @@ -421,6 +424,10 @@ class Configuration implements ConfigurationInterface } if (isset($nestings['properties'])) { + $node + ->booleanNode('include_in_parent')->end() + ->booleanNode('include_in_root')->end() + ; $this->addNestedFieldConfig($node, $nestings, 'properties'); } } diff --git a/DependencyInjection/FOSElasticaExtension.php b/DependencyInjection/FOSElasticaExtension.php index 52070fd..553af14 100644 --- a/DependencyInjection/FOSElasticaExtension.php +++ b/DependencyInjection/FOSElasticaExtension.php @@ -4,6 +4,7 @@ namespace FOS\ElasticaBundle\DependencyInjection; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; @@ -124,6 +125,7 @@ class FOSElasticaExtension extends Extension $indexIds[$name] = $indexId; $this->indexConfigs[$name] = array( 'index' => new Reference($indexId), + 'name_or_alias' => $indexName, 'config' => array( 'mappings' => array() ) @@ -134,6 +136,10 @@ class FOSElasticaExtension extends Extension if (!empty($index['settings'])) { $this->indexConfigs[$name]['config']['settings'] = $index['settings']; } + if ($index['use_alias']) { + $this->indexConfigs[$name]['use_alias'] = true; + } + $this->loadTypes(isset($index['types']) ? $index['types'] : array(), $container, $name, $indexId, $typePrototypeConfig); } @@ -198,6 +204,10 @@ class FOSElasticaExtension extends Extension if (isset($type['serializer']['version'])) { $callbackDef->addMethodCall('setVersion', array($type['serializer']['version'])); } + $callbackClassImplementedInterfaces = class_implements($this->serializerConfig['callback_class']); // PHP < 5.4 friendly + if (isset($callbackClassImplementedInterfaces['Symfony\Component\DependencyInjection\ContainerAwareInterface'])) { + $callbackDef->addMethodCall('setContainer', array(new Reference('service_container'))); + } $container->setDefinition($callbackId, $callbackDef); @@ -443,13 +453,32 @@ class FOSElasticaExtension extends Extension return $listenerId; } + /** + * Map Elastica to Doctrine events for the current driver + */ private function getDoctrineEvents(array $typeConfig) { + // Flush always calls depending on actions scheduled in lifecycle listeners + $typeConfig['listener']['flush'] = true; + + switch ($typeConfig['driver']) { + case 'orm': + $eventsClass = '\Doctrine\ORM\Events'; + break; + case 'mongodb': + $eventsClass = '\Doctrine\ODM\MongoDB\Events'; + break; + default: + throw new InvalidArgumentException(sprintf('Cannot determine events for driver "%s"', $typeConfig['driver'])); + break; + } + $events = array(); $eventMapping = array( - 'insert' => array('postPersist'), - 'update' => array('postUpdate'), - 'delete' => array('postRemove', 'preRemove') + 'insert' => array(constant($eventsClass.'::postPersist')), + 'update' => array(constant($eventsClass.'::postUpdate')), + 'delete' => array(constant($eventsClass.'::preRemove')), + 'flush' => array($typeConfig['listener']['immediate'] ? constant($eventsClass.'::preFlush') : constant($eventsClass.'::postFlush')) ); foreach ($eventMapping as $event => $doctrineEvents) { diff --git a/Doctrine/AbstractListener.php b/Doctrine/Listener.php similarity index 70% rename from Doctrine/AbstractListener.php rename to Doctrine/Listener.php index 3b62444..c254513 100644 --- a/Doctrine/AbstractListener.php +++ b/Doctrine/Listener.php @@ -2,15 +2,15 @@ namespace FOS\ElasticaBundle\Doctrine; +use Doctrine\Common\EventArgs; 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 +class Listener implements EventSubscriber { /** * Object persister @@ -48,11 +48,11 @@ abstract class AbstractListener implements EventSubscriber protected $isIndexableCallback; /** - * Objects scheduled for removal - * - * @var array + * Objects scheduled for insertion, replacement, or removal */ - private $scheduledForRemoval = array(); + public $scheduledForInsertion = array(); + public $scheduledForUpdate = array(); + public $scheduledForDeletion = array(); /** * An instance of ExpressionLanguage @@ -149,37 +149,6 @@ abstract class AbstractListener implements EventSubscriber : 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); - if (isset($this->scheduledForRemoval[$objectHash])) { - $this->objectPersister->deleteById($this->scheduledForRemoval[$objectHash]); - unset($this->scheduledForRemoval[$objectHash]); - } - } - /** * @param mixed $object * @return string @@ -207,4 +176,70 @@ abstract class AbstractListener implements EventSubscriber return $this->expressionLanguage; } + + public function postPersist(EventArgs $eventArgs) + { + $entity = $eventArgs->getEntity(); + + if ($entity instanceof $this->objectClass && $this->isObjectIndexable($entity)) { + $this->scheduledForInsertion[] = $entity; + } + } + + public function postUpdate(EventArgs $eventArgs) + { + $entity = $eventArgs->getEntity(); + + if ($entity instanceof $this->objectClass) { + if ($this->isObjectIndexable($entity)) { + $this->scheduledForUpdate[] = $entity; + } else { + // Delete if no longer indexable + $this->scheduledForDeletion[] = $entity; + } + } + } + + public function preRemove(EventArgs $eventArgs) + { + $entity = $eventArgs->getEntity(); + + if ($entity instanceof $this->objectClass) { + $this->scheduledForDeletion[] = $entity; + } + } + + /** + * Persist scheduled objects to ElasticSearch + */ + private function persistScheduled() + { + if (count($this->scheduledForInsertion)) { + $this->objectPersister->insertMany($this->scheduledForInsertion); + } + if (count($this->scheduledForUpdate)) { + $this->objectPersister->replaceMany($this->scheduledForUpdate); + } + if (count($this->scheduledForDeletion)) { + $this->objectPersister->deleteMany($this->scheduledForDeletion); + } + } + + /** + * Iterate through scheduled actions before flushing to emulate 2.x behavior. Note that the ElasticSearch index + * will fall out of sync with the source data in the event of a crash during flush. + */ + public function preFlush(EventArgs $eventArgs) + { + $this->persistScheduled(); + } + + /** + * Iterating through scheduled actions *after* flushing ensures that the ElasticSearch index will be affected + * only if the query is successful + */ + public function postFlush(EventArgs $eventArgs) + { + $this->persistScheduled(); + } } diff --git a/Doctrine/MongoDB/Listener.php b/Doctrine/MongoDB/Listener.php deleted file mode 100644 index 9fa3536..0000000 --- a/Doctrine/MongoDB/Listener.php +++ /dev/null @@ -1,50 +0,0 @@ -getDocument(); - - if ($document instanceof $this->objectClass && $this->isObjectIndexable($document)) { - $this->objectPersister->insertOne($document); - } - } - - public function postUpdate(LifecycleEventArgs $eventArgs) - { - $document = $eventArgs->getDocument(); - - if ($document instanceof $this->objectClass) { - if ($this->isObjectIndexable($document)) { - $this->objectPersister->replaceOne($document); - } else { - $this->scheduleForRemoval($document, $eventArgs->getDocumentManager()); - $this->removeIfScheduled($document); - } - } - } - - public function preRemove(LifecycleEventArgs $eventArgs) - { - $document = $eventArgs->getDocument(); - - if ($document instanceof $this->objectClass) { - $this->scheduleForRemoval($document, $eventArgs->getDocumentManager()); - } - } - - public function postRemove(LifecycleEventArgs $eventArgs) - { - $document = $eventArgs->getDocument(); - - if ($document instanceof $this->objectClass) { - $this->removeIfScheduled($document); - } - } -} diff --git a/Doctrine/ORM/Listener.php b/Doctrine/ORM/Listener.php deleted file mode 100644 index 790ddb8..0000000 --- a/Doctrine/ORM/Listener.php +++ /dev/null @@ -1,50 +0,0 @@ -getEntity(); - - if ($entity instanceof $this->objectClass && $this->isObjectIndexable($entity)) { - $this->objectPersister->insertOne($entity); - } - } - - public function postUpdate(LifecycleEventArgs $eventArgs) - { - $entity = $eventArgs->getEntity(); - - if ($entity instanceof $this->objectClass) { - if ($this->isObjectIndexable($entity)) { - $this->objectPersister->replaceOne($entity); - } else { - $this->scheduleForRemoval($entity, $eventArgs->getEntityManager()); - $this->removeIfScheduled($entity); - } - } - } - - public function preRemove(LifecycleEventArgs $eventArgs) - { - $entity = $eventArgs->getEntity(); - - if ($entity instanceof $this->objectClass) { - $this->scheduleForRemoval($entity, $eventArgs->getEntityManager()); - } - } - - public function postRemove(LifecycleEventArgs $eventArgs) - { - $entity = $eventArgs->getEntity(); - - if ($entity instanceof $this->objectClass) { - $this->removeIfScheduled($entity); - } - } -} diff --git a/DynamicIndex.php b/DynamicIndex.php new file mode 100644 index 0000000..cbec8e9 --- /dev/null +++ b/DynamicIndex.php @@ -0,0 +1,28 @@ + + */ +class DynamicIndex extends Index +{ + /** + * Reassign index name + * + * While it's technically a regular setter for name property, it's specifically named overrideName, but not setName + * since it's used for a very specific case and normally should not be used + * + * @param string $name Index name + * + * @return void + */ + public function overrideName($name) + { + $this->_name = $name; + } +} diff --git a/Logger/ElasticaLogger.php b/Logger/ElasticaLogger.php index 85acf70..8cb5cbc 100644 --- a/Logger/ElasticaLogger.php +++ b/Logger/ElasticaLogger.php @@ -14,32 +14,42 @@ use Psr\Log\LoggerInterface; */ class ElasticaLogger implements LoggerInterface { + /** + * @var LoggerInterface + */ protected $logger; - protected $queries; + + /** + * @var array + */ + protected $queries = array(); + + /** + * @var boolean + */ protected $debug; /** * Constructor. * * @param LoggerInterface|null $logger The Symfony logger - * @param bool $debug + * @param boolean $debug */ public function __construct(LoggerInterface $logger = null, $debug = false) { $this->logger = $logger; - $this->queries = array(); $this->debug = $debug; } /** * Logs a query. * - * @param string $path Path to call - * @param string $method Rest method to use (GET, POST, DELETE, PUT) - * @param array $data arguments - * @param float $time execution time - * @param array $connection host, port, transport and headers of the query - * @param array $query arguments + * @param string $path Path to call + * @param string $method Rest method to use (GET, POST, DELETE, PUT) + * @param array $data Arguments + * @param float $time Execution time + * @param array $connection Host, port, transport, and headers of the query + * @param array $query Arguments */ public function logQuery($path, $method, $data, $time, $connection = array(), $query = array()) { diff --git a/Paginator/RawPaginatorAdapter.php b/Paginator/RawPaginatorAdapter.php index e99746f..b74a9e4 100644 --- a/Paginator/RawPaginatorAdapter.php +++ b/Paginator/RawPaginatorAdapter.php @@ -33,7 +33,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface private $totalHits; /** - * @array for the facets + * @var array for the facets */ private $facets; diff --git a/Persister/ObjectPersister.php b/Persister/ObjectPersister.php index 450e43b..3592a78 100644 --- a/Persister/ObjectPersister.php +++ b/Persister/ObjectPersister.php @@ -83,12 +83,12 @@ class ObjectPersister implements ObjectPersisterInterface } catch (NotFoundException $e) {} } - /** - * Inserts an array of objects in the type + * Bulk insert an array of objects in the type for the given method * * @param array $objects array of domain model objects - **/ + * @param string Method to call + */ public function insertMany(array $objects) { $documents = array(); @@ -98,6 +98,34 @@ class ObjectPersister implements ObjectPersisterInterface $this->type->addDocuments($documents); } + /** + * Bulk updates an array of objects in the type + * + * @param array $objects array of domain model objects + */ + public function replaceMany(array $objects) + { + $documents = array(); + foreach ($objects as $object) { + $documents[] = $this->transformToElasticaDocument($object); + } + $this->type->updateDocuments($documents); + } + + /** + * Bulk deletes an array of objects in the type + * + * @param array $objects array of domain model objects + */ + public function deleteMany(array $objects) + { + $documents = array(); + foreach ($objects as $object) { + $documents[] = $this->transformToElasticaDocument($object); + } + $this->type->deleteDocuments($documents); + } + /** * Transforms an object to an elastica document * @@ -108,4 +136,4 @@ class ObjectPersister implements ObjectPersisterInterface { return $this->transformer->transform($object, $this->fields); } -} +} \ No newline at end of file diff --git a/Persister/ObjectPersisterInterface.php b/Persister/ObjectPersisterInterface.php index a50bcc8..a25aafc 100644 --- a/Persister/ObjectPersisterInterface.php +++ b/Persister/ObjectPersisterInterface.php @@ -38,13 +38,27 @@ interface ObjectPersisterInterface * @param mixed $id * * @return null - **/ + */ function deleteById($id); /** - * Inserts an array of objects in the type + * Bulk inserts an array of objects in the type * * @param array $objects array of domain model objects - **/ + */ function insertMany(array $objects); + + /** + * Bulk updates an array of objects in the type + * + * @param array $objects array of domain model objects + */ + function replaceMany(array $objects); + + /** + * Bulk deletes an array of objects in the type + * + * @param array $objects array of domain model objects + */ + function deleteMany(array $objects); } diff --git a/README.md b/README.md index cd1a1ba..112ec69 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ A sample configuration with Basic HTTP Authentication is: headers: Authorization: "Basic jdumrGK7rY9TMuQOPng7GZycmxyMHNoir==" +A client configuration can also override the Elastica logger to change the used class ```logger: ``` or to simply disable it ```logger: false```. Disabling the logger should be done on production because it can cause a memory leak. + #### Declare a serializer Elastica can handle objects instead of data arrays if a serializer callable is configured @@ -601,7 +603,11 @@ class User ### Realtime, selective index update If you use the Doctrine integration, you can let ElasticaBundle update the indexes automatically -when an object is added, updated or removed. It uses Doctrine lifecycle events. +when an object is added, updated or removed. It uses Doctrine lifecycle events to schedule updates +and then synchronizes changes either before or after flush. + +> **Propel** doesn't support this feature yet. + Declare that you want to update the index in real time: fos_elastica: @@ -617,7 +623,7 @@ Declare that you want to update the index in real time: persistence: driver: orm model: Application\UserBundle\Entity\User - listener: ~ # by default, listens to "insert", "update" and "delete" + listener: ~ # by default, listens to "insert", "update" and "delete" and updates `postFlush` Now the index is automatically updated each time the state of the bound Doctrine repository changes. No need to repopulate the whole "user" index when a new `User` is created. @@ -630,7 +636,19 @@ You can also choose to only listen for some of the events: update: false delete: true -> **Propel** doesn't support this feature yet. +By default, the ElasticSearch index will be updated after flush. To update before flushing, set `immediate` +to `true`: + + persistence: + listener: + insert: true + update: false + delete: true + immediate: true + +> Using `immediate` to update ElasticSearch before flush completes may cause the ElasticSearch index to fall out of +> sync with the source database in the event of a crash during the flush itself, such as in the case of a bad query. + ### Checking an entity method for listener diff --git a/Resetter.php b/Resetter.php index 69620a2..c7f4638 100644 --- a/Resetter.php +++ b/Resetter.php @@ -2,6 +2,9 @@ namespace FOS\ElasticaBundle; +use Elastica\Exception\ExceptionInterface; +use Elastica\Index; +use Elastica\Exception\ResponseException; use Elastica\Type\Mapping; /** @@ -26,8 +29,8 @@ class Resetter */ public function resetAllIndexes() { - foreach ($this->indexConfigsByName as $indexConfig) { - $indexConfig['index']->create($indexConfig['config'], true); + foreach (array_keys($this->indexConfigsByName) as $name) { + $this->resetIndex($name); } } @@ -40,7 +43,17 @@ class Resetter public function resetIndex($indexName) { $indexConfig = $this->getIndexConfig($indexName); - $indexConfig['index']->create($indexConfig['config'], true); + $esIndex = $indexConfig['index']; + if (isset($indexConfig['use_alias']) && $indexConfig['use_alias']) { + $name = $indexConfig['name_or_alias']; + $name .= uniqid(); + $esIndex->overrideName($name); + $esIndex->create($indexConfig['config']); + + return; + } + + $esIndex->create($indexConfig['config'], true); } /** @@ -59,7 +72,13 @@ class Resetter } $type = $indexConfig['index']->getType($typeName); - $type->delete(); + try { + $type->delete(); + } catch (ResponseException $e) { + if (strpos($e->getMessage(), 'TypeMissingException') === false) { + throw $e; + } + } $mapping = $this->createMapping($indexConfig['config']['mappings'][$typeName]); $type->setMapping($mapping); } @@ -105,4 +124,122 @@ class Resetter return $this->indexConfigsByName[$indexName]; } + + public function postPopulate($indexName) + { + $indexConfig = $this->getIndexConfig($indexName); + if (isset($indexConfig['use_alias']) && $indexConfig['use_alias']) { + $this->switchIndexAlias($indexName); + } + } + + /** + * Switches the alias for given index (by key) to the newly populated index + * and deletes the old index + * + * @param string $indexName Index name + * + * @throws \RuntimeException + */ + private function switchIndexAlias($indexName) + { + $indexConfig = $this->getIndexConfig($indexName); + $esIndex = $indexConfig['index']; + $aliasName = $indexConfig['name_or_alias']; + $oldIndexName = false; + $newIndexName = $esIndex->getName(); + + $aliasedIndexes = $this->getAliasedIndexes($esIndex, $aliasName); + + if (count($aliasedIndexes) > 1) { + throw new \RuntimeException( + sprintf( + 'Alias %s is used for multiple indexes: [%s]. + Make sure it\'s either not used or is assigned to one index only', + $aliasName, + join(', ', $aliasedIndexes) + ) + ); + } + + // Change the alias to point to the new index + // Elastica's addAlias can't be used directly, because in current (0.19.x) version it's not atomic + // In 0.20.x it's atomic, but it doesn't return the old index name + $aliasUpdateRequest = array('actions' => array()); + if (count($aliasedIndexes) == 1) { + // if the alias is set - add an action to remove it + $oldIndexName = $aliasedIndexes[0]; + $aliasUpdateRequest['actions'][] = array( + 'remove' => array('index' => $oldIndexName, 'alias' => $aliasName) + ); + } + + // add an action to point the alias to the new index + $aliasUpdateRequest['actions'][] = array( + 'add' => array('index' => $newIndexName, 'alias' => $aliasName) + ); + + try { + $esIndex->getClient()->request('_aliases', 'POST', $aliasUpdateRequest); + } catch (ExceptionInterface $renameAliasException) { + $additionalError = ''; + // if we failed to move the alias, delete the newly built index + try { + $esIndex->delete(); + } catch (ExceptionInterface $deleteNewIndexException) { + $additionalError = sprintf( + 'Tried to delete newly built index %s, but also failed: %s', + $newIndexName, + $deleteNewIndexException->getError() + ); + } + + throw new \RuntimeException( + sprintf( + 'Failed to updated index alias: %s. %s', + $renameAliasException->getMessage(), + $additionalError ?: sprintf('Newly built index %s was deleted', $newIndexName) + ) + ); + } + + // Delete the old index after the alias has been switched + if ($oldIndexName) { + $oldIndex = new Index($esIndex->getClient(), $oldIndexName); + try { + $oldIndex->delete(); + } catch (ExceptionInterface $deleteOldIndexException) { + throw new \RuntimeException( + sprintf( + 'Failed to delete old index %s with message: %s', + $oldIndexName, + $deleteOldIndexException->getMessage() + ) + ); + } + } + } + + /** + * Returns array of indexes which are mapped to given alias + * + * @param Index $esIndex ES Index + * @param string $aliasName Alias name + * + * @return array + */ + private function getAliasedIndexes(Index $esIndex, $aliasName) + { + $aliasesInfo = $esIndex->getClient()->request('_aliases', 'GET')->getData(); + $aliasedIndexes = array(); + + foreach ($aliasesInfo as $indexName => $indexInfo) { + $aliases = array_keys($indexInfo['aliases']); + if (in_array($aliasName, $aliases)) { + $aliasedIndexes[] = $indexName; + } + } + + return $aliasedIndexes; + } } diff --git a/Resources/config/config.xml b/Resources/config/config.xml index 85cfbed..4419b4a 100644 --- a/Resources/config/config.xml +++ b/Resources/config/config.xml @@ -6,7 +6,7 @@ FOS\ElasticaBundle\Client - Elastica\Index + FOS\ElasticaBundle\DynamicIndex Elastica\Type FOS\ElasticaBundle\Logger\ElasticaLogger FOS\ElasticaBundle\DataCollector\ElasticaDataCollector diff --git a/Resources/config/mongodb.xml b/Resources/config/mongodb.xml index e60e3dc..0af7aa1 100644 --- a/Resources/config/mongodb.xml +++ b/Resources/config/mongodb.xml @@ -13,7 +13,7 @@ - + diff --git a/Resources/config/orm.xml b/Resources/config/orm.xml index 4fd6ae7..5bd16e5 100644 --- a/Resources/config/orm.xml +++ b/Resources/config/orm.xml @@ -13,7 +13,7 @@ - + diff --git a/Tests/Doctrine/AbstractListenerTest.php b/Tests/Doctrine/AbstractListenerTest.php index e99e26d..a9eff66 100644 --- a/Tests/Doctrine/AbstractListenerTest.php +++ b/Tests/Doctrine/AbstractListenerTest.php @@ -3,9 +3,11 @@ namespace FOS\ElasticaBundle\Tests\Doctrine; /** + * See concrete MongoDB/ORM instances of this abstract test + * * @author Richard Miller */ -abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase +abstract class ListenerTest extends \PHPUnit_Framework_TestCase { public function testObjectInsertedOnPersist() { @@ -14,12 +16,16 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase $entity = new Listener\Entity(1); $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); - $persister->expects($this->once()) - ->method('insertOne') - ->with($entity); - $listener = $this->createListener($persister, get_class($entity), array()); $listener->postPersist($eventArgs); + + $this->assertEquals($entity, current($listener->scheduledForInsertion)); + + $persister->expects($this->once()) + ->method('insertMany') + ->with($listener->scheduledForInsertion); + + $listener->postFlush($eventArgs); } /** @@ -32,12 +38,18 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase $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); + + $this->assertEmpty($listener->scheduledForInsertion); + + $persister->expects($this->never()) + ->method('insertOne'); + $persister->expects($this->never()) + ->method('insertMany'); + + $listener->postFlush($eventArgs); } public function testObjectReplacedOnUpdate() @@ -47,15 +59,18 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase $entity = new Listener\Entity(1); $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); - $persister->expects($this->once()) - ->method('replaceOne') - ->with($entity); + $listener = $this->createListener($persister, get_class($entity), array()); + $listener->postUpdate($eventArgs); + $this->assertEquals($entity, current($listener->scheduledForUpdate)); + + $persister->expects($this->once()) + ->method('replaceMany') + ->with(array($entity)); $persister->expects($this->never()) ->method('deleteById'); - $listener = $this->createListener($persister, get_class($entity), array()); - $listener->postUpdate($eventArgs); + $listener->postFlush($eventArgs); } /** @@ -80,16 +95,20 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase ->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); + + $this->assertEmpty($listener->scheduledForUpdate); + $this->assertEquals($entity, current($listener->scheduledForDeletion)); + + $persister->expects($this->never()) + ->method('replaceOne'); + $persister->expects($this->once()) + ->method('deleteMany') + ->with(array($entity)); + + $listener->postFlush($eventArgs); } public function testObjectDeletedOnRemove() @@ -111,13 +130,16 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase ->with($entity, 'id') ->will($this->returnValue($entity->getId())); - $persister->expects($this->once()) - ->method('deleteById') - ->with($entity->getId()); - $listener = $this->createListener($persister, get_class($entity), array()); $listener->preRemove($eventArgs); - $listener->postRemove($eventArgs); + + $this->assertEquals($entity, current($listener->scheduledForDeletion)); + + $persister->expects($this->once()) + ->method('deleteMany') + ->with(array($entity)); + + $listener->postFlush($eventArgs); } public function testObjectWithNonStandardIdentifierDeletedOnRemove() @@ -139,13 +161,16 @@ abstract class AbstractListenerTest extends \PHPUnit_Framework_TestCase ->with($entity, 'identifier') ->will($this->returnValue($entity->getId())); - $persister->expects($this->once()) - ->method('deleteById') - ->with($entity->getId()); - $listener = $this->createListener($persister, get_class($entity), array(), 'identifier'); $listener->preRemove($eventArgs); - $listener->postRemove($eventArgs); + + $this->assertEquals($entity, current($listener->scheduledForDeletion)); + + $persister->expects($this->once()) + ->method('deleteMany') + ->with(array($entity)); + + $listener->postFlush($eventArgs); } /** diff --git a/Tests/Doctrine/MongoDB/ListenerTest.php b/Tests/Doctrine/MongoDB/ListenerTest.php index 7f1a9ab..37a0203 100644 --- a/Tests/Doctrine/MongoDB/ListenerTest.php +++ b/Tests/Doctrine/MongoDB/ListenerTest.php @@ -2,9 +2,9 @@ namespace FOS\ElasticaBundle\Tests\Doctrine\MongoDB; -use FOS\ElasticaBundle\Tests\Doctrine\AbstractListenerTest; +use FOS\ElasticaBundle\Tests\Doctrine\ListenerTest as BaseListenerTest; -class ListenerTest extends AbstractListenerTest +class ListenerTest extends BaseListenerTest { public function setUp() { @@ -25,7 +25,7 @@ class ListenerTest extends AbstractListenerTest protected function getListenerClass() { - return 'FOS\ElasticaBundle\Doctrine\MongoDB\Listener'; + return 'FOS\ElasticaBundle\Doctrine\Listener'; } protected function getObjectManagerClass() diff --git a/Tests/Doctrine/ORM/ListenerTest.php b/Tests/Doctrine/ORM/ListenerTest.php index 48702c0..12a89b2 100644 --- a/Tests/Doctrine/ORM/ListenerTest.php +++ b/Tests/Doctrine/ORM/ListenerTest.php @@ -2,9 +2,9 @@ namespace FOS\ElasticaBundle\Tests\Doctrine\ORM; -use FOS\ElasticaBundle\Tests\Doctrine\AbstractListenerTest; +use FOS\ElasticaBundle\Tests\Doctrine\ListenerTest as BaseListenerTest; -class ListenerTest extends AbstractListenerTest +class ListenerTest extends BaseListenerTest { public function setUp() { @@ -25,7 +25,7 @@ class ListenerTest extends AbstractListenerTest protected function getListenerClass() { - return 'FOS\ElasticaBundle\Doctrine\ORM\Listener'; + return 'FOS\ElasticaBundle\Doctrine\Listener'; } protected function getObjectManagerClass() diff --git a/Tests/ResetterTest.php b/Tests/ResetterTest.php index aa0fbcc..b4e5649 100644 --- a/Tests/ResetterTest.php +++ b/Tests/ResetterTest.php @@ -2,6 +2,9 @@ namespace FOS\ElasticaBundle\Tests\Resetter; +use Elastica\Exception\ResponseException; +use Elastica\Request; +use Elastica\Response; use FOS\ElasticaBundle\Resetter; use Elastica\Type\Mapping; @@ -130,6 +133,32 @@ class ResetterTest extends \PHPUnit_Framework_TestCase $resetter->resetIndexType('foo', 'c'); } + public function testResetIndexTypeIgnoreTypeMissingException() + { + $type = $this->getMockElasticaType(); + + $this->indexConfigsByName['foo']['index']->expects($this->once()) + ->method('getType') + ->with('a') + ->will($this->returnValue($type)); + + $type->expects($this->once()) + ->method('delete') + ->will($this->throwException(new ResponseException( + new Request(''), + new Response(array('error' => 'TypeMissingException[[de_20131022] type[bla] missing]', 'status' => 404))) + )); + + $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['mappings']['a']['properties']); + $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['mappings']['a']['dynamic_templates']); + $type->expects($this->once()) + ->method('setMapping') + ->with($mapping); + + $resetter = new Resetter($this->indexConfigsByName); + $resetter->resetIndexType('foo', 'a'); + } + public function testIndexMappingForParent() { $type = $this->getMockElasticaType(); diff --git a/composer.json b/composer.json index 094eba5..8f22393 100644 --- a/composer.json +++ b/composer.json @@ -12,11 +12,11 @@ ], "require": { "php": ">=5.3.2", - "symfony/framework-bundle": "~2.1", + "symfony/framework-bundle": "~2.3", "symfony/console": "~2.1", "symfony/form": "~2.1", "symfony/property-access": "~2.2", - "ruflin/elastica": "~0.20", + "ruflin/elastica": ">=0.20, <1.1-dev", "psr/log": "~1.0" }, "require-dev":{