Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Profiler integration #83

Merged
merged 26 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4c1a518
refactor: Remove profiler integration
exaby73 Aug 28, 2024
5e6fdc5
refactor: Bring back EventHandler and fix CS
exaby73 Aug 28, 2024
f84e9dd
remove: Unit test config
exaby73 Aug 28, 2024
c704332
revert: Remove profiling
exaby73 Aug 28, 2024
ca80e25
test: Add failing test for profiler
exaby73 Aug 28, 2024
f9e2217
ci: Run CI on all pull requests
exaby73 Aug 28, 2024
e8eaedb
chore: Psalm and CS fixes
exaby73 Aug 28, 2024
c6e85ce
fix: Symfony routing and twig versions
exaby73 Aug 28, 2024
8dd1d66
fix: Add invoke to TestController
exaby73 Aug 28, 2024
36334aa
fix: Change route type to attribute
exaby73 Aug 28, 2024
9186fd4
refactor: Move to invoke routing
exaby73 Aug 28, 2024
5dda8b9
wip: Profiler
exaby73 Aug 28, 2024
5e5f531
working proof of concept
transistive Aug 28, 2024
e24a8b7
feat: Add profiler integration
exaby73 Sep 12, 2024
85cca34
chore: CS and Psalm fixes
exaby73 Sep 12, 2024
ecf6c76
feat: Upgrade static analysis PHP version to 8.3
exaby73 Sep 12, 2024
ae22ba9
fix: Add recursive toArray, and add stopwatch events
exaby73 Sep 15, 2024
c7b2f50
fix: nits
exaby73 Sep 15, 2024
deeed45
feat: Add scheme for future scope
exaby73 Sep 16, 2024
1578284
fix: Remove ProfileListener from services.php
exaby73 Sep 16, 2024
211b121
Merge remote-tracking branch 'origin/master' into feat/profiling
exaby73 Sep 16, 2024
06da73c
fix: Remove EventHandler from services.php
exaby73 Sep 16, 2024
5298dce
chore: Run fix-cs
exaby73 Sep 16, 2024
d2ce89f
chore: Run fix-cs
exaby73 Sep 16, 2024
03d771b
chore: Fix Psalm warnings
exaby73 Sep 16, 2024
cf78648
force correct environment to prepare the cache before running psalm
transistive Sep 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ on:
branches:
- master
pull_request:
branches:
- master

jobs:
php-cs-fixer:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ on:
branches:
- master
pull_request:
branches:
- master

jobs:
build:
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ RUN apt-get update \
&& docker-php-ext-enable xdebug \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

WORKDIR /opt/project

RUN echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.mode=debug,develop" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini

WORKDIR /opt/project

CMD ["php", "-S", "0.0.0.0:80", "-t", "/opt/project/tests/App"]
2 changes: 1 addition & 1 deletion bin/console.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

require __DIR__ . '/../vendor/autoload.php';

$console = new Application(new TestKernel('test', true));
$console = new Application(new TestKernel($_ENV['APP_ENV'] ?? 'dev', true));

$console->run();
14 changes: 9 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,22 @@
"symfony/config": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.30",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.0",
"matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0",
"phpunit/phpunit": "^9.5",
"psalm/plugin-phpunit": "^0.18",
"psalm/plugin-symfony": "^5.0",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/routing": "^5.4 || ^6.0 || ^7.0",
"symfony/stopwatch": "^6.4",
"symfony/test-pack": "^1.1",
"symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0",
"vimeo/psalm": "^5.15.0",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.0",
"friendsofphp/php-cs-fixer": "^3.30",
"psalm/plugin-phpunit": "^0.18"
"vimeo/psalm": "^5.15.0"
},
"autoload": {
"psr-4": {
Expand All @@ -49,7 +53,7 @@
}
},
"scripts": {
"psalm": "php bin/console.php cache:warmup && vendor/bin/psalm --show-info=true",
"psalm": "APP_ENV=dev php bin/console.php cache:warmup && vendor/bin/psalm --show-info=true",
"fix-cs": "vendor/bin/php-cs-fixer fix",
"check-cs": "vendor/bin/php-cs-fixer fix --dry-run",
"ci-symfony-install-version": "./.github/scripts/setup-symfony-env.bash"
Expand Down
9 changes: 4 additions & 5 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Laudis\Neo4j\Contracts\SessionInterface;
use Laudis\Neo4j\Contracts\TransactionInterface;
use Neo4j\Neo4jBundle\ClientFactory;
use Neo4j\Neo4jBundle\EventHandler;
use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener;
use Neo4j\Neo4jBundle\SymfonyClient;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

Expand All @@ -16,10 +16,6 @@
return static function (ContainerConfigurator $configurator) {
$services = $configurator->services();

$services->set('neo4j.event_handler', EventHandler::class)
->autowire()
->autoconfigure();

$services->set('neo4j.client_factory', ClientFactory::class)
->args([
service('neo4j.event_handler'),
Expand Down Expand Up @@ -47,4 +43,7 @@
$services->alias(DriverInterface::class, 'neo4j.driver');
$services->alias(SessionInterface::class, 'neo4j.session');
$services->alias(TransactionInterface::class, 'neo4j.transaction');

$services->set('neo4j.subscriber', Neo4jProfileListener::class)
->tag('kernel.event_subscriber');
};
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ services:
- NEO4J_PORT=7687
- NEO4J_USER=neo4j
- NEO4J_PASSWORD=testtest
- XDEBUG_CONFIG="client_host=host.docker.internal log=/tmp/xdebug.log"
working_dir: /opt/project
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- neo4j-symfony

Expand Down
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</projectFiles>
<plugins>
<pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin">
<containerXml>var/cache/test/Neo4j_Neo4jBundle_Tests_App_TestKernelTestDebugContainer.xml</containerXml>
<containerXml>var/cache/dev/Neo4j_Neo4jBundle_Tests_App_TestKernelDevDebugContainer.xml</containerXml>
</pluginClass>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
Expand Down
8 changes: 4 additions & 4 deletions src/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ public function create(): SymfonyClient
/** @var ClientBuilder<SummarizedResult<CypherMap>> $builder */
$builder = ClientBuilder::create();

if ($this->driverConfig) {
if (null !== $this->driverConfig) {
$builder = $builder->withDefaultDriverConfiguration($this->makeDriverConfig());
}

if ($this->sessionConfiguration) {
if (null !== $this->sessionConfiguration) {
$builder = $builder->withDefaultSessionConfiguration($this->makeSessionConfig());
}

if ($this->transactionConfiguration) {
if (null !== $this->transactionConfiguration) {
$builder = $builder->withDefaultTransactionConfiguration($this->makeTransactionConfig());
}

Expand All @@ -77,7 +77,7 @@ public function create(): SymfonyClient
);
}

if ($this->defaultDriver) {
if (null !== $this->defaultDriver) {
$builder = $builder->withDefaultDriver($this->defaultDriver);
}

Expand Down
148 changes: 148 additions & 0 deletions src/Collector/Neo4jDataCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

declare(strict_types=1);

namespace Neo4j\Neo4jBundle\Collector;

use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener;
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @var array{
* successful_statements_count: int,
* failed_statements_count: int,
* statements: array<array-key, array<string, mixed>> | list<array{
* statement: mixed,
* exception: mixed,
* alias: string|null
* }>,
* } $data
*/
final class Neo4jDataCollector extends AbstractDataCollector
{
public function __construct(
private readonly Neo4jProfileListener $subscriber,
) {
}

public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$t = $this;
$profiledSummaries = $this->subscriber->getProfiledSummaries();
$successfulStatements = [];
foreach ($profiledSummaries as $summary) {
$statement = ['status' => 'success'];
foreach ($summary as $key => $value) {
if (!is_array($value) && !is_object($value)) {
$statement[$key] = $value;
continue;
}

$statement[$key] = $t->recursiveToArray($value);
}
$successfulStatements[] = $statement;
}

$failedStatements = array_map(
static fn (array $x) => [
'status' => 'failure',
'time' => $x['time'],
'timestamp' => $x['timestamp'],
'result' => [
'statement' => $x['statement']->toArray(),
],
'exception' => [
'code' => $x['exception']->getErrors()[0]->getCode(),
'message' => $x['exception']->getErrors()[0]->getMessage(),
'classification' => $x['exception']->getErrors()[0]->getClassification(),
'category' => $x['exception']->getErrors()[0]->getCategory(),
'title' => $x['exception']->getErrors()[0]->getTitle(),
],
'alias' => $x['alias'],
],
$this->subscriber->getProfiledFailures()
);

$this->data['successful_statements_count'] = count($successfulStatements);
$this->data['failed_statements_count'] = count($failedStatements);
$mergedArray = array_merge($successfulStatements, $failedStatements);
uasort(
$mergedArray,
static fn (array $a, array $b) => $a['start_time'] <=> $b['timestamp']
);
$this->data['statements'] = $mergedArray;
}

public function reset(): void
{
parent::reset();
$this->subscriber->reset();
}

public function getName(): string
{
return 'neo4j';
}

/** @api */
public function getStatements(): array
{
return $this->data['statements'];
}

public function getSuccessfulStatements(): array
{
return array_filter(
$this->data['statements'],
static fn (array $x) => 'success' === $x['status']
);
}

public function getFailedStatements(): array
{
return array_filter(
$this->data['statements'],
static fn (array $x) => 'failure' === $x['status']
);
}

/** @api */
public function getFailedStatementsCount(): array
{
return $this->data['failed_statements_count'];
}

/** @api */
public function getSuccessfulStatementsCount(): array
{
return $this->data['successful_statements_count'];
}

public function getQueryCount(): int
{
return count($this->data['statements']);
}

public static function getTemplate(): ?string
{
return '@Neo4j/web_profiler.html.twig';
}

private function recursiveToArray(mixed $obj): mixed
{
if (is_array($obj)) {
return array_map(
fn (mixed $x): mixed => $this->recursiveToArray($x),
$obj
);
}

if (is_object($obj) && method_exists($obj, 'toArray')) {
return $obj->toArray();
}

return $obj;
}
}
51 changes: 45 additions & 6 deletions src/DependencyInjection/Neo4jExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

namespace Neo4j\Neo4jBundle\DependencyInjection;

use Neo4j\Neo4jBundle\Collector\Neo4jDataCollector;
use Neo4j\Neo4jBundle\EventHandler;
use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -27,28 +31,63 @@ public function load(array $configs, ContainerBuilder $container): ContainerBuil
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));
$loader->load('services.php');

$defaultAlias = $mergedConfig['default_driver'] ?? $mergedConfig['drivers'][0]['alias'] ?? 'default';

$container->setDefinition('neo4j.event_handler', new Definition(EventHandler::class))
->setAutowired(true)
->addTag('neo4j.event_handler')
->setArgument(1, $defaultAlias);

$container->getDefinition('neo4j.client_factory')
->setArgument(1, $mergedConfig['default_driver_config'] ?? null)
->setArgument(2, $mergedConfig['default_session_config'] ?? null)
->setArgument(3, $mergedConfig['default_transaction_config'] ?? null)
->setArgument(4, $mergedConfig['drivers'] ?? [])
->setArgument(5, $mergedConfig['default_driver'] ?? null)
->setArgument(6, new Reference(ClientInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
->setArgument(7, new Reference(StreamFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
->setArgument(8, new Reference(RequestFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
->setAbstract(false)
;
->setArgument(
7,
new Reference(StreamFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)
)
->setArgument(
8,
new Reference(RequestFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)
)
->setAbstract(false);

$container->getDefinition('neo4j.driver')
->setArgument(0, $mergedConfig['drivers']['alias'] ?? 'default');
->setArgument(0, $defaultAlias);

$enabledProfiles = [];
foreach ($mergedConfig['drivers'] as $driver) {
if (true === $driver['profiling'] || (null === $driver['profiling'] && $container->getParameter('kernel.debug'))) {
if (true === $driver['profiling'] || (null === $driver['profiling'] && $container->getParameter(
'kernel.debug'
))) {
$enabledProfiles[] = $driver['alias'];
}
}

if (0 !== count($enabledProfiles)) {
$container->setDefinition(
'neo4j.data_collector',
(new Definition(Neo4jDataCollector::class))
->setAutowired(true)
->addTag('data_collector', [
'id' => 'neo4j',
'priority' => 500,
])
);

$container->setAlias(Neo4jProfileListener::class, 'neo4j.subscriber');

$container->setDefinition(
'neo4j.subscriber',
(new Definition(Neo4jProfileListener::class))
->setArgument(0, $enabledProfiles)
->addTag('kernel.event_subscriber')
);
}

return $container;
}

Expand Down
Loading
Loading