From 597f8045a0bf8267def731b555af9d8133b7fcfc Mon Sep 17 00:00:00 2001 From: Alessandro Galli Date: Tue, 29 Aug 2017 18:15:08 +0200 Subject: [PATCH] - added explain services - added explain to profiler - some refactoring - redefined some bundle requirements, extension version - custom mongodb install in travis ci --- .travis.yml | 8 +- README.MD | 4 +- composer.json | 2 +- docker-compose.yml | 2 + src/Capsule/Client.php | 20 ++- src/Capsule/Collection.php | 37 +++- src/Capsule/Database.php | 29 +++- src/Controller/ProfilerController.php | 77 +++++++++ src/DataCollector/MongoDbDataCollector.php | 2 +- src/DataCollector/MongoQuerySerializer.php | 2 +- .../MongoDbBundleExtension.php | 14 ++ src/Models/Query.php | 40 ++++- src/Resources/views/Collector/mongo.html.twig | 71 ++++++-- src/Services/ClientRegistry.php | 13 +- .../Explain/ExplainCommandBuilder.php | 124 ++++++++++++++ src/Services/Explain/ExplainQueryService.php | 71 ++++++++ src/Services/ExplainQueryService.php | 85 --------- src/Twig/FacileMongoDbBundleExtension.php | 11 ++ tests/Functional/AppTestCase.php | 9 +- tests/Functional/Capsule/CollectionTest.php | 41 ++--- .../Controller/ProfilerControllerTest.php | 97 +++++++++++ .../MongoDbBundleExtensionTest.php | 3 + .../Explain/ExplainQueryServiceTest.php | 42 +++++ tests/Unit/Capsule/ClientTest.php | 2 +- tests/Unit/Capsule/DatabaseTest.php | 4 +- .../Explain/ExplainCommandBuilderTest.php | 161 ++++++++++++++++++ .../Services/Loggers/Model/LogEventTest.php | 26 ++- .../Twig/FacileMongoDbBundleExtensionTest.php | 37 ++++ 28 files changed, 868 insertions(+), 166 deletions(-) create mode 100644 src/Controller/ProfilerController.php create mode 100644 src/Services/Explain/ExplainCommandBuilder.php create mode 100644 src/Services/Explain/ExplainQueryService.php delete mode 100644 src/Services/ExplainQueryService.php create mode 100644 tests/Functional/Controller/ProfilerControllerTest.php create mode 100644 tests/Functional/Services/Explain/ExplainQueryServiceTest.php create mode 100644 tests/Unit/Services/Explain/ExplainCommandBuilderTest.php diff --git a/.travis.yml b/.travis.yml index 1ae2e96..fde9a54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,6 @@ cache: directories: - $HOME/.composer/cache/files -services: - - mongodb - matrix: fast_finish: true include: @@ -50,6 +47,9 @@ before_install: - if [ "$SYMFONY" != "" ]; then composer require "symfony/symfony:${SYMFONY}" --no-update; fi; install: + - wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.0.14.tgz + - tar xzf mongodb-linux-x86_64-3.0.14.tgz + - ${PWD}/mongodb-linux-x86_64-3.0.14/bin/mongod --version - if [ "$MONGO_EXT_VERSION" != "" ]; then pecl install -f mongodb-${MONGO_EXT_VERSION}; fi; - if [ "$MONGO_EXT_VERSION" = "" ]; then echo "extension=mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; - mkdir --parents "${HOME}/bin" @@ -57,6 +57,8 @@ install: before_script: - echo "zend_extension=xdebug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - mkdir ${PWD}/mongodb-linux-x86_64-3.0.14/data + - ${PWD}/mongodb-linux-x86_64-3.0.14/bin/mongod --dbpath ${PWD}/mongodb-linux-x86_64-3.0.14/data --logpath ${PWD}/mongodb-linux-x86_64-3.0.14/mongodb.log --fork script: - composer validate diff --git a/README.MD b/README.MD index e1c73b0..d7e33c0 100644 --- a/README.MD +++ b/README.MD @@ -2,7 +2,9 @@ Bundle service integration of official [mongodb/mongo-php-library](https://github.com/mongodb/mongo-php-library) driver library, ([mongodb/mongodb](https://packagist.org/packages/mongodb/mongodb) on packagist) -[![PHP Version](https://img.shields.io/badge/php-%5E7.0-blue.svg)](https://img.shields.io/badge/php-%5E7.0-blue.svg) +[![PHP](https://img.shields.io/badge/php-%5E7.0-blue.svg)](https://img.shields.io/badge/php-%5E7.0-blue.svg) +[![MongoDB](https://img.shields.io/badge/MongoDB-%5E3.0-lightgrey.svg)](https://img.shields.io/badge/MongoDB-%5E3.0-lightgrey.svg) +[![ext-mongodb](https://img.shields.io/badge/ext_mongodb-%5E1.1.5-orange.svg)](https://img.shields.io/badge/ext_mongodb-%5E1.1.5-orange.svg) [![Latest Stable Version](https://poser.pugx.org/facile-it/mongodb-bundle/v/stable)](https://packagist.org/packages/facile-it/mongodb-bundle) [![Latest Unstable Version](https://poser.pugx.org/facile-it/mongodb-bundle/v/unstable)](https://packagist.org/packages/facile-it/mongodb-bundle) [![Total Downloads](https://poser.pugx.org/facile-it/mongodb-bundle/downloads)](https://packagist.org/packages/facile-it/mongodb-bundle) [![License](https://poser.pugx.org/facile-it/mongodb-bundle/license)](https://packagist.org/packages/facile-it/mongodb-bundle) diff --git a/composer.json b/composer.json index fc68e43..b7d8946 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { "php" : "^7.0", - "ext-mongodb": "^1.1.0", + "ext-mongodb": "^1.1.5", "mongodb/mongodb": "^1.0", "symfony/framework-bundle": "^2.8 || ^3.0" }, diff --git a/docker-compose.yml b/docker-compose.yml index bd11d14..43ecf1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: container_name: mb_php mongo: image: mongo:3.4.2 + ports: + - 27017:27017 entrypoint: - mongod - --quiet diff --git a/src/Capsule/Client.php b/src/Capsule/Client.php index eacc4f4..a1257ae 100644 --- a/src/Capsule/Client.php +++ b/src/Capsule/Client.php @@ -1,4 +1,4 @@ -eventDispatcher = $eventDispatcher; + $this->clientName = $clientName; } + /** * {@inheritdoc} */ @@ -39,7 +49,7 @@ public function selectDatabase($databaseName, array $options = []) 'typeMap' => $debug['typeMap'], ]; - return new Database($debug['manager'], $databaseName, $options, $this->eventDispatcher); + return new Database($debug['manager'], $this->clientName, $databaseName, $options, $this->eventDispatcher); } /** @@ -52,6 +62,6 @@ public function selectCollection($databaseName, $collectionName, array $options 'typeMap' => $debug['typeMap'], ]; - return new Collection($debug['manager'], $databaseName, $collectionName, $options, $this->eventDispatcher); + return new Collection($debug['manager'], $this->clientName, $databaseName, $collectionName, $options, $this->eventDispatcher); } } diff --git a/src/Capsule/Collection.php b/src/Capsule/Collection.php index 14077d3..0b7a777 100644 --- a/src/Capsule/Collection.php +++ b/src/Capsule/Collection.php @@ -1,4 +1,4 @@ -eventDispatcher = $eventDispatcher; + $this->clientName = $clientName; + $this->databaseName = $databaseName; } /** @@ -194,6 +207,8 @@ private function prepareQuery(string $method, $filters = null, $data = null, arr $query->setData($data); $query->setOptions($options); $query->setMethod($method); + $query->setClient($this->getClientName()); + $query->setDatabase($this->getDatabaseName()); $query->setCollection($this->getCollectionName()); $query->setReadPreference( $this->translateReadPreference($options['readPreference'] ?? $this->__debugInfo()['readPreference']) @@ -238,5 +253,21 @@ private function notifyQueryExecution(Query $queryLog) $this->eventDispatcher->dispatch(QueryEvent::QUERY_EXECUTED, new QueryEvent($queryLog)); } + + /** + * @return string + */ + public function getClientName(): string + { + return $this->clientName; + } + + /** + * @return string + */ + public function getDatabaseName(): string + { + return $this->databaseName; + } } diff --git a/src/Capsule/Database.php b/src/Capsule/Database.php index 682da55..44ffa5b 100644 --- a/src/Capsule/Database.php +++ b/src/Capsule/Database.php @@ -1,4 +1,4 @@ -eventDispatcher = $eventDispatcher; + $this->clientName = $clientName; + $this->databaseName = $databaseName; } /** @@ -44,7 +56,14 @@ public function selectCollection($collectionName, array $options = []) 'writeConcern' => $debug['writeConcern'], ]; - return new Collection($debug['manager'], $debug['databaseName'], $collectionName, $options, $this->eventDispatcher); + return new Collection( + $debug['manager'], + $this->clientName, + $this->databaseName, + $collectionName, + $options, + $this->eventDispatcher + ); } /** @@ -60,6 +79,6 @@ public function withOptions(array $options = []) 'writeConcern' => $debug['writeConcern'], ]; - return new self($debug['manager'], $debug['databaseName'], $options, $this->eventDispatcher); + return new self($debug['manager'], $this->clientName, $debug['databaseName'], $options, $this->eventDispatcher); } } diff --git a/src/Controller/ProfilerController.php b/src/Controller/ProfilerController.php new file mode 100644 index 0000000..b8f2467 --- /dev/null +++ b/src/Controller/ProfilerController.php @@ -0,0 +1,77 @@ +container = $container; + } + + public function explainAction($token, $queryNumber) + { + /** @var $profiler \Symfony\Component\HttpKernel\Profiler\Profiler */ + $profiler = $this->container->get('profiler'); + $profiler->disable(); + + $profile = $profiler->loadProfile($token); + $queries = $profile->getCollector('mongodb')->getQueries(); + + $query = $queries[$queryNumber]; + + $query->setFilters($this->walkAndConvertToUTCDatetime($query->getFilters())); + + $service = $this->container->get('mongo.explain_query_service'); + + try{ + $result = $service->execute($query); + } catch (\InvalidArgumentException $e) { + return new JsonResponse([ + "err" => $e->getMessage() + ]); + } + + return new JsonResponse(MongoQuerySerializer::prepareItemData($result->toArray())); + } + + /** + * @param array $data + * + * @return array + */ + private function walkAndConvertToUTCDatetime($data) + { + if (!is_array($data)) { + return $data; + } + + foreach ($data as $key => $item) { + + if (is_string($item) && preg_match('/^ISODate/', $item)) { + $time = str_replace(['ISODate("','")'], '', $item); + $dateTime = new \DateTime($time); + $item = new UTCDatetime($dateTime->getTimestamp() * 1000); + } + + $data[$key] = $this->walkAndConvertToUTCDatetime($item); + } + + return $data; + } +} \ No newline at end of file diff --git a/src/DataCollector/MongoDbDataCollector.php b/src/DataCollector/MongoDbDataCollector.php index 4d1bc34..6e5e4ae 100644 --- a/src/DataCollector/MongoDbDataCollector.php +++ b/src/DataCollector/MongoDbDataCollector.php @@ -12,7 +12,7 @@ * Class MongoDbDataCollector. * @internal */ -final class MongoDbDataCollector extends DataCollector +class MongoDbDataCollector extends DataCollector { const QUERY_KEYWORD = 'queries'; const CONNECTION_KEYWORD = 'connections'; diff --git a/src/DataCollector/MongoQuerySerializer.php b/src/DataCollector/MongoQuerySerializer.php index f563f50..38f4891 100644 --- a/src/DataCollector/MongoQuerySerializer.php +++ b/src/DataCollector/MongoQuerySerializer.php @@ -39,7 +39,7 @@ private static function prepareUnserializableData($data) * * @return mixed */ - private static function prepareItemData($item) + public static function prepareItemData($item) { if (method_exists($item, 'getArrayCopy')) { return self::prepareUnserializableData($item->getArrayCopy()); diff --git a/src/DependencyInjection/MongoDbBundleExtension.php b/src/DependencyInjection/MongoDbBundleExtension.php index 4cc01da..c00af5f 100644 --- a/src/DependencyInjection/MongoDbBundleExtension.php +++ b/src/DependencyInjection/MongoDbBundleExtension.php @@ -8,6 +8,7 @@ use Facile\MongoDbBundle\Event\QueryEvent; use Facile\MongoDbBundle\Services\ClientRegistry; use Facile\MongoDbBundle\Services\ConnectionFactory; +use Facile\MongoDbBundle\Services\Explain\ExplainQueryService; use Facile\MongoDbBundle\Services\Loggers\MongoQueryLogger; use Facile\MongoDbBundle\Twig\FacileMongoDbBundleExtension; use MongoDB\Database; @@ -47,8 +48,10 @@ public function load(array $configs, ContainerBuilder $container) $this->attachDataCollectionListenerToEventManager(); $this->defineDataCollector(); $this->attachTwigExtesion(); + $this->defineExplainQueryService(); } + return $config; } @@ -182,4 +185,15 @@ private function attachTwigExtesion() $this->containerBuilder->setDefinition('facile_mongo_db.twig_extesion', $extesion); } + + private function defineExplainQueryService() + { + $explainServiceDefinition = new Definition( + ExplainQueryService::class, + [new Reference('mongo.client_registry')] + ); + $explainServiceDefinition->setPublic(true); + + $this->containerBuilder->setDefinition('mongo.explain_query_service', $explainServiceDefinition); + } } diff --git a/src/Models/Query.php b/src/Models/Query.php index d15529a..e15f7bc 100644 --- a/src/Models/Query.php +++ b/src/Models/Query.php @@ -1,4 +1,4 @@ -start = microtime(true); + $this->client = 'undefined'; + $this->database = 'undefined'; $this->collection = 'undefined'; $this->method = 'undefined'; $this->filters = []; @@ -159,4 +165,36 @@ public function setReadPreference(string $readPreference) { $this->readPreference = $readPreference; } + + /** + * @return string + */ + public function getClient(): string + { + return $this->client; + } + + /** + * @param string $client + */ + public function setClient(string $client) + { + $this->client = $client; + } + + /** + * @return string + */ + public function getDatabase(): string + { + return $this->database; + } + + /** + * @param string $database + */ + public function setDatabase(string $database) + { + $this->database = $database; + } } diff --git a/src/Resources/views/Collector/mongo.html.twig b/src/Resources/views/Collector/mongo.html.twig index 0514cab..7f2ad75 100644 --- a/src/Resources/views/Collector/mongo.html.twig +++ b/src/Resources/views/Collector/mongo.html.twig @@ -1,4 +1,4 @@ -{% extends '@WebProfiler/Profiler/layout.html.twig' %} +{% extends app.request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %} {# collector \Facile\MongoDbBundle\DataCollector\MongoDbDataCollector #} {% block toolbar %} {% set icon %} @@ -131,6 +131,18 @@ {% endblock %} {% block panel %} + {% if 'explain' == page %} + {{ render(controller('FacileMongoDbBundle:Profiler:explain', { + token: token, + panel: 'mongodb', + queryNumber: app.request.query.get('queryNumber') + })) }} + {% else %} + {{ block('queries') }} + {% endif %} +{% endblock panel %} + +{% block queries %} {# Optional, for showing the most details. #}

Mongo DB Query Metrics

@@ -194,7 +206,7 @@ {{ q.collection|length > 12 ? q.collection|slice(0,12)~'...' : q.collection }}
  • Exceution time:
  • -
  • +
  • {{ "%.2f"|format(q.executionTime * 1000) }} ms
  • @@ -203,34 +215,40 @@ {% if q.filters | length > 0 %}
    - {{ filterLabelTranslate('Filters', q.method) }}:
    - {{ q.filters|json_encode(128) }} -
    -
    + {{ filterLabelTranslate('Filters', q.method) }}: Expand +
    + {{ q.filters|json_encode(128) }}
    {% endif %} {% if q.data | length > 0 %}
    - {{ dataLabelTranslate('Data', q.method) }}:
    - {{ q.data|json_encode(128) }} -
    -
    + {{ dataLabelTranslate('Data', q.method) }}: Expand +
    + {{ q.data|json_encode(128) }}
    {% endif %} {% if q.options | length > 0 %}
    - Options:
    + Options: + Expand +
    {{ q.options|json_encode(128) }}
    +
    + {% endif %} + {% if isQueryExplainable(q.method) %} -
    {% endif %} + {% else %} @@ -274,6 +292,33 @@ return '' + match + ''; }); } + + var doExplainQuery = function (el) { + var elId = 'facile-mongodb-explain-'+el.getAttribute('data-index')+'-expand'; + var target = document.getElementById(elId); + + Sfjs.load( + elId, + el.getAttribute('data-link'), + function (response) { + var explain = JSON.stringify(JSON.parse(response.responseText), undefined, 4); + target.innerHTML = '
    ' + facileMongoDbBundlePrettify(explain) + '
    '; + }, + function(xhr, el) { + document.getElementById(elId).innerHTML = 'An error occurred while loading the query explanation.'; + } + ); + + if (target.classList.contains('hidden')) { + el.innerHTML = 'Close explain'; + target.classList.remove('hidden'); + + return; + } + + el.innerHTML = 'Explain query'; + target.classList.add('hidden'); + }; {% endblock %} diff --git a/src/Services/ClientRegistry.php b/src/Services/ClientRegistry.php index 501917d..f35a3ec 100644 --- a/src/Services/ClientRegistry.php +++ b/src/Services/ClientRegistry.php @@ -129,7 +129,7 @@ public function getClient(string $name, string $databaseName = null): Client $conf = $this->configurations[$name]; $uri = sprintf('mongodb://%s', $conf->getHosts()); $options = array_merge(['database' => $databaseName], $conf->getOptions()); - $this->clients[$clientKey] = $this->buildClient($uri, $options, []); + $this->clients[$clientKey] = $this->buildClient($name, $uri, $options, []); $this->eventDispatcher->dispatch( ConnectionEvent::CLIENT_CREATED, @@ -141,16 +141,17 @@ public function getClient(string $name, string $databaseName = null): Client } /** - * @param $uri - * @param array $options - * @param array $driverOptions + * @param string $clientName + * @param string $uri + * @param array $options + * @param array $driverOptions * * @return Client */ - private function buildClient($uri, array $options, array $driverOptions): Client + private function buildClient(string $clientName, string $uri, array $options, array $driverOptions): Client { if ('dev' === $this->environment) { - return new BundleClient($uri, $options, $driverOptions, $this->eventDispatcher); + return new BundleClient($uri, $options, $driverOptions, $clientName, $this->eventDispatcher); } return new Client($uri, $options, $driverOptions); diff --git a/src/Services/Explain/ExplainCommandBuilder.php b/src/Services/Explain/ExplainCommandBuilder.php new file mode 100644 index 0000000..e916a1c --- /dev/null +++ b/src/Services/Explain/ExplainCommandBuilder.php @@ -0,0 +1,124 @@ +getMethod()) { + return [ + 'aggregate' => $query->getCollection(), + 'pipeline' => $query->getData(), + 'explain' => true, + ]; + } + + $args = [ + $query->getMethod() => $query->getCollection(), + ]; + + $args = self::manageCount($query, $args); + $args = self::manageDistinct($query, $args); + $args = self::manageFind($query, $args); + $args = self::manageDelete($query, $args); + + return [ + 'explain' => $args, + 'verbosity' => $verbosity, + ]; + } + + /** + * @param Query $query + * @param $args + * + * @return array + */ + private static function manageCount(Query $query, array $args): array + { + if ('count' === $query->getMethod()) { + $args += [ + 'query' => $query->getFilters(), + ]; + + foreach (['limit', 'hint', 'skip'] as $supportedOption) { + $args += (isset($query->getOptions()[$supportedOption]) ? [$supportedOption => $query->getOptions()[$supportedOption]] : []); + } + } + + return $args; + } + + /** + * @param Query $query + * @param $args + * + * @return array + */ + private static function manageDistinct(Query $query, array $args): array + { + if ('distinct' === $query->getMethod()) { + $args += [ + 'key' => $query->getData()['fieldName'], + 'query' => $query->getFilters(), + ]; + } + + return $args; + } + + /** + * @param Query $query + * @param array $args + * + * @return array + */ + private static function manageFind(Query $query, array $args): array + { + if (in_array($query->getMethod(), ['find', 'findOne', 'findOneAndUpdate', 'findOneAndDelete'])) { + $args = [ + 'find' => $query->getCollection(), + 'filter' => $query->getFilters(), + ]; + + foreach (['sort', 'projection', 'hint', 'skip', 'limit'] as $supportedOption) { + $args += (isset($query->getOptions()[$supportedOption]) ? [$supportedOption => $query->getOptions()[$supportedOption]] : []); + } + } + + return $args; + } + + /** + * @param Query $query + * @param array $args + * + * @return array + */ + private static function manageDelete(Query $query, array $args): array + { + if (in_array($query->getMethod(), ['deleteOne', 'deleteMany'])) { + return [ + 'delete' => $query->getCollection(), + 'deletes' => [ + ['q' => $query->getFilters(), 'limit' => $query->getOptions()['limit'] ?? 0,] + ] + ]; + } + + return $args; + } +} \ No newline at end of file diff --git a/src/Services/Explain/ExplainQueryService.php b/src/Services/Explain/ExplainQueryService.php new file mode 100644 index 0000000..a2de4c7 --- /dev/null +++ b/src/Services/Explain/ExplainQueryService.php @@ -0,0 +1,71 @@ +clientRegistry = $clientRegistry; + } + + /** + * Execute the operation. + * + * @param Query $query + * @param string $verbosity + * + * @return Cursor + */ + public function execute(Query $query, string $verbosity = self::VERBOSITY_ALL_PLAN_EXECUTION): Cursor + { + if (!in_array($query->getMethod(), self::$acceptedMethods)) { + throw new \InvalidArgumentException( + 'Cannot explain the method \''.$query->getMethod().'\'. Allowed methods: '. implode(', ',self::$acceptedMethods) + ); + }; + + $manager = $this->clientRegistry->getClient($query->getClient())->__debugInfo()['manager']; + + return $manager + ->executeCommand( + $query->getDatabase(), + new Command(ExplainCommandBuilder::createCommandArgs($query, $verbosity)) + ); + } +} + diff --git a/src/Services/ExplainQueryService.php b/src/Services/ExplainQueryService.php deleted file mode 100644 index b654135..0000000 --- a/src/Services/ExplainQueryService.php +++ /dev/null @@ -1,85 +0,0 @@ -clientRegistry = $clientRegistry; - } - - /** - * Execute the operation. - * - * @param string $connection - * @param Query $query - * @param string $verbosity - * @return array - * @throws \MongoDB\Driver\Exception\InvalidArgumentException - * @throws \MongoDB\Driver\Exception\WriteException - * @throws \MongoDB\Driver\Exception\WriteConcernException - * @throws \MongoDB\Driver\Exception\RuntimeException - * @throws \MongoDB\Driver\Exception\Exception - * @throws \MongoDB\Driver\Exception\DuplicateKeyException - * @throws \MongoDB\Driver\Exception\ConnectionException - * @throws \MongoDB\Driver\Exception\AuthenticationException - */ - public function execute(string $connection, Query $query, string $verbosity = self::VERBOSITY_ALL_PLAN_EXECUTION) - { - if (!in_array($query->getMethod(), self::$acceptedMethod)) { - throw new InvalidArgumentException( - 'Cannot explain the method'.$query->getMethod().'. Allowed method '.self::$acceptedMethod - ); - }; - - $manager = $this->clientRegistry->getClient('test_client')->getManager(); - - return $manager->executeCommand('collaboratori', $this->createCommand($query, $verbosity))->toArray(); - } - - /** - * Create the explain command. - * - * @return Command - * @throws \MongoDB\Driver\Exception\InvalidArgumentException - */ - private function createCommand(Query $query, string $verbosity) - { - $args = [ - $query->getMethod() => $query->getCollection(), - 'query' => $query->getFilters(), - ]; - - $cmd = [ - 'explain' => $args, - 'verbosity' => $verbosity, - ]; - - return new Command($cmd); - } -} diff --git a/src/Twig/FacileMongoDbBundleExtension.php b/src/Twig/FacileMongoDbBundleExtension.php index 03ec9bf..bd83c06 100644 --- a/src/Twig/FacileMongoDbBundleExtension.php +++ b/src/Twig/FacileMongoDbBundleExtension.php @@ -2,6 +2,8 @@ namespace Facile\MongoDbBundle\Twig; +use Facile\MongoDbBundle\Services\Explain\ExplainQueryService; + class FacileMongoDbBundleExtension extends \Twig_Extension { private $methodDataTranslationMap = [ @@ -12,11 +14,15 @@ class FacileMongoDbBundleExtension extends \Twig_Extension 'replaceOne' => 'Replacement', ]; + /** + * @return array + */ public function getFunctions() { return [ new \Twig_Simplefunction('filterLabelTranslate', array($this, 'queryFilterTranslate')), new \Twig_Simplefunction('dataLabelTranslate', array($this, 'queryDataTranslate')), + new \Twig_Simplefunction('isQueryExplainable', array($this, 'isQueryExplainable')), ]; } @@ -42,6 +48,11 @@ public function queryDataTranslate(string $label, string $methodName): string return $this->methodDataTranslationMap[$methodName] ?? $label; } + public function isQueryExplainable(string $methodName): bool + { + return in_array($methodName, ExplainQueryService::$acceptedMethods); + } + /** * Returns the name of the extension. * diff --git a/tests/Functional/AppTestCase.php b/tests/Functional/AppTestCase.php index 67fa999..e861b8f 100644 --- a/tests/Functional/AppTestCase.php +++ b/tests/Functional/AppTestCase.php @@ -17,6 +17,8 @@ class AppTestCase extends TestCase /** @var Application */ private $application; + private $env = 'test'; + /** * {@inheritdoc} */ @@ -24,7 +26,7 @@ protected function setUp() { parent::setUp(); - $kernel = new TestKernel('test', true); + $kernel = new TestKernel($this->env, true); $kernel->boot(); $this->application = new Application($kernel); } @@ -53,4 +55,9 @@ protected function getContainer(): ContainerInterface { return $this->application->getKernel()->getContainer(); } + + protected function setEnvDev() + { + $this->env = 'dev'; + } } diff --git a/tests/Functional/Capsule/CollectionTest.php b/tests/Functional/Capsule/CollectionTest.php index 8b0a4fe..15a2980 100644 --- a/tests/Functional/Capsule/CollectionTest.php +++ b/tests/Functional/Capsule/CollectionTest.php @@ -2,11 +2,8 @@ use Facile\MongoDbBundle\Capsule\Collection; use Facile\MongoDbBundle\Event\QueryEvent; -use Facile\MongoDbBundle\Models\Query; -use Facile\MongoDbBundle\Services\ExplainQueryService; use Facile\MongoDbBundle\Tests\Functional\AppTestCase; use MongoDB\Driver\Manager; -use MongoDB\Driver\Server; use Prophecy\Argument; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -27,7 +24,7 @@ public function test_construction() $manager = $this->getManager(); $ev = self::prophesize(EventDispatcherInterface::class); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); self::assertInstanceOf(\MongoDB\Collection::class, $coll); } @@ -38,7 +35,7 @@ public function test_insertOne() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->insertOne(['test' => 1]); } @@ -49,7 +46,7 @@ public function test_updateOne() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->updateOne(['filter' => 1],['$set' => ['testField' => 1]]); } @@ -60,7 +57,7 @@ public function test_count() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->count(['test' => 1]); } @@ -71,7 +68,7 @@ public function test_find() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->find([]); } @@ -82,7 +79,7 @@ public function test_findOne() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->findOne([]); } @@ -93,7 +90,7 @@ public function test_findOneAndUpdate() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->findOneAndUpdate([], ['$set' => ['country' => 'us']]); } @@ -104,7 +101,7 @@ public function test_findOneAndDelete() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->findOneAndDelete([]); } @@ -115,7 +112,7 @@ public function test_deleteOne() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->deleteOne([]); } @@ -126,7 +123,7 @@ public function test_replaceOne() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->replaceOne([], []); } @@ -137,7 +134,7 @@ public function test_aggregate() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->deleteMany([]); @@ -168,7 +165,7 @@ public function test_deleteMany() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->deleteMany([]); } @@ -179,23 +176,11 @@ public function test_distinct() $ev = self::prophesize(EventDispatcherInterface::class); $this->assertEventsDispatching($ev); - $coll = new Collection($manager, 'testdb', 'test_collection', [], $ev->reveal()); + $coll = new Collection($manager, 'test_client', 'testdb', 'test_collection', [], $ev->reveal()); $coll->distinct('field'); } - public function test_explainQuery() - { - $query = new Query(); - $query->setCollection('agenda_task'); - $query->setMethod('count'); - $query->setFilters([ - "target" => null - ]); - - $service = new ExplainQueryService($this->getContainer()->get('mongo.client_registry')); - $service->execute('mongo.connection.test_db', $query); - } /** * @param $ev */ diff --git a/tests/Functional/Controller/ProfilerControllerTest.php b/tests/Functional/Controller/ProfilerControllerTest.php new file mode 100644 index 0000000..82ab8df --- /dev/null +++ b/tests/Functional/Controller/ProfilerControllerTest.php @@ -0,0 +1,97 @@ +setEnvDev(); + parent::setUp(); + } + + public function test_explainAction() + { + $query = new Query(); + $query->setClient('test_client'); + $query->setDatabase('testFunctionaldb'); + $query->setCollection('fooCollection'); + $query->setMethod('count'); + $query->setFilters(['date' => new UTCDateTime((new \DateTime())->getTimestamp() * 1000)]); + + $collector = $this->prophesize(MongoDbDataCollector::class); + $collector->getQueries()->shouldBeCalledTimes(1)->willReturn([$query]); + + $profile = $this->prophesize(Profile::class); + $profile->getCollector('mongodb')->shouldBeCalledTimes(1)->willReturn($collector->reveal()); + + $profiler = $this->prophesize(Profiler::class); + $profiler->loadProfile('fooToken')->shouldBeCalledTimes(1)->willReturn($profile->reveal()); + $profiler->disable()->shouldBeCalledTimes(1); + + $explainService = $this->getContainer()->get('mongo.explain_query_service'); + + $container = $this->prophesize(Container::class); + $container->get('profiler')->willReturn($profiler->reveal()); + $container->get('mongo.explain_query_service')->willReturn($explainService); + + $controller = new ProfilerController(); + $controller->setContainer($container->reveal()); + + $response = $controller->explainAction('fooToken', 0); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true); + $this->assertEquals(JSON_ERROR_NONE, json_last_error()); + + $this->assertTrue(is_array($data)); + $this->assertArrayNotHasKey('err', $data); + } + + public function test_explainAction_error() + { + $query = new Query(); + $query->setMethod('fooo'); + + $collector = $this->prophesize(MongoDbDataCollector::class); + $collector->getQueries()->shouldBeCalledTimes(1)->willReturn([$query]); + + $profile = $this->prophesize(Profile::class); + $profile->getCollector('mongodb')->shouldBeCalledTimes(1)->willReturn($collector->reveal()); + + $profiler = $this->prophesize(Profiler::class); + $profiler->loadProfile('fooToken')->shouldBeCalledTimes(1)->willReturn($profile->reveal()); + $profiler->disable()->shouldBeCalledTimes(1); + + $explainService = $this->getContainer()->get('mongo.explain_query_service'); + + $container = $this->prophesize(Container::class); + $container->get('profiler')->willReturn($profiler->reveal()); + $container->get('mongo.explain_query_service')->willReturn($explainService); + + $controller = new ProfilerController(); + $controller->setContainer($container->reveal()); + + $response = $controller->explainAction('fooToken', 0); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true); + + $this->assertTrue(is_array($data)); + $this->assertArrayHasKey('err', $data); + } +} \ No newline at end of file diff --git a/tests/Functional/DependencyInjection/MongoDbBundleExtensionTest.php b/tests/Functional/DependencyInjection/MongoDbBundleExtensionTest.php index 825ac4c..65b6743 100644 --- a/tests/Functional/DependencyInjection/MongoDbBundleExtensionTest.php +++ b/tests/Functional/DependencyInjection/MongoDbBundleExtensionTest.php @@ -6,6 +6,7 @@ use Facile\MongoDbBundle\DependencyInjection\MongoDbBundleExtension; use Facile\MongoDbBundle\Event\ConnectionEvent; use Facile\MongoDbBundle\Event\QueryEvent; +use Facile\MongoDbBundle\Services\Explain\ExplainQueryService; use Facile\MongoDbBundle\Services\Loggers\MongoQueryLogger; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use MongoDB\Database; @@ -71,6 +72,8 @@ public function test_load() self::assertCount(2, $ed->getListeners()); self::assertCount(1, $ed->getListeners(QueryEvent::QUERY_EXECUTED)); self::assertCount(1, $ed->getListeners(ConnectionEvent::CLIENT_CREATED)); + + $this->assertContainerBuilderHasService('mongo.explain_query_service', ExplainQueryService::class); } public function test_load_data_collection_disabled() diff --git a/tests/Functional/Services/Explain/ExplainQueryServiceTest.php b/tests/Functional/Services/Explain/ExplainQueryServiceTest.php new file mode 100644 index 0000000..27eb069 --- /dev/null +++ b/tests/Functional/Services/Explain/ExplainQueryServiceTest.php @@ -0,0 +1,42 @@ +setEnvDev(); + parent::setUp(); + } + + public function test_execute() + { + $query = new Query(); + $query->setMethod('findOne'); + $query->setFilters(['_id' => 1]); + $query->setClient('test_client'); + $query->setDatabase('testFunctionaldb'); + + $service = $this->getContainer()->get('mongo.explain_query_service'); + $explain = $service->execute($query)->toArray(); + + $this->assertNotEmpty($explain); + } + + public function test_execute_not_available_method() + { + $query = new Query(); + $query->setMethod('fooooo'); + $query->setFilters(['_id' => 1]); + $query->setClient('test_client'); + $query->setDatabase('testFunctionaldb'); + + $service = $this->getContainer()->get('mongo.explain_query_service'); + $this->expectException(\InvalidArgumentException::class); + $service->execute($query); + } +} \ No newline at end of file diff --git a/tests/Unit/Capsule/ClientTest.php b/tests/Unit/Capsule/ClientTest.php index d84acdb..ae20bce 100644 --- a/tests/Unit/Capsule/ClientTest.php +++ b/tests/Unit/Capsule/ClientTest.php @@ -13,7 +13,7 @@ class ClientTest extends TestCase { public function test_mongodb_client_encapsulation() { - $client = new Client('mongodb://localhost:27017', [], [], $this->prophesize(EventDispatcherInterface::class)->reveal()); + $client = new Client('mongodb://localhost:27017', [], [], 'test_client', $this->prophesize(EventDispatcherInterface::class)->reveal()); self::assertInstanceOf(MongoClient::class, $client); diff --git a/tests/Unit/Capsule/DatabaseTest.php b/tests/Unit/Capsule/DatabaseTest.php index a78cf4f..327fe00 100644 --- a/tests/Unit/Capsule/DatabaseTest.php +++ b/tests/Unit/Capsule/DatabaseTest.php @@ -16,7 +16,7 @@ public function test_selectCollection() $manager = new Manager('mongodb://localhost'); $logger = self::prophesize(EventDispatcherInterface::class); - $db = new Database($manager, 'testdb', [], $logger->reveal()); + $db = new Database($manager,'client_name', 'testdb', [], $logger->reveal()); self::assertInstanceOf(\MongoDB\Database::class, $db); $coll = $db->selectCollection('test_collection'); @@ -33,7 +33,7 @@ public function test_withOptions() $manager = new Manager('mongodb://localhost'); $logger = self::prophesize(EventDispatcherInterface::class); - $db = new Database($manager, 'testdb', [], $logger->reveal()); + $db = new Database($manager, 'client_name','testdb', [], $logger->reveal()); self::assertInstanceOf(\MongoDB\Database::class, $db); $newDb = $db->withOptions(['readPreference' => new ReadPreference(ReadPreference::RP_NEAREST)]); diff --git a/tests/Unit/Services/Explain/ExplainCommandBuilderTest.php b/tests/Unit/Services/Explain/ExplainCommandBuilderTest.php new file mode 100644 index 0000000..b174f22 --- /dev/null +++ b/tests/Unit/Services/Explain/ExplainCommandBuilderTest.php @@ -0,0 +1,161 @@ +setCollection('test_collection'); + $query->setMethod('count'); + $query->setFilters(['id' => 1]); + + $args = ExplainCommandBuilder::createCommandArgs($query); + + $this->assertEquals( + [ + 'explain' => [ + 'count' => 'test_collection', + 'query' => ['id' => 1] + ], + 'verbosity' => ExplainQueryService::VERBOSITY_ALL_PLAN_EXECUTION, + ], + $args + ); + } + + public function test_distinct() + { + $query = new Query(); + $query->setCollection('test_collection'); + $query->setMethod('distinct'); + $query->setFilters(['id' => 1]); + $query->setData(['fieldName'=>'test']); + + $args = ExplainCommandBuilder::createCommandArgs($query); + + $this->assertEquals( + [ + 'explain' => [ + 'distinct' => $query->getCollection(), + 'key' => 'test', + 'query' => $query->getFilters(), + ], + 'verbosity' => ExplainQueryService::VERBOSITY_ALL_PLAN_EXECUTION, + ], + $args + ); + } + + public function test_aggregate() + { + $query = new Query(); + $query->setCollection('test_collection'); + $query->setMethod('aggregate'); + $query->setFilters(['id' => 1]); + + $args = ExplainCommandBuilder::createCommandArgs($query); + + $this->assertEquals( + [ + 'aggregate' => $query->getCollection(), + 'pipeline' => $query->getData(), + 'explain' => true, + ], + $args + ); + } + + /** + * @dataProvider findsProvider + * + * @param string $method + * @param bool $projection + */ + public function test_finds(string $method, bool $projection = false) + { + $query = new Query(); + $query->setCollection('test_collection'); + $query->setMethod($method); + $query->setFilters(['id' => 1]); + if($projection) { + $query->setOptions([ + 'projection' => '_id', + ]); + } + + + $args = ExplainCommandBuilder::createCommandArgs($query); + + $expected = [ + 'explain' => [ + 'find' => 'test_collection', + 'filter' => ['id' => 1] + ], + 'verbosity' => ExplainQueryService::VERBOSITY_ALL_PLAN_EXECUTION, + ]; + + if($projection) { + $expected['explain']['projection'] = '_id'; + } + + $this->assertEquals($expected, $args); + } + + public function findsProvider() + { + return [ + ['find', true], + ['findOne'], + ['findOneAndUpdate'], + ['findOneAndDelete'], + ]; + } + + /** + * @dataProvider deletedsProvider + * + * @param string $method + * @param int $limit + */ + public function test_deletes(string $method, int $limit = 0) + { + $query = new Query(); + $query->setCollection('test_collection'); + $query->setMethod($method); + $query->setFilters(['id' => 1]); + if($limit) { + $query->setOptions([ + 'limit' => $limit, + ]); + } + + $args = ExplainCommandBuilder::createCommandArgs($query); + + $expected = [ + 'explain' => [ + 'delete' => $query->getCollection(), + 'deletes' => [ + ['q' => $query->getFilters(), 'limit' => $limit,] + ] + ], + 'verbosity' => ExplainQueryService::VERBOSITY_ALL_PLAN_EXECUTION, + ]; + + $this->assertEquals($expected, $args); + } + + public function deletedsProvider() + { + return [ + ['deleteOne', 0], + ['deleteMany', 4], + ]; + } +} diff --git a/tests/Unit/Services/Loggers/Model/LogEventTest.php b/tests/Unit/Services/Loggers/Model/LogEventTest.php index 90eead5..7b92321 100644 --- a/tests/Unit/Services/Loggers/Model/LogEventTest.php +++ b/tests/Unit/Services/Loggers/Model/LogEventTest.php @@ -11,15 +11,23 @@ class LogEventTest extends TestCase { public function test_construction() { - $event = new Query(); - $event->setCollection('test_collection'); - $event->setMethod('find'); - $event->setExecutionTime(1000); - $event->setData(['_id'=>'1000000000001']); + $query = new Query(); + $query->setCollection('test_collection'); + $query->setMethod('find'); + $query->setData(['_id'=>'1000000000001']); + $query->setExecutionTime(1000); + $query->setClient('test_client'); + $query->setDatabase('test_db'); + $query->setReadPreference('secondaryPreferred'); + + $this->assertNotNull($query->getStart()); + $this->assertEquals('test_collection', $query->getCollection()); + $this->assertEquals('find',$query->getMethod()); + $this->assertEquals(1000,$query->getExecutionTime()); + $this->assertEquals(['_id'=>'1000000000001'],$query->getData()); + $this->assertEquals('test_client',$query->getClient()); + $this->assertEquals('test_db',$query->getDatabase()); + $this->assertEquals('secondaryPreferred',$query->getReadPreference()); - self::assertEquals('test_collection', $event->getCollection()); - self::assertEquals('find',$event->getMethod()); - self::assertEquals(1000,$event->getExecutionTime()); - self::assertEquals(['_id'=>'1000000000001'],$event->getData()); } } diff --git a/tests/Unit/Twig/FacileMongoDbBundleExtensionTest.php b/tests/Unit/Twig/FacileMongoDbBundleExtensionTest.php index f20134c..385850a 100644 --- a/tests/Unit/Twig/FacileMongoDbBundleExtensionTest.php +++ b/tests/Unit/Twig/FacileMongoDbBundleExtensionTest.php @@ -4,6 +4,7 @@ use Facile\MongoDbBundle\Twig\FacileMongoDbBundleExtension; use PHPUnit\Framework\TestCase; +use Twig_Function; class FacileMongoDbBundleExtensionTest extends TestCase { @@ -28,6 +29,24 @@ public function test_queryFilterTranslate() $this->assertEquals('label2', $ext->queryFilterTranslate('label2', '')); } + /** + * @dataProvider explainMethodsProvider + * + * @param string $methodname + * @param bool $expected + */ + public function test_isQueryExplainable(string $methodname, bool $expected) + { + $ext = new FacileMongoDbBundleExtension(); + $this->assertEquals($expected, $ext->isQueryExplainable( $methodname)); + } + + public function test_get_name() + { + $ext = new FacileMongoDbBundleExtension(); + $this->assertEquals('facile_mongo_db_extesion', $ext->getName()); + } + public function labelMethodProvider(): array { return [ @@ -39,4 +58,22 @@ public function labelMethodProvider(): array ['filters', 'find', 'filters'], ]; } + + public function explainMethodsProvider(): array + { + return [ + ['aggregate', true], + ['count', true], + ['distinct', true], + ['find', true], + ['findOne', true], + ['findOneAndUpdate', true], + ['findOneAndDelete', true], + ['deleteOne', true], + ['deleteMany', true], + ['updateOne', false], + ['insertOne', false], + ['replaceOne', false], + ]; + } } \ No newline at end of file