diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c33e8914..9bdb6a03 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -10,6 +10,8 @@ jobs:
tests:
name: Tests (PHP ${{ matrix.php }}, ext-mongodb ${{ matrix.mongo-ext }}, MongoDB ${{ matrix.mongo-img }}, Symfony ${{ matrix.symfony }})
runs-on: ubuntu-latest
+ env:
+ SYMFONY_REQUIRE: ${{ matrix.symfony }}
services:
mongo:
image: mongo:${{ matrix.mongo-img }}
@@ -38,9 +40,10 @@ jobs:
- php: 8.2
mongo-ext: 1.15.0
mongo-img: 6.0
+ symfony: "^6.4"
- php: 8.3
- mongo-ext: 1.16.0
- mongo-img: 6.0
+ mongo-ext: 1.19.0
+ mongo-img: 7.0
steps:
- name: Checkout
@@ -50,12 +53,10 @@ jobs:
with:
php-version: ${{ matrix.php }}
extensions: mongodb-${{ matrix.mongo-ext }}
+ tools: flex
- name: Allow unstable dependencies
run: composer config minimum-stability dev
if: matrix.symfony == 'dev-master'
- - name: Restrict Symfony version
- run: composer require "symfony/symfony:${{ matrix.symfony }}" --no-update
- if: matrix.symfony
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Await a bit for Mongo to spin up...
diff --git a/composer.json b/composer.json
index 28285608..aeb9b1cf 100644
--- a/composer.json
+++ b/composer.json
@@ -25,18 +25,22 @@
"symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0 || ^7.0"
},
"require-dev": {
- "matthiasnoback/symfony-dependency-injection-test": "^4",
+ "matthiasnoback/symfony-dependency-injection-test": "^4 || ^5",
"symfony/web-profiler-bundle": "^4.4 || ^5.0 || ^6.0 || ^7.0",
"symfony/console": "^4.4 || ^5.0 || ^6.0 || ^7.0",
"phpunit/phpunit": "^9.6.13 || ^10.5.27",
+ "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/dom-crawler": "^4.4 || ^5.0 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^7.0",
+ "symfony/routing": "^4.4 || ^5.0 || ^6.0 || ^7.0",
"facile-it/facile-coding-standard": "1.2.0",
"phpstan/phpstan": "1.11.7",
"phpstan/extension-installer": "1.4.1",
"jangregor/phpstan-prophecy": "1.0.2",
"phpspec/prophecy": "^1.17",
"rector/rector": "^1.0.3",
- "phpspec/prophecy-phpunit": "^2.0"
+ "phpspec/prophecy-phpunit": "^2.0",
+ "symfony/monolog-bundle": "*"
},
"minimum-stability": "stable",
"suggest": {
diff --git a/src/Controller/ProfilerController.php b/src/Controller/ProfilerController.php
index b5a7b025..57125582 100644
--- a/src/Controller/ProfilerController.php
+++ b/src/Controller/ProfilerController.php
@@ -6,49 +6,45 @@
use Facile\MongoDbBundle\DataCollector\MongoDbDataCollector;
use Facile\MongoDbBundle\DataCollector\MongoQuerySerializer;
+use Facile\MongoDbBundle\Services\Explain\ExplainQueryService;
use MongoDB\BSON\UTCDateTime;
-use Symfony\Component\DependencyInjection\Container;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Profiler\Profiler;
-class ProfilerController implements ContainerAwareInterface
+class ProfilerController
{
- private ?ContainerInterface $container = null;
+ private ExplainQueryService $explain;
- /**
- * Sets the container.
- *
- * @param ContainerInterface|null $container A ContainerInterface instance or null
- */
- public function setContainer(ContainerInterface $container = null): void
+ private ?Profiler $profiler;
+
+ public function __construct(ExplainQueryService $explain, ?Profiler $profiler)
{
- $this->container = $container;
+ $this->explain = $explain;
+ $this->profiler = $profiler;
}
- /**
- * @throws \Exception
- */
public function explainAction(string $token, $queryNumber): JsonResponse
{
- /** @var Profiler $profiler */
- $profiler = $this->container->get('profiler');
- $profiler->disable();
+ $this->profiler->disable();
+
+ $profile = $this->profiler->loadProfile($token);
+ if (! $profile) {
+ throw new \RuntimeException('No profile found');
+ }
- $profile = $profiler->loadProfile($token);
- /** @var MongoDbDataCollector $dataCollector */
$dataCollector = $profile->getCollector('mongodb');
+ if (! $dataCollector instanceof MongoDbDataCollector) {
+ throw new \RuntimeException('MongoDb data collector not found');
+ }
+
$queries = $dataCollector->getQueries();
$query = $queries[$queryNumber];
$query->setFilters($this->walkAndConvertToUTCDatetime($query->getFilters()));
- $service = $this->container->get('mongo.explain_query_service');
-
try {
- $result = $service->execute($query);
+ $result = $this->explain->execute($query);
} catch (\InvalidArgumentException $e) {
return new JsonResponse([
'err' => $e->getMessage(),
diff --git a/src/Resources/config/profiler.xml b/src/Resources/config/profiler.xml
index a77cff53..404773b2 100644
--- a/src/Resources/config/profiler.xml
+++ b/src/Resources/config/profiler.xml
@@ -42,5 +42,12 @@
+
+
+
+
+
+
+
diff --git a/tests/Functional/Controller/ProfilerControllerTest.php b/tests/Functional/Controller/ProfilerControllerTest.php
index 22be035a..f3d486ec 100644
--- a/tests/Functional/Controller/ProfilerControllerTest.php
+++ b/tests/Functional/Controller/ProfilerControllerTest.php
@@ -4,99 +4,88 @@
namespace Facile\MongoDbBundle\Tests\Functional\Controller;
+use Facile\MongoDbBundle\Tests\Functional\TestApp\TestKernelWithProfiler;
use Prophecy\PhpUnit\ProphecyTrait;
-use Facile\MongoDbBundle\Controller\ProfilerController;
-use Facile\MongoDbBundle\DataCollector\MongoDbDataCollector;
-use Facile\MongoDbBundle\Models\Query;
-use Facile\MongoDbBundle\Tests\Functional\AppTestCase;
-use MongoDB\BSON\UTCDateTime;
-use Symfony\Component\DependencyInjection\Container;
-use Symfony\Component\HttpFoundation\JsonResponse;
-use Symfony\Component\HttpKernel\Profiler\Profile;
-use Symfony\Component\HttpKernel\Profiler\Profiler;
-
-class ProfilerControllerTest extends AppTestCase
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+use Symfony\Component\DomCrawler\Crawler;
+
+class ProfilerControllerTest extends WebTestCase
{
use ProphecyTrait;
+ protected static function getKernelClass(): string
+ {
+ return TestKernelWithProfiler::class;
+ }
+
protected function setUp(): void
{
- $this->setEnvDev();
+ $this->cleanUpDir(__DIR__ . '/../../../var/cache');
parent::setUp();
}
public function test_explainAction(): void
{
- $query = new Query();
- $query->setClient('test_client');
- $query->setDatabase('testFunctionaldb');
- $query->setCollection('fooCollection');
- $query->setMethod('count');
- $query->setFilters(['date' => new UTCDateTime((new \DateTime())->getTimestamp() * 1_000)]);
-
- $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);
+ $client = self::createClient();
+
+ $client->request('GET', '/trigger_query');
+ $this->assertResponseIsSuccessful();
+ $crawler = $client->request('GET', '/_profiler/latest?panel=mongodb');
+
+ $this->assertResponseIsSuccessful();
+ $this->assertHeadersArePresent($crawler, 'http://localhost/trigger_query');
+ $explainTable = $crawler->filterXPath('//table[2]');
+ $this->assertCrawlerTextContainsString('insertOne', $explainTable);
+ $this->assertCrawlerTextContainsString('test_collection', $explainTable);
+ $this->assertCrawlerTextContainsString('{ "foo": "bar" }', $explainTable);
}
public function test_explainAction_error(): void
{
- $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);
+ $client = self::createClient();
- $controller = new ProfilerController();
- $controller->setContainer($container->reveal());
+ $client->request('GET', '/noop');
+ $this->assertResponseIsSuccessful();
+ $crawler = $client->request('GET', '/_profiler/latest?panel=mongodb');
+ $this->assertResponseIsSuccessful();
- $response = $controller->explainAction('fooToken', 0);
+ $this->assertHeadersArePresent($crawler, 'http://localhost/noop');
+ $explainTable = $crawler->filterXPath('//table[2]');
+ $this->assertCrawlerTextContainsString('No queries', $explainTable);
+ }
- $this->assertInstanceOf(JsonResponse::class, $response);
- $this->assertEquals(200, $response->getStatusCode());
+ private function cleanUpDir(string $dir): bool
+ {
+ if (! file_exists($dir)) {
+ return false;
+ }
+
+ $it = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS);
+ $files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
+
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ $this->cleanUpDir($file->getRealPath());
+ } else {
+ unlink($file->getRealPath());
+ }
+ }
+
+ return rmdir($dir);
+ }
- $data = json_decode($response->getContent(), true);
+ private function assertHeadersArePresent(Crawler $crawler, string $expectedTitle): void
+ {
+ $this->assertCrawlerTextContainsString($expectedTitle, $crawler->filterXPath('//h2[1]'));
+ $this->assertCrawlerTextContainsString('Mongo DB Query Metrics', $crawler->filterXPath('//h2[2]'));
+ $this->assertCrawlerTextContainsString('Connections list', $crawler->filterXPath('//h2[3]'));
+ $this->assertCrawlerTextContainsString('Queries Detail', $crawler->filterXPath('//h2[4]'));
+ $this->assertCrawlerTextContainsString('test_client.testFunctionaldb', $crawler->filterXPath('//table[1]'));
+ }
- $this->assertTrue(is_array($data));
- $this->assertArrayHasKey('err', $data);
+ private function assertCrawlerTextContainsString(string $needle, Crawler $explainTable): void
+ {
+ // silence 4.4 deprecation about whitespace normalization
+ $this->assertStringContainsString($needle, $explainTable->text('', true));
}
}
diff --git a/tests/Functional/TestApp/MainController.php b/tests/Functional/TestApp/MainController.php
new file mode 100644
index 00000000..c1d80245
--- /dev/null
+++ b/tests/Functional/TestApp/MainController.php
@@ -0,0 +1,31 @@
+database = $database;
+ }
+
+ public function noop(): Response
+ {
+ return new Response('Hello there');
+ }
+
+ public function triggerQuery(): Response
+ {
+ $this->database->selectCollection('test_collection')
+ ->insertOne(['foo' => 'bar']);
+
+ return new Response('Hello there');
+ }
+}
diff --git a/tests/Functional/TestApp/TestKernel.php b/tests/Functional/TestApp/TestKernel.php
index 1e46cf59..50127735 100644
--- a/tests/Functional/TestApp/TestKernel.php
+++ b/tests/Functional/TestApp/TestKernel.php
@@ -6,6 +6,7 @@
use Facile\MongoDbBundle\FacileMongoDbBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
+use Symfony\Bundle\MonologBundle\MonologBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\HttpKernel\Kernel;
@@ -21,6 +22,7 @@ public function registerBundles(): array
{
return [
new FrameworkBundle(),
+ new MonologBundle(),
new FacileMongoDbBundle(),
];
}
@@ -30,22 +32,18 @@ public function registerBundles(): array
*/
public function registerContainerConfiguration(LoaderInterface $loader): void
{
- $suffix = '';
- $version = '';
+ $loader->load(__DIR__ . '/config.yaml');
if ('docker' === getenv('TEST_ENV')) {
- $suffix = '_docker';
+ $loader->load(__DIR__ . '/docker.yaml');
}
if (version_compare(Kernel::VERSION, '6.1.0') >= 0) {
- $version = '_61';
+ $loader->load(__DIR__ . '/deprecations_6.1.yml');
}
if (version_compare(Kernel::VERSION, '6.4.0') >= 0) {
- $version = '_64';
+ $loader->load(__DIR__ . '/deprecations_6.4.yml');
}
-
- $configFile = sprintf('/config_test%s%s.yml', $version, $suffix);
- $loader->load(__DIR__ . $configFile);
}
}
diff --git a/tests/Functional/TestApp/TestKernelWithProfiler.php b/tests/Functional/TestApp/TestKernelWithProfiler.php
new file mode 100644
index 00000000..2597ae27
--- /dev/null
+++ b/tests/Functional/TestApp/TestKernelWithProfiler.php
@@ -0,0 +1,52 @@
+load(__DIR__ . '/services.yaml');
+ $loader->load(__DIR__ . '/config_profiler.yml');
+
+ if (version_compare(Kernel::VERSION, '5.1.0') >= 0) {
+ $loader->load(__DIR__ . '/deprecations_5.1.yml');
+ }
+ }
+
+ protected function build(ContainerBuilder $container): void
+ {
+ $container->setParameter('routing_config_dir', __DIR__);
+
+ parent::build($container);
+ }
+
+ protected function configureRoutes(RouteCollectionBuilder $routes)
+ {
+ // noop - 4.4 backward compat
+ }
+
+ protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
+ {
+ // noop - 4.4 backward compat
+ }
+}
diff --git a/tests/Functional/TestApp/config_test.yml b/tests/Functional/TestApp/config.yaml
similarity index 78%
rename from tests/Functional/TestApp/config_test.yml
rename to tests/Functional/TestApp/config.yaml
index b50be7ce..3d36584c 100644
--- a/tests/Functional/TestApp/config_test.yml
+++ b/tests/Functional/TestApp/config.yaml
@@ -1,3 +1,6 @@
+parameters:
+ mongo_host: localhost
+
framework:
secret: "Four can keep a secret, if three of them are dead."
@@ -8,7 +11,7 @@ mongo_db_bundle:
password: rootPass
authSource: admin
hosts:
- - { host: localhost, port: 27017 }
+ - { host: '%mongo_host%', port: 27017 }
connections:
test_db:
diff --git a/tests/Functional/TestApp/config_profiler.yml b/tests/Functional/TestApp/config_profiler.yml
new file mode 100644
index 00000000..d3a5e6ee
--- /dev/null
+++ b/tests/Functional/TestApp/config_profiler.yml
@@ -0,0 +1,8 @@
+framework:
+ router: { resource: "%routing_config_dir%/routing.yaml" }
+ test: true
+ profiler: true
+
+twig:
+ exception_controller: ~
+ strict_variables: "%kernel.debug%"
diff --git a/tests/Functional/TestApp/config_test_61.yml b/tests/Functional/TestApp/config_test_61.yml
deleted file mode 100644
index bcbfa1d3..00000000
--- a/tests/Functional/TestApp/config_test_61.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-framework:
- secret: "Four can keep a secret, if three of them are dead."
- http_method_override: true
-
-mongo_db_bundle:
- clients:
- test_client:
- username: root
- password: rootPass
- authSource: admin
- hosts:
- - { host: localhost, port: 27017 }
-
- connections:
- test_db:
- client_name: test_client
- database_name: testFunctionaldb
diff --git a/tests/Functional/TestApp/config_test_61_docker.yml b/tests/Functional/TestApp/config_test_61_docker.yml
deleted file mode 100644
index b1dddaaa..00000000
--- a/tests/Functional/TestApp/config_test_61_docker.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-framework:
- secret: "Four can keep a secret, if three of them are dead."
- http_method_override: true
-
-mongo_db_bundle:
- clients:
- test_client:
- username: root
- password: rootPass
- authSource: admin
- hosts:
- - { host: mongo, port: 27017 }
-
- connections:
- test_db:
- client_name: test_client
- database_name: testFunctionaldb
diff --git a/tests/Functional/TestApp/config_test_64_docker.yml b/tests/Functional/TestApp/config_test_64_docker.yml
deleted file mode 100644
index a20c3234..00000000
--- a/tests/Functional/TestApp/config_test_64_docker.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-imports:
- - { resource: ./config_test_61_docker.yml }
-
-framework:
- handle_all_throwables: true
- php_errors: true
diff --git a/tests/Functional/TestApp/config_test_docker.yml b/tests/Functional/TestApp/config_test_docker.yml
deleted file mode 100644
index 245349a6..00000000
--- a/tests/Functional/TestApp/config_test_docker.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-framework:
- secret: "Four can keep a secret, if three of them are dead."
-
-mongo_db_bundle:
- clients:
- test_client:
- username: root
- password: rootPass
- authSource: admin
- hosts:
- - { host: mongo, port: 27017 }
-
- connections:
- test_db:
- client_name: test_client
- database_name: testFunctionaldb
diff --git a/tests/Functional/TestApp/deprecations_5.1.yml b/tests/Functional/TestApp/deprecations_5.1.yml
new file mode 100644
index 00000000..5ed9bbbf
--- /dev/null
+++ b/tests/Functional/TestApp/deprecations_5.1.yml
@@ -0,0 +1,3 @@
+framework:
+ router:
+ utf8: true
diff --git a/tests/Functional/TestApp/deprecations_6.1.yml b/tests/Functional/TestApp/deprecations_6.1.yml
new file mode 100644
index 00000000..072634e7
--- /dev/null
+++ b/tests/Functional/TestApp/deprecations_6.1.yml
@@ -0,0 +1,2 @@
+framework:
+ http_method_override: true
diff --git a/tests/Functional/TestApp/config_test_64.yml b/tests/Functional/TestApp/deprecations_6.4.yml
similarity index 58%
rename from tests/Functional/TestApp/config_test_64.yml
rename to tests/Functional/TestApp/deprecations_6.4.yml
index 4856dc5b..109d2e1a 100644
--- a/tests/Functional/TestApp/config_test_64.yml
+++ b/tests/Functional/TestApp/deprecations_6.4.yml
@@ -1,6 +1,3 @@
-imports:
- - { resource: ./config_test_61.yml }
-
framework:
handle_all_throwables: true
php_errors:
diff --git a/tests/Functional/TestApp/docker.yaml b/tests/Functional/TestApp/docker.yaml
new file mode 100644
index 00000000..04e005ed
--- /dev/null
+++ b/tests/Functional/TestApp/docker.yaml
@@ -0,0 +1,2 @@
+parameters:
+ mongo_host: mongo
diff --git a/tests/Functional/TestApp/routing.yaml b/tests/Functional/TestApp/routing.yaml
new file mode 100644
index 00000000..fe3b3ccc
--- /dev/null
+++ b/tests/Functional/TestApp/routing.yaml
@@ -0,0 +1,17 @@
+noop:
+ path: /noop
+ defaults: { _controller: '\Facile\MongoDbBundle\Tests\Functional\TestApp\MainController::noop' }
+
+trigger_query:
+ path: /trigger_query
+ defaults: { _controller: '\Facile\MongoDbBundle\Tests\Functional\TestApp\MainController::triggerQuery' }
+
+## profiler
+
+web_profiler_wdt:
+ resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
+ prefix: /_wdt
+
+web_profiler_profiler:
+ resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
+ prefix: /_profiler
diff --git a/tests/Functional/TestApp/services.yaml b/tests/Functional/TestApp/services.yaml
new file mode 100644
index 00000000..eeaf35b9
--- /dev/null
+++ b/tests/Functional/TestApp/services.yaml
@@ -0,0 +1,6 @@
+services:
+ Facile\MongoDbBundle\Tests\Functional\TestApp\MainController:
+ arguments:
+ $database: '@mongo.connection.test_db'
+ tags:
+ - controller.service_arguments