From 39d329b2c553df824b81e3eb98aa91dd0bc6fd3e Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 14:41:26 -0500 Subject: [PATCH 01/10] feat(Table): implement automatic saving of columns on destruction --- src/Database/Migrations/Table.php | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index cf3024c3..26411354 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -31,11 +31,47 @@ class Table extends PhinxTable */ protected array $columns = []; + protected bool $executed = false; + + public function __destruct() + { + if (!$this->executed) { + $this->save(); + } + } + public function getColumnBuilders(): array { return $this->columns; } + public function create(): void + { + $this->addColumnFromBuilders(); + + parent::create(); + + $this->executed = true; + } + + public function update(): void + { + $this->addColumnFromBuilders(); + + parent::update(); + + $this->executed = true; + } + + public function save(): void + { + $this->addColumnFromBuilders(); + + parent::save(); + + $this->executed = true; + } + /** * @template T of Column * @param T $column @@ -49,4 +85,11 @@ protected function addColumnWithAdapter(Column $column): Column return $column; } + + protected function addColumnFromBuilders(): void + { + foreach ($this->columns as $column) { + $this->addColumn($column->getName(), $column->getType(), $column->getOptions()); + } + } } From 970bb3444403135acb101db2f5b68493ce0c5a3a Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 14:46:03 -0500 Subject: [PATCH 02/10] style: php cs --- src/Database/Migrations/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 26411354..b8573f87 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -35,7 +35,7 @@ class Table extends PhinxTable public function __destruct() { - if (!$this->executed) { + if (! $this->executed) { $this->save(); } } From 7dd68486019e17bff75e2d0b6db3960de77c3441 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 16:53:54 -0500 Subject: [PATCH 03/10] fix(WithConvenience): update id method to use UnsignedBigInteger type --- .../Migrations/Columns/Concerns/WithConvenience.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Migrations/Columns/Concerns/WithConvenience.php b/src/Database/Migrations/Columns/Concerns/WithConvenience.php index c76ed3da..88a0d3c5 100644 --- a/src/Database/Migrations/Columns/Concerns/WithConvenience.php +++ b/src/Database/Migrations/Columns/Concerns/WithConvenience.php @@ -5,13 +5,13 @@ namespace Phenix\Database\Migrations\Columns\Concerns; use Phenix\Database\Migrations\Columns\Timestamp; -use Phenix\Database\Migrations\Columns\UnsignedInteger; +use Phenix\Database\Migrations\Columns\UnsignedBigInteger; trait WithConvenience { - public function id(string $name = 'id'): UnsignedInteger + public function id(string $name = 'id'): UnsignedBigInteger { - return $this->addColumnWithAdapter(new UnsignedInteger($name, null, true)); + return $this->addColumnWithAdapter(new UnsignedBigInteger($name, true)); } public function timestamps(bool $timezone = false): self From 65b8b569288b0e8ac704ab13bdbdb17efc957bbe Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 16:55:46 -0500 Subject: [PATCH 04/10] feat: add foreign key column --- .../Columns/Concerns/WithForeignKeys.php | 24 +++ src/Database/Migrations/ForeignKey.php | 123 ++++++++++++++ src/Database/Migrations/Table.php | 35 ++++ .../Database/Migrations/ForeignKeyTest.php | 157 ++++++++++++++++++ tests/Unit/Database/Migrations/TableTest.php | 37 +++++ 5 files changed, 376 insertions(+) create mode 100644 src/Database/Migrations/Columns/Concerns/WithForeignKeys.php create mode 100644 src/Database/Migrations/ForeignKey.php create mode 100644 tests/Unit/Database/Migrations/ForeignKeyTest.php diff --git a/src/Database/Migrations/Columns/Concerns/WithForeignKeys.php b/src/Database/Migrations/Columns/Concerns/WithForeignKeys.php new file mode 100644 index 00000000..afd4bec1 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithForeignKeys.php @@ -0,0 +1,24 @@ +addForeignKeyWithAdapter(new ForeignKey($columns, $referencedTable, $referencedColumns, $options)); + } + + public function foreign(string|array $columns): ForeignKey + { + return $this->addForeignKeyWithAdapter(new ForeignKey($columns, '', 'id')); + } +} diff --git a/src/Database/Migrations/ForeignKey.php b/src/Database/Migrations/ForeignKey.php new file mode 100644 index 00000000..91ccc201 --- /dev/null +++ b/src/Database/Migrations/ForeignKey.php @@ -0,0 +1,123 @@ +options = $options; + } + + public function getColumns(): string|array + { + return $this->columns; + } + + public function getReferencedTable(): string + { + return $this->referencedTable; + } + + public function getReferencedColumns(): string|array + { + return $this->referencedColumns; + } + + public function getOptions(): array + { + return $this->options; + } + + public function onDelete(string $action): static + { + $this->options['delete'] = $action; + + return $this; + } + + public function onUpdate(string $action): static + { + $this->options['update'] = $action; + + return $this; + } + + public function constraint(string $name): static + { + $this->options['constraint'] = $name; + + return $this; + } + + public function deferrable(string $deferrable = 'DEFERRED'): static + { + if ($this->isPostgres()) { + $this->options['deferrable'] = $deferrable; + } + + return $this; + } + + public function references(string|array $columns): static + { + $this->referencedColumns = $columns; + + return $this; + } + + public function on(string $table): static + { + $this->referencedTable = $table; + + return $this; + } + + public function setAdapter(AdapterInterface $adapter): static + { + $this->adapter = $adapter; + + return $this; + } + + public function getAdapter(): ?AdapterInterface + { + return $this->adapter; + } + + public function isMysql(): bool + { + return $this->adapter instanceof MysqlAdapter; + } + + public function isPostgres(): bool + { + return $this->adapter instanceof PostgresAdapter; + } + + public function isSQLite(): bool + { + return $this->adapter instanceof SQLiteAdapter; + } + + public function isSqlServer(): bool + { + return $this->adapter instanceof SqlServerAdapter; + } +} diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index b8573f87..5bdf579e 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -8,6 +8,7 @@ use Phenix\Database\Migrations\Columns\Concerns\WithBinary; use Phenix\Database\Migrations\Columns\Concerns\WithConvenience; use Phenix\Database\Migrations\Columns\Concerns\WithDateTime; +use Phenix\Database\Migrations\Columns\Concerns\WithForeignKeys; use Phenix\Database\Migrations\Columns\Concerns\WithJson; use Phenix\Database\Migrations\Columns\Concerns\WithNetwork; use Phenix\Database\Migrations\Columns\Concerns\WithNumeric; @@ -20,6 +21,7 @@ class Table extends PhinxTable use WithBinary; use WithConvenience; use WithDateTime; + use WithForeignKeys; use WithJson; use WithNetwork; use WithNumeric; @@ -31,6 +33,11 @@ class Table extends PhinxTable */ protected array $columns = []; + /** + * @var array + */ + protected array $foreignKeys = []; + protected bool $executed = false; public function __destruct() @@ -45,6 +52,11 @@ public function getColumnBuilders(): array return $this->columns; } + public function getForeignKeyBuilders(): array + { + return $this->foreignKeys; + } + public function create(): void { $this->addColumnFromBuilders(); @@ -86,10 +98,33 @@ protected function addColumnWithAdapter(Column $column): Column return $column; } + /** + * @template T of ForeignKey + * @param T $foreignKey + * @return T + */ + protected function addForeignKeyWithAdapter(ForeignKey $foreignKey): ForeignKey + { + $foreignKey->setAdapter($this->getAdapter()); + + $this->foreignKeys[] = $foreignKey; + + return $foreignKey; + } + protected function addColumnFromBuilders(): void { foreach ($this->columns as $column) { $this->addColumn($column->getName(), $column->getType(), $column->getOptions()); } + + foreach ($this->foreignKeys as $foreignKey) { + $this->addForeignKey( + $foreignKey->getColumns(), + $foreignKey->getReferencedTable(), + $foreignKey->getReferencedColumns(), + $foreignKey->getOptions() + ); + } } } diff --git a/tests/Unit/Database/Migrations/ForeignKeyTest.php b/tests/Unit/Database/Migrations/ForeignKeyTest.php new file mode 100644 index 00000000..3436e506 --- /dev/null +++ b/tests/Unit/Database/Migrations/ForeignKeyTest.php @@ -0,0 +1,157 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockAdapter->expects($this->any()) + ->method('hasTable') + ->willReturn(false); + + $this->mockAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $this->mockAdapter->expects($this->any()) + ->method('execute') + ->willReturnCallback(function ($sql) { + return true; + }); +}); + +it('can create a simple foreign key', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + + expect($foreignKey->getColumns())->toEqual('user_id'); + expect($foreignKey->getReferencedTable())->toEqual('users'); + expect($foreignKey->getReferencedColumns())->toEqual('id'); + expect($foreignKey->getOptions())->toEqual([]); +}); + +it('can create a foreign key with multiple columns', function (): void { + $foreignKey = new ForeignKey(['user_id', 'role_id'], 'user_roles', ['user_id', 'role_id']); + + expect($foreignKey->getColumns())->toEqual(['user_id', 'role_id']); + expect($foreignKey->getReferencedTable())->toEqual('user_roles'); + expect($foreignKey->getReferencedColumns())->toEqual(['user_id', 'role_id']); +}); + +it('can set delete and update actions', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + $foreignKey->onDelete('CASCADE')->onUpdate('SET_NULL'); + + $options = $foreignKey->getOptions(); + expect($options['delete'])->toEqual('CASCADE'); + expect($options['update'])->toEqual('SET_NULL'); +}); + +it('can set constraint name', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + $foreignKey->constraint('fk_posts_user_id'); + + expect($foreignKey->getOptions()['constraint'])->toEqual('fk_posts_user_id'); +}); + +it('can use fluent interface with references and on', function (): void { + $foreignKey = new ForeignKey('user_id'); + $foreignKey->references('id')->on('users'); + + expect($foreignKey->getColumns())->toEqual('user_id'); + expect($foreignKey->getReferencedTable())->toEqual('users'); + expect($foreignKey->getReferencedColumns())->toEqual('id'); +}); + +it('can set deferrable option for PostgreSQL', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $foreignKey->setAdapter($postgresAdapter); + $foreignKey->deferrable('IMMEDIATE'); + + expect($foreignKey->getOptions()['deferrable'])->toEqual('IMMEDIATE'); + expect($foreignKey->isPostgres())->toBeTrue(); +}); + +it('ignores deferrable option for non-PostgreSQL adapters', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $foreignKey->setAdapter($mysqlAdapter); + $foreignKey->deferrable('IMMEDIATE'); + + expect($foreignKey->getOptions())->not->toHaveKey('deferrable'); + expect($foreignKey->isMysql())->toBeTrue(); +}); + +it('can add foreign key to table using foreignKey method', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $foreignKey = $table->foreignKey('user_id', 'users', 'id', ['delete' => 'CASCADE']); + + expect($foreignKey)->toBeInstanceOf(ForeignKey::class); + expect($foreignKey->getColumns())->toEqual('user_id'); + expect($foreignKey->getReferencedTable())->toEqual('users'); + expect($foreignKey->getReferencedColumns())->toEqual('id'); + expect($foreignKey->getOptions()['delete'])->toEqual('CASCADE'); + + $foreignKeys = $table->getForeignKeyBuilders(); + expect(count($foreignKeys))->toEqual(1); + expect($foreignKeys[0])->toEqual($foreignKey); +}); + +it('can add foreign key to table using foreign method with fluent interface', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $foreignKey = $table->foreign('user_id')->references('id')->on('users')->onDelete('CASCADE'); + + expect($foreignKey->getColumns())->toEqual('user_id'); + expect($foreignKey->getReferencedTable())->toEqual('users'); + expect($foreignKey->getReferencedColumns())->toEqual('id'); + expect($foreignKey->getOptions()['delete'])->toEqual('CASCADE'); + + $foreignKeys = $table->getForeignKeyBuilders(); + expect(count($foreignKeys))->toEqual(1); + expect($foreignKeys[0])->toEqual($foreignKey); +}); + +it('can create foreign key with multiple columns using fluent interface', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $foreignKey = $table->foreign(['user_id', 'role_id']) + ->references(['user_id', 'role_id']) + ->on('user_roles') + ->onDelete('NO_ACTION') + ->onUpdate('NO_ACTION') + ->constraint('fk_posts_user_role'); + + expect($foreignKey->getColumns())->toEqual(['user_id', 'role_id']); + expect($foreignKey->getReferencedTable())->toEqual('user_roles'); + expect($foreignKey->getReferencedColumns())->toEqual(['user_id', 'role_id']); + expect($foreignKey->getOptions())->toEqual([ + 'delete' => 'NO_ACTION', + 'update' => 'NO_ACTION', + 'constraint' => 'fk_posts_user_role', + ]); +}); + +it('sets adapter correctly when added to table', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $foreignKey = $table->foreignKey('user_id', 'users'); + + // Verify adapter is set correctly + expect($foreignKey->getAdapter())->not->toBeNull(); +}); diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 51a739d1..961de723 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -1030,3 +1030,40 @@ expect($migration->table('users'))->toBeInstanceOf(Table::class); }); + +it('can add foreign key using table methods', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $table->string('title'); + $table->foreignKey('user_id', 'users', 'id', ['delete' => 'CASCADE']); + + $columns = $table->getColumnBuilders(); + $foreignKeys = $table->getForeignKeyBuilders(); + + expect(count($columns))->toBe(1); + expect(count($foreignKeys))->toBe(1); + + $foreignKey = $foreignKeys[0]; + expect($foreignKey->getColumns())->toBe('user_id'); + expect($foreignKey->getReferencedTable())->toBe('users'); + expect($foreignKey->getReferencedColumns())->toBe('id'); + expect($foreignKey->getOptions()['delete'])->toBe('CASCADE'); +}); + +it('can add foreign key using fluent interface', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $table->string('title'); + $table->foreign('author_id')->references('id')->on('authors')->onDelete('SET_NULL')->constraint('fk_post_author'); + + $foreignKeys = $table->getForeignKeyBuilders(); + + expect(count($foreignKeys))->toBe(1); + + $foreignKey = $foreignKeys[0]; + expect($foreignKey->getColumns())->toBe('author_id'); + expect($foreignKey->getReferencedTable())->toBe('authors'); + expect($foreignKey->getReferencedColumns())->toBe('id'); + expect($foreignKey->getOptions()['delete'])->toBe('SET_NULL'); + expect($foreignKey->getOptions()['constraint'])->toBe('fk_post_author'); +}); From 4536c652c0fbd25f121e35cee681eaa54de5b678 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 17:04:31 -0500 Subject: [PATCH 05/10] refactor(Column, ForeignKey): extend from TableColumn and remove adapter logic --- src/Database/Migrations/Columns/Column.php | 48 +------------------ src/Database/Migrations/ForeignKey.php | 49 +------------------ src/Database/Migrations/TableColumn.php | 55 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 94 deletions(-) create mode 100644 src/Database/Migrations/TableColumn.php diff --git a/src/Database/Migrations/Columns/Column.php b/src/Database/Migrations/Columns/Column.php index 441f4067..4477c789 100644 --- a/src/Database/Migrations/Columns/Column.php +++ b/src/Database/Migrations/Columns/Column.php @@ -4,18 +4,11 @@ namespace Phenix\Database\Migrations\Columns; -use Phinx\Db\Adapter\AdapterInterface; +use Phenix\Database\Migrations\TableColumn; use Phinx\Db\Adapter\MysqlAdapter; -use Phinx\Db\Adapter\PostgresAdapter; -use Phinx\Db\Adapter\SQLiteAdapter; -use Phinx\Db\Adapter\SqlServerAdapter; -abstract class Column +abstract class Column extends TableColumn { - protected array $options = []; - - protected AdapterInterface|null $adapter = null; - public function __construct( protected string $name ) { @@ -27,11 +20,6 @@ public function getName(): string return $this->name; } - public function getOptions(): array - { - return $this->options; - } - abstract public function getType(): string; public function nullable(): static @@ -109,36 +97,4 @@ public function limit(int $limit): static return $this; } - - public function setAdapter(AdapterInterface $adapter): static - { - $this->adapter = $adapter; - - return $this; - } - - public function getAdapter(): ?AdapterInterface - { - return $this->adapter; - } - - public function isMysql(): bool - { - return $this->adapter instanceof MysqlAdapter; - } - - public function isPostgres(): bool - { - return $this->adapter instanceof PostgresAdapter; - } - - public function isSQLite(): bool - { - return $this->adapter instanceof SQLiteAdapter; - } - - public function isSqlServer(): bool - { - return $this->adapter instanceof SqlServerAdapter; - } } diff --git a/src/Database/Migrations/ForeignKey.php b/src/Database/Migrations/ForeignKey.php index 91ccc201..5dbd64b2 100644 --- a/src/Database/Migrations/ForeignKey.php +++ b/src/Database/Migrations/ForeignKey.php @@ -4,18 +4,8 @@ namespace Phenix\Database\Migrations; -use Phinx\Db\Adapter\AdapterInterface; -use Phinx\Db\Adapter\MysqlAdapter; -use Phinx\Db\Adapter\PostgresAdapter; -use Phinx\Db\Adapter\SQLiteAdapter; -use Phinx\Db\Adapter\SqlServerAdapter; - -class ForeignKey +class ForeignKey extends TableColumn { - protected array $options = []; - - protected AdapterInterface|null $adapter = null; - public function __construct( protected string|array $columns, protected string $referencedTable = '', @@ -40,11 +30,6 @@ public function getReferencedColumns(): string|array return $this->referencedColumns; } - public function getOptions(): array - { - return $this->options; - } - public function onDelete(string $action): static { $this->options['delete'] = $action; @@ -88,36 +73,4 @@ public function on(string $table): static return $this; } - - public function setAdapter(AdapterInterface $adapter): static - { - $this->adapter = $adapter; - - return $this; - } - - public function getAdapter(): ?AdapterInterface - { - return $this->adapter; - } - - public function isMysql(): bool - { - return $this->adapter instanceof MysqlAdapter; - } - - public function isPostgres(): bool - { - return $this->adapter instanceof PostgresAdapter; - } - - public function isSQLite(): bool - { - return $this->adapter instanceof SQLiteAdapter; - } - - public function isSqlServer(): bool - { - return $this->adapter instanceof SqlServerAdapter; - } } diff --git a/src/Database/Migrations/TableColumn.php b/src/Database/Migrations/TableColumn.php new file mode 100644 index 00000000..f90569b2 --- /dev/null +++ b/src/Database/Migrations/TableColumn.php @@ -0,0 +1,55 @@ +options; + } + + public function setAdapter(AdapterInterface $adapter): static + { + $this->adapter = $adapter; + + return $this; + } + + public function getAdapter(): ?AdapterInterface + { + return $this->adapter; + } + + public function isMysql(): bool + { + return $this->adapter instanceof MysqlAdapter; + } + + public function isPostgres(): bool + { + return $this->adapter instanceof PostgresAdapter; + } + + public function isSQLite(): bool + { + return $this->adapter instanceof SQLiteAdapter; + } + + public function isSqlServer(): bool + { + return $this->adapter instanceof SqlServerAdapter; + } +} From 062086ce03e8fb375d6de2bfdbd87de9d474baca Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 17:05:51 -0500 Subject: [PATCH 06/10] refactor(Column): remove nullable method and delegate to TableColumn --- src/Database/Migrations/Columns/Column.php | 7 ------- src/Database/Migrations/TableColumn.php | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Database/Migrations/Columns/Column.php b/src/Database/Migrations/Columns/Column.php index 4477c789..26f8d95c 100644 --- a/src/Database/Migrations/Columns/Column.php +++ b/src/Database/Migrations/Columns/Column.php @@ -22,13 +22,6 @@ public function getName(): string abstract public function getType(): string; - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - public function comment(string $comment): static { $this->options['comment'] = $comment; diff --git a/src/Database/Migrations/TableColumn.php b/src/Database/Migrations/TableColumn.php index f90569b2..7f8e7d78 100644 --- a/src/Database/Migrations/TableColumn.php +++ b/src/Database/Migrations/TableColumn.php @@ -16,6 +16,13 @@ abstract class TableColumn protected AdapterInterface|null $adapter = null; + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + public function getOptions(): array { return $this->options; From e6761bbf397d28167e01e3a7307cd6a0d0ef8605 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 17:12:01 -0500 Subject: [PATCH 07/10] refactor(ForeignKeyTest): remove redundant comment verifying adapter setting --- tests/Unit/Database/Migrations/ForeignKeyTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Unit/Database/Migrations/ForeignKeyTest.php b/tests/Unit/Database/Migrations/ForeignKeyTest.php index 3436e506..09767df8 100644 --- a/tests/Unit/Database/Migrations/ForeignKeyTest.php +++ b/tests/Unit/Database/Migrations/ForeignKeyTest.php @@ -152,6 +152,5 @@ $foreignKey = $table->foreignKey('user_id', 'users'); - // Verify adapter is set correctly expect($foreignKey->getAdapter())->not->toBeNull(); }); From 808b10821792d843db81ba07f54d06d35eaf7968 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 17:53:56 -0500 Subject: [PATCH 08/10] feat: introduce ColumnAction enum for foreign key actions and update related methods --- src/Database/Constants/ColumnAction.php | 16 ++++++++ src/Database/Migrations/ForeignKey.php | 10 +++-- .../Database/Migrations/ForeignKeyTest.php | 38 +++++++++++++++++-- tests/Unit/Database/Migrations/TableTest.php | 5 ++- 4 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 src/Database/Constants/ColumnAction.php diff --git a/src/Database/Constants/ColumnAction.php b/src/Database/Constants/ColumnAction.php new file mode 100644 index 00000000..36d93d76 --- /dev/null +++ b/src/Database/Constants/ColumnAction.php @@ -0,0 +1,16 @@ +referencedColumns; } - public function onDelete(string $action): static + public function onDelete(string|ColumnAction $action): static { - $this->options['delete'] = $action; + $this->options['delete'] = $action instanceof ColumnAction ? $action->value : $action; return $this; } - public function onUpdate(string $action): static + public function onUpdate(string|ColumnAction $action): static { - $this->options['update'] = $action; + $this->options['update'] = $action instanceof ColumnAction ? $action->value : $action; return $this; } diff --git a/tests/Unit/Database/Migrations/ForeignKeyTest.php b/tests/Unit/Database/Migrations/ForeignKeyTest.php index 09767df8..e01cddda 100644 --- a/tests/Unit/Database/Migrations/ForeignKeyTest.php +++ b/tests/Unit/Database/Migrations/ForeignKeyTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Phenix\Database\Constants\ColumnAction; use Phenix\Database\Migrations\ForeignKey; use Phenix\Database\Migrations\Table; use Phinx\Db\Adapter\AdapterInterface; @@ -43,7 +44,7 @@ expect($foreignKey->getReferencedColumns())->toEqual(['user_id', 'role_id']); }); -it('can set delete and update actions', function (): void { +it('can set delete and update actions with strings', function (): void { $foreignKey = new ForeignKey('user_id', 'users', 'id'); $foreignKey->onDelete('CASCADE')->onUpdate('SET_NULL'); @@ -133,8 +134,8 @@ $foreignKey = $table->foreign(['user_id', 'role_id']) ->references(['user_id', 'role_id']) ->on('user_roles') - ->onDelete('NO_ACTION') - ->onUpdate('NO_ACTION') + ->onDelete(ColumnAction::NO_ACTION) + ->onUpdate(ColumnAction::NO_ACTION) ->constraint('fk_posts_user_role'); expect($foreignKey->getColumns())->toEqual(['user_id', 'role_id']); @@ -154,3 +155,34 @@ expect($foreignKey->getAdapter())->not->toBeNull(); }); + +it('can use ColumnAction enum constants for onDelete and onUpdate', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + $foreignKey->onDelete(ColumnAction::CASCADE)->onUpdate(ColumnAction::SET_NULL); + + $options = $foreignKey->getOptions(); + expect($options['delete'])->toEqual('CASCADE'); + expect($options['update'])->toEqual('SET_NULL'); +}); + +it('can use mixed string and ColumnAction enum parameters', function (): void { + $foreignKey = new ForeignKey('user_id', 'users', 'id'); + $foreignKey->onDelete('RESTRICT')->onUpdate(ColumnAction::NO_ACTION); + + $options = $foreignKey->getOptions(); + expect($options['delete'])->toEqual('RESTRICT'); + expect($options['update'])->toEqual('NO_ACTION'); +}); + +it('can use ColumnAction enum in fluent interface', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $foreignKey = $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete(ColumnAction::CASCADE) + ->onUpdate(ColumnAction::RESTRICT); + + expect($foreignKey->getOptions()['delete'])->toEqual('CASCADE'); + expect($foreignKey->getOptions()['update'])->toEqual('RESTRICT'); +}); diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 961de723..fe924d71 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Phenix\Database\Constants\ColumnAction; use Phenix\Database\Migration; use Phenix\Database\Migrations\Columns\BigInteger; use Phenix\Database\Migrations\Columns\Binary; @@ -1035,7 +1036,7 @@ $table = new Table('posts', adapter: $this->mockAdapter); $table->string('title'); - $table->foreignKey('user_id', 'users', 'id', ['delete' => 'CASCADE']); + $table->foreignKey('user_id', 'users', 'id', ['delete' => ColumnAction::CASCADE->value]); $columns = $table->getColumnBuilders(); $foreignKeys = $table->getForeignKeyBuilders(); @@ -1054,7 +1055,7 @@ $table = new Table('posts', adapter: $this->mockAdapter); $table->string('title'); - $table->foreign('author_id')->references('id')->on('authors')->onDelete('SET_NULL')->constraint('fk_post_author'); + $table->foreign('author_id')->references('id')->on('authors')->onDelete(ColumnAction::SET_NULL)->constraint('fk_post_author'); $foreignKeys = $table->getForeignKeyBuilders(); From 766965ba8869328e7fb4cd00cac44d4916f00d7f Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 18:07:59 -0500 Subject: [PATCH 09/10] tests(fix): change expectation for id column --- tests/Unit/Database/Migrations/TableTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index fe924d71..9e426e8c 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -351,7 +351,7 @@ $column = $table->id('user_id'); - expect($column)->toBeInstanceOf(UnsignedInteger::class); + expect($column)->toBeInstanceOf(UnsignedBigInteger::class); expect($column->getName())->toBe('user_id'); expect($column->getType())->toBe('integer'); expect($column->getOptions())->toBe([ From 5669f65301aa4f675ad8b05c2601aed1f20a47ad Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 18:14:44 -0500 Subject: [PATCH 10/10] tests(fix: change expectation biginteger --- tests/Unit/Database/Migrations/TableTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 9e426e8c..f0b18a5b 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -353,7 +353,7 @@ expect($column)->toBeInstanceOf(UnsignedBigInteger::class); expect($column->getName())->toBe('user_id'); - expect($column->getType())->toBe('integer'); + expect($column->getType())->toBe('biginteger'); expect($column->getOptions())->toBe([ 'null' => false, 'signed' => false,