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
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<directory>tests/bundle/Debug</directory>
</testsuite>
<testsuite name="bundle_installer">
<directory>tests/bundle/Installer</directory>
<directory>tests/bundle/RepositoryInstaller</directory>
</testsuite>
<testsuite name="bundle_io">
<directory>tests/bundle/IO</directory>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\RepositoryInstaller\Event\Subscriber;

use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Tools\SchemaTool;
use Ibexa\Contracts\DoctrineSchema\Event\SchemaBuilderEvent;
use Ibexa\Contracts\DoctrineSchema\SchemaBuilderEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Contributes the tables of plain Doctrine ORM entities to the SchemaBuilderEvent schema by
* introspecting their mappings, instead of a hand-maintained parallel schema.yaml declaration --
* the ORM mapping is already the single source of truth for what these tables look like, and
* SchemaTool already knows how to turn mapping metadata into a Doctrine\DBAL\Schema\Schema.
*
* A package registers its own instance, listing only its own entities:
*
* ibexa.<package>.schema_builder.orm_entities:
* class: Ibexa\Bundle\RepositoryInstaller\Event\Subscriber\OrmEntitiesSchemaSubscriber
* arguments:
* $entityManager: '@ibexa.doctrine.orm.entity_manager'
* $entityClasses:
* - Ibexa\<Package>\Persistence\Entity\SomeEntity
* tags: [kernel.event_subscriber]
*
* Tables already present in the schema are left as they are.
*/
final class OrmEntitiesSchemaSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $entityManager;

/** @var list<class-string> */
private array $entityClasses;

/**
* @param list<class-string> $entityClasses
*/
public function __construct(EntityManagerInterface $entityManager, array $entityClasses)
{
$this->entityManager = $entityManager;
$this->entityClasses = $entityClasses;
}

public static function getSubscribedEvents(): array
{
return [
SchemaBuilderEvents::BUILD_SCHEMA => ['onBuildSchema', 250],
];
}

public function onBuildSchema(SchemaBuilderEvent $event): void
{
$metadataFactory = $this->entityManager->getMetadataFactory();
$classMetadata = array_map(
static function (string $class) use ($metadataFactory): ClassMetadata {
return $metadataFactory->getMetadataFor($class);
},
$this->entityClasses,
);

$schemaTool = new SchemaTool($this->entityManager);
$ormSchema = $schemaTool->getSchemaFromMetadata($classMetadata);

// SchemaTool::getSchemaFromMetadata() fires Doctrine's ToolEvents::postGenerateSchema on
// the entity manager's (shared) event manager -- any *other* listener on that same event
// manager (e.g. Symfony Messenger's Doctrine transport auto-setup) reacts too and adds its
// own table, regardless of which classes were actually requested here. Only transplant the
// tables that genuinely belong to the requested entities.
$ownTableNames = array_map(
static function (ClassMetadata $metadata): string {
return $metadata->getTableName();
},
$classMetadata,
);

$targetSchema = $event->getSchema();
foreach ($ormSchema->getTables() as $ormTable) {
if (!in_array($ormTable->getName(), $ownTableNames, true)) {
continue;
}

if ($targetSchema->hasTable($ormTable->getName())) {
continue;
}

$this->copyTable($ormTable, $targetSchema->createTable($ormTable->getName()));
}

$ownSequenceNames = array_filter(array_map(
static function (ClassMetadata $metadata): ?string {
return isset($metadata->sequenceGeneratorDefinition['sequenceName'])
? $metadata->sequenceGeneratorDefinition['sequenceName']
: null;
},
$classMetadata,
));

foreach ($ormSchema->getSequences() as $sequence) {
if (!in_array($sequence->getName(), $ownSequenceNames, true)) {
continue;
}

if ($targetSchema->hasSequence($sequence->getName())) {
continue;
}

$targetSchema->createSequence(
$sequence->getName(),
$sequence->getAllocationSize(),
$sequence->getInitialValue(),
);
}
}

/**
* Doctrine\DBAL\Schema\Schema has no public API to transplant an existing Table between two
* Schema instances (Schema::_addTable() is protected), so each aspect is copied individually
* through Table's own public API -- the same approach ibexa/doctrine-schema's own
* SchemaImporter uses when building a table from a parsed Yaml array, just reading from an
* already-built Table here instead.
*/
private function copyTable(Table $source, Table $target): void
{
foreach ($source->getColumns() as $column) {
$options = [
'length' => $column->getLength(),
'precision' => $column->getPrecision(),
'scale' => $column->getScale(),
'unsigned' => $column->getUnsigned(),
'fixed' => $column->getFixed(),
'notnull' => $column->getNotnull(),
'default' => $column->getDefault(),
'autoincrement' => $column->getAutoincrement(),
'columnDefinition' => $column->getColumnDefinition(),
'comment' => $column->getComment(),
];
if ($column->getPlatformOptions() !== []) {
$options['platformOptions'] = $column->getPlatformOptions();
}
$target->addColumn($column->getName(), Type::getTypeRegistry()->lookupName($column->getType()), $options);
}

$primaryKey = $source->getPrimaryKey();
if ($primaryKey !== null) {
$target->setPrimaryKey($primaryKey->getUnquotedColumns());
}

foreach ($source->getIndexes() as $index) {
if ($index->isPrimary()) {
continue;
}

if ($index->isUnique()) {
$target->addUniqueIndex($index->getUnquotedColumns(), $index->getName(), $index->getOptions());
} else {
$target->addIndex($index->getUnquotedColumns(), $index->getName(), $index->getFlags(), $index->getOptions());
}
}

foreach ($source->getForeignKeys() as $foreignKey) {
$target->addForeignKeyConstraint(
$foreignKey->getForeignTableName(),
$foreignKey->getUnquotedLocalColumns(),
$foreignKey->getUnquotedForeignColumns(),
$foreignKey->getOptions(),
$foreignKey->getName(),
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function testLoadLoadsTaggedCoreInstaller(): void
$this->assertContainerBuilderHasServiceDefinitionWithTag(
CoreInstaller::class,
InstallerTagPass::INSTALLER_TAG,
['type' => 'clean']
['type' => 'ibexa-oss']
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Bundle\RepositoryInstaller\Event\Subscriber\Fixtures\Entity;

final class Category
{
public int $id;

public string $identifier;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Bundle\RepositoryInstaller\Event\Subscriber\Fixtures\Entity;

final class Item
{
public int $id;

public Category $category;

public string $name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Ibexa\Tests\Bundle\RepositoryInstaller\Event\Subscriber\Fixtures\Entity\Category" table="test_orm_category">
<unique-constraints>
<unique-constraint name="test_orm_category_identifier_idx" columns="identifier"/>
</unique-constraints>
<id name="id" type="integer">
<generator strategy="AUTO"/>
</id>
<field name="identifier" type="string" length="64"/>
</entity>
</doctrine-mapping>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Ibexa\Tests\Bundle\RepositoryInstaller\Event\Subscriber\Fixtures\Entity\Item" table="test_orm_item">
<indexes>
<index name="test_orm_item_name_idx" columns="name"/>
</indexes>
<id name="id" type="integer">
<generator strategy="SEQUENCE"/>
<sequence-generator sequence-name="test_orm_item_id_seq" allocation-size="1" initial-value="1"/>
</id>
<field name="name" type="string" length="255"/>
<many-to-one field="category" target-entity="Ibexa\Tests\Bundle\RepositoryInstaller\Event\Subscriber\Fixtures\Entity\Category">
<join-column name="category_id" referenced-column-name="id" nullable="false" on-delete="CASCADE"/>
</many-to-one>
</entity>
</doctrine-mapping>
Loading
Loading