Skip to content

Commit d76a65a

Browse files
committed
Add alternative to mutation
The existing schema event API forces user to resort on mutation, which is deprecated. Let's offer an alternative API.
1 parent 471b129 commit d76a65a

7 files changed

Lines changed: 164 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
@@ -3207,18 +3207,6 @@ parameters:
32073207
count: 1
32083208
path: src/Tools/SchemaTool.php
32093209

3210-
-
3211-
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addIndex\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
3212-
identifier: argument.type
3213-
count: 1
3214-
path: src/Tools/SchemaTool.php
3215-
3216-
-
3217-
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addUniqueIndex\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
3218-
identifier: argument.type
3219-
count: 1
3220-
path: src/Tools/SchemaTool.php
3221-
32223210
-
32233211
message: '#^Parameter \#2 \$columns of class Doctrine\\DBAL\\Schema\\Index constructor expects non\-empty\-list\<string\>, list\<string\> given\.$#'
32243212
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: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,18 +390,45 @@ public function getSchemaFromMetadata(array $classes): Schema
390390
}
391391

392392
if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
393+
$originalTableName = $table->getObjectName();
394+
$tableEventArgs = new GenerateSchemaTableEventArgs($class, $schema, $table);
393395
$eventManager->dispatchEvent(
394396
ToolEvents::postGenerateSchemaTable,
395-
new GenerateSchemaTableEventArgs($class, $schema, $table),
397+
$tableEventArgs,
396398
);
399+
400+
// Always retrieve the schema (listener may have mutated it)
401+
$schema = $tableEventArgs->getSchema();
402+
403+
// If the listener mutated the table, we need to replace it in the schema
404+
if ($tableEventArgs->classTableWasMutated()) {
405+
$newTable = $tableEventArgs->getClassTable();
406+
407+
// Use SchemaEditor to replace the table
408+
// @phpstan-ignore method.notFound (Using unreleased Schema::edit() API)
409+
$schemaEditor = $schema->edit();
410+
411+
// Drop the old table
412+
$schemaEditor->dropTable($originalTableName);
413+
414+
// Add the new table
415+
$schemaEditor->addTable($newTable);
416+
417+
$schema = $schemaEditor->create();
418+
$table = $newTable;
419+
}
397420
}
398421
}
399422

400423
if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
424+
$schemaEventArgs = new GenerateSchemaEventArgs($this->em, $schema);
401425
$eventManager->dispatchEvent(
402426
ToolEvents::postGenerateSchema,
403-
new GenerateSchemaEventArgs($this->em, $schema),
427+
$schemaEventArgs,
404428
);
429+
430+
// Always retrieve the schema (listener may have mutated it)
431+
$schema = $schemaEventArgs->getSchema();
405432
}
406433

407434
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)