diff --git a/src/EventListener/CreateSchemaListener.php b/src/EventListener/CreateSchemaListener.php index 83776556..6c54aebd 100644 --- a/src/EventListener/CreateSchemaListener.php +++ b/src/EventListener/CreateSchemaListener.php @@ -98,11 +98,7 @@ public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $eventArgs) $revisionsTable = $this->createRevisionsTable($schema); $entityTable = $eventArgs->getClassTable(); - if ($this->isDbal4_3()) { - $tableName = $this->config->getTablePrefix().$entityTable->getObjectName()->toString().$this->config->getTableSuffix(); - } else { - $tableName = $this->config->getTablePrefix().$entityTable->getName().$this->config->getTableSuffix(); // @phpstan-ignore-line - } + $tableName = $this->config->getTablePrefix().$this->getUnquotedTableName($entityTable).$this->config->getTableSuffix(); $revisionTable = $schema->createTable($tableName); foreach ($entityTable->getColumns() as $column) { @@ -263,22 +259,14 @@ private function createRevisionsTable(Schema $schema): Table private function createRevisionJoinTableForJoinTable(Schema $schema, string $joinTableName): void { $joinTable = $schema->getTable($joinTableName); - if ($this->isDbal4_3()) { - $revisionJoinTableName = $this->config->getTablePrefix().$joinTable->getObjectName()->toString().$this->config->getTableSuffix(); - } else { - $revisionJoinTableName = $this->config->getTablePrefix().$joinTable->getName().$this->config->getTableSuffix(); // @phpstan-ignore-line - } + $revisionJoinTableName = $this->config->getTablePrefix().$this->getUnquotedTableName($joinTable).$this->config->getTableSuffix(); if ($schema->hasTable($revisionJoinTableName)) { return; } $typeRegistry = Type::getTypeRegistry(); - if ($this->isDbal4_3()) { - $tableName = $this->config->getTablePrefix().$joinTable->getObjectName()->toString().$this->config->getTableSuffix(); - } else { - $tableName = $this->config->getTablePrefix().$joinTable->getName().$this->config->getTableSuffix(); // @phpstan-ignore-line - } + $tableName = $revisionJoinTableName; $revisionJoinTable = $schema->createTable($tableName); /** @var Column $column */ foreach ($joinTable->getColumns() as $column) { diff --git a/src/Utils/DbalCompatibilityTrait.php b/src/Utils/DbalCompatibilityTrait.php index 73c86f9a..0b5338d4 100644 --- a/src/Utils/DbalCompatibilityTrait.php +++ b/src/Utils/DbalCompatibilityTrait.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\Table; /** * NEXT_MAJOR: remove this trait and all `if` blocks. @@ -28,6 +29,28 @@ protected function isDbal4_3(): bool return class_exists(UnqualifiedName::class); // @phpstan-ignore-line class added in doctrine/dbal 4.3 } + /** + * Returns the unquoted name of a table, usable as a base for deriving a related + * table name through concatenation. + * + * On DBAL >= 4.3, Table::getName() is deprecated and Table::getObjectName()->toString() + * renders a *quoted* identifier for reserved keywords (e.g. `"user"`). Concatenating + * that with a prefix/suffix yields an unparseable name (`"user"_audit`), which DBAL + * leaves uninitialized and then rejects from getObjectName(). Using the raw identifier + * value preserves the historical (pre-4.3) behaviour. + */ + protected function getUnquotedTableName(Table $table): string + { + if ($this->isDbal4_3()) { + $name = $table->getObjectName(); + $qualifier = $name->getQualifier(); + + return (null !== $qualifier ? $qualifier->getValue().'.' : '').$name->getUnqualifiedName()->getValue(); + } + + return $table->getName(); // @phpstan-ignore-line + } + protected function isDbal4(): bool { return is_subclass_of(ParameterType::class, \UnitEnum::class); // @phpstan-ignore-line class changed to enum in doctrine/dbal 4 diff --git a/tests/Fixtures/Issue/ReservedTableNameEntity.php b/tests/Fixtures/Issue/ReservedTableNameEntity.php new file mode 100644 index 00000000..31bc9b18 --- /dev/null +++ b/tests/Fixtures/Issue/ReservedTableNameEntity.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\EntityAuditBundle\Tests\Fixtures\Issue; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * The table name is an SQL reserved keyword, so it is force-quoted by Doctrine. + * + * @psalm-suppress ClassMustBeFinal + */ +#[ORM\Entity] +#[ORM\Table(name: '`user`')] +class ReservedTableNameEntity +{ + /** + * @var int|null + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\GeneratedValue] + protected $id; + + #[ORM\Column(type: Types::STRING, length: 255)] + private ?string $name = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name = null): void + { + $this->name = $name; + } +} diff --git a/tests/Issue/IssueReservedTableNameTest.php b/tests/Issue/IssueReservedTableNameTest.php new file mode 100644 index 00000000..750d9614 --- /dev/null +++ b/tests/Issue/IssueReservedTableNameTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\EntityAuditBundle\Tests\Issue; + +use Sonata\EntityAuditBundle\Tests\BaseTestCase; +use Sonata\EntityAuditBundle\Tests\Fixtures\Issue\ReservedTableNameEntity; + +final class IssueReservedTableNameTest extends BaseTestCase +{ + protected $schemaEntities = [ + ReservedTableNameEntity::class, + ]; + + protected $auditedEntities = [ + ReservedTableNameEntity::class, + ]; + + /** + * On DBAL >= 4.3 the audit table name was derived from the *quoted* identifier + * (e.g. `"user"` + `_audit` => `"user"_audit`), which is not a parseable name, so + * schema generation crashed with "Object name has not been initialized". + */ + public function testAuditSchemaForReservedKeywordTableName(): void + { + $entity = new ReservedTableNameEntity(); + $entity->setName('foo'); + + $em = $this->getEntityManager(); + + $em->persist($entity); + $em->flush(); + + $reader = $this->getAuditManager()->createAuditReader($em); + + $id = $entity->getId(); + static::assertNotNull($id); + + $audited = $reader->find(ReservedTableNameEntity::class, $id, 1); + static::assertSame('foo', $audited->getName()); + } +}