Skip to content

Commit b9d4149

Browse files
committed
feature #36655 Automatically provide Messenger Doctrine schema to "diff" (weaverryan)
This PR was squashed before being merged into the 5.1-dev branch. Discussion ---------- Automatically provide Messenger Doctrine schema to "diff" | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Alternative to #36629 | License | MIT | Doc PR | TODO - WILL be needed This follows this conversation: #36629 (comment) - it automatically adds SQL to Doctrine's migration/diff system when features are added the require a database table: The new feature works for: ### A) Messenger Doctrine transport **FULL support** Works perfectly: configure a doctrine transport and run `make:migration` **Note**: There is no current way to disable this. So if you have `auto_setup` ON and you run `make:migration` before trying Messenger, it will generate the table SQL. Adding a flag to disable it might be very complicated, because we need to know (in DoctrineBundle, at compile time) whether or not this feature is enabled/disabled so that we can decide *not* to add `messenger_messages` to the `schema_filter`. ### B) `PdoAdapter` from Cache **FULL support** Works perfectly: configure a doctrine transport and run `make:migration` ### C) `PdoStore` from Lock **PARTIAL support** I added `PdoStore::configureSchema()` but did NOT add a listener. While `PdoStore` *does* accept a DBAL `Connection`, I don't think it's possible via the `framework.lock` config to create a `PdoStore` that is passed a `Connection`. In other words: if we added a listener that called `PdoStore::configureSchema` if the user configured a `pdo` lock, that service will *never* have a `Connection` object... so it's kind of worthless. **NEED**: A proper way to inject a DBAL `Connection` into `PdoStore` via `framework.lock` config. ### D) `PdoSessionHandler` **NO support** This class doesn't accept a DBAL `Connection` object. And so, we can't reliably create a listener to add the schema because (if there are multiple connections) we wouldn't know which Connection to use. We could compare (`===`) the `PDO` instance inside `PdoSessionHandler` to the wrapped `PDO` connection in Doctrine. That would only work if the user has configured their `PdoSessionHandler` to re-use the Doctrine PDO connection. The `PdoSessionHandler` *already* has a `createTable()` method on it to help with manual migration. But... it's not easy to call from a migration because you would need to fetch the `PdoSessionHandler` service from the container. Adding something **NEED**: Either: A) A way for `PdoSessionHandler` to use a DBAL Connection or B) We try to hack this feature by comparing the `PDO` instances in the event subscriber or C) We add an easier way to access the `createTable()` method from inside a migration. TODOs * [X] Determine service injection XML needed for getting all PdoAdapter pools * [ ] Finish DoctrineBundle PR: doctrine/DoctrineBundle#1163 Commits ------- 2dd9c3c Automatically provide Messenger Doctrine schema to "diff"
2 parents 3d30ff7 + 2dd9c3c commit b9d4149

16 files changed

+566
-27
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\SchemaListener;
13+
14+
use Doctrine\Common\EventSubscriber;
15+
use Doctrine\DBAL\Event\SchemaCreateTableEventArgs;
16+
use Doctrine\DBAL\Events;
17+
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
18+
use Doctrine\ORM\Tools\ToolEvents;
19+
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport;
20+
use Symfony\Component\Messenger\Transport\TransportInterface;
21+
22+
/**
23+
* Automatically adds any required database tables to the Doctrine Schema.
24+
*
25+
* @author Ryan Weaver <ryan@symfonycasts.com>
26+
*/
27+
final class MessengerTransportDoctrineSchemaSubscriber implements EventSubscriber
28+
{
29+
private const PROCESSING_TABLE_FLAG = self::class.':processing';
30+
31+
private $transports;
32+
33+
/**
34+
* @param iterable|TransportInterface[] $transports
35+
*/
36+
public function __construct(iterable $transports)
37+
{
38+
$this->transports = $transports;
39+
}
40+
41+
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
42+
{
43+
$dbalConnection = $event->getEntityManager()->getConnection();
44+
foreach ($this->transports as $transport) {
45+
if (!$transport instanceof DoctrineTransport) {
46+
continue;
47+
}
48+
49+
$transport->configureSchema($event->getSchema(), $dbalConnection);
50+
}
51+
}
52+
53+
public function onSchemaCreateTable(SchemaCreateTableEventArgs $event): void
54+
{
55+
$table = $event->getTable();
56+
57+
// if this method triggers a nested create table below, allow Doctrine to work like normal
58+
if ($table->hasOption(self::PROCESSING_TABLE_FLAG)) {
59+
return;
60+
}
61+
62+
foreach ($this->transports as $transport) {
63+
if (!$transport instanceof DoctrineTransport) {
64+
continue;
65+
}
66+
67+
$extraSql = $transport->getExtraSetupSqlForTable($table);
68+
if (null === $extraSql) {
69+
continue;
70+
}
71+
72+
// avoid this same listener from creating a loop on this table
73+
$table->addOption(self::PROCESSING_TABLE_FLAG, true);
74+
$createTableSql = $event->getPlatform()->getCreateTableSQL($table);
75+
76+
/*
77+
* Add all the SQL needed to create the table and tell Doctrine
78+
* to "preventDefault" so that only our SQL is used. This is
79+
* the only way to inject some extra SQL.
80+
*/
81+
$event->addSql($createTableSql);
82+
$event->addSql($extraSql);
83+
$event->preventDefault();
84+
85+
return;
86+
}
87+
}
88+
89+
public function getSubscribedEvents(): array
90+
{
91+
return [
92+
ToolEvents::postGenerateSchema,
93+
Events::onSchemaCreateTable,
94+
];
95+
}
96+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\SchemaListener;
13+
14+
use Doctrine\Common\EventSubscriber;
15+
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
16+
use Doctrine\ORM\Tools\ToolEvents;
17+
use Symfony\Component\Cache\Adapter\PdoAdapter;
18+
19+
/**
20+
* Automatically adds the cache table needed for the PdoAdapter.
21+
*
22+
* @author Ryan Weaver <ryan@symfonycasts.com>
23+
*/
24+
final class PdoCacheAdapterDoctrineSchemaSubscriber implements EventSubscriber
25+
{
26+
private $pdoAdapters;
27+
28+
/**
29+
* @param iterable|PdoAdapter[] $pdoAdapters
30+
*/
31+
public function __construct(iterable $pdoAdapters)
32+
{
33+
$this->pdoAdapters = $pdoAdapters;
34+
}
35+
36+
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
37+
{
38+
$dbalConnection = $event->getEntityManager()->getConnection();
39+
foreach ($this->pdoAdapters as $pdoAdapter) {
40+
$pdoAdapter->configureSchema($event->getSchema(), $dbalConnection);
41+
}
42+
}
43+
44+
public function getSubscribedEvents(): array
45+
{
46+
return [
47+
ToolEvents::postGenerateSchema,
48+
];
49+
}
50+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Tests\SchemaListener;
13+
14+
use Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\Event\SchemaCreateTableEventArgs;
16+
use Doctrine\DBAL\Platforms\AbstractPlatform;
17+
use Doctrine\DBAL\Schema\Schema;
18+
use Doctrine\DBAL\Schema\Table;
19+
use Doctrine\ORM\EntityManagerInterface;
20+
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
21+
use PHPUnit\Framework\TestCase;
22+
use Symfony\Bridge\Doctrine\SchemaListener\MessengerTransportDoctrineSchemaSubscriber;
23+
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport;
24+
use Symfony\Component\Messenger\Transport\TransportInterface;
25+
26+
class MessengerTransportDoctrineSchemaSubscriberTest extends TestCase
27+
{
28+
public function testPostGenerateSchema()
29+
{
30+
$schema = new Schema();
31+
$dbalConnection = $this->createMock(Connection::class);
32+
$entityManager = $this->createMock(EntityManagerInterface::class);
33+
$entityManager->expects($this->once())
34+
->method('getConnection')
35+
->willReturn($dbalConnection);
36+
$event = new GenerateSchemaEventArgs($entityManager, $schema);
37+
38+
$doctrineTransport = $this->createMock(DoctrineTransport::class);
39+
$doctrineTransport->expects($this->once())
40+
->method('configureSchema')
41+
->with($schema, $dbalConnection);
42+
$otherTransport = $this->createMock(TransportInterface::class);
43+
$otherTransport->expects($this->never())
44+
->method($this->anything());
45+
46+
$subscriber = new MessengerTransportDoctrineSchemaSubscriber([$doctrineTransport, $otherTransport]);
47+
$subscriber->postGenerateSchema($event);
48+
}
49+
50+
public function testOnSchemaCreateTable()
51+
{
52+
$platform = $this->createMock(AbstractPlatform::class);
53+
$table = new Table('queue_table');
54+
$event = new SchemaCreateTableEventArgs($table, [], [], $platform);
55+
56+
$otherTransport = $this->createMock(TransportInterface::class);
57+
$otherTransport->expects($this->never())
58+
->method($this->anything());
59+
60+
$doctrineTransport = $this->createMock(DoctrineTransport::class);
61+
$doctrineTransport->expects($this->once())
62+
->method('getExtraSetupSqlForTable')
63+
->with($table)
64+
->willReturn('ALTER TABLE pizza ADD COLUMN extra_cheese boolean');
65+
66+
// we use the platform to generate the full create table sql
67+
$platform->expects($this->once())
68+
->method('getCreateTableSQL')
69+
->with($table)
70+
->willReturn('CREATE TABLE pizza (id integer NOT NULL)');
71+
72+
$subscriber = new MessengerTransportDoctrineSchemaSubscriber([$otherTransport, $doctrineTransport]);
73+
$subscriber->onSchemaCreateTable($event);
74+
$this->assertTrue($event->isDefaultPrevented());
75+
$this->assertSame([
76+
'CREATE TABLE pizza (id integer NOT NULL)',
77+
'ALTER TABLE pizza ADD COLUMN extra_cheese boolean',
78+
], $event->getSql());
79+
}
80+
81+
public function testOnSchemaCreateTableNoExtraSql()
82+
{
83+
$platform = $this->createMock(AbstractPlatform::class);
84+
$table = new Table('queue_table');
85+
$event = new SchemaCreateTableEventArgs($table, [], [], $platform);
86+
87+
$doctrineTransport = $this->createMock(DoctrineTransport::class);
88+
$doctrineTransport->expects($this->once())
89+
->method('getExtraSetupSqlForTable')
90+
->willReturn(null);
91+
92+
$platform->expects($this->never())
93+
->method('getCreateTableSQL');
94+
95+
$subscriber = new MessengerTransportDoctrineSchemaSubscriber([$doctrineTransport]);
96+
$subscriber->onSchemaCreateTable($event);
97+
$this->assertFalse($event->isDefaultPrevented());
98+
}
99+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Tests\SchemaListener;
13+
14+
use Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\Schema\Schema;
16+
use Doctrine\ORM\EntityManagerInterface;
17+
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
18+
use PHPUnit\Framework\TestCase;
19+
use Symfony\Bridge\Doctrine\SchemaListener\PdoCacheAdapterDoctrineSchemaSubscriber;
20+
use Symfony\Component\Cache\Adapter\PdoAdapter;
21+
22+
class PdoCacheAdapterDoctrineSchemaSubscriberTest extends TestCase
23+
{
24+
public function testPostGenerateSchema()
25+
{
26+
$schema = new Schema();
27+
$dbalConnection = $this->createMock(Connection::class);
28+
$entityManager = $this->createMock(EntityManagerInterface::class);
29+
$entityManager->expects($this->once())
30+
->method('getConnection')
31+
->willReturn($dbalConnection);
32+
$event = new GenerateSchemaEventArgs($entityManager, $schema);
33+
34+
$pdoAdapter = $this->createMock(PdoAdapter::class);
35+
$pdoAdapter->expects($this->once())
36+
->method('configureSchema')
37+
->with($schema, $dbalConnection);
38+
39+
$subscriber = new PdoCacheAdapterDoctrineSchemaSubscriber([$pdoAdapter]);
40+
$subscriber->postGenerateSchema($event);
41+
}
42+
}

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
},
2727
"require-dev": {
2828
"symfony/stopwatch": "^4.4|^5.0",
29+
"symfony/cache": "^5.1",
2930
"symfony/config": "^4.4|^5.0",
3031
"symfony/dependency-injection": "^4.4|^5.0",
3132
"symfony/form": "^5.1",
3233
"symfony/http-kernel": "^5.0",
3334
"symfony/messenger": "^4.4|^5.0",
35+
"symfony/doctrine-messenger": "^5.1",
3436
"symfony/property-access": "^4.4|^5.0",
3537
"symfony/property-info": "^5.0",
3638
"symfony/proxy-manager-bridge": "^4.4|^5.0",

src/Symfony/Component/Cache/Adapter/PdoAdapter.php

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -115,24 +115,8 @@ public function createTable()
115115
$conn = $this->getConnection();
116116

117117
if ($conn instanceof Connection) {
118-
$types = [
119-
'mysql' => 'binary',
120-
'sqlite' => 'text',
121-
'pgsql' => 'string',
122-
'oci' => 'string',
123-
'sqlsrv' => 'string',
124-
];
125-
if (!isset($types[$this->driver])) {
126-
throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
127-
}
128-
129118
$schema = new Schema();
130-
$table = $schema->createTable($this->table);
131-
$table->addColumn($this->idCol, $types[$this->driver], ['length' => 255]);
132-
$table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
133-
$table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
134-
$table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
135-
$table->setPrimaryKey([$this->idCol]);
119+
$this->addTableToSchema($schema);
136120

137121
foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
138122
$conn->exec($sql);
@@ -169,6 +153,23 @@ public function createTable()
169153
$conn->exec($sql);
170154
}
171155

156+
/**
157+
* Adds the Table to the Schema if the adapter uses this Connection.
158+
*/
159+
public function configureSchema(Schema $schema, Connection $forConnection): void
160+
{
161+
// only update the schema for this connection
162+
if ($forConnection !== $this->getConnection()) {
163+
return;
164+
}
165+
166+
if ($schema->hasTable($this->table)) {
167+
return;
168+
}
169+
170+
$this->addTableToSchema($schema);
171+
}
172+
172173
/**
173174
* {@inheritdoc}
174175
*/
@@ -467,4 +468,25 @@ private function getServerVersion(): string
467468

468469
return $this->serverVersion;
469470
}
471+
472+
private function addTableToSchema(Schema $schema): void
473+
{
474+
$types = [
475+
'mysql' => 'binary',
476+
'sqlite' => 'text',
477+
'pgsql' => 'string',
478+
'oci' => 'string',
479+
'sqlsrv' => 'string',
480+
];
481+
if (!isset($types[$this->driver])) {
482+
throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
483+
}
484+
485+
$table = $schema->createTable($this->table);
486+
$table->addColumn($this->idCol, $types[$this->driver], ['length' => 255]);
487+
$table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
488+
$table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
489+
$table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
490+
$table->setPrimaryKey([$this->idCol]);
491+
}
470492
}

0 commit comments

Comments
 (0)