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":{