Skip to content

Commit 744b970

Browse files
authored
Merge pull request #789 from cakephp/foreign-key-builder
Port changes from cakephp/phinx#2322
2 parents 2faa558 + cce96af commit 744b970

File tree

4 files changed

+98
-1
lines changed

4 files changed

+98
-1
lines changed

docs/en/writing-migrations.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ Option Description
881881
update set an action to be triggered when the row is updated
882882
delete set an action to be triggered when the row is deleted
883883
constraint set a name to be used by foreign key constraint
884+
deferrable define deferred constraint application (postgres only)
884885
========== ===========
885886

886887
You can pass one or more of these options to any column with the optional

src/Db/Adapter/PostgresAdapter.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta
13291329
if ($foreignKey->getOnUpdate()) {
13301330
$def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
13311331
}
1332+
if ($foreignKey->getDeferrableMode()) {
1333+
$def .= " {$foreignKey->getDeferrableMode()}";
1334+
}
13321335

13331336
return $def;
13341337
}

src/Db/Table/ForeignKey.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ class ForeignKey
1717
public const RESTRICT = 'RESTRICT';
1818
public const SET_NULL = 'SET NULL';
1919
public const NO_ACTION = 'NO ACTION';
20+
public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED';
21+
public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE';
22+
public const NOT_DEFERRED = 'NOT DEFERRABLE';
2023

2124
/**
2225
* @var array<string>
2326
*/
24-
protected static array $validOptions = ['delete', 'update', 'constraint'];
27+
protected static array $validOptions = ['delete', 'update', 'constraint', 'deferrable'];
2528

2629
/**
2730
* @var string[]
@@ -53,6 +56,11 @@ class ForeignKey
5356
*/
5457
protected ?string $constraint = null;
5558

59+
/**
60+
* @var string|null
61+
*/
62+
protected ?string $deferrableMode = null;
63+
5664
/**
5765
* Sets the foreign key columns.
5866
*
@@ -191,6 +199,27 @@ public function getConstraint(): ?string
191199
return $this->constraint;
192200
}
193201

202+
/**
203+
* Sets deferrable mode for the foreign key.
204+
*
205+
* @param string $deferrableMode Constraint
206+
* @return $this
207+
*/
208+
public function setDeferrableMode(string $deferrableMode)
209+
{
210+
$this->deferrableMode = $this->normalizeDeferrable($deferrableMode);
211+
212+
return $this;
213+
}
214+
215+
/**
216+
* Gets deferrable mode for the foreign key.
217+
*/
218+
public function getDeferrableMode(): ?string
219+
{
220+
return $this->deferrableMode;
221+
}
222+
194223
/**
195224
* Utility method that maps an array of index options to this objects methods.
196225
*
@@ -210,6 +239,8 @@ public function setOptions(array $options)
210239
$this->setOnDelete($value);
211240
} elseif ($option === 'update') {
212241
$this->setOnUpdate($value);
242+
} elseif ($option === 'deferrable') {
243+
$this->setDeferrableMode($value);
213244
} else {
214245
$method = 'set' . ucfirst($option);
215246
$this->$method($value);
@@ -235,4 +266,29 @@ protected function normalizeAction(string $action): string
235266

236267
return constant($constantName);
237268
}
269+
270+
/**
271+
* From passed value checks if it's correct and fixes if needed
272+
*
273+
* @param string $deferrable Deferrable
274+
* @throws \InvalidArgumentException
275+
* @return string
276+
*/
277+
protected function normalizeDeferrable(string $deferrable): string
278+
{
279+
$mapping = [
280+
'DEFERRED' => ForeignKey::DEFERRED,
281+
'IMMEDIATE' => ForeignKey::IMMEDIATE,
282+
'NOT DEFERRED' => ForeignKey::NOT_DEFERRED,
283+
ForeignKey::DEFERRED => ForeignKey::DEFERRED,
284+
ForeignKey::IMMEDIATE => ForeignKey::IMMEDIATE,
285+
ForeignKey::NOT_DEFERRED => ForeignKey::NOT_DEFERRED,
286+
];
287+
$normalized = strtoupper(str_replace('_', ' ', $deferrable));
288+
if (array_key_exists($normalized, $mapping)) {
289+
return $mapping[$normalized];
290+
}
291+
292+
throw new InvalidArgumentException('Unknown deferrable passed: ' . $deferrable);
293+
}
238294
}

tests/TestCase/Db/Table/ForeignKeyTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,41 @@ public function testSetOptionThrowsExceptionIfOptionIsNotString()
9797

9898
$this->fk->setOptions(['update']);
9999
}
100+
101+
#[DataProvider('deferrableProvider')]
102+
public function testDeferrableCanBeSetThroughSetters(string $dirtyValue, string $valueOfConstant): void
103+
{
104+
$this->fk->setDeferrableMode($dirtyValue);
105+
$this->assertEquals($valueOfConstant, $this->fk->getDeferrableMode());
106+
}
107+
108+
#[DataProvider('deferrableProvider')]
109+
public function testDeferrableCanBeSetThroughOptions(string $dirtyValue, string $valueOfConstant): void
110+
{
111+
$this->fk->setOptions([
112+
'deferrable' => $dirtyValue,
113+
]);
114+
$this->assertEquals($valueOfConstant, $this->fk->getDeferrableMode());
115+
}
116+
117+
public static function deferrableProvider(): array
118+
{
119+
return [
120+
['DEFERRED', ForeignKey::DEFERRED],
121+
['IMMEDIATE', ForeignKey::IMMEDIATE],
122+
['NOT_DEFERRED', ForeignKey::NOT_DEFERRED],
123+
['Deferred', ForeignKey::DEFERRED],
124+
['Immediate', ForeignKey::IMMEDIATE],
125+
['Not_deferred', ForeignKey::NOT_DEFERRED],
126+
[ForeignKey::DEFERRED, ForeignKey::DEFERRED],
127+
[ForeignKey::IMMEDIATE, ForeignKey::IMMEDIATE],
128+
[ForeignKey::NOT_DEFERRED, ForeignKey::NOT_DEFERRED],
129+
];
130+
}
131+
132+
public function testThrowsErrorForInvalidDeferrableValue(): void
133+
{
134+
$this->expectException(InvalidArgumentException::class);
135+
$this->fk->setDeferrableMode('invalid_value');
136+
}
100137
}

0 commit comments

Comments
 (0)