From 3902b97749689cd4bff3cf7f50c9265ee7b1209f Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Jan 2024 23:14:00 -0500 Subject: [PATCH 1/2] Import more adapter classes necessary to make Environment work These wrappers are used by Environment to make the output have timing and allow change to work correctly. --- src/Db/Adapter/AdapterFactory.php | 170 ++++++ src/Db/Adapter/AdapterWrapper.php | 524 ++++++++++++++++++ src/Db/Adapter/RecordingAdapter.php | 128 +++++ src/Db/Adapter/TimedOutputAdapter.php | 424 ++++++++++++++ src/Db/Adapter/WrapperInterface.php | 38 ++ .../IrreversibleMigrationException.php | 19 + .../Db/Adapter/AdapterFactoryTest.php | 113 ++++ .../Db/Adapter/RecordingAdapterTest.php | 160 ++++++ 8 files changed, 1576 insertions(+) create mode 100644 src/Db/Adapter/AdapterFactory.php create mode 100644 src/Db/Adapter/AdapterWrapper.php create mode 100644 src/Db/Adapter/RecordingAdapter.php create mode 100644 src/Db/Adapter/TimedOutputAdapter.php create mode 100644 src/Db/Adapter/WrapperInterface.php create mode 100644 src/Migration/IrreversibleMigrationException.php create mode 100644 tests/TestCase/Db/Adapter/AdapterFactoryTest.php create mode 100644 tests/TestCase/Db/Adapter/RecordingAdapterTest.php diff --git a/src/Db/Adapter/AdapterFactory.php b/src/Db/Adapter/AdapterFactory.php new file mode 100644 index 00000000..01fcc77c --- /dev/null +++ b/src/Db/Adapter/AdapterFactory.php @@ -0,0 +1,170 @@ + + * @phpstan-var array> + */ + protected array $adapters = [ + 'mysql' => 'Migrations\Db\Adapter\MysqlAdapter', + 'postgres' => 'Migrations\Db\Adapter\PostgresAdapter', + 'sqlite' => 'Migrations\Db\Adapter\SqliteAdapter', + 'sqlserver' => 'Migrations\Db\Adapter\SqlserverAdapter', + ]; + + /** + * Class map of adapters wrappers, indexed by name. + * + * @var array + */ + protected array $wrappers = [ + 'record' => 'Migrations\Db\Adapter\RecordingAdapter', + 'timed' => 'Migrations\Db\Adapter\TimedOutputAdapter', + ]; + + /** + * Register an adapter class with a given name. + * + * @param string $name Name + * @param object|string $class Class + * @throws \RuntimeException + * @return $this + */ + public function registerAdapter(string $name, object|string $class) + { + if (!is_subclass_of($class, 'Migrations\Db\Adapter\AdapterInterface')) { + throw new RuntimeException(sprintf( + 'Adapter class "%s" must implement Migrations\\Db\\Adapter\\AdapterInterface', + is_string($class) ? $class : get_class($class) + )); + } + $this->adapters[$name] = $class; + + return $this; + } + + /** + * Get an adapter class by name. + * + * @param string $name Name + * @throws \RuntimeException + * @return object|string + * @phpstan-return object|class-string<\Migrations\Db\Adapter\AdapterInterface> + */ + protected function getClass(string $name): object|string + { + if (empty($this->adapters[$name])) { + throw new RuntimeException(sprintf( + 'Adapter "%s" has not been registered', + $name + )); + } + + return $this->adapters[$name]; + } + + /** + * Get an adapter instance by name. + * + * @param string $name Name + * @param array $options Options + * @return \Migrations\Db\Adapter\AdapterInterface + */ + public function getAdapter(string $name, array $options): AdapterInterface + { + $class = $this->getClass($name); + + return new $class($options); + } + + /** + * Add or replace a wrapper with a fully qualified class name. + * + * @param string $name Name + * @param object|string $class Class + * @throws \RuntimeException + * @return $this + */ + public function registerWrapper(string $name, object|string $class) + { + if (!is_subclass_of($class, 'Migrations\Db\Adapter\WrapperInterface')) { + throw new RuntimeException(sprintf( + 'Wrapper class "%s" must implement Migrations\\Db\\Adapter\\WrapperInterface', + is_string($class) ? $class : get_class($class) + )); + } + $this->wrappers[$name] = $class; + + return $this; + } + + /** + * Get a wrapper class by name. + * + * @param string $name Name + * @throws \RuntimeException + * @return \Migrations\Db\Adapter\WrapperInterface|string + */ + protected function getWrapperClass(string $name): WrapperInterface|string + { + if (empty($this->wrappers[$name])) { + throw new RuntimeException(sprintf( + 'Wrapper "%s" has not been registered', + $name + )); + } + + return $this->wrappers[$name]; + } + + /** + * Get a wrapper instance by name. + * + * @param string $name Name + * @param \Migrations\Db\Adapter\AdapterInterface $adapter Adapter + * @return \Migrations\Db\Adapter\AdapterWrapper + */ + public function getWrapper(string $name, AdapterInterface $adapter): AdapterWrapper + { + $class = $this->getWrapperClass($name); + + return new $class($adapter); + } +} diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php new file mode 100644 index 00000000..b0a8e4f1 --- /dev/null +++ b/src/Db/Adapter/AdapterWrapper.php @@ -0,0 +1,524 @@ +setAdapter($adapter); + } + + /** + * @inheritDoc + */ + public function setAdapter(AdapterInterface $adapter): AdapterInterface + { + $this->adapter = $adapter; + + return $this; + } + + /** + * @inheritDoc + */ + public function getAdapter(): AdapterInterface + { + return $this->adapter; + } + + /** + * @inheritDoc + */ + public function setOptions(array $options): AdapterInterface + { + $this->adapter->setOptions($options); + + return $this; + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return $this->adapter->getOptions(); + } + + /** + * @inheritDoc + */ + public function hasOption(string $name): bool + { + return $this->adapter->hasOption($name); + } + + /** + * @inheritDoc + */ + public function getOption(string $name): mixed + { + return $this->adapter->getOption($name); + } + + /** + * @inheritDoc + */ + public function setInput(InputInterface $input): AdapterInterface + { + $this->adapter->setInput($input); + + return $this; + } + + /** + * @inheritDoc + */ + public function getInput(): InputInterface + { + return $this->adapter->getInput(); + } + + /** + * @inheritDoc + */ + public function setOutput(OutputInterface $output): AdapterInterface + { + $this->adapter->setOutput($output); + + return $this; + } + + /** + * @inheritDoc + */ + public function getOutput(): OutputInterface + { + return $this->adapter->getOutput(); + } + + /** + * @inheritDoc + */ + public function getColumnForType(string $columnName, string $type, array $options): Column + { + return $this->adapter->getColumnForType($columnName, $type, $options); + } + + /** + * @inheritDoc + */ + public function connect(): void + { + $this->getAdapter()->connect(); + } + + /** + * @inheritDoc + */ + public function disconnect(): void + { + $this->getAdapter()->disconnect(); + } + + /** + * @inheritDoc + */ + public function execute(string $sql, array $params = []): int + { + return $this->getAdapter()->execute($sql, $params); + } + + /** + * @inheritDoc + */ + public function query(string $sql, array $params = []): mixed + { + return $this->getAdapter()->query($sql, $params); + } + + /** + * @inheritDoc + */ + public function insert(Table $table, array $row): void + { + $this->getAdapter()->insert($table, $row); + } + + /** + * @inheritDoc + */ + public function bulkinsert(Table $table, array $rows): void + { + $this->getAdapter()->bulkinsert($table, $rows); + } + + /** + * @inheritDoc + */ + public function fetchRow(string $sql): array|false + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * @inheritDoc + */ + public function fetchAll(string $sql): array + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * @inheritDoc + */ + public function getVersions(): array + { + return $this->getAdapter()->getVersions(); + } + + /** + * @inheritDoc + */ + public function getVersionLog(): array + { + return $this->getAdapter()->getVersionLog(); + } + + /** + * @inheritDoc + */ + public function migrated(MigrationInterface $migration, string $direction, string $startTime, string $endTime): AdapterInterface + { + $this->getAdapter()->migrated($migration, $direction, $startTime, $endTime); + + return $this; + } + + /** + * @inheritDoc + */ + public function toggleBreakpoint(MigrationInterface $migration): AdapterInterface + { + $this->getAdapter()->toggleBreakpoint($migration); + + return $this; + } + + /** + * @inheritDoc + */ + public function resetAllBreakpoints(): int + { + return $this->getAdapter()->resetAllBreakpoints(); + } + + /** + * @inheritDoc + */ + public function setBreakpoint(MigrationInterface $migration): AdapterInterface + { + $this->getAdapter()->setBreakpoint($migration); + + return $this; + } + + /** + * @inheritDoc + */ + public function unsetBreakpoint(MigrationInterface $migration): AdapterInterface + { + $this->getAdapter()->unsetBreakpoint($migration); + + return $this; + } + + /** + * @inheritDoc + */ + public function createSchemaTable(): void + { + $this->getAdapter()->createSchemaTable(); + } + + /** + * @inheritDoc + */ + public function getColumnTypes(): array + { + return $this->getAdapter()->getColumnTypes(); + } + + /** + * @inheritDoc + */ + public function isValidColumnType(Column $column): bool + { + return $this->getAdapter()->isValidColumnType($column); + } + + /** + * @inheritDoc + */ + public function hasTransactions(): bool + { + return $this->getAdapter()->hasTransactions(); + } + + /** + * @inheritDoc + */ + public function beginTransaction(): void + { + $this->getAdapter()->beginTransaction(); + } + + /** + * @inheritDoc + */ + public function commitTransaction(): void + { + $this->getAdapter()->commitTransaction(); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction(): void + { + $this->getAdapter()->rollbackTransaction(); + } + + /** + * @inheritDoc + */ + public function quoteTableName(string $tableName): string + { + return $this->getAdapter()->quoteTableName($tableName); + } + + /** + * @inheritDoc + */ + public function quoteColumnName(string $columnName): string + { + return $this->getAdapter()->quoteColumnName($columnName); + } + + /** + * @inheritDoc + */ + public function hasTable(string $tableName): bool + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []): void + { + $this->getAdapter()->createTable($table, $columns, $indexes); + } + + /** + * @inheritDoc + */ + public function getColumns(string $tableName): array + { + return $this->getAdapter()->getColumns($tableName); + } + + /** + * @inheritDoc + */ + public function hasColumn(string $tableName, string $columnName): bool + { + return $this->getAdapter()->hasColumn($tableName, $columnName); + } + + /** + * @inheritDoc + */ + public function hasIndex(string $tableName, string|array $columns): bool + { + return $this->getAdapter()->hasIndex($tableName, $columns); + } + + /** + * @inheritDoc + */ + public function hasIndexByName(string $tableName, string $indexName): bool + { + return $this->getAdapter()->hasIndexByName($tableName, $indexName); + } + + /** + * @inheritDoc + */ + public function hasPrimaryKey(string $tableName, $columns, ?string $constraint = null): bool + { + return $this->getAdapter()->hasPrimaryKey($tableName, $columns, $constraint); + } + + /** + * @inheritDoc + */ + public function hasForeignKey(string $tableName, $columns, ?string $constraint = null): bool + { + return $this->getAdapter()->hasForeignKey($tableName, $columns, $constraint); + } + + /** + * @inheritDoc + */ + public function getSqlType(Literal|string $type, ?int $limit = null): array + { + return $this->getAdapter()->getSqlType($type, $limit); + } + + /** + * @inheritDoc + */ + public function createDatabase(string $name, array $options = []): void + { + $this->getAdapter()->createDatabase($name, $options); + } + + /** + * @inheritDoc + */ + public function hasDatabase(string $name): bool + { + return $this->getAdapter()->hasDatabase($name); + } + + /** + * @inheritDoc + */ + public function dropDatabase(string $name): void + { + $this->getAdapter()->dropDatabase($name); + } + + /** + * @inheritDoc + */ + public function createSchema(string $schemaName = 'public'): void + { + $this->getAdapter()->createSchema($schemaName); + } + + /** + * @inheritDoc + */ + public function dropSchema(string $schemaName): void + { + $this->getAdapter()->dropSchema($schemaName); + } + + /** + * @inheritDoc + */ + public function truncateTable(string $tableName): void + { + $this->getAdapter()->truncateTable($tableName); + } + + /** + * @inheritDoc + */ + public function castToBool($value): mixed + { + return $this->getAdapter()->castToBool($value); + } + + /** + * @return \PDO + */ + public function getConnection(): PDO + { + return $this->getAdapter()->getConnection(); + } + + /** + * @inheritDoc + */ + public function executeActions(Table $table, array $actions): void + { + $this->getAdapter()->executeActions($table, $actions); + } + + /** + * @inheritDoc + */ + public function getQueryBuilder(string $type): Query + { + return $this->getAdapter()->getQueryBuilder($type); + } + + /** + * @inheritDoc + */ + public function getSelectBuilder(): SelectQuery + { + return $this->getAdapter()->getSelectBuilder(); + } + + /** + * @inheritDoc + */ + public function getInsertBuilder(): InsertQuery + { + return $this->getAdapter()->getInsertBuilder(); + } + + /** + * @inheritDoc + */ + public function getUpdateBuilder(): UpdateQuery + { + return $this->getAdapter()->getUpdateBuilder(); + } + + /** + * @inheritDoc + */ + public function getDeleteBuilder(): DeleteQuery + { + return $this->getAdapter()->getDeleteBuilder(); + } +} diff --git a/src/Db/Adapter/RecordingAdapter.php b/src/Db/Adapter/RecordingAdapter.php new file mode 100644 index 00000000..9c9e6052 --- /dev/null +++ b/src/Db/Adapter/RecordingAdapter.php @@ -0,0 +1,128 @@ +commands[] = new CreateTable($table); + } + + /** + * @inheritDoc + */ + public function executeActions(Table $table, array $actions): void + { + $this->commands = array_merge($this->commands, $actions); + } + + /** + * Gets an array of the recorded commands in reverse. + * + * @throws \Phinx\Migration\IrreversibleMigrationException if a command cannot be reversed. + * @return \Phinx\Db\Plan\Intent + */ + public function getInvertedCommands(): Intent + { + $inverted = new Intent(); + + foreach (array_reverse($this->commands) as $command) { + switch (true) { + case $command instanceof CreateTable: + /** @var \Migrations\Db\Action\CreateTable $command */ + $inverted->addAction(new DropTable($command->getTable())); + break; + + case $command instanceof RenameTable: + /** @var \Migrations\Db\Action\RenameTable $command */ + $inverted->addAction(new RenameTable(new Table($command->getNewName()), $command->getTable()->getName())); + break; + + case $command instanceof AddColumn: + /** @var \Migrations\Db\Action\AddColumn $command */ + $inverted->addAction(new RemoveColumn($command->getTable(), $command->getColumn())); + break; + + case $command instanceof RenameColumn: + /** @var \Migrations\Db\Action\RenameColumn $command */ + $column = clone $command->getColumn(); + $name = $column->getName(); + $column->setName($command->getNewName()); + $inverted->addAction(new RenameColumn($command->getTable(), $column, $name)); + break; + + case $command instanceof AddIndex: + /** @var \Migrations\Db\Action\AddIndex $command */ + $inverted->addAction(new DropIndex($command->getTable(), $command->getIndex())); + break; + + case $command instanceof AddForeignKey: + /** @var \Migrations\Db\Action\AddForeignKey $command */ + $inverted->addAction(new DropForeignKey($command->getTable(), $command->getForeignKey())); + break; + + default: + throw new IrreversibleMigrationException(sprintf( + 'Cannot reverse a "%s" command', + get_class($command) + )); + } + } + + return $inverted; + } + + /** + * Execute the recorded commands in reverse. + * + * @return void + */ + public function executeInvertedCommands(): void + { + $plan = new Plan($this->getInvertedCommands()); + $plan->executeInverse($this->getAdapter()); + } +} diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php new file mode 100644 index 00000000..48a43501 --- /dev/null +++ b/src/Db/Adapter/TimedOutputAdapter.php @@ -0,0 +1,424 @@ +getAdapter()->getAdapterType(); + } + + /** + * Start timing a command. + * + * @return callable A function that is to be called when the command finishes + */ + public function startCommandTimer(): callable + { + $started = microtime(true); + + return function () use ($started): void { + $end = microtime(true); + if (OutputInterface::VERBOSITY_VERBOSE <= $this->getOutput()->getVerbosity()) { + $this->getOutput()->writeln(' -> ' . sprintf('%.4fs', $end - $started)); + } + }; + } + + /** + * Write a Phinx command to the output. + * + * @param string $command Command Name + * @param array $args Command Args + * @return void + */ + public function writeCommand(string $command, array $args = []): void + { + if (OutputInterface::VERBOSITY_VERBOSE > $this->getOutput()->getVerbosity()) { + return; + } + + if (count($args)) { + $outArr = []; + foreach ($args as $arg) { + if (is_array($arg)) { + $arg = array_map( + function ($value) { + return '\'' . $value . '\''; + }, + $arg + ); + $outArr[] = '[' . implode(', ', $arg) . ']'; + continue; + } + + $outArr[] = '\'' . $arg . '\''; + } + $this->getOutput()->writeln(' -- ' . $command . '(' . implode(', ', $outArr) . ')'); + + return; + } + + $this->getOutput()->writeln(' -- ' . $command); + } + + /** + * @inheritDoc + */ + public function insert(Table $table, array $row): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('insert', [$table->getName()]); + parent::insert($table, $row); + $end(); + } + + /** + * @inheritDoc + */ + public function bulkinsert(Table $table, array $rows): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('bulkinsert', [$table->getName()]); + parent::bulkinsert($table, $rows); + $end(); + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('createTable', [$table->getName()]); + parent::createTable($table, $columns, $indexes); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changePrimaryKey(Table $table, $newColumns): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('changePrimaryKey', [$table->getName()]); + $adapter->changePrimaryKey($table, $newColumns); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changeComment(Table $table, ?string $newComment): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('changeComment', [$table->getName()]); + $adapter->changeComment($table, $newComment); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function renameTable(string $tableName, string $newTableName): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('renameTable', [$tableName, $newTableName]); + $adapter->renameTable($tableName, $newTableName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropTable(string $tableName): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropTable', [$tableName]); + $adapter->dropTable($tableName); + $end(); + } + + /** + * @inheritDoc + */ + public function truncateTable(string $tableName): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('truncateTable', [$tableName]); + parent::truncateTable($tableName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addColumn(Table $table, Column $column): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand( + 'addColumn', + [ + $table->getName(), + $column->getName(), + $column->getType(), + ] + ); + $adapter->addColumn($table, $column); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function renameColumn(string $tableName, string $columnName, string $newColumnName): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('renameColumn', [$tableName, $columnName, $newColumnName]); + $adapter->renameColumn($tableName, $columnName, $newColumnName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changeColumn(string $tableName, string $columnName, Column $newColumn): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('changeColumn', [$tableName, $columnName, $newColumn->getType()]); + $adapter->changeColumn($tableName, $columnName, $newColumn); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropColumn(string $tableName, string $columnName): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropColumn', [$tableName, $columnName]); + $adapter->dropColumn($tableName, $columnName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addIndex(Table $table, Index $index): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('addIndex', [$table->getName(), $index->getColumns()]); + $adapter->addIndex($table, $index); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropIndex(string $tableName, $columns): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropIndex', [$tableName, $columns]); + $adapter->dropIndex($tableName, $columns); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropIndexByName(string $tableName, string $indexName): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropIndexByName', [$tableName, $indexName]); + $adapter->dropIndexByName($tableName, $indexName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('addForeignKey', [$table->getName(), $foreignKey->getColumns()]); + $adapter->addForeignKey($table, $foreignKey); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropForeignKey(string $tableName, array $columns, ?string $constraint = null): void + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropForeignKey', [$tableName, $columns]); + $adapter->dropForeignKey($tableName, $columns, $constraint); + $end(); + } + + /** + * @inheritDoc + */ + public function createDatabase(string $name, array $options = []): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('createDatabase', [$name]); + parent::createDatabase($name, $options); + $end(); + } + + /** + * @inheritDoc + */ + public function dropDatabase(string $name): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('dropDatabase', [$name]); + parent::dropDatabase($name); + $end(); + } + + /** + * @inheritDoc + */ + public function createSchema(string $name = 'public'): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('createSchema', [$name]); + parent::createSchema($name); + $end(); + } + + /** + * @inheritDoc + */ + public function dropSchema(string $name): void + { + $end = $this->startCommandTimer(); + $this->writeCommand('dropSchema', [$name]); + parent::dropSchema($name); + $end(); + } + + /** + * @inheritDoc + */ + public function executeActions(Table $table, array $actions): void + { + $end = $this->startCommandTimer(); + $this->writeCommand(sprintf('Altering table %s', $table->getName())); + parent::executeActions($table, $actions); + $end(); + } +} diff --git a/src/Db/Adapter/WrapperInterface.php b/src/Db/Adapter/WrapperInterface.php new file mode 100644 index 00000000..e8aaf497 --- /dev/null +++ b/src/Db/Adapter/WrapperInterface.php @@ -0,0 +1,38 @@ +factory = AdapterFactory::instance(); + } + + protected function tearDown(): void + { + unset($this->factory); + } + + public function testInstanceIsFactory() + { + $this->assertInstanceOf('Migrations\Db\Adapter\AdapterFactory', $this->factory); + } + + public function testRegisterAdapter() + { + // AdapterFactory::getClass is protected, work around it to avoid + // creating unnecessary instances and making the test more complex. + $method = new ReflectionMethod(get_class($this->factory), 'getClass'); + $method->setAccessible(true); + + $adapter = $method->invoke($this->factory, 'mysql'); + $this->factory->registerAdapter('test', $adapter); + + $this->assertEquals($adapter, $method->invoke($this->factory, 'test')); + } + + public function testRegisterAdapterFailure() + { + $adapter = static::class; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Adapter class "Migrations\Test\Db\Adapter\AdapterFactoryTest" must implement Migrations\Db\Adapter\AdapterInterface'); + + $this->factory->registerAdapter('test', $adapter); + } + + public function testGetAdapter() + { + $adapter = $this->factory->getAdapter('mysql', []); + + $this->assertInstanceOf('Migrations\Db\Adapter\MysqlAdapter', $adapter); + } + + public function testGetAdapterFailure() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Adapter "bad" has not been registered'); + + $this->factory->getAdapter('bad', []); + } + + public function testRegisterWrapper() + { + // WrapperFactory::getClass is protected, work around it to avoid + // creating unnecessary instances and making the test more complex. + $method = new ReflectionMethod(get_class($this->factory), 'getWrapperClass'); + $method->setAccessible(true); + + $wrapper = $method->invoke($this->factory, 'record'); + $this->factory->registerWrapper('test', $wrapper); + + $this->assertEquals($wrapper, $method->invoke($this->factory, 'test')); + } + + public function testRegisterWrapperFailure() + { + $wrapper = static::class; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Wrapper class "Migrations\Test\Db\Adapter\AdapterFactoryTest" must implement Migrations\Db\Adapter\WrapperInterface'); + + $this->factory->registerWrapper('test', $wrapper); + } + + private function getAdapterMock() + { + return $this->getMockBuilder('Migrations\Db\Adapter\AdapterInterface')->getMock(); + } + + public function testGetWrapper() + { + $wrapper = $this->factory->getWrapper('timed', $this->getAdapterMock()); + + $this->assertInstanceOf('Migrations\Db\Adapter\TimedOutputAdapter', $wrapper); + } + + public function testGetWrapperFailure() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Wrapper "nope" has not been registered'); + + $this->factory->getWrapper('nope', $this->getAdapterMock()); + } +} diff --git a/tests/TestCase/Db/Adapter/RecordingAdapterTest.php b/tests/TestCase/Db/Adapter/RecordingAdapterTest.php new file mode 100644 index 00000000..b1c85000 --- /dev/null +++ b/tests/TestCase/Db/Adapter/RecordingAdapterTest.php @@ -0,0 +1,160 @@ +getMockBuilder('\Migrations\Db\Adapter\PdoAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + + $stub->expects($this->any()) + ->method('isValidColumnType') + ->will($this->returnValue(true)); + + $this->adapter = new RecordingAdapter($stub); + } + + protected function tearDown(): void + { + unset($this->adapter); + } + + public function testRecordingAdapterCanInvertCreateTable() + { + $table = new Table('atable', [], $this->adapter); + $table->addColumn('column1', 'string') + ->save(); + + $commands = $this->adapter->getInvertedCommands()->getActions(); + $this->assertInstanceOf('Migrations\Db\Action\DropTable', $commands[0]); + $this->assertEquals('atable', $commands[0]->getTable()->getName()); + } + + public function testRecordingAdapterCanInvertRenameTable() + { + $table = new Table('oldname', [], $this->adapter); + $table->rename('newname') + ->save(); + + $commands = $this->adapter->getInvertedCommands()->getActions(); + $this->assertInstanceOf('Migrations\Db\Action\RenameTable', $commands[0]); + $this->assertEquals('newname', $commands[0]->getTable()->getName()); + $this->assertEquals('oldname', $commands[0]->getNewName()); + } + + public function testRecordingAdapterCanInvertAddColumn() + { + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('hasTable') + ->will($this->returnValue(true)); + + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options) { + return (new Column()) + ->setName($columnName) + ->setType($type) + ->setOptions($options); + }); + + $table = new Table('atable', [], $this->adapter); + $table->addColumn('acolumn', 'string') + ->save(); + + $commands = $this->adapter->getInvertedCommands()->getActions(); + $this->assertInstanceOf('Migrations\Db\Action\RemoveColumn', $commands[0]); + $this->assertEquals('atable', $commands[0]->getTable()->getName()); + $this->assertEquals('acolumn', $commands[0]->getColumn()->getName()); + } + + public function testRecordingAdapterCanInvertRenameColumn() + { + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('hasTable') + ->will($this->returnValue(true)); + + $table = new Table('atable', [], $this->adapter); + $table->renameColumn('oldname', 'newname') + ->save(); + + $commands = $this->adapter->getInvertedCommands()->getActions(); + $this->assertInstanceOf('Migrations\Db\Action\RenameColumn', $commands[0]); + $this->assertEquals('newname', $commands[0]->getColumn()->getName()); + $this->assertEquals('oldname', $commands[0]->getNewName()); + } + + public function testRecordingAdapterCanInvertAddIndex() + { + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('hasTable') + ->will($this->returnValue(true)); + + $table = new Table('atable', [], $this->adapter); + $table->addIndex(['email']) + ->save(); + + $commands = $this->adapter->getInvertedCommands()->getActions(); + $this->assertInstanceOf('Migrations\Db\Action\DropIndex', $commands[0]); + $this->assertEquals('atable', $commands[0]->getTable()->getName()); + $this->assertEquals(['email'], $commands[0]->getIndex()->getColumns()); + } + + public function testRecordingAdapterCanInvertAddForeignKey() + { + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('hasTable') + ->will($this->returnValue(true)); + + $table = new Table('atable', [], $this->adapter); + $table->addForeignKey(['ref_table_id'], 'refTable') + ->save(); + + $commands = $this->adapter->getInvertedCommands()->getActions(); + $this->assertInstanceOf('Migrations\Db\Action\DropForeignKey', $commands[0]); + $this->assertEquals('atable', $commands[0]->getTable()->getName()); + $this->assertEquals(['ref_table_id'], $commands[0]->getForeignKey()->getColumns()); + } + + public function testGetInvertedCommandsThrowsExceptionForIrreversibleCommand() + { + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('hasTable') + ->will($this->returnValue(true)); + + $table = new Table('atable', [], $this->adapter); + $table->removeColumn('thing') + ->save(); + + $this->expectException(IrreversibleMigrationException::class); + $this->expectExceptionMessage('Cannot reverse a "Migrations\Db\Action\RemoveColumn" command'); + + $this->adapter->getInvertedCommands(); + } +} From 7a154ebcddceb50fe99e35df00cebe5793fbfee1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Jan 2024 23:33:21 -0500 Subject: [PATCH 2/2] Fix phpcs, psalm and phpstan --- phpstan-baseline.neon | 30 +++++++++++++++++ psalm-baseline.xml | 20 +++++++++++ src/Db/Adapter/AdapterFactory.php | 48 ++++++++++++++------------- src/Db/Adapter/AdapterWrapper.php | 2 +- src/Db/Adapter/RecordingAdapter.php | 8 ++--- src/Db/Adapter/TimedOutputAdapter.php | 18 +++++----- src/Db/Adapter/WrapperInterface.php | 2 +- 7 files changed, 90 insertions(+), 38 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a73420b8..2d806152 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -15,6 +15,36 @@ parameters: count: 1 path: src/Command/BakeMigrationSnapshotCommand.php + - + message: "#^Method Migrations\\\\Db\\\\Adapter\\\\AdapterFactory\\:\\:getAdapter\\(\\) should return Migrations\\\\Db\\\\Adapter\\\\AdapterInterface but returns object\\.$#" + count: 1 + path: src/Db/Adapter/AdapterFactory.php + + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: src/Db/Adapter/AdapterFactory.php + + - + message: "#^Call to an undefined method Migrations\\\\Db\\\\Adapter\\\\AdapterInterface\\:\\:getDeleteBuilder\\(\\)\\.$#" + count: 1 + path: src/Db/Adapter/AdapterWrapper.php + + - + message: "#^Call to an undefined method Migrations\\\\Db\\\\Adapter\\\\AdapterInterface\\:\\:getInsertBuilder\\(\\)\\.$#" + count: 1 + path: src/Db/Adapter/AdapterWrapper.php + + - + message: "#^Call to an undefined method Migrations\\\\Db\\\\Adapter\\\\AdapterInterface\\:\\:getSelectBuilder\\(\\)\\.$#" + count: 1 + path: src/Db/Adapter/AdapterWrapper.php + + - + message: "#^Call to an undefined method Migrations\\\\Db\\\\Adapter\\\\AdapterInterface\\:\\:getUpdateBuilder\\(\\)\\.$#" + count: 1 + path: src/Db/Adapter/AdapterWrapper.php + - message: "#^Offset 'id' on non\\-empty\\-array\\ in isset\\(\\) always exists and is not nullable\\.$#" count: 2 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 37e441b5..4b73c27a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -15,6 +15,20 @@ output)]]> + + + InputInterface + + + adapter->getInput()]]> + + + getDeleteBuilder + getInsertBuilder + getSelectBuilder + getUpdateBuilder + + $opened @@ -46,6 +60,12 @@ is_array($newColumns) + + + $columns + $newColumns + + array_merge($versions, array_keys($migrations)) diff --git a/src/Db/Adapter/AdapterFactory.php b/src/Db/Adapter/AdapterFactory.php index 01fcc77c..6cdf7563 100644 --- a/src/Db/Adapter/AdapterFactory.php +++ b/src/Db/Adapter/AdapterFactory.php @@ -39,40 +39,42 @@ public static function instance(): static /** * Class map of database adapters, indexed by PDO::ATTR_DRIVER_NAME. * - * @var array - * @phpstan-var array> + * @var array + * @phpstan-var array> + * @psalm-var array> */ protected array $adapters = [ - 'mysql' => 'Migrations\Db\Adapter\MysqlAdapter', - 'postgres' => 'Migrations\Db\Adapter\PostgresAdapter', - 'sqlite' => 'Migrations\Db\Adapter\SqliteAdapter', - 'sqlserver' => 'Migrations\Db\Adapter\SqlserverAdapter', + 'mysql' => MysqlAdapter::class, + 'postgres' => PostgresAdapter::class, + 'sqlite' => SqliteAdapter::class, + 'sqlserver' => SqlserverAdapter::class, ]; /** * Class map of adapters wrappers, indexed by name. * - * @var array + * @var array + * @psalm-var array> */ protected array $wrappers = [ - 'record' => 'Migrations\Db\Adapter\RecordingAdapter', - 'timed' => 'Migrations\Db\Adapter\TimedOutputAdapter', + 'record' => RecordingAdapter::class, + 'timed' => TimedOutputAdapter::class, ]; /** * Register an adapter class with a given name. * * @param string $name Name - * @param object|string $class Class + * @param string $class Class * @throws \RuntimeException * @return $this */ - public function registerAdapter(string $name, object|string $class) + public function registerAdapter(string $name, string $class) { - if (!is_subclass_of($class, 'Migrations\Db\Adapter\AdapterInterface')) { + if (!is_subclass_of($class, AdapterInterface::class)) { throw new RuntimeException(sprintf( 'Adapter class "%s" must implement Migrations\\Db\\Adapter\\AdapterInterface', - is_string($class) ? $class : get_class($class) + $class )); } $this->adapters[$name] = $class; @@ -85,8 +87,8 @@ public function registerAdapter(string $name, object|string $class) * * @param string $name Name * @throws \RuntimeException - * @return object|string - * @phpstan-return object|class-string<\Migrations\Db\Adapter\AdapterInterface> + * @return string + * @phpstan-return class-string<\Migrations\Db\Adapter\AdapterInterface> */ protected function getClass(string $name): object|string { @@ -118,16 +120,16 @@ public function getAdapter(string $name, array $options): AdapterInterface * Add or replace a wrapper with a fully qualified class name. * * @param string $name Name - * @param object|string $class Class + * @param string $class Class * @throws \RuntimeException * @return $this */ - public function registerWrapper(string $name, object|string $class) + public function registerWrapper(string $name, string $class) { - if (!is_subclass_of($class, 'Migrations\Db\Adapter\WrapperInterface')) { + if (!is_subclass_of($class, WrapperInterface::class)) { throw new RuntimeException(sprintf( 'Wrapper class "%s" must implement Migrations\\Db\\Adapter\\WrapperInterface', - is_string($class) ? $class : get_class($class) + $class )); } $this->wrappers[$name] = $class; @@ -140,9 +142,9 @@ public function registerWrapper(string $name, object|string $class) * * @param string $name Name * @throws \RuntimeException - * @return \Migrations\Db\Adapter\WrapperInterface|string + * @return class-string<\Migrations\Db\Adapter\WrapperInterface> */ - protected function getWrapperClass(string $name): WrapperInterface|string + protected function getWrapperClass(string $name): string { if (empty($this->wrappers[$name])) { throw new RuntimeException(sprintf( @@ -159,9 +161,9 @@ protected function getWrapperClass(string $name): WrapperInterface|string * * @param string $name Name * @param \Migrations\Db\Adapter\AdapterInterface $adapter Adapter - * @return \Migrations\Db\Adapter\AdapterWrapper + * @return \Migrations\Db\Adapter\WrapperInterface */ - public function getWrapper(string $name, AdapterInterface $adapter): AdapterWrapper + public function getWrapper(string $name, AdapterInterface $adapter): WrapperInterface { $class = $this->getWrapperClass($name); diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php index b0a8e4f1..76110341 100644 --- a/src/Db/Adapter/AdapterWrapper.php +++ b/src/Db/Adapter/AdapterWrapper.php @@ -27,7 +27,7 @@ * Proxy commands through to another adapter, allowing modification of * parameters during calls. */ -abstract class AdapterWrapper implements AdapterInterface, WrapperInterface +abstract class AdapterWrapper implements WrapperInterface { /** * @var \Migrations\Db\Adapter\AdapterInterface diff --git a/src/Db/Adapter/RecordingAdapter.php b/src/Db/Adapter/RecordingAdapter.php index 9c9e6052..1b94cb61 100644 --- a/src/Db/Adapter/RecordingAdapter.php +++ b/src/Db/Adapter/RecordingAdapter.php @@ -31,7 +31,7 @@ class RecordingAdapter extends AdapterWrapper { /** - * @var \Phinx\Db\Action\Action[] + * @var \Migrations\Db\Action\Action[] */ protected array $commands = []; @@ -62,8 +62,8 @@ public function executeActions(Table $table, array $actions): void /** * Gets an array of the recorded commands in reverse. * - * @throws \Phinx\Migration\IrreversibleMigrationException if a command cannot be reversed. - * @return \Phinx\Db\Plan\Intent + * @throws \Migrations\Migration\IrreversibleMigrationException if a command cannot be reversed. + * @return \Migrations\Db\Plan\Intent */ public function getInvertedCommands(): Intent { @@ -89,7 +89,7 @@ public function getInvertedCommands(): Intent case $command instanceof RenameColumn: /** @var \Migrations\Db\Action\RenameColumn $command */ $column = clone $command->getColumn(); - $name = $column->getName(); + $name = (string)$column->getName(); $column->setName($command->getNewName()); $inverted->addAction(new RenameColumn($command->getTable(), $column, $name)); break; diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php index 48a43501..e0ab9b25 100644 --- a/src/Db/Adapter/TimedOutputAdapter.php +++ b/src/Db/Adapter/TimedOutputAdapter.php @@ -157,15 +157,15 @@ public function changeComment(Table $table, ?string $newComment): void * @throws \BadMethodCallException * @return void */ - public function renameTable(string $tableName, string $newTableName): void + public function renameTable(string $tableName, string $newName): void { $adapter = $this->getAdapter(); if (!$adapter instanceof DirectActionInterface) { throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); } $end = $this->startCommandTimer(); - $this->writeCommand('renameTable', [$tableName, $newTableName]); - $adapter->renameTable($tableName, $newTableName); + $this->writeCommand('renameTable', [$tableName, $newName]); + $adapter->renameTable($tableName, $newName); $end(); } @@ -392,22 +392,22 @@ public function dropDatabase(string $name): void /** * @inheritDoc */ - public function createSchema(string $name = 'public'): void + public function createSchema(string $schemaName = 'public'): void { $end = $this->startCommandTimer(); - $this->writeCommand('createSchema', [$name]); - parent::createSchema($name); + $this->writeCommand('createSchema', [$schemaName]); + parent::createSchema($schemaName); $end(); } /** * @inheritDoc */ - public function dropSchema(string $name): void + public function dropSchema(string $schemaName): void { $end = $this->startCommandTimer(); - $this->writeCommand('dropSchema', [$name]); - parent::dropSchema($name); + $this->writeCommand('dropSchema', [$schemaName]); + parent::dropSchema($schemaName); $end(); } diff --git a/src/Db/Adapter/WrapperInterface.php b/src/Db/Adapter/WrapperInterface.php index e8aaf497..bda3a08f 100644 --- a/src/Db/Adapter/WrapperInterface.php +++ b/src/Db/Adapter/WrapperInterface.php @@ -11,7 +11,7 @@ /** * Wrapper Interface. */ -interface WrapperInterface +interface WrapperInterface extends AdapterInterface { /** * Class constructor, must always wrap another adapter.