diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3ad2b2965..748ac52ba 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -25,11 +25,6 @@ parameters: count: 4 path: src/Propel/Common/Config/PropelConfiguration.php - - - message: "#^Access to an undefined property Propel\\\\Generator\\\\Model\\\\Table\\:\\:\\$isArchiveTable\\.$#" - count: 1 - path: src/Propel/Generator/Behavior/Archivable/ArchivableBehavior.php - - message: "#^Call to an undefined method Propel\\\\Generator\\\\Config\\\\GeneratorConfigInterface\\:\\:get\\(\\)\\.$#" count: 1 @@ -40,6 +35,11 @@ parameters: count: 1 path: src/Propel/Generator/Behavior/ConcreteInheritance/ConcreteInheritanceBehavior.php + - + message: "#^Parameter \\#1 \\$unique of method Propel\\\\Generator\\\\Model\\\\Table\\:\\:addUnique\\(\\) expects array\\|Propel\\\\Generator\\\\Model\\\\Unique, Propel\\\\Generator\\\\Model\\\\Index given\\.$#" + count: 1 + path: src/Propel/Generator/Behavior/SyncedTable/TableSyncer.php + - message: "#^Access to an undefined property Propel\\\\Generator\\\\Model\\\\Table\\:\\:\\$isVersionTable\\.$#" count: 1 @@ -195,19 +195,9 @@ parameters: count: 1 path: src/Propel/Generator/Manager/AbstractManager.php - - - message: "#^Strict comparison using \\=\\=\\= between int and null will always evaluate to false\\.$#" - count: 1 - path: src/Propel/Generator/Model/Domain.php - - - - message: "#^Strict comparison using \\=\\=\\= between array\\ and null will always evaluate to false\\.$#" - count: 1 - path: src/Propel/Generator/Model/Table.php - - message: "#^Call to an undefined method Propel\\\\Generator\\\\Config\\\\GeneratorConfigInterface\\:\\:get\\(\\)\\.$#" - count: 2 + count: 1 path: src/Propel/Generator/Platform/MysqlPlatform.php - @@ -380,11 +370,6 @@ parameters: count: 1 path: src/Propel/Runtime/Collection/ObjectCombinationCollection.php - - - message: "#^Return type \\(Propel\\\\Runtime\\\\Collection\\\\OnDemandIterator\\) of method Propel\\\\Runtime\\\\Collection\\\\OnDemandCollection\\:\\:getIterator\\(\\) should be compatible with return type \\(Propel\\\\Runtime\\\\Collection\\\\CollectionIterator\\) of method Propel\\\\Runtime\\\\Collection\\\\Collection\\:\\:getIterator\\(\\)$#" - count: 1 - path: src/Propel/Runtime/Collection/OnDemandCollection.php - - message: "#^Call to an undefined method Propel\\\\Runtime\\\\Connection\\\\ConnectionWrapper\\:\\:getProfiler\\(\\)\\.$#" count: 3 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b213fa1c3..4a44622d3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -11,7 +11,7 @@ - + $versionTable->isVersionTable diff --git a/src/Propel/Generator/Behavior/Archivable/ArchivableBehavior.php b/src/Propel/Generator/Behavior/Archivable/ArchivableBehavior.php index c34548e00..8159c73c8 100644 --- a/src/Propel/Generator/Behavior/Archivable/ArchivableBehavior.php +++ b/src/Propel/Generator/Behavior/Archivable/ArchivableBehavior.php @@ -8,49 +8,39 @@ namespace Propel\Generator\Behavior\Archivable; +use Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior; use Propel\Generator\Builder\Om\AbstractOMBuilder; use Propel\Generator\Exception\InvalidArgumentException; -use Propel\Generator\Exception\SchemaException; -use Propel\Generator\Model\Behavior; use Propel\Generator\Model\Column; -use Propel\Generator\Model\ForeignKey; -use Propel\Generator\Model\Index; use Propel\Generator\Model\Table; -use Propel\Generator\Platform\PgsqlPlatform; -use Propel\Generator\Platform\PlatformInterface; -use Propel\Generator\Platform\SqlitePlatform; /** * Keeps tracks of an ActiveRecord object, even after deletion * * @author Francois Zaninotto */ -class ArchivableBehavior extends Behavior +class ArchivableBehavior extends SyncedTableBehavior { /** - * Default parameters value + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::DEFAULT_SYNCED_TABLE_SUFFIX * - * @var array + * @var string DEFAULT_SYNCED_TABLE_SUFFIX */ - protected $parameters = [ - 'archive_table' => '', - 'archive_phpname' => null, - 'archive_class' => '', - 'sync' => 'false', - 'inherit_foreign_key_relations' => 'false', - 'inherit_foreign_key_constraints' => 'false', - 'foreign_keys' => null, - 'log_archived_at' => 'true', - 'archived_at_column' => 'archived_at', - 'archive_on_insert' => 'false', - 'archive_on_update' => 'false', - 'archive_on_delete' => 'true', - ]; + protected const DEFAULT_SYNCED_TABLE_SUFFIX = '_archive'; /** - * @var \Propel\Generator\Model\Table|null + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::PARAMETER_KEY_SYNCED_TABLE + * + * @var string */ - protected $archiveTable; + public const PARAMETER_KEY_SYNCED_TABLE = 'archive_table'; + + /** + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::PARAMETER_KEY_SYNCED_PHPNAME + * + * @var string + */ + public const PARAMETER_KEY_SYNCED_PHPNAME = 'archive_phpname'; /** * @var \Propel\Generator\Behavior\Archivable\ArchivableBehaviorObjectBuilderModifier|null @@ -63,18 +53,29 @@ class ArchivableBehavior extends Behavior protected $queryBuilderModifier; /** - * @return void + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::getDefaultParameters() + * + * @return array */ - public function modifyDatabase(): void + protected function getDefaultParameters(): array { - foreach ($this->getDatabase()->getTables() as $table) { - if ($table->hasBehavior($this->getId())) { - // don't add the same behavior twice - continue; - } - $b = clone $this; - $table->addBehavior($b); - } + return [ + static::PARAMETER_KEY_SYNCED_TABLE => '', + static::PARAMETER_KEY_SYNCED_PHPNAME => null, + 'archive_class' => '', + static::PARAMETER_KEY_SYNC => 'false', + static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_RELATIONS => 'false', + static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_CONSTRAINTS => 'false', + static::PARAMETER_KEY_FOREIGN_KEYS => null, + static::PARAMETER_KEY_SYNC_INDEXES => 'true', + static::PARAMETER_KEY_SYNC_UNIQUE_AS => null, + static::PARAMETER_KEY_EMPTY_ACCESSOR_COLUMNS => 'true', + 'log_archived_at' => 'true', + 'archived_at_column' => 'archived_at', + 'archive_on_insert' => 'false', + 'archive_on_update' => 'false', + 'archive_on_delete' => 'true', + ]; } /** @@ -84,275 +85,40 @@ public function modifyDatabase(): void */ public function modifyTable(): void { - if ($this->getParameter('archive_class') && $this->getParameter('archive_table')) { + if ($this->getParameter('archive_class') && $this->getParameter(static::PARAMETER_KEY_SYNCED_TABLE)) { throw new InvalidArgumentException('Please set only one of the two parameters "archive_class" and "archive_table".'); } if (!$this->getParameter('archive_class')) { - $this->addArchiveTable(); - } - } - - /** - * @return string - */ - protected function getArchiveTableName(): string - { - return $this->getParameter('archive_table') ?: ($this->getTable()->getOriginCommonName() . '_archive'); - } - - /** - * @return void - */ - protected function addArchiveTable(): void - { - $table = $this->getTable(); - $database = $table->getDatabase(); - $archiveTableName = $this->getArchiveTableName(); - - $archiveTableExistsInSchema = $database->hasTable($archiveTableName); - - $this->archiveTable = $archiveTableExistsInSchema ? - $database->getTable($archiveTableName) : - $this->createArchiveTable(); - - if ($archiveTableExistsInSchema && !$this->parameterHasValue('sync', 'true')) { - return; - } - - $this->syncTables(); - } - - /** - * @return \Propel\Generator\Model\Table - */ - protected function createArchiveTable(): Table - { - $sourceTable = $this->getTable(); - $database = $sourceTable->getDatabase(); - - // create the version table - return $database->addTable([ - 'name' => $this->getArchiveTableName(), - 'phpName' => $this->getParameter('archive_phpname'), - 'package' => $sourceTable->getPackage(), - 'schema' => $sourceTable->getSchema(), - 'namespace' => $sourceTable->getNamespace() ? '\\' . $sourceTable->getNamespace() : null, - 'identifierQuoting' => $sourceTable->isIdentifierQuotingEnabled(), - ]); - } - - /** - * @return \Propel\Generator\Model\Table - */ - protected function syncTables(): Table - { - $archiveTable = $this->getArchiveTable(); - $sourceTable = $this->getTable(); - - $columns = $sourceTable->getColumns(); - $this->syncColumns($archiveTable, $columns); - - $this->addArchivedAtColumn($archiveTable); - - $foreignKeys = $this->getParameter('foreign_keys'); - if ($foreignKeys) { - foreach ($foreignKeys as $fkData) { - $this->createForeignKeyFromParameters($archiveTable, $fkData); - } - } - - $inheritFkRelations = $this->parameterHasValue('inherit_foreign_key_relations', 'true'); - $inheritFkConstraints = $this->parameterHasValue('inherit_foreign_key_constraints', 'true'); - if ($inheritFkRelations || $inheritFkConstraints) { - $foreignKeys = $sourceTable->getForeignKeys(); - $this->syncForeignKeys($archiveTable, $foreignKeys, $inheritFkConstraints); - } - - $indexes = $sourceTable->getIndices(); - $platform = $sourceTable->getDatabase()->getPlatform(); - $renameIndexes = $this->isDistinctiveIndexNameRequired($platform); - $this->syncIndexes($archiveTable, $indexes, $renameIndexes); - - $uniqueIndexes = $sourceTable->getUnices(); - $this->syncUniqueIndexes($archiveTable, $uniqueIndexes); - - $behaviors = $sourceTable->getDatabase()->getBehaviors(); - $this->reapplyBehaviors($behaviors); - - return $archiveTable; - } - - /** - * @param \Propel\Generator\Model\Table $archiveTable - * @param array<\Propel\Generator\Model\Column> $columns - * - * @return void - */ - protected function syncColumns(Table $archiveTable, array $columns) - { - foreach ($columns as $sourceColumn) { - if ($archiveTable->hasColumn($sourceColumn)) { - continue; - } - $archiveColumn = clone $sourceColumn; - $archiveColumn->clearReferrers(); - $archiveColumn->setAutoIncrement(false); - $archiveTable->addColumn($archiveColumn); + parent::modifyTable(); } } /** - * @param \Propel\Generator\Model\Table $archiveTable + * @param \Propel\Generator\Model\Table $syncedTable + * @param bool $tableExistsInSchema * * @return void */ - protected function addArchivedAtColumn(Table $archiveTable) + public function addTableElements(Table $syncedTable, $tableExistsInSchema): void { - if (!$this->parameterHasValue('log_archived_at', 'true')) { - return; - } - $columnName = $this->getParameter('archived_at_column'); - if ($archiveTable->hasColumn($columnName)) { - return; - } - $archiveTable->addColumn([ - 'name' => $columnName, - 'type' => 'TIMESTAMP', - ]); + parent::addTableElements($syncedTable, $tableExistsInSchema); + $this->addCustomColumnsToSyncedTable($syncedTable); } /** - * @param \Propel\Generator\Model\Table $archiveTable - * @param array<\Propel\Generator\Model\ForeignKey> $foreignKeys - * @param bool $inheritConstraints + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::addCustomColumnsToSyncedTable() * - * @return void - */ - protected function syncForeignKeys(Table $archiveTable, array $foreignKeys, bool $inheritConstraints) - { - foreach ($foreignKeys as $foreignKey) { - if ($archiveTable->containsForeignKeyWithSameName($foreignKey)) { - continue; - } - $copiedForeignKey = clone $foreignKey; - $copiedForeignKey->setSkipSql(!$inheritConstraints); - $archiveTable->addForeignKey($copiedForeignKey); - } - } - - /** - * @param \Propel\Generator\Model\Table $archiveTable - * @param array<\Propel\Generator\Model\Index> $indexes - * @param bool $rename + * @param \Propel\Generator\Model\Table $syncedTable * * @return void */ - protected function syncIndexes(Table $archiveTable, array $indexes, bool $rename) + protected function addCustomColumnsToSyncedTable(Table $syncedTable) { - foreach ($indexes as $index) { - $copiedIndex = clone $index; - if ($rename) { - // by removing the name, Propel will generate a unique name based on table and columns - $copiedIndex->setName(null); - } - if ($archiveTable->hasIndex($index->getName())) { - continue; - } - $archiveTable->addIndex($copiedIndex); + if ($this->parameterHasValue('log_archived_at', 'true')) { + $this->addColumnFromParameterIfNotExists($syncedTable, 'archived_at_column', ['type' => 'TIMESTAMP']); } } - /** - * Create regular indexes from unique indexes on the given archive table. - * - * The archive table cannot use unique indexes, as even unique data on the - * source table can be archived several times. - * - * @param \Propel\Generator\Model\Table $archiveTable - * @param array<\Propel\Generator\Model\Unique> $uniqueIndexes - * - * @return void - */ - protected function syncUniqueIndexes(Table $archiveTable, array $uniqueIndexes) - { - foreach ($uniqueIndexes as $unique) { - $index = new Index(); - $index->setTable($archiveTable); - foreach ($unique->getColumns() as $columnName) { - $columnDef = [ - 'name' => $columnName, - 'size' => $unique->getColumnSize($columnName), - ]; - $index->addColumn($columnDef); - } - - if ($archiveTable->hasIndex($index->getName())) { - continue; - } - $archiveTable->addIndex($index); - } - } - - /** - * @param array $behaviors - * - * @return void - */ - protected function reapplyBehaviors(array $behaviors) - { - foreach ($behaviors as $behavior) { - if ($behavior instanceof ArchivableBehavior) { - continue; - } - $behavior->modifyDatabase(); - } - } - - /** - * @psalm-param array{name?: string, localColumn: string, foreignTable: string, foreignColumn: string, relationOnly?: string} $fkParameterData - * - * @param \Propel\Generator\Model\Table $table - * @param array $fkParameterData - * - * @throws \Propel\Generator\Exception\SchemaException - * - * @return void - */ - protected function createForeignKeyFromParameters(Table $table, array $fkParameterData): void - { - if ( - empty($fkParameterData['localColumn']) || - empty($fkParameterData['foreignColumn']) - ) { - $tableName = $this->table->getName(); - - throw new SchemaException("Table `$tableName`: Archivable behavior misses foreign key parameters. Please supply `localColumn`, `foreignTable` and `foreignColumn` for every entry"); - } - - $fk = new ForeignKey($fkParameterData['name'] ?? null); - $fk->addReference($fkParameterData['localColumn'], $fkParameterData['foreignColumn']); - $table->addForeignKey($fk); - $fk->loadMapping($fkParameterData); - } - - /** - * @param \Propel\Generator\Platform\PlatformInterface|null $platform - * - * @return bool - */ - protected function isDistinctiveIndexNameRequired(?PlatformInterface $platform): bool - { - return $platform instanceof PgsqlPlatform || $platform instanceof SqlitePlatform; - } - - /** - * @return \Propel\Generator\Model\Table|null - */ - public function getArchiveTable(): ?Table - { - return $this->archiveTable; - } - /** * @param \Propel\Generator\Builder\Om\AbstractOMBuilder $builder * @@ -364,7 +130,7 @@ public function getArchiveTablePhpName(AbstractOMBuilder $builder): string return $this->getParameter('archive_class'); } - $archiveTable = $this->getArchiveTable(); + $archiveTable = $this->getSyncedTable(); $tableStub = $builder->getNewStubObjectBuilder($archiveTable); return $builder->getClassNameFromBuilder($tableStub); @@ -381,7 +147,7 @@ public function getArchiveTableQueryName(AbstractOMBuilder $builder): string return $this->getParameter('archive_class') . 'Query'; } - return $builder->getClassNameFromBuilder($builder->getNewStubQueryBuilder($this->getArchiveTable())); + return $builder->getClassNameFromBuilder($builder->getNewStubQueryBuilder($this->getSyncedTable())); } /** @@ -389,7 +155,7 @@ public function getArchiveTableQueryName(AbstractOMBuilder $builder): string */ public function hasArchiveClass(): bool { - return $this->getParameter('archive_class') ? true : false; + return (bool)$this->getParameter('archive_class'); } /** @@ -397,8 +163,8 @@ public function hasArchiveClass(): bool */ public function getArchivedAtColumn(): ?Column { - if ($this->getArchiveTable() && $this->getParameter('log_archived_at') === 'true') { - return $this->getArchiveTable()->getColumn($this->getParameter('archived_at_column')); + if ($this->getSyncedTable() && $this->getParameter('log_archived_at') === 'true') { + return $this->getSyncedTable()->getColumn($this->getParameter('archived_at_column')); } return null; diff --git a/src/Propel/Generator/Behavior/SyncedTable/EmptyColumnAccessorsBehavior.php b/src/Propel/Generator/Behavior/SyncedTable/EmptyColumnAccessorsBehavior.php new file mode 100644 index 000000000..a985826a1 --- /dev/null +++ b/src/Propel/Generator/Behavior/SyncedTable/EmptyColumnAccessorsBehavior.php @@ -0,0 +1,114 @@ +buildCodeForHooks($columnNames); + $behavior->setup($insertingBehavior, $table, $codeForHooks); + + return $behavior; + } + + /** + * @param array $columnNames + * + * @return array + */ + protected function buildCodeForHooks(array $columnNames): array + { + $accessorNames = $this->buildAccessorNames($columnNames); + + return [ + 'objectAttributes' => $this->buildObjectAttributes($accessorNames), + 'objectCall' => $this->buildObjectCall(), + ]; + } + + /** + * @param array $columnNames + * + * @return array + */ + protected function buildAccessorNames(array $columnNames): array + { + $accessors = []; + foreach ($columnNames as $columnName) { + $phpName = (new Column($columnName))->getPhpName(); + array_push($accessors, 'get' . $phpName, 'set' . $phpName); + } + + return $accessors; + } + + /** + * @param array $accessorNames + * + * @return string + */ + public function buildObjectAttributes(array $accessorNames): string + { + if (!$accessorNames) { + return ''; + } + $nameToArrayKeyFun = fn (string $name) => " '$name' => 1,"; + $namesAsArrayKeys = implode("\n", array_map($nameToArrayKeyFun, $accessorNames)); + + return <<__parentCall(\$name, \$params); + } catch(BadMethodCallException \$e){ + return null; + } + } + +EOT; + } +} diff --git a/src/Propel/Generator/Behavior/SyncedTable/SyncedTableBehavior.php b/src/Propel/Generator/Behavior/SyncedTable/SyncedTableBehavior.php new file mode 100644 index 000000000..964fe8ed8 --- /dev/null +++ b/src/Propel/Generator/Behavior/SyncedTable/SyncedTableBehavior.php @@ -0,0 +1,248 @@ +syncedTable; + } + + /** + * @return string + */ + public function getDefaultTableSuffix(): string + { + return static::DEFAULT_SYNCED_TABLE_SUFFIX; + } + + /** + * @return void + */ + protected function setupObject(): void + { + parent::setupObject(); + $this->setParameterDefaults(); + } + + /** + * @return void + */ + protected function setParameterDefaults(): void + { + $params = $this->getParameters(); + $defaultParams = $this->getDefaultParameters(); + $this->setParameters(array_merge($defaultParams, $params)); + } + + /** + * @return string + */ + public function resolveSyncedTableName(): string + { + return $this->getSyncedTableName() + ?: $this->getTable()->getOriginCommonName() . $this->getDefaultSyncedTableSuffix(); + } + + /** + * @see \Propel\Generator\Model\Behavior::modifyDatabase() + * + * @return void + */ + public function modifyDatabase(): void + { + foreach ($this->getDatabase()->getTables() as $table) { + $this->addBehaviorToTable($table); + } + } + + /** + * Note overridden by inheriting classes. + * + * @param \Propel\Generator\Model\Table $table + * + * @return void + */ + protected function addBehaviorToTable(Table $table): void + { + if ($table->hasBehavior($this->getId())) { + // don't add the same behavior twice + return; + } + $b = clone $this; + $table->addBehavior($b); + } + + /** + * @see \Propel\Generator\Model\Behavior::modifyTable() + * + * @return void + */ + public function modifyTable(): void + { + if ($this->omitOnSkipSql() && $this->table->isSkipSql()) { + return; + } + $this->validateParameters(); + $this->syncedTable = TableSyncer::getSyncedTable($this, $this->getTable()); + $this->addEmptyAccessorsToTable($this->syncedTable); + } + + /** + * @param \Propel\Generator\Model\Table $table + * + * @return void + */ + protected function addEmptyAccessorsToTable(Table $table): void + { + $emptyAccessorColumnNames = $this->getEmptyAccessorColumnNames(); + if (!$emptyAccessorColumnNames) { + return; + } + EmptyColumnAccessorsBehavior::addEmptyAccessors($this, $table, $emptyAccessorColumnNames); + } + + /** + * @throws \Propel\Generator\Behavior\SyncedTable\SyncedTableException + * + * @return void + */ + public function validateParameters(): void + { + foreach ($this->getForeignKeys() as $fkData) { + if (empty($fkData['localColumn']) || empty($fkData['foreignTable']) || empty($fkData['foreignColumn'])) { + throw new SyncedTableException($this, 'Missing foreign key parameters - please supply `localColumn`, `foreignTable` and `foreignColumn` for every entry'); + } + } + } + + /** + * Manual add elements to the synced table. + * + * Allows extending classes to setup custom element. Happens somewhat + * between table setup for backward compatibility. + * + * @param \Propel\Generator\Model\Table $syncedTable + * @param bool $tableExistsInSchema + * + * @return void + */ + public function addTableElements(Table $syncedTable, bool $tableExistsInSchema): void + { + // base implementation does nothing + } + + /** + * @param \Propel\Generator\Model\Table $table + * @param string $parameterWithColumnName + * @param array $columnDefinition + * + * @return void + */ + protected function addColumnFromParameterIfNotExists(Table $table, string $parameterWithColumnName, array $columnDefinition): void + { + $columnName = $this->getParameter($parameterWithColumnName); + TableSyncer::addColumnIfNotExists($table, $columnName, $columnDefinition); + } + + /** + * @param string $parameterName + * @param bool $canBeBoolean + * @param \Propel\Generator\Model\Table|null $table + * + * @throws \Propel\Generator\Behavior\SyncedTable\SyncedTableException + * + * @return void + */ + protected function checkColumnsInParameterExistInTable(string $parameterName, bool $canBeBoolean = false, ?Table $table = null): void + { + $table ??= $this->getTable(); + + if ( + empty($this->parameters[$parameterName]) || + ($canBeBoolean && in_array(strtolower($this->parameters[$parameterName]), ['true', 'false', 0, 1])) + ) { + return; + } + $columnNames = $this->getParameterCsv($parameterName); + foreach ($columnNames as $columnName) { + if ($table->hasColumn($columnName)) { + continue; + } + + throw new SyncedTableException($this, "Column '$columnName' in parameter '$parameterName' does not exist in table"); + } + } + + /** + * @return string + */ + public function getColumnPrefix(): string + { + $val = $this->useColumnPrefix(); + if ($val === true) { + return $this->table->getName() . '_'; + } + + return is_string($val) ? $val : ''; + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * + * @throws \Propel\Generator\Behavior\SyncedTable\SyncedTableException + * + * @return array<\Propel\Generator\Model\Column> + */ + protected function getSyncedPrimaryKeyColumns(Table $syncedTable): array + { + $prefix = $this->getColumnPrefix(); + $pkColumns = []; + foreach ($this->table->getPrimaryKey() as $sourcePkColumn) { + $syncedPkColumnName = $prefix . $sourcePkColumn->getName(); + $syncedPkColumn = $syncedTable->getColumn($syncedPkColumnName); + if (!$syncedPkColumn) { + throw new SyncedTableException($this, "Cannot find synced PK column '{$syncedPkColumnName}' for source column '{$sourcePkColumn->getName()}'"); + } + $pkColumns[] = $syncedPkColumn; + } + + return $pkColumns; + } + + /** + * @param array<\Propel\Generator\Model\ForeignKey> $foreignKeys + * + * @return \Propel\Generator\Model\ForeignKey|null + */ + public function findSyncedRelation(array $foreignKeys): ?ForeignKey + { + return TableSyncer::findSyncedRelation($this, $foreignKeys); + } +} diff --git a/src/Propel/Generator/Behavior/SyncedTable/SyncedTableBehaviorDeclaration.php b/src/Propel/Generator/Behavior/SyncedTable/SyncedTableBehaviorDeclaration.php new file mode 100644 index 000000000..c638c2963 --- /dev/null +++ b/src/Propel/Generator/Behavior/SyncedTable/SyncedTableBehaviorDeclaration.php @@ -0,0 +1,362 @@ + '', + static::PARAMETER_KEY_SYNCED_PHPNAME => null, + static::PARAMETER_KEY_ADD_PK => null, + static::PARAMETER_KEY_SYNC => 'true', + static::PARAMETER_KEY_FOREIGN_KEYS => null, + static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_RELATIONS => 'false', + static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_CONSTRAINTS => 'false', + static::PARAMETER_KEY_SYNC_INDEXES => 'false', + static::PARAMETER_KEY_SYNC_UNIQUE_AS => null, + static::PARAMETER_KEY_RELATION => null, + static::PARAMETER_KEY_IGNORE_COLUMNS => null, + static::PARAMETER_KEY_EMPTY_ACCESSOR_COLUMNS => null, + static::PARAMETER_KEY_SYNC_PK_ONLY => 'false', + ]; + } + + /** + * @return string|null + */ + public function getSyncedTableName(): ?string + { + return $this->getParameter(static::PARAMETER_KEY_SYNCED_TABLE); + } + + /** + * @return array + */ + public function getTableAttributes(): array + { + $val = $this->parameters[static::PARAMETER_KEY_TABLE_ATTRIBUTES] ?? null; + + return $val ? reset($val) : []; + } + + /** + * @return string|null + */ + public function getSyncedTablePhpName(): ?string + { + return $this->getParameter(static::PARAMETER_KEY_SYNCED_PHPNAME); + } + + /** + * @return string|null + */ + public function addPkAs(): ?string + { + $val = $this->getParameterTrueOrValue(static::PARAMETER_KEY_ADD_PK, false); + + return $val === true ? 'id' : $val; + } + + /** + * @return array|null + */ + public function getColmns(): ?array + { + return $this->parameters[static::PARAMETER_KEY_COLUMNS] ?? []; + } + + /** + * @return array + */ + public function getForeignKeys(): array + { + return $this->parameters[static::PARAMETER_KEY_FOREIGN_KEYS] ?? []; + } + + /** + * @return bool + */ + public function isSync(): bool + { + return $this->getParameterBool(static::PARAMETER_KEY_SYNC, false); + } + + /** + * @return string|bool + */ + public function useColumnPrefix() + { + return $this->getParameterTrueOrValue(static::PARAMETER_KEY_COLUMN_PREFIX, false); + } + + /** + * @return bool + */ + public function isSyncIndexes(): bool + { + return $this->getParameterBool(static::PARAMETER_KEY_SYNC_INDEXES, false); + } + + /** + * @return string|null + */ + public function getSyncUniqueIndexAs(): ?string + { + return $this->getParameter(static::PARAMETER_KEY_SYNC_UNIQUE_AS); + } + + /** + * @return array|true|null + */ + public function getRelation() + { + $val = $this->getParameterTrueOrValue(static::PARAMETER_KEY_RELATION); + + return is_array($val) ? $val[0] : $val; // unwrap param-list-item + } + + /** + * @return bool + */ + public function isInheritForeignKeyRelations(): bool + { + return $this->getParameterBool(static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_RELATIONS, false); + } + + /** + * @return bool + */ + public function isInheritForeignKeyConstraints(): bool + { + return $this->getParameterBool(static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_CONSTRAINTS, false); + } + + /** + * @return array + */ + public function getIgnoredColumnNames(): array + { + return $this->getParameterCsv(static::PARAMETER_KEY_IGNORE_COLUMNS, []); + } + + /** + * @return array|null + */ + public function getEmptyAccessorColumnNames(): ?array + { + $val = $this->getParameterTrueOrCsv(static::PARAMETER_KEY_EMPTY_ACCESSOR_COLUMNS); + + return ($val === true) ? $this->getIgnoredColumnNames() : $val; + } + + /** + * @return bool + */ + public function isSyncPkOnly(): bool + { + return $this->getParameterBool(static::PARAMETER_KEY_SYNC_PK_ONLY, false); + } + + /** + * @return string + */ + public function onSkipSql(): string + { + $val = strtolower($this->getParameter(static::PARAMETER_KEY_ON_SKIP_SQL, 'omit')); + + return in_array($val, ['ignore', 'inherit', 'omit']) ? $val : 'omit'; + } + + /** + * @return bool + */ + public function inheritSkipSql(): bool + { + return $this->onSkipSql() === 'inherit'; + } + + /** + * @return bool + */ + public function omitOnSkipSql(): bool + { + return $this->onSkipSql() === 'omit'; + } + + /** + * @throws \Propel\Generator\Behavior\SyncedTable\SyncedTableException + * + * @return array|null + */ + public function getTableInheritance() + { + $val = $this->parameters[static::PARAMETER_KEY_INHERIT_FROM_TABLE] ?? null; + if ($val === null) { + return null; + } + if (is_string($val)) { + return ['source_table' => $val]; + } + + $val = reset($val); // need to unwrap parameter-list + if (empty($val['source_table'])) { + $format = 'Array input to parameter "%s" requires a table name in '; + $msg = sprintf($format, static::PARAMETER_KEY_INHERIT_FROM_TABLE, 'source_table'); + + throw new SyncedTableException($this, $msg); + } + + return $val; + } +} diff --git a/src/Propel/Generator/Behavior/SyncedTable/SyncedTableException.php b/src/Propel/Generator/Behavior/SyncedTable/SyncedTableException.php new file mode 100644 index 000000000..0d8606920 --- /dev/null +++ b/src/Propel/Generator/Behavior/SyncedTable/SyncedTableException.php @@ -0,0 +1,29 @@ +getName()}' on table '{$behavior->getTable()->getName()}': "; + parent::__construct($messageHead . $message, $code, $previous); + } +} diff --git a/src/Propel/Generator/Behavior/SyncedTable/TableSyncer/TableSyncer.php b/src/Propel/Generator/Behavior/SyncedTable/TableSyncer/TableSyncer.php new file mode 100644 index 000000000..17b955d2a --- /dev/null +++ b/src/Propel/Generator/Behavior/SyncedTable/TableSyncer/TableSyncer.php @@ -0,0 +1,577 @@ +config = $config; + } + + /** + * @param \Propel\Generator\Behavior\SyncedTable\TableSyncer\TableSyncerConfigInterface $config + * @param \Propel\Generator\Model\Table $sourceTable + * + * @return \Propel\Generator\Model\Table + */ + public static function getSyncedTable(TableSyncerConfigInterface $config, Table $sourceTable): Table + { + return (new self($config))->buildSyncedTable($sourceTable); + } + + /** + * @param \Propel\Generator\Model\Table $sourceTable + * + * @return \Propel\Generator\Model\Table + */ + protected function buildSyncedTable(Table $sourceTable): Table + { + $database = $sourceTable->getDatabase(); + $syncedTableName = $this->config->resolveSyncedTableName(); + + $tableExistsInSchema = $database->hasTable($syncedTableName); + + $syncedTable = $tableExistsInSchema ? + $database->getTable($syncedTableName) : + $this->createSyncedTable($sourceTable); + + $this->resolveInheritance($syncedTable); + + if (!$tableExistsInSchema || $this->config->isSync()) { + $this->syncTables($sourceTable, $syncedTable); + } else { + $this->addCustomElements($syncedTable, true); + } + + return $syncedTable; + } + + /** + * @param \Propel\Generator\Model\Table $sourceTable + * + * @return \Propel\Generator\Model\Table + */ + protected function createSyncedTable(Table $sourceTable): Table + { + $database = $sourceTable->getDatabase(); + + $tableAttributes = $this->config->getTableAttributes(); + $defaultAttributes = [ + 'name' => $this->config->resolveSyncedTableName(), + 'phpName' => $this->config->getSyncedTablePhpName(), + 'package' => $sourceTable->getPackage(), + 'schema' => $sourceTable->getSchema(), + 'namespace' => $sourceTable->getNamespace() ? '\\' . $sourceTable->getNamespace() : null, + 'identifierQuoting' => $sourceTable->isIdentifierQuotingEnabled(), + ]; + + if ($this->config->inheritSkipSql()) { + $defaultAttributes['skipSql'] = $sourceTable->isSkipSql(); + } + + return $database->addTable(array_merge($defaultAttributes, $tableAttributes)); + } + + /** + * @param \Propel\Generator\Model\Table $targetTable Table to copy to. + * + * @throws \Propel\Generator\Exception\SchemaException + * + * @return void + */ + protected function resolveInheritance(Table $targetTable): void + { + $inheritance = $this->config->getTableInheritance(); + if (!$inheritance) { + return; + } + $sourceTableName = $inheritance['source_table']; + $sourceTable = $targetTable->getDatabase()->getTable($sourceTableName); + if (!$sourceTable) { + throw new SchemaException("Cannot find source table '$sourceTableName'"); + } + $behavior = new SyncedTableBehavior(); + $behavior->setId('sync_to_table_' . $targetTable->getName()); + $behavior->setTable($sourceTable); + $defaultParameters = [ + SyncedTableBehaviorDeclaration::PARAMETER_KEY_SYNCED_TABLE => $targetTable->getName(), + SyncedTableBehaviorDeclaration::PARAMETER_KEY_SYNC => 'true', + SyncedTableBehaviorDeclaration::PARAMETER_KEY_SYNC_INDEXES => 'true', + SyncedTableBehaviorDeclaration::PARAMETER_KEY_SYNC_UNIQUE_AS => 'unique', + SyncedTableBehaviorDeclaration::PARAMETER_KEY_INHERIT_FOREIGN_KEY_CONSTRAINTS => 'true', + SyncedTableBehaviorDeclaration::PARAMETER_KEY_ON_SKIP_SQL => 'ignore', + ]; + $parameters = array_merge($defaultParameters, $inheritance); + $behavior->setParameters($parameters); + $behavior->modifyTable(); + InsertCodeBehavior::addToTable($behavior, $targetTable, ['parentClass' => $sourceTable]); + } + + /** + * @param \Propel\Generator\Model\Table $sourceTable + * @param \Propel\Generator\Model\Table $syncedTable + * + * @return \Propel\Generator\Model\Table + */ + protected function syncTables(Table $sourceTable, Table $syncedTable): Table + { + $columns = $sourceTable->getColumns(); + $ignoreColumnNames = $this->resolveIgnoredColumnNames($sourceTable); + $this->syncColumns($syncedTable, $columns, $ignoreColumnNames); + + $createRelationParam = $this->config->getRelation(); + if ($createRelationParam) { + $this->addForeignKeyRelationToSyncedTable($syncedTable, $sourceTable, $createRelationParam); + } + + $this->addCustomElements($syncedTable, false); + + $inheritFkRelations = $this->config->isInheritForeignKeyRelations(); + $inheritFkConstraints = $this->config->isInheritForeignKeyConstraints(); + if ($inheritFkRelations || $inheritFkConstraints) { + $foreignKeys = $sourceTable->getForeignKeys(); + $this->syncForeignKeys($syncedTable, $foreignKeys, $inheritFkConstraints, $ignoreColumnNames); + } + + if ($this->config->isSyncIndexes()) { + $indexes = $sourceTable->getIndices(); + $platform = $sourceTable->getDatabase()->getPlatform(); + $renameIndexes = $this->isDistinctiveIndexNameRequired($platform); + $this->syncIndexes($syncedTable, $indexes, $renameIndexes, $ignoreColumnNames); + } + + $syncUniqueAs = $this->config->getSyncUniqueIndexAs(); + if ($syncUniqueAs) { + $asIndex = $syncUniqueAs !== 'unique'; + $uniqueIndexes = $sourceTable->getUnices(); + $this->syncUniqueIndexes($asIndex, $syncedTable, $uniqueIndexes, $ignoreColumnNames); + } + + $this->reapplyTableBehaviors($sourceTable); + + return $syncedTable; + } + + /** + * @param \Propel\Generator\Model\Table $sourceTable + * + * @return array + */ + protected function resolveIgnoredColumnNames(Table $sourceTable): array + { + $ignoreColumnNames = $this->config->getIgnoredColumnNames(); + if (!$this->config->isSyncPkOnly()) { + return $ignoreColumnNames; + } + $nonPkColumns = array_filter($sourceTable->getColumns(), fn (Column $column) => !$column->isPrimaryKey()); + $nonPkColumnNames = array_map(fn (Column $column) => $column->getName(), $nonPkColumns); + + return array_unique(array_merge($ignoreColumnNames, $nonPkColumnNames)); + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * @param bool $tableExistsInSchema + * + * @return void + */ + protected function addCustomElements(Table $syncedTable, bool $tableExistsInSchema) + { + $this->addPkColumn($syncedTable); + $this->addColumnsFromParameter($syncedTable); + $this->addCustomForeignKeysToSyncedTable($syncedTable); + $this->config->addTableElements($syncedTable, $tableExistsInSchema); + } + + /** + * @param \Propel\Generator\Model\Table $table + * + * @return void + */ + protected function addColumnsFromParameter(Table $table): void + { + $columnData = $this->config->getColmns(); + if (!$columnData) { + return; + } + array_map([$table, 'addColumn'], $columnData); + } + + /** + * Allows inheriting classes to add columns. + * + * @param \Propel\Generator\Model\Table $table + * + * @return void + */ + protected function addPkColumn(Table $table) + { + $idColumnName = $this->config->addPkAs(); + if (!$idColumnName) { + return; + } + static::addColumnIfNotExists($table, $idColumnName, [ + 'type' => 'INTEGER', + 'required' => 'true', + 'primaryKey' => 'true', + 'autoIncrement' => 'true', + ]); + foreach ($table->getPrimaryKey() as $pkColumn) { + if ($pkColumn->getName() === $idColumnName) { + continue; + } + $pkColumn->setPrimaryKey(false); + } + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * @param \Propel\Generator\Model\Table $sourceTable + * @param array|bool $createRelationParam + * + * @throws \Propel\Generator\Exception\SchemaException If columns are not found. + * + * @return void + */ + protected function addForeignKeyRelationToSyncedTable(Table $syncedTable, Table $sourceTable, $createRelationParam): void + { + $fk = new ForeignKey(); + $syncedTable->addForeignKey($fk); + $attributes = [ + 'foreignTable' => $sourceTable->getOriginCommonName(), + 'foreignSchema' => $sourceTable->getSchema(), + 'skipSql' => is_array($createRelationParam) ? 'false' : 'true', + static::ATTRIBUTE_KEY_SYNCED_THROUGH => $this->config->getId(), // allows to retrieve relation + ]; + + if (is_array($createRelationParam)) { + $attributes = array_merge($attributes, $createRelationParam); + } + $fk->loadMapping($attributes); + + foreach ($sourceTable->getPrimaryKey() as $sourceColumn) { + $syncedColumnName = $this->getPrefixedColumnName($sourceColumn->getName()); + $syncedColumn = $syncedTable->getColumn($syncedColumnName); + if (!$syncedColumn) { + throw new SchemaException('Synced table behavior cannot create relation: primary key column of source table is missing on synced table: ' . $syncedColumnName); + } + $fk->addReference($syncedColumn, $sourceColumn); + } + } + + /** + * @param \Propel\Generator\Behavior\SyncedTable\TableSyncer\TableSyncerConfigInterface $config + * @param array<\Propel\Generator\Model\ForeignKey> $foreignKeys + * + * @throws \RuntimeException If more than one relation is found. + * + * @return \Propel\Generator\Model\ForeignKey|null + */ + public static function findSyncedRelation(TableSyncerConfigInterface $config, array $foreignKeys): ?ForeignKey + { + $filter = fn (ForeignKey $fk) => $fk->getAttribute(static::ATTRIBUTE_KEY_SYNCED_THROUGH) === $config->getId(); + $matches = array_filter($foreignKeys, $filter); + if (count($matches) > 1) { + throw new RuntimeException('More than one relation identified through '); + } + + return reset($matches) ?: null; + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * + * @return void + */ + protected function addCustomForeignKeysToSyncedTable(Table $syncedTable) + { + $foreignKeys = $this->config->getForeignKeys(); + foreach ($foreignKeys as $fkData) { + $this->createForeignKeyFromParameters($syncedTable, $fkData); + } + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * @param array<\Propel\Generator\Model\Column> $columns + * @param array $ignoreColumnNames + * + * @return void + */ + protected function syncColumns(Table $syncedTable, array $columns, array $ignoreColumnNames) + { + foreach ($columns as $sourceColumn) { + $syncedColumnName = $this->getPrefixedColumnName($sourceColumn->getName()); + if (in_array($sourceColumn->getName(), $ignoreColumnNames) || $syncedTable->hasColumn($syncedColumnName)) { + continue; + } + $syncedColumn = clone $sourceColumn; + $syncedColumn->setName($syncedColumnName); + $syncedColumn->setPhpName(null); + $syncedColumn->clearReferrers(); + $syncedColumn->clearInheritanceList(); + $syncedColumn->setAutoIncrement(false); + $syncedTable->addColumn($syncedColumn); + } + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * @param array<\Propel\Generator\Model\ForeignKey> $foreignKeys + * @param bool $inheritConstraints + * @param array $ignoreColumnNames + * + * @return void + */ + protected function syncForeignKeys(Table $syncedTable, array $foreignKeys, bool $inheritConstraints, array $ignoreColumnNames) + { + foreach ($foreignKeys as $originalForeignKey) { + if ( + $syncedTable->containsForeignKeyWithSameName($originalForeignKey) + || array_intersect($originalForeignKey->getLocalColumns(), $ignoreColumnNames) + ) { + continue; + } + $syncedForeignKey = clone $originalForeignKey; + $syncedForeignKey->setSkipSql(!$inheritConstraints); + $this->prefixForeignKeyColumnNames($syncedForeignKey); + $syncedTable->addForeignKey($syncedForeignKey); + } + } + + /** + * @param \Propel\Generator\Model\ForeignKey $fk + * + * @return void + */ + protected function prefixForeignKeyColumnNames(ForeignKey $fk): void + { + if (!$this->config->getColumnPrefix()) { + return; + } + $mapping = $fk->getColumnObjectsMapping(); + $fk->clearReferences(); + foreach ($mapping as $def) { + $fk->addReference([ + 'local' => $this->getPrefixedColumnName($def['local']->getName()), + 'foreign' => $def['foreign'] ? $def['foreign']->getName() : null, + 'value' => $def['value'], + ]); + } + } + + /** + * @param \Propel\Generator\Model\Table $syncedTable + * @param array<\Propel\Generator\Model\Index> $indexes + * @param bool $rename + * @param array $ignoreColumnNames + * + * @return void + */ + protected function syncIndexes(Table $syncedTable, array $indexes, bool $rename, array $ignoreColumnNames) + { + foreach ($indexes as $originalIndex) { + $index = clone $originalIndex; + + if (!$this->removeColumnsFromIndex($index, $ignoreColumnNames)) { + continue; + } + + if ($rename) { + // by removing the name, Propel will generate a unique name based on table and columns + $index->setName(null); + } + + $this->prefixIndexColumnNames($index, $syncedTable); + + if ($syncedTable->hasIndex($index->getName())) { + continue; + } + $syncedTable->addIndex($index); + } + } + + /** + * @param \Propel\Generator\Model\Index $index + * @param \Propel\Generator\Model\Table $table + * + * @return void + */ + protected function prefixIndexColumnNames(Index $index, Table $table): void + { + if (!$this->config->getColumnPrefix()) { + return; + } + $updatedColumnNames = array_map([$this, 'getPrefixedColumnName'], $index->getColumns()); + $columns = array_map([$table, 'getColumn'], $updatedColumnNames); + $index->setColumns($columns); + } + + /** + * @param \Propel\Generator\Model\Index $index + * @param array $ignoreColumnNames + * + * @return \Propel\Generator\Model\Index|null Returns null if the index has no remaining columns. + */ + protected function removeColumnsFromIndex(Index $index, array $ignoreColumnNames): ?Index + { + $ignoredColumnsInIndex = array_intersect($index->getColumns(), $ignoreColumnNames); + if (!$ignoredColumnsInIndex) { + return $index; + } + if (count($ignoredColumnsInIndex) === count($index->getColumns())) { + return null; + } + $indexColumns = array_filter($index->getColumnObjects(), fn (Column $col) => !in_array($col->getName(), $ignoredColumnsInIndex)); + $index->setColumns($indexColumns); + + return $index; + } + + /** + * Create regular indexes from unique indexes on the given synced table. + * + * The synced table cannot use unique indexes, as even unique data on the + * source table can be syncedd several times. + * + * @param bool $asIndex + * @param \Propel\Generator\Model\Table $syncedTable + * @param array<\Propel\Generator\Model\Unique> $uniqueIndexes + * @param array $ignoreColumnNames + * + * @return void + */ + protected function syncUniqueIndexes(bool $asIndex, Table $syncedTable, array $uniqueIndexes, array $ignoreColumnNames) + { + $indexClass = $asIndex ? Index::class : Unique::class; + foreach ($uniqueIndexes as $unique) { + if (array_intersect($unique->getColumns(), $ignoreColumnNames)) { + continue; + } + $index = new $indexClass(); + $index->setTable($syncedTable); + foreach ($unique->getColumns() as $columnName) { + $columnDef = [ + 'name' => $this->getPrefixedColumnName($columnName), + 'size' => $unique->getColumnSize($columnName), + ]; + $index->addColumn($columnDef); + } + + $existingIndexes = $asIndex ? $syncedTable->getIndices() : $syncedTable->getUnices(); + $existingIndexNames = array_map(fn ($index) => $index->getName(), $existingIndexes); + if (in_array($index->getName(), $existingIndexNames)) { + continue; + } + $asIndex ? $syncedTable->addIndex($index) : $syncedTable->addUnique($index); + } + } + + /** + * @param \Propel\Generator\Model\Table $table + * + * @return void + */ + protected function reapplyTableBehaviors(Table $table) + { + $behaviors = $table->getDatabase()->getBehaviors(); + foreach ($behaviors as $behavior) { + if ($behavior instanceof SyncedTableBehavior) { + continue; + } + $behavior->modifyDatabase(); + } + } + + /** + * @psalm-param array{name?: string, localColumn: string, foreignTable: string, foreignColumn: string, relationOnly?: string} $fkData + * + * @param \Propel\Generator\Model\Table $syncedTable + * @param array $fkData + * + * @return void + */ + protected function createForeignKeyFromParameters(Table $syncedTable, array $fkData): void + { + $fk = new ForeignKey($fkData['name'] ?? null); + $fk->addReference($fkData['localColumn'], $fkData['foreignColumn']); + $syncedTable->addForeignKey($fk); + $fk->loadMapping($fkData); + } + + /** + * @param \Propel\Generator\Model\Table $table + * @param string $columnName + * @param array $columnDefinition + * + * @return \Propel\Generator\Model\Column + */ + public static function addColumnIfNotExists(Table $table, string $columnName, array $columnDefinition): Column + { + if ($table->hasColumn($columnName)) { + return $table->getColumn($columnName); + } + $columnDefinitionWithName = array_merge(['name' => $columnName], $columnDefinition); + + return $table->addColumn($columnDefinitionWithName); + } + + /** + * @param \Propel\Generator\Platform\PlatformInterface|null $platform + * + * @return bool + */ + protected function isDistinctiveIndexNameRequired(?PlatformInterface $platform): bool + { + return $platform instanceof PgsqlPlatform || $platform instanceof SqlitePlatform; + } + + /** + * @param string $columnName + * + * @return string + */ + protected function getPrefixedColumnName(string $columnName): string + { + return $this->config->getColumnPrefix() . $columnName; + } +} diff --git a/src/Propel/Generator/Behavior/SyncedTable/TableSyncer/TableSyncerConfigInterface.php b/src/Propel/Generator/Behavior/SyncedTable/TableSyncer/TableSyncerConfigInterface.php new file mode 100644 index 000000000..9bae7a2b9 --- /dev/null +++ b/src/Propel/Generator/Behavior/SyncedTable/TableSyncer/TableSyncerConfigInterface.php @@ -0,0 +1,122 @@ +parameters[$name] ?? null; + + return is_string($val) ? trim($val) : $defaultValue; // means empty space (' ') cannot be a value, seems ok. + } + + /** + * @param string $parameterName + * @param bool|null $defaultValue + * + * @return bool|null + */ + public function getParameterBool(string $parameterName, ?bool $defaultValue = null): ?bool + { + $val = $this->getParameter($parameterName); + + return !$val ? $defaultValue : in_array(strtolower($val), ['true', '1']); + } + + /** + * @param string $parameterName + * @param int|null $defaultValue + * + * @throws \Propel\Generator\Exception\SchemaException + * + * @return int|null + */ + public function getParameterInt(string $parameterName, ?int $defaultValue = null): ?int + { + $val = $this->getParameter($parameterName); + if ($val === null) { + return $defaultValue; + } + if (!is_numeric($val)) { + throw new SchemaException("Parameter $parameterName should be numeric, but is '$val'"); + } + + return (int)$val; + } + + /** + * @param string $parameterName + * @param array|null $defaultValue + * @param callable|null $mapper + * + * @return array|null + */ + public function getParameterCsv(string $parameterName, ?array $defaultValue = [], ?callable $mapper = null): ?array + { + $valString = $this->getParameter($parameterName); + if (!$valString) { + return $defaultValue; + } + $valList = $this->explodeCsv($valString); + + return $mapper ? array_map($mapper, $valList) : $valList; + } + + /** + * @param string $parameterName + * @param mixed $defaultValue + * + * @return mixed|true + */ + public function getParameterTrueOrValue(string $parameterName, $defaultValue = null) + { + $val = $this->parameters[$parameterName] ?? null; + $isTrue = is_string($val) && $this->getParameterBool($parameterName); + + return $isTrue ?: ($this->parameters[$parameterName] ?? $defaultValue); + } + + /** + * @param string $parameterName + * @param array|null $defaultValue + * @param callable|null $mapper + * + * @return array|true|null + */ + public function getParameterTrueOrCsv(string $parameterName, ?array $defaultValue = null, $mapper = null) + { + $val = $this->getParameterBool($parameterName); + + return $val ?: $this->getParameterCsv($parameterName, $defaultValue, $mapper); + } + + /** + * @param string $stringValue + * + * @return array|null + */ + protected function explodeCsv(string $stringValue): ?array + { + $stringValue = trim($stringValue); + + return trim($stringValue) ? array_map('trim', explode(',', $stringValue)) : null; + } +} diff --git a/src/Propel/Generator/Behavior/Util/InsertCodeBehavior.php b/src/Propel/Generator/Behavior/Util/InsertCodeBehavior.php new file mode 100644 index 000000000..899ccf8ae --- /dev/null +++ b/src/Propel/Generator/Behavior/Util/InsertCodeBehavior.php @@ -0,0 +1,265 @@ + + */ + protected $codeForHooks = []; + + /** + * Add this behavior to a table. + * + * @param \Propel\Generator\Model\Behavior $insertingBehavior + * @param \Propel\Generator\Model\Table $table + * @param array $codeForHooks + * + * @return \Propel\Generator\Behavior\Util\InsertCodeBehavior|self + */ + public static function addToTable(Behavior $insertingBehavior, Table $table, array $codeForHooks): self + { + $behavior = new self(); + $behavior->setup($insertingBehavior, $table, $codeForHooks); + + return $behavior; + } + + /** + * @param \Propel\Generator\Model\Behavior $insertingBehavior + * @param \Propel\Generator\Model\Table $table + * @param array $codeForHooks + * + * @return void + */ + public function setup(Behavior $insertingBehavior, Table $table, array $codeForHooks) + { + $id = "insert_code_from_{$insertingBehavior->getName()}_behavior_on_table_{$insertingBehavior->getTable()->getName()}"; + $this->setId($id); + $this->setDatabase($table->getDatabase()); + $this->setTable($table); + $this->codeForHooks = $codeForHooks; + $table->addBehavior($this); + } + + /** + * @see \Propel\Generator\Model\Behavior::allowMultiple() + * + * @return bool + */ + public function allowMultiple(): bool + { + return true; + } + + // object builder hooks + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function objectAttributes(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('objectAttributes', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function objectMethods(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('objectMethods', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function objectCall(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('objectCall', $objectBuilder); + } + + /** + * @see \Propel\Generator\Model\Behavior::objectFilter() + * + * @param string $script + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function objectFilter(string &$script, ObjectBuilder $objectBuilder) + { + $fun = $this->codeForHooks['objectFilter'] ?? null; + if (!$fun) { + return; + } + if (!is_callable($fun)) { + throw new InvalidArgumentException("Value in 'objectFilter' has to be callable."); + } + + $script = $fun($script, $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function preInsert(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('preInsert', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function postInsert(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('postInsert', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function preUpdate(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('preUpdate', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function postUpdate(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('postUpdate', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function preDelete(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('preDelete', $objectBuilder); + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + public function postDelete(ObjectBuilder $objectBuilder): string + { + return $this->resolveCode('postDelete', $objectBuilder); + } + + /** + * @param string $hook + * @param \Propel\Generator\Builder\Om\ObjectBuilder $objectBuilder + * + * @return string + */ + protected function resolveCode(string $hook, ObjectBuilder $objectBuilder): string + { + $code = $this->codeForHooks[$hook] ?? null; + if (!$code) { + return ''; + } + + return is_callable($code) ? $code($objectBuilder) : $code; + } + + // parentClass + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder|\Propel\Generator\Builder\Om\QueryBuilder $builder + * + * @return string|null + */ + public function parentClass($builder): ?string + { + $parentTable = $this->resolveTableFromHookContent($builder, 'parentClass'); + if (!$parentTable) { + return null; + } + $stubBuilder = $this->buildStubBuilder($builder, $parentTable); + + return $stubBuilder ? $builder->declareClassFromBuilder($stubBuilder, true) : null; + } + + /** + * @param \Propel\Generator\Builder\Om\AbstractOMBuilder $builder + * @param string $hook + * + * @throws \InvalidArgumentException If the table cannot be resolved + * + * @return \Propel\Generator\Model\Table|null + */ + protected function resolveTableFromHookContent(AbstractOMBuilder $builder, string $hook): ?Table + { + $parentTable = $this->codeForHooks[$hook] ?? null; + if (is_string($parentTable)) { + $resolvedTable = $builder->getDatabase()->getTable($parentTable); + if (!$resolvedTable) { + throw new InvalidArgumentException("Could not find table in '$hook': '$parentTable'"); + } + + return $resolvedTable; + } + if ($parentTable && !($parentTable instanceof Table)) { + throw new InvalidArgumentException("Value in '$hook' has to be an instance of Table"); + } + + return $parentTable; + } + + /** + * @param \Propel\Generator\Builder\Om\ObjectBuilder|\Propel\Generator\Builder\Om\QueryBuilder $builder + * @param \Propel\Generator\Model\Table $table + * + * @return \Propel\Generator\Builder\Om\AbstractOMBuilder|null + */ + protected function buildStubBuilder($builder, Table $table): ?AbstractOMBuilder + { + switch (get_class($builder)) { + case ObjectBuilder::class: + return $builder->getNewStubObjectBuilder($table); + case QueryBuilder::class: + return $builder->getNewStubQueryBuilder($table); + default: + return null; + } + } +} diff --git a/src/Propel/Generator/Behavior/Versionable/VersionableBehavior.php b/src/Propel/Generator/Behavior/Versionable/VersionableBehavior.php index 62b9af2b3..aed4e862f 100644 --- a/src/Propel/Generator/Behavior/Versionable/VersionableBehavior.php +++ b/src/Propel/Generator/Behavior/Versionable/VersionableBehavior.php @@ -8,7 +8,8 @@ namespace Propel\Generator\Behavior\Versionable; -use Propel\Generator\Model\Behavior; +use Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior; +use Propel\Generator\Behavior\SyncedTable\TableSyncer\TableSyncer; use Propel\Generator\Model\Column; use Propel\Generator\Model\ForeignKey; use Propel\Generator\Model\Table; @@ -18,24 +19,33 @@ * * @author Francois Zaninotto */ -class VersionableBehavior extends Behavior +class VersionableBehavior extends SyncedTableBehavior { /** - * Default parameters value + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::DEFAULT_SYNCED_TABLE_SUFFIX * - * @var array + * @var string DEFAULT_SYNCED_TABLE_SUFFIX */ - protected $parameters = [ - 'version_column' => 'version', - 'version_table' => '', - 'log_created_at' => 'false', - 'log_created_by' => 'false', - 'log_comment' => 'false', - 'version_created_at_column' => 'version_created_at', - 'version_created_by_column' => 'version_created_by', - 'version_comment_column' => 'version_comment', - 'indices' => 'false', - ]; + protected const DEFAULT_SYNCED_TABLE_SUFFIX = '_version'; + + /** + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::PARAMETER_KEY_SYNCED_TABLE + * + * @var string + */ + public const PARAMETER_KEY_SYNCED_TABLE = 'version_table'; + + /** + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::PARAMETER_KEY_SYNCED_PHPNAME + * + * @var string + */ + public const PARAMETER_KEY_SYNCED_PHPNAME = 'version_phpname'; + + /** + * @var string + */ + public const PARAMETER_KEY_SYNC_INDEXES = 'indices'; /** * @var \Propel\Generator\Model\Table @@ -58,73 +68,101 @@ class VersionableBehavior extends Behavior protected $tableModificationOrder = 80; /** + * @see \Propel\Generator\Behavior\SyncedTable\SyncedTableBehavior::getDefaultParameters() + * + * @return array + */ + protected function getDefaultParameters(): array + { + return [ + 'version_column' => 'version', + static::PARAMETER_KEY_SYNCED_TABLE => '', + static::PARAMETER_KEY_SYNCED_PHPNAME => null, + static::PARAMETER_KEY_SYNC => 'false', + static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_RELATIONS => 'false', + static::PARAMETER_KEY_INHERIT_FOREIGN_KEY_CONSTRAINTS => 'false', + static::PARAMETER_KEY_FOREIGN_KEYS => null, + static::PARAMETER_KEY_SYNC_INDEXES => 'false', + static::PARAMETER_KEY_SYNC_UNIQUE_AS => null, + static::PARAMETER_KEY_RELATION => [['onDelete' => 'cascade']], + static::PARAMETER_KEY_ON_SKIP_SQL => 'inherit', + 'log_created_at' => 'false', + 'log_created_by' => 'false', + 'log_comment' => 'false', + 'version_created_at_column' => 'version_created_at', + 'version_created_by_column' => 'version_created_by', + 'version_comment_column' => 'version_comment', + ]; + } + + /** + * @return \Propel\Generator\Model\Table + */ + public function getVersionTable(): Table + { + return $this->syncedTable; + } + + /** + * @param \Propel\Generator\Model\Table $table + * * @return void */ - public function modifyDatabase(): void + protected function addBehaviorToTable(Table $table): void { - foreach ($this->getDatabase()->getTables() as $table) { - if ($table->hasBehavior($this->getId())) { - // don't add the same behavior twice - continue; - } - if (property_exists($table, 'isVersionTable')) { - // don't add the behavior to version tables - continue; - } - $b = clone $this; - $table->addBehavior($b); + if (property_exists($table, 'isVersionTable')) { + // don't add the behavior to version tables + return; } + parent::addBehaviorToTable($table); } /** - * @return void + * @return string|null */ - public function modifyTable(): void + public function getSyncedTablePhpName(): ?string { - $this->addVersionColumn(); - $this->addLogColumns(); - $this->addVersionTable(); - $this->addForeignKeyVersionColumns(); + // required for BC + return parent::getSyncedTablePhpName() ?? $this->getTable()->getPhpName() . 'Version'; } /** * @return void */ - protected function addVersionColumn(): void + public function modifyTable(): void { - $table = $this->getTable(); - // add the version column - if (!$table->hasColumn($this->getParameter('version_column'))) { - $table->addColumn([ - 'name' => $this->getParameter('version_column'), - 'type' => 'INTEGER', - 'default' => 0, - ]); - } + $this->addColumnsToSourceTable(); + parent::modifyTable(); + $this->addForeignKeyVersionColumns(); } /** * @return void */ - protected function addLogColumns(): void + protected function addColumnsToSourceTable(): void { $table = $this->getTable(); - if ($this->getParameter('log_created_at') === 'true' && !$table->hasColumn($this->getParameter('version_created_at_column'))) { - $table->addColumn([ - 'name' => $this->getParameter('version_created_at_column'), + + $this->addColumnFromParameterIfNotExists($table, 'version_column', [ + 'type' => 'INTEGER', + 'default' => 0, + ]); + + if ($this->getParameter('log_created_at') === 'true') { + $this->addColumnFromParameterIfNotExists($table, 'version_created_at_column', [ 'type' => 'TIMESTAMP', ]); } - if ($this->getParameter('log_created_by') === 'true' && !$table->hasColumn($this->getParameter('version_created_by_column'))) { - $table->addColumn([ - 'name' => $this->getParameter('version_created_by_column'), + + if ($this->getParameter('log_created_by') === 'true') { + $this->addColumnFromParameterIfNotExists($table, 'version_created_by_column', [ 'type' => 'VARCHAR', 'size' => 100, ]); } - if ($this->getParameter('log_comment') === 'true' && !$table->hasColumn($this->getParameter('version_comment_column'))) { - $table->addColumn([ - 'name' => $this->getParameter('version_comment_column'), + + if ($this->getParameter('log_comment') === 'true') { + $this->addColumnFromParameterIfNotExists($table, 'version_comment_column', [ 'type' => 'VARCHAR', 'size' => 255, ]); @@ -132,68 +170,24 @@ protected function addLogColumns(): void } /** + * @param \Propel\Generator\Model\Table $syncedTable + * @param bool $tableExistsInSchema + * * @return void */ - protected function addVersionTable(): void + public function addTableElements(Table $syncedTable, bool $tableExistsInSchema): void { - $table = $this->getTable(); - $database = $table->getDatabase(); - $versionTableName = $this->getParameter('version_table') ?: ($table->getOriginCommonName() . '_version'); - if (!$database->hasTable($versionTableName)) { - // create the version table - $versionTable = $database->addTable([ - 'name' => $versionTableName, - 'phpName' => $this->getVersionTablePhpName(), - 'package' => $table->getPackage(), - 'schema' => $table->getSchema(), - 'namespace' => $table->getNamespace() ? '\\' . $table->getNamespace() : null, - 'skipSql' => $table->isSkipSql(), - 'identifierQuoting' => $table->isIdentifierQuotingEnabled(), - ]); - $versionTable->isVersionTable = true; - // every behavior adding a table should re-execute database behaviors - foreach ($database->getBehaviors() as $behavior) { - $behavior->modifyDatabase(); - } - // copy all the columns - foreach ($table->getColumns() as $column) { - $columnInVersionTable = clone $column; - $columnInVersionTable->clearInheritanceList(); - if ($columnInVersionTable->hasReferrers()) { - $columnInVersionTable->clearReferrers(); - } - if ($columnInVersionTable->isAutoincrement()) { - $columnInVersionTable->setAutoIncrement(false); - } - $versionTable->addColumn($columnInVersionTable); - } - // create the foreign key - $fk = new ForeignKey(); - $fk->setForeignTableCommonName($table->getCommonName()); - $fk->setForeignSchemaName($table->getSchema()); - $fk->setOnDelete('CASCADE'); - $fk->setOnUpdate(null); - $tablePKs = $table->getPrimaryKey(); - foreach ($versionTable->getPrimaryKey() as $key => $column) { - $fk->addReference($column, $tablePKs[$key]); - } - $versionTable->addForeignKey($fk); - - if ($this->getParameter('indices') === 'true') { - foreach ($table->getIndices() as $index) { - $index = clone $index; - $versionTable->addIndex($index); - } - } + parent::addTableElements($syncedTable, $tableExistsInSchema); - // add the version column to the primary key - $versionColumn = $versionTable->getColumn($this->getParameter('version_column')); - $versionColumn->setNotNull(true); - $versionColumn->setPrimaryKey(true); - $this->versionTable = $versionTable; - } else { - $this->versionTable = $database->getTable($versionTableName); + if ($tableExistsInSchema) { + return; } + $syncedTable->isVersionTable = true; + + // add the version column to the primary key + $versionColumn = $syncedTable->getColumn($this->getParameter('version_column')); + $versionColumn->setNotNull(true); + $versionColumn->setPrimaryKey(true); } /** @@ -201,54 +195,29 @@ protected function addVersionTable(): void */ public function addForeignKeyVersionColumns(): void { - $versionTable = $this->versionTable; + $versionTable = $this->syncedTable; foreach ($this->getVersionableFks() as $fk) { $fkVersionColumnName = $fk->getLocalColumnName() . '_version'; - if (!$versionTable->hasColumn($fkVersionColumnName)) { - $versionTable->addColumn([ - 'name' => $fkVersionColumnName, - 'type' => 'INTEGER', - 'default' => 0, - ]); - } + TableSyncer::addColumnIfNotExists($versionTable, $fkVersionColumnName, [ + 'type' => 'INTEGER', + 'default' => 0, + ]); } foreach ($this->getVersionableReferrers() as $fk) { $fkTableName = $fk->getTable()->getName(); $fkIdsColumnName = $fkTableName . '_ids'; - if (!$versionTable->hasColumn($fkIdsColumnName)) { - $versionTable->addColumn([ - 'name' => $fkIdsColumnName, - 'type' => 'ARRAY', - ]); - } + TableSyncer::addColumnIfNotExists($versionTable, $fkIdsColumnName, [ + 'type' => 'ARRAY', + ]); $fkVersionsColumnName = $fkTableName . '_versions'; - if (!$versionTable->hasColumn($fkVersionsColumnName)) { - $versionTable->addColumn([ - 'name' => $fkVersionsColumnName, - 'type' => 'ARRAY', - ]); - } + TableSyncer::addColumnIfNotExists($versionTable, $fkVersionsColumnName, [ + 'type' => 'ARRAY', + ]); } } - /** - * @return \Propel\Generator\Model\Table - */ - public function getVersionTable(): Table - { - return $this->versionTable; - } - - /** - * @return string - */ - public function getVersionTablePhpName(): string - { - return $this->getTable()->getPhpName() . 'Version'; - } - /** * @return list<\Propel\Generator\Model\ForeignKey> */ @@ -297,7 +266,7 @@ public function getReferrerIdsColumn(ForeignKey $fk): ?Column $fkTableName = $fk->getTable()->getName(); $fkIdsColumnName = $fkTableName . '_ids'; - return $this->versionTable->getColumn($fkIdsColumnName); + return $this->syncedTable->getColumn($fkIdsColumnName); } /** @@ -310,7 +279,7 @@ public function getReferrerVersionsColumn(ForeignKey $fk): ?Column $fkTableName = $fk->getTable()->getName(); $fkIdsColumnName = $fkTableName . '_versions'; - return $this->versionTable->getColumn($fkIdsColumnName); + return $this->syncedTable->getColumn($fkIdsColumnName); } /** diff --git a/src/Propel/Generator/Model/Behavior.php b/src/Propel/Generator/Model/Behavior.php index 0859dc0ab..5b6b60a51 100644 --- a/src/Propel/Generator/Model/Behavior.php +++ b/src/Propel/Generator/Model/Behavior.php @@ -275,7 +275,7 @@ public function getParameter(string $name) */ public function parameterHasValue(string $paramName, $value): bool { - return $this->parameters[$paramName] === $value; + return ($this->parameters[$paramName] ?? null) === $value; } /** diff --git a/tests/Propel/Tests/Generator/Behavior/Archivable/ArchivableBehaviorSyncTest.php b/tests/Propel/Tests/Generator/Behavior/Archivable/ArchivableBehaviorSyncTest.php deleted file mode 100644 index 107823423..000000000 --- a/tests/Propel/Tests/Generator/Behavior/Archivable/ArchivableBehaviorSyncTest.php +++ /dev/null @@ -1,362 +0,0 @@ - - - - ', - // archive table input columns - '', - // auxiliary schema data - '', - // archive output columns - ' - - - - ', - ], [ - // description - 'Cannot override columns declared on archive table', - //additional behavior parameters - '', - // source table columns: column with size 8 - '', - // archive table input columns: column with size 999 - '', - // auxiliary schema data - '', - // archive output columns - '', - ], [ - // description - 'Should sync index', - //additional behavior parameters - '', - // source table columns: column with index - ' - - - - - ', - // archive table input columns - '', - // auxiliary schema data - '', - // archive output columns - ' - - - - - ', - ], [ - // description - 'Should sync fk column without relation', - //additional behavior parameters - '', - // source table columns: column with fk - ' - - - - - ', - // archive table input columns - '', - // auxiliary schema data - ' - - -
- ', - // archive output columns - '', - ], [ - // description - 'Should sync fk column with relation through parameter', - //additional behavior parameters - '', - // source table columns: column with fk - ' - - - - - ', - // archive table input columns - '', - // auxiliary schema data - ' - - -
- ', - // archive output columns - ' - - - - - ', - ], [ - // description - 'Behavior can override synced FKs', - //additional behavior parameters: inherit fks but override relation "LeName" - ' - - - - - - - - - - ', - // source table columns: column with fk - ' - - - - - ', - // archive table input columns - '', - // auxiliary schema data - ' - - -
- - -
- ', - // archive output columns - ' - - - - - ', - ], [ - // description - 'Behavior cannot override FKs declared on archive table', - //additional behavior parameters: declare fk - ' - - - - - - - - - ', - // source table columns - '', - // archive table input columns: fk conflicting with behavior fk - ' - - - - - ', - // auxiliary schema data - ' - - -
- - -
- ', - // archive output columns: expect exception - EngineException::class, - ], - ]; - } - - /** - * @dataProvider syncTestDataProvider - * - * @param string $message - * @param string $behaviorAdditions - * @param string $sourceTableContentTags - * @param string $archiveTableInputTags - * @param string $auxiliaryTables - * @param string $archiveTableOutputTags - * - * @return void - */ - public function testSync( - string $message, - string $behaviorAdditions, - string $sourceTableContentTags, - string $archiveTableInputTags, - string $auxiliaryTables, - string $archiveTableOutputTags - ) { - // source table: some columns - // archive table: empty - $schema = << - - - - - - $behaviorAdditions - - - $sourceTableContentTags - -
- - $auxiliaryTables - - $archiveTableInputTags
- -EOT; - - // archive table: all columns plus archived_at - $expected = << - - $archiveTableOutputTags -
- - $auxiliaryTables - -EOT; - - if (class_exists($archiveTableOutputTags) && is_subclass_of($archiveTableOutputTags, Exception::class)) { - $this->expectException($archiveTableOutputTags); - } - $this->assertSchemaTableMatches($expected, $schema, 'archive_table', $message); - } - - /** - * @param string $expectedTableXml - * @param string $schema - * @param string $tableName - * @param string|null $message - * - * @return void - */ - protected function assertSchemaTableMatches(string $expectedTableXml, string $schema, string $tableName, ?string $message = null) - { - $expectedSchema = $this->buildSchema($expectedTableXml); - $expectedTable = $expectedSchema->getTable($tableName); - - $actualSchema = $this->buildSchema($schema); - $actualTable = $actualSchema->getTable($tableName); - - $diff = TableComparator::computeDiff($actualTable, $expectedTable); - if ($diff !== false) { - $message = $this->buildTestMessage($message, $diff, $expectedSchema, $actualSchema); - $this->fail($message); - } - $this->expectNotToPerformAssertions(); - } - - /** - * @param string $schema - * - * @return \Propel\Generator\Model\Database - */ - protected function buildSchema(string $schema): Database - { - $builder = new QuickBuilder(); - $builder->setSchema($schema); - - return $builder->getDatabase(); - } - - /** - * @param string $inputMessage - * @param \Propel\Generator\Model\Diff\TableDiff $diff - * @param \Propel\Generator\Model\Database $expectedSchema - * @param \Propel\Generator\Model\Database $actualSchema - * - * @return string - */ - protected function buildTestMessage(string $inputMessage, TableDiff $diff, Database $expectedSchema, Database $actualSchema) - { - $inputMessage ??= ''; - $platform = new MysqlPlatform(); - $sql = $platform->getModifyTableDDL($diff); - - return <<markTestSkipped(); $schema = << @@ -241,7 +240,7 @@ public function testMissingFkParametersThrowsException(string $description, stri $builder = new QuickBuilder(); $builder->setSchema($schema); - $this->expectException(SchemaException::class); + $this->expectException(SyncedTableException::class); $builder->getDatabase(); } diff --git a/tests/Propel/Tests/Generator/Behavior/SyncedTable/EmptyColumnAccessorsBehaviorTest.php b/tests/Propel/Tests/Generator/Behavior/SyncedTable/EmptyColumnAccessorsBehaviorTest.php new file mode 100644 index 000000000..f0d5ef0df --- /dev/null +++ b/tests/Propel/Tests/Generator/Behavior/SyncedTable/EmptyColumnAccessorsBehaviorTest.php @@ -0,0 +1,38 @@ +callMethod($behavior, 'buildAccessorNames', [['a_column', 'le_column']]); + $expected = ['getAColumn', 'setAColumn', 'getLeColumn', 'setLeColumn']; + + $this->assertEqualsCanonicalizing($expected, $accessors); + } +} diff --git a/tests/Propel/Tests/Generator/Behavior/SyncedTable/SyncedTableBehaviorTest.php b/tests/Propel/Tests/Generator/Behavior/SyncedTable/SyncedTableBehaviorTest.php new file mode 100644 index 000000000..b6792adc0 --- /dev/null +++ b/tests/Propel/Tests/Generator/Behavior/SyncedTable/SyncedTableBehaviorTest.php @@ -0,0 +1,991 @@ + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + ', + ], + [ + // description + 'Cannot override columns declared on synced table', + //additional behavior parameters + '', + // source table columns: column with size 8 + '', + // synced table input columns: column with size 999 + '', + // auxiliary schema data + '', + // synced output columns + '', + ], + [ + // description + 'Should sync index if requested', + //additional behavior parameters + '', + // source table columns: column with index + ' + + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + + ', + ], + [ + // description + 'Syncing index can be enabled', + //additional behavior parameters + '', + // source table columns: column with index + ' + + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + + ', + ], + [ + // description + 'Should sync fk column without relation', + //additional behavior parameters + '', + // source table columns: column with fk + ' + + + + + ', + // synced table input columns + '', + // auxiliary schema data + ' + + +
+ ', + // synced output columns + '', + ], + [ + // description + 'Should sync fk column with relation through parameter', + //additional behavior parameters + '', + // source table columns: column with fk + ' + + + + + ', + // synced table input columns + '', + // auxiliary schema data + ' + + +
+ ', + // synced output columns + ' + + + + + ', + ], + [ + // description + 'Behavior can override synced FKs', + //additional behavior parameters: inherit fks but override relation "LeName" + ' + + + + + + + + + + ', + // source table columns: column with fk + ' + + + + + ', + // synced table input columns + '', + // auxiliary schema data + ' + + +
+ + +
+ ', + // synced output columns + ' + + + + + ', + ], + [ + // description + 'Behavior cannot override FKs declared on synced table', + //additional behavior parameters: declare fk + ' + + + + + + + + + ', + // source table columns + '', + // synced table input columns: fk conflicting with behavior fk + ' + + + + + ', + // auxiliary schema data + ' + + +
+ + +
+ ', + // synced output columns: expect exception + EngineException::class, + ], + [ + // description + 'Behavior does not sync unique indexes by default', + //additional behavior parameters + '', + // source table columns: column with uniques + ' + + + + + + + + + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + ', + ], + [ + // description + 'Behavior syncs unique indexes as regular indexes if requested', + //additional behavior parameters + '', + // source table columns: column with uniques + ' + + + + + + + + + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + + + + + + + + ', + ], + [ + // description + 'Behavior syncs unique indexes as unique indexes if requested', + //additional behavior parameters + '', + // source table columns: column with uniques + ' + + + + + + + + + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + + + + + + + + ', + ], + [ + // description + 'Behavior can add pk', + //additional behavior parameters + '', + // source table columns: column with index + ' + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + ', + ], + [ + // description + 'Behavior can add renamed pk', + //additional behavior parameters + '', + // source table columns: column with index + '', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + ', + ], + [ + // description + 'Behavior can add FK', + //additional behavior parameters + ' + + + + + + ', + // source table columns: column with index + '', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + + ', + ], + [ + // description + 'Behavior can add cascading FK when changing id', + //additional behavior parameters + ' + + + + + + + ', + // source table columns: column with index + '', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + + + + ', + ], + [ + // description + 'Behavior ignores marked columns', + //additional behavior parameters + ' + + + + + ', + // source table columns + ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ', + // synced table input columns + '', + // auxiliary schema data + ' + + + +
+ ', + // synced output columns + ' + + + + + + + + + + + ', + ], + [ + // description + 'Behavior can sync only PKs', + //additional behavior parameters + ' + + ', + // source table columns + ' + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + ', + ], + [ + // description + 'Behavior can sync reduced PKs', + //additional behavior parameters + ' + + + ', + // source table columns + ' + + + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + ', + ], + [ + // description + 'Behavior can add columns', + //additional behavior parameters + ' + + + + + + + + + ', + // source table columns + ' + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + + ', + ], + [ + // description + 'Behavior prefixes synced columns', + //additional behavior parameters + ' + + + + + + + + + + ', + // source table columns + ' + + + + + + + + + + + + + + + ', + // synced table input columns + '', + // auxiliary schema data + ' + + + +
+ ', + // synced output columns + ' + + + + + + + + + + + + + + + + + + ', + ], + [ + // description + 'Behavior prefixes synced columns with table name by default', + //additional behavior parameters + ' + + ', + // source table columns + ' + + ', + // synced table input columns + '', + // auxiliary schema data + '', + // synced output columns + ' + + ', + ], + [ + // description + 'Behavior can inherit from other table by table name', + //additional behavior parameters + ' + + ', + // source table columns + '', + // synced table input columns + ' + + ', + // auxiliary schema data + ' + + +
+ ', + // synced output columns + ' + + + ', + ], + [ + // description + 'Behavior can inherit from other table by array', + //additional behavior parameters + ' + + + + + + ', + // source table columns + '', + // synced table input columns + ' + + ', + // auxiliary schema data + ' + + +
+ ', + // synced output columns + ' + + + ', + ], + [ + // description + 'Inheritance overrides defaults', + //additional behavior parameters + ' + + ', + // source table columns + ' + + + ', + // synced table input columns + ' + ', + // auxiliary schema data + ' + + +
+ ', + // synced output columns + ' + + + ', + ], + ]; + } + + /** + * @dataProvider syncTestDataProvider + * + * @param string $message + * @param string $behaviorAdditions + * @param string $sourceTableContentTags + * @param string $syncedTableInputTags + * @param string $auxiliaryTables + * @param string $syncedTableOutputTags + * + * @return void + */ + public function testSync( + string $message, + string $behaviorAdditions, + string $sourceTableContentTags, + string $syncedTableInputTags, + string $auxiliaryTables, + string $syncedTableOutputTags + ) { + // source table: some columns + // synced table: empty + $inputSchemaXml = << + + + + $behaviorAdditions + + + $sourceTableContentTags + +
+ + $auxiliaryTables + + $syncedTableInputTags
+ +EOT; + + // synced table: all columns + $expectedTableXml = << + + $sourceTableContentTags +
+ + + $syncedTableOutputTags +
+ + $auxiliaryTables + +EOT; + + if (class_exists($syncedTableOutputTags) && is_subclass_of($syncedTableOutputTags, Exception::class)) { + $this->expectException($syncedTableOutputTags); + } + $this->assertSchemaTableMatches($expectedTableXml, $inputSchemaXml, 'synced_table', $message); + } + + /** + * @return void + */ + public function testNoSyncByDefaultIfParentSkipsSql(): void + { + $inputSchemaXml = << + + +
+ + + + + +
+ +EOT; + $db = $this->buildSchema($inputSchemaXml); + $this->assertcount(2, $db->getTables()); + $this->assertNull($db->getTable('source_table1_synced')); + $this->assertNull($db->getTable('source_table2_synced')); + } + + /** + * @return void + */ + public function testInheritSkipsSql(): void + { + $inputSchemaXml = << + + + + +
+ + + + +
+ +EOT; + $db = $this->buildSchema($inputSchemaXml); + + $syncedTable1 = $db->getTable('source_table1_synced'); + $this->assertNotNull($syncedTable1, 'Should create synced table 1'); + $this->assertTrue($syncedTable1->isSkipSql(), 'Should inherit skipSql="true"'); + + $syncedTable2 = $db->getTable('source_table2_synced'); + $this->assertNotNull($syncedTable2, 'Should create synced table 2'); + $this->assertFalse($syncedTable2->isSkipSql(), 'Should inherit skipSql="false"'); + } + + /** + * @return void + */ + public function testIgnoreSkipsSql(): void + { + $inputSchemaXml = << + + + + +
+ +EOT; + $db = $this->buildSchema($inputSchemaXml); + + $syncedTable = $db->getTable('source_table_synced'); + $this->assertNotNull($syncedTable, 'Should create synced table'); + $this->assertFalse($syncedTable->isSkipSql(), 'Should ignore skipSql="true"'); + } + + /** + * @return void + */ + public function testSetTableAttributes(): void + { + $inputSchemaXml = << + + + + + + + + +
+ +EOT; + $db = $this->buildSchema($inputSchemaXml); + + $syncedTable = $db->getTable('source_table_synced'); + $this->assertArrayHasKey('foo', $syncedTable->getAttributes()); + $this->assertSame('bar', $syncedTable->getAttribute('foo')); + } + + + /** + * @param string $expectedTableXml + * @param string $schema + * @param string $tableName + * @param string|null $message + * + * @return void + */ + protected function assertSchemaTableMatches(string $expectedTableXml, string $inputSchemaXml, string $tableName, ?string $message = null) + { + $expectedDb = $this->buildSchema($expectedTableXml); + $expectedTable = $expectedDb->getTable($tableName); + + $actualDb = $this->buildSchema($inputSchemaXml); + $actualTable = $actualDb->getTable($tableName); + + $diff = TableComparator::computeDiff($actualTable, $expectedTable); + if ($diff !== false) { + $message = $this->buildTestMessage($message, $diff, $expectedDb, $actualDb); + $this->fail($message); + } + $this->expectNotToPerformAssertions(); + } + + /** + * @param string $schema + * + * @return \Propel\Generator\Model\Database + */ + protected function buildSchema(string $schema): Database + { + $builder = new QuickBuilder(); + $builder->setSchema($schema); + + return $builder->getDatabase(); + } + + /** + * @param string $inputMessage + * @param \Propel\Generator\Model\Diff\TableDiff $diff + * @param \Propel\Generator\Model\Database $expectedDb + * @param \Propel\Generator\Model\Database $actualDb + * + * @return string + */ + protected function buildTestMessage(string $inputMessage, TableDiff $diff, Database $expectedDb, Database $actualDb) + { + $inputMessage ??= ''; + $platform = new MysqlPlatform(); + $sql = $platform->getModifyTableDDL($diff); + + return <<