Skip to content

Commit 2f396b8

Browse files
authored
Merge pull request #12483 from greg0ire/no-mutations
Add alternative to mutation
2 parents bc217c0 + 44e844b commit 2f396b8

7 files changed

Lines changed: 159 additions & 17 deletions

File tree

docs/en/reference/events.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,15 @@ instance and class metadata.
999999
}
10001000
}
10011001
1002+
.. note::
1003+
1004+
Starting in ORM 3.6, the ``GenerateSchemaTableEventArgs`` class
1005+
provides ``setSchema()`` and ``setClassTable()`` methods to replace
1006+
the schema or table objects. These methods require ``doctrine/dbal``
1007+
^4.5 or higher, which provides the Schema Editor API. If called with
1008+
an earlier DBAL version, a ``BadMethodCallException`` will be
1009+
thrown.
1010+
10021011
postGenerateSchema
10031012
~~~~~~~~~~~~~~~~~~
10041013

@@ -1025,6 +1034,14 @@ and the EntityManager.
10251034
}
10261035
}
10271036
1037+
.. note::
1038+
1039+
Starting in ORM 3.6, the ``GenerateSchemaEventArgs`` class provides
1040+
a ``setSchema()`` method to replace the schema object. This method
1041+
requires ``doctrine/dbal`` ^4.5 or higher, which provides the Schema
1042+
Editor API. If called with an earlier DBAL version, a
1043+
``BadMethodCallException`` will be thrown.
1044+
10281045
.. _PrePersistEventArgs: https://github.com/doctrine/orm/blob/HEAD/src/Event/PrePersistEventArgs.php
10291046
.. _PreRemoveEventArgs: https://github.com/doctrine/orm/blob/HEAD/src/Event/PreRemoveEventArgs.php
10301047
.. _PreUpdateEventArgs: https://github.com/doctrine/orm/blob/HEAD/src/Event/PreUpdateEventArgs.php

phpstan-baseline.neon

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,18 +3201,6 @@ parameters:
32013201
count: 1
32023202
path: src/Tools/SchemaTool.php
32033203

3204-
-
3205-
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addIndex\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
3206-
identifier: argument.type
3207-
count: 1
3208-
path: src/Tools/SchemaTool.php
3209-
3210-
-
3211-
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addUniqueIndex\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
3212-
identifier: argument.type
3213-
count: 1
3214-
path: src/Tools/SchemaTool.php
3215-
32163204
-
32173205
message: '#^Parameter \#2 \$columns of class Doctrine\\DBAL\\Schema\\Index constructor expects non\-empty\-list\<string\>, list\<string\> given\.$#'
32183206
identifier: argument.type

phpstan-dbal3.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ parameters:
4242
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:getObjectName\(\)\.$~'
4343
path: src/Mapping/Driver/DatabaseDriver.php
4444

45+
-
46+
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:getObjectName\(\)\.$~'
47+
path: src/Tools/SchemaTool.php
48+
4549
-
4650
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint::get.*\.$~'
4751
identifier: method.notFound

src/Tools/Event/GenerateSchemaEventArgs.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
namespace Doctrine\ORM\Tools\Event;
66

7+
use BadMethodCallException;
78
use Doctrine\Common\EventArgs;
89
use Doctrine\DBAL\Schema\Schema;
910
use Doctrine\ORM\EntityManagerInterface;
1011

12+
use function method_exists;
13+
1114
/**
1215
* Event Args used for the Events::postGenerateSchema event.
1316
*
@@ -17,7 +20,7 @@ class GenerateSchemaEventArgs extends EventArgs
1720
{
1821
public function __construct(
1922
private readonly EntityManagerInterface $em,
20-
private readonly Schema $schema,
23+
private Schema $schema,
2124
) {
2225
}
2326

@@ -30,4 +33,17 @@ public function getSchema(): Schema
3033
{
3134
return $this->schema;
3235
}
36+
37+
public function setSchema(Schema $schema): void
38+
{
39+
// @phpstan-ignore function.impossibleType (Checking for unreleased Schema::edit() API)
40+
if (! method_exists(Schema::class, 'edit')) {
41+
throw new BadMethodCallException(
42+
'The setSchema() method requires the DBAL Schema::edit() API which is not available in the current DBAL version. '
43+
. 'This feature requires doctrine/dbal ^4.5 or higher.',
44+
);
45+
}
46+
47+
$this->schema = $schema;
48+
}
3349
}

src/Tools/Event/GenerateSchemaTableEventArgs.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,27 @@
44

55
namespace Doctrine\ORM\Tools\Event;
66

7+
use BadMethodCallException;
78
use Doctrine\Common\EventArgs;
89
use Doctrine\DBAL\Schema\Schema;
910
use Doctrine\DBAL\Schema\Table;
1011
use Doctrine\ORM\Mapping\ClassMetadata;
1112

13+
use function method_exists;
14+
1215
/**
1316
* Event Args used for the Events::postGenerateSchemaTable event.
1417
*
1518
* @link www.doctrine-project.com
1619
*/
1720
class GenerateSchemaTableEventArgs extends EventArgs
1821
{
22+
private bool $classTableWasMutated = false;
23+
1924
public function __construct(
2025
private readonly ClassMetadata $classMetadata,
21-
private readonly Schema $schema,
22-
private readonly Table $classTable,
26+
private Schema $schema,
27+
private Table $classTable,
2328
) {
2429
}
2530

@@ -37,4 +42,36 @@ public function getClassTable(): Table
3742
{
3843
return $this->classTable;
3944
}
45+
46+
public function setSchema(Schema $schema): void
47+
{
48+
// @phpstan-ignore function.impossibleType (Checking for unreleased Schema::edit() API)
49+
if (! method_exists(Schema::class, 'edit')) {
50+
throw new BadMethodCallException(
51+
'The setSchema() method requires the DBAL Schema::edit() API which is not available in the current DBAL version. '
52+
. 'This feature requires doctrine/dbal ^4.5 or higher.',
53+
);
54+
}
55+
56+
$this->schema = $schema;
57+
}
58+
59+
public function setClassTable(Table $classTable): void
60+
{
61+
// @phpstan-ignore function.impossibleType (Checking for unreleased Schema::edit() API)
62+
if (! method_exists(Schema::class, 'edit')) {
63+
throw new BadMethodCallException(
64+
'The setClassTable() method requires the DBAL Schema::edit() API which is not available in the current DBAL version. '
65+
. 'This feature requires doctrine/dbal ^4.5 or higher.',
66+
);
67+
}
68+
69+
$this->classTable = $classTable;
70+
$this->classTableWasMutated = true;
71+
}
72+
73+
public function classTableWasMutated(): bool
74+
{
75+
return $this->classTableWasMutated;
76+
}
4077
}

src/Tools/SchemaTool.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,18 +390,40 @@ public function getSchemaFromMetadata(array $classes): Schema
390390
}
391391

392392
if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
393+
$tableEventArgs = new GenerateSchemaTableEventArgs($class, $schema, $table);
393394
$eventManager->dispatchEvent(
394395
ToolEvents::postGenerateSchemaTable,
395-
new GenerateSchemaTableEventArgs($class, $schema, $table),
396+
$tableEventArgs,
396397
);
398+
399+
// Always retrieve the schema (listener may have mutated it)
400+
$schema = $tableEventArgs->getSchema();
401+
402+
// If the listener mutated the table, we need to replace it in the schema
403+
if ($tableEventArgs->classTableWasMutated()) {
404+
$originalTableName = $table->getObjectName();
405+
$newTable = $tableEventArgs->getClassTable();
406+
407+
// @phpstan-ignore method.notFound (Using unreleased Schema::edit() API)
408+
$schemaEditor = $schema->edit();
409+
$schemaEditor->dropTable($originalTableName);
410+
$schemaEditor->addTable($newTable);
411+
412+
$schema = $schemaEditor->create();
413+
$table = $newTable;
414+
}
397415
}
398416
}
399417

400418
if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
419+
$schemaEventArgs = new GenerateSchemaEventArgs($this->em, $schema);
401420
$eventManager->dispatchEvent(
402421
ToolEvents::postGenerateSchema,
403-
new GenerateSchemaEventArgs($this->em, $schema),
422+
$schemaEventArgs,
404423
);
424+
425+
// Always retrieve the schema (listener may have mutated it)
426+
$schema = $schemaEventArgs->getSchema();
405427
}
406428

407429
return $schema;

tests/Tests/ORM/Tools/SchemaToolTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
1313
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
1414
use Doctrine\DBAL\Schema\PrimaryKeyConstraintEditor;
15+
use Doctrine\DBAL\Schema\Schema;
1516
use Doctrine\DBAL\Schema\Table as DbalTable;
1617
use Doctrine\DBAL\Types\EnumType;
1718
use Doctrine\DBAL\Types\Types;
@@ -52,6 +53,7 @@
5253
use Doctrine\Tests\Models\NullDefault\NullDefaultColumn;
5354
use Doctrine\Tests\OrmTestCase;
5455
use PHPUnit\Framework\Attributes\Group;
56+
use PHPUnit\Framework\Attributes\RequiresMethod;
5557

5658
use function array_map;
5759
use function class_exists;
@@ -503,6 +505,62 @@ public function testQuotedIdentifiers(): void
503505
self::assertSame('quoted-id', $pkColumn->getIdentifier()->getValue());
504506
}
505507

508+
#[RequiresMethod(Schema::class, 'edit')]
509+
public function testOverwritingSchemaInListener(): void
510+
{
511+
$em = $this->getTestEntityManager();
512+
513+
$em->getEventManager()->addEventListener(
514+
[ToolEvents::postGenerateSchemaTable, ToolEvents::postGenerateSchema],
515+
new class () {
516+
public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $eventArgs): void
517+
{
518+
$eventArgs->setSchema(new Schema());
519+
}
520+
521+
public function postGenerateSchema(GenerateSchemaEventArgs $eventArgs): void
522+
{
523+
$eventArgs->setSchema(new Schema());
524+
}
525+
},
526+
);
527+
$schemaTool = new SchemaTool($em);
528+
529+
$schema = $schemaTool->getSchemaFromMetadata([$em->getClassMetadata(CmsAddress::class)]);
530+
531+
self::assertFalse(
532+
$schema->hasTable('cms_address'),
533+
'The original schema should have been overwritten by the listener, so cms_address table should not exist.',
534+
);
535+
}
536+
537+
#[RequiresMethod(Schema::class, 'edit')]
538+
public function testOverwritingTableInListener(): void
539+
{
540+
$em = $this->getTestEntityManager();
541+
542+
$em->getEventManager()->addEventListener(
543+
[ToolEvents::postGenerateSchemaTable],
544+
new class () {
545+
public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $eventArgs): void
546+
{
547+
$eventArgs->setClassTable(new DbalTable('just_address'));
548+
}
549+
},
550+
);
551+
$schemaTool = new SchemaTool($em);
552+
553+
$schema = $schemaTool->getSchemaFromMetadata([$em->getClassMetadata(CmsAddress::class)]);
554+
self::assertTrue(
555+
$schema->hasTable('just_address'),
556+
'The original table should have been overwritten by the listener, so just_address table should exist.',
557+
);
558+
self::assertFalse(
559+
$schema->hasTable('cms_address'),
560+
'The original table should have been overwritten by the listener, so cms_address table should not exist.',
561+
);
562+
}
563+
506564
/** @return string[] */
507565
private static function getIndexedColumns(DbalIndex $index): array
508566
{

0 commit comments

Comments
 (0)