<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class IndexPass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('fos_elastica.index_manager')) {
            return;
        }

        $indexes = [];
        foreach ($container->findTaggedServiceIds('fos_elastica.index') as $id => $tags) {
            foreach ($tags as $tag) {
                $indexes[$tag['name']] = new Reference($id);
            }
        }

        $container->getDefinition('fos_elastica.index_manager')->replaceArgument(0, $indexes);
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Finder;

use Elastica\Query;
use Elastica\SearchableInterface;
use FOS\ElasticaBundle\Paginator\FantaPaginatorAdapter;
use FOS\ElasticaBundle\Paginator\HybridPaginatorAdapter;
use FOS\ElasticaBundle\Paginator\RawPaginatorAdapter;
use FOS\ElasticaBundle\Paginator\TransformedPaginatorAdapter;
use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface;
use Pagerfanta\Pagerfanta;

/**
 * Finds elastica documents and map them to persisted objects.
 */
class TransformedFinder implements PaginatedFinderInterface
{
    /**
     * @var SearchableInterface
     */
    protected $searchable;

    /**
     * @var ElasticaToModelTransformerInterface
     */
    protected $transformer;

    /**
     * @param SearchableInterface                 $searchable
     * @param ElasticaToModelTransformerInterface $transformer
     */
    public function __construct(SearchableInterface $searchable, ElasticaToModelTransformerInterface $transformer)
    {
        $this->searchable = $searchable;
        $this->transformer = $transformer;
    }

    /**
     * {@inheritdoc}
     */
    public function find($query, $limit = null, $options = [])
    {
        $results = $this->search($query, $limit, $options);

        return $this->transformer->transform($results);
    }

    /**
     * @param $query
     * @param null|int $limit
     * @param array    $options
     *
     * @return array
     */
    public function findHybrid($query, $limit = null, $options = [])
    {
        $results = $this->search($query, $limit, $options);

        return $this->transformer->hybridTransform($results);
    }

    /**
     * {@inheritdoc}
     */
    public function findPaginated($query, $options = [])
    {
        $queryObject = Query::create($query);
        $paginatorAdapter = $this->createPaginatorAdapter($queryObject, $options);

        return new Pagerfanta(new FantaPaginatorAdapter($paginatorAdapter));
    }

    /**
     * {@inheritdoc}
     */
    public function createPaginatorAdapter($query, $options = [])
    {
        $query = Query::create($query);

        return new TransformedPaginatorAdapter($this->searchable, $query, $options, $this->transformer);
    }

    /**
     * {@inheritdoc}
     */
    public function createHybridPaginatorAdapter($query)
    {
        $query = Query::create($query);

        return new HybridPaginatorAdapter($this->searchable, $query, $this->transformer);
    }

    /**
     * {@inheritdoc}
     */
    public function createRawPaginatorAdapter($query, $options = [])
    {
        $query = Query::create($query);

        return new RawPaginatorAdapter($this->searchable, $query, $options);
    }

    /**
     * @param $query
     * @param null|int $limit
     * @param array    $options
     *
     * @return array
     */
    protected function search($query, $limit = null, $options = [])
    {
        $queryObject = Query::create($query);
        if (null !== $limit) {
            $queryObject->setSize($limit);
        }
        $results = $this->searchable->search($queryObject, $options)->getResults();

        return $results;
    }
}


<?php

namespace FOS\ElasticaBundle\Index;

use Elastica\IndexTemplate;

/**
 * Class Index template manager
 *
 * @author Dmitry Balabka <dmitry.balabka@intexsys.lv>
 */
class IndexTemplateManager
{
    /**
     * Templates
     *
     * @var array
     */
    private $templates;

    public function __construct(array $templates)
    {
        $this->templates = $templates;
    }

    /**
     * Gets an index template by its name.
     *
     * @param string $name Index template to return
     *
     * @return IndexTemplate
     *
     * @throws \InvalidArgumentException if no index template exists for the given name
     */
    public function getIndexTemplate($name)
    {
        if (!isset($this->templates[$name])) {
            throw new \InvalidArgumentException(sprintf('The index template "%s" does not exist', $name));
        }

        return $this->templates[$name];
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle;

use FOS\ElasticaBundle\DependencyInjection\Compiler\ConfigSourcePass;
use FOS\ElasticaBundle\DependencyInjection\Compiler\IndexPass;
use FOS\ElasticaBundle\DependencyInjection\Compiler\RegisterPagerPersistersPass;
use FOS\ElasticaBundle\DependencyInjection\Compiler\TransformerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use FOS\ElasticaBundle\DependencyInjection\Compiler\RegisterPagerProvidersPass;
use FOS\ElasticaBundle\DependencyInjection\Compiler\RegisterPersistersPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class FOSElasticaBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new ConfigSourcePass());
        $container->addCompilerPass(new IndexPass());
        $container->addCompilerPass(new RegisterPagerProvidersPass());
        $container->addCompilerPass(new RegisterPersistersPass());
        $container->addCompilerPass(new RegisterPagerPersistersPass());
        $container->addCompilerPass(new TransformerPass());
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Paginator;

use Elastica\Query;
use Elastica\SearchableInterface;
use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface;

/**
 * Allows pagination of \Elastica\Query.
 */
class HybridPaginatorAdapter extends RawPaginatorAdapter
{
    private $transformer;

    /**
     * @param SearchableInterface                 $searchable  the object to search in
     * @param Query                               $query       the query to search
     * @param ElasticaToModelTransformerInterface $transformer the transformer for fetching the results
     */
    public function __construct(SearchableInterface $searchable, Query $query, ElasticaToModelTransformerInterface $transformer)
    {
        parent::__construct($searchable, $query);

        $this->transformer = $transformer;
    }

    /**
     * {@inheritdoc}
     */
    public function getResults($offset, $length)
    {
        return new HybridPartialResults($this->getElasticaResults($offset, $length), $this->transformer);
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Index;

use Elastica\Exception\ResponseException;
use Elastica\Type\Mapping;
use FOS\ElasticaBundle\Configuration\ManagerInterface;
use Elastica\Client;
use FOS\ElasticaBundle\Event\IndexResetEvent;
use FOS\ElasticaBundle\Event\TypeResetEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Deletes and recreates indexes.
 */
class Resetter implements ResetterInterface
{
    /**
     * @var AliasProcessor
     */
    private $aliasProcessor;

    /***
     * @var ManagerInterface
     */
    private $configManager;

    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * @var IndexManager
     */
    private $indexManager;

    /**
     * @var MappingBuilder
     */
    private $mappingBuilder;

    /**
     * @param ManagerInterface         $configManager
     * @param IndexManager             $indexManager
     * @param AliasProcessor           $aliasProcessor
     * @param MappingBuilder           $mappingBuilder
     * @param EventDispatcherInterface $eventDispatcher
     * @param Client                   $client
     */
    public function __construct(
        ManagerInterface $configManager,
        IndexManager $indexManager,
        AliasProcessor $aliasProcessor,
        MappingBuilder $mappingBuilder,
        EventDispatcherInterface $eventDispatcher
    ) {
        $this->aliasProcessor = $aliasProcessor;
        $this->configManager = $configManager;
        $this->dispatcher = $eventDispatcher;
        $this->indexManager = $indexManager;
        $this->mappingBuilder = $mappingBuilder;
    }

    /**
     * Deletes and recreates all indexes.
     *
     * @param bool $populating
     * @param bool $force
     */
    public function resetAllIndexes($populating = false, $force = false)
    {
        foreach ($this->configManager->getIndexNames() as $name) {
            $this->resetIndex($name, $populating, $force);
        }
    }

    /**
     * Deletes and recreates the named index. If populating, creates a new index
     * with a randomised name for an alias to be set after population.
     *
     * @param string $indexName
     * @param bool   $populating
     * @param bool   $force      If index exists with same name as alias, remove it
     *
     * @throws \InvalidArgumentException if no index exists for the given name
     */
    public function resetIndex($indexName, $populating = false, $force = false)
    {
        $indexConfig = $this->configManager->getIndexConfiguration($indexName);
        $index = $this->indexManager->getIndex($indexName);

        if ($indexConfig->isUseAlias()) {
            $this->aliasProcessor->setRootName($indexConfig, $index);
        }

        $event = new IndexResetEvent($indexName, $populating, $force);
        $this->dispatcher->dispatch(IndexResetEvent::PRE_INDEX_RESET, $event);

        $mapping = $this->mappingBuilder->buildIndexMapping($indexConfig);
        $index->create($mapping, true);

        if (!$populating and $indexConfig->isUseAlias()) {
            $this->aliasProcessor->switchIndexAlias($indexConfig, $index, $force);
        }

        $this->dispatcher->dispatch(IndexResetEvent::POST_INDEX_RESET, $event);
    }

    /**
     * Deletes and recreates a mapping type for the named index.
     *
     * @param string $indexName
     * @param string $typeName
     *
     * @throws \InvalidArgumentException if no index or type mapping exists for the given names
     * @throws ResponseException
     */
    public function resetIndexType($indexName, $typeName)
    {
        $typeConfig = $this->configManager->getTypeConfiguration($indexName, $typeName);

        $this->resetIndex($indexName, true);

        $index = $this->indexManager->getIndex($indexName);
        $type = $index->getType($typeName);

        $event = new TypeResetEvent($indexName, $typeName);
        $this->dispatcher->dispatch(TypeResetEvent::PRE_TYPE_RESET, $event);

        $mapping = new Mapping();
        foreach ($this->mappingBuilder->buildTypeMapping($typeConfig) as $name => $field) {
            $mapping->setParam($name, $field);
        }

        $type->setMapping($mapping);

        $this->dispatcher->dispatch(TypeResetEvent::POST_TYPE_RESET, $event);
    }

    /**
     * A command run when a population has finished.
     *
     * @param string $indexName
     *
     * @deprecated
     */
    public function postPopulate($indexName)
    {
        $this->switchIndexAlias($indexName);
    }

    /**
     * Switching aliases.
     *
     * @param string $indexName
     * @param bool   $delete    Delete or close index
     *
     * @throws \FOS\ElasticaBundle\Exception\AliasIsIndexException
     */
    public function switchIndexAlias($indexName, $delete = true)
    {
        $indexConfig = $this->configManager->getIndexConfiguration($indexName);

        if ($indexConfig->isUseAlias()) {
            $index = $this->indexManager->getIndex($indexName);
            $this->aliasProcessor->switchIndexAlias($indexConfig, $index, false, $delete);
        }
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Persister;

use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

final class PagerPersisterRegistry implements ContainerAwareInterface
{
    use ContainerAwareTrait;

    /**
     * @var string[]
     */
    private $nameToServiceIdMap = [];

    /**
     * @param string[] $nameToServiceIdMap
     */
    public function __construct(array $nameToServiceIdMap)
    {
        $this->nameToServiceIdMap = $nameToServiceIdMap;
    }

    /**
     * @param string $name
     *
     * @throws \InvalidArgumentException if no pager persister was registered for the given name
     *
     * @return PagerPersisterInterface
     */
    public function getPagerPersister($name)
    {
        if (!isset($this->nameToServiceIdMap[$name])) {
            throw new \InvalidArgumentException(sprintf('No pager persister was registered for the give name "%s".', $name));
        }

        $serviceId = $this->nameToServiceIdMap[$name];

        $pagerPersister = $this->container->get($serviceId);

        if (!$pagerPersister instanceof PagerPersisterInterface) {
            throw new \LogicException(sprintf(
                'The pager provider service "%s" must implement "%s" interface but it is an instance of "%s" class.',
                $serviceId,
                PagerPersisterInterface::class,
                get_class($pagerPersister)
            ));
        }

        return $pagerPersister;
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\DependencyInjection;

use Elastica\Client as ElasticaClient;
use FOS\ElasticaBundle\Elastica\Client;
use FOS\ElasticaBundle\Manager\RepositoryManagerInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class FOSElasticaExtension extends Extension
{
    /**
     * Definition of elastica clients as configured by this extension.
     *
     * @var array
     */
    private $clients = [];

    /**
     * An array of indexes as configured by the extension.
     *
     * @var array
     */
    private $indexConfigs = [];

    /**
     * An array of index templates as configured by the extension.
     *
     * @var array
     */
    private $indexTemplateConfigs = array();

    /**
     * If we've encountered a type mapped to a specific persistence driver, it will be loaded
     * here.
     *
     * @var array
     */
    private $loadedDrivers = [];

    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

        if (empty($config['clients']) || empty($config['indexes'])) {
            // No Clients or indexes are defined
            return;
        }

        foreach (['config', 'index', 'persister', 'provider', 'source', 'transformer', 'event_listener', 'commands'] as $basename) {
            $loader->load(sprintf('%s.xml', $basename));
        }

        if (empty($config['default_client'])) {
            $keys = array_keys($config['clients']);
            $config['default_client'] = reset($keys);
        }

        if (empty($config['default_index'])) {
            $keys = array_keys($config['indexes']);
            $config['default_index'] = reset($keys);
        }

        if (isset($config['serializer'])) {
            $loader->load('serializer.xml');

            $this->loadSerializer($config['serializer'], $container);
        }

        $this->loadClients($config['clients'], $container);
        $container->setAlias('fos_elastica.client', sprintf('fos_elastica.client.%s', $config['default_client']));
        $container->getAlias('fos_elastica.client')->setPublic(true);
        $container->setAlias(ElasticaClient::class, new Alias('fos_elastica.client', false));
        $container->setAlias(Client::class, 'fos_elastica.client');
        $container->getAlias(Client::class)->setPublic(false);

        $this->loadIndexes($config['indexes'], $container);
        $container->setAlias('fos_elastica.index', sprintf('fos_elastica.index.%s', $config['default_index']));
        $container->getAlias('fos_elastica.index')->setPublic(true);
        $container->setParameter('fos_elastica.default_index', $config['default_index']);

        if ($usedIndexNames = \array_intersect_key($config['indexes'], $config['index_templates'])) {
            throw new \DomainException(
                \sprintf(
                    'Index names "%s" are already in use and can not be used for index templates names',
                    \implode('","', \array_keys($usedIndexNames))
                )
            );
        }
        $this->loadIndexTemplates($config['index_templates'], $container);

        $container->getDefinition('fos_elastica.config_source.container')->replaceArgument(0, $this->indexConfigs);
        $container
            ->getDefinition('fos_elastica.config_source.template_container')
            ->replaceArgument(0, $this->indexTemplateConfigs);

        $this->loadIndexManager($container);
        $this->loadIndexTemplateManager($container);

        $this->createDefaultManagerAlias($config['default_manager'], $container);
    }

    /**
     * @param array            $config
     * @param ContainerBuilder $container
     *
     * @return Configuration
     */
    public function getConfiguration(array $config, ContainerBuilder $container)
    {
        return new Configuration($container->getParameter('kernel.debug'));
    }

    /**
     * Loads the configured clients.
     *
     * @param array            $clients   An array of clients configurations
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @return array
     */
    private function loadClients(array $clients, ContainerBuilder $container)
    {
        foreach ($clients as $name => $clientConfig) {
            $clientId = sprintf('fos_elastica.client.%s', $name);

            $clientDef = new ChildDefinition('fos_elastica.client_prototype');
            $clientDef->replaceArgument(0, $clientConfig);

            $logger = $clientConfig['connections'][0]['logger'];
            if (false !== $logger) {
                $clientDef->addMethodCall('setLogger', [new Reference($logger)]);
            }

            $clientDef->addTag('fos_elastica.client');

            $container->setDefinition($clientId, $clientDef);

            $this->clients[$name] = [
                'id' => $clientId,
                'reference' => new Reference($clientId),
            ];
        }
    }

    /**
     * Loads the configured indexes.
     *
     * @param array            $indexes   An array of indexes configurations
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @throws \InvalidArgumentException
     *
     * @return array
     */
    private function loadIndexes(array $indexes, ContainerBuilder $container)
    {
        $indexableCallbacks = [];

        foreach ($indexes as $name => $index) {
            $indexId = sprintf('fos_elastica.index.%s', $name);
            $indexName = isset($index['index_name']) ? $index['index_name'] : $name;

            $indexDef = new ChildDefinition('fos_elastica.index_prototype');
            $indexDef->setFactory([new Reference('fos_elastica.client'), 'getIndex']);
            $indexDef->replaceArgument(0, $indexName);
            $indexDef->addTag('fos_elastica.index', [
                'name' => $name,
            ]);

            if (isset($index['client'])) {
                $client = $this->getClient($index['client']);

                $indexDef->setFactory([$client, 'getIndex']);
            }

            $container->setDefinition($indexId, $indexDef);
            $reference = new Reference($indexId);

            $this->indexConfigs[$name] = [
                'elasticsearch_name' => $indexName,
                'reference' => $reference,
                'name' => $name,
                'settings' => $index['settings'],
                'type_prototype' => isset($index['type_prototype']) ? $index['type_prototype'] : [],
                'use_alias' => $index['use_alias'],
            ];

            if ($index['finder']) {
                $this->loadIndexFinder($container, $name, $reference);
            }

            $this->loadTypes((array) $index['types'], $container, $this->indexConfigs[$name], $indexableCallbacks);
        }

        $indexable = $container->getDefinition('fos_elastica.indexable');
        $indexable->replaceArgument(0, $indexableCallbacks);
    }

    /**
     * Loads the configured indexes.
     *
     * @param array            $indexTemplates   An array of indexes configurations
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @throws \InvalidArgumentException
     *
     * @return void
     */
    private function loadIndexTemplates(array $indexTemplates, ContainerBuilder $container)
    {
        $indexableCallbacks = array();
        foreach ($indexTemplates as $name => $indexTemplate) {
            $indexId = sprintf('fos_elastica.index_template.%s', $name);
            $indexTemplateName = isset($indexTemplate['template_name']) ? $indexTemplate['template_name'] : $name;

            $indexDef = new ChildDefinition('fos_elastica.index_template_prototype');
            $indexDef->setFactory([new Reference('fos_elastica.client'), 'getIndexTemplate']);
            $indexDef->replaceArgument(0, $indexTemplateName);
            $indexDef->addTag('fos_elastica.index_template', array(
                'name' => $name,
            ));

            if (isset($indexTemplate['client'])) {
                $client = $this->getClient($indexTemplate['client']);
                $indexDef->setFactory([$client, 'getIndexTemplate']);
            }

            $container->setDefinition($indexId, $indexDef);
            $reference = new Reference($indexId);

            $this->indexTemplateConfigs[$name] = array(
                'elasticsearch_name' => $indexTemplateName,
                'reference' => $reference,
                'name' => $name,
                'settings' => $indexTemplate['settings'],
                'template' => $indexTemplate['template'],
            );

            $this->loadTypes(
                (array) $indexTemplate['types'],
                $container,
                $this->indexTemplateConfigs[$name],
                $indexableCallbacks
            );
        }

        if ($indexableCallbacks) {
            throw new \RuntimeException('`indexable_callback` option is not supported by index templates');
        }
    }

    /**
     * Loads the configured index finders.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     * @param string                                                  $name      The index name
     * @param Reference                                               $index     Reference to the related index
     *
     * @return string
     */
    private function loadIndexFinder(ContainerBuilder $container, $name, Reference $index)
    {
        /* Note: transformer services may conflict with "collection.index", if
         * an index and type names were "collection" and an index, respectively.
         */
        $transformerId = sprintf('fos_elastica.elastica_to_model_transformer.collection.%s', $name);
        $transformerDef = new ChildDefinition('fos_elastica.elastica_to_model_transformer.collection');
        $container->setDefinition($transformerId, $transformerDef);

        $finderId = sprintf('fos_elastica.finder.%s', $name);
        $finderDef = new ChildDefinition('fos_elastica.finder');
        $finderDef->replaceArgument(0, $index);
        $finderDef->replaceArgument(1, new Reference($transformerId));

        $container->setDefinition($finderId, $finderDef);
    }

    /**
     * Loads the configured types.
     *
     * @param array            $types
     * @param ContainerBuilder $container
     * @param array            $indexConfig
     * @param array            $indexableCallbacks
     */
    private function loadTypes(array $types, ContainerBuilder $container, array &$indexConfig, array &$indexableCallbacks)
    {
        foreach ($types as $name => $type) {
            $indexName = $indexConfig['name'];

            $typeId = sprintf('%s.%s', $indexConfig['reference'], $name);
            $typeDef = new ChildDefinition('fos_elastica.type_prototype');
            $typeDef->setFactory([$indexConfig['reference'], 'getType']);
            $typeDef->replaceArgument(0, $name);

            $container->setDefinition($typeId, $typeDef);

            $typeConfig = [
                'name' => $name,
                'mapping' => [], // An array containing anything that gets sent directly to ElasticSearch
                'config' => [],
            ];

            foreach ([
                'dynamic_templates',
                'properties',
                '_all',
                '_id',
                '_parent',
                '_routing',
                '_source',
            ] as $field) {
                if (isset($type[$field])) {
                    $typeConfig['mapping'][$field] = $type[$field];
                }
            }

            foreach ([
                'persistence',
                'serializer',
                'analyzer',
                'search_analyzer',
                'dynamic',
                'date_detection',
                'dynamic_date_formats',
                'numeric_detection',
            ] as $field) {
                $typeConfig['config'][$field] = array_key_exists($field, $type) ?
                    $type[$field] :
                    null;
            }

            $indexConfig['types'][$name] = $typeConfig;

            if (isset($type['persistence'])) {
                $this->loadTypePersistenceIntegration($type['persistence'], $container, new Reference($typeId), $indexName, $name);

                $typeConfig['persistence'] = $type['persistence'];
            }

            if (isset($type['_parent'])) {
                // _parent mapping cannot contain `property` and `identifier`, so removing them after building `persistence`
                unset($indexConfig['types'][$name]['mapping']['_parent']['property'], $indexConfig['types'][$name]['mapping']['_parent']['identifier']);
            }

            if (isset($type['indexable_callback'])) {
                $indexableCallbacks[sprintf('%s/%s', $indexName, $name)] = $this->buildCallback($type['indexable_callback'], $name);
            }

            if ($container->hasDefinition('fos_elastica.serializer_callback_prototype')) {
                $typeSerializerId = sprintf('%s.serializer.callback', $typeId);
                $typeSerializerDef = new ChildDefinition('fos_elastica.serializer_callback_prototype');

                if (isset($type['serializer']['groups'])) {
                    $typeSerializerDef->addMethodCall('setGroups', [$type['serializer']['groups']]);
                }

                if (isset($type['serializer']['serialize_null'])) {
                    $typeSerializerDef->addMethodCall('setSerializeNull', [$type['serializer']['serialize_null']]);
                }

                if (isset($type['serializer']['version'])) {
                    $typeSerializerDef->addMethodCall('setVersion', [$type['serializer']['version']]);
                }

                $typeDef->addMethodCall('setSerializer', [[new Reference($typeSerializerId), 'serialize']]);
                $container->setDefinition($typeSerializerId, $typeSerializerDef);
            }
        }
    }

    private function buildCallback($indexCallback, $typeName)
    {
        if (is_array($indexCallback)) {
            if (!isset($indexCallback[0])) {
                throw new \InvalidArgumentException(sprintf('Invalid indexable_callback for type %s', $typeName));
            }

            $classOrServiceRef = $this->transformServiceReference($indexCallback[0]);
            if ($classOrServiceRef instanceof Reference && !isset($indexCallback[1])) {
                return $classOrServiceRef; // __invoke
            }

            if (!isset($indexCallback[1])) {
                throw new \InvalidArgumentException(sprintf('Invalid indexable_callback for type %s', $typeName));
            }

            return [$classOrServiceRef, $indexCallback[1]];
        }

        if (is_string($indexCallback)) {
            return $this->transformServiceReference($indexCallback);
        }

        throw new \InvalidArgumentException(sprintf('Invalid indexable_callback for type %s', $typeName));
    }

    private function transformServiceReference($classOrService)
    {
        return 0 === strpos($classOrService, '@') ? new Reference(substr($classOrService, 1)) : $classOrService;
    }

    /**
     * Loads the optional provider and finder for a type.
     *
     * @param array            $typeConfig
     * @param ContainerBuilder $container
     * @param Reference        $typeRef
     * @param string           $indexName
     * @param string           $typeName
     */
    private function loadTypePersistenceIntegration(array $typeConfig, ContainerBuilder $container, Reference $typeRef, $indexName, $typeName)
    {
        if (isset($typeConfig['driver'])) {
            $this->loadDriver($container, $typeConfig['driver']);
        }

        $elasticaToModelTransformerId = $this->loadElasticaToModelTransformer($typeConfig, $container, $indexName, $typeName);
        $modelToElasticaTransformerId = $this->loadModelToElasticaTransformer($typeConfig, $container, $indexName, $typeName);
        $objectPersisterId = $this->loadObjectPersister($typeConfig, $typeRef, $container, $indexName, $typeName, $modelToElasticaTransformerId);

        if (isset($typeConfig['provider'])) {
            $this->loadTypePagerProvider($typeConfig, $container, $indexName, $typeName);
        }
        if (isset($typeConfig['finder'])) {
            $this->loadTypeFinder($typeConfig, $container, $elasticaToModelTransformerId, $typeRef, $indexName, $typeName);
        }
        if (isset($typeConfig['listener']) && $typeConfig['listener']['enabled']) {
            $this->loadTypeListener($typeConfig, $container, $objectPersisterId, $indexName, $typeName);
        }
    }

    /**
     * Creates and loads an ElasticaToModelTransformer.
     *
     * @param array            $typeConfig
     * @param ContainerBuilder $container
     * @param string           $indexName
     * @param string           $typeName
     *
     * @return string
     */
    private function loadElasticaToModelTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName)
    {
        if (isset($typeConfig['elastica_to_model_transformer']['service'])) {
            return $typeConfig['elastica_to_model_transformer']['service'];
        }

        /* Note: transformer services may conflict with "prototype.driver", if
         * the index and type names were "prototype" and a driver, respectively.
         */
        $abstractId = sprintf('fos_elastica.elastica_to_model_transformer.prototype.%s', $typeConfig['driver']);
        $serviceId = sprintf('fos_elastica.elastica_to_model_transformer.%s.%s', $indexName, $typeName);
        $serviceDef = new ChildDefinition($abstractId);
        $serviceDef->addTag('fos_elastica.elastica_to_model_transformer', ['type' => $typeName, 'index' => $indexName]);

        $serviceDef->replaceArgument(1, $typeConfig['model']);
        $serviceDef->replaceArgument(2, array_merge($typeConfig['elastica_to_model_transformer'], [
            'identifier' => $typeConfig['identifier'],
        ]));
        $container->setDefinition($serviceId, $serviceDef);

        return $serviceId;
    }

    /**
     * Creates and loads a ModelToElasticaTransformer for an index/type.
     *
     * @param array            $typeConfig
     * @param ContainerBuilder $container
     * @param string           $indexName
     * @param string           $typeName
     *
     * @return string
     */
    private function loadModelToElasticaTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName)
    {
        if (isset($typeConfig['model_to_elastica_transformer']['service'])) {
            return $typeConfig['model_to_elastica_transformer']['service'];
        }

        $abstractId = $container->hasDefinition('fos_elastica.serializer_callback_prototype') ?
            'fos_elastica.model_to_elastica_identifier_transformer' :
            'fos_elastica.model_to_elastica_transformer';

        $serviceId = sprintf('fos_elastica.model_to_elastica_transformer.%s.%s', $indexName, $typeName);
        $serviceDef = new ChildDefinition($abstractId);
        $serviceDef->replaceArgument(0, [
            'identifier' => $typeConfig['identifier'],
            'index' => $indexName,
        ]);
        $container->setDefinition($serviceId, $serviceDef);

        return $serviceId;
    }

    /**
     * Creates and loads an object persister for a type.
     *
     * @param array            $typeConfig
     * @param Reference        $typeRef
     * @param ContainerBuilder $container
     * @param string           $indexName
     * @param string           $typeName
     * @param string           $transformerId
     *
     * @return string
     */
    private function loadObjectPersister(array $typeConfig, Reference $typeRef, ContainerBuilder $container, $indexName, $typeName, $transformerId)
    {
        if (isset($typeConfig['persister']['service'])) {
            return $typeConfig['persister']['service'];
        }

        $arguments = [
            $typeRef,
            new Reference($transformerId),
            $typeConfig['model'],
        ];

        if ($container->hasDefinition('fos_elastica.serializer_callback_prototype')) {
            $abstractId = 'fos_elastica.object_serializer_persister';
            $callbackId = sprintf('%s.%s.serializer.callback', $this->indexConfigs[$indexName]['reference'], $typeName);
            $arguments[] = [new Reference($callbackId), 'serialize'];
        } else {
            $abstractId = 'fos_elastica.object_persister';
            $mapping = $this->indexConfigs[$indexName]['types'][$typeName]['mapping'];
            $argument = $mapping['properties'];
            if (isset($mapping['_parent'])) {
                $argument['_parent'] = $mapping['_parent'];
            }
            $arguments[] = $argument;
        }

        $arguments[] = array_intersect_key($typeConfig['persister'], array_flip(['refresh']));

        $serviceId = sprintf('fos_elastica.object_persister.%s.%s', $indexName, $typeName);
        $serviceDef = new ChildDefinition($abstractId);
        foreach ($arguments as $i => $argument) {
            $serviceDef->replaceArgument($i, $argument);
        }

        $serviceDef->addTag('fos_elastica.persister', ['index' => $indexName, 'type' => $typeName]);

        $container->setDefinition($serviceId, $serviceDef);

        return $serviceId;
    }

    /**
     * Loads a pager provider for a type.
     *
     * @param array            $typeConfig
     * @param ContainerBuilder $container
     * @param string           $indexName
     * @param string           $typeName
     *
     * @return string
     */
    private function loadTypePagerProvider(array $typeConfig, ContainerBuilder $container, $indexName, $typeName)
    {
        if (isset($typeConfig['provider']['service'])) {
            return $typeConfig['provider']['service'];
        }

        $baseConfig = $typeConfig['provider'];
        unset($baseConfig['service']);

        $driver = $typeConfig['driver'];

        switch ($driver) {
            case 'orm':
                $providerDef = new ChildDefinition('fos_elastica.pager_provider.prototype.'.$driver);
                $providerDef->replaceArgument(2, $typeConfig['model']);
                $providerDef->replaceArgument(3, $baseConfig);

                break;
            case 'mongodb':
                $providerDef = new ChildDefinition('fos_elastica.pager_provider.prototype.'.$driver);
                $providerDef->replaceArgument(2, $typeConfig['model']);
                $providerDef->replaceArgument(3, $baseConfig);

                break;
            case 'phpcr':
                $providerDef = new ChildDefinition('fos_elastica.pager_provider.prototype.'.$driver);
                $providerDef->replaceArgument(2, $typeConfig['model']);
                $providerDef->replaceArgument(3, $baseConfig);

                break;
            default:
                throw new \LogicException(sprintf('The pager provider for driver "%s" does not exist.', $driver));
        }

        /* Note: provider services may conflict with "prototype.driver", if the
         * index and type names were "prototype" and a driver, respectively.
         */
        $providerId = sprintf('fos_elastica.pager_provider.%s.%s', $indexName, $typeName);
        $providerDef->addTag('fos_elastica.pager_provider', ['index' => $indexName, 'type' => $typeName]);

        $container->setDefinition($providerId, $providerDef);

        return $providerId;
    }

    /**
     * Loads doctrine listeners to handle indexing of new or updated objects.
     *
     * @param array            $typeConfig
     * @param ContainerBuilder $container
     * @param string           $objectPersisterId
     * @param string           $indexName
     * @param string           $typeName
     *
     * @return string
     */
    private function loadTypeListener(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $indexName, $typeName)
    {
        if (isset($typeConfig['listener']['service'])) {
            return $typeConfig['listener']['service'];
        }

        /* Note: listener services may conflict with "prototype.driver", if the
         * index and type names were "prototype" and a driver, respectively.
         */
        $abstractListenerId = sprintf('fos_elastica.listener.prototype.%s', $typeConfig['driver']);
        $listenerId = sprintf('fos_elastica.listener.%s.%s', $indexName, $typeName);
        $listenerDef = new ChildDefinition($abstractListenerId);
        $listenerDef->replaceArgument(0, new Reference($objectPersisterId));
        $listenerDef->replaceArgument(3, $typeConfig['listener']['logger'] ?
            new Reference($typeConfig['listener']['logger']) :
            null
        );
        $listenerConfig = [
            'identifier' => $typeConfig['identifier'],
            'indexName' => $indexName,
            'typeName' => $typeName,
        ];

        $tagName = null;
        switch ($typeConfig['driver']) {
            case 'orm':
                $tagName = 'doctrine.event_listener';
                break;
            case 'phpcr':
                $tagName = 'doctrine_phpcr.event_listener';
                break;
            case 'mongodb':
                $tagName = 'doctrine_mongodb.odm.event_listener';
                break;
        }

        if ($typeConfig['listener']['defer']) {
            $listenerDef->setPublic(true);
            $listenerDef->addTag(
                'kernel.event_listener',
                ['event' => 'kernel.terminate', 'method' => 'onTerminate']
            );
            $listenerDef->addTag(
                'kernel.event_listener',
                ['event' => 'console.terminate', 'method' => 'onTerminate']
            );
            $listenerConfig['defer'] = true;
        }

        $listenerDef->replaceArgument(2, $listenerConfig);

        if (null !== $tagName) {
            foreach ($this->getDoctrineEvents($typeConfig) as $event) {
                $listenerDef->addTag($tagName, ['event' => $event]);
            }
        }

        $container->setDefinition($listenerId, $listenerDef);

        return $listenerId;
    }

    /**
     * Map Elastica to Doctrine events for the current driver.
     */
    private function getDoctrineEvents(array $typeConfig)
    {
        switch ($typeConfig['driver']) {
            case 'orm':
                $eventsClass = '\Doctrine\ORM\Events';
                break;
            case 'phpcr':
                $eventsClass = '\Doctrine\ODM\PHPCR\Event';
                break;
            case 'mongodb':
                $eventsClass = '\Doctrine\ODM\MongoDB\Events';
                break;
            default:
                throw new \InvalidArgumentException(sprintf('Cannot determine events for driver "%s"', $typeConfig['driver']));
        }

        $events = [];
        $eventMapping = [
            'insert' => [constant($eventsClass.'::postPersist')],
            'update' => [constant($eventsClass.'::postUpdate')],
            'delete' => [constant($eventsClass.'::preRemove')],
            'flush' => [constant($eventsClass.'::postFlush')],
        ];

        foreach ($eventMapping as $event => $doctrineEvents) {
            if (isset($typeConfig['listener'][$event]) && $typeConfig['listener'][$event]) {
                $events = array_merge($events, $doctrineEvents);
            }
        }

        return $events;
    }

    /**
     * Loads a Type specific Finder.
     *
     * @param array            $typeConfig
     * @param ContainerBuilder $container
     * @param string           $elasticaToModelId
     * @param Reference        $typeRef
     * @param string           $indexName
     * @param string           $typeName
     *
     * @return string
     */
    private function loadTypeFinder(array $typeConfig, ContainerBuilder $container, $elasticaToModelId, Reference $typeRef, $indexName, $typeName)
    {
        if (isset($typeConfig['finder']['service'])) {
            $finderId = $typeConfig['finder']['service'];
        } else {
            $finderId = sprintf('fos_elastica.finder.%s.%s', $indexName, $typeName);
            $finderDef = new ChildDefinition('fos_elastica.finder');
            $finderDef->replaceArgument(0, $typeRef);
            $finderDef->replaceArgument(1, new Reference($elasticaToModelId));
            $container->setDefinition($finderId, $finderDef);
        }

        $indexTypeName = "$indexName/$typeName";
        $arguments = [$indexTypeName, new Reference($finderId)];
        if (isset($typeConfig['repository'])) {
            $arguments[] = $typeConfig['repository'];
        }

        $container->getDefinition('fos_elastica.repository_manager')
            ->addMethodCall('addType', $arguments);

        $managerId = sprintf('fos_elastica.manager.%s', $typeConfig['driver']);
        $container->getDefinition($managerId)
            ->addMethodCall('addEntity', [$typeConfig['model'], $indexTypeName]);

        return $finderId;
    }

    /**
     * Loads the index manager.
     *
     * @param ContainerBuilder $container
     **/
    private function loadIndexManager(ContainerBuilder $container)
    {
        $indexRefs = array_map(function ($index) {
            return $index['reference'];
        }, $this->indexConfigs);

        $managerDef = $container->getDefinition('fos_elastica.index_manager');
        $managerDef->replaceArgument(0, $indexRefs);
    }

    /**
     * Load index template manager
     *
     * @param ContainerBuilder $container
     *
     * @return void
     */
    private function loadIndexTemplateManager(ContainerBuilder $container)
    {
        $indexTemplateRefs = array_map(function ($index) {
            return $index['reference'];
        }, $this->indexTemplateConfigs);

        $managerDef = $container->getDefinition('fos_elastica.index_template_manager');
        $managerDef->replaceArgument(0, $indexTemplateRefs);
    }

    /**
     * Makes sure a specific driver has been loaded.
     *
     * @param ContainerBuilder $container
     * @param string           $driver
     */
    private function loadDriver(ContainerBuilder $container, $driver)
    {
        if (in_array($driver, $this->loadedDrivers)) {
            return;
        }

        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load($driver.'.xml');
        $this->loadedDrivers[] = $driver;
    }

    /**
     * Loads and configures the serializer prototype.
     *
     * @param array            $config
     * @param ContainerBuilder $container
     */
    private function loadSerializer($config, ContainerBuilder $container)
    {
        $container->setAlias('fos_elastica.serializer', $config['serializer']);

        $serializer = $container->getDefinition('fos_elastica.serializer_callback_prototype');
        $serializer->setClass($config['callback_class']);

        if (is_subclass_of($config['callback_class'], ContainerAwareInterface::class)) {
            $serializer->addMethodCall('setContainer', [new Reference('service_container')]);
        }
    }

    /**
     * Creates a default manager alias for defined default manager or the first loaded driver.
     *
     * @param string           $defaultManager
     * @param ContainerBuilder $container
     */
    private function createDefaultManagerAlias($defaultManager, ContainerBuilder $container)
    {
        if (0 == count($this->loadedDrivers)) {
            return;
        }

        if (count($this->loadedDrivers) > 1
            && in_array($defaultManager, $this->loadedDrivers)
        ) {
            $defaultManagerService = $defaultManager;
        } else {
            $defaultManagerService = $this->loadedDrivers[0];
        }

        $container->setAlias('fos_elastica.manager', sprintf('fos_elastica.manager.%s', $defaultManagerService));
        $container->getAlias('fos_elastica.manager')->setPublic(true);
        $container->setAlias(RepositoryManagerInterface::class, 'fos_elastica.manager');
        $container->getAlias(RepositoryManagerInterface::class)->setPublic(false);
    }

    /**
     * Returns a reference to a client given its configured name.
     *
     * @param string $clientName
     *
     * @return Reference
     *
     * @throws \InvalidArgumentException
     */
    private function getClient($clientName)
    {
        if (!array_key_exists($clientName, $this->clients)) {
            throw new \InvalidArgumentException(sprintf('The elastica client with name "%s" is not defined', $clientName));
        }

        return $this->clients[$clientName]['reference'];
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    const SUPPORTED_DRIVERS = ['orm', 'mongodb', 'phpcr'];

    /**
     * If the kernel is running in debug mode.
     *
     * @var bool
     */
    private $debug;

    public function __construct($debug)
    {
        $this->debug = $debug;
    }

    /**
     * Generates the configuration tree.
     *
     * @return TreeBuilder
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder('fos_elastica');

        if (method_exists($treeBuilder, 'getRootNode')) {
            $rootNode = $treeBuilder->getRootNode();
        } else {
            // BC layer for symfony/config 4.1 and older
            $rootNode = $treeBuilder->root('fos_elastica');
        }

        $this->addClientsSection($rootNode);
        $this->addIndexesSection($rootNode);
        $this->addIndexTemplatesSection($rootNode);

        $rootNode
            ->children()
                ->scalarNode('default_client')
                    ->info('Defaults to the first client defined')
                ->end()
                ->scalarNode('default_index')
                    ->info('Defaults to the first index defined')
                ->end()
                ->scalarNode('default_manager')->defaultValue('orm')->end()
                ->arrayNode('serializer')
                    ->treatNullLike([])
                    ->children()
                        ->scalarNode('callback_class')->defaultValue('FOS\ElasticaBundle\Serializer\Callback')->end()
                        ->scalarNode('serializer')->defaultValue('serializer')->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }

    /**
     * Returns the array node used for "dynamic_templates".
     */
    private function getDynamicTemplateNode()
    {
        $node = $this->createTreeBuilderNode('dynamic_templates');

        $node
            ->prototype('array')
                ->prototype('array')
                    ->children()
                        ->scalarNode('match')->end()
                        ->scalarNode('unmatch')->end()
                        ->scalarNode('match_mapping_type')->end()
                        ->scalarNode('path_match')->end()
                        ->scalarNode('path_unmatch')->end()
                        ->scalarNode('match_pattern')->end()
                        ->arrayNode('mapping')
                            ->prototype('variable')
                                ->treatNullLike([])
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $node;
    }

    /**
     * Returns the array node used for "types".
     */
    private function getTypesNode()
    {
        $node = $this->createTreeBuilderNode('types');

        $node
            ->useAttributeAsKey('name')
            ->prototype('array')
                ->treatNullLike([])
                ->beforeNormalization()
                ->ifNull()
                    ->thenEmptyArray()
                ->end()
                // Support multiple dynamic_template formats to match the old bundle style
                // and the way ElasticSearch expects them
                ->beforeNormalization()
                ->ifTrue(function ($v) {
                    return isset($v['dynamic_templates']);
                })
                ->then(function ($v) {
                    $dt = [];
                    foreach ($v['dynamic_templates'] as $key => $type) {
                        if (is_int($key)) {
                            $dt[] = $type;
                        } else {
                            $dt[][$key] = $type;
                        }
                    }

                    $v['dynamic_templates'] = $dt;

                    return $v;
                })
                ->end()
                ->children()
                    ->booleanNode('date_detection')->end()
                    ->arrayNode('dynamic_date_formats')->prototype('scalar')->end()->end()
                    ->scalarNode('analyzer')->end()
                    ->booleanNode('numeric_detection')->end()
                    ->scalarNode('dynamic')->end()
                    ->variableNode('indexable_callback')->end()
                    ->append($this->getPersistenceNode())
                    ->append($this->getSerializerNode())
                ->end()
                ->append($this->getIdNode())
                ->append($this->getPropertiesNode())
                ->append($this->getDynamicTemplateNode())
                ->append($this->getSourceNode())
                ->append($this->getRoutingNode())
                ->append($this->getParentNode())
                ->append($this->getAllNode())
            ->end()
        ;

        return $node;
    }

    /**
     * Returns the array node used for "properties".
     */
    private function getPropertiesNode()
    {
        $node = $this->createTreeBuilderNode('properties');

        $node
            ->useAttributeAsKey('name')
            ->prototype('variable')
                ->treatNullLike([]);

        return $node;
    }

    /**
     * Returns the array node used for "_id".
     */
    private function getIdNode()
    {
        $node = $this->createTreeBuilderNode('_id');

        $node
            ->children()
            ->scalarNode('path')->end()
            ->end()
        ;

        return $node;
    }

    /**
     * Returns the array node used for "_source".
     */
    private function getSourceNode()
    {
        $node = $this->createTreeBuilderNode('_source');

        $node
            ->children()
                ->arrayNode('excludes')
                    ->useAttributeAsKey('name')
                    ->prototype('scalar')->end()
                ->end()
                ->arrayNode('includes')
                    ->useAttributeAsKey('name')
                    ->prototype('scalar')->end()
                ->end()
                ->scalarNode('compress')->end()
                ->scalarNode('compress_threshold')->end()
                ->scalarNode('enabled')->defaultTrue()->end()
            ->end()
        ;

        return $node;
    }

    /**
     * Returns the array node used for "_routing".
     */
    private function getRoutingNode()
    {
        $node = $this->createTreeBuilderNode('_routing');

        $node
            ->children()
                ->scalarNode('required')->end()
                ->scalarNode('path')->end()
            ->end()
        ;

        return $node;
    }

    /**
     * Returns the array node used for "_parent".
     */
    private function getParentNode()
    {
        $node = $this->createTreeBuilderNode('_parent');

        $node
            ->children()
                ->scalarNode('type')->end()
                ->scalarNode('property')->defaultValue(null)->end()
                ->scalarNode('identifier')->defaultValue('id')->end()
            ->end()
        ;

        return $node;
    }

    /**
     * Returns the array node used for "_all".
     */
    private function getAllNode()
    {
        $node = $this->createTreeBuilderNode('_all');

        $node
            ->children()
            ->scalarNode('enabled')->defaultValue(true)->end()
            ->scalarNode('analyzer')->end()
            ->end()
        ;

        return $node;
    }

    /**
     * @return ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition
     */
    private function getPersistenceNode()
    {
        $node = $this->createTreeBuilderNode('persistence');

        $node
            ->validate()
                ->ifTrue(function ($v) {
                    return isset($v['driver']) && 'orm' !== $v['driver'] && !empty($v['elastica_to_model_transformer']['hints']);
                })
                    ->thenInvalid('Hints are only supported by the "orm" driver')
            ->end()
            ->children()
                ->scalarNode('driver')
                    ->defaultValue('orm')
                    ->validate()
                    ->ifNotInArray(self::SUPPORTED_DRIVERS)
                        ->thenInvalid('The driver %s is not supported. Please choose one of '.json_encode(self::SUPPORTED_DRIVERS))
                    ->end()
                ->end()
                ->scalarNode('model')->defaultValue(null)->end()
                ->scalarNode('repository')->end()
                ->scalarNode('identifier')->defaultValue('id')->end()
                ->arrayNode('provider')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('batch_size')->defaultValue(100)->end()
                        ->scalarNode('clear_object_manager')->defaultTrue()->end()
                        ->scalarNode('debug_logging')
                            ->defaultValue($this->debug)
                            ->treatNullLike(true)
                        ->end()
                        ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end()
                        ->scalarNode('service')->end()
                    ->end()
                ->end()
                ->arrayNode('listener')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->booleanNode('enabled')->defaultTrue()->end()
                        ->scalarNode('insert')->defaultTrue()->end()
                        ->scalarNode('update')->defaultTrue()->end()
                        ->scalarNode('delete')->defaultTrue()->end()
                        ->scalarNode('flush')->defaultTrue()->end()
                        ->booleanNode('defer')->defaultFalse()->end()
                        ->scalarNode('logger')
                            ->defaultFalse()
                            ->treatNullLike('fos_elastica.logger')
                            ->treatTrueLike('fos_elastica.logger')
                        ->end()
                        ->scalarNode('service')->end()
                    ->end()
                ->end()
                ->arrayNode('finder')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('service')->end()
                    ->end()
                ->end()
                ->arrayNode('elastica_to_model_transformer')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->arrayNode('hints')
                            ->prototype('array')
                                ->children()
                                    ->scalarNode('name')->end()
                                    ->scalarNode('value')->end()
                                ->end()
                            ->end()
                        ->end()
                        ->booleanNode('hydrate')->defaultTrue()->end()
                        ->booleanNode('ignore_missing')
                            ->defaultFalse()
                            ->info('Silently ignore results returned from Elasticsearch without corresponding persistent object.')
                        ->end()
                        ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end()
                        ->scalarNode('service')->end()
                    ->end()
                ->end()
                ->arrayNode('model_to_elastica_transformer')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('service')->end()
                    ->end()
                ->end()
                ->arrayNode('persister')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->enumNode('refresh')
                            ->treatTrueLike('true')
                            ->treatFalseLike('false')
                            ->values(['true', 'wait_for', 'false'])
                        ->end()
                        ->scalarNode('service')->end()
                    ->end()
                ->end()
            ->end();

        return $node;
    }

    /**
     * @return ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition
     */
    private function getSerializerNode()
    {
        $node = $this->createTreeBuilderNode('serializer');

        $node
            ->addDefaultsIfNotSet()
            ->children()
                ->arrayNode('groups')
                    ->treatNullLike([])
                    ->prototype('scalar')->end()
                ->end()
                ->scalarNode('version')->end()
                ->booleanNode('serialize_null')
                    ->defaultFalse()
                ->end()
            ->end();

        return $node;
    }

    /**
     * Adds the configuration for the "clients" key.
     */
    private function addClientsSection(ArrayNodeDefinition $rootNode)
    {
        $rootNode
            ->fixXmlConfig('client')
            ->children()
                ->arrayNode('clients')
                    ->useAttributeAsKey('id')
                    ->prototype('array')
                        ->performNoDeepMerging()
                        // Elastica names its properties with camel case, support both
                        ->beforeNormalization()
                        ->ifTrue(function ($v) {
                            return isset($v['connection_strategy']);
                        })
                        ->then(function ($v) {
                            $v['connectionStrategy'] = $v['connection_strategy'];
                            unset($v['connection_strategy']);

                            return $v;
                        })
                        ->end()
                        // If there is no connections array key defined, assume a single connection.
                        ->beforeNormalization()
                        ->ifTrue(function ($v) {
                            return is_array($v) && !array_key_exists('connections', $v);
                        })
                        ->then(function ($v) {
                            return [
                                'connections' => [$v],
                            ];
                        })
                        ->end()
                        ->children()
                            ->arrayNode('connections')
                                ->requiresAtLeastOneElement()
                                ->prototype('array')
                                    ->fixXmlConfig('header')
                                    ->children()
                                        ->scalarNode('url')
                                            ->validate()
                                                ->ifTrue(function ($url) {
                                                    return $url && '/' !== substr($url, -1);
                                                })
                                                ->then(function ($url) {
                                                    return $url.'/';
                                                })
                                            ->end()
                                        ->end()
                                        ->scalarNode('username')->end()
                                        ->scalarNode('password')->end()
                                        ->scalarNode('host')->end()
                                        ->scalarNode('port')->end()
                                        ->scalarNode('proxy')->end()
                                        ->arrayNode('http_error_codes')
                                            ->beforeNormalization()
                                                ->ifTrue(function ($v) { return !is_array($v); })
                                                ->then(function ($v) { return array($v); })
                                            ->end()
                                            ->cannotBeEmpty()
                                            ->defaultValue([400, 403, 404])
                                            ->prototype('scalar')->end()
                                        ->end()
                                        ->scalarNode('aws_access_key_id')->end()
                                        ->scalarNode('aws_secret_access_key')->end()
                                        ->scalarNode('aws_region')->end()
                                        ->scalarNode('aws_session_token')->end()
                                        ->booleanNode('ssl')->defaultValue(false)->end()
                                        ->scalarNode('logger')
                                            ->defaultValue($this->debug ? 'fos_elastica.logger' : false)
                                            ->treatNullLike('fos_elastica.logger')
                                            ->treatTrueLike('fos_elastica.logger')
                                        ->end()
                                        ->booleanNode('compression')->defaultValue(false)->end()
                                        ->arrayNode('headers')
                                            ->normalizeKeys(false)
                                            ->useAttributeAsKey('name')
                                            ->prototype('scalar')->end()
                                        ->end()
                                        ->arrayNode('curl')
                                            ->useAttributeAsKey(CURLOPT_SSL_VERIFYPEER)
                                            ->prototype('boolean')->end()
                                        ->end()
                                        ->scalarNode('transport')->end()
                                        ->scalarNode('timeout')->end()
                                        ->scalarNode('connectTimeout')->end()
                                        ->scalarNode('retryOnConflict')
                                            ->defaultValue(0)
                                        ->end()
                                        ->booleanNode('persistent')->defaultValue(true)->end()
                                    ->end()
                                ->end()
                            ->end()
                            ->scalarNode('timeout')->end()
                            ->scalarNode('connectTimeout')->end()
                            ->scalarNode('headers')->end()
                            ->scalarNode('connectionStrategy')->defaultValue('Simple')->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;
    }

    /**
     * Adds the configuration for the "indexes" key.
     */
    private function addIndexesSection(ArrayNodeDefinition $rootNode)
    {
        $rootNode
            ->fixXmlConfig('index')
            ->children()
                ->arrayNode('indexes')
                    ->useAttributeAsKey('name')
                    ->prototype('array')
                        ->children()
                            ->scalarNode('index_name')
                                ->info('Defaults to the name of the index, but can be modified if the index name is different in ElasticSearch')
                            ->end()
                            ->booleanNode('use_alias')->defaultValue(false)->end()
                            ->scalarNode('client')->end()
                            ->scalarNode('finder')
                                ->treatNullLike(true)
                                ->defaultFalse()
                            ->end()
                            ->arrayNode('type_prototype')
                                ->children()
                                    ->scalarNode('analyzer')->end()
                                    ->append($this->getPersistenceNode())
                                    ->append($this->getSerializerNode())
                                ->end()
                            ->end()
                            ->variableNode('settings')->defaultValue([])->end()
                        ->end()
                        ->append($this->getTypesNode())
                    ->end()
                ->end()
            ->end()
        ;
    }

    /**
     * @return ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition
     */
    private function createTreeBuilderNode($name)
    {
        $builder = new TreeBuilder($name);

        if (method_exists($builder, 'getRootNode')) {
            $node = $builder->getRootNode();
        } else {
            // BC layer for symfony/config 4.1 and older
            $node = $builder->root($name);
        }

        return $node;
    }

    /**
     * Adds the configuration for the "index_templates" key.
     *
     * @param ArrayNodeDefinition $rootNode
     *
     * @return void
     */
    private function addIndexTemplatesSection(ArrayNodeDefinition $rootNode)
    {
        $rootNode
            ->fixXmlConfig('index_template')
            ->children()
                ->arrayNode('index_templates')
                    ->useAttributeAsKey('name')
                    ->prototype('array')
                        ->children()
                            ->scalarNode('template_name')
                                ->info('Defaults to the name of the index template, but can be modified if the index name is different in ElasticSearch')
                            ->end()
                            ->scalarNode('template')->isRequired()->end()
                            ->scalarNode('client')->end()
                            ->variableNode('settings')->defaultValue([])->end()
                        ->end()
                        ->append($this->getTypesNode())
                    ->end()
                ->end()
            ->end()
        ;
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Doctrine;

use Doctrine\Common\Persistence\ManagerRegistry;
use FOS\ElasticaBundle\HybridResult;
use FOS\ElasticaBundle\Transformer\AbstractElasticaToModelTransformer as BaseTransformer;
use FOS\ElasticaBundle\Transformer\HighlightableModelInterface;

/**
 * Maps Elastica documents with Doctrine objects
 * This mapper assumes an exact match between
 * elastica documents ids and doctrine object ids.
 */
abstract class AbstractElasticaToModelTransformer extends BaseTransformer
{
    /**
     * Manager registry.
     *
     * @var ManagerRegistry
     */
    protected $registry = null;

    /**
     * Class of the model to map to the elastica documents.
     *
     * @var string
     */
    protected $objectClass = null;

    /**
     * Optional parameters.
     *
     * @var array
     */
    protected $options = [
        'hints' => [],
        'hydrate' => true,
        'identifier' => 'id',
        'ignore_missing' => false,
        'query_builder_method' => 'createQueryBuilder',
    ];

    /**
     * Instantiates a new Mapper.
     *
     * @param ManagerRegistry $registry
     * @param string          $objectClass
     * @param array           $options
     */
    public function __construct(ManagerRegistry $registry, $objectClass, array $options = [])
    {
        $this->registry = $registry;
        $this->objectClass = $objectClass;
        $this->options = array_merge($this->options, $options);
    }

    /**
     * Returns the object class that is used for conversion.
     *
     * @return string
     */
    public function getObjectClass()
    {
        return $this->objectClass;
    }

    /**
     * Transforms an array of elastica objects into an array of
     * model objects fetched from the doctrine repository.
     *
     * @param array $elasticaObjects of elastica objects
     *
     * @throws \RuntimeException
     *
     * @return array
     **/
    public function transform(array $elasticaObjects)
    {
        $ids = $highlights = [];
        foreach ($elasticaObjects as $elasticaObject) {
            $ids[] = $elasticaObject->getId();
            $highlights[$elasticaObject->getId()] = $elasticaObject->getHighlights();
        }

        $objects = $this->findByIdentifiers($ids, $this->options['hydrate']);
        $objectsCnt = count($objects);
        $elasticaObjectsCnt = count($elasticaObjects);
        if (!$this->options['ignore_missing'] && $objectsCnt < $elasticaObjectsCnt) {
            throw new \RuntimeException(sprintf('Cannot find corresponding Doctrine objects (%d) for all Elastica results (%d). IDs: %s', $objectsCnt, $elasticaObjectsCnt, implode(', ', $ids)));
        }

        $propertyAccessor = $this->propertyAccessor;
        $identifier = $this->options['identifier'];
        foreach ($objects as $object) {
            if ($object instanceof HighlightableModelInterface) {
                $id = $propertyAccessor->getValue($object, $identifier);
                $object->setElasticHighlights($highlights[(string) $id]);
            }
        }

        // sort objects in the order of ids
        $idPos = array_flip($ids);
        usort(
            $objects,
            function ($a, $b) use ($idPos, $identifier, $propertyAccessor) {
                if ($this->options['hydrate']) {
                    return $idPos[(string) $propertyAccessor->getValue(
                        $a,
                        $identifier
                    )] > $idPos[(string) $propertyAccessor->getValue($b, $identifier)];
                }

                return $idPos[$a[$identifier]] > $idPos[$b[$identifier]];
            }
        );

        return $objects;
    }

    public function hybridTransform(array $elasticaObjects)
    {
        $indexedElasticaResults = [];
        foreach ($elasticaObjects as $elasticaObject) {
            $indexedElasticaResults[(string) $elasticaObject->getId()] = $elasticaObject;
        }

        $objects = $this->transform($elasticaObjects);

        $result = [];
        foreach ($objects as $object) {
            if ($this->options['hydrate']) {
                $id = $this->propertyAccessor->getValue($object, $this->options['identifier']);
            } else {
                $id = $object[$this->options['identifier']];
            }
            $result[] = new HybridResult($indexedElasticaResults[(string) $id], $object);
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentifierField()
    {
        return $this->options['identifier'];
    }

    /**
     * Fetches objects by theses identifier values.
     *
     * @param array $identifierValues ids values
     * @param bool  $hydrate          whether or not to hydrate the objects, false returns arrays
     *
     * @return array of objects or arrays
     */
    abstract protected function findByIdentifiers(array $identifierValues, $hydrate);
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\DependencyInjection\Compiler;

use InvalidArgumentException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Registers Transformer implementations into the TransformerCollection.
 *
 * @author Tim Nagel <tim@nagel.com.au>
 */
class TransformerPass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('fos_elastica.elastica_to_model_transformer.collection')) {
            return;
        }

        $transformers = [];

        foreach ($container->findTaggedServiceIds('fos_elastica.elastica_to_model_transformer') as $id => $tags) {
            foreach ($tags as $tag) {
                if (empty($tag['index']) || empty($tag['type'])) {
                    throw new InvalidArgumentException('The Transformer must have both a type and an index defined.');
                }

                $transformers[$tag['index']][$tag['type']] = new Reference($id);
            }
        }

        foreach ($transformers as $index => $indexTransformers) {
            if (!$container->hasDefinition(sprintf('fos_elastica.elastica_to_model_transformer.collection.%s', $index))) {
                continue;
            }

            $index = $container->getDefinition(sprintf('fos_elastica.elastica_to_model_transformer.collection.%s', $index));
            $index->replaceArgument(0, $indexTransformers);
        }
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Doctrine\MongoDB;

use FOS\ElasticaBundle\Doctrine\AbstractElasticaToModelTransformer;

/**
 * Maps Elastica documents with Doctrine objects
 * This mapper assumes an exact match between
 * elastica documents ids and doctrine object ids.
 */
class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer
{
    /**
     * Fetch objects for theses identifier values.
     *
     * @param array $identifierValues ids values
     * @param bool  $hydrate          whether or not to hydrate the objects, false returns arrays
     *
     * @return array of objects or arrays
     */
    protected function findByIdentifiers(array $identifierValues, $hydrate)
    {
        return $this->registry
            ->getManagerForClass($this->objectClass)
            ->getRepository($this->objectClass)
            ->{$this->options['query_builder_method']}($this->objectClass)
            ->field($this->options['identifier'])->in($identifierValues)
            ->hydrate($hydrate)
            ->getQuery()
            ->execute()
            ->toArray();
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Provider;

use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;

class Indexable implements IndexableInterface
{
    /**
     * An array of raw configured callbacks for all types.
     *
     * @var array
     */
    private $callbacks = [];

    /**
     * An instance of ExpressionLanguage.
     *
     * @var ExpressionLanguage
     */
    private $expressionLanguage;

    /**
     * An array of initialised callbacks.
     *
     * @var array
     */
    private $initialisedCallbacks = [];

    public function __construct(array $callbacks)
    {
        $this->callbacks = $callbacks;
    }

    /**
     * Return whether the object is indexable with respect to the callback.
     *
     * @param string $indexName
     * @param string $typeName
     * @param mixed  $object
     *
     * @return bool
     */
    public function isObjectIndexable($indexName, $typeName, $object)
    {
        $type = sprintf('%s/%s', $indexName, $typeName);
        $callback = $this->getCallback($type, $object);
        if (!$callback) {
            return true;
        }

        if ($callback instanceof Expression) {
            return (bool) $this->getExpressionLanguage()->evaluate($callback, [
                'object' => $object,
                $this->getExpressionVar($object) => $object,
            ]);
        }

        return is_string($callback)
            ? call_user_func([$object, $callback])
            : call_user_func($callback, $object);
    }

    /**
     * Builds and initialises a callback.
     *
     * @param string $type
     * @param object $object
     *
     * @return callable|string|ExpressionLanguage|null
     */
    private function buildCallback($type, $object)
    {
        if (!array_key_exists($type, $this->callbacks)) {
            return null;
        }

        $callback = $this->callbacks[$type];

        if (is_callable($callback) or is_callable([$object, $callback])) {
            return $callback;
        }

        if (is_string($callback)) {
            return $this->buildExpressionCallback($type, $object, $callback);
        }

        throw new \InvalidArgumentException(sprintf('Callback for type "%s" is not a valid callback.', $type));
    }

    /**
     * Processes a string expression into an Expression.
     *
     * @param string $type
     * @param mixed  $object
     * @param string $callback
     *
     * @return Expression
     */
    private function buildExpressionCallback($type, $object, $callback)
    {
        $expression = $this->getExpressionLanguage();
        if (!$expression) {
            throw new \RuntimeException('Unable to process an expression without the ExpressionLanguage component.');
        }

        try {
            $callback = new Expression($callback);
            $expression->compile($callback, [
                'object', $this->getExpressionVar($object),
            ]);

            return $callback;
        } catch (SyntaxError $e) {
            throw new \InvalidArgumentException(sprintf(
                'Callback for type "%s" is an invalid expression',
                $type
            ), $e->getCode(), $e);
        }
    }

    /**
     * Retreives a cached callback, or creates a new callback if one is not found.
     *
     * @param string $type
     * @param object $object
     *
     * @return mixed
     */
    private function getCallback($type, $object)
    {
        if (!array_key_exists($type, $this->initialisedCallbacks)) {
            $this->initialisedCallbacks[$type] = $this->buildCallback($type, $object);
        }

        return $this->initialisedCallbacks[$type];
    }

    /**
     * Returns the ExpressionLanguage class if it is available.
     *
     * @return ExpressionLanguage|null
     */
    private function getExpressionLanguage()
    {
        if (null === $this->expressionLanguage) {
            $this->expressionLanguage = new ExpressionLanguage();
        }

        return $this->expressionLanguage;
    }

    /**
     * Returns the variable name to be used to access the object when using the ExpressionLanguage
     * component to parse and evaluate an expression.
     *
     * @param mixed $object
     *
     * @return string
     */
    private function getExpressionVar($object = null)
    {
        if (!is_object($object)) {
            return 'object';
        }

        $ref = new \ReflectionClass($object);

        return strtolower($ref->getShortName());
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Configuration;

/**
 * Central manager for index and type configuration.
 */
class ConfigManager implements ManagerInterface
{
    /**
     * @var IndexConfig[]
     */
    private $indexes = [];

    /**
     * @param Source\SourceInterface[] $sources
     */
    public function __construct(array $sources)
    {
        foreach ($sources as $source) {
            $this->indexes = array_merge($source->getConfiguration(), $this->indexes);
        }
    }

    /**
     * @param string $indexName
     *
     * @return IndexConfig
     */
    public function getIndexConfiguration($indexName)
    {
        if (!$this->hasIndexConfiguration($indexName)) {
            throw new \InvalidArgumentException(sprintf('Index with name "%s" is not configured.', $indexName));
        }

        return $this->indexes[$indexName];
    }

    /**
     * {@inheritdoc}
     */
    public function getIndexNames()
    {
        return array_keys($this->indexes);
    }

    /**
     * {@inheritdoc}
     */
    public function getTypeConfiguration($indexName, $typeName)
    {
        $index = $this->getIndexConfiguration($indexName);
        $type = $index->getType($typeName);

        if (!$type) {
            throw new \InvalidArgumentException(sprintf('Type with name "%s" on index "%s" is not configured', $typeName, $indexName));
        }

        return $type;
    }

    /**
     * @param string $indexName
     *
     * @return bool
     */
    public function hasIndexConfiguration($indexName)
    {
        return isset($this->indexes[$indexName]);
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Index;

use Elastica\Client;
use Elastica\Exception\ExceptionInterface;
use Elastica\Request;
use FOS\ElasticaBundle\Configuration\IndexConfig;
use FOS\ElasticaBundle\Elastica\Index;
use FOS\ElasticaBundle\Exception\AliasIsIndexException;

class AliasProcessor
{
    /**
     * Sets the randomised root name for an index.
     *
     * @param IndexConfig $indexConfig
     * @param Index       $index
     */
    public function setRootName(IndexConfig $indexConfig, Index $index)
    {
        $index->overrideName(
            sprintf('%s_%s',
                $indexConfig->getElasticSearchName(),
                date('Y-m-d-His')
            )
        );
    }

    /**
     * Switches an index to become the new target for an alias. Only applies for
     * indexes that are set to use aliases.
     *
     * $force will delete an index encountered where an alias is expected.
     *
     * @param IndexConfig $indexConfig
     * @param Index       $index
     * @param bool        $force
     * @param bool        $delete
     *
     * @throws AliasIsIndexException
     */
    public function switchIndexAlias(IndexConfig $indexConfig, Index $index, $force = false, $delete = true)
    {
        $client = $index->getClient();

        $aliasName = $indexConfig->getElasticSearchName();
        $oldIndexName = null;
        $newIndexName = $index->getName();

        try {
            $oldIndexName = $this->getAliasedIndex($client, $aliasName);
        } catch (AliasIsIndexException $e) {
            if (!$force) {
                throw $e;
            }

            if ($delete) {
                $this->deleteIndex($client, $aliasName);
            } else {
                $this->closeIndex($client, $aliasName);
            }
        }

        try {
            $aliasUpdateRequest = $this->buildAliasUpdateRequest($oldIndexName, $aliasName, $newIndexName);
            $client->request('_aliases', 'POST', $aliasUpdateRequest);
        } catch (ExceptionInterface $e) {
            $this->cleanupRenameFailure($client, $newIndexName, $e);
        }

        // Delete the old index after the alias has been switched
        if (null !== $oldIndexName) {
            if ($delete) {
                $this->deleteIndex($client, $oldIndexName);
            } else {
                $this->closeIndex($client, $oldIndexName);
            }
        }
    }

    /**
     * Builds an ElasticSearch request to rename or create an alias.
     *
     * @param string|null $aliasedIndex
     * @param string      $aliasName
     * @param string      $newIndexName
     *
     * @return array
     */
    private function buildAliasUpdateRequest($aliasedIndex, $aliasName, $newIndexName)
    {
        $aliasUpdateRequest = ['actions' => []];
        if (null !== $aliasedIndex) {
            // if the alias is set - add an action to remove it
            $aliasUpdateRequest['actions'][] = [
                'remove' => ['index' => $aliasedIndex, 'alias' => $aliasName],
            ];
        }

        // add an action to point the alias to the new index
        $aliasUpdateRequest['actions'][] = [
            'add' => ['index' => $newIndexName, 'alias' => $aliasName],
        ];

        return $aliasUpdateRequest;
    }

    /**
     * Cleans up an index when we encounter a failure to rename the alias.
     *
     * @param Client     $client
     * @param string     $indexName
     * @param \Exception $renameAliasException
     */
    private function cleanupRenameFailure(Client $client, $indexName, \Exception $renameAliasException)
    {
        $additionalError = '';
        try {
            $this->deleteIndex($client, $indexName);
        } catch (ExceptionInterface $deleteNewIndexException) {
            $additionalError = sprintf(
                'Tried to delete newly built index %s, but also failed: %s',
                $indexName,
                $deleteNewIndexException->getMessage()
            );
        }

        throw new \RuntimeException(sprintf(
            'Failed to updated index alias: %s. %s',
            $renameAliasException->getMessage(),
            $additionalError ?: sprintf('Newly built index %s was deleted', $indexName)
        ), 0, $renameAliasException);
    }

    /**
     * Delete an index.
     *
     * @param Client $client
     * @param string $indexName Index name to delete
     */
    private function deleteIndex(Client $client, $indexName)
    {
        try {
            $path = sprintf('%s', $indexName);
            $client->request($path, Request::DELETE);
        } catch (ExceptionInterface $deleteOldIndexException) {
            throw new \RuntimeException(sprintf(
                'Failed to delete index %s with message: %s',
                $indexName,
                $deleteOldIndexException->getMessage()
            ), 0, $deleteOldIndexException);
        }
    }

    /**
     * Close an index.
     *
     * @param Client $client
     * @param string $indexName
     */
    private function closeIndex(Client $client, $indexName)
    {
        try {
            $path = sprintf('%s/_close', $indexName);
            $client->request($path, Request::POST);
        } catch (ExceptionInterface $e) {
            throw new \RuntimeException(
                sprintf(
                    'Failed to close index %s with message: %s',
                    $indexName,
                    $e->getMessage()
                ),
                0,
                $e
            );
        }
    }

    /**
     * Returns the name of a single index that an alias points to or throws
     * an exception if there is more than one.
     *
     * @param Client $client
     * @param string $aliasName Alias name
     *
     * @return string|null
     *
     * @throws AliasIsIndexException
     */
    private function getAliasedIndex(Client $client, $aliasName)
    {
        $aliasesInfo = $client->request('_aliases', 'GET')->getData();
        $aliasedIndexes = [];

        foreach ($aliasesInfo as $indexName => $indexInfo) {
            if ($indexName === $aliasName) {
                throw new AliasIsIndexException($indexName);
            }
            if (!isset($indexInfo['aliases'])) {
                continue;
            }

            $aliases = array_keys($indexInfo['aliases']);
            if (in_array($aliasName, $aliases)) {
                $aliasedIndexes[] = $indexName;
            }
        }

        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,
                implode(', ', $aliasedIndexes)
            ));
        }

        return array_shift($aliasedIndexes);
    }
}


<?php

/*
 * This file is part of the FOSElasticaBundle package.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace FOS\ElasticaBundle\Elastica;

use Elastica\Client as BaseClient;
use Elastica\Exception\ClientException;
use Elastica\Request;
use FOS\ElasticaBundle\Logger\ElasticaLogger;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * Extends the default Elastica client to provide logging for errors that occur
 * during communication with ElasticSearch.
 *
 * @author Gordon Franke <info@nevalon.de>
 */
class Client extends BaseClient
{
    /**
     * Stores created indexes to avoid recreation.
     *
     * @var array
     */
    private $indexCache = [];

    /**
     * Stores created index template to avoid recreation.
     *
     * @var array
     */
    private $indexTemplateCache = array();

    /**
     * Symfony's debugging Stopwatch.
     *
     * @var Stopwatch|null
     */
    private $stopwatch;

    /**
     * {@inheritdoc}
     */
    public function request($path, $method = Request::GET, $data = [], array $query = [], $contentType = Request::DEFAULT_CONTENT_TYPE)
    {
        if ($this->stopwatch) {
            $this->stopwatch->start('es_request', 'fos_elastica');
        }

        $response = parent::request($path, $method, $data, $query, $contentType);
        $responseData = $response->getData();

        $transportInfo = $response->getTransferInfo();
        $connection = $this->getLastRequest()->getConnection();
        $forbiddenHttpCodes = $connection->hasConfig('http_error_codes') ? $connection->getConfig('http_error_codes') : [];

        if (isset($transportInfo['http_code']) && in_array($transportInfo['http_code'], $forbiddenHttpCodes, true)) {
            $body = is_array($responseData) ? json_encode($responseData) : $responseData;
            $message = sprintf('Error in transportInfo: response code is %s, response body is %s', $transportInfo['http_code'], $body);
            throw new ClientException($message);
        }

        if (isset($responseData['took']) && isset($responseData['hits'])) {
            $this->logQuery($path, $method, $data, $query, $response->getQueryTime(), $response->getEngineTime(), $responseData['hits']['total']);
        } else {
            $this->logQuery($path, $method, $data, $query, $response->getQueryTime(), 0, 0);
        }

        if ($this->stopwatch) {
            $this->stopwatch->stop('es_request');
        }

        return $response;
    }

    /**
     * @param string $name
     *
     * @return Index|mixed
     */
    public function getIndex($name)
    {
        if (isset($this->indexCache[$name])) {
            return $this->indexCache[$name];
        }

        return $this->indexCache[$name] = new Index($this, $name);
    }

    public function getIndexTemplate($name)
    {
        if (isset($this->indexTemplateCache[$name])) {
            return $this->indexTemplateCache[$name];
        }

        return $this->indexTemplateCache[$name] = new IndexTemplate($this, $name);
    }

    /**
     * Sets a stopwatch instance for debugging purposes.
     *
     * @param Stopwatch $stopwatch
     */
    public function setStopwatch(Stopwatch $stopwatch = null)
    {
        $this->stopwatch = $stopwatch;
    }

    /**
     * Log the query if we have an instance of ElasticaLogger.
     *
     * @param string $path
     * @param string $method
     * @param array|string $data
     * @param array  $query
     * @param int    $queryTime
     * @param int    $engineMS
     * @param int    $itemCount
     */
    private function logQuery($path, $method, $data, array $query, $queryTime, $engineMS = 0, $itemCount = 0)
    {
        if (!$this->_logger or !$this->_logger instanceof ElasticaLogger) {
            return;
        }

        $connection = $this->getLastRequest()->getConnection();

        $connectionArray = [
            'host' => $connection->getHost(),
            'port' => $connection->getPort(),
            'transport' => $connection->getTransport(),
            'headers' => $connection->hasConfig('headers') ? $connection->getConfig('headers') : [],
        ];

        /** @var ElasticaLogger $logger */
        $logger = $this->_logger;
        $logger->logQuery($path, $method, $data, $queryTime, $connectionArray, $query, $engineMS, $itemCount);
    }
}