From f0084b4b0d4e467305621154404f8d00817a4c30 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 2 Sep 2024 23:55:14 -0400 Subject: [PATCH 01/47] Port insert & bulkinsert compatibility from phinx Port cakephp/phinx#2301 to migrations. Fixes #737 --- src/Db/Adapter/PdoAdapter.php | 46 +++++++++---- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 67 +++++++++++++++++++ .../Db/Adapter/PostgresAdapterTest.php | 67 +++++++++++++++++++ .../TestCase/Db/Adapter/SqliteAdapterTest.php | 67 +++++++++++++++++++ .../Db/Adapter/SqlserverAdapterTest.php | 67 +++++++++++++++++++ 5 files changed, 302 insertions(+), 12 deletions(-) diff --git a/src/Db/Adapter/PdoAdapter.php b/src/Db/Adapter/PdoAdapter.php index 55570ef8..d1a462a5 100644 --- a/src/Db/Adapter/PdoAdapter.php +++ b/src/Db/Adapter/PdoAdapter.php @@ -40,6 +40,7 @@ use PDOException; use Phinx\Config\Config; use Phinx\Migration\MigrationInterface; +use Phinx\Util\Literal as PhinxLiteral; use ReflectionMethod; use RuntimeException; use UnexpectedValueException; @@ -316,8 +317,20 @@ public function insert(Table $table, array $row): void $sql .= ' VALUES (' . implode(', ', array_map([$this, 'quoteValue'], $row)) . ');'; $this->io->out($sql); } else { - $sql .= ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; - $this->getConnection()->execute($sql, array_values($row)); + $values = []; + $vals = []; + foreach ($row as $value) { + $placeholder = '?'; + if ($value instanceof Literal || $value instanceof PhinxLiteral) { + $placeholder = (string)$value; + } + $values[] = $placeholder; + if ($placeholder === '?') { + $vals[] = $value; + } + } + $sql .= ' VALUES (' . implode(',', $values) . ')'; + $this->getConnection()->execute($sql, $vals); } } @@ -336,6 +349,9 @@ protected function quoteValue(mixed $value): mixed if ($value === null) { return 'null'; } + if ($value instanceof Literal || $value instanceof PhinxLiteral) { + return (string)$value; + } // TODO remove hacks like this by using cake's database layer better. $driver = $this->getConnection()->getDriver(); $method = new ReflectionMethod($driver, 'getPdo'); @@ -382,22 +398,28 @@ public function bulkinsert(Table $table, array $rows): void $sql .= implode(', ', $values) . ';'; $this->io->out($sql); } else { - $count_keys = count($keys); - $query = '(' . implode(', ', array_fill(0, $count_keys, '?')) . ')'; - $count_vars = count($rows); - $queries = array_fill(0, $count_vars, $query); - $sql .= implode(',', $queries); $vals = []; - + $queries = []; foreach ($rows as $row) { + $values = []; foreach ($row as $v) { - if (is_bool($v)) { - $vals[] = $this->castToBool($v); - } else { - $vals[] = $v; + $placeholder = '?'; + if ($v instanceof Literal || $v instanceof PhinxLiteral) { + $placeholder = (string)$v; + } + $values[] = $placeholder; + if ($placeholder == '?') { + if (is_bool($v)) { + $vals[] = $this->castToBool($v); + } else { + $vals[] = $v; + } } } + $query = '(' . implode(', ', $values) . ')'; + $queries[] = $query; } + $sql .= implode(',', $queries); $this->getConnection()->execute($sql, $vals); } } diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index dc77d565..2a06e553 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -2024,6 +2024,37 @@ public function testBulkInsertData() $this->assertEquals('test', $rows[2]['column3']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $data = [ @@ -2059,6 +2090,42 @@ public function testInsertData() $this->assertEquals('foo', $rows[2]['column3']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testDumpCreateTable() { $options = $this->adapter->getOptions(); diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 04205c78..2be45186 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -2233,6 +2233,37 @@ public function testBulkInsertBoolean() $this->assertNull($rows[2]['column1']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $table = new Table('table1', [], $this->adapter); @@ -2257,6 +2288,42 @@ public function testInsertData() $this->assertEquals(2, $rows[1]['column2']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testInsertBoolean() { $table = new Table('table1', [], $this->adapter); diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index aa9e7a5a..903c992a 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -1684,6 +1684,37 @@ public function testBulkInsertData() $this->assertNull($rows[3]['column2']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $table = new Table('table1', [], $this->adapter); @@ -1725,6 +1756,42 @@ public function testInsertData() $this->assertNull($rows[3]['column2']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testBulkInsertDataEnum() { $table = new Table('table1', [], $this->adapter); diff --git a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php index 004a9f49..ec5a2e05 100644 --- a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php @@ -1197,6 +1197,37 @@ public function testBulkInsertData() $this->assertEquals(3, $rows[2]['column2']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $table = new Table('table1', [], $this->adapter); @@ -1230,6 +1261,42 @@ public function testInsertData() $this->assertEquals(3, $rows[2]['column2']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testTruncateTable() { $table = new Table('table1', [], $this->adapter); From d3a9e9f46419044c7ce471755a5115e6dae04d61 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 3 Sep 2024 00:28:07 -0400 Subject: [PATCH 02/47] Fix postgres tests --- src/Db/Adapter/PostgresAdapter.php | 47 ++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index d8fe5aa4..44047134 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -16,6 +16,7 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; +use Phinx\Util\Literal as PhinxLiteral; class PostgresAdapter extends PdoAdapter { @@ -1560,8 +1561,20 @@ public function insert(Table $table, array $row): void $sql .= ' ' . $override . 'VALUES (' . implode(', ', array_map([$this, 'quoteValue'], $row)) . ');'; $this->io->out($sql); } else { - $sql .= ' ' . $override . 'VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; - $this->getConnection()->execute($sql, array_values($row)); + $values = []; + $vals = []; + foreach ($row as $value) { + $placeholder = '?'; + if ($value instanceof Literal || $value instanceof PhinxLiteral) { + $placeholder = (string)$value; + } + $values[] = $placeholder; + if ($placeholder === '?') { + $vals[] = $value; + } + } + $sql .= ' ' . $override . 'VALUES (' . implode(',', $values) . ')'; + $this->getConnection()->execute($sql, $vals); } } @@ -1592,25 +1605,29 @@ public function bulkinsert(Table $table, array $rows): void $sql .= implode(', ', $values) . ';'; $this->io->out($sql); } else { - $connection = $this->getConnection(); - $count_keys = count($keys); - $query = '(' . implode(', ', array_fill(0, $count_keys, '?')) . ')'; - $count_vars = count($rows); - $queries = array_fill(0, $count_vars, $query); - $sql .= implode(',', $queries); $vals = []; - + $queries = []; foreach ($rows as $row) { + $values = []; foreach ($row as $v) { - if (is_bool($v)) { - $vals[] = $this->castToBool($v); - } else { - $vals[] = $v; + $placeholder = '?'; + if ($v instanceof Literal || $v instanceof PhinxLiteral) { + $placeholder = (string)$v; + } + $values[] = $placeholder; + if ($placeholder == '?') { + if (is_bool($v)) { + $vals[] = $this->castToBool($v); + } else { + $vals[] = $v; + } } } + $query = '(' . implode(', ', $values) . ')'; + $queries[] = $query; } - - $connection->execute($sql, $vals); + $sql .= implode(',', $queries); + $this->getConnection()->execute($sql, $vals); } } } From a1bc157631ccf6c830b08aaa0d856c563d799e1e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 4 Sep 2024 23:10:51 -0400 Subject: [PATCH 03/47] Fix sqlserver tests --- tests/TestCase/Db/Adapter/SqlserverAdapterTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php index ec5a2e05..cc3bbf36 100644 --- a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php @@ -1224,8 +1224,8 @@ public function testBulkInsertLiteral() $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); - $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); - $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + $this->assertEquals('2024-01-01 00:00:00.000', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00.000', $rows[2]['column2']); } public function testInsertData() @@ -1293,8 +1293,8 @@ public function testInsertLiteral() $this->assertEquals('test', $rows[1]['column2']); $this->assertEquals('foo', $rows[2]['column2']); $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); - $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); - $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + $this->assertEquals('2024-01-01 00:00:00.000', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00.000', $rows[2]['column3']); } public function testTruncateTable() From 491b311cb4454bf15741e88e76fb1c0b54c3fef1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 17 Sep 2024 23:37:56 -0400 Subject: [PATCH 04/47] Start to add Migrations versions of base interfaces These are important interfaces for userland code. All of the current migrations extend from phinx base classes. I'd like to try updating migrations internals by doing the following changes - [ ] Add SeedInterface and MigrationInterface. - [ ] Add decorator that adapts between phinx and migrations. Manager::getSeeds(), getMigrations() - [ ] Move phinx shims into adapter/decorator - [ ] Update the internals to use migrations interfaces. - [ ] Introduce a new base class that is all migrations - [ ] Provide upgrade tool to update migration classes. If all goes well, we'll have an opt-in path for applications to upgrade to the new namespaces and code before a breaking change where phinx compatibility is removed. --- src/MigrationsInterface.php | 319 ++++++++++++++++++++++++++++++++++++ src/SeedInterface.php | 190 +++++++++++++++++++++ 2 files changed, 509 insertions(+) create mode 100644 src/MigrationsInterface.php create mode 100644 src/SeedInterface.php diff --git a/src/MigrationsInterface.php b/src/MigrationsInterface.php new file mode 100644 index 00000000..046a54db --- /dev/null +++ b/src/MigrationsInterface.php @@ -0,0 +1,319 @@ + $options Options + * @return void + */ + public function createDatabase(string $name, array $options): void; + + /** + * Drop a database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase(string $name): void; + + /** + * Creates schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function createSchema(string $name): void; + + /** + * Drops schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function dropSchema(string $name): void; + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable(string $tableName): bool; + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options): Table; + + /** + * Perform checks on the migration, printing a warning + * if there are potential problems. + * + * @return void + */ + public function preFlightCheck(): void; + + /** + * Perform checks on the migration after completion + * + * Right now, the only check is whether all changes were committed + * + * @return void + */ + public function postFlightCheck(): void; + + /** + * Checks to see if the migration should be executed. + * + * Returns true by default. + * + * You can use this to prevent a migration from executing. + * + * @return bool + */ + public function shouldExecute(): bool; +} diff --git a/src/SeedInterface.php b/src/SeedInterface.php new file mode 100644 index 00000000..f4bb8d1c --- /dev/null +++ b/src/SeedInterface.php @@ -0,0 +1,190 @@ +\Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options): Table; + + /** + * Checks to see if the seed should be executed. + * + * Returns true by default. + * + * You can use this to prevent a seed from executing. + * + * @return bool + */ + public function shouldExecute(): bool; +} From fe677f45082118690b698ae574a22cad2d067ab2 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 18 Sep 2024 10:08:13 -0400 Subject: [PATCH 05/47] Fix file name --- src/{MigrationsInterface.php => MigrationInterface.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{MigrationsInterface.php => MigrationInterface.php} (100%) diff --git a/src/MigrationsInterface.php b/src/MigrationInterface.php similarity index 100% rename from src/MigrationsInterface.php rename to src/MigrationInterface.php From f04d92f42512f99faf478fb338028a0dd65552cc Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 22 Sep 2024 23:13:29 -0400 Subject: [PATCH 06/47] 4.next - Revise the migrations interfaces - Add methods that will allow symfony support to be dropped in the next major - Add better intergration with CakePHP abstractions. - Put some TODOs in for the next set of changes. --- src/Migration/Manager.php | 10 ++++++++ src/MigrationInterface.php | 47 ++++++++++++++++++++++------------ src/SeedInterface.php | 52 ++++++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index cb0faf18..b6a1021c 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -853,13 +853,17 @@ function ($phpFile) { $io->verbose("Constructing $class."); $config = $this->getConfig(); + // TODO Subset config and pass forward. + // Move this to the Migration/phinx shim $input = new ArrayInput([ '--plugin' => $config['plugin'] ?? null, '--source' => $config['source'] ?? null, '--connection' => $config->getConnection(), ]); + // TODO move this to the migration/phinx shim $output = new OutputAdapter($io); + // TODO constructor should take $io and $config // instantiate it $migration = new $class('default', $version, $input, $output); @@ -967,16 +971,20 @@ public function getSeeds(): array $seeds = []; $config = $this->getConfig(); + // TODO Subset config and pass forward. + // TODO move this to the migration/phinx shim $optionDef = new InputDefinition([ new InputOption('plugin', mode: InputOption::VALUE_OPTIONAL, default: ''), new InputOption('connection', mode: InputOption::VALUE_OPTIONAL, default: ''), new InputOption('source', mode: InputOption::VALUE_OPTIONAL, default: ''), ]); + // TODO move this to the migration/phinx shim $input = new ArrayInput([ '--plugin' => $config['plugin'] ?? null, '--source' => $config['source'] ?? null, '--connection' => $config->getConnection(), ], $optionDef); + // TODO move this to the migration/phinx shim $output = new OutputAdapter($this->io); foreach ($phpFiles as $filePath) { @@ -1003,6 +1011,7 @@ public function getSeeds(): array } else { $seed = new $class(); } + // TODO Replace with with setIo and setConfig $seed->setEnvironment('default'); $seed->setInput($input); $seed->setOutput($output); @@ -1027,6 +1036,7 @@ public function getSeeds(): array return []; } + // TODO remove this foreach ($this->seeds as $instance) { if (isset($input) && $instance instanceof AbstractSeed) { $instance->setInput($input); diff --git a/src/MigrationInterface.php b/src/MigrationInterface.php index 046a54db..2a8e505d 100644 --- a/src/MigrationInterface.php +++ b/src/MigrationInterface.php @@ -8,11 +8,13 @@ namespace Migrations; +use Cake\Console\ConsoleIo; use Cake\Database\Query; use Cake\Database\Query\DeleteQuery; use Cake\Database\Query\InsertQuery; use Cake\Database\Query\SelectQuery; use Cake\Database\Query\UpdateQuery; +use Migrations\Config\ConfigInterface; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Table; use Symfony\Component\Console\Input\InputInterface; @@ -61,32 +63,52 @@ public function setAdapter(AdapterInterface $adapter); public function getAdapter(): ?AdapterInterface; /** - * Sets the input object to be used in migration object + * Set the Console IO object to be used. * - * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Cake\Console\ConsoleIo $io The Io * @return $this */ - public function setInput(InputInterface $input); + public function setIo(ConsoleIo $io); /** - * Gets the input object to be used in migration object + * Get the Console IO object to be used. * - * @return \Symfony\Component\Console\Input\InputInterface|null + * @return \Cake\Console\ConsoleIo|null */ - public function getInput(): ?InputInterface; + public function getIo(): ?ConsoleIo; /** - * Sets the output object to be used in migration object + * Gets the config. * - * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return \Migrations\Config\ConfigInterface + */ + public function getConfig(): ConfigInterface; + + /** + * Sets the config. + * + * @param \Migrations\Config\ConfigInterface $config Configuration Object * @return $this */ - public function setOutput(OutputInterface $output); + public function setConfig(ConfigInterface $config); + + /** + * Gets the input object to be used in migration object + * + * A new InputInterface will be generated each time `getOutput` is called. + * + * @return \Symfony\Component\Console\Input\InputInterface|null + * @deprecated 4.5.0 Use getIo() instead. + */ + public function getInput(): ?InputInterface; /** * Gets the output object to be used in migration object * + * A new OutputInterface will be generated each time `getOutput` is called. + * * @return \Symfony\Component\Console\Output\OutputInterface|null + * @deprecated 4.5.0 Use getIo() instead. */ public function getOutput(): ?OutputInterface; @@ -97,13 +119,6 @@ public function getOutput(): ?OutputInterface; */ public function getName(): string; - /** - * Gets the detected environment - * - * @return string - */ - public function getEnvironment(): string; - /** * Sets the migration version number. * diff --git a/src/SeedInterface.php b/src/SeedInterface.php index f4bb8d1c..d9788122 100644 --- a/src/SeedInterface.php +++ b/src/SeedInterface.php @@ -8,6 +8,8 @@ namespace Migrations; +use Cake\Console\ConsoleIo; +use Migrations\Config\ConfigInterface; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Table; use Symfony\Component\Console\Input\InputInterface; @@ -45,61 +47,67 @@ public function run(): void; public function getDependencies(): array; /** - * Sets the environment. + * Sets the database adapter. * + * @param \Migrations\Db\Adapter\AdapterInterface $adapter Database Adapter * @return $this */ - public function setEnvironment(string $environment); + public function setAdapter(AdapterInterface $adapter); /** - * Gets the environment. + * Gets the database adapter. * - * @return string + * @return \Migrations\Db\Adapter\AdapterInterface */ - public function getEnvironment(): string; + public function getAdapter(): AdapterInterface; /** - * Sets the database adapter. + * Set the Console IO object to be used. * - * @param \Migrations\Db\Adapter\AdapterInterface $adapter Database Adapter + * @param \Cake\Console\ConsoleIo $io The Io * @return $this */ - public function setAdapter(AdapterInterface $adapter); + public function setIo(ConsoleIo $io); /** - * Gets the database adapter. + * Get the Console IO object to be used. * - * @return \Migrations\Db\Adapter\AdapterInterface + * @return \Cake\Console\ConsoleIo|null */ - public function getAdapter(): AdapterInterface; + public function getIo(): ?ConsoleIo; /** - * Sets the input object to be used in migration object + * Gets the config. * - * @param \Symfony\Component\Console\Input\InputInterface $input Input - * @return $this + * @return \Migrations\Config\ConfigInterface */ - public function setInput(InputInterface $input); + public function getConfig(): ConfigInterface; /** - * Gets the input object to be used in migration object + * Sets the config. * - * @return \Symfony\Component\Console\Input\InputInterface + * @param \Migrations\Config\ConfigInterface $config Configuration Object + * @return $this */ - public function getInput(): InputInterface; + public function setConfig(ConfigInterface $config); /** - * Sets the output object to be used in migration object + * Gets the input object to be used in migration object * - * @param \Symfony\Component\Console\Output\OutputInterface $output Output - * @return $this + * A new InputInteface will be generated each time `getOutput` is called. + * + * @return \Symfony\Component\Console\Input\InputInterface + * @deprecated 4.5.0 Use getIo() instead. */ - public function setOutput(OutputInterface $output); + public function getInput(): InputInterface; /** * Gets the output object to be used in migration object * + * A new OutputInteface will be generated each time `getOutput` is called. + * * @return \Symfony\Component\Console\Output\OutputInterface + * @deprecated 4.5.0 Use getIo() instead. */ public function getOutput(): OutputInterface; From fa26f5a39a859586bb47e2c0ec9e2f5cee3d46ba Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 25 Sep 2024 23:43:13 -0400 Subject: [PATCH 07/47] Add and use a ShimAdapter Update the internals of Manager and Environment to use Migrations interfaces for Seeds. This pushes the compatibility shims down a layer and should continue to give the same backwards compatibility. Next I'll build a BaseSeed that is all migrations internals. --- src/Migration/Environment.php | 8 +- src/Migration/Manager.php | 75 ++---- src/SeedInterface.php | 34 +-- src/Shim/SeedAdapter.php | 303 +++++++++++++++++++++++ tests/TestCase/Migration/ManagerTest.php | 8 +- 5 files changed, 347 insertions(+), 81 deletions(-) create mode 100644 src/Shim/SeedAdapter.php diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 3ce62820..0ae06d4b 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -13,8 +13,8 @@ use Migrations\Db\Adapter\AdapterFactory; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Adapter\PhinxAdapter; +use Migrations\SeedInterface; use Phinx\Migration\MigrationInterface; -use Phinx\Seed\SeedInterface; use RuntimeException; class Environment @@ -131,15 +131,13 @@ public function executeMigration(MigrationInterface $migration, string $directio /** * Executes the specified seeder on this environment. * - * @param \Phinx\Seed\SeedInterface $seed Seed + * @param \Migrations\Seed\SeedInterface $seed Seed * @return void */ public function executeSeed(SeedInterface $seed): void { $adapter = $this->getAdapter(); - $phinxAdapter = new PhinxAdapter($adapter); - - $seed->setAdapter($phinxAdapter); + $seed->setAdapter($adapter); if (method_exists($seed, SeedInterface::INIT)) { $seed->{SeedInterface::INIT}(); } diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index b6a1021c..b2c30c72 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -14,17 +14,16 @@ use Exception; use InvalidArgumentException; use Migrations\Config\ConfigInterface; +use Migrations\SeedInterface; use Migrations\Shim\OutputAdapter; +use Migrations\Shim\SeedAdapter; use Phinx\Migration\AbstractMigration; use Phinx\Migration\MigrationInterface; -use Phinx\Seed\AbstractSeed; -use Phinx\Seed\SeedInterface; +use Phinx\Seed\SeedInterface as PhinxSeedInterface; use Phinx\Util\Util; use Psr\Container\ContainerInterface; use RuntimeException; use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputOption; class Manager { @@ -53,7 +52,7 @@ class Manager protected ?array $migrations = null; /** - * @var \Phinx\Seed\SeedInterface[]|null + * @var \Migrations\SeedInterface[]|null */ protected ?array $seeds = null; @@ -475,7 +474,7 @@ public function executeMigration(MigrationInterface $migration, string $directio /** * Execute a seeder against the specified environment. * - * @param \Phinx\Seed\SeedInterface $seed Seed + * @param \Migrations\Seed\SeedInterface $seed Seed * @return void */ public function executeSeed(SeedInterface $seed): void @@ -523,7 +522,7 @@ protected function printMigrationStatus(MigrationInterface $migration, string $s /** * Print Seed Status * - * @param \Phinx\Seed\SeedInterface $seed Seed + * @param \Migrations\Seed\SeedInterface $seed Seed * @param string $status Status of the seed * @param string|null $duration Duration the seed took the be executed * @return void @@ -901,7 +900,7 @@ protected function getMigrationFiles(): array /** * Sets the database seeders. * - * @param \Phinx\Seed\SeedInterface[] $seeds Seeders + * @param \Migrations\SeedInterface[] $seeds Seeders * @return $this */ public function setSeeds(array $seeds) @@ -914,8 +913,8 @@ public function setSeeds(array $seeds) /** * Get seed dependencies instances from seed dependency array * - * @param \Phinx\Seed\SeedInterface $seed Seed - * @return \Phinx\Seed\SeedInterface[] + * @param \Migrations\SeedInterface $seed Seed + * @return \Migrations\SeedInterface[] */ protected function getSeedDependenciesInstances(SeedInterface $seed): array { @@ -924,8 +923,9 @@ protected function getSeedDependenciesInstances(SeedInterface $seed): array if (!empty($dependencies) && !empty($this->seeds)) { foreach ($dependencies as $dependency) { foreach ($this->seeds as $seed) { - if (get_class($seed) === $dependency) { - $dependenciesInstances[get_class($seed)] = $seed; + $name = $seed->getName(); + if ($name === $dependency) { + $dependenciesInstances[$name] = $seed; } } } @@ -937,14 +937,15 @@ protected function getSeedDependenciesInstances(SeedInterface $seed): array /** * Order seeds by dependencies * - * @param \Phinx\Seed\SeedInterface[] $seeds Seeds - * @return \Phinx\Seed\SeedInterface[] + * @param \Migrations\SeedInterface[] $seeds Seeds + * @return \Migrations\SeedInterface[] */ protected function orderSeedsByDependencies(array $seeds): array { $orderedSeeds = []; foreach ($seeds as $seed) { - $orderedSeeds[get_class($seed)] = $seed; + $name = $seed->getName(); + $orderedSeeds[$name] = $seed; $dependencies = $this->getSeedDependenciesInstances($seed); if (!empty($dependencies)) { $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds); @@ -958,7 +959,7 @@ protected function orderSeedsByDependencies(array $seeds): array * Gets an array of database seeders. * * @throws \InvalidArgumentException - * @return \Phinx\Seed\SeedInterface[] + * @return \Migrations\SeedInterface[] */ public function getSeeds(): array { @@ -967,25 +968,11 @@ public function getSeeds(): array // filter the files to only get the ones that match our naming scheme $fileNames = []; - /** @var \Phinx\Seed\SeedInterface[] $seeds */ + /** @var \Migrations\SeedInterface[] $seeds */ $seeds = []; $config = $this->getConfig(); - // TODO Subset config and pass forward. - // TODO move this to the migration/phinx shim - $optionDef = new InputDefinition([ - new InputOption('plugin', mode: InputOption::VALUE_OPTIONAL, default: ''), - new InputOption('connection', mode: InputOption::VALUE_OPTIONAL, default: ''), - new InputOption('source', mode: InputOption::VALUE_OPTIONAL, default: ''), - ]); - // TODO move this to the migration/phinx shim - $input = new ArrayInput([ - '--plugin' => $config['plugin'] ?? null, - '--source' => $config['source'] ?? null, - '--connection' => $config->getConnection(), - ], $optionDef); - // TODO move this to the migration/phinx shim - $output = new OutputAdapter($this->io); + $io = $this->getIo(); foreach ($phpFiles as $filePath) { if (Util::isValidSeedFileName(basename($filePath))) { @@ -1011,18 +998,13 @@ public function getSeeds(): array } else { $seed = new $class(); } - // TODO Replace with with setIo and setConfig - $seed->setEnvironment('default'); - $seed->setInput($input); - $seed->setOutput($output); - - if (!($seed instanceof AbstractSeed)) { - throw new InvalidArgumentException(sprintf( - 'The class "%s" in file "%s" must extend \Phinx\Seed\AbstractSeed', - $class, - $filePath - )); + // Shim phinx seeds so that the rest of migrations + // can be isolated from phinx. + if ($seed instanceof PhinxSeedInterface) { + $seed = new SeedAdapter($seed); } + $seed->setIo($io); + $seed->setConfig($config); $seeds[$class] = $seed; } @@ -1036,13 +1018,6 @@ public function getSeeds(): array return []; } - // TODO remove this - foreach ($this->seeds as $instance) { - if (isset($input) && $instance instanceof AbstractSeed) { - $instance->setInput($input); - } - } - return $this->seeds; } diff --git a/src/SeedInterface.php b/src/SeedInterface.php index d9788122..df94c178 100644 --- a/src/SeedInterface.php +++ b/src/SeedInterface.php @@ -79,9 +79,9 @@ public function getIo(): ?ConsoleIo; /** * Gets the config. * - * @return \Migrations\Config\ConfigInterface + * @return \Migrations\Config\ConfigInterface|null */ - public function getConfig(): ConfigInterface; + public function getConfig(): ?ConfigInterface; /** * Sets the config. @@ -91,26 +91,6 @@ public function getConfig(): ConfigInterface; */ public function setConfig(ConfigInterface $config); - /** - * Gets the input object to be used in migration object - * - * A new InputInteface will be generated each time `getOutput` is called. - * - * @return \Symfony\Component\Console\Input\InputInterface - * @deprecated 4.5.0 Use getIo() instead. - */ - public function getInput(): InputInterface; - - /** - * Gets the output object to be used in migration object - * - * A new OutputInteface will be generated each time `getOutput` is called. - * - * @return \Symfony\Component\Console\Output\OutputInterface - * @deprecated 4.5.0 Use getIo() instead. - */ - public function getOutput(): OutputInterface; - /** * Gets the name. * @@ -195,4 +175,14 @@ public function table(string $tableName, array $options): Table; * @return bool */ public function shouldExecute(): bool; + + /** + * Gives the ability to a seeder to call another seeder. + * This is particularly useful if you need to run the seeders of your applications in a specific sequences, + * for instance to respect foreign key constraints. + * + * @param string $seeder Name of the seeder to call from the current seed + * @return void + */ + public function call(string $seeder): void; } diff --git a/src/Shim/SeedAdapter.php b/src/Shim/SeedAdapter.php new file mode 100644 index 00000000..fe0250eb --- /dev/null +++ b/src/Shim/SeedAdapter.php @@ -0,0 +1,303 @@ +seed, PhinxSeedInterface::INIT)) { + $this->seed->{PhinxSeedInterface::INIT}(); + } + } + + /** + * {@inheritDoc} + */ + public function run(): void + { + $this->seed->run(); + } + + /** + * {@inheritDoc} + */ + public function getDependencies(): array + { + return $this->seed->getDependencies(); + } + + /** + * Sets the database adapter. + * + * @param \Migrations\Db\Adapter\AdapterInterface $adapter Database Adapter + * @return $this + */ + public function setAdapter(AdapterInterface $adapter) + { + $phinxAdapter = new PhinxAdapter($adapter); + $this->seed->setAdapter($phinxAdapter); + $this->adapter = $adapter; + + return $this; + } + + /** + * Gets the database adapter. + * + * @return \Migrations\Db\Adapter\AdapterInterface + */ + public function getAdapter(): AdapterInterface + { + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + $this->seed->setOutput(new OutputAdapter($io)); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * Gets the config. + * + * @return ?\Migrations\Config\ConfigInterface + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * Sets the config. + * + * @param \Migrations\Config\ConfigInterface $config Configuration Object + * @return $this + */ + public function setConfig(ConfigInterface $config) + { + $optionDef = new InputDefinition([ + new InputOption('plugin', mode: InputOption::VALUE_OPTIONAL, default: ''), + new InputOption('connection', mode: InputOption::VALUE_OPTIONAL, default: ''), + new InputOption('source', mode: InputOption::VALUE_OPTIONAL, default: ''), + ]); + $input = new ArrayInput([ + '--plugin' => $config['plugin'] ?? null, + '--source' => $config['source'] ?? null, + '--connection' => $config->getConnection(), + ], $optionDef); + + $this->seed->setInput($input); + $this->config = $config; + + return $this; + } + + /** + * Gets the name. + * + * @return string + */ + public function getName(): string + { + return $this->seed->getName(); + } + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return int + */ + public function execute(string $sql, array $params = []): int + { + return $this->seed->execute($sql, $params); + } + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. To improve + * IDE auto-completion possibility, you can overwrite the query method + * phpDoc in your (typically custom abstract parent) seed class, where + * you can set the return type by the adapter in your current use. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return mixed + */ + public function query(string $sql, array $params = []): mixed + { + return $this->query($sql, $params); + } + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow(string $sql): array|false + { + return $this->fetchRow($sql); + } + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll(string $sql): array + { + return $this->fetchAll($sql); + } + + /** + * Insert data into a table. + * + * @param string $tableName Table name + * @param array $data Data + * @return void + */ + public function insert(string $tableName, array $data): void + { + $this->insert($tableName, $data); + } + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable(string $tableName): bool + { + return $this->hasTable($tableName); + } + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options): Table + { + throw new RuntimeException('Not implemented'); + } + + /** + * Checks to see if the seed should be executed. + * + * Returns true by default. + * + * You can use this to prevent a seed from executing. + * + * @return bool + */ + public function shouldExecute(): bool + { + return $this->seed->shouldExecute(); + } + + /** + * Gives the ability to a seeder to call another seeder. + * This is particularly useful if you need to run the seeders of your applications in a specific sequences, + * for instance to respect foreign key constraints. + * + * @param string $seeder Name of the seeder to call from the current seed + * @return void + */ + public function call(string $seeder): void + { + throw new RuntimeException('Not implemented'); + } +} diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index 764b9a5b..b739ace8 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -2187,9 +2187,9 @@ public function testExecuteANonExistentSeedWorksAsExpected(): void public function testOrderSeeds(): void { $seeds = array_values($this->manager->getSeeds()); - $this->assertInstanceOf('UserSeeder', $seeds[0]); - $this->assertInstanceOf('GSeeder', $seeds[1]); - $this->assertInstanceOf('PostSeeder', $seeds[2]); + $this->assertEquals('UserSeeder', $seeds[0]->getName()); + $this->assertEquals('GSeeder', $seeds[1]->getName()); + $this->assertEquals('PostSeeder', $seeds[2]->getName()); } public function testSeedWillNotBeExecuted(): void @@ -2228,7 +2228,7 @@ public function testGettingIo(): void $this->assertInstanceOf(OutputAdapter::class, $migration->getOutput()); } foreach ($seeds as $seed) { - $this->assertInstanceOf(OutputAdapter::class, $seed->getOutput()); + $this->assertInstanceOf(ConsoleIo::class, $seed->getIo()); } } From 8e5faa49c02f2a064bed7384d026510e6182af7c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 26 Sep 2024 23:18:49 -0400 Subject: [PATCH 08/47] Fix test and phpcs --- src/Migration/Environment.php | 2 +- src/Migration/Manager.php | 7 ++++--- src/SeedInterface.php | 2 -- src/Shim/SeedAdapter.php | 4 ++++ tests/TestCase/Migration/EnvironmentTest.php | 6 ++++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 0ae06d4b..25bc8a5f 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -131,7 +131,7 @@ public function executeMigration(MigrationInterface $migration, string $directio /** * Executes the specified seeder on this environment. * - * @param \Migrations\Seed\SeedInterface $seed Seed + * @param \Migrations\SeedInterface $seed Seed * @return void */ public function executeSeed(SeedInterface $seed): void diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index b2c30c72..a50e2a93 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -474,7 +474,7 @@ public function executeMigration(MigrationInterface $migration, string $directio /** * Execute a seeder against the specified environment. * - * @param \Migrations\Seed\SeedInterface $seed Seed + * @param \Migrations\SeedInterface $seed Seed * @return void */ public function executeSeed(SeedInterface $seed): void @@ -522,7 +522,7 @@ protected function printMigrationStatus(MigrationInterface $migration, string $s /** * Print Seed Status * - * @param \Migrations\Seed\SeedInterface $seed Seed + * @param \Migrations\SeedInterface $seed Seed * @param string $status Status of the seed * @param string|null $duration Duration the seed took the be executed * @return void @@ -992,7 +992,7 @@ public function getSeeds(): array } // instantiate it - /** @var \Phinx\Seed\AbstractSeed $seed */ + /** @var \Phinx\Seed\AbstractSeed|\Migrations\SeedInterface $seed */ if (isset($this->container)) { $seed = $this->container->get($class); } else { @@ -1003,6 +1003,7 @@ public function getSeeds(): array if ($seed instanceof PhinxSeedInterface) { $seed = new SeedAdapter($seed); } + /** @var \Migrations\SeedInterface $seed */ $seed->setIo($io); $seed->setConfig($config); diff --git a/src/SeedInterface.php b/src/SeedInterface.php index df94c178..cd2e3243 100644 --- a/src/SeedInterface.php +++ b/src/SeedInterface.php @@ -12,8 +12,6 @@ use Migrations\Config\ConfigInterface; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Table; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; /** * Seed interface diff --git a/src/Shim/SeedAdapter.php b/src/Shim/SeedAdapter.php index fe0250eb..e2f946be 100644 --- a/src/Shim/SeedAdapter.php +++ b/src/Shim/SeedAdapter.php @@ -119,6 +119,10 @@ public function setAdapter(AdapterInterface $adapter) */ public function getAdapter(): AdapterInterface { + if (!$this->adapter) { + throw new RuntimeException('Cannot call getAdapter() until after setAdapter().'); + } + return $this->adapter; } diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index 6d24b088..0778242e 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -8,6 +8,7 @@ use Migrations\Db\Adapter\AdapterWrapper; use Migrations\Db\Adapter\PdoAdapter; use Migrations\Migration\Environment; +use Migrations\Shim\SeedAdapter; use Phinx\Migration\AbstractMigration; use Phinx\Migration\MigrationInterface; use Phinx\Seed\AbstractSeed; @@ -314,7 +315,6 @@ public function testExecuteSeedInit() $this->environment->setAdapter($adapterStub); - // up $seed = new class ('mockenv', 20110301080000) extends AbstractSeed { public bool $initExecuted = false; public bool $runExecuted = false; @@ -330,7 +330,9 @@ public function run(): void } }; - $this->environment->executeSeed($seed); + $seedWrapper = new SeedAdapter($seed); + $this->environment->executeSeed($seedWrapper); + $this->assertTrue($seed->initExecuted); $this->assertTrue($seed->runExecuted); } From 2c55d5cf12987417af829b8cde781c22e6b1ea4f Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 27 Sep 2024 11:40:23 -0400 Subject: [PATCH 09/47] Fix more phpcs --- src/Shim/SeedAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shim/SeedAdapter.php b/src/Shim/SeedAdapter.php index e2f946be..e404bc39 100644 --- a/src/Shim/SeedAdapter.php +++ b/src/Shim/SeedAdapter.php @@ -61,7 +61,7 @@ class SeedAdapter implements SeedInterface /** * Constructor * - * @param \Phinx\Seed\SeedInterface; + * @param \Phinx\Seed\SeedInterface $seed The seed being decorated */ public function __construct( private PhinxSeedInterface $seed From 62b88cd0a3488cd70c7539c1e2c99c062afcc114 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 28 Sep 2024 23:40:38 -0400 Subject: [PATCH 10/47] Fix up returns and doctypes --- src/Shim/SeedAdapter.php | 95 +++++++++------------------------------- 1 file changed, 20 insertions(+), 75 deletions(-) diff --git a/src/Shim/SeedAdapter.php b/src/Shim/SeedAdapter.php index e404bc39..c33840e8 100644 --- a/src/Shim/SeedAdapter.php +++ b/src/Shim/SeedAdapter.php @@ -38,7 +38,7 @@ class SeedAdapter implements SeedInterface { /** - * A ConsoleIo instance + * A migrations adapter instance * * @var \Migrations\Db\Adapter\AdapterInterface|null */ @@ -98,10 +98,7 @@ public function getDependencies(): array } /** - * Sets the database adapter. - * - * @param \Migrations\Db\Adapter\AdapterInterface $adapter Database Adapter - * @return $this + * {@inheritDoc} */ public function setAdapter(AdapterInterface $adapter) { @@ -113,9 +110,7 @@ public function setAdapter(AdapterInterface $adapter) } /** - * Gets the database adapter. - * - * @return \Migrations\Db\Adapter\AdapterInterface + * {@inheritDoc} */ public function getAdapter(): AdapterInterface { @@ -146,9 +141,7 @@ public function getIo(): ?ConsoleIo } /** - * Gets the config. - * - * @return ?\Migrations\Config\ConfigInterface + * {@inheritDoc} */ public function getConfig(): ?ConfigInterface { @@ -156,10 +149,7 @@ public function getConfig(): ?ConfigInterface } /** - * Sets the config. - * - * @param \Migrations\Config\ConfigInterface $config Configuration Object - * @return $this + * {@inheritDoc} */ public function setConfig(ConfigInterface $config) { @@ -181,9 +171,7 @@ public function setConfig(ConfigInterface $config) } /** - * Gets the name. - * - * @return string + * {@inheritDoc} */ public function getName(): string { @@ -191,11 +179,7 @@ public function getName(): string } /** - * Executes a SQL statement and returns the number of affected rows. - * - * @param string $sql SQL - * @param array $params parameters to use for prepared query - * @return int + * {@inheritDoc} */ public function execute(string $sql, array $params = []): int { @@ -203,75 +187,47 @@ public function execute(string $sql, array $params = []): int } /** - * Executes a SQL statement. - * - * The return type depends on the underlying adapter being used. To improve - * IDE auto-completion possibility, you can overwrite the query method - * phpDoc in your (typically custom abstract parent) seed class, where - * you can set the return type by the adapter in your current use. - * - * @param string $sql SQL - * @param array $params parameters to use for prepared query - * @return mixed + * {@inheritDoc} */ public function query(string $sql, array $params = []): mixed { - return $this->query($sql, $params); + return $this->seed->query($sql, $params); } /** - * Executes a query and returns only one row as an array. - * - * @param string $sql SQL - * @return array|false + * {@inheritDoc} */ public function fetchRow(string $sql): array|false { - return $this->fetchRow($sql); + return $this->seed->fetchRow($sql); } /** - * Executes a query and returns an array of rows. - * - * @param string $sql SQL - * @return array + * {@inheritDoc} */ public function fetchAll(string $sql): array { - return $this->fetchAll($sql); + return $this->seed->fetchAll($sql); } /** - * Insert data into a table. - * - * @param string $tableName Table name - * @param array $data Data - * @return void + * {@inheritDoc} */ public function insert(string $tableName, array $data): void { - $this->insert($tableName, $data); + $this->seed->insert($tableName, $data); } /** - * Checks to see if a table exists. - * - * @param string $tableName Table name - * @return bool + * {@inheritDoc} */ public function hasTable(string $tableName): bool { - return $this->hasTable($tableName); + return $this->seed->hasTable($tableName); } /** - * Returns an instance of the \Table class. - * - * You can use this class to create and manipulate tables. - * - * @param string $tableName Table name - * @param array $options Options - * @return \Migrations\Db\Table + * {@inheritDoc} */ public function table(string $tableName, array $options): Table { @@ -279,13 +235,7 @@ public function table(string $tableName, array $options): Table } /** - * Checks to see if the seed should be executed. - * - * Returns true by default. - * - * You can use this to prevent a seed from executing. - * - * @return bool + * {@inheritDoc} */ public function shouldExecute(): bool { @@ -293,12 +243,7 @@ public function shouldExecute(): bool } /** - * Gives the ability to a seeder to call another seeder. - * This is particularly useful if you need to run the seeders of your applications in a specific sequences, - * for instance to respect foreign key constraints. - * - * @param string $seeder Name of the seeder to call from the current seed - * @return void + * {@inheritDoc} */ public function call(string $seeder): void { From 131c717c307ed2df4f42d8bc3718cd955d05a18e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 28 Sep 2024 23:40:49 -0400 Subject: [PATCH 11/47] Move phinx compatibility shim logic into a migration wrapper Start adapting the internals for Migrations to not rely on phinx. Like seeds, there will be a wrapping adapter to shim the behavior up. --- src/Db/Adapter/AdapterInterface.php | 10 +- src/Db/Adapter/AdapterWrapper.php | 2 +- src/Db/Adapter/PdoAdapter.php | 4 +- src/Db/Adapter/SqlserverAdapter.php | 4 +- src/Migration/Environment.php | 17 +- src/Migration/Manager.php | 43 +- src/MigrationInterface.php | 24 +- src/Shim/MigrationAdapter.php | 430 +++++++++++++++++++ tests/TestCase/Migration/EnvironmentTest.php | 5 +- tests/TestCase/Migration/ManagerTest.php | 14 +- 10 files changed, 470 insertions(+), 83 deletions(-) create mode 100644 src/Shim/MigrationAdapter.php diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 96d4f538..fcc67cc7 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -18,7 +18,7 @@ use Migrations\Db\Literal; use Migrations\Db\Table\Column; use Migrations\Db\Table\Table; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; /** * Adapter Interface. @@ -138,7 +138,7 @@ public function getColumnForType(string $columnName, string $type, array $option /** * Records a migration being run. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param string $startTime Start Time * @param string $endTime End Time @@ -149,7 +149,7 @@ public function migrated(MigrationInterface $migration, string $direction, strin /** * Toggle a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @return $this */ public function toggleBreakpoint(MigrationInterface $migration); @@ -164,7 +164,7 @@ public function resetAllBreakpoints(): int; /** * Set a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint set + * @param \Migrations\MigrationInterface $migration The migration target for the breakpoint set * @return $this */ public function setBreakpoint(MigrationInterface $migration); @@ -172,7 +172,7 @@ public function setBreakpoint(MigrationInterface $migration); /** * Unset a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint unset + * @param \Migrations\MigrationInterface $migration The migration target for the breakpoint unset * @return $this */ public function unsetBreakpoint(MigrationInterface $migration); diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php index 42e4f83e..7fe5ba63 100644 --- a/src/Db/Adapter/AdapterWrapper.php +++ b/src/Db/Adapter/AdapterWrapper.php @@ -18,7 +18,7 @@ use Migrations\Db\Literal; use Migrations\Db\Table\Column; use Migrations\Db\Table\Table; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; /** * Adapter Wrapper. diff --git a/src/Db/Adapter/PdoAdapter.php b/src/Db/Adapter/PdoAdapter.php index d1a462a5..1c30f755 100644 --- a/src/Db/Adapter/PdoAdapter.php +++ b/src/Db/Adapter/PdoAdapter.php @@ -36,10 +36,10 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; +use Migrations\MigrationInterface; use PDO; use PDOException; use Phinx\Config\Config; -use Phinx\Migration\MigrationInterface; use Phinx\Util\Literal as PhinxLiteral; use ReflectionMethod; use RuntimeException; @@ -570,7 +570,7 @@ public function unsetBreakpoint(MigrationInterface $migration): AdapterInterface /** * Mark a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint + * @param \Migrations\MigrationInterface $migration The migration target for the breakpoint * @param bool $state The required state of the breakpoint * @return \Migrations\Db\Adapter\AdapterInterface */ diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index f3667c3e..99f0ec4f 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -16,7 +16,7 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; /** * Migrations SqlServer Adapter. @@ -1238,7 +1238,7 @@ public function getColumnTypes(): array /** * Records a migration being run. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param string $startTime Start Time * @param string $endTime End Time diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 25bc8a5f..6077cba9 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -14,7 +14,7 @@ use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Adapter\PhinxAdapter; use Migrations\SeedInterface; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; use RuntimeException; class Environment @@ -62,7 +62,7 @@ public function __construct(string $name, array $options) /** * Executes the specified migration on this environment. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration * @return void @@ -73,11 +73,11 @@ public function executeMigration(MigrationInterface $migration, string $directio $migration->setMigratingUp($direction === MigrationInterface::UP); $startTime = time(); + // Use an adapter shim to bridge between the new migrations // engine and the Phinx compatible interface $adapter = $this->getAdapter(); - $phinxShim = new PhinxAdapter($adapter); - $migration->setAdapter($phinxShim); + $migration->setAdapter($adapter); $migration->preFlightCheck(); @@ -91,6 +91,10 @@ public function executeMigration(MigrationInterface $migration, string $directio } if (!$fake) { + // TODO this is tricky as the adapter would need to implement + // this method, but then it always exists. One option is to copy this + // is to special case the adapter and move this logic there? + // Run the migration if (method_exists($migration, MigrationInterface::CHANGE)) { if ($direction === MigrationInterface::DOWN) { @@ -102,13 +106,12 @@ public function executeMigration(MigrationInterface $migration, string $directio ->getWrapper('record', $adapter); // Wrap the adapter with a phinx shim to maintain contain - $phinxAdapter = new PhinxAdapter($recordAdapter); - $migration->setAdapter($phinxAdapter); + $migration->setAdapter($adapter); $migration->{MigrationInterface::CHANGE}(); $recordAdapter->executeInvertedCommands(); - $migration->setAdapter(new PhinxAdapter($this->getAdapter())); + $migration->setAdapter($this->getAdapter()); } else { $migration->{MigrationInterface::CHANGE}(); } diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index a50e2a93..a9ed2c96 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -14,16 +14,15 @@ use Exception; use InvalidArgumentException; use Migrations\Config\ConfigInterface; +use Migrations\MigrationInterface; use Migrations\SeedInterface; -use Migrations\Shim\OutputAdapter; +use Migrations\Shim\MigrationAdapter; use Migrations\Shim\SeedAdapter; -use Phinx\Migration\AbstractMigration; -use Phinx\Migration\MigrationInterface; +use Phinx\Migration\MigrationInterface as PhinxMigrationInterface; use Phinx\Seed\SeedInterface as PhinxSeedInterface; use Phinx\Util\Util; use Psr\Container\ContainerInterface; use RuntimeException; -use Symfony\Component\Console\Input\ArrayInput; class Manager { @@ -771,7 +770,7 @@ public function setContainer(ContainerInterface $container) /** * Sets the database migrations. * - * @param \Phinx\Migration\AbstractMigration[] $migrations Migrations + * @param \Migrations\MigrationInterface[] $migrations Migrations * @return $this */ public function setMigrations(array $migrations) @@ -786,7 +785,7 @@ public function setMigrations(array $migrations) * order * * @throws \InvalidArgumentException - * @return \Phinx\Migration\MigrationInterface[] + * @return \Migrations\MigrationInterface[] */ public function getMigrations(): array { @@ -806,7 +805,7 @@ function ($phpFile) { // filter the files to only get the ones that match our naming scheme $fileNames = []; - /** @var \Phinx\Migration\AbstractMigration[] $versions */ + /** @var \Migration\MigrationInterface[] $versions */ $versions = []; $io = $this->getIo(); @@ -850,29 +849,15 @@ function ($phpFile) { } $io->verbose("Constructing $class."); - - $config = $this->getConfig(); - // TODO Subset config and pass forward. - // Move this to the Migration/phinx shim - $input = new ArrayInput([ - '--plugin' => $config['plugin'] ?? null, - '--source' => $config['source'] ?? null, - '--connection' => $config->getConnection(), - ]); - // TODO move this to the migration/phinx shim - $output = new OutputAdapter($io); - - // TODO constructor should take $io and $config - // instantiate it - $migration = new $class('default', $version, $input, $output); - - if (!($migration instanceof AbstractMigration)) { - throw new InvalidArgumentException(sprintf( - 'The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration', - $class, - $filePath - )); + /** @var \Migrations\MigrationInterface $migration */ + if (is_subclass_of($class, PhinxMigrationInterface::class)) { + $migration = new MigrationAdapter($class, $version); + } else { + $migration = new $class($version); } + $config = $this->getConfig(); + $migration->setConfig($config); + $migration->setIo($io); $versions[$version] = $migration; } else { diff --git a/src/MigrationInterface.php b/src/MigrationInterface.php index 2a8e505d..289a1065 100644 --- a/src/MigrationInterface.php +++ b/src/MigrationInterface.php @@ -80,9 +80,9 @@ public function getIo(): ?ConsoleIo; /** * Gets the config. * - * @return \Migrations\Config\ConfigInterface + * @return \Migrations\Config\ConfigInterface|null */ - public function getConfig(): ConfigInterface; + public function getConfig(): ?ConfigInterface; /** * Sets the config. @@ -92,26 +92,6 @@ public function getConfig(): ConfigInterface; */ public function setConfig(ConfigInterface $config); - /** - * Gets the input object to be used in migration object - * - * A new InputInterface will be generated each time `getOutput` is called. - * - * @return \Symfony\Component\Console\Input\InputInterface|null - * @deprecated 4.5.0 Use getIo() instead. - */ - public function getInput(): ?InputInterface; - - /** - * Gets the output object to be used in migration object - * - * A new OutputInterface will be generated each time `getOutput` is called. - * - * @return \Symfony\Component\Console\Output\OutputInterface|null - * @deprecated 4.5.0 Use getIo() instead. - */ - public function getOutput(): ?OutputInterface; - /** * Gets the name. * diff --git a/src/Shim/MigrationAdapter.php b/src/Shim/MigrationAdapter.php new file mode 100644 index 00000000..fbd31b1b --- /dev/null +++ b/src/Shim/MigrationAdapter.php @@ -0,0 +1,430 @@ +migration = new $migrationClass('default', $version); + } else { + if (!is_subclass_of($migrationClass, PhinxMigrationInterface::class)) { + throw new RuntimeException( + 'The provided $migrationClass must be a ' . + 'subclass of Phinx\Migration\MigrationInterface' + ); + } + $this->migration = $migrationClass; + } + } + + /** + * Because we're a compatibility shim, we implement this hook + * so that it can be conditionally called when it is implemented. + * + * @return void + */ + public function init(): void + { + if (method_exists($this->migration, MigrationInterface::INIT)) { + $this->migration->{MigrationInterface::INIT}(); + } + } + + /** + * {@inheritDoc} + */ + public function setAdapter(AdapterInterface $adapter) + { + $phinxAdapter = new PhinxAdapter($adapter); + $this->migration->setAdapter($phinxAdapter); + $this->adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Cannot call getAdapter() until after setAdapter().'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + $this->migration->setOutput(new OutputAdapter($io)); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $input = new ArrayInput([ + '--plugin' => $config['plugin'] ?? null, + '--source' => $config['source'] ?? null, + '--connection' => $config->getConnection(), + ]); + + $this->migration->setInput($input); + $this->config = $config; + + return $this; + } + + /** + * Gets the name. + * + * @return string + */ + public function getName(): string + { + return $this->migration->getName(); + } + + /** + * Sets the migration version number. + * + * @param int $version Version + * @return $this + */ + public function setVersion(int $version) + { + $this->migration->setVersion($version); + + return $this; + } + + /** + * Gets the migration version number. + * + * @return int + */ + public function getVersion(): int + { + return $this->migration->getVersion(); + } + + /** + * Sets whether this migration is being applied or reverted + * + * @param bool $isMigratingUp True if the migration is being applied + * @return $this + */ + public function setMigratingUp(bool $isMigratingUp) + { + $this->migration->setMigratingUp($isMigratingUp); + + return $this; + } + + /** + * Gets whether this migration is being applied or reverted. + * True means that the migration is being applied. + * + * @return bool + */ + public function isMigratingUp(): bool + { + return $this->migration->isMigratingUp(); + } + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return int + */ + public function execute(string $sql, array $params = []): int + { + return $this->migration->execute($sql, $params); + } + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. To improve + * IDE auto-completion possibility, you can overwrite the query method + * phpDoc in your (typically custom abstract parent) migration class, where + * you can set the return type by the adapter in your current use. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return mixed + */ + public function query(string $sql, array $params = []): mixed + { + return $this->migration->query($sql, $params); + } + + /** + * Returns a new Query object that can be used to build complex SELECT, UPDATE, INSERT or DELETE + * queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @see https://api.cakephp.org/3.6/class-Cake.Database.Query.html + * @param string $type Query + * @return \Cake\Database\Query + */ + public function getQueryBuilder(string $type): Query + { + return $this->migration->getQueryBuilder($type); + } + + /** + * Returns a new SelectQuery object that can be used to build complex + * SELECT queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\SelectQuery + */ + public function getSelectBuilder(): SelectQuery + { + return $this->migration->getSelectBuilder(); + } + + /** + * Returns a new InsertQuery object that can be used to build complex + * INSERT queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\InsertQuery + */ + public function getInsertBuilder(): InsertQuery + { + return $this->migration->getInsertBuilder(); + } + + /** + * Returns a new UpdateQuery object that can be used to build complex + * UPDATE queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\UpdateQuery + */ + public function getUpdateBuilder(): UpdateQuery + { + return $this->migration->getUpdateBuilder(); + } + + /** + * Returns a new DeleteQuery object that can be used to build complex + * DELETE queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\DeleteQuery + */ + public function getDeleteBuilder(): DeleteQuery + { + return $this->migration->getDeleteBuilder(); + } + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow(string $sql): array|false + { + return $this->migration->fetchRow($sql); + } + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll(string $sql): array + { + return $this->migration->fetchAll($sql); + } + + /** + * Create a new database. + * + * @param string $name Database Name + * @param array $options Options + * @return void + */ + public function createDatabase(string $name, array $options): void + { + $this->migration->createDatabase($name, $options); + } + + /** + * Drop a database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase(string $name): void + { + $this->migration->dropDatabase($name); + } + + /** + * {@inheritDoc} + */ + public function createSchema(string $name): void + { + $this->migration->createSchema($name); + } + + /** + * {@inheritDoc} + */ + public function dropSchema(string $name): void + { + $this->migration->dropSchema($name); + } + + /** + * {@inheritDoc} + */ + public function hasTable(string $tableName): bool + { + return $this->migration->hasTable($tableName); + } + + /** + * {@inheritDoc} + */ + public function table(string $tableName, array $options): Table + { + throw new RuntimeException('MigrationAdapter::table is not implemented'); + } + + /** + * {@inheritDoc} + */ + public function preFlightCheck(): void + { + $this->migration->preFlightCheck(); + } + + /** + * {@inheritDoc} + */ + public function postFlightCheck(): void + { + $this->migration->postFlightCheck(); + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return $this->migration->shouldExecute(); + } +} diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index 0778242e..72fceba0 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -8,6 +8,7 @@ use Migrations\Db\Adapter\AdapterWrapper; use Migrations\Db\Adapter\PdoAdapter; use Migrations\Migration\Environment; +use Migrations\Shim\MigrationAdapter; use Migrations\Shim\SeedAdapter; use Phinx\Migration\AbstractMigration; use Phinx\Migration\MigrationInterface; @@ -300,8 +301,8 @@ public function up(): void $this->upExecuted = true; } }; - - $this->environment->executeMigration($upMigration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($upMigration, $upMigration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($upMigration->initExecuted); $this->assertTrue($upMigration->upExecuted); } diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index b739ace8..ad654b15 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -2205,18 +2205,6 @@ public function testSeedWillNotBeExecuted(): void $this->assertStringContainsString('skipped', $output); } - public function testGettingInputObject(): void - { - $migrations = $this->manager->getMigrations(); - $seeds = $this->manager->getSeeds(); - foreach ($migrations as $migration) { - $this->assertInstanceOf(InputInterface::class, $migration->getInput()); - } - foreach ($seeds as $seed) { - $this->assertInstanceOf(InputInterface::class, $migration->getInput()); - } - } - public function testGettingIo(): void { $migrations = $this->manager->getMigrations(); @@ -2225,7 +2213,7 @@ public function testGettingIo(): void $this->assertInstanceOf(ConsoleIo::class, $io); foreach ($migrations as $migration) { - $this->assertInstanceOf(OutputAdapter::class, $migration->getOutput()); + $this->assertInstanceOf(ConsoleIo::class, $migration->getIo()); } foreach ($seeds as $seed) { $this->assertInstanceOf(ConsoleIo::class, $seed->getIo()); From 0629a5e3d34fe808a724456a9eb10e36070fe9dc Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 29 Sep 2024 00:17:19 -0400 Subject: [PATCH 12/47] Get more tests passing --- src/Migration/Environment.php | 49 ++++++++++---------- src/Migration/Manager.php | 12 ++++- src/Shim/MigrationAdapter.php | 35 ++++++++++++++ tests/TestCase/Migration/EnvironmentTest.php | 18 ++++--- 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 6077cba9..0c1ff6a7 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -15,6 +15,7 @@ use Migrations\Db\Adapter\PhinxAdapter; use Migrations\SeedInterface; use Migrations\MigrationInterface; +use Migrations\Shim\MigrationAdapter; use RuntimeException; class Environment @@ -91,32 +92,32 @@ public function executeMigration(MigrationInterface $migration, string $directio } if (!$fake) { - // TODO this is tricky as the adapter would need to implement - // this method, but then it always exists. One option is to copy this - // is to special case the adapter and move this logic there? - - // Run the migration - if (method_exists($migration, MigrationInterface::CHANGE)) { - if ($direction === MigrationInterface::DOWN) { - // Create an instance of the RecordingAdapter so we can record all - // of the migration commands for reverse playback - - /** @var \Migrations\Db\Adapter\RecordingAdapter $recordAdapter */ - $recordAdapter = AdapterFactory::instance() - ->getWrapper('record', $adapter); - - // Wrap the adapter with a phinx shim to maintain contain - $migration->setAdapter($adapter); - - $migration->{MigrationInterface::CHANGE}(); - $recordAdapter->executeInvertedCommands(); - - $migration->setAdapter($this->getAdapter()); + if ($migration instanceof MigrationAdapter) { + $migration->applyDirection($direction); + } else { + // Run the migration + if (method_exists($migration, MigrationInterface::CHANGE)) { + if ($direction === MigrationInterface::DOWN) { + // Create an instance of the RecordingAdapter so we can record all + // of the migration commands for reverse playback + + /** @var \Migrations\Db\Adapter\RecordingAdapter $recordAdapter */ + $recordAdapter = AdapterFactory::instance() + ->getWrapper('record', $adapter); + + // Wrap the adapter with a phinx shim to maintain contain + $migration->setAdapter($adapter); + + $migration->{MigrationInterface::CHANGE}(); + $recordAdapter->executeInvertedCommands(); + + $migration->setAdapter($this->getAdapter()); + } else { + $migration->{MigrationInterface::CHANGE}(); + } } else { - $migration->{MigrationInterface::CHANGE}(); + $migration->{$direction}(); } - } else { - $migration->{$direction}(); } } diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index a9ed2c96..b18591df 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -257,11 +257,19 @@ public function markMigrated(int $version, string $path): bool /** @var class-string<\Phinx\Migration\MigrationInterface> $className */ $className = $this->getMigrationClassName($migrationFile); require_once $migrationFile; - $Migration = new $className('default', $version); + + /** @var \Migrations\MigrationInterface $migration */ + if (is_subclass_of($className, PhinxMigrationInterface::class)) { + $migration = new MigrationAdapter($className, $version); + } else { + $migration = new $className($version); + } + $config = $this->getConfig(); + $migration->setConfig($config); $time = date('Y-m-d H:i:s', time()); - $adapter->migrated($Migration, 'up', $time, $time); + $adapter->migrated($migration, 'up', $time, $time); return true; } diff --git a/src/Shim/MigrationAdapter.php b/src/Shim/MigrationAdapter.php index fbd31b1b..4452efae 100644 --- a/src/Shim/MigrationAdapter.php +++ b/src/Shim/MigrationAdapter.php @@ -19,6 +19,7 @@ use Migrations\Db\Adapter\PhinxAdapter; use Migrations\Db\Table; use Migrations\MigrationInterface; +use Phinx\Db\Adapter\AdapterFactory as PhinxAdapterFactory; use Phinx\Migration\MigrationInterface as PhinxMigrationInterface; use RuntimeException; use Symfony\Component\Console\Input\ArrayInput; @@ -101,6 +102,40 @@ public function init(): void } } + /** + * Compatibility shim for executing change/up/down + */ + public function applyDirection(string $direction): void + { + $adapter = $this->getAdapter(); + + // Run the migration + if (method_exists($this->migration, MigrationInterface::CHANGE)) { + if ($direction === MigrationInterface::DOWN) { + // Create an instance of the RecordingAdapter so we can record all + // of the migration commands for reverse playback + $adapter = $this->migration->getAdapter(); + assert($adapter !== null, 'Adapter must be set in migration'); + + /** @var \Phinx\Db\Adapter\ProxyAdapter $proxyAdapter */ + $proxyAdapter = PhinxAdapterFactory::instance() + ->getWrapper('proxy', $adapter); + + // Wrap the adapter with a phinx shim to maintain contain + $this->migration->setAdapter($proxyAdapter); + + $this->migration->{MigrationInterface::CHANGE}(); + $proxyAdapter->executeInvertedCommands(); + + $this->migration->setAdapter($adapter); + } else { + $this->migration->{MigrationInterface::CHANGE}(); + } + } else { + $this->migration->{$direction}(); + } + } + /** * {@inheritDoc} */ diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index 72fceba0..bb6a5c4e 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -131,7 +131,8 @@ public function up(): void } }; - $this->environment->executeMigration($upMigration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($upMigration, $upMigration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($upMigration->executed); } @@ -156,7 +157,8 @@ public function down(): void } }; - $this->environment->executeMigration($downMigration, MigrationInterface::DOWN); + $migrationWrapper = new MigrationAdapter($downMigration, $downMigration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::DOWN); $this->assertTrue($downMigration->executed); } @@ -187,7 +189,8 @@ public function up(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($migration->executed); } @@ -212,7 +215,8 @@ public function change(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($migration->executed); } @@ -237,7 +241,8 @@ public function change(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::DOWN); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::DOWN); $this->assertTrue($migration->executed); } @@ -262,7 +267,8 @@ public function change(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP, true); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP, true); $this->assertFalse($migration->executed); } From 46a344366838fa57d95162eb52eae1e5b66108f9 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 30 Sep 2024 09:04:17 -0400 Subject: [PATCH 13/47] Fix phpcs and psalm --- src/Db/Adapter/PhinxAdapter.php | 23 ++++++++++++++--------- src/Migration/Environment.php | 3 +-- src/Migration/Manager.php | 14 +++++++------- src/MigrationInterface.php | 2 -- src/Shim/MigrationAdapter.php | 1 - tests/TestCase/Migration/ManagerTest.php | 2 -- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Db/Adapter/PhinxAdapter.php b/src/Db/Adapter/PhinxAdapter.php index f3cf451d..492ca369 100644 --- a/src/Db/Adapter/PhinxAdapter.php +++ b/src/Db/Adapter/PhinxAdapter.php @@ -33,6 +33,7 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; +use Migrations\Shim\MigrationAdapter; use Phinx\Db\Action\Action as PhinxAction; use Phinx\Db\Action\AddColumn as PhinxAddColumn; use Phinx\Db\Action\AddForeignKey as PhinxAddForeignKey; @@ -52,7 +53,7 @@ use Phinx\Db\Table\ForeignKey as PhinxForeignKey; use Phinx\Db\Table\Index as PhinxIndex; use Phinx\Db\Table\Table as PhinxTable; -use Phinx\Migration\MigrationInterface; +use Phinx\Migration\MigrationInterface as PhinxMigrationInterface; use Phinx\Util\Literal as PhinxLiteral; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; @@ -491,9 +492,10 @@ public function getVersionLog(): array /** * @inheritDoc */ - public function migrated(MigrationInterface $migration, string $direction, string $startTime, string $endTime): PhinxAdapterInterface + public function migrated(PhinxMigrationInterface $migration, string $direction, string $startTime, string $endTime): PhinxAdapterInterface { - $this->adapter->migrated($migration, $direction, $startTime, $endTime); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->migrated($wrapped, $direction, $startTime, $endTime); return $this; } @@ -501,9 +503,10 @@ public function migrated(MigrationInterface $migration, string $direction, strin /** * @inheritDoc */ - public function toggleBreakpoint(MigrationInterface $migration): PhinxAdapterInterface + public function toggleBreakpoint(PhinxMigrationInterface $migration): PhinxAdapterInterface { - $this->adapter->toggleBreakpoint($migration); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->toggleBreakpoint($wrapped); return $this; } @@ -519,9 +522,10 @@ public function resetAllBreakpoints(): int /** * @inheritDoc */ - public function setBreakpoint(MigrationInterface $migration): PhinxAdapterInterface + public function setBreakpoint(PhinxMigrationInterface $migration): PhinxAdapterInterface { - $this->adapter->setBreakpoint($migration); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->setBreakpoint($wrapped); return $this; } @@ -529,9 +533,10 @@ public function setBreakpoint(MigrationInterface $migration): PhinxAdapterInterf /** * @inheritDoc */ - public function unsetBreakpoint(MigrationInterface $migration): PhinxAdapterInterface + public function unsetBreakpoint(PhinxMigrationInterface $migration): PhinxAdapterInterface { - $this->adapter->unsetBreakpoint($migration); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->unsetBreakpoint($wrapped); return $this; } diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 0c1ff6a7..28d72572 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -12,9 +12,8 @@ use Cake\Datasource\ConnectionManager; use Migrations\Db\Adapter\AdapterFactory; use Migrations\Db\Adapter\AdapterInterface; -use Migrations\Db\Adapter\PhinxAdapter; -use Migrations\SeedInterface; use Migrations\MigrationInterface; +use Migrations\SeedInterface; use Migrations\Shim\MigrationAdapter; use RuntimeException; diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index b18591df..59872621 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -46,7 +46,7 @@ class Manager protected ?Environment $environment; /** - * @var \Phinx\Migration\MigrationInterface[]|null + * @var \Migrations\MigrationInterface[]|null */ protected ?array $migrations = null; @@ -254,16 +254,16 @@ public function markMigrated(int $version, string $path): bool } $migrationFile = $migrationFile[0]; - /** @var class-string<\Phinx\Migration\MigrationInterface> $className */ + /** @var class-string<\Phinx\Migration\MigrationInterface|\Migrations\MigrationInterface> $className */ $className = $this->getMigrationClassName($migrationFile); require_once $migrationFile; - /** @var \Migrations\MigrationInterface $migration */ if (is_subclass_of($className, PhinxMigrationInterface::class)) { $migration = new MigrationAdapter($className, $version); } else { $migration = new $className($version); } + /** @var \Migrations\MigrationInterface $migration */ $config = $this->getConfig(); $migration->setConfig($config); @@ -448,7 +448,7 @@ public function migrate(?int $version = null, bool $fake = false): void /** * Execute a migration against the specified environment. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration * @return void @@ -512,7 +512,7 @@ public function executeSeed(SeedInterface $seed): void /** * Print Migration Status * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $status Status of the migration * @param string|null $duration Duration the migration took the be executed * @return void @@ -813,7 +813,7 @@ function ($phpFile) { // filter the files to only get the ones that match our naming scheme $fileNames = []; - /** @var \Migration\MigrationInterface[] $versions */ + /** @var \Migrations\MigrationInterface[] $versions */ $versions = []; $io = $this->getIo(); @@ -857,12 +857,12 @@ function ($phpFile) { } $io->verbose("Constructing $class."); - /** @var \Migrations\MigrationInterface $migration */ if (is_subclass_of($class, PhinxMigrationInterface::class)) { $migration = new MigrationAdapter($class, $version); } else { $migration = new $class($version); } + /** @var \Migrations\MigrationInterface $migration */ $config = $this->getConfig(); $migration->setConfig($config); $migration->setIo($io); diff --git a/src/MigrationInterface.php b/src/MigrationInterface.php index 289a1065..7b6153f9 100644 --- a/src/MigrationInterface.php +++ b/src/MigrationInterface.php @@ -17,8 +17,6 @@ use Migrations\Config\ConfigInterface; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Table; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; /** * Migration interface. diff --git a/src/Shim/MigrationAdapter.php b/src/Shim/MigrationAdapter.php index 4452efae..6cc13236 100644 --- a/src/Shim/MigrationAdapter.php +++ b/src/Shim/MigrationAdapter.php @@ -64,7 +64,6 @@ class MigrationAdapter implements MigrationInterface * * @param string|object $migrationClass The phinx migration to adapt * @param int $version The migration version - * @param \Cake\Console\ConsoleIo $io The io */ public function __construct( string|object $migrationClass, diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index ad654b15..58393ef7 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -13,12 +13,10 @@ use Migrations\Db\Adapter\AdapterInterface; use Migrations\Migration\Environment; use Migrations\Migration\Manager; -use Migrations\Shim\OutputAdapter; use Phinx\Console\Command\AbstractCommand; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; -use Symfony\Component\Console\Input\InputInterface; class ManagerTest extends TestCase { From 17ead38c8cd3e2bb5be52cc700c748aff084a6c3 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 3 Oct 2024 23:51:28 -0400 Subject: [PATCH 14/47] Import most of Phinx\Util\Util Most but not all. I've only included the methods we're currently relying on in migrations. This is another step towards decoupling from phinx for the new backend. --- src/Command/BakeSimpleMigrationCommand.php | 2 +- src/Command/Phinx/Create.php | 2 +- src/Migration/Manager.php | 2 +- src/Table.php | 2 + src/Util/Util.php | 319 +++++++++++++++++++++ 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 src/Util/Util.php diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php index 7c3f06a8..879ee237 100644 --- a/src/Command/BakeSimpleMigrationCommand.php +++ b/src/Command/BakeSimpleMigrationCommand.php @@ -21,7 +21,7 @@ use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Utility\Inflector; -use Phinx\Util\Util; +use Migrations\Util\Util; /** * Task class for generating migration snapshot files. diff --git a/src/Command/Phinx/Create.php b/src/Command/Phinx/Create.php index 51d0d441..c619a25f 100644 --- a/src/Command/Phinx/Create.php +++ b/src/Command/Phinx/Create.php @@ -15,8 +15,8 @@ use Cake\Utility\Inflector; use Migrations\ConfigurationTrait; +use Migrations\Util\Util; use Phinx\Console\Command\Create as CreateCommand; -use Phinx\Util\Util; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 59872621..dbdb159c 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -18,9 +18,9 @@ use Migrations\SeedInterface; use Migrations\Shim\MigrationAdapter; use Migrations\Shim\SeedAdapter; +use Migrations\Util\Util; use Phinx\Migration\MigrationInterface as PhinxMigrationInterface; use Phinx\Seed\SeedInterface as PhinxSeedInterface; -use Phinx\Util\Util; use Psr\Container\ContainerInterface; use RuntimeException; diff --git a/src/Table.php b/src/Table.php index bfb504b9..5cc3feca 100644 --- a/src/Table.php +++ b/src/Table.php @@ -21,6 +21,8 @@ use Phinx\Util\Literal; /** + * TODO figure out how to update this for built-in backend. + * * @method \Migrations\CakeAdapter getAdapter() */ class Table extends BaseTable diff --git a/src/Util/Util.php b/src/Util/Util.php new file mode 100644 index 00000000..de00455e --- /dev/null +++ b/src/Util/Util.php @@ -0,0 +1,319 @@ +format(static::DATE_FORMAT); + } + + /** + * Gets an array of all the existing migration class names. + * + * @param string $path Path + * @return string[] + */ + public static function getExistingMigrationClassNames(string $path): array + { + $classNames = []; + + if (!is_dir($path)) { + return $classNames; + } + + // filter the files to only get the ones that match our naming scheme + $phpFiles = static::getFiles($path); + + foreach ($phpFiles as $filePath) { + $fileName = basename($filePath); + if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName)) { + $classNames[] = static::mapFileNameToClassName($fileName); + } + } + + return $classNames; + } + + /** + * Get the version from the beginning of a file name. + * + * @param string $fileName File Name + * @return int + */ + public static function getVersionFromFileName(string $fileName): int + { + $matches = []; + preg_match('/^[0-9]+/', basename($fileName), $matches); + $value = (int)($matches[0] ?? null); + if (!$value) { + throw new RuntimeException(sprintf('Cannot get a valid version from filename `%s`', $fileName)); + } + + return $value; + } + + /** + * Turn migration names like 'CreateUserTable' into file names like + * '12345678901234_create_user_table.php' or 'LimitResourceNamesTo30Chars' into + * '12345678901234_limit_resource_names_to_30_chars.php'. + * + * @param string $className Class Name + * @return string + */ + public static function mapClassNameToFileName(string $className): string + { + // TODO replace with Inflector + $snake = function ($matches) { + return '_' . strtolower($matches[0]); + }; + $fileName = preg_replace_callback('/\d+|[A-Z]/', $snake, $className); + $fileName = static::getCurrentTimestamp() . "$fileName.php"; + + return $fileName; + } + + /** + * Turn file names like '12345678901234_create_user_table.php' into class + * names like 'CreateUserTable'. + * + * @param string $fileName File Name + * @return string + */ + public static function mapFileNameToClassName(string $fileName): string + { + // TODO replace with Inflector + $matches = []; + if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName, $matches)) { + $fileName = $matches[1]; + } elseif (preg_match(static::MIGRATION_FILE_NAME_NO_NAME_PATTERN, $fileName)) { + return 'V' . substr($fileName, 0, strlen($fileName) - 4); + } + + $className = str_replace('_', '', ucwords($fileName, '_')); + + return $className; + } + + /** + * Check if a migration class name is unique regardless of the + * timestamp. + * + * This method takes a class name and a path to a migrations directory. + * + * Migration class names must be in PascalCase format but consecutive + * capitals are allowed. + * e.g: AddIndexToPostsTable or CustomHTMLTitle. + * + * @param string $className Class Name + * @param string $path Path + * @return bool + */ + public static function isUniqueMigrationClassName(string $className, string $path): bool + { + $existingClassNames = static::getExistingMigrationClassNames($path); + + return !in_array($className, $existingClassNames, true); + } + + /** + * Check if a migration/seed class name is valid. + * + * Migration & Seed class names must be in CamelCase format. + * e.g: CreateUserTable, AddIndexToPostsTable or UserSeeder. + * + * Single words are not allowed on their own. + * + * @param string $className Class Name + * @return bool + */ + public static function isValidPhinxClassName(string $className): bool + { + return (bool)preg_match(static::CLASS_NAME_PATTERN, $className); + } + + /** + * Check if a migration file name is valid. + * + * @param string $fileName File Name + * @return bool + */ + public static function isValidMigrationFileName(string $fileName): bool + { + return (bool)preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName) + || (bool)preg_match(static::MIGRATION_FILE_NAME_NO_NAME_PATTERN, $fileName); + } + + /** + * Check if a seed file name is valid. + * + * @param string $fileName File Name + * @return bool + */ + public static function isValidSeedFileName(string $fileName): bool + { + return (bool)preg_match(static::SEED_FILE_NAME_PATTERN, $fileName); + } + + /** + * Expands a set of paths with curly braces (if supported by the OS). + * + * @param string[] $paths Paths + * @return string[] + */ + public static function globAll(array $paths): array + { + $result = []; + + foreach ($paths as $path) { + $result = array_merge($result, static::glob($path)); + } + + return $result; + } + + /** + * Expands a path with curly braces (if supported by the OS). + * + * @param string $path Path + * @return string[] + */ + public static function glob(string $path): array + { + return glob($path, defined('GLOB_BRACE') ? GLOB_BRACE : 0); + } + + /** + * Takes the path to a php file and attempts to include it if readable + * + * @param string $filename Filename + * @param \Symfony\Component\Console\Input\InputInterface|null $input Input + * @param \Symfony\Component\Console\Output\OutputInterface|null $output Output + * @param \Phinx\Console\Command\AbstractCommand|mixed|null $context Context + * @throws \Exception + * @return string + */ + public static function loadPhpFile(string $filename, ?InputInterface $input = null, ?OutputInterface $output = null, mixed $context = null): string + { + $filePath = realpath($filename); + if (!file_exists($filePath)) { + throw new Exception(sprintf("File does not exist: %s \n", $filename)); + } + + /** + * I lifed this from phpunits FileLoader class + * + * @see https://github.com/sebastianbergmann/phpunit/pull/2751 + */ + $isReadable = @fopen($filePath, 'r') !== false; + + if (!$isReadable) { + throw new Exception(sprintf("Cannot open file %s \n", $filename)); + } + + // TODO remove $input, $output, and $context from scope + // prevent this to be propagated to the included file + unset($isReadable); + + include_once $filePath; + + return $filePath; + } + + /** + * Given an array of paths, return all unique PHP files that are in them + * + * @param string|string[] $paths Path or array of paths to get .php files. + * @return string[] + */ + public static function getFiles(string|array $paths): array + { + $files = static::globAll(array_map(function ($path) { + return $path . DIRECTORY_SEPARATOR . '*.php'; + }, (array)$paths)); + // glob() can return the same file multiple times + // This will cause the migration to fail with a + // false assumption of duplicate migrations + // https://php.net/manual/en/function.glob.php#110340 + $files = array_unique($files); + + return $files; + } + + /** + * Attempt to remove the current working directory from a path for output. + * + * @param string $path Path to remove cwd prefix from + * @return string + */ + public static function relativePath(string $path): string + { + $realpath = realpath($path); + if ($realpath !== false) { + $path = $realpath; + } + + $cwd = getcwd(); + if ($cwd !== false) { + $cwd .= DIRECTORY_SEPARATOR; + $cwdLen = strlen($cwd); + + if (substr($path, 0, $cwdLen) === $cwd) { + $path = substr($path, $cwdLen); + } + } + + return $path; + } +} From b720bb511e3efa5713262a5583183a04821400e8 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 3 Oct 2024 23:56:54 -0400 Subject: [PATCH 15/47] Import tests as well --- tests/TestCase/Util/UtilTest.php | 174 ++++++++++++++++++ .../20120111235330_test_migration.php | 22 +++ .../20120116183504_test_migration_2.php | 22 +++ .../TestCase/Util/_files/migrations/empty.txt | 0 .../_files/migrations/not_a_migration.php | 3 + .../_files/migrations/subdirectory/empty.txt | 0 .../_files/migrations/subdirectory/foobar.php | 3 + 7 files changed, 224 insertions(+) create mode 100644 tests/TestCase/Util/UtilTest.php create mode 100644 tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php create mode 100644 tests/TestCase/Util/_files/migrations/20120116183504_test_migration_2.php create mode 100644 tests/TestCase/Util/_files/migrations/empty.txt create mode 100644 tests/TestCase/Util/_files/migrations/not_a_migration.php create mode 100644 tests/TestCase/Util/_files/migrations/subdirectory/empty.txt create mode 100644 tests/TestCase/Util/_files/migrations/subdirectory/foobar.php diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php new file mode 100644 index 00000000..a9c21205 --- /dev/null +++ b/tests/TestCase/Util/UtilTest.php @@ -0,0 +1,174 @@ +getCorrectedPath(__DIR__ . '/_files/migrations')); + $this->assertCount(count($expectedResults), $existingClassNames); + foreach ($expectedResults as $expectedResult) { + $this->assertContains($expectedResult, $existingClassNames); + } + } + + public function testGetExistingMigrationClassNamesWithFile() + { + $file = $this->getCorrectedPath(__DIR__ . '/_files/migrations/20120111235330_test_migration.php'); + $existingClassNames = Util::getExistingMigrationClassNames($file); + $this->assertCount(0, $existingClassNames); + } + + public function testGetCurrentTimestamp() + { + $dt = new DateTime('now', new DateTimeZone('UTC')); + $expected = $dt->format(Util::DATE_FORMAT); + + $current = Util::getCurrentTimestamp(); + + // Rather than using a strict equals, we use greater/lessthan checks to + // prevent false positives when the test hits the edge of a second. + $this->assertGreaterThanOrEqual($expected, $current); + // We limit the assertion time to 2 seconds, which should never fail. + $this->assertLessThanOrEqual($expected + 2, $current); + } + + public function testGetVersionFromFileName(): void + { + $this->assertSame(20221130101652, Util::getVersionFromFileName('20221130101652_test.php')); + } + + public function testGetVersionFromFileNameErrorNoVersion(): void + { + $this->expectException(RuntimeException::class); + Util::getVersionFromFileName('foo.php'); + } + + public function testGetVersionFromFileNameErrorZeroVersion(): VoidCommand + { + $this->expectException(RuntimeException::class); + Util::getVersionFromFileName('0_foo.php'); + } + + public function providerMapClassNameToFileName(): array + { + return [ + ['CamelCase87afterSomeBooze', '/^\d{14}_camel_case_87after_some_booze\.php$/'], + ['CreateUserTable', '/^\d{14}_create_user_table\.php$/'], + ['LimitResourceNamesTo30Chars', '/^\d{14}_limit_resource_names_to_30_chars\.php$/'], + ]; + } + + /** + * @dataProvider providerMapClassNameToFileName + */ + public function testMapClassNameToFileName(string $name, string $pattern): void + { + $this->assertMatchesRegularExpression($pattern, Util::mapClassNameToFileName($name)); + } + + public function providerMapFileName(): array + { + return [ + ['20150902094024_create_user_table.php', 'CreateUserTable'], + ['20150902102548_my_first_migration2.php', 'MyFirstMigration2'], + ['20200412012035_camel_case_87after_some_booze.php', 'CamelCase87afterSomeBooze'], + ['20200412012036_limit_resource_names_to_30_chars.php', 'LimitResourceNamesTo30Chars'], + ['20200412012037_back_compat_names_to30_chars.php', 'BackCompatNamesTo30Chars'], + ['20200412012037.php', 'V20200412012037'], + ]; + } + + /** + * @dataProvider providerMapFileName + */ + public function testMapFileNameToClassName(string $fileName, string $className) + { + $this->assertEquals($className, Util::mapFileNameToClassName($fileName)); + } + + public function providerValidClassName(): array + { + return [ + ['camelCase', false], + ['CreateUserTable', true], + ['UserSeeder', true], + ['Test', true], + ['test', false], + ['Q', true], + ['XMLTriggers', true], + ['Form_Cards', false], + ['snake_high_scores', false], + ['Code2319Incidents', true], + ['V20200509232007', true], + ]; + } + + /** + * @dataProvider providerValidClassName + */ + public function testIsValidPhinxClassName(string $className, bool $valid): void + { + $this->assertSame($valid, Util::isValidPhinxClassName($className)); + } + + public function testGlobPath() + { + $files = Util::glob(__DIR__ . '/_files/migrations/empty.txt'); + $this->assertCount(1, $files); + $this->assertEquals('empty.txt', basename($files[0])); + + $files = Util::glob(__DIR__ . '/_files/migrations/*.php'); + $this->assertCount(3, $files); + $this->assertEquals('20120111235330_test_migration.php', basename($files[0])); + $this->assertEquals('20120116183504_test_migration_2.php', basename($files[1])); + $this->assertEquals('not_a_migration.php', basename($files[2])); + } + + public function testGlobAll() + { + $files = Util::globAll([ + __DIR__ . '/_files/migrations/*.php', + __DIR__ . '/_files/migrations/subdirectory/*.txt', + ]); + + $this->assertCount(4, $files); + $this->assertEquals('20120111235330_test_migration.php', basename($files[0])); + $this->assertEquals('20120116183504_test_migration_2.php', basename($files[1])); + $this->assertEquals('not_a_migration.php', basename($files[2])); + $this->assertEquals('empty.txt', basename($files[3])); + } + + public function testGetFiles() + { + $files = Util::getFiles([ + __DIR__ . '/_files/migrations', + __DIR__ . '/_files/migrations/subdirectory', + __DIR__ . '/_files/migrations/subdirectory', + ]); + + $this->assertCount(4, $files); + $this->assertEquals('20120111235330_test_migration.php', basename($files[0])); + $this->assertEquals('20120116183504_test_migration_2.php', basename($files[1])); + $this->assertEquals('not_a_migration.php', basename($files[2])); + $this->assertEquals('foobar.php', basename($files[3])); + } +} diff --git a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php new file mode 100644 index 00000000..a68cc4d4 --- /dev/null +++ b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php @@ -0,0 +1,22 @@ + Date: Fri, 4 Oct 2024 00:11:10 -0400 Subject: [PATCH 16/47] Apply formatting and type checking rules --- psalm-baseline.xml | 95 +++++++++++-------- src/Util/Util.php | 19 +++- tests/TestCase/Util/UtilTest.php | 2 +- .../20120111235330_test_migration.php | 1 + .../20120116183504_test_migration_2.php | 5 +- .../_files/migrations/not_a_migration.php | 1 + .../_files/migrations/subdirectory/foobar.php | 1 + 7 files changed, 77 insertions(+), 47 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 17223cad..e5bf28b4 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,64 +1,73 @@ - + + + + + + - MigrationsDispatcher - MigrationsDispatcher::getCommands() - MigrationsDispatcher::getCommands() - \Migrations\MigrationsDispatcher - new MigrationsDispatcher(PHINX_VERSION) + + + + + + + + + - ConfigurationTrait + - $phinxName + - ConfigurationTrait + - ConfigurationTrait + - ConfigurationTrait + - ConfigurationTrait + - ConfigurationTrait + - setInput + - ConfigurationTrait + - ArrayAccess + io]]> - null + @@ -68,41 +77,41 @@ - getQueryBuilder + - $opened - is_array($newColumns) + + - getQueryBuilder + - \Phinx\Db\Adapter\AdapterInterface + - \Phinx\Db\Adapter\AdapterInterface + - \Phinx\Db\Adapter\AdapterInterface + - is_array($newColumns) + - $columns - $newColumns + + - verbose + @@ -118,47 +127,55 @@ }, $phpFiles )]]> - array_merge($versions, array_keys($migrations)) + + + getConfig()->getMigrationPath())]]> + getConfig()->getSeedPath())]]> + + + + + container)]]> - $executedVersion + - CONFIG + - ConfigurationTrait + - ConfigurationTrait + - $messages - $messages + + io->level()]]> - self::VERBOSITY_* + - $dropTables - $phinxTables - $tables + + + diff --git a/src/Util/Util.php b/src/Util/Util.php index de00455e..597a6c2b 100644 --- a/src/Util/Util.php +++ b/src/Util/Util.php @@ -18,7 +18,7 @@ /** * Temporary compatibility shim that can be refactored away. * - * @internal + * @deprecated This compatibility shim will be removed in 5.0 */ class Util { @@ -29,21 +29,25 @@ class Util /** * @var string + * @psalm-var non-empty-string */ protected const MIGRATION_FILE_NAME_PATTERN = '/^\d+_([a-z][a-z\d]*(?:_[a-z\d]+)*)\.php$/i'; /** * @var string + * @psalm-var non-empty-string */ protected const MIGRATION_FILE_NAME_NO_NAME_PATTERN = '/^[0-9]{14}\.php$/'; /** * @var string + * @psalm-var non-empty-string */ protected const SEED_FILE_NAME_PATTERN = '/^([a-z][a-z\d]*)\.php$/i'; /** * @var string + * @psalm-var non-empty-string */ protected const CLASS_NAME_PATTERN = '/^(?:[A-Z][a-z\d]*)+$/'; @@ -78,7 +82,7 @@ public static function getExistingMigrationClassNames(string $path): array foreach ($phpFiles as $filePath) { $fileName = basename($filePath); - if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName)) { + if ($fileName && preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName)) { $classNames[] = static::mapFileNameToClassName($fileName); } } @@ -210,7 +214,7 @@ public static function isValidSeedFileName(string $fileName): bool * Expands a set of paths with curly braces (if supported by the OS). * * @param string[] $paths Paths - * @return string[] + * @return array */ public static function globAll(array $paths): array { @@ -231,7 +235,12 @@ public static function globAll(array $paths): array */ public static function glob(string $path): array { - return glob($path, defined('GLOB_BRACE') ? GLOB_BRACE : 0); + $result = glob($path, defined('GLOB_BRACE') ? GLOB_BRACE : 0); + if ($result) { + return $result; + } + + return []; } /** @@ -247,7 +256,7 @@ public static function glob(string $path): array public static function loadPhpFile(string $filename, ?InputInterface $input = null, ?OutputInterface $output = null, mixed $context = null): string { $filePath = realpath($filename); - if (!file_exists($filePath)) { + if (!$filePath || !file_exists($filePath)) { throw new Exception(sprintf("File does not exist: %s \n", $filename)); } diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php index a9c21205..932d6ac4 100644 --- a/tests/TestCase/Util/UtilTest.php +++ b/tests/TestCase/Util/UtilTest.php @@ -3,11 +3,11 @@ namespace Test\Phinx\Util; +use Cake\TestSuite\TestCase; use DateTime; use DateTimeZone; use Migrations\Util\Util; use RuntimeException; -use Cake\TestSuite\TestCase; class UtilTest extends TestCase { diff --git a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php index a68cc4d4..27b10239 100644 --- a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php +++ b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php @@ -1,4 +1,5 @@ Date: Fri, 4 Oct 2024 23:02:56 -0400 Subject: [PATCH 17/47] Fix provider signatures --- tests/TestCase/Util/UtilTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php index 932d6ac4..a1aa2cd7 100644 --- a/tests/TestCase/Util/UtilTest.php +++ b/tests/TestCase/Util/UtilTest.php @@ -68,7 +68,7 @@ public function testGetVersionFromFileNameErrorZeroVersion(): VoidCommand Util::getVersionFromFileName('0_foo.php'); } - public function providerMapClassNameToFileName(): array + public static function providerMapClassNameToFileName(): array { return [ ['CamelCase87afterSomeBooze', '/^\d{14}_camel_case_87after_some_booze\.php$/'], @@ -85,7 +85,7 @@ public function testMapClassNameToFileName(string $name, string $pattern): void $this->assertMatchesRegularExpression($pattern, Util::mapClassNameToFileName($name)); } - public function providerMapFileName(): array + public static function providerMapFileName(): array { return [ ['20150902094024_create_user_table.php', 'CreateUserTable'], @@ -105,7 +105,7 @@ public function testMapFileNameToClassName(string $fileName, string $className) $this->assertEquals($className, Util::mapFileNameToClassName($fileName)); } - public function providerValidClassName(): array + public static function providerValidClassName(): array { return [ ['camelCase', false], From 33c7c93cb1fdcb56b13ef64d4905e6f83fee1225 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 4 Oct 2024 23:15:25 -0400 Subject: [PATCH 18/47] Remove isValidPhinxFileName it isn't used internally and I don't see a reason to maintain it. --- src/Util/Util.php | 25 ++++--------------------- tests/TestCase/Util/UtilTest.php | 25 ------------------------- 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/src/Util/Util.php b/src/Util/Util.php index 597a6c2b..27814234 100644 --- a/src/Util/Util.php +++ b/src/Util/Util.php @@ -8,6 +8,7 @@ namespace Migrations\Util; +use Cake\Utility\Inflector; use DateTime; use DateTimeZone; use Exception; @@ -118,7 +119,8 @@ public static function getVersionFromFileName(string $fileName): int */ public static function mapClassNameToFileName(string $className): string { - // TODO replace with Inflector + // TODO it would be nice to replace this with Inflector::underscore + // but it will break compatibility for little end user gain. $snake = function ($matches) { return '_' . strtolower($matches[0]); }; @@ -137,7 +139,6 @@ public static function mapClassNameToFileName(string $className): string */ public static function mapFileNameToClassName(string $fileName): string { - // TODO replace with Inflector $matches = []; if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName, $matches)) { $fileName = $matches[1]; @@ -145,9 +146,7 @@ public static function mapFileNameToClassName(string $fileName): string return 'V' . substr($fileName, 0, strlen($fileName) - 4); } - $className = str_replace('_', '', ucwords($fileName, '_')); - - return $className; + return Inflector::camelize($fileName); } /** @@ -171,22 +170,6 @@ public static function isUniqueMigrationClassName(string $className, string $pat return !in_array($className, $existingClassNames, true); } - /** - * Check if a migration/seed class name is valid. - * - * Migration & Seed class names must be in CamelCase format. - * e.g: CreateUserTable, AddIndexToPostsTable or UserSeeder. - * - * Single words are not allowed on their own. - * - * @param string $className Class Name - * @return bool - */ - public static function isValidPhinxClassName(string $className): bool - { - return (bool)preg_match(static::CLASS_NAME_PATTERN, $className); - } - /** * Check if a migration file name is valid. * diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php index a1aa2cd7..65283066 100644 --- a/tests/TestCase/Util/UtilTest.php +++ b/tests/TestCase/Util/UtilTest.php @@ -105,31 +105,6 @@ public function testMapFileNameToClassName(string $fileName, string $className) $this->assertEquals($className, Util::mapFileNameToClassName($fileName)); } - public static function providerValidClassName(): array - { - return [ - ['camelCase', false], - ['CreateUserTable', true], - ['UserSeeder', true], - ['Test', true], - ['test', false], - ['Q', true], - ['XMLTriggers', true], - ['Form_Cards', false], - ['snake_high_scores', false], - ['Code2319Incidents', true], - ['V20200509232007', true], - ]; - } - - /** - * @dataProvider providerValidClassName - */ - public function testIsValidPhinxClassName(string $className, bool $valid): void - { - $this->assertSame($valid, Util::isValidPhinxClassName($className)); - } - public function testGlobPath() { $files = Util::glob(__DIR__ . '/_files/migrations/empty.txt'); From 438722e4987bb2d415b51593c1ceed420db5589a Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 4 Oct 2024 23:17:06 -0400 Subject: [PATCH 19/47] Remove Util::relativePath It is unused internally. --- src/Util/Util.php | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/Util/Util.php b/src/Util/Util.php index 27814234..4d3b18d3 100644 --- a/src/Util/Util.php +++ b/src/Util/Util.php @@ -282,30 +282,4 @@ public static function getFiles(string|array $paths): array return $files; } - - /** - * Attempt to remove the current working directory from a path for output. - * - * @param string $path Path to remove cwd prefix from - * @return string - */ - public static function relativePath(string $path): string - { - $realpath = realpath($path); - if ($realpath !== false) { - $path = $realpath; - } - - $cwd = getcwd(); - if ($cwd !== false) { - $cwd .= DIRECTORY_SEPARATOR; - $cwdLen = strlen($cwd); - - if (substr($path, 0, $cwdLen) === $cwd) { - $path = substr($path, $cwdLen); - } - } - - return $path; - } } From 342ce3384a547b990594f07afe0378a7c79247f9 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 4 Oct 2024 23:19:22 -0400 Subject: [PATCH 20/47] Fix namespaces and mark Util as internal I won't be able to fully refactor it away. There are conventions for migration files that Inflector does not replicate. --- src/Util/Util.php | 2 +- tests/TestCase/Util/UtilTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Util/Util.php b/src/Util/Util.php index 4d3b18d3..f24a8f86 100644 --- a/src/Util/Util.php +++ b/src/Util/Util.php @@ -19,7 +19,7 @@ /** * Temporary compatibility shim that can be refactored away. * - * @deprecated This compatibility shim will be removed in 5.0 + * @internal */ class Util { diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php index 65283066..a7620401 100644 --- a/tests/TestCase/Util/UtilTest.php +++ b/tests/TestCase/Util/UtilTest.php @@ -1,7 +1,7 @@ Date: Fri, 4 Oct 2024 23:22:52 -0400 Subject: [PATCH 21/47] Fix phpcs failure The test stubs for Util don't need to adhere to coding standards as they contain migration files from phinx. --- phpcs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml b/phpcs.xml index d180d0a7..7a7a8a87 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -6,6 +6,7 @@ tests/ */tests/comparisons/* + */tests/TestCase/Util/_files/* */test_app/config/* */TestBlog/config/* */BarPlugin/config/* From a2e46b81c0e3cb470dd791f80d5835300f8a5dcb Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 6 Oct 2024 22:37:35 -0400 Subject: [PATCH 22/47] Clean up baseline file --- psalm-baseline.xml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index e5bf28b4..2ef77c40 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,10 +1,5 @@ - - - - - @@ -15,10 +10,6 @@ - - - - @@ -129,14 +120,6 @@ )]]> - - getConfig()->getMigrationPath())]]> - getConfig()->getSeedPath())]]> - - - - - container)]]> From 3c490e796b036a0db0c390a374f4a733bb3f2bc4 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 9 Oct 2024 23:59:01 -0400 Subject: [PATCH 23/47] Add deprecations for phinx commands --- src/Command/Phinx/Create.php | 3 +++ src/Command/Phinx/MarkMigrated.php | 1 + src/Command/Phinx/Migrate.php | 3 +++ src/Command/Phinx/Rollback.php | 3 +++ src/Command/Phinx/Seed.php | 3 +++ src/Command/Phinx/Status.php | 1 + 6 files changed, 14 insertions(+) diff --git a/src/Command/Phinx/Create.php b/src/Command/Phinx/Create.php index c619a25f..c434fb73 100644 --- a/src/Command/Phinx/Create.php +++ b/src/Command/Phinx/Create.php @@ -22,6 +22,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Create extends CreateCommand { use CommandTrait { diff --git a/src/Command/Phinx/MarkMigrated.php b/src/Command/Phinx/MarkMigrated.php index 3bd0ab5e..1170cbb0 100644 --- a/src/Command/Phinx/MarkMigrated.php +++ b/src/Command/Phinx/MarkMigrated.php @@ -23,6 +23,7 @@ /** * @method \Migrations\CakeManager getManager() + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. */ class MarkMigrated extends AbstractCommand { diff --git a/src/Command/Phinx/Migrate.php b/src/Command/Phinx/Migrate.php index 01b14057..8c49e7a2 100644 --- a/src/Command/Phinx/Migrate.php +++ b/src/Command/Phinx/Migrate.php @@ -20,6 +20,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Migrate extends MigrateCommand { use CommandTrait { diff --git a/src/Command/Phinx/Rollback.php b/src/Command/Phinx/Rollback.php index 5d779973..c025a2c5 100644 --- a/src/Command/Phinx/Rollback.php +++ b/src/Command/Phinx/Rollback.php @@ -20,6 +20,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Rollback extends RollbackCommand { use CommandTrait { diff --git a/src/Command/Phinx/Seed.php b/src/Command/Phinx/Seed.php index 652585e4..37d886df 100644 --- a/src/Command/Phinx/Seed.php +++ b/src/Command/Phinx/Seed.php @@ -20,6 +20,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Seed extends SeedRun { use CommandTrait { diff --git a/src/Command/Phinx/Status.php b/src/Command/Phinx/Status.php index bb406a7f..9375a216 100644 --- a/src/Command/Phinx/Status.php +++ b/src/Command/Phinx/Status.php @@ -21,6 +21,7 @@ /** * @method \Migrations\CakeManager getManager() + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. */ class Status extends StatusCommand { From 5faeeddd68c97a5298407642384452a10e71b59f Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 9 Oct 2024 23:59:39 -0400 Subject: [PATCH 24/47] Replace phinx constants with Migrations clones --- src/Db/Adapter/PdoAdapter.php | 2 +- src/Db/Table/Column.php | 4 ++-- src/Util/ColumnParser.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Db/Adapter/PdoAdapter.php b/src/Db/Adapter/PdoAdapter.php index 1c30f755..33a29482 100644 --- a/src/Db/Adapter/PdoAdapter.php +++ b/src/Db/Adapter/PdoAdapter.php @@ -17,6 +17,7 @@ use Cake\Database\Query\SelectQuery; use Cake\Database\Query\UpdateQuery; use InvalidArgumentException; +use Migrations\Config\Config; use Migrations\Db\Action\AddColumn; use Migrations\Db\Action\AddForeignKey; use Migrations\Db\Action\AddIndex; @@ -39,7 +40,6 @@ use Migrations\MigrationInterface; use PDO; use PDOException; -use Phinx\Config\Config; use Phinx\Util\Literal as PhinxLiteral; use ReflectionMethod; use RuntimeException; diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index d740e0d0..22bdf851 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -9,9 +9,9 @@ namespace Migrations\Db\Table; use Cake\Core\Configure; +use Migrations\Db\Adapter\AdapterInterface; +use Migrations\Db\Adapter\PostgresAdapter; use Migrations\Db\Literal; -use Phinx\Db\Adapter\AdapterInterface; -use Phinx\Db\Adapter\PostgresAdapter; use RuntimeException; /** diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php index 6b98ad97..9c6c7d83 100644 --- a/src/Util/ColumnParser.php +++ b/src/Util/ColumnParser.php @@ -5,7 +5,7 @@ use Cake\Collection\Collection; use Cake\Utility\Hash; -use Phinx\Db\Adapter\AdapterInterface; +use Migrations\Db\Adapter\AdapterInterface; use ReflectionClass; /** From 58023eccee65d7d84a21775e68d381e3bbeaa408 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 10 Oct 2024 00:12:17 -0400 Subject: [PATCH 25/47] Define an interface for migration backends This removes an odd union and allows for future refactors to have a smaller public API. --- src/Migration/BackendInterface.php | 81 ++++++++++++++++++++++++++++++ src/Migration/BuiltinBackend.php | 61 +++------------------- src/Migration/PhinxBackend.php | 69 +++---------------------- src/Migrations.php | 4 +- 4 files changed, 96 insertions(+), 119 deletions(-) create mode 100644 src/Migration/BackendInterface.php diff --git a/src/Migration/BackendInterface.php b/src/Migration/BackendInterface.php new file mode 100644 index 00000000..7719494d --- /dev/null +++ b/src/Migration/BackendInterface.php @@ -0,0 +1,81 @@ + $options Options to pass to the command + * Available options are : + * + * - `format` Format to output the response. Can be 'json' + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return array The migrations list and their statuses + */ + public function status(array $options = []): array; + + /** + * Migrates available migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will migrate + * everything it can + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to migrate to + * @return bool Success + */ + public function migrate(array $options = []): bool; + + /** + * Rollbacks migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will only migrate + * the last migrations registered in the phinx log + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to rollback to + * @return bool Success + */ + public function rollback(array $options = []): bool; + + /** + * Marks a migration as migrated + * + * @param int|string|null $version The version number of the migration to mark as migrated + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return bool Success + */ + public function markMigrated(int|string|null $version = null, array $options = []): bool; + + /** + * Seed the database using a seed file + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `seed` The seed file to use + * @return bool Success + */ + public function seed(array $options = []): bool; +} diff --git a/src/Migration/BuiltinBackend.php b/src/Migration/BuiltinBackend.php index 2b806d0e..3b4d29ef 100644 --- a/src/Migration/BuiltinBackend.php +++ b/src/Migration/BuiltinBackend.php @@ -30,7 +30,7 @@ * * @internal */ -class BuiltinBackend +class BuiltinBackend implements BackendInterface { /** * The OutputInterface. @@ -91,16 +91,7 @@ public function __construct(array $default = []) } /** - * Returns the status of each migrations based on the options passed - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `format` Format to output the response. Can be 'json' - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return array The migrations list and their statuses + * {@inheritDoc} */ public function status(array $options = []): array { @@ -110,18 +101,7 @@ public function status(array $options = []): array } /** - * Migrates available migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will migrate - * everything it can - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to migrate to - * @return bool Success + * {@inheritDoc} */ public function migrate(array $options = []): bool { @@ -141,18 +121,7 @@ public function migrate(array $options = []): bool } /** - * Rollbacks migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will only migrate - * the last migrations registered in the phinx log - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to rollback to - * @return bool Success + * {@inheritDoc} */ public function rollback(array $options = []): bool { @@ -172,16 +141,7 @@ public function rollback(array $options = []): bool } /** - * Marks a migration as migrated - * - * @param int|string|null $version The version number of the migration to mark as migrated - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return bool Success + * {@inheritDoc} */ public function markMigrated(int|string|null $version = null, array $options = []): bool { @@ -206,16 +166,7 @@ public function markMigrated(int|string|null $version = null, array $options = [ } /** - * Seed the database using a seed file - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `seed` The seed file to use - * @return bool Success + * {@inheritDoc} */ public function seed(array $options = []): bool { diff --git a/src/Migration/PhinxBackend.php b/src/Migration/PhinxBackend.php index 8b81a511..14ad28a7 100644 --- a/src/Migration/PhinxBackend.php +++ b/src/Migration/PhinxBackend.php @@ -35,7 +35,7 @@ * * @internal */ -class PhinxBackend +class PhinxBackend implements BackendInterface { use ConfigurationTrait; @@ -133,16 +133,7 @@ public function getCommand(): string } /** - * Returns the status of each migrations based on the options passed - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `format` Format to output the response. Can be 'json' - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return array The migrations list and their statuses + * {@inheritDoc} */ public function status(array $options = []): array { @@ -153,18 +144,7 @@ public function status(array $options = []): array } /** - * Migrates available migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will migrate - * everything it can - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to migrate to - * @return bool Success + * {@inheritDoc} */ public function migrate(array $options = []): bool { @@ -184,18 +164,7 @@ public function migrate(array $options = []): bool } /** - * Rollbacks migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will only migrate - * the last migrations registered in the phinx log - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to rollback to - * @return bool Success + * {@inheritDoc} */ public function rollback(array $options = []): bool { @@ -215,16 +184,7 @@ public function rollback(array $options = []): bool } /** - * Marks a migration as migrated - * - * @param int|string|null $version The version number of the migration to mark as migrated - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return bool Success + * {@inheritDoc} */ public function markMigrated(int|string|null $version = null, array $options = []): bool { @@ -257,16 +217,7 @@ public function markMigrated(int|string|null $version = null, array $options = [ } /** - * Seed the database using a seed file - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `seed` The seed file to use - * @return bool Success + * {@inheritDoc} */ public function seed(array $options = []): bool { @@ -285,13 +236,7 @@ public function seed(array $options = []): bool } /** - * Runs the method needed to execute and return - * - * @param string $method Manager method to call - * @param array $params Manager params to pass - * @param \Symfony\Component\Console\Input\InputInterface $input InputInterface needed for the - * Manager to properly run - * @return mixed The result of the CakeManager::$method() call + * {@inheritDoc} */ protected function run(string $method, array $params, InputInterface $input): mixed { diff --git a/src/Migrations.php b/src/Migrations.php index 7354436a..26f5b726 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -27,7 +27,7 @@ /** * The Migrations class is responsible for handling migrations command - * within an none-shell application. + * within an non-shell application. */ class Migrations { @@ -129,7 +129,7 @@ public function getCommand(): string /** * Get the Migrations interface backend based on configuration data. * - * @return \Migrations\Migration\BuiltinBackend|\Migrations\Migration\PhinxBackend + * @return \Migrations\Migration\BackendInterface */ protected function getBackend(): BuiltinBackend|PhinxBackend { From 7a7e5e78a6387cb59fe3a5d164ec8bf42d06475c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 10 Oct 2024 00:40:18 -0400 Subject: [PATCH 26/47] Remove references to Symfony We don't use these in the builtin backend. --- src/Migration/BuiltinBackend.php | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/Migration/BuiltinBackend.php b/src/Migration/BuiltinBackend.php index 3b4d29ef..ee70978c 100644 --- a/src/Migration/BuiltinBackend.php +++ b/src/Migration/BuiltinBackend.php @@ -20,9 +20,6 @@ use DateTime; use InvalidArgumentException; use Migrations\Config\ConfigInterface; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Output\OutputInterface; /** * The Migrations class is responsible for handling migrations command @@ -32,14 +29,6 @@ */ class BuiltinBackend implements BackendInterface { - /** - * The OutputInterface. - * Should be a \Symfony\Component\Console\Output\NullOutput instance - * - * @var \Symfony\Component\Console\Output\OutputInterface - */ - protected OutputInterface $output; - /** * Manager instance * @@ -63,14 +52,6 @@ class BuiltinBackend implements BackendInterface */ protected string $command; - /** - * Stub input to feed the manager class since we might not have an input ready when we get the Manager using - * the `getManager()` method - * - * @var \Symfony\Component\Console\Input\ArrayInput - */ - protected ArrayInput $stubInput; - /** * Constructor * @@ -82,9 +63,6 @@ class BuiltinBackend implements BackendInterface */ public function __construct(array $default = []) { - $this->output = new NullOutput(); - $this->stubInput = new ArrayInput([]); - if ($default) { $this->default = $default; } From feb9579668c6ec9fe3ad0618951a51f803a632c8 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 10 Oct 2024 00:43:23 -0400 Subject: [PATCH 27/47] Fix interface reference and add missed file. --- src/Command/Phinx/Dump.php | 2 ++ src/Migrations.php | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Command/Phinx/Dump.php b/src/Command/Phinx/Dump.php index 78208837..b4eca9f3 100644 --- a/src/Command/Phinx/Dump.php +++ b/src/Command/Phinx/Dump.php @@ -26,6 +26,8 @@ * Dump command class. * A "dump" is a snapshot of a database at a given point in time. It is stored in a * .lock file in the same folder as migrations files. + * + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. */ class Dump extends AbstractCommand { diff --git a/src/Migrations.php b/src/Migrations.php index 26f5b726..71859f89 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -16,6 +16,7 @@ use Cake\Core\Configure; use Cake\Datasource\ConnectionManager; use InvalidArgumentException; +use Migrations\Migration\BackendInterface; use Migrations\Migration\BuiltinBackend; use Migrations\Migration\PhinxBackend; use Phinx\Config\ConfigInterface; @@ -131,7 +132,7 @@ public function getCommand(): string * * @return \Migrations\Migration\BackendInterface */ - protected function getBackend(): BuiltinBackend|PhinxBackend + protected function getBackend(): BackendInterface { $backend = (string)(Configure::read('Migrations.backend') ?? 'builtin'); if ($backend === 'builtin') { From 418a103a5cc384addf5c0990d4945e8fb826b3d5 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 10 Oct 2024 10:30:39 -0400 Subject: [PATCH 28/47] Fix up broken behavior with changes in 4.next --- src/Shim/MigrationAdapter.php | 115 +++++-------------- tests/TestCase/Migration/EnvironmentTest.php | 7 +- 2 files changed, 32 insertions(+), 90 deletions(-) diff --git a/src/Shim/MigrationAdapter.php b/src/Shim/MigrationAdapter.php index 6cc13236..ba80f2de 100644 --- a/src/Shim/MigrationAdapter.php +++ b/src/Shim/MigrationAdapter.php @@ -204,9 +204,7 @@ public function setConfig(ConfigInterface $config) } /** - * Gets the name. - * - * @return string + * {@inheritDoc} */ public function getName(): string { @@ -214,10 +212,7 @@ public function getName(): string } /** - * Sets the migration version number. - * - * @param int $version Version - * @return $this + * {@inheritDoc} */ public function setVersion(int $version) { @@ -227,9 +222,7 @@ public function setVersion(int $version) } /** - * Gets the migration version number. - * - * @return int + * {@inheritDoc} */ public function getVersion(): int { @@ -237,10 +230,19 @@ public function getVersion(): int } /** - * Sets whether this migration is being applied or reverted - * - * @param bool $isMigratingUp True if the migration is being applied - * @return $this + * {@inheritDoc} + */ + public function useTransactions(): bool + { + if (method_exists($this->migration, 'useTransactions')) { + return $this->migration->useTransactions(); + } + + return $this->migration->getAdapter()->hasTransactions(); + } + + /** + * {@inheritDoc} */ public function setMigratingUp(bool $isMigratingUp) { @@ -250,10 +252,7 @@ public function setMigratingUp(bool $isMigratingUp) } /** - * Gets whether this migration is being applied or reverted. - * True means that the migration is being applied. - * - * @return bool + * {@inheritDoc} */ public function isMigratingUp(): bool { @@ -261,11 +260,7 @@ public function isMigratingUp(): bool } /** - * Executes a SQL statement and returns the number of affected rows. - * - * @param string $sql SQL - * @param array $params parameters to use for prepared query - * @return int + * {@inheritDoc} */ public function execute(string $sql, array $params = []): int { @@ -273,16 +268,7 @@ public function execute(string $sql, array $params = []): int } /** - * Executes a SQL statement. - * - * The return type depends on the underlying adapter being used. To improve - * IDE auto-completion possibility, you can overwrite the query method - * phpDoc in your (typically custom abstract parent) migration class, where - * you can set the return type by the adapter in your current use. - * - * @param string $sql SQL - * @param array $params parameters to use for prepared query - * @return mixed + * {@inheritDoc} */ public function query(string $sql, array $params = []): mixed { @@ -290,15 +276,7 @@ public function query(string $sql, array $params = []): mixed } /** - * Returns a new Query object that can be used to build complex SELECT, UPDATE, INSERT or DELETE - * queries and execute them against the current database. - * - * Queries executed through the query builder are always sent to the database, regardless of the - * the dry-run settings. - * - * @see https://api.cakephp.org/3.6/class-Cake.Database.Query.html - * @param string $type Query - * @return \Cake\Database\Query + * {@inheritDoc} */ public function getQueryBuilder(string $type): Query { @@ -306,13 +284,7 @@ public function getQueryBuilder(string $type): Query } /** - * Returns a new SelectQuery object that can be used to build complex - * SELECT queries and execute them against the current database. - * - * Queries executed through the query builder are always sent to the database, regardless of the - * the dry-run settings. - * - * @return \Cake\Database\Query\SelectQuery + * {@inheritDoc} */ public function getSelectBuilder(): SelectQuery { @@ -320,13 +292,7 @@ public function getSelectBuilder(): SelectQuery } /** - * Returns a new InsertQuery object that can be used to build complex - * INSERT queries and execute them against the current database. - * - * Queries executed through the query builder are always sent to the database, regardless of the - * the dry-run settings. - * - * @return \Cake\Database\Query\InsertQuery + * {@inheritDoc} */ public function getInsertBuilder(): InsertQuery { @@ -334,13 +300,7 @@ public function getInsertBuilder(): InsertQuery } /** - * Returns a new UpdateQuery object that can be used to build complex - * UPDATE queries and execute them against the current database. - * - * Queries executed through the query builder are always sent to the database, regardless of the - * the dry-run settings. - * - * @return \Cake\Database\Query\UpdateQuery + * {@inheritDoc} */ public function getUpdateBuilder(): UpdateQuery { @@ -348,13 +308,7 @@ public function getUpdateBuilder(): UpdateQuery } /** - * Returns a new DeleteQuery object that can be used to build complex - * DELETE queries and execute them against the current database. - * - * Queries executed through the query builder are always sent to the database, regardless of the - * the dry-run settings. - * - * @return \Cake\Database\Query\DeleteQuery + * {@inheritDoc} */ public function getDeleteBuilder(): DeleteQuery { @@ -362,10 +316,7 @@ public function getDeleteBuilder(): DeleteQuery } /** - * Executes a query and returns only one row as an array. - * - * @param string $sql SQL - * @return array|false + * {@inheritDoc} */ public function fetchRow(string $sql): array|false { @@ -373,10 +324,7 @@ public function fetchRow(string $sql): array|false } /** - * Executes a query and returns an array of rows. - * - * @param string $sql SQL - * @return array + * {@inheritDoc} */ public function fetchAll(string $sql): array { @@ -384,11 +332,7 @@ public function fetchAll(string $sql): array } /** - * Create a new database. - * - * @param string $name Database Name - * @param array $options Options - * @return void + * {@inheritDoc} */ public function createDatabase(string $name, array $options): void { @@ -396,10 +340,7 @@ public function createDatabase(string $name, array $options): void } /** - * Drop a database. - * - * @param string $name Database Name - * @return void + * {@inheritDoc} */ public function dropDatabase(string $name): void { diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index 990bc577..7e79a780 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -174,7 +174,7 @@ public function testExecutingAMigrationWithTransactions() $adapterStub->expects($this->once()) ->method('commitTransaction'); - $adapterStub->expects($this->exactly(1)) + $adapterStub->expects($this->atLeastOnce()) ->method('hasTransactions') ->willReturn(true); @@ -206,7 +206,7 @@ public function testExecutingAMigrationWithUseTransactions() $adapterStub->expects($this->never()) ->method('commitTransaction'); - $adapterStub->expects($this->exactly(1)) + $adapterStub->expects($this->atLeastOnce()) ->method('hasTransactions') ->willReturn(true); @@ -227,7 +227,8 @@ public function up(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($migration->executed); } From 928cd37a9394515cd72f1478d3a9a6a926ae60ac Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 10 Oct 2024 11:55:19 -0400 Subject: [PATCH 29/47] Add method I missed --- src/MigrationInterface.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/MigrationInterface.php b/src/MigrationInterface.php index 7b6153f9..28046a39 100644 --- a/src/MigrationInterface.php +++ b/src/MigrationInterface.php @@ -120,6 +120,16 @@ public function getVersion(): int; */ public function setMigratingUp(bool $isMigratingUp); + /** + * Hook method to decide if this migration should use transactions + * + * By default if your driver supports transactions, a transaction will be opened + * before the migration begins, and commit when the migration completes. + * + * @return bool + */ + public function useTransactions(): bool; + /** * Gets whether this migration is being applied or reverted. * True means that the migration is being applied. From 9eed02ba261cf225b4f8395a6098ebb20c2033ce Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 10 Oct 2024 15:27:21 -0400 Subject: [PATCH 30/47] Update baseline for deprecations and interface compat --- psalm-baseline.xml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 2ef77c40..7b8f4c30 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,16 @@ + + + + + + + + + + + @@ -9,6 +20,23 @@ + + + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + + @@ -142,6 +170,22 @@ + + + + + + + + + + + + + + + + From 8681b804d55f577c74984b0a6c0f8a1ffe691511 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 26 Oct 2024 00:09:16 -0400 Subject: [PATCH 31/47] 4.next - Add base seed (#760) This class will be the new base class for Migrations that are compatible with only the built-in backend. This gives a opt-in flow to having a fully compatible migration set before the breaking change is done. * Fix up table() interface and add integration test * Expand coverage with an integration test. * Add another assertion * Fix usage of keywords --- src/AbstractSeed.php | 2 + src/BaseSeed.php | 235 ++++++++++++++++++ src/SeedInterface.php | 5 +- src/Shim/SeedAdapter.php | 4 +- tests/TestCase/Command/SeedCommandTest.php | 19 ++ .../config/BaseSeeds/MigrationSeedNumbers.php | 45 ++++ 6 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 src/BaseSeed.php create mode 100644 tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php diff --git a/src/AbstractSeed.php b/src/AbstractSeed.php index fd993807..cfcc9f08 100644 --- a/src/AbstractSeed.php +++ b/src/AbstractSeed.php @@ -23,6 +23,8 @@ * Class AbstractSeed * Extends Phinx base AbstractSeed class in order to extend the features the seed class * offers. + * + * @deprecated 4.5.0 You should use Migrations\BaseSeed for new seeds. */ abstract class AbstractSeed extends BaseAbstractSeed { diff --git a/src/BaseSeed.php b/src/BaseSeed.php new file mode 100644 index 00000000..9ccb8eca --- /dev/null +++ b/src/BaseSeed.php @@ -0,0 +1,235 @@ +adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Adapter not set.'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return static::class; + } + + /** + * {@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 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 insert(string $tableName, array $data): void + { + // convert to table object + $table = new Table($tableName, [], $this->getAdapter()); + $table->insert($data)->save(); + } + + /** + * {@inheritDoc} + */ + public function hasTable(string $tableName): bool + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * {@inheritDoc} + */ + public function table(string $tableName, array $options = []): Table + { + return new Table($tableName, $options, $this->getAdapter()); + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function call(string $seeder, array $options = []): void + { + $io = $this->getIo(); + assert($io !== null, 'Requires ConsoleIo'); + $io->out(''); + $io->out( + ' ====' . + ' ' . $seeder . ':' . + ' seeding' + ); + + $start = microtime(true); + $this->runCall($seeder, $options); + $end = microtime(true); + + $io->out( + ' ====' . + ' ' . $seeder . ':' . + ' seeded' . + ' ' . sprintf('%.4fs', $end - $start) . '' + ); + $io->out(''); + } + + /** + * Calls another seeder from this seeder. + * It will load the Seed class you are calling and run it. + * + * @param string $seeder Name of the seeder to call from the current seed + * @param array $options The CLI options passed to ManagerFactory. + * @return void + */ + protected function runCall(string $seeder, array $options = []): void + { + [$pluginName, $seeder] = pluginSplit($seeder); + $adapter = $this->getAdapter(); + $connection = $adapter->getConnection()->configName(); + + $factory = new ManagerFactory([ + 'plugin' => $options['plugin'] ?? $pluginName ?? null, + 'source' => $options['source'] ?? null, + 'connection' => $options['connection'] ?? $connection, + ]); + $io = $this->getIo(); + assert($io !== null, 'Missing ConsoleIo instance'); + $manager = $factory->createManager($io); + $manager->seed($seeder); + } +} diff --git a/src/SeedInterface.php b/src/SeedInterface.php index cd2e3243..50ee1285 100644 --- a/src/SeedInterface.php +++ b/src/SeedInterface.php @@ -161,7 +161,7 @@ public function hasTable(string $tableName): bool; * @param array $options Options * @return \Migrations\Db\Table */ - public function table(string $tableName, array $options): Table; + public function table(string $tableName, array $options = []): Table; /** * Checks to see if the seed should be executed. @@ -180,7 +180,8 @@ public function shouldExecute(): bool; * for instance to respect foreign key constraints. * * @param string $seeder Name of the seeder to call from the current seed + * @param array $options The CLI options for the seeder. * @return void */ - public function call(string $seeder): void; + public function call(string $seeder, array $options = []): void; } diff --git a/src/Shim/SeedAdapter.php b/src/Shim/SeedAdapter.php index c33840e8..b1cc65f0 100644 --- a/src/Shim/SeedAdapter.php +++ b/src/Shim/SeedAdapter.php @@ -229,7 +229,7 @@ public function hasTable(string $tableName): bool /** * {@inheritDoc} */ - public function table(string $tableName, array $options): Table + public function table(string $tableName, array $options = []): Table { throw new RuntimeException('Not implemented'); } @@ -245,7 +245,7 @@ public function shouldExecute(): bool /** * {@inheritDoc} */ - public function call(string $seeder): void + public function call(string $seeder, array $options = []): void { throw new RuntimeException('Not implemented'); } diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php index 8a9bedcc..8bac092e 100644 --- a/tests/TestCase/Command/SeedCommandTest.php +++ b/tests/TestCase/Command/SeedCommandTest.php @@ -122,6 +122,25 @@ public function testSeederOne(): void $this->assertEquals(1, $query->fetchColumn(0)); } + public function testSeederBaseSeed(): void + { + $this->createTables(); + $this->exec('migrations seed -c test --source BaseSeeds --seed MigrationSeedNumbers'); + $this->assertExitSuccess(); + $this->assertOutputContains('MigrationSeedNumbers: seeding'); + $this->assertOutputContains('AnotherNumbersSeed: seeding'); + $this->assertOutputContains('radix=10'); + $this->assertOutputContains('fetchRow=121'); + $this->assertOutputContains('hasTable=1'); + $this->assertOutputContains('fetchAll=121'); + $this->assertOutputContains('All Done'); + + $connection = ConnectionManager::get('test'); + $query = $connection->execute('SELECT COUNT(*) FROM numbers'); + // Two seeders run == 2 rows + $this->assertEquals(2, $query->fetchColumn(0)); + } + public function testSeederImplictAll(): void { $this->createTables(); diff --git a/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php new file mode 100644 index 00000000..e778b424 --- /dev/null +++ b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php @@ -0,0 +1,45 @@ + '5', + 'radix' => '10', + ], + ]; + + // Call various methods on the seeder for runtime checks + // and generate output to assert behavior with in an integration test. + $this->table('numbers'); + $this->insert('numbers', $data); + + $this->call('AnotherNumbersSeed', ['source' => 'AltSeeds']); + + $io = $this->getIo(); + $query = $this->query('SELECT radix FROM numbers'); + $io->out('radix=' . $query->fetchColumn(0)); + + $row = $this->fetchRow('SELECT 121 as row_val'); + $io->out('fetchRow=' . $row['row_val']); + $io->out('hasTable=' . $this->hasTable('numbers')); + + $rows = $this->fetchAll('SELECT 121 as row_val'); + $io->out('fetchAll=' . $rows[0]['row_val']); + } +} From e5836ac422542cad32dd6d544aa313dd54f52343 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 28 Oct 2024 10:17:02 -0400 Subject: [PATCH 32/47] 4.next - Add migration BaseClass (#761) * Add documentation blocks * Add missing default value. * Add integration test for BaseMigration migrations * Add BaseMigration * Rename migrations to not conflict * Import logic from Migrations\Table. We'll need to keep compatibility with these methods as well. * Consolidate test stubs. One migration should be enough to do the integration testing I want * Fix failing tests. * Fix broken behavior in addPrimaryKey() * Improve test assertions * Update mysql collation to current defaults * Fix psalm --- psalm-baseline.xml | 5 + src/BaseMigration.php | 483 ++++++++++++++++++ src/BaseSeed.php | 17 + src/Db/Table.php | 108 +++- src/Db/Table/Column.php | 1 + src/MigrationInterface.php | 2 +- src/Shim/MigrationAdapter.php | 2 +- tests/TestCase/Command/MigrateCommandTest.php | 21 + .../TestCase/Db/Adapter/MysqlAdapterTest.php | 10 +- .../TestCase/Db/Adapter/SqliteAdapterTest.php | 12 +- .../20230628181900_base_migration_tables.php | 28 + 11 files changed, 673 insertions(+), 16 deletions(-) create mode 100644 src/BaseMigration.php create mode 100644 tests/test_app/config/BaseMigrations/20230628181900_base_migration_tables.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7b8f4c30..a12664b1 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -5,6 +5,11 @@ + + + + + diff --git a/src/BaseMigration.php b/src/BaseMigration.php new file mode 100644 index 00000000..4e8fc3f8 --- /dev/null +++ b/src/BaseMigration.php @@ -0,0 +1,483 @@ + + */ + protected array $tables = []; + + /** + * Is migrating up prop + * + * @var bool + */ + protected bool $isMigratingUp = true; + + /** + * Whether the tables created in this migration + * should auto-create an `id` field or not + * + * This option is global for all tables created in the migration file. + * If you set it to false, you have to manually add the primary keys for your + * tables using the Migrations\Table::addPrimaryKey() method + * + * @var bool + */ + public bool $autoId = true; + + /** + * Constructor + * + * @param int $version The version this migration is + */ + public function __construct(protected int $version) + { + $this->validateVersion($this->version); + } + + /** + * {@inheritDoc} + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Adapter not set.'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return static::class; + } + + /** + * Sets the migration version number. + * + * @param int $version Version + * @return $this + */ + public function setVersion(int $version) + { + $this->validateVersion($version); + $this->version = $version; + + return $this; + } + + /** + * Gets the migration version number. + * + * @return int + */ + public function getVersion(): int + { + return $this->version; + } + + /** + * Sets whether this migration is being applied or reverted + * + * @param bool $isMigratingUp True if the migration is being applied + * @return $this + */ + public function setMigratingUp(bool $isMigratingUp) + { + $this->isMigratingUp = $isMigratingUp; + + return $this; + } + + /** + * Hook method to decide if this migration should use transactions + * + * By default if your driver supports transactions, a transaction will be opened + * before the migration begins, and commit when the migration completes. + * + * @return bool + */ + public function useTransactions(): bool + { + return $this->getAdapter()->hasTransactions(); + } + + /** + * Gets whether this migration is being applied or reverted. + * True means that the migration is being applied. + * + * @return bool + */ + public function isMigratingUp(): bool + { + return $this->isMigratingUp; + } + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return int + */ + public function execute(string $sql, array $params = []): int + { + return $this->getAdapter()->execute($sql, $params); + } + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. To improve + * IDE auto-completion possibility, you can overwrite the query method + * phpDoc in your (typically custom abstract parent) migration class, where + * you can set the return type by the adapter in your current use. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return mixed + */ + public function query(string $sql, array $params = []): mixed + { + return $this->getAdapter()->query($sql, $params); + } + + /** + * Returns a new Query object that can be used to build complex SELECT, UPDATE, INSERT or DELETE + * queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @see https://api.cakephp.org/3.6/class-Cake.Database.Query.html + * @param string $type Query + * @return \Cake\Database\Query + */ + public function getQueryBuilder(string $type): Query + { + return $this->getAdapter()->getQueryBuilder($type); + } + + /** + * Returns a new SelectQuery object that can be used to build complex + * SELECT queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\SelectQuery + */ + public function getSelectBuilder(): SelectQuery + { + return $this->getAdapter()->getSelectBuilder(); + } + + /** + * Returns a new InsertQuery object that can be used to build complex + * INSERT queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\InsertQuery + */ + public function getInsertBuilder(): InsertQuery + { + return $this->getAdapter()->getInsertBuilder(); + } + + /** + * Returns a new UpdateQuery object that can be used to build complex + * UPDATE queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\UpdateQuery + */ + public function getUpdateBuilder(): UpdateQuery + { + return $this->getAdapter()->getUpdateBuilder(); + } + + /** + * Returns a new DeleteQuery object that can be used to build complex + * DELETE queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\DeleteQuery + */ + public function getDeleteBuilder(): DeleteQuery + { + return $this->getAdapter()->getDeleteBuilder(); + } + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow(string $sql): array|false + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll(string $sql): array + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * Create a new database. + * + * @param string $name Database Name + * @param array $options Options + * @return void + */ + public function createDatabase(string $name, array $options): void + { + $this->getAdapter()->createDatabase($name, $options); + } + + /** + * Drop a database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase(string $name): void + { + $this->getAdapter()->dropDatabase($name); + } + + /** + * Creates schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function createSchema(string $name): void + { + $this->getAdapter()->createSchema($name); + } + + /** + * Drops schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function dropSchema(string $name): void + { + $this->getAdapter()->dropSchema($name); + } + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable(string $tableName): bool + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options = []): Table + { + if ($this->autoId === false) { + $options['id'] = false; + } + + $table = new Table($tableName, $options, $this->getAdapter()); + $this->tables[] = $table; + + return $table; + } + + /** + * Perform checks on the migration, printing a warning + * if there are potential problems. + * + * @return void + */ + public function preFlightCheck(): void + { + if (method_exists($this, MigrationInterface::CHANGE)) { + if ( + method_exists($this, MigrationInterface::UP) || + method_exists($this, MigrationInterface::DOWN) + ) { + $io = $this->getIo(); + if ($io) { + $io->out( + 'warning Migration contains both change() and up()/down() methods.' . + ' Ignoring up() and down().' + ); + } + } + } + } + + /** + * Perform checks on the migration after completion + * + * Right now, the only check is whether all changes were committed + * + * @return void + */ + public function postFlightCheck(): void + { + foreach ($this->tables as $table) { + if ($table->hasPendingActions()) { + throw new RuntimeException(sprintf('Migration %s_%s has pending actions after execution!', $this->getVersion(), $this->getName())); + } + } + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return true; + } + + /** + * Makes sure the version int is within range for valid datetime. + * This is required to have a meaningful order in the overview. + * + * @param int $version Version + * @return void + */ + protected function validateVersion(int $version): void + { + $length = strlen((string)$version); + if ($length === 14) { + return; + } + + throw new RuntimeException('Invalid version `' . $version . '`, should be in format `YYYYMMDDHHMMSS` (length of 14).'); + } +} diff --git a/src/BaseSeed.php b/src/BaseSeed.php index 9ccb8eca..95b60b0b 100644 --- a/src/BaseSeed.php +++ b/src/BaseSeed.php @@ -23,8 +23,25 @@ */ class BaseSeed implements SeedInterface { + /** + * The Adapter instance + * + * @var \Migrations\Db\Adapter\AdapterInterface + */ protected ?AdapterInterface $adapter = null; + + /** + * The ConsoleIo instance + * + * @var \Cake\Console\ConsoleIo + */ protected ?ConsoleIo $io = null; + + /** + * The config instance. + * + * @var \Migrations\Config\ConfigInterface + */ protected ?ConfigInterface $config; /** diff --git a/src/Db/Table.php b/src/Db/Table.php index 30a62003..aff92485 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -8,6 +8,7 @@ namespace Migrations\Db; +use Cake\Collection\Collection; use Cake\Core\Configure; use InvalidArgumentException; use Migrations\Db\Action\AddColumn; @@ -61,6 +62,15 @@ class Table */ protected array $data = []; + /** + * Primary key for this table. + * Can either be a string or an array in case of composite + * primary key. + * + * @var string|string[] + */ + protected string|array $primaryKey; + /** * @param string $name Table Name * @param array $options Options @@ -289,6 +299,19 @@ public function reset(): void $this->resetData(); } + /** + * Add a primary key to a database table. + * + * @param string|string[] $columns Table Column(s) + * @return $this + */ + public function addPrimaryKey(string|array $columns) + { + $this->primaryKey = $columns; + + return $this; + } + /** * Add a table column. * @@ -538,10 +561,10 @@ public function hasForeignKey(string|array $columns, ?string $constraint = null) * @param bool $withTimezone Whether to set the timezone option on the added columns * @return $this */ - public function addTimestamps(string|false|null $createdAt = 'created_at', string|false|null $updatedAt = 'updated_at', bool $withTimezone = false) + public function addTimestamps(string|false|null $createdAt = 'created', string|false|null $updatedAt = 'updated', bool $withTimezone = false) { - $createdAt = $createdAt ?? 'created_at'; - $updatedAt = $updatedAt ?? 'updated_at'; + $createdAt = $createdAt ?? 'created'; + $updatedAt = $updatedAt ?? 'updated'; if (!$createdAt && !$updatedAt) { throw new RuntimeException('Cannot set both created_at and updated_at columns to false'); @@ -625,11 +648,90 @@ public function insert(array $data) */ public function create(): void { + $options = $this->getTable()->getOptions(); + if ((!isset($options['id']) || $options['id'] === false) && !empty($this->primaryKey)) { + $options['primary_key'] = (array)$this->primaryKey; + $this->filterPrimaryKey($options); + } + + $adapter = $this->getAdapter(); + if ($adapter->getAdapterType() === 'mysql' && empty($options['collation'])) { + // TODO this should be a method on the MySQL adapter. + // It could be a hook method on the adapter? + $encodingRequest = 'SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME + FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :dbname'; + + $connection = $adapter->getConnection(); + $connectionConfig = $connection->config(); + + $statement = $connection->execute($encodingRequest, ['dbname' => $connectionConfig['database']]); + $defaultEncoding = $statement->fetch('assoc'); + if (!empty($defaultEncoding['DEFAULT_COLLATION_NAME'])) { + $options['collation'] = $defaultEncoding['DEFAULT_COLLATION_NAME']; + } + } + + $this->getTable()->setOptions($options); + $this->executeActions(false); $this->saveData(); $this->reset(); // reset pending changes } + /** + * This method is called in case a primary key was defined using the addPrimaryKey() method. + * It currently does something only if using SQLite. + * If a column is an auto-increment key in SQLite, it has to be a primary key and it has to defined + * when defining the column. Phinx takes care of that so we have to make sure columns defined as autoincrement were + * not added with the addPrimaryKey method, otherwise, SQL queries will be wrong. + * + * @return void + */ + protected function filterPrimaryKey(array $options): void + { + if ($this->getAdapter()->getAdapterType() !== 'sqlite' || empty($options['primary_key'])) { + return; + } + + $primaryKey = $options['primary_key']; + if (!is_array($primaryKey)) { + $primaryKey = [$primaryKey]; + } + $primaryKey = array_flip($primaryKey); + + $columnsCollection = (new Collection($this->actions->getActions())) + ->filter(function ($action) { + return $action instanceof AddColumn; + }) + ->map(function ($action) { + /** @var \Phinx\Db\Action\ChangeColumn|\Phinx\Db\Action\RenameColumn|\Phinx\Db\Action\RemoveColumn|\Phinx\Db\Action\AddColumn $action */ + return $action->getColumn(); + }); + $primaryKeyColumns = $columnsCollection->filter(function (Column $columnDef, $key) use ($primaryKey) { + return isset($primaryKey[$columnDef->getName()]); + })->toArray(); + + if (empty($primaryKeyColumns)) { + return; + } + + foreach ($primaryKeyColumns as $primaryKeyColumn) { + if ($primaryKeyColumn->isIdentity()) { + unset($primaryKey[$primaryKeyColumn->getName()]); + } + } + + $primaryKey = array_flip($primaryKey); + + if (!empty($primaryKey)) { + $options['primary_key'] = $primaryKey; + } else { + unset($options['primary_key']); + } + + $this->getTable()->setOptions($options); + } + /** * Updates a table from the object instance. * diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index 22bdf851..668d911f 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -765,6 +765,7 @@ protected function getAliasedOptions(): array return [ 'length' => 'limit', 'precision' => 'limit', + 'autoIncrement' => 'identity', ]; } diff --git a/src/MigrationInterface.php b/src/MigrationInterface.php index 28046a39..8ec7c81f 100644 --- a/src/MigrationInterface.php +++ b/src/MigrationInterface.php @@ -290,7 +290,7 @@ public function hasTable(string $tableName): bool; * @param array $options Options * @return \Migrations\Db\Table */ - public function table(string $tableName, array $options): Table; + public function table(string $tableName, array $options = []): Table; /** * Perform checks on the migration, printing a warning diff --git a/src/Shim/MigrationAdapter.php b/src/Shim/MigrationAdapter.php index ba80f2de..70a3b916 100644 --- a/src/Shim/MigrationAdapter.php +++ b/src/Shim/MigrationAdapter.php @@ -374,7 +374,7 @@ public function hasTable(string $tableName): bool /** * {@inheritDoc} */ - public function table(string $tableName, array $options): Table + public function table(string $tableName, array $options = []): Table { throw new RuntimeException('MigrationAdapter::table is not implemented'); } diff --git a/tests/TestCase/Command/MigrateCommandTest.php b/tests/TestCase/Command/MigrateCommandTest.php index 1950d11c..b474a468 100644 --- a/tests/TestCase/Command/MigrateCommandTest.php +++ b/tests/TestCase/Command/MigrateCommandTest.php @@ -108,6 +108,27 @@ public function testMigrateSourceDefault(): void $this->assertFileExists($dumpFile); } + /** + * Integration test for BaseMigration with built-in backend. + */ + public function testMigrateBaseMigration(): void + { + $migrationPath = ROOT . DS . 'config' . DS . 'BaseMigrations'; + $this->exec('migrations migrate -v --source BaseMigrations -c test --no-lock'); + $this->assertExitSuccess(); + + $this->assertOutputContains('using connection test'); + $this->assertOutputContains('using paths ' . $migrationPath); + $this->assertOutputContains('BaseMigrationTables: migrated'); + $this->assertOutputContains('query=121'); + $this->assertOutputContains('fetchRow=122'); + $this->assertOutputContains('hasTable=1'); + $this->assertOutputContains('All Done'); + + $table = $this->fetchTable('Phinxlog'); + $this->assertCount(1, $table->find()->all()->toArray()); + } + /** * Test that running with a no-op migrations is successful */ diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index f1999989..824b0f4d 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -397,7 +397,7 @@ public function testCreateTableAndInheritDefaultCollation() ->save(); $this->assertTrue($adapter->hasTable('table_with_default_collation')); $row = $adapter->fetchRow(sprintf("SHOW TABLE STATUS WHERE Name = '%s'", 'table_with_default_collation')); - $this->assertEquals('utf8mb4_unicode_ci', $row['Collation']); + $this->assertEquals('utf8mb4_0900_ai_ci', $row['Collation']); } public function testCreateTableWithLatin1Collate() @@ -498,13 +498,13 @@ public function testAddTimestampsFeatureFlag() $this->assertCount(3, $columns); $this->assertSame('id', $columns[0]->getName()); - $this->assertEquals('created_at', $columns[1]->getName()); + $this->assertEquals('created', $columns[1]->getName()); $this->assertEquals('datetime', $columns[1]->getType()); $this->assertEquals('', $columns[1]->getUpdate()); $this->assertFalse($columns[1]->isNull()); $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getDefault()); - $this->assertEquals('updated_at', $columns[2]->getName()); + $this->assertEquals('updated', $columns[2]->getName()); $this->assertEquals('datetime', $columns[2]->getType()); $this->assertEquals('CURRENT_TIMESTAMP', $columns[2]->getUpdate()); $this->assertTrue($columns[2]->isNull()); @@ -2153,7 +2153,7 @@ public function testDumpCreateTable() ->save(); $expectedOutput = <<<'OUTPUT' -CREATE TABLE `table1` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, `column3` VARCHAR(255) NOT NULL DEFAULT 'test', PRIMARY KEY (`id`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE TABLE `table1` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, `column3` VARCHAR(255) NOT NULL DEFAULT 'test', PRIMARY KEY (`id`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; OUTPUT; $actualOutput = join("\n", $this->out->messages()); $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create table query to the output'); @@ -2258,7 +2258,7 @@ public function testDumpCreateTableAndThenInsert() ])->save(); $expectedOutput = <<<'OUTPUT' -CREATE TABLE `table1` (`column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, PRIMARY KEY (`column1`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE TABLE `table1` (`column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, PRIMARY KEY (`column1`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; INSERT INTO `table1` (`column1`, `column2`) VALUES ('id1', 1); OUTPUT; $actualOutput = join("\n", $this->out->messages()); diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index a3f95ec7..f1212768 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -1875,7 +1875,7 @@ public function testNullWithoutDefaultValue() public function testDumpCreateTable() { - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string', ['null' => false]) @@ -1902,7 +1902,7 @@ public function testDumpInsert() ->addColumn('int_col', 'integer') ->save(); - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $this->adapter->insert($table->getTable(), [ 'string_col' => 'test data', ]); @@ -1942,7 +1942,7 @@ public function testDumpBulkinsert() ->addColumn('int_col', 'integer') ->save(); - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $this->adapter->bulkinsert($table->getTable(), [ [ 'string_col' => 'test_data1', @@ -1968,7 +1968,7 @@ public function testDumpBulkinsert() public function testDumpCreateTableAndThenInsert() { - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $table = new Table('table1', ['id' => false, 'primary_key' => ['column1']], $this->adapter); $table->addColumn('column1', 'string', ['null' => false]) @@ -2086,8 +2086,8 @@ public function testAlterTableColumnAdd() ['name' => 'string_col', 'type' => 'string', 'default' => '', 'null' => true], ['name' => 'string_col_2', 'type' => 'string', 'default' => null, 'null' => true], ['name' => 'string_col_3', 'type' => 'string', 'default' => null, 'null' => false], - ['name' => 'created_at', 'type' => 'timestamp', 'default' => 'CURRENT_TIMESTAMP', 'null' => false], - ['name' => 'updated_at', 'type' => 'timestamp', 'default' => null, 'null' => true], + ['name' => 'created', 'type' => 'timestamp', 'default' => 'CURRENT_TIMESTAMP', 'null' => false], + ['name' => 'updated', 'type' => 'timestamp', 'default' => null, 'null' => true], ]; $this->assertEquals(count($expected), count($columns)); diff --git a/tests/test_app/config/BaseMigrations/20230628181900_base_migration_tables.php b/tests/test_app/config/BaseMigrations/20230628181900_base_migration_tables.php new file mode 100644 index 00000000..c57cc0ff --- /dev/null +++ b/tests/test_app/config/BaseMigrations/20230628181900_base_migration_tables.php @@ -0,0 +1,28 @@ +table('base_stores', ['collation' => 'utf8_bin']); + $table + ->addColumn('name', 'string') + ->addTimestamps() + ->addPrimaryKey('id') + ->create(); + $io = $this->getIo(); + + $res = $this->query('SELECT 121 as val'); + $io->out('query=' . $res->fetchColumn(0)); + $io->out('fetchRow=' . $this->fetchRow('SELECT 122 as val')['val']); + $io->out('hasTable=' . $this->hasTable('base_stores')); + + // Run for coverage + $this->getSelectBuilder(); + $this->getInsertBuilder(); + $this->getDeleteBuilder(); + $this->getUpdateBuilder(); + } +} From b0fc9af948106514e17cbb008b449342c6d21641 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 2 Nov 2024 00:26:55 -0400 Subject: [PATCH 33/47] Update bake commands to use builtin base classe (#764) * Update seed generation to use new base classes When the new builtin backend is in use, generated seeds should use the new backend. * Update migrations generation to use builtin backend When enabled, bake migration should use migrations base classes for new migrations. --- src/BaseMigration.php | 12 ++++++-- src/Command/BakeMigrationCommand.php | 2 ++ src/Command/BakeSeedCommand.php | 1 + templates/bake/Seed/seed.twig | 9 ++++++ templates/bake/config/skeleton.twig | 6 ++++ .../Command/BakeMigrationCommandTest.php | 17 +++++++++++ .../BakeMigrationSnapshotCommandTest.php | 1 + .../TestCase/Command/BakeSeedCommandTest.php | 17 +++++++++++ tests/TestCase/Db/Table/TableTest.php | 2 +- tests/comparisons/Migration/testCreate.php | 4 +-- .../Migration/testCreateDatetime.php | 4 +-- .../Migration/testCreateDropMigration.php | 4 +-- .../Migration/testCreateFieldLength.php | 4 +-- .../comparisons/Migration/testCreatePhinx.php | 25 +++++++++++++++++ .../Migration/testCreatePrimaryKey.php | 4 +-- .../Migration/testCreatePrimaryKeyUuid.php | 4 +-- .../comparisons/Migration/testNoContents.php | 4 +-- .../comparisons/Seeds/pgsql/testWithData.php | 4 +-- .../Seeds/pgsql/testWithDataAndLimit.php | 4 +-- .../comparisons/Seeds/php81/testWithData.php | 4 +-- .../Seeds/php81/testWithDataAndLimit.php | 4 +-- .../Seeds/sqlserver/testWithData.php | 4 +-- .../Seeds/sqlserver/testWithDataAndLimit.php | 4 +-- tests/comparisons/Seeds/testBasicBaking.php | 4 +-- .../Seeds/testBasicBakingPhinx.php | 28 +++++++++++++++++++ tests/comparisons/Seeds/testPrettifyArray.php | 4 +-- .../Seeds/testWithDataAndFields.php | 4 +-- 27 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 tests/comparisons/Migration/testCreatePhinx.php create mode 100644 tests/comparisons/Seeds/testBasicBakingPhinx.php diff --git a/src/BaseMigration.php b/src/BaseMigration.php index 4e8fc3f8..589ca8eb 100644 --- a/src/BaseMigration.php +++ b/src/BaseMigration.php @@ -61,6 +61,13 @@ class BaseMigration implements MigrationInterface */ protected bool $isMigratingUp = true; + /** + * The version number. + * + * @var int + */ + protected int $version; + /** * Whether the tables created in this migration * should auto-create an `id` field or not @@ -78,9 +85,10 @@ class BaseMigration implements MigrationInterface * * @param int $version The version this migration is */ - public function __construct(protected int $version) + public function __construct(int $version) { - $this->validateVersion($this->version); + $this->validateVersion($version); + $this->version = $version; } /** diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index 1a1f2e0b..2e6fad8c 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -98,6 +98,7 @@ public function templateData(Arguments $arguments): array 'tables' => [], 'action' => null, 'name' => $className, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } @@ -120,6 +121,7 @@ public function templateData(Arguments $arguments): array 'primaryKey' => $primaryKey, ], 'name' => $className, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index 6aedaf61..61d2ce72 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -142,6 +142,7 @@ public function templateData(Arguments $arguments): array 'namespace' => $namespace, 'records' => $records, 'table' => $table, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/templates/bake/Seed/seed.twig b/templates/bake/Seed/seed.twig index 9ce3b231..8ba8f122 100644 --- a/templates/bake/Seed/seed.twig +++ b/templates/bake/Seed/seed.twig @@ -16,12 +16,21 @@ assertSameAsFile(__FUNCTION__ . $fileSuffix, $result); } + /** + * Test that when the phinx backend is active migrations use + * phinx base classes. + */ + public function testCreatePhinx() + { + Configure::write('Migrations.backend', 'phinx'); + $this->exec('bake migration CreateUsers name --connection test'); + + $file = glob(ROOT . DS . 'config' . DS . 'Migrations' . DS . '*_CreateUsers.php'); + $filePath = current($file); + + $this->assertExitCode(BaseCommand::CODE_SUCCESS); + $result = file_get_contents($filePath); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + /** * Tests that baking a migration with the name as another will throw an exception. */ diff --git a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php index 2d0c5f04..4960d542 100644 --- a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php @@ -200,6 +200,7 @@ protected function runSnapshotTest(string $scenario, string $arguments = ''): vo $generatedMigration = glob($this->migrationPath . "*_TestSnapshot{$scenario}*.php"); $this->generatedFiles = $generatedMigration; $this->generatedFiles[] = $this->migrationPath . 'schema-dump-test.lock'; + $generatedMigration = basename($generatedMigration[0]); $fileName = pathinfo($generatedMigration, PATHINFO_FILENAME); $this->assertOutputContains('Marking the migration ' . $fileName . ' as migrated...'); diff --git a/tests/TestCase/Command/BakeSeedCommandTest.php b/tests/TestCase/Command/BakeSeedCommandTest.php index 5b3731b7..0ab7be7b 100644 --- a/tests/TestCase/Command/BakeSeedCommandTest.php +++ b/tests/TestCase/Command/BakeSeedCommandTest.php @@ -14,6 +14,7 @@ namespace Migrations\Test\TestCase\Command; use Cake\Console\BaseCommand; +use Cake\Core\Configure; use Cake\Core\Plugin; use Cake\TestSuite\StringCompareTrait; use Migrations\Test\TestCase\TestCase; @@ -51,6 +52,22 @@ public function setUp(): void $this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Seeds' . DS; } + /** + * Test empty migration with phinx base class. + * + * @return void + */ + public function testBasicBakingPhinx() + { + Configure::write('Migrations.backend', 'phinx'); + $this->generatedFile = ROOT . DS . 'config/Seeds/ArticlesSeed.php'; + $this->exec('bake seed Articles --connection test'); + + $this->assertExitCode(BaseCommand::CODE_SUCCESS); + $result = file_get_contents($this->generatedFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + /** * Test empty migration. * diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 679650c5..4aa2ef26 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -22,7 +22,7 @@ class TableTest extends TestCase { public static function provideAdapters() { - return [[new SqlServerAdapter([])], [new MysqlAdapter([])], [new PostgresAdapter([])], [new SQLiteAdapter([])]]; + return [[new SqlServerAdapter([])], [new MysqlAdapter([])], [new PostgresAdapter([])], [new SQLiteAdapter(['name' => ':memory:'])]]; } public static function provideTimestampColumnNames() diff --git a/tests/comparisons/Migration/testCreate.php b/tests/comparisons/Migration/testCreate.php index b354f24b..4f84045e 100644 --- a/tests/comparisons/Migration/testCreate.php +++ b/tests/comparisons/Migration/testCreate.php @@ -1,9 +1,9 @@ table('users'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->create(); + } +} diff --git a/tests/comparisons/Migration/testCreatePrimaryKey.php b/tests/comparisons/Migration/testCreatePrimaryKey.php index bf9471ab..dbb6ab39 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKey.php +++ b/tests/comparisons/Migration/testCreatePrimaryKey.php @@ -1,9 +1,9 @@ table('articles'); + $table->insert($data)->save(); + } +} diff --git a/tests/comparisons/Seeds/testPrettifyArray.php b/tests/comparisons/Seeds/testPrettifyArray.php index 58ced7ff..51eacbea 100644 --- a/tests/comparisons/Seeds/testPrettifyArray.php +++ b/tests/comparisons/Seeds/testPrettifyArray.php @@ -1,12 +1,12 @@ Date: Tue, 5 Nov 2024 17:22:59 -0500 Subject: [PATCH 34/47] Import documentation for seed + migration authoring (#765) I've adapted the content slightly to reflect current state of migrations more. * Update docs link in generated code * Update docs links for seeds with new base class * Update more snapshots. * Update sqlserver snapshots --- docs/en/seeding.rst | 229 ++ docs/en/writing-migrations.rst | 1867 +++++++++++++++++ templates/bake/Seed/seed.twig | 4 + templates/bake/config/skeleton.twig | 4 + tests/comparisons/Migration/testCreate.php | 2 +- .../Migration/testCreateDatetime.php | 2 +- .../Migration/testCreateDropMigration.php | 2 +- .../Migration/testCreateFieldLength.php | 2 +- .../Migration/testCreatePrimaryKey.php | 2 +- .../Migration/testCreatePrimaryKeyUuid.php | 2 +- .../comparisons/Migration/testNoContents.php | 2 +- .../comparisons/Seeds/pgsql/testWithData.php | 2 +- .../Seeds/pgsql/testWithDataAndLimit.php | 2 +- .../comparisons/Seeds/php81/testWithData.php | 2 +- .../Seeds/php81/testWithDataAndLimit.php | 2 +- .../Seeds/sqlserver/testWithData.php | 2 +- .../Seeds/sqlserver/testWithDataAndLimit.php | 2 +- tests/comparisons/Seeds/testBasicBaking.php | 2 +- tests/comparisons/Seeds/testPrettifyArray.php | 2 +- .../Seeds/testWithDataAndFields.php | 2 +- 20 files changed, 2120 insertions(+), 16 deletions(-) create mode 100644 docs/en/seeding.rst create mode 100644 docs/en/writing-migrations.rst diff --git a/docs/en/seeding.rst b/docs/en/seeding.rst new file mode 100644 index 00000000..74c338f7 --- /dev/null +++ b/docs/en/seeding.rst @@ -0,0 +1,229 @@ +Database Seeding +================ + +Seed classes are a great way to easily fill your database with data after +it's created. By default they are stored in the `seeds` directory; however, this +path can be changed in your configuration file. + +.. note:: + + Database seeding is entirely optional, and Migrations does not create a `Seeds` + directory by default. + +Creating a New Seed Class +------------------------- + +Migrations includes a command to easily generate a new seed class: + +.. code-block:: bash + + $ bin/cake bake seed MyNewSeeder + +It is based on a skeleton template: + +.. code-block:: php + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ],[ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + } + } + +.. note:: + + You must call the ``saveData()`` method to commit your data to the table. + Migrations will buffer data until you do so. + +Truncating Tables +----------------- + +In addition to inserting data Migrations makes it trivial to empty your tables using the +SQL `TRUNCATE` command: + +.. code-block:: php + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + + // empty the table + $posts->truncate(); + } + } + +.. note:: + + SQLite doesn't natively support the ``TRUNCATE`` command so behind the scenes + ``DELETE FROM`` is used. It is recommended to call the ``VACUUM`` command + after truncating a table. Migrations does not do this automatically. + +Executing Seed Classes +---------------------- + +This is the easy part. To seed your database, simply use the ``migrations seed`` command: + +.. code-block:: bash + + $ bin/cake migrations seed + +By default, Migrations will execute all available seed classes. If you would like to +run a specific class, simply pass in the name of it using the ``--seed`` parameter: + +.. code-block:: bash + + $ bin/cake migrations seed --seed UserSeeder + +You can also run multiple seeders: + +.. code-block:: bash + + $ bin/cake migrations seed --seed UserSeeder --seed PermissionSeeder --seed LogSeeder + +You can also use the `-v` parameter for more output verbosity: + +.. code-block:: bash + + $ bin/cake migrations seed -v + +The Migrations seed functionality provides a simple mechanism to easily and repeatably +insert test data into your database, this is great for development environment +sample data or getting state for demos. diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst new file mode 100644 index 00000000..723c2ab9 --- /dev/null +++ b/docs/en/writing-migrations.rst @@ -0,0 +1,1867 @@ +Writing Migrations +================== + +Migrations are a declarative API that helps you transform your database. Each migration +is represented by a PHP class in a unique file. It is preferred that you write +your migrations using the Migrations API, but raw SQL is also supported. + +Creating a New Migration +------------------------ +Generating a skeleton migration file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's start by creating a new migration with ``bake``: + +.. code-block:: bash + + $ bin/cake bake migration + +This will create a new migration in the format +``YYYYMMDDHHMMSS_my_new_migration.php``, where the first 14 characters are +replaced with the current timestamp down to the second. + +If you have specified multiple migration paths, you will be asked to select +which path to create the new migration in. + +Bake will automatically creates a skeleton migration file with a single method: + +.. code-block:: php + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + } + +When executing this migration, Migrations will create the ``user_logins`` table on +the way up and automatically figure out how to drop the table on the way down. +Please be aware that when a ``change`` method exists, Migrations will automatically +ignore the ``up`` and ``down`` methods. If you need to use these methods it is +recommended to create a separate migration file. + +.. note:: + + When creating or updating tables inside a ``change()`` method you must use + the Table ``create()`` and ``update()`` methods. Migrations cannot automatically + determine whether a ``save()`` call is creating a new table or modifying an + existing one. + +The following actions are reversible when done through the Table API in +Migrations, and will be automatically reversed: + +- Creating a table +- Renaming a table +- Adding a column +- Renaming a column +- Adding an index +- Adding a foreign key + +If a command cannot be reversed then Migrations will throw an +``IrreversibleMigrationException`` when it's migrating down. If you wish to +use a command that cannot be reversed in the change function, you can use an +if statement with ``$this->isMigratingUp()`` to only run things in the +up or down direction. For example: + +.. code-block:: php + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + if ($this->isMigratingUp()) { + $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) + ->save(); + } + } + } + +The Up Method +~~~~~~~~~~~~~ + +The up method is automatically run by Migrations when you are migrating up and it +detects the given migration hasn't been executed previously. You should use the +up method to transform the database with your intended changes. + +The Down Method +~~~~~~~~~~~~~~~ + +The down method is automatically run by Migrations when you are migrating down and +it detects the given migration has been executed in the past. You should use +the down method to reverse/undo the transformations described in the up method. + +The Init Method +~~~~~~~~~~~~~~~ + +The ``init()`` method is run by Migrations before the migration methods if it exists. +This can be used for setting common class properties that are then used within +the migration methods. + +The Should Execute Method +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``shouldExecute()`` method is run by Migrations before executing the migration. +This can be used to prevent the migration from being executed at this time. It always +returns true by default. You can override it in your custom ``BaseMigration`` +implementation. + +Executing Queries +----------------- + +Queries can be executed with the ``execute()`` and ``query()`` methods. The +``execute()`` method returns the number of affected rows whereas the +``query()`` method returns the result as a +`CakePHP Statement `_. Both methods +accept an optional second parameter ``$params`` which is an array of elements, +and if used will cause the underlying connection to use a prepared statement. + +.. code-block:: php + + execute('DELETE FROM users'); // returns the number of affected rows + + // query() + $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement + $rows = $stmt->fetchAll(); // returns the result as an array + + // using prepared queries + $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); + $stmt = $this->query('SELECT * FROM users WHERE id > ?', [5]); // returns PDOStatement + $rows = $stmt->fetchAll(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +.. note:: + + These commands run using the PHP Data Objects (PDO) extension which + defines a lightweight, consistent interface for accessing databases + in PHP. Always make sure your queries abide with PDOs before using + the ``execute()`` command. This is especially important when using + DELIMITERs during insertion of stored procedures or triggers which + don't support DELIMITERs. + +.. note:: + + If you wish to execute multiple queries at once, you may not also use the prepared + variant of these functions. When using prepared queries, PDO can only execute + them one at a time. + +.. warning:: + + When using ``execute()`` or ``query()`` with a batch of queries, PDO doesn't + throw an exception if there is an issue with one or more of the queries + in the batch. + + As such, the entire batch is assumed to have passed without issue. + + If Migrations was to iterate any potential result sets, looking to see if one + had an error, then Migrations would be denying access to all the results as there + is no facility in PDO to get a previous result set + `nextRowset() `_ - + but no ``previousSet()``). + + So, as a consequence, due to the design decision in PDO to not throw + an exception for batched queries, Migrations is unable to provide the fullest + support for error handling when batches of queries are supplied. + + Fortunately though, all the features of PDO are available, so multiple batches + can be controlled within the migration by calling upon + `nextRowset() `_ + and examining `errorInfo `_. + +Fetching Rows +------------- + +There are two methods available to fetch rows. The ``fetchRow()`` method will +fetch a single row, whilst the ``fetchAll()`` method will return multiple rows. +Both methods accept raw SQL as their only parameter. + +.. code-block:: php + + fetchRow('SELECT * FROM users'); + + // fetch an array of messages + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Inserting Data +-------------- + +Migrations makes it easy to insert data into your tables. Whilst this feature is +intended for the :doc:`seed feature `, you are also free to use the +insert methods in your migrations. + +.. code-block:: php + + table('status'); + + // inserting only one row + $singleRow = [ + 'id' => 1, + 'name' => 'In Progress' + ]; + + $table->insert($singleRow)->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + $table->insert($rows)->saveData(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute('DELETE FROM status'); + } + } + +.. note:: + + You cannot use the insert methods inside a `change()` method. Please use the + `up()` and `down()` methods. + +Working With Tables +------------------- + +The Table Object +~~~~~~~~~~~~~~~~ + +The Table object is one of the most useful APIs provided by Migrations. It allows +you to easily manipulate database tables using PHP code. You can retrieve an +instance of the Table object by calling the ``table()`` method from within +your database migration. + +.. code-block:: php + + table('tableName'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +You can then manipulate this table using the methods provided by the Table +object. + +Saving Changes +~~~~~~~~~~~~~~ + +When working with the Table object, Migrations stores certain operations in a +pending changes cache. Once you have made the changes you want to the table, +you must save them. To perform this operation, Migrations provides three methods, +``create()``, ``update()``, and ``save()``. ``create()`` will first create +the table and then run the pending changes. ``update()`` will just run the +pending changes, and should be used when the table already exists. ``save()`` +is a helper function that checks first if the table exists and if it does not +will run ``create()``, else it will run ``update()``. + +As stated above, when using the ``change()`` migration method, you should always +use ``create()`` or ``update()``, and never ``save()`` as otherwise migrating +and rolling back may result in different states, due to ``save()`` calling +``create()`` when running migrate and then ``update()`` on rollback. When +using the ``up()``/``down()`` methods, it is safe to use either ``save()`` or +the more explicit methods. + +When in doubt with working with tables, it is always recommended to call +the appropriate function and commit any pending changes to the database. + +Creating a Table +~~~~~~~~~~~~~~~~ + +Creating a table is really easy using the Table object. Let's create a table to +store a collection of users. + +.. code-block:: php + + table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->create(); + } + } + +Columns are added using the ``addColumn()`` method. We create a unique index +for both the username and email columns using the ``addIndex()`` method. +Finally calling ``create()`` commits the changes to the database. + +.. note:: + + Migrations automatically creates an auto-incrementing primary key column called ``id`` for every + table. + +The ``id`` option sets the name of the automatically created identity field, while the ``primary_key`` +option selects the field or fields used for primary key. ``id`` will always override the ``primary_key`` +option unless it's set to false. If you don't need a primary key set ``id`` to false without +specifying a ``primary_key``, and no primary key will be created. + +To specify an alternate primary key, you can specify the ``primary_key`` option +when accessing the Table object. Let's disable the automatic ``id`` column and +create a primary key using two columns instead: + +.. code-block:: php + + table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + } + +Setting a single ``primary_key`` doesn't enable the ``AUTO_INCREMENT`` option. +To simply change the name of the primary key, we need to override the default ``id`` field name: + +.. code-block:: php + + table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } + } + +In addition, the MySQL adapter supports following options: + +========== =========== +Option Description +========== =========== +comment set a text comment on the table +row_format set the table row format +engine define table engine *(defaults to ``InnoDB``)* +collation define table collation *(defaults to ``utf8mb4_unicode_ci``)* +signed whether the primary key is ``signed`` *(defaults to ``false``)* +limit set the maximum length for the primary key +========== =========== + +By default, the primary key is ``unsigned``. +To simply set it to be signed just pass ``signed`` option with a ``true`` value: + +.. code-block:: php + + table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } + } + + +The PostgreSQL adapter supports the following options: + +========= =========== +Option Description +========= =========== +comment set a text comment on the table +========= =========== + +To view available column types and options, see `Valid Column Types`_ for details. + +Determining Whether a Table Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can determine whether or not a table exists by using the ``hasTable()`` +method. + +.. code-block:: php + + hasTable('users'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Dropping a Table +~~~~~~~~~~~~~~~~ + +Tables can be dropped quite easily using the ``drop()`` method. It is a +good idea to recreate the table again in the ``down()`` method. + +Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved. + +.. code-block:: php + + table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + } + +Renaming a Table +~~~~~~~~~~~~~~~~ + +To rename a table access an instance of the Table object then call the +``rename()`` method. + +.. code-block:: php + + table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } + } + +Changing the Primary Key +~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the primary key on an existing table, use the ``changePrimaryKey()`` method. +Pass in a column name or array of columns names to include in the primary key, or ``null`` to drop the primary key. +Note that the mentioned columns must be added to the table, they will not be added implicitly. + +.. code-block:: php + + table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Changing the Table Comment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the comment on an existing table, use the ``changeComment()`` method. +Pass in a string to set as the new table comment, or ``null`` to drop the existing comment. + +.. code-block:: php + + table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Columns +-------------------- + +.. _valid-column-types: + +Valid Column Types +~~~~~~~~~~~~~~~~~~ + +Column types are specified as strings and can be one of: + +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- double +- smallinteger +- integer +- biginteger +- string +- text +- time +- timestamp +- uuid + +In addition, the MySQL adapter supports ``enum``, ``set``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob``, ``bit`` and ``json`` column types +(``json`` in MySQL 5.7 and above). When providing a limit value and using ``binary``, ``varbinary`` or ``blob`` and its subtypes, the retained column +type will be based on required length (see `Limit Option and MySQL`_ for details); + +In addition, the Postgres adapter supports ``interval``, ``json``, ``jsonb``, ``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types +(PostgreSQL 9.3 and above). + +Valid Column Options +~~~~~~~~~~~~~~~~~~~~ + +The following are valid column options: + +For any column type: + +======= =========== +Option Description +======= =========== +limit set maximum length for strings, also hints column types in adapters (see note below) +length alias for ``limit`` +default set default value or action +null allow ``NULL`` values, defaults to ``true`` (setting ``identity`` will override default to ``false``) +after specify the column that a new column should be placed after, or use ``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` to place the column at the start of the table *(only applies to MySQL)* +comment set a text comment on the column +======= =========== + +For ``decimal`` columns: + +========= =========== +Option Description +========= =========== +precision combine with ``scale`` set to set decimal accuracy +scale combine with ``precision`` to set decimal accuracy +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +========= =========== + +For ``enum`` and ``set`` columns: + +========= =========== +Option Description +========= =========== +values Can be a comma separated list or an array of values +========= =========== + +For ``smallinteger``, ``integer`` and ``biginteger`` columns: + +======== =========== +Option Description +======== =========== +identity enable or disable automatic incrementing (if enabled, will set ``null: false`` if ``null`` option is not set) +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For Postgres, when using ``identity``, it will utilize the ``serial`` type appropriate for the integer size, so that +``smallinteger`` will give you ``smallserial``, ``integer`` gives ``serial``, and ``biginteger`` gives ``bigserial``. + +For ``timestamp`` columns: + +======== =========== +Option Description +======== =========== +default set default value (use with ``CURRENT_TIMESTAMP``) +update set an action to be triggered when the row is updated (use with ``CURRENT_TIMESTAMP``) *(only applies to MySQL)* +timezone enable or disable the ``with time zone`` option for ``time`` and ``timestamp`` columns *(only applies to Postgres)* +======== =========== + +You can add ``created_at`` and ``updated_at`` timestamps to a table using the ``addTimestamps()`` method. This method accepts +three arguments, where the first two allow setting alternative names for the columns while the third argument allows you to +enable the ``timezone`` option for the columns. The defaults for these arguments are ``created_at``, ``updated_at``, and ``false`` +respectively. For the first and second argument, if you provide ``null``, then the default name will be used, and if you provide +``false``, then that column will not be created. Please note that attempting to set both to ``false`` will throw a +``\RuntimeException``. Additionally, you can use the ``addTimestampsWithTimezone()`` method, which is an alias to +``addTimestamps()`` that will always set the third argument to ``true`` (see examples below). The ``created_at`` column will +have a default set to ``CURRENT_TIMESTAMP``. For MySQL only, ``update_at`` column will have update set to +``CURRENT_TIMESTAMP``. + +.. code-block:: php + + table('users')->addTimestamps()->create(); + // Use defaults (with timezones) + $table = $this->table('users')->addTimestampsWithTimezone()->create(); + + // Override the 'created' column name with 'recorded_at'. + $table = $this->table('books')->addTimestamps('recorded_at')->create(); + + // Override the 'updated' column name with 'amended_at', preserving timezones. + // The two lines below do the same, the second one is simply cleaner. + $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); + $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); + + // Only add the created column to the table + $table = $this->table('books')->addTimestamps(null, false); + // Only add the updated column to the table + $table = $this->table('users')->addTimestamps(false); + // Note, setting both false will throw a \RuntimeError + } + } + +For ``boolean`` columns: + +======== =========== +Option Description +======== =========== +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For ``string`` and ``text`` columns: + +========= =========== +Option Description +========= =========== +collation set collation that differs from table defaults *(only applies to MySQL)* +encoding set character set that differs from table defaults *(only applies to MySQL)* +========= =========== + +For foreign key definitions: + +========== =========== +Option Description +========== =========== +update set an action to be triggered when the row is updated +delete set an action to be triggered when the row is deleted +constraint set a name to be used by foreign key constraint +========== =========== + +You can pass one or more of these options to any column with the optional +third argument array. + +Limit Option and MySQL +~~~~~~~~~~~~~~~~~~~~~~ + +When using the MySQL adapter, there are a couple things to consider when working with limits: + +- When using a ``string`` primary key or index on MySQL 5.7 or below, or the MyISAM storage engine, and the default charset of ``utf8mb4_unicode_ci``, you must specify a limit less than or equal to 191, or use a different charset. +- Additional hinting of database column type can be made for ``integer``, ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using ``limit`` with one the following options will modify the column type accordingly: + +============ ============== +Limit Column Type +============ ============== +BLOB_TINY TINYBLOB +BLOB_REGULAR BLOB +BLOB_MEDIUM MEDIUMBLOB +BLOB_LONG LONGBLOB +TEXT_TINY TINYTEXT +TEXT_REGULAR TEXT +TEXT_MEDIUM MEDIUMTEXT +TEXT_LONG LONGTEXT +INT_TINY TINYINT +INT_SMALL SMALLINT +INT_MEDIUM MEDIUMINT +INT_REGULAR INT +INT_BIG BIGINT +============ ============== + +For ``binary`` or ``varbinary`` types, if limit is set greater than allowed 255 bytes, the type will be changed to the best matching blob type given the length. + +.. code-block:: php + + table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); + +Custom Column Types & Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some DBMS systems provide additional column types and default values that are specific to them. +If you don't want to keep your migrations DBMS-agnostic you can use those custom types in your migrations +through the ``\Migrations\Db\Literal::from`` method, which takes a string as its only argument, and returns an +instance of ``\Migrations\Db\Literal``. When Migrations encounters this value as a column's type it knows not to +run any validation on it and to use it exactly as supplied without escaping. This also works for ``default`` +values. + +You can see an example below showing how to add a ``citext`` column as well as a column whose default value +is a function, in PostgreSQL. This method of preventing the built-in escaping is supported in all adapters. + +.. code-block:: php + + table('users') + ->addColumn('username', Literal::from('citext')) + ->addColumn('uniqid', 'uuid', [ + 'default' => Literal::from('uuid_generate_v4()') + ]) + ->addColumn('creation', 'timestamp', [ + 'timezone' => true, + 'default' => Literal::from('now()') + ]) + ->create(); + } + } + +Get a column list +~~~~~~~~~~~~~~~~~ + +To retrieve all table columns, simply create a ``table`` object and call ``getColumns()`` +method. This method will return an array of Column classes with basic info. Example below: + +.. code-block:: php + + table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Get a column by name +~~~~~~~~~~~~~~~~~~~~ + +To retrieve one table column, simply create a ``table`` object and call the ``getColumn()`` +method. This method will return a Column class with basic info or NULL when the column doesn't exist. Example below: + +.. code-block:: php + + table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Checking whether a column exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can check if a table already has a certain column by using the +``hasColumn()`` method. + +.. code-block:: php + + table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } + } + +Renaming a Column +~~~~~~~~~~~~~~~~~ + +To rename a column, access an instance of the Table object then call the +``renameColumn()`` method. + +.. code-block:: php + + table('users'); + $table->renameColumn('bio', 'biography') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio') + ->save(); + } + } + +Adding a Column After Another Column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When adding a column with the MySQL adapter, you can dictate its position using the ``after`` option, +where its value is the name of the column to position it after. + +.. code-block:: php + + table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } + } + +This would create the new column ``city`` and position it after the ``email`` column. The +``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` constant can be used to specify that the new column should be +created as the first column in that table. + +Dropping a Column +~~~~~~~~~~~~~~~~~ + +To drop a column, use the ``removeColumn()`` method. + +.. code-block:: php + + table('users'); + $table->removeColumn('short_name') + ->save(); + } + } + + +Specifying a Column Limit +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can limit the maximum length of a column by using the ``limit`` option. + +.. code-block:: php + + table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } + } + +Changing Column Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change column type or options on an existing column, use the ``changeColumn()`` method. +See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values. + +.. code-block:: php + + table('users'); + $users->changeColumn('email', 'string', ['limit' => 255]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Indexes +-------------------- + +To add an index to a table you can simply call the ``addIndex()`` method on the +table object. + +.. code-block:: php + + table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +By default Migrations instructs the database adapter to create a normal index. We +can pass an additional parameter ``unique`` to the ``addIndex()`` method to +specify a unique index. We can also explicitly specify a name for the index +using the ``name`` parameter, the index columns sort order can also be specified using +the ``order`` parameter. The order parameter takes an array of column names and sort order key/value pairs. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex(['email', 'username'], [ + 'unique' => true, + 'name' => 'idx_users_email', + 'order' => ['email' => 'DESC', 'username' => 'ASC']] + ) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +The MySQL adapter also supports ``fulltext`` indexes. If you are using a version before 5.6 you must +ensure the table uses the ``MyISAM`` engine. + +.. code-block:: php + + table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } + } + +In addition, MySQL adapter also supports setting the index length defined by limit option. +When you are using a multi-column index, you are able to define each column index length. +The single column index can define its index length with or without defining column name in limit option. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addColumn('user_guid', 'string', ['limit' => 36]) + ->addIndex(['email','username'], ['limit' => ['email' => 5, 'username' => 2]]) + ->addIndex('user_guid', ['limit' => 6]) + ->create(); + } + } + +The SQL Server and PostgreSQL adapters also supports ``include`` (non-key) columns on indexes. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('firstname','string') + ->addColumn('lastname','string') + ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) + ->create(); + } + } + +In addition PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes. + +.. code-block:: php + + table('users'); + $table->addColumn('address', 'string') + ->addIndex('address', ['type' => 'gin']) + ->create(); + } + } + +Removing indexes is as easy as calling the ``removeIndex()`` method. You must +call this method for each index. + +.. code-block:: php + + table('users'); + $table->removeIndex(['email']) + ->save(); + + // alternatively, you can delete an index by its name, ie: + $table->removeIndexByName('idx_users_email') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + + +Working With Foreign Keys +------------------------- + +Migrations has support for creating foreign key constraints on your database tables. +Let's add a foreign key to an example table: + +.. code-block:: php + + table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer', ['null' => true]) + ->addForeignKey('tag_id', 'tags', 'id', ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION']) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +"On delete" and "On update" actions are defined with a 'delete' and 'update' options array. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and 'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable with the option ``['null' => true]``. +Constraint name can be changed with the 'constraint' option. + +It is also possible to pass ``addForeignKey()`` an array of columns. +This allows us to establish a foreign key relationship to a table which uses a combined key. + +.. code-block:: php + + table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey(['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + ['delete'=> 'NO_ACTION', 'update'=> 'NO_ACTION', 'constraint' => 'user_follower_id']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can add named foreign keys using the ``constraint`` parameter. + +.. code-block:: php + + table('your_table'); + $table->addForeignKey('foreign_id', 'reference_table', ['id'], + ['constraint' => 'your_foreign_key_name']); + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can also easily check if a foreign key exists: + +.. code-block:: php + + table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Finally, to delete a foreign key, use the ``dropForeignKey`` method. + +Note that like other methods in the ``Table`` class, ``dropForeignKey`` also needs ``save()`` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved. + +.. code-block:: php + + table('tag_relationships'); + $table->dropForeignKey('tag_id')->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + + + +Using the Query Builder +----------------------- + +It is not uncommon to pair database structure changes with data changes. For example, you may want to +migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, +Migrations provides access to a Query builder object, that you may use to execute complex ``SELECT``, ``UPDATE``, +``INSERT`` or ``DELETE`` statements. + +The Query builder is provided by the `cakephp/database `_ project, and should +be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the +``getQueryBuilder(string $type)`` function. The ``string $type`` options are `'select'`, `'insert'`, `'update'` and `'delete'`: + + +.. code-block:: php + + getQueryBuilder('select'); + $statement = $builder->select('*')->from('users')->execute(); + var_dump($statement->fetchAll()); + } + } + +Selecting Fields +~~~~~~~~~~~~~~~~ + +Adding fields to the SELECT clause: + + +.. code-block:: php + + select(['id', 'title', 'body']); + + // Results in SELECT id AS pk, title AS aliased_title, body ... + $builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + + // Use a closure + $builder->select(function ($builder) { + return ['id', 'title', 'body']; + }); + + +Where Conditions +~~~~~~~~~~~~~~~~ + +Generating conditions: + +.. code-block:: php + + // WHERE id = 1 + $builder->where(['id' => 1]); + + // WHERE id > 1 + $builder->where(['id >' => 1]); + + +As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well: + + +.. code-block:: php + + where(['id >' => 1])->andWhere(['title' => 'My Title']); + + // Equivalent to + $builder->where(['id >' => 1, 'title' => 'My title']); + + // WHERE id > 1 OR title = 'My title' + $builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); + + +For even more complex conditions you can use closures and expression objects: + +.. code-block:: php + + select('*') + ->from('articles') + ->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); + + +Which results in: + +.. code-block:: sql + + SELECT * FROM articles + WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 + + +Combining expressions is also possible: + + +.. code-block:: php + + select('*') + ->from('articles') + ->where(function ($exp) { + $orConditions = $exp->or_(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); + +It generates: + +.. code-block:: sql + + SELECT * + FROM articles + WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 + + +When using the expression objects you can use the following methods to create conditions: + +* ``eq()`` Creates an equality condition. +* ``notEq()`` Create an inequality condition +* ``like()`` Create a condition using the ``LIKE`` operator. +* ``notLike()`` Create a negated ``LIKE`` condition. +* ``in()`` Create a condition using ``IN``. +* ``notIn()`` Create a negated condition using ``IN``. +* ``gt()`` Create a ``>`` condition. +* ``gte()`` Create a ``>=`` condition. +* ``lt()`` Create a ``<`` condition. +* ``lte()`` Create a ``<=`` condition. +* ``isNull()`` Create an ``IS NULL`` condition. +* ``isNotNull()`` Create a negated ``IS NULL`` condition. + + +Aggregates and SQL Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +.. code-block:: php + + select(['count' => $builder->func()->count('*')]); + +A number of commonly used functions can be created with the func() method: + +* ``sum()`` Calculate a sum. The arguments will be treated as literal values. +* ``avg()`` Calculate an average. The arguments will be treated as literal values. +* ``min()`` Calculate the min of a column. The arguments will be treated as literal values. +* ``max()`` Calculate the max of a column. The arguments will be treated as literal values. +* ``count()`` Calculate the count. The arguments will be treated as literal values. +* ``concat()`` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +* ``coalesce()`` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +* ``dateDiff()`` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +* ``now()`` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, +literal arguments and bound parameters. Literal parameters allow you to reference columns or +other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: + + +.. code-block:: php + + func()->concat([ + 'title' => 'literal', + ' NEW' + ]); + $query->select(['title' => $concat]); + + +Getting Results out of a Query +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + + +.. code-block:: php + + execute()->fetchAll('assoc'); + + +Creating an Insert Query +~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating insert queries is also possible: + + +.. code-block:: php + + getQueryBuilder('insert'); + $builder + ->insert(['first_name', 'last_name']) + ->into('users') + ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) + ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) + ->execute(); + + +For increased performance, you can use another builder object as the values for an insert query: + +.. code-block:: php + + getQueryBuilder('select'); + $namesQuery + ->select(['fname', 'lname']) + ->from('users') + ->where(['is_active' => true]); + + $builder = $this->getQueryBuilder('insert'); + $st = $builder + ->insert(['first_name', 'last_name']) + ->into('names') + ->values($namesQuery) + ->execute(); + + var_dump($st->lastInsertId('names', 'id')); + + +The above code will generate: + +.. code-block:: sql + + INSERT INTO names (first_name, last_name) + (SELECT fname, lname FROM USERS where is_active = 1) + + +Creating an update Query +~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating update queries is similar to both inserting and selecting: + +.. code-block:: php + + getQueryBuilder('update'); + $builder + ->update('users') + ->set('fname', 'Snow') + ->where(['fname' => 'Jon']) + ->execute(); + + +Creating a Delete Query +~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, delete queries: + +.. code-block:: php + + getQueryBuilder('delete'); + $builder + ->delete('users') + ->where(['accepted_gdpr' => false]) + ->execute(); diff --git a/templates/bake/Seed/seed.twig b/templates/bake/Seed/seed.twig index 8ba8f122..d6d691e1 100644 --- a/templates/bake/Seed/seed.twig +++ b/templates/bake/Seed/seed.twig @@ -38,7 +38,11 @@ class {{ name }}Seed extends AbstractSeed * Write your database seeder using this method. * * More information on writing seeds is available here: +{% if backend == "builtin" %} + * https://book.cakephp.org/migrations/4/en/seeding.html +{% else %} * https://book.cakephp.org/phinx/0/en/seeding.html +{% endif %} * * @return void */ diff --git a/templates/bake/config/skeleton.twig b/templates/bake/config/skeleton.twig index 0c9203af..5e2cbe5f 100644 --- a/templates/bake/config/skeleton.twig +++ b/templates/bake/config/skeleton.twig @@ -38,7 +38,11 @@ class {{ name }} extends AbstractMigration * Change Method. * * More information on this method is available here: +{% if backend == "builtin" %} + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method +{% else %} * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method +{% endif %} * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testCreate.php b/tests/comparisons/Migration/testCreate.php index 4f84045e..98225f72 100644 --- a/tests/comparisons/Migration/testCreate.php +++ b/tests/comparisons/Migration/testCreate.php @@ -9,7 +9,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testCreateDatetime.php b/tests/comparisons/Migration/testCreateDatetime.php index 9d744831..2846aecf 100644 --- a/tests/comparisons/Migration/testCreateDatetime.php +++ b/tests/comparisons/Migration/testCreateDatetime.php @@ -9,7 +9,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testCreateDropMigration.php b/tests/comparisons/Migration/testCreateDropMigration.php index c149488b..a7cf2868 100644 --- a/tests/comparisons/Migration/testCreateDropMigration.php +++ b/tests/comparisons/Migration/testCreateDropMigration.php @@ -9,7 +9,7 @@ class DropUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testCreateFieldLength.php b/tests/comparisons/Migration/testCreateFieldLength.php index bdec0922..38a6131f 100644 --- a/tests/comparisons/Migration/testCreateFieldLength.php +++ b/tests/comparisons/Migration/testCreateFieldLength.php @@ -9,7 +9,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testCreatePrimaryKey.php b/tests/comparisons/Migration/testCreatePrimaryKey.php index dbb6ab39..cec07a2c 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKey.php +++ b/tests/comparisons/Migration/testCreatePrimaryKey.php @@ -11,7 +11,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php index 3362f65f..8c7c65d7 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php +++ b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php @@ -11,7 +11,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Migration/testNoContents.php b/tests/comparisons/Migration/testNoContents.php index 32be4df5..dd4b84ce 100644 --- a/tests/comparisons/Migration/testNoContents.php +++ b/tests/comparisons/Migration/testNoContents.php @@ -9,7 +9,7 @@ class NoContents extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Seeds/pgsql/testWithData.php b/tests/comparisons/Seeds/pgsql/testWithData.php index 5cba5c9f..63047b5b 100644 --- a/tests/comparisons/Seeds/pgsql/testWithData.php +++ b/tests/comparisons/Seeds/pgsql/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php b/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php index 75600224..f5346af8 100644 --- a/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/php81/testWithData.php b/tests/comparisons/Seeds/php81/testWithData.php index 5cba5c9f..63047b5b 100644 --- a/tests/comparisons/Seeds/php81/testWithData.php +++ b/tests/comparisons/Seeds/php81/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/php81/testWithDataAndLimit.php b/tests/comparisons/Seeds/php81/testWithDataAndLimit.php index 75600224..f5346af8 100644 --- a/tests/comparisons/Seeds/php81/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/php81/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/sqlserver/testWithData.php b/tests/comparisons/Seeds/sqlserver/testWithData.php index f65f960c..ff9d221b 100644 --- a/tests/comparisons/Seeds/sqlserver/testWithData.php +++ b/tests/comparisons/Seeds/sqlserver/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php b/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php index 1dd7c604..a5c8fec7 100644 --- a/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testBasicBaking.php b/tests/comparisons/Seeds/testBasicBaking.php index d72af831..086dd451 100644 --- a/tests/comparisons/Seeds/testBasicBaking.php +++ b/tests/comparisons/Seeds/testBasicBaking.php @@ -14,7 +14,7 @@ class ArticlesSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testPrettifyArray.php b/tests/comparisons/Seeds/testPrettifyArray.php index 51eacbea..5f48bac7 100644 --- a/tests/comparisons/Seeds/testPrettifyArray.php +++ b/tests/comparisons/Seeds/testPrettifyArray.php @@ -14,7 +14,7 @@ class TextsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testWithDataAndFields.php b/tests/comparisons/Seeds/testWithDataAndFields.php index 48bc5d6b..daef7fbd 100644 --- a/tests/comparisons/Seeds/testWithDataAndFields.php +++ b/tests/comparisons/Seeds/testWithDataAndFields.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/phinx/0/en/seeding.html + * https://book.cakephp.org/migrations/4/en/seeding.html * * @return void */ From 6865664d7143b3ee70a28714b80cd850030d48f0 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 6 Nov 2024 23:15:24 -0500 Subject: [PATCH 35/47] Made the migrationdiff command tests runnable locally Leaving tables behind isn't very nice. --- .../Command/BakeMigrationDiffCommandTest.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 3d7205d5..d959c91a 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -13,6 +13,7 @@ */ namespace Migrations\Test\TestCase\Command; +use Cake\Cache\Cache; use Cake\Console\BaseCommand; use Cake\Core\Configure; use Cake\Core\Plugin; @@ -55,6 +56,15 @@ public function tearDown(): void unlink($file); } } + if (env('DB_URL_COMPARE')) { + // Clean up the comparison database each time. Table order is important. + $connection = ConnectionManager::get('test_comparisons'); + $tables = ['articles', 'categories', 'comments', 'users', 'phinxlog']; + foreach ($tables as $table) { + $connection->execute("DROP TABLE IF EXISTS $table"); + } + Cache::clear('_cake_model_'); + } } /** @@ -206,7 +216,8 @@ protected function runDiffBakingTest(string $scenario): void $destinationDumpPath, ]; - $this->getMigrations("MigrationsDiff$scenario")->migrate(); + $migrations = $this->getMigrations("MigrationsDiff$scenario"); + $migrations->migrate(); unlink($destination); copy($diffDumpPath, $destinationDumpPath); From 716199e824a26c0ac6bc3b412dd8f115af8f2918 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 6 Nov 2024 23:36:40 -0500 Subject: [PATCH 36/47] Update templates and snapshots for bake migration_diff --- src/Command/BakeMigrationDiffCommand.php | 2 ++ templates/bake/config/diff.twig | 10 ++++++++++ .../Diff/addRemove/the_diff_add_remove_mysql.php | 6 +++--- .../Diff/default/the_diff_default_mysql.php | 6 +++--- .../comparisons/Diff/simple/the_diff_simple_mysql.php | 6 +++--- ...th_auto_id_compatible_signed_primary_keys_mysql.php | 6 +++--- ..._auto_id_incompatible_signed_primary_keys_mysql.php | 6 +++--- ...uto_id_incompatible_unsigned_primary_keys_mysql.php | 6 +++--- 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index 5ee75239..c4083f5a 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -18,6 +18,7 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Core\Configure; use Cake\Database\Connection; use Cake\Database\Schema\CollectionInterface; use Cake\Database\Schema\TableSchema; @@ -199,6 +200,7 @@ public function templateData(Arguments $arguments): array 'data' => $this->templateData, 'dumpSchema' => $this->dumpSchema, 'currentSchema' => $this->currentSchema, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/templates/bake/config/diff.twig b/templates/bake/config/diff.twig index 21b56a22..d73ae5a1 100644 --- a/templates/bake/config/diff.twig +++ b/templates/bake/config/diff.twig @@ -20,9 +20,15 @@ Date: Wed, 6 Nov 2024 23:43:57 -0500 Subject: [PATCH 37/47] Leave some todos for the next time. --- src/Command/BakeMigrationDiffCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index c4083f5a..1f076f0b 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -493,6 +493,7 @@ protected function bakeSnapshot(string $name, Arguments $args, ConsoleIo $io): ? $newArgs = array_merge($newArgs, $this->parseOptions($args)); + // TODO(mark) This nested command call always uses phinx backend. $exitCode = $this->executeCommand(BakeMigrationSnapshotCommand::class, $newArgs, $io); if ($exitCode === 1) { @@ -521,6 +522,7 @@ protected function getDumpSchema(Arguments $args): array $inputArgs['--plugin'] = $args->getOption('plugin'); } + // TODO(mark) This has to change for the built-in backend $className = Dump::class; $definition = (new $className())->getDefinition(); From 0336f34f48adc9f2bf7ae0dae85eb0ea7115815d Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 13 Nov 2024 23:56:30 -0500 Subject: [PATCH 38/47] Add support for the builtin backend to migration_snapshot Enable builtin backend tests for migration_diff as well --- src/Command/SnapshotTrait.php | 22 +++++++++++++++++-- src/Migration/Manager.php | 1 + src/View/Helper/MigrationHelper.php | 2 +- .../Command/BakeMigrationDiffCommandTest.php | 1 + .../BakeMigrationSnapshotCommandTest.php | 1 + 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Command/SnapshotTrait.php b/src/Command/SnapshotTrait.php index 2c09fea8..67332617 100644 --- a/src/Command/SnapshotTrait.php +++ b/src/Command/SnapshotTrait.php @@ -15,6 +15,7 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; +use Cake\Core\Configure; /** * Trait needed for all "snapshot" type of bake operations. @@ -40,6 +41,15 @@ protected function createFile(string $path, string $contents, Arguments $args, C return $createFile; } + /** + * @internal + * @return bool Whether or not the builtin backend is active. + */ + protected function useBuiltinBackend(): bool + { + return Configure::read('Migrations.backend', 'builtin') === 'builtin'; + } + /** * Will mark a snapshot created, the snapshot being identified by its * full file path. @@ -62,7 +72,11 @@ protected function markSnapshotApplied(string $path, Arguments $args, ConsoleIo $newArgs = array_merge($newArgs, $this->parseOptions($args)); $io->out('Marking the migration ' . $fileName . ' as migrated...'); - $this->executeCommand(MigrationsMarkMigratedCommand::class, $newArgs, $io); + if ($this->useBuiltinBackend()) { + $this->executeCommand(MarkMigratedCommand::class, $newArgs, $io); + } else { + $this->executeCommand(MigrationsMarkMigratedCommand::class, $newArgs, $io); + } } /** @@ -78,7 +92,11 @@ protected function refreshDump(Arguments $args, ConsoleIo $io): void $newArgs = $this->parseOptions($args); $io->out('Creating a dump of the new database state...'); - $this->executeCommand(MigrationsDumpCommand::class, $newArgs, $io); + if ($this->useBuiltinBackend()) { + $this->executeCommand(DumpCommand::class, $newArgs, $io); + } else { + $this->executeCommand(MigrationsDumpCommand::class, $newArgs, $io); + } } /** diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index dbdb159c..7a74c4d7 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -313,6 +313,7 @@ public function getVersionsToMark(Arguments $args): array $versionArg = $args->getArgument('version'); } $targetArg = $args->getOption('target'); + // This is where version and target params are parsed. $hasAllVersion = in_array($versionArg, ['all', '*'], true); if ((empty($versionArg) && empty($targetArg)) || $hasAllVersion) { return $versions; diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index b4d7745c..adc84d94 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -414,7 +414,7 @@ public function getColumnOption(array $options): array } // TODO this can be cleaned up when we stop using phinx data structures for column definitions - if ($columnOptions['precision'] === null) { + if (!isset($columnOptions['precision']) || $columnOptions['precision'] === null) { unset($columnOptions['precision']); } else { // due to Phinx using different naming for the precision and scale to CakePHP diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index d959c91a..a7362740 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -46,6 +46,7 @@ public function setUp(): void parent::setUp(); $this->generatedFiles = []; + Configure::write('Migrations.backend', 'builtin'); } public function tearDown(): void diff --git a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php index 4960d542..238e3c37 100644 --- a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php @@ -61,6 +61,7 @@ public function setUp(): void $this->migrationPath = ROOT . DS . 'config' . DS . 'Migrations' . DS; $this->generatedFiles = []; + Configure::write('Migrations.backend', 'builtin'); } /** From 5e6d8bb2cc3c92b6a35973f9aaa5ca59e524e107 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 14 Nov 2024 00:09:05 -0500 Subject: [PATCH 39/47] Get all migration_snapshot tests passing locally --- src/Command/BakeMigrationSnapshotCommand.php | 1 + templates/bake/config/snapshot.twig | 6 ++++++ .../Migration/test_snapshot_auto_id_disabled.php | 4 ++-- tests/comparisons/Migration/test_snapshot_not_empty.php | 4 ++-- tests/comparisons/Migration/test_snapshot_plugin_blog.php | 4 ++-- ...snapshot_with_auto_id_compatible_signed_primary_keys.php | 4 ++-- ...apshot_with_auto_id_incompatible_signed_primary_keys.php | 4 ++-- ...shot_with_auto_id_incompatible_unsigned_primary_keys.php | 4 ++-- .../Migration/test_snapshot_with_non_default_collation.php | 4 ++-- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Command/BakeMigrationSnapshotCommand.php b/src/Command/BakeMigrationSnapshotCommand.php index ea711069..bc51d3a8 100644 --- a/src/Command/BakeMigrationSnapshotCommand.php +++ b/src/Command/BakeMigrationSnapshotCommand.php @@ -115,6 +115,7 @@ public function templateData(Arguments $arguments): array 'action' => 'create_table', 'name' => $this->_name, 'autoId' => $autoId, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/templates/bake/config/snapshot.twig b/templates/bake/config/snapshot.twig index df2e9967..66666aa6 100644 --- a/templates/bake/config/snapshot.twig +++ b/templates/bake/config/snapshot.twig @@ -22,9 +22,15 @@ Date: Thu, 14 Nov 2024 00:12:15 -0500 Subject: [PATCH 40/47] Update snapshots and cleanup - Update postgres, sqlserver and sqlite fixtures - Fix phpstan - Update todos and doc blocks --- src/Db/Table.php | 5 ----- src/Db/Table/Table.php | 2 +- src/Migration/Manager.php | 1 - src/View/Helper/MigrationHelper.php | 2 +- .../Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php | 4 ++-- .../Migration/pgsql/test_snapshot_not_empty_pgsql.php | 4 ++-- .../Migration/pgsql/test_snapshot_plugin_blog_pgsql.php | 4 ++-- .../sqlite/test_snapshot_auto_id_disabled_sqlite.php | 4 ++-- .../Migration/sqlite/test_snapshot_not_empty_sqlite.php | 4 ++-- .../Migration/sqlite/test_snapshot_plugin_blog_sqlite.php | 4 ++-- .../sqlserver/test_snapshot_auto_id_disabled_sqlserver.php | 4 ++-- .../sqlserver/test_snapshot_not_empty_sqlserver.php | 4 ++-- .../sqlserver/test_snapshot_plugin_blog_sqlserver.php | 4 ++-- 13 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Db/Table.php b/src/Db/Table.php index aff92485..a2a63d75 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -34,11 +34,6 @@ /** * This object is based loosely on: https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html. - * - * TODO(mark) Having both Migrations\Db\Table and Migrations\Db\Table\Table seems redundant. - * The table models should be joined together so that we have a simpler API exposed. - * - * @internal */ class Table { diff --git a/src/Db/Table/Table.php b/src/Db/Table/Table.php index 70f270f6..cf2c16e7 100644 --- a/src/Db/Table/Table.php +++ b/src/Db/Table/Table.php @@ -12,7 +12,7 @@ /** * @internal - * @TODO rename this to `TableMetadata` having two classes with very similar names is confusing for me. + * @TODO rename this to `TableMetadata` having two classes with very similar names is confusing. */ class Table { diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 7a74c4d7..dbdb159c 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -313,7 +313,6 @@ public function getVersionsToMark(Arguments $args): array $versionArg = $args->getArgument('version'); } $targetArg = $args->getOption('target'); - // This is where version and target params are parsed. $hasAllVersion = in_array($versionArg, ['all', '*'], true); if ((empty($versionArg) && empty($targetArg)) || $hasAllVersion) { return $versions; diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index adc84d94..143d4fbb 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -414,7 +414,7 @@ public function getColumnOption(array $options): array } // TODO this can be cleaned up when we stop using phinx data structures for column definitions - if (!isset($columnOptions['precision']) || $columnOptions['precision'] === null) { + if (!isset($columnOptions['precision']) || $columnOptions['precision'] == null) { unset($columnOptions['precision']); } else { // due to Phinx using different naming for the precision and scale to CakePHP diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php index 7a7ed11a..9a6d5dc2 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php @@ -1,9 +1,9 @@ Date: Thu, 14 Nov 2024 12:24:58 -0500 Subject: [PATCH 41/47] Extend class hacking for new base class --- tests/TestCase/MigrationsTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php index 0d3a8875..9120d182 100644 --- a/tests/TestCase/MigrationsTest.php +++ b/tests/TestCase/MigrationsTest.php @@ -1092,8 +1092,10 @@ protected function runMigrateSnapshots(string $basePath, string $filename, array // change class name to avoid conflict with other classes // to avoid 'Fatal error: Cannot declare class Test...., because the name is already in use' $content = file_get_contents($destination . $copiedFileName); - $pattern = ' extends AbstractMigration'; - $content = str_replace($pattern, 'NewSuffix' . $pattern, $content); + $patterns = [' extends AbstractMigration', ' extends BaseMigration']; + foreach ($patterns as $pattern) { + $content = str_replace($pattern, 'NewSuffix' . $pattern, $content); + } file_put_contents($destination . $copiedFileName, $content); $migrations = new Migrations([ From 327c371868c87153c85d953aaf7029a5ca19bcbf Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 14 Nov 2024 12:33:46 -0500 Subject: [PATCH 42/47] Chill phpstan --- src/Util/ColumnParser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php index 9c6c7d83..707ebd4e 100644 --- a/src/Util/ColumnParser.php +++ b/src/Util/ColumnParser.php @@ -42,7 +42,7 @@ class ColumnParser /** * Parses a list of arguments into an array of fields * - * @param array $arguments A list of arguments being parsed + * @param array $arguments A list of arguments being parsed * @return array */ public function parseFields(array $arguments): array @@ -95,7 +95,7 @@ public function parseFields(array $arguments): array /** * Parses a list of arguments into an array of indexes * - * @param array $arguments A list of arguments being parsed + * @param array $arguments A list of arguments being parsed * @return array */ public function parseIndexes(array $arguments): array @@ -144,7 +144,7 @@ public function parseIndexes(array $arguments): array * Parses a list of arguments into an array of fields composing the primary key * of the table * - * @param array $arguments A list of arguments being parsed + * @param array $arguments A list of arguments being parsed * @return array */ public function parsePrimaryKey(array $arguments): array From ca8e1001267a4d8ef323bf9be915bc586f07b352 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 14 Nov 2024 14:25:00 -0500 Subject: [PATCH 43/47] Change variable names to make types simpler. --- src/Command/BakeMigrationCommand.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index 2e6fad8c..c669bfd4 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -76,12 +76,13 @@ public function templateData(Arguments $arguments): array $pluginPath = $this->plugin . '.'; } - $arguments = $arguments->getArguments(); - unset($arguments[0]); + /** @var array $args */ + $args = $arguments->getArguments(); + unset($args[0]); $columnParser = new ColumnParser(); - $fields = $columnParser->parseFields($arguments); - $indexes = $columnParser->parseIndexes($arguments); - $primaryKey = $columnParser->parsePrimaryKey($arguments); + $fields = $columnParser->parseFields($args); + $indexes = $columnParser->parseIndexes($args); + $primaryKey = $columnParser->parsePrimaryKey($args); $action = $this->detectAction($className); From ffe8a417c8d1a2f8b671efac0392d24b5d23834e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 14 Nov 2024 22:35:22 -0500 Subject: [PATCH 44/47] Allow a deprecated method --- psalm-baseline.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index a12664b1..58309755 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -83,6 +83,11 @@ + + + + + From 19144ebb23e66a2d28d9c7396a6f36f11b67c128 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 22 Nov 2024 22:57:14 -0500 Subject: [PATCH 45/47] Fix timer output being included at non-verbose output levels --- src/Db/Adapter/TimedOutputAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php index 300bb80a..52062ab7 100644 --- a/src/Db/Adapter/TimedOutputAdapter.php +++ b/src/Db/Adapter/TimedOutputAdapter.php @@ -39,7 +39,7 @@ public function startCommandTimer(): callable return function () use ($started): void { $end = microtime(true); - $this->getIo()?->out(' -> ' . sprintf('%.4fs', $end - $started)); + $this->getIo()?->verbose(' -> ' . sprintf('%.4fs', $end - $started)); }; } From 5500338169f4231708baf5d3846cb5fc671bd224 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 23 Nov 2024 00:00:44 -0500 Subject: [PATCH 46/47] Fix baseline --- phpstan-baseline.neon | 54 ++++++++++++------------------------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f5dae26c..362adcc8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,24 +6,6 @@ parameters: count: 1 path: src/Command/BakeMigrationCommand.php - - - message: '#^Parameter \#1 \$arguments of method Migrations\\Util\\ColumnParser\:\:parseFields\(\) expects array\, array\\|int\<1, max\>, list\\|string\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/BakeMigrationCommand.php - - - - message: '#^Parameter \#1 \$arguments of method Migrations\\Util\\ColumnParser\:\:parseIndexes\(\) expects array\, array\\|int\<1, max\>, list\\|string\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/BakeMigrationCommand.php - - - - message: '#^Parameter \#1 \$arguments of method Migrations\\Util\\ColumnParser\:\:parsePrimaryKey\(\) expects array\, array\\|int\<1, max\>, list\\|string\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/BakeMigrationCommand.php - - message: '#^Call to an undefined method object\:\:loadHelper\(\)\.$#' identifier: method.notFound @@ -60,6 +42,18 @@ parameters: count: 1 path: src/Command/Phinx/CacheClear.php + - + message: '#^Instanceof between Migrations\\CakeManager and Migrations\\CakeManager will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/Command/Phinx/MarkMigrated.php + + - + message: '#^Instanceof between Migrations\\CakeManager and Migrations\\CakeManager will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/Command/Phinx/Status.php + - message: '#^Unsafe usage of new static\(\)\.$#' identifier: new.static @@ -115,10 +109,10 @@ parameters: path: src/Db/Adapter/SqlserverAdapter.php - - message: '#^Parameter \#1 \$message of method Cake\\Console\\ConsoleIo\:\:verbose\(\) expects list\\|string, array\ given\.$#' - identifier: argument.type + message: '#^Call to function method_exists\(\) with Migrations\\MigrationInterface and ''useTransactions'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/Migration/Manager.php + path: src/Migration/Environment.php - message: '#^Parameter \#1 \.\.\.\$arg1 of function max expects non\-empty\-array, array given\.$#' @@ -138,24 +132,6 @@ parameters: count: 1 path: src/Shim/OutputAdapter.php - - - message: '#^Parameter \#1 \$message of method Cake\\Console\\ConsoleIo\:\:out\(\) expects list\\|string, array\|string given\.$#' - identifier: argument.type - count: 2 - path: src/Shim/OutputAdapter.php - - - - message: '#^Parameter \#2 \$tables of method Cake\\TestSuite\\ConnectionHelper\:\:dropTables\(\) expects list\\|null, non\-empty\-array\ given\.$#' - identifier: argument.type - count: 1 - path: src/TestSuite/Migrator.php - - - - message: '#^Parameter \#2 \$tables of method Cake\\TestSuite\\ConnectionHelper\:\:truncateTables\(\) expects list\\|null, non\-empty\-array\ given\.$#' - identifier: argument.type - count: 2 - path: src/TestSuite/Migrator.php - - message: '#^Offset 0 on non\-empty\-list\ in isset\(\) always exists and is not nullable\.$#' identifier: isset.offset From e2336ae2c8f6e8aad0e4118193f7fa0c89106bc1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 23 Nov 2024 12:41:37 -0500 Subject: [PATCH 47/47] Update psalm baseline --- psalm-baseline.xml | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5b7121ab..850c9680 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -59,10 +59,6 @@ - - - - @@ -86,23 +82,6 @@ - - - - - getManager()->maxNameLength]]> - - - - - - - - - - - - @@ -171,12 +150,6 @@ - {$phpFile}"; - }, - $phpFiles - )]]> @@ -218,10 +191,6 @@ - - - - io->level()]]> @@ -229,13 +198,6 @@ - - - - - - - regexpParseColumn]]>