Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@
"require-dev": {
"doctrine/coding-standard": "^14",
"doctrine/orm": "^3.4.4",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan": "^2.1.13",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpstan/phpstan-symfony": "^2.0",
"phpstan/phpstan-symfony": "^2.0.9",
"phpunit/phpunit": "^12.3.10",
"psr/log": "^3.0",
"symfony/doctrine-messenger": "^6.4 || ^7.0 || ^8.0",
"symfony/expression-language": "^6.4 || ^7.0 || ^8.0",
"symfony/http-kernel": "^6.4 || ^7.0 || ^8.0",
"symfony/messenger": "^6.4 || ^7.0 || ^8.0",
"symfony/property-info": "^6.4 || ^7.0 || ^8.0",
"symfony/security-bundle": "^6.4 || ^7.0 || ^8.0",
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
includes:
- vendor/phpstan/phpstan-symfony/extension.neon
parameters:
level: 7
level: 8
reportUnmatchedIgnoredErrors: true
paths:
- config
Expand Down
6 changes: 5 additions & 1 deletion src/Controller/ProfilerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ public function explainAction(string $token, string $connectionName, int $query)
{
$this->profiler->disable();

$profile = $this->profiler->loadProfile($token);
$profile = $this->profiler->loadProfile($token);
if ($profile === null) {
return new Response('Profile not found.', 404);
}

$collector = $profile->getCollector('db');

assert($collector instanceof DoctrineDataCollector);
Expand Down
16 changes: 8 additions & 8 deletions src/DataCollector/DoctrineDataCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,18 @@ class DoctrineDataCollector extends BaseCollector

private int|null $managedEntityCount = null;

/**
* @var mixed[][]|null
* @phpstan-var ?GroupedQueriesType
*/
/** @var GroupedQueriesType|null */
private array|null $groupedQueries = null;

public function __construct(
private readonly ManagerRegistry $registry,
private readonly bool $shouldValidateSchema = true,
DebugDataHolder|null $debugDataHolder = null,
) {
if ($debugDataHolder === null) {
$debugDataHolder = new DebugDataHolder();
}

parent::__construct($registry, $debugDataHolder);
}

Expand Down Expand Up @@ -307,10 +308,9 @@ public function getGroupedQueries(): array
$this->groupedQueries[$connection] = $connectionGroupedQueries;
}

foreach ($this->groupedQueries as $connection => $queries) {
foreach ($queries as $i => $query) {
$this->groupedQueries[$connection][$i]['executionPercent'] =
$this->executionTimePercentage($query['executionMS'], $totalExecutionMS);
foreach ($this->groupedQueries as &$queries) {
foreach ($queries as &$query) {
$query['executionPercent'] = $this->executionTimePercentage($query['executionMS'], $totalExecutionMS);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/Compiler/MiddlewaresPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public function process(ContainerBuilder $container): void
);
$middlewareRefs[$id] = [new Reference($childId), ++$i];

if (! is_subclass_of($abstractDef->getClass(), ConnectionNameAwareInterface::class)) {
$class = $abstractDef->getClass();
if ($class === null || ! is_subclass_of($class, ConnectionNameAwareInterface::class)) {
continue;
}

Expand Down
10 changes: 1 addition & 9 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ private function addDbalSection(ArrayNodeDefinition $node): void
// Key that should not be rewritten to the connection config
$excludedKeys = ['default_connection' => true, 'driver_schemes' => true, 'driver_scheme' => true, 'types' => true, 'type' => true];

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$node
->children()
->arrayNode('dbal')
Expand Down Expand Up @@ -167,7 +166,6 @@ private function getDbalConnectionsNode(): ArrayNodeDefinition

$this->configureDbalDriverNode($connectionNode);

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$connectionNode
->fixXmlConfig('option')
->fixXmlConfig('mapping_type')
Expand Down Expand Up @@ -214,7 +212,6 @@ private function getDbalConnectionsNode(): ArrayNodeDefinition
->scalarNode('result_cache')->end()
->end();

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$replicaNode = $connectionNode
->children()
->arrayNode('replicas')
Expand All @@ -232,7 +229,6 @@ private function getDbalConnectionsNode(): ArrayNodeDefinition
*/
private function configureDbalDriverNode(ArrayNodeDefinition $node): void
{
/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$node
->validate()
->always(static function (array $values) {
Expand Down Expand Up @@ -288,7 +284,7 @@ private function configureDbalDriverNode(ArrayNodeDefinition $node): void
->end()
->scalarNode('default_dbname')
->info(
'Override the default database (postgres) to connect to for PostgreSQL connexion.',
'Override the default database (postgres) to connect to for PostgreSQL connection.',
)
->end()
->scalarNode('sslmode')
Expand Down Expand Up @@ -370,7 +366,6 @@ private function addOrmSection(ArrayNodeDefinition $node): void
'controller_resolver' => true,
];

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$node
->children()
->arrayNode('orm')
Expand Down Expand Up @@ -518,7 +513,6 @@ private function getOrmEntityListenersNode(): NodeDefinition
return ['entities' => $entities];
};

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$node
->beforeNormalization()
// Yaml normalization
Expand Down Expand Up @@ -564,7 +558,6 @@ private function getOrmEntityManagersNode(): ArrayNodeDefinition
$treeBuilder = new TreeBuilder('entity_managers');
$node = $treeBuilder->getRootNode();

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$node
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
Expand Down Expand Up @@ -739,7 +732,6 @@ private function getOrmCacheDriverNode(string $name): ArrayNodeDefinition
$treeBuilder = new TreeBuilder($name);
$node = $treeBuilder->getRootNode();

/** @phpstan-ignore class.notFound (Phpstan Symfony extension does not know yet how to deal with these) */
$node
->beforeNormalization()
->ifString()
Expand Down
8 changes: 5 additions & 3 deletions src/DependencyInjection/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,11 @@ private function loadMappingInformation(array $objectManager, ContainerBuilder $
throw new InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled.', $mappingName));
}

$mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container, $bundleMetadata['path']);
if (! $mappingConfig) {
continue;
if ($bundleMetadata !== null) {
$mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container, $bundleMetadata['path']);
if (! $mappingConfig) {
continue;
}
}
} elseif (! $mappingConfig['type']) {
$mappingConfig['type'] = 'attribute';
Expand Down
4 changes: 4 additions & 0 deletions src/DoctrineBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public function process(ContainerBuilder $container): void

public function shutdown(): void
{
if ($this->container === null) {
return;
}

// Clear all entity managers to clear references to entities for GC
if ($this->container->hasParameter('doctrine.entity_managers')) {
foreach ($this->container->getParameter('doctrine.entity_managers') as $id) {
Expand Down
18 changes: 17 additions & 1 deletion src/Twig/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\SqlFormatter\HtmlHighlighter;
use Doctrine\SqlFormatter\NullHighlighter;
use Doctrine\SqlFormatter\SqlFormatter;
use RuntimeException;
use Stringable;
use Symfony\Component\VarDumper\Cloner\Data;
use Twig\Extension\AbstractExtension;
Expand All @@ -24,11 +25,15 @@
use function is_array;
use function is_bool;
use function is_string;
use function preg_last_error;
use function preg_match;
use function preg_replace_callback;
use function sprintf;
use function strtoupper;
use function substr;

use const PREG_NO_ERROR;

/**
* This class contains the needed functions in order to do the query highlighting
*
Expand Down Expand Up @@ -116,7 +121,7 @@ public function replaceQueryParameters(string $query, array|Data $parameters): s

$i = 0;

return preg_replace_callback(
$result = preg_replace_callback(
'/(?<!\?)\?(?!\?)|(?<!:)(:[a-z0-9_]+)/i',
static function (array $matches) use ($parameters, &$i): string {
$key = substr($matches[0], 1);
Expand All @@ -133,6 +138,17 @@ static function (array $matches) use ($parameters, &$i): string {
},
$query,
);

$pregError = preg_last_error();
if ($pregError !== PREG_NO_ERROR) {
throw new RuntimeException(sprintf('Failed to replace query parameters: PCRE error %d', $pregError));
}

if ($result === null) {
throw new RuntimeException('Failed to replace query parameters: unexpected null result');
}

return $result;
}

public function prettifySql(string $sql): string
Expand Down
2 changes: 1 addition & 1 deletion tests/Command/CreateDatabaseDoctrineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private function getMockContainer(string $connectionName, array|null $params = n

$mockContainer = $this->createStub(Container::class);

$mockContainer->method('get')->with('doctrine')->willReturn($mockDoctrine);
$mockContainer->method('get')->willReturnMap([['doctrine', $mockDoctrine]]);

return $mockContainer;
}
Expand Down
5 changes: 2 additions & 3 deletions tests/Command/DropDatabaseDoctrineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public static function provideIncompatibleDriverOptions(): Generator
}

/**
* @param list<mixed> $params Connection parameters
* @param array{url?: string, path?: string, driver: string} $params Connection parameters
* @psalm-param Params $params
*
* @return Stub&Container
Expand All @@ -156,8 +156,7 @@ private function getMockContainer(string $connectionName, array $params): Stub
$mockContainer = $this->createStub(Container::class);

$mockContainer->method('get')
->with('doctrine')
->willReturn($mockDoctrine);
->willReturnMap([['doctrine', $mockDoctrine]]);

return $mockContainer;
}
Expand Down
65 changes: 62 additions & 3 deletions tests/DataCollector/DoctrineDataCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\Bundle\DoctrineBundle\Tests\DataCollector;

use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
Expand All @@ -21,6 +22,21 @@

use function interface_exists;

/**
* @phpstan-type GroupedQueryItemType = array{
* executionMS: float,
* explainable: bool,
* sql: string,
* params: ?array<array-key, mixed>,
* runnable: bool,
* types: ?array<array-key, Type|int|string|null>,
* count: int,
* index: int,
* executionPercent?: float
* }
* @phpstan-type GroupedQueriesType = array<string, array<int, GroupedQueryItemType>>
*/

class DoctrineDataCollectorTest extends TestCase
{
public const string FIRST_ENTITY = 'TestBundle\Test\Entity\Test1';
Expand Down Expand Up @@ -139,16 +155,59 @@ public function testGetGroupedQueries(): void
$this->assertSame(1, $groupedQueries['default'][1]['count']);
}

public function testGetGroupedQueriesExecutionPercent(): void
{
$debugDataHolder = $this->createStub(DebugDataHolder::class);

$queries = [
'default' => [
[
'sql' => 'SELECT * FROM foo',
'params' => [],
'types' => null,
'executionMS' => 100.0,
],
[
'sql' => 'SELECT * FROM bar',
'params' => [],
'types' => null,
'executionMS' => 200.0,
],
],
];

$debugDataHolder->method('getData')
->willReturnCallback(static function () use (&$queries) {
return $queries;
});

$collector = $this->createCollector([], true, $debugDataHolder);
$collector->collect(new Request(), new Response());

$groupedQueries = $collector->getGroupedQueries();

$this->assertCount(2, $groupedQueries['default']);

$firstItem = $groupedQueries['default'][0];
$this->assertEqualsWithDelta(66.667, $firstItem['executionPercent'] ?? null, 0.001);
$this->assertSame('SELECT * FROM bar', $firstItem['sql']);
$secondItem = $groupedQueries['default'][1];
$this->assertEqualsWithDelta(33.333, $secondItem['executionPercent'] ?? null, 0.001);
$this->assertSame('SELECT * FROM foo', $secondItem['sql']);
}

/**
* @param class-string $entityFQCN
*
* @return ClassMetadata<object>
*/
private function createEntityMetadata(string $entityFQCN): ClassMetadata
{
$metadata = new ClassMetadata($entityFQCN);
$metadata->name = $entityFQCN;
$metadata->reflClass = new ReflectionClass('stdClass');
$metadata = new ClassMetadata($entityFQCN);
$metadata->name = $entityFQCN;
/** @var ReflectionClass<object> $stdClassReflection */
$stdClassReflection = new ReflectionClass('stdClass');
$metadata->reflClass = $stdClassReflection;

return $metadata;
}
Expand Down
Loading
Loading