├── .atoum.php ├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json └── src └── M6Web └── Bundle └── ElasticsearchBundle ├── DataCollector └── ElasticsearchDataCollector.php ├── DependencyInjection ├── Configuration.php └── M6WebElasticsearchExtension.php ├── Elasticsearch └── ConnectionPool │ ├── Selector │ └── RandomStickySelector.php │ └── StaticAliveNoPingConnectionPool.php ├── EventDispatcher └── ElasticsearchEvent.php ├── Handler ├── EventHandler.php └── HeadersHandler.php ├── M6WebElasticsearchBundle.php ├── Resources ├── config │ └── services.yml └── views │ └── Collector │ └── elasticsearch.html.twig └── Tests └── Units ├── DependencyInjection └── M6WebElasticsearchExtension.php ├── Elasticsearch ├── ConnectionMocker.php └── ConnectionPool │ ├── Selector │ └── RandomStickySelector.php │ └── StaticAliveNoPingConnectionPool.php └── Handler ├── EventHandler.php └── HeadersHandler.php /.atoum.php: -------------------------------------------------------------------------------- 1 | addTestsFromDirectory(__DIR__.'/src/M6Web/Bundle/ElasticsearchBundle/Tests'); 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @BedrockStreaming/cache-db 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | tests: 6 | name: Tests 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | matrix: 10 | php-version: [ '7.4', '8.0', '8.1' ] 11 | symfony-version: ['^4.4', '^5.0', '^5.3', '^6.0'] 12 | exclude: 13 | - php-version: '7.4' 14 | symfony-version: '^6.0' 15 | fail-fast: false 16 | steps: 17 | - uses: actions/checkout@master 18 | - uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-version }} 21 | coverage: xdebug2 22 | extensions: apcu 23 | ini-values: apc.enable_cli=1 24 | - name: Install symfony version from matrix 25 | env: 26 | SYMFONY_VERSION: ${{ matrix.symfony-version }} 27 | run: composer require symfony/symfony:$SYMFONY_VERSION --no-update 28 | - name: Install dependencies 29 | run: composer update --prefer-dist --no-interaction 30 | - name: Unit tests 31 | run: |- 32 | vendor/bin/php-cs-fixer fix --dry-run --using-cache=no --verbose 33 | vendor/bin/atoum 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .php-cs-fixer.cache 4 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in( 5 | [ 6 | __DIR__.'/src', 7 | ] 8 | ); 9 | 10 | $config = new M6Web\CS\Config\BedrockStreaming(); 11 | $config->setFinder($finder); 12 | 13 | return $config; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 M6Web 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElasticsearchBundle [![Build Status](https://github.com/BedrockStreaming/ElasticsearchBundle/actions/workflows/ci.yml/badge.svg)](https://github.com/BedrockStreaming/ElasticsearchBundle/actions/workflows/ci.yml) [![Total Downloads](https://poser.pugx.org/m6web/elasticsearch-bundle/downloads.svg)](https://packagist.org/packages/m6web/elasticsearch-bundle) [![License](http://poser.pugx.org/m6web/elasticsearch-bundle/license)](https://packagist.org/packages/m6web/elasticsearch-bundle) [![PHP Version Require](http://poser.pugx.org/m6web/elasticsearch-bundle/require/php)](https://packagist.org/packages/m6web/elasticsearch-bundle) 2 | 3 | 4 | Integration of the [Elasticsearch official PHP client](http://github.com/elasticsearch/elasticsearch-php) within a Symfony Project. 5 | 6 | ## Features 7 | 8 | This bundle creates one or more Elasticsearch client services from settings defined in the application configuration. 9 | 10 | ## Usage 11 | 12 | ### Installation 13 | 14 | You must first add the bundle to your `composer.json`: 15 | 16 | ```json 17 | "require": { 18 | "m6web/elasticsearch-bundle": "dev-master" 19 | } 20 | ``` 21 | 22 | Then register the bundle in your `AppKernel` class: 23 | 24 | ```php 25 | =7.4", 14 | "ext-json": "*", 15 | "elasticsearch/elasticsearch": "^5.1.0||^6.0.0||^7.0", 16 | "symfony/event-dispatcher": "^4.4||^5.0||^6.0", 17 | "symfony/yaml": "^4.4||^5.0||^6.0", 18 | "symfony/http-foundation": "^4.4||^5.0||^6.0", 19 | "symfony/config": "^4.4||^5.0||^6.0", 20 | "symfony/http-kernel": "^4.4||^5.0||^6.0", 21 | "symfony/dependency-injection": "^4.4||^5.0||^6.0" 22 | }, 23 | "require-dev": { 24 | "atoum/atoum": "^4.0", 25 | "m6web/php-cs-fixer-config": "^2.0" 26 | }, 27 | "autoload": { 28 | "psr-0": { "": "src/" } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/DataCollector/ElasticsearchDataCollector.php: -------------------------------------------------------------------------------- 1 | reset(); 23 | } 24 | 25 | public function handleEvent(ElasticsearchEvent $event) 26 | { 27 | $query = [ 28 | 'method' => $event->getMethod(), 29 | 'uri' => $event->getUri(), 30 | 'headers' => $this->stringifyVariable($event->getHeaders()), 31 | 'status_code' => $event->getStatusCode(), 32 | 'duration' => $event->getDuration(), 33 | 'took' => $event->getTook(), 34 | 'body' => json_decode($event->getBody()), 35 | 'error' => $event->getError(), 36 | ]; 37 | $this->data['queries'][] = $query; 38 | $this->data['total_execution_time'] += $query['duration']; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function collect(Request $request, Response $response, \Throwable $exception = null) 45 | { 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getName(): string 52 | { 53 | return 'elasticsearch'; 54 | } 55 | 56 | public function getQueries(): array 57 | { 58 | return $this->data['queries']; 59 | } 60 | 61 | public function getTotalExecutionTime(): float 62 | { 63 | return $this->data['total_execution_time']; 64 | } 65 | 66 | /** 67 | * Resets this data collector to its initial state. 68 | */ 69 | public function reset() 70 | { 71 | $this->data = [ 72 | 'queries' => [], 73 | 'total_execution_time' => 0, 74 | ]; 75 | } 76 | 77 | /** 78 | * Converts a PHP variable to a string or 79 | * Converts the variable into a serializable Data instance. 80 | * 81 | * The convert action depend on method available in the sf DataCollector class. 82 | * In sf >= 4, the DataCollector::varToString() doesn't exists anymore 83 | * 84 | * @param mixed $var 85 | * 86 | * @return mixed 87 | */ 88 | protected function stringifyVariable($var) 89 | { 90 | if (method_exists($this, 'varToString')) { 91 | return $this->varToString($var); 92 | } else { 93 | return $this->cloneVar($var); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 24 | 25 | $rootNode 26 | ->children() 27 | ->scalarNode('default_client')->end() 28 | ->arrayNode('clients') 29 | ->useAttributeAsKey('id') 30 | ->prototype('array') 31 | ->children() 32 | ->arrayNode('hosts') 33 | ->isRequired() 34 | ->requiresAtLeastOneElement() 35 | ->performNoDeepMerging() 36 | ->prototype('scalar')->end() 37 | ->end() 38 | // @deprecated 39 | ->scalarNode('client_class')->defaultValue('Elasticsearch\Client')->end() 40 | ->scalarNode('clientBuilderClass')->defaultValue('Elasticsearch\ClientBuilder')->end() 41 | ->scalarNode('connectionPoolClass')->end() 42 | ->scalarNode('selectorClass')->end() 43 | ->variableNode('connectionParams') 44 | ->validate()->ifString()->thenInvalid('connectionParams cannot be a string')->end() 45 | ->defaultValue([]) 46 | ->end() 47 | ->integerNode('retries')->end() 48 | ->scalarNode('logger')->end() 49 | ->variableNode('headers')->end() 50 | ->end() 51 | ->end() 52 | ->end(); 53 | 54 | return $treeBuilder; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/DependencyInjection/M6WebElasticsearchExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 28 | 29 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 30 | $loader->load('services.yml'); 31 | 32 | if (isset($config['clients'])) { 33 | foreach ($config['clients'] as $clientName => $clientConfig) { 34 | $this->createElasticsearchClient($container, $clientName, $clientConfig); 35 | } 36 | 37 | if (isset($config['default_client'])) { 38 | $container->setAlias( 39 | 'm6web_elasticsearch.client.default', 40 | sprintf('m6web_elasticsearch.client.%s', $config['default_client']) 41 | ); 42 | } 43 | } 44 | } 45 | 46 | public function getAlias(): string 47 | { 48 | return 'm6web_elasticsearch'; 49 | } 50 | 51 | /** 52 | * Add a new Elasticsearch client definition in the container 53 | * 54 | * @param string $name 55 | * @param array $config 56 | */ 57 | protected function createElasticsearchClient(ContainerBuilder $container, $name, $config) 58 | { 59 | $definitionId = 'm6web_elasticsearch.client.'.$name; 60 | 61 | $handlerId = $this->createHandler($container, $config, $definitionId); 62 | 63 | $clientConfig = [ 64 | 'hosts' => $config['hosts'], 65 | 'handler' => new Reference($handlerId), 66 | ]; 67 | 68 | if (isset($config['retries'])) { 69 | $clientConfig['retries'] = $config['retries']; 70 | } 71 | 72 | if (isset($config['logger'])) { 73 | $clientConfig['logger'] = new Reference($config['logger']); 74 | } 75 | 76 | if (!empty($config['connectionPoolClass'])) { 77 | $clientConfig['connectionPool'] = $config['connectionPoolClass']; 78 | } 79 | 80 | if (!empty($config['selectorClass'])) { 81 | $clientConfig['selector'] = $config['selectorClass']; 82 | } 83 | 84 | if (!empty($config['connectionParams'])) { 85 | $clientConfig['connectionParams'] = $config['connectionParams']; 86 | } 87 | 88 | $definition = (new Definition($config['client_class'])) 89 | ->setArguments([$clientConfig]); 90 | $this->setFactoryToDefinition($config['clientBuilderClass'], 'fromConfig', $definition); 91 | 92 | $container->setDefinition($definitionId, $definition); 93 | 94 | if ($container->getParameter('kernel.debug')) { 95 | $this->createDataCollector($container); 96 | } 97 | } 98 | 99 | /** 100 | * Create request handler 101 | */ 102 | protected function createHandler(ContainerBuilder $container, array $config, string $definitionId): string 103 | { 104 | // cURL handler 105 | $singleHandler = (new Definition('GuzzleHttp\Ring\Client\CurlHandler')) 106 | ->setPublic(false); 107 | 108 | $this->setFactoryToDefinition('Elasticsearch\ClientBuilder', 'defaultHandler', $singleHandler); 109 | 110 | $singleHandlerId = $definitionId.'.single_handler'; 111 | $container->setDefinition($singleHandlerId, $singleHandler); 112 | 113 | // Headers handler 114 | $headersHandler = (new Definition('M6Web\Bundle\ElasticsearchBundle\Handler\HeadersHandler')) 115 | ->setPublic(false) 116 | ->setArguments([new Reference($singleHandlerId)]); 117 | if (isset($config['headers'])) { 118 | foreach ($config['headers'] as $key => $value) { 119 | $headersHandler->addMethodCall('setHeader', [$key, $value]); 120 | } 121 | } 122 | $headersHandlerId = $definitionId.'.headers_handler'; 123 | $container->setDefinition($headersHandlerId, $headersHandler); 124 | 125 | // Event handler 126 | $eventHandler = (new Definition('M6Web\Bundle\ElasticsearchBundle\Handler\EventHandler')) 127 | ->setPublic(false) 128 | ->setArguments([new Reference('event_dispatcher'), new Reference($headersHandlerId)]); 129 | $eventHandlerId = $definitionId.'.event_handler'; 130 | $container->setDefinition($eventHandlerId, $eventHandler); 131 | 132 | return $eventHandlerId; 133 | } 134 | 135 | private function setFactoryToDefinition(string $className, string $method, Definition $definition) 136 | { 137 | $definition 138 | ->setFactory([$className, $method]); 139 | } 140 | 141 | protected function createDataCollector(ContainerBuilder $container) 142 | { 143 | $collectorDefinition = new Definition( 144 | 'M6Web\Bundle\ElasticsearchBundle\DataCollector\ElasticsearchDataCollector' 145 | ); 146 | $collectorDefinition->addTag( 147 | 'data_collector', 148 | [ 149 | 'template' => '@M6WebElasticsearch/Collector/elasticsearch.html.twig', 150 | 'id' => 'elasticsearch', 151 | ] 152 | ); 153 | 154 | $collectorDefinition->addTag( 155 | 'kernel.event_listener', 156 | [ 157 | 'event' => 'm6web.elasticsearch', 158 | 'method' => 'handleEvent', 159 | ] 160 | ); 161 | 162 | $container->setDefinition('m6web_elasticsearch.data_collector', $collectorDefinition); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Elasticsearch/ConnectionPool/Selector/RandomStickySelector.php: -------------------------------------------------------------------------------- 1 | current === null) || !isset($connections[$this->current])) { 32 | $this->current = array_rand($connections); 33 | } 34 | 35 | return $connections[$this->current]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Elasticsearch/ConnectionPool/StaticAliveNoPingConnectionPool.php: -------------------------------------------------------------------------------- 1 | Extend StaticNoPingConnectionPool and add the ability to remove any failed connection 16 | * from the list of eligible connections to be chosen from by the selector. 17 | */ 18 | class StaticAliveNoPingConnectionPool extends StaticNoPingConnectionPool 19 | { 20 | private int $pingTimeout = 60; 21 | private int $maxPingTimeout = 3600; 22 | 23 | /** 24 | * > Allow to customize the ping time out. 25 | */ 26 | public function setPingTimeout(int $pingTimeout) 27 | { 28 | $this->pingTimeout = $pingTimeout; 29 | } 30 | 31 | /** 32 | * > Allow to customize the ping max time out. 33 | */ 34 | public function setMaxPingTimeout(int $maxPingTimeout) 35 | { 36 | $this->maxPingTimeout = $maxPingTimeout; 37 | } 38 | 39 | /** 40 | * @throws NoNodesAvailableException 41 | */ 42 | public function nextConnection(bool $force = false): ConnectionInterface 43 | { 44 | // > Replace $this->connections by $connections in order to modify the list later. 45 | $connections = $this->connections; 46 | $total = count($connections); 47 | while ($total--) { 48 | /** @var Connection $connection */ 49 | $connection = $this->selector->select($connections); 50 | if ($connection->isAlive() === true) { 51 | return $connection; 52 | } 53 | 54 | if ($this->readyToRevive($connection) === true) { 55 | return $connection; 56 | } 57 | 58 | // > Remove the failed connection from the list of eligible connections. 59 | $connections = array_filter( 60 | $connections, 61 | function ($baseConnection) use ($connection) { 62 | return $baseConnection !== $connection; 63 | } 64 | ); 65 | } 66 | 67 | throw new NoNodesAvailableException('No alive nodes found in your cluster'); 68 | } 69 | 70 | /** 71 | * > Same as parent private method. 72 | */ 73 | protected function readyToRevive(Connection $connection): bool 74 | { 75 | $timeout = min( 76 | $this->pingTimeout * pow(2, $connection->getPingFailures()), 77 | $this->maxPingTimeout 78 | ); 79 | 80 | if ($connection->getLastPing() + $timeout < time()) { 81 | return true; 82 | } else { 83 | return false; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/EventDispatcher/ElasticsearchEvent.php: -------------------------------------------------------------------------------- 1 | duration; 34 | } 35 | 36 | public function setDuration(float $duration): self 37 | { 38 | $this->duration = $duration; 39 | 40 | return $this; 41 | } 42 | 43 | public function getMethod(): ?string 44 | { 45 | return $this->method; 46 | } 47 | 48 | public function setMethod(?string $method): self 49 | { 50 | $this->method = $method; 51 | 52 | return $this; 53 | } 54 | 55 | public function getUri(): ?string 56 | { 57 | return $this->uri; 58 | } 59 | 60 | public function setUri(?string $uri): self 61 | { 62 | $this->uri = $uri; 63 | 64 | return $this; 65 | } 66 | 67 | public function getStatusCode(): ?int 68 | { 69 | return $this->statusCode; 70 | } 71 | 72 | public function setStatusCode(?int $statusCode): self 73 | { 74 | $this->statusCode = $statusCode; 75 | 76 | return $this; 77 | } 78 | 79 | public function getTook(): ?int 80 | { 81 | return $this->took; 82 | } 83 | 84 | public function setTook(?int $took): self 85 | { 86 | $this->took = $took; 87 | 88 | return $this; 89 | } 90 | 91 | public function getHeaders(): array 92 | { 93 | return $this->headers; 94 | } 95 | 96 | public function setHeaders(array $headers): self 97 | { 98 | $this->headers = $headers; 99 | 100 | return $this; 101 | } 102 | 103 | public function getBody(): string 104 | { 105 | return $this->body; 106 | } 107 | 108 | public function setBody(string $body): self 109 | { 110 | $this->body = $body; 111 | 112 | return $this; 113 | } 114 | 115 | public function getError(): string 116 | { 117 | return $this->error; 118 | } 119 | 120 | public function setError(string $error): self 121 | { 122 | $this->error = $error; 123 | 124 | return $this; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Handler/EventHandler.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 31 | $this->handler = $handler; 32 | } 33 | 34 | public function __invoke(array $request): \GuzzleHttp\Ring\Future\FutureArray 35 | { 36 | $handler = $this->handler; 37 | 38 | $wrapper = $this; 39 | $getTook = function (array $response) use ($wrapper) { 40 | return $wrapper->extractTookFromResponse($response); 41 | }; 42 | 43 | $dispatchEvent = function ($response) use ($request, $getTook) { 44 | $event = (new ElasticsearchEvent()) 45 | ->setUri($request['uri']) 46 | ->setMethod($request['http_method']) 47 | ->setStatusCode($response['status']) 48 | ->setDuration($response['transfer_stats']['total_time'] * 1000) 49 | ->setTook($getTook($response)); 50 | 51 | if (isset($request['body'])) { 52 | $event->setBody($request['body']); 53 | } 54 | if (isset($request['headers'])) { 55 | $event->setHeaders($request['headers']); 56 | } 57 | if (isset($response['error'])) { 58 | $event->setError($response['error']->getMessage()); 59 | } 60 | 61 | $this->eventDispatcher->dispatch($event, 'm6web.elasticsearch'); 62 | 63 | return $response; 64 | }; 65 | 66 | return Core::proxy($handler($request), $dispatchEvent); 67 | } 68 | 69 | protected function extractTookFromResponse(array $response): ?int 70 | { 71 | if (is_null($response['body'])) { 72 | return null; 73 | } 74 | $content = stream_get_contents($response['body']); 75 | rewind($response['body']); 76 | 77 | if (preg_match('/\"took\":(\d+)/', $content, $matches)) { 78 | return (int) $matches[1]; 79 | } 80 | 81 | return null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Handler/HeadersHandler.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 27 | } 28 | 29 | public function setHeader(string $key, $value): self 30 | { 31 | $this->headers[$key] = $value; 32 | 33 | return $this; 34 | } 35 | 36 | public function __invoke(array $request) 37 | { 38 | // By default, host is stored in headers and final url is forged later. 39 | // We need to build url now to handle the case when we want to add a custom Host header. 40 | $request['url'] = Core::url($request); 41 | 42 | $handler = $this->handler; 43 | foreach ($this->headers as $key => $value) { 44 | if ($key == 'Accept-Encoding' && in_array('gzip', $value)) { 45 | $request['client']['decode_content'] = true; 46 | } 47 | $request['headers'][$key] = $value; 48 | } 49 | 50 | return $handler($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/M6WebElasticsearchBundle.php: -------------------------------------------------------------------------------- 1 | 6 | {{ collector.queries|length }} 7 | {% if collector.queries|length > 0 %} 8 | in {{ '%0.2f'|format(collector.totalExecutionTime * 1000) }}ms 9 | {% endif %} 10 | {% endset %} 11 | {% set text %} 12 |
13 | Queries 14 | {{ collector.queries|length }} 15 |
16 |
17 | Query Time 18 | {{ '%0.2f'|format(collector.totalExecutionTime * 1000) }} ms 19 |
20 | {% endset %} 21 | {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with {'link': profiler_url} %} 22 | {% endblock %} 23 | 24 | {% block menu %} 25 | 26 | 27 | elasticsearch 28 | 29 | Elasticsearch 30 | 31 | {{ collector.queries|length }} 32 | {{ '%0.0f'|format(collector.totalExecutionTime * 1000) }} ms 33 | 34 | 35 | {% endblock %} 36 | 37 | {% block panel %} 38 |

Queries

39 | 103 | {% endblock %} 104 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Tests/Units/DependencyInjection/M6WebElasticsearchExtension.php: -------------------------------------------------------------------------------- 1 | [ 24 | 'no_hosts_client' => [ 25 | ], 26 | ]], 27 | ]; 28 | 29 | $parameterBag = new ParameterBag(['kernel.debug' => true]); 30 | 31 | $container = new \Symfony\Component\DependencyInjection\ContainerBuilder($parameterBag); 32 | 33 | $this->if($extension = new TestedClass()) 34 | ->exception(function () use ($extension, $configs, $container) { 35 | $extension->load($configs, $container); 36 | }) 37 | ->isInstanceOf('\Symfony\Component\Config\Definition\Exception\InvalidConfigurationException'); 38 | } 39 | 40 | /** 41 | * The loading of a configuration with an empty `hosts` entry should fail 42 | */ 43 | public function testEmptyHostsError() 44 | { 45 | $configs = [ 46 | ['clients' => [ 47 | 'empty_hosts_client' => [ 48 | 'hosts' => [], 49 | ], 50 | ]], 51 | ]; 52 | 53 | $parameterBag = new ParameterBag(['kernel.debug' => true]); 54 | 55 | $container = new \Symfony\Component\DependencyInjection\ContainerBuilder($parameterBag); 56 | 57 | $this->if($extension = new TestedClass()) 58 | ->exception(function () use ($extension, $configs, $container) { 59 | $extension->load($configs, $container); 60 | }) 61 | ->isInstanceOf('\Symfony\Component\Config\Definition\Exception\InvalidConfigurationException'); 62 | } 63 | 64 | /** 65 | * Test the loading of a single Elasticsearch Client 66 | */ 67 | public function testLoadsElasticsearchClient() 68 | { 69 | $configs = [ 70 | ['clients' => [ 71 | 'my_only_client' => [ 72 | 'hosts' => [ 73 | 'localhost:9200', 74 | 'localhost:9201', 75 | ], 76 | ], 77 | ]], 78 | ]; 79 | 80 | $parameterBag = new ParameterBag(['kernel.debug' => true]); 81 | 82 | $container = new \Symfony\Component\DependencyInjection\ContainerBuilder($parameterBag); 83 | $container->set('event_dispatcher', new \mock\Symfony\Component\EventDispatcher\EventDispatcherInterface()); 84 | 85 | $this->if($extension = new TestedClass()) 86 | ->when($extension->load($configs, $container)) 87 | ->clientIsDefinedInContainer($container, 'my_only_client', 2) 88 | ->clientIsCorrectlyInstanciated($container, 'my_only_client'); 89 | } 90 | 91 | /** 92 | * Test the loading of multiple Elasticsearch Clients 93 | */ 94 | public function testLoadMultipleElasticsearchClients() 95 | { 96 | $configs = [ 97 | ['clients' => [ 98 | 'my_first_client' => [ 99 | 'hosts' => [ 100 | 'localhost:9200', 101 | 'localhost:9201', 102 | ], 103 | ], 104 | 'my_second_client' => [ 105 | 'hosts' => [ 106 | 'myserver:9200', 107 | ], 108 | ], 109 | ]], 110 | ]; 111 | 112 | $parameterBag = new ParameterBag(['kernel.debug' => true]); 113 | 114 | $container = new \Symfony\Component\DependencyInjection\ContainerBuilder($parameterBag); 115 | $container->set('event_dispatcher', new \mock\Symfony\Component\EventDispatcher\EventDispatcherInterface()); 116 | 117 | $this->if($extension = new TestedClass()) 118 | ->when($extension->load($configs, $container)) 119 | ->clientIsDefinedInContainer($container, 'my_first_client', 2) 120 | ->clientIsCorrectlyInstanciated($container, 'my_first_client') 121 | ->clientIsDefinedInContainer($container, 'my_second_client', 1) 122 | ->clientIsCorrectlyInstanciated($container, 'my_second_client'); 123 | } 124 | 125 | /** 126 | * Test the definition of a default client 127 | */ 128 | public function testDefineDefaultElasticsearchClients() 129 | { 130 | $configs = [[ 131 | 'clients' => [ 132 | 'my_first_client' => [ 133 | 'hosts' => [ 134 | 'localhost:9200', 135 | ], 136 | ], 137 | 'my_second_client' => [ 138 | 'hosts' => [ 139 | 'myserver:9200', 140 | ], 141 | ], 142 | ], 143 | 'default_client' => 'my_second_client', 144 | ]]; 145 | 146 | $parameterBag = new ParameterBag(['kernel.debug' => true]); 147 | 148 | $container = new \Symfony\Component\DependencyInjection\ContainerBuilder($parameterBag); 149 | $container->set('event_dispatcher', new \mock\Symfony\Component\EventDispatcher\EventDispatcherInterface()); 150 | 151 | $this->if($extension = new TestedClass()) 152 | ->when($extension->load($configs, $container)) 153 | ->boolean($container->has('m6web_elasticsearch.client.default')) 154 | ->isTrue() 155 | ->and() 156 | ->clientIsCorrectlyInstanciated($container, 'default'); 157 | } 158 | 159 | /** 160 | * Check if the client is correctly defined in the container 161 | * 162 | * @param string $clientName 163 | * @param int $hostsSize 164 | * 165 | * @return M6WebElasticsearchExtension $this 166 | */ 167 | protected function clientIsDefinedInContainer(ContainerInterface $container, $clientName, $hostsSize) 168 | { 169 | $this 170 | ->boolean($container->has('m6web_elasticsearch.client.'.$clientName)) 171 | ->isTrue() 172 | ->and($arguments = $container->getDefinition('m6web_elasticsearch.client.'.$clientName)->getArguments()) 173 | ->array($arguments[0]) 174 | ->hasKey('hosts') 175 | ->array($arguments[0]['hosts']) 176 | ->hasSize($hostsSize); 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Check if the client is correctly instanciated 183 | * 184 | * @return M6WebElasticsearchExtension $this 185 | */ 186 | protected function clientIsCorrectlyInstanciated(ContainerInterface $container, string $clientName) 187 | { 188 | $this 189 | ->object($container->get('m6web_elasticsearch.client.'.$clientName)) 190 | ->isInstanceOf('\Elasticsearch\Client'); 191 | 192 | return $this; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Tests/Units/Elasticsearch/ConnectionMocker.php: -------------------------------------------------------------------------------- 1 | mockGenerator->orphanize('__construct'); 28 | $this->mockGenerator->shuntParentClassCalls(); 29 | 30 | $connectionMock = new \mock\Elasticsearch\Connections\Connection(); 31 | 32 | foreach ($callbacks as $method => $callback) { 33 | $this->calling($connectionMock)->$method = $callback; 34 | } 35 | 36 | return $connectionMock; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Tests/Units/Elasticsearch/ConnectionPool/Selector/RandomStickySelector.php: -------------------------------------------------------------------------------- 1 | if($this->newTestedInstance) 26 | 27 | ->then 28 | ->exception( 29 | function () { 30 | $this->testedInstance->select([]); 31 | } 32 | ) 33 | ->isInstanceOf(NoNodesAvailableException::class); 34 | } 35 | 36 | /** 37 | * Test the stickiness of the select method. 38 | * 39 | * @return void 40 | */ 41 | public function testSelectSticky() 42 | { 43 | $this 44 | ->given($connections = $this->getConnectionMocks(1000)) 45 | ->if($this->newTestedInstance) 46 | 47 | // Test returned class is a connection. 48 | ->then 49 | ->object($firstConnectionReturned = $this->testedInstance->select($connections)) 50 | ->isInstanceOf(Connection::class) 51 | 52 | // Every next select should be identical to the first. 53 | ->and 54 | ->object($newSelect = $this->testedInstance->select($connections)) 55 | ->isIdenticalTo($firstConnectionReturned) 56 | ->and 57 | ->object($newSelect = $this->testedInstance->select($connections)) 58 | ->isIdenticalTo($firstConnectionReturned) 59 | ->and 60 | ->object($newSelect = $this->testedInstance->select($connections)) 61 | ->isIdenticalTo($firstConnectionReturned) 62 | ->and 63 | ->object($newSelect = $this->testedInstance->select($connections)) 64 | ->isIdenticalTo($firstConnectionReturned) 65 | ->and 66 | ->object($newSelect = $this->testedInstance->select($connections)) 67 | ->isIdenticalTo($firstConnectionReturned) 68 | 69 | // First connection returned is excluded from the available connections. 70 | ->if($connections = 71 | array_filter( 72 | $connections, 73 | function ($baseConnection) use ($firstConnectionReturned) { 74 | return $baseConnection !== $firstConnectionReturned; 75 | } 76 | ) 77 | ) 78 | // Next connection returned is a new one and every call after should be the same. 79 | ->then 80 | ->object($newConnectionReturned = $this->testedInstance->select($connections)) 81 | ->isInstanceOf(Connection::class) 82 | ->isNotIdenticalTo($firstConnectionReturned) 83 | ->and 84 | ->object($newSelect = $this->testedInstance->select($connections)) 85 | ->isIdenticalTo($newConnectionReturned) 86 | ->and 87 | ->object($newSelect = $this->testedInstance->select($connections)) 88 | ->isIdenticalTo($newConnectionReturned) 89 | ->and 90 | ->object($newSelect = $this->testedInstance->select($connections)) 91 | ->isIdenticalTo($newConnectionReturned) 92 | ->and 93 | ->object($newSelect = $this->testedInstance->select($connections)) 94 | ->isIdenticalTo($newConnectionReturned) 95 | ->and 96 | ->object($newSelect = $this->testedInstance->select($connections)) 97 | ->isIdenticalTo($newConnectionReturned); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Tests/Units/Elasticsearch/ConnectionPool/StaticAliveNoPingConnectionPool.php: -------------------------------------------------------------------------------- 1 | given($connections = [$first = $this->getConnectionMockReady(), $second = $this->getConnectionMockReady()]) 28 | ->if($this->newTestedInstance($connections, $this->getSelectorMock(), $this->getConnectionFactoryMock(), [])) 29 | 30 | // Test returned class is a connection. 31 | ->then 32 | ->object($firstConnectionReturned = $this->testedInstance->nextConnection()) 33 | ->isIdenticalTo($first); 34 | } 35 | 36 | /** 37 | * Test Select 38 | */ 39 | public function testNextConnectionOneFailure() 40 | { 41 | $this 42 | ->given($connections = [$first = $this->getConnectionMockFailed(), $second = $this->getConnectionMockReady()]) 43 | ->if($this->newTestedInstance($connections, $this->getSelectorMock(), $this->getConnectionFactoryMock(), [])) 44 | 45 | // Test returned class is a connection. 46 | ->then 47 | ->object($firstConnectionReturned = $this->testedInstance->nextConnection()) 48 | ->isIdenticalTo($second); 49 | } 50 | 51 | /** 52 | * Test Select 53 | */ 54 | public function testNextConnectionFullFailure() 55 | { 56 | $this 57 | ->given($connections = [$first = $this->getConnectionMockFailed(), $second = $this->getConnectionMockFailed()]) 58 | ->if($this->newTestedInstance($connections, $this->getSelectorMock(), $this->getConnectionFactoryMock(), [])) 59 | 60 | // Test returned class is a connection. 61 | ->then 62 | ->exception( 63 | function () { 64 | $this->testedInstance->nextConnection(); 65 | } 66 | ) 67 | ->isInstanceOf(NoNodesAvailableException::class); 68 | } 69 | 70 | /** 71 | * @return Connection 72 | */ 73 | protected function getConnectionMockReady() 74 | { 75 | return $this->getConnectionMock( 76 | [ 77 | 'isAlive' => true, 78 | 'getPingFailures' => 0, 79 | 'getLastPing' => 0, 80 | ] 81 | ); 82 | } 83 | 84 | /** 85 | * @return Connection 86 | */ 87 | protected function getConnectionMockFailed() 88 | { 89 | return $this->getConnectionMock( 90 | [ 91 | 'isAlive' => false, 92 | 'getPingFailures' => time(), 93 | 'getLastPing' => time(), 94 | ] 95 | ); 96 | } 97 | 98 | /** 99 | * @return SelectorInterface 100 | */ 101 | protected function getSelectorMock() 102 | { 103 | $this->mockGenerator->orphanize('__construct'); 104 | $this->mockGenerator->shuntParentClassCalls(); 105 | 106 | $selectorMock = new \mock\Elasticsearch\ConnectionPool\Selectors\SelectorInterface(); 107 | 108 | $this->calling($selectorMock)->select = function ($connections) { 109 | return reset($connections); 110 | }; 111 | 112 | return $selectorMock; 113 | } 114 | 115 | /** 116 | * @return ConnectionFactoryInterface 117 | */ 118 | protected function getConnectionFactoryMock() 119 | { 120 | $this->mockGenerator->orphanize('__construct'); 121 | $this->mockGenerator->shuntParentClassCalls(); 122 | 123 | $selectorMock = new \mock\Elasticsearch\Connections\ConnectionFactoryInterface(); 124 | 125 | return $selectorMock; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Tests/Units/Handler/EventHandler.php: -------------------------------------------------------------------------------- 1 | getEventDispatcher(); 25 | $requestHandler = $this->getRequestHandler(); 26 | $future = new CompletedFutureArray($response); 27 | 28 | $this->calling($requestHandler)->__invoke = $future; 29 | 30 | $this 31 | ->if($handler = new TestedClass($eventDispatcher, $requestHandler)) 32 | ->variable($future = $handler($request)) 33 | ->mock($eventDispatcher)->call('dispatch')->withArguments($expectedEvent, 'm6web.elasticsearch')->once(); 34 | } 35 | 36 | /** 37 | * Test no dispatch 38 | */ 39 | public function testNoDispatch() 40 | { 41 | $request = []; 42 | $eventDispatcher = $this->getEventDispatcher(); 43 | $requestHandler = $this->getRequestHandler(); 44 | $promise = new RejectedPromise(); 45 | $future = new FutureArray($promise); 46 | 47 | $this->calling($requestHandler)->__invoke = $future; 48 | 49 | $this 50 | ->if($handler = new TestedClass($eventDispatcher, $requestHandler)) 51 | ->variable($future = $handler($request)) 52 | ->mock($eventDispatcher)->call('dispatch')->never(); 53 | } 54 | 55 | protected function testDispatchDataProvider(): array 56 | { 57 | return [ 58 | [ 59 | 'request' => ['uri' => '/_search', 'http_method' => 'GET'], 60 | 'response' => [ 61 | 'transfer_stats' => ['total_time' => 0.5], 62 | 'status' => 200, 63 | 'body' => fopen('data://text/plain,', 'r'), 64 | ], 65 | 'expectedEvent' => (new ElasticsearchEvent()) 66 | ->setUri('/_search') 67 | ->setMethod('GET') 68 | ->setStatusCode(200) 69 | ->setDuration(500) 70 | ->setTook(null), 71 | ], 72 | [ 73 | 'request' => ['uri' => '/_count', 'http_method' => 'POST'], 74 | 'response' => [ 75 | 'transfer_stats' => ['total_time' => 1], 76 | 'status' => 500, 77 | 'body' => fopen('data://text/plain,'.json_encode(['took' => 10]), 'r'), 78 | ], 79 | 'expectedEvent' => (new ElasticsearchEvent()) 80 | ->setUri('/_count') 81 | ->setMethod('POST') 82 | ->setStatusCode(500) 83 | ->setDuration(1000) 84 | ->setTook(10), 85 | ], 86 | ]; 87 | } 88 | 89 | /** 90 | * Get event dispatcher 91 | * 92 | * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface 93 | */ 94 | protected function getEventDispatcher() 95 | { 96 | $eventDispatcher = new \mock\Symfony\Component\EventDispatcher\EventDispatcherInterface(); 97 | 98 | return $eventDispatcher; 99 | } 100 | 101 | /** 102 | * Get request handler 103 | * 104 | * @return \GuzzleHttp\Ring\Client\CurlHandler 105 | */ 106 | protected function getRequestHandler() 107 | { 108 | $requestHandler = new \mock\GuzzleHttp\Ring\Client\CurlHandler(); 109 | 110 | return $requestHandler; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/M6Web/Bundle/ElasticsearchBundle/Tests/Units/Handler/HeadersHandler.php: -------------------------------------------------------------------------------- 1 | getRequestHandler(); 21 | $expectedResult = true; 22 | 23 | $this->calling($requestHandler)->__invoke = $expectedResult; 24 | 25 | $handler = new TestedClass($requestHandler); 26 | foreach ($headers as $key => $value) { 27 | $handler->setHeader($key, $value); 28 | } 29 | 30 | $this 31 | ->variable($handler($request))->isEqualTo($expectedResult) 32 | ->mock($requestHandler)->call('__invoke')->withArguments($expectedRequest)->once(); 33 | } 34 | 35 | /** 36 | * Get request handler 37 | * 38 | * @return \GuzzleHttp\Ring\Client\CurlHandler 39 | */ 40 | protected function getRequestHandler() 41 | { 42 | $requestHandler = new \mock\GuzzleHttp\Ring\Client\CurlHandler(); 43 | 44 | return $requestHandler; 45 | } 46 | 47 | protected function testInvokeDataProvider(): array 48 | { 49 | return [ 50 | [ 51 | 'request' => ['headers' => ['host' => ['localhost:9200']]], 52 | 'expectedRequest' => ['headers' => ['host' => ['localhost:9200']], 'url' => 'http://localhost:9200'], 53 | 'headers' => [], 54 | ], 55 | [ 56 | 'request' => ['headers' => ['host' => ['localhost:9200']]], 57 | 'expectedRequest' => ['headers' => ['host' => ['localhost:9200'], 'X-AddMe' => ['Hello']], 'url' => 'http://localhost:9200'], 58 | 'headers' => ['X-AddMe' => ['Hello']], 59 | ], 60 | [ 61 | 'request' => ['headers' => ['host' => ['localhost:9200']]], 62 | 'expectedRequest' => ['headers' => ['host' => ['other-host.com']], 'url' => 'http://localhost:9200'], 63 | 'headers' => ['host' => ['other-host.com']], 64 | ], 65 | [ 66 | 'request' => ['headers' => ['host' => ['localhost:9200']]], 67 | 'expectedRequest' => [ 68 | 'headers' => ['host' => ['localhost:9200'], 'Accept-Encoding' => ['gzip']], 69 | 'client' => ['decode_content' => true], 70 | 'url' => 'http://localhost:9200', 71 | ], 72 | 'headers' => ['Accept-Encoding' => ['gzip']], 73 | ], 74 | [ 75 | 'request' => ['headers' => ['host' => ['localhost:9200']]], 76 | 'expectedRequest' => [ 77 | 'headers' => ['host' => ['localhost:9200'], 'Accept-Encoding' => ['deflate']], 78 | 'url' => 'http://localhost:9200', 79 | ], 80 | 'headers' => ['Accept-Encoding' => ['deflate']], 81 | ], 82 | ]; 83 | } 84 | } 85 | --------------------------------------------------------------------------------