Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/Command/ModelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg
];
} else {
$tmpModelName = $this->_modelNameFromKey($fieldName);
// A key that resolves to the table itself (e.g. a `system_id`
// column on `systems`) is only a real self-reference when it is
// actually constrained as a foreign key to this table. Otherwise
// (e.g. an external id that merely follows the `<table>_id`
// naming) skip it to avoid generating an invalid self-association
// that collides with the table's own alias.
if ($tmpModelName === $model->getAlias() && !$this->findTableReferencedBy($schema, $fieldName)) {
continue;
}
if (!$this->getTableLocator()->exists($tmpModelName)) {
$this->getTableLocator()->get(
$tmpModelName,
Expand Down Expand Up @@ -520,7 +529,11 @@ public function findHasOne(Table $model, array $associations): array
}

$assoc = false;
if (!in_array($fieldName, $primaryKey) && $fieldName === $foreignKey) {
if (
$otherTableName !== $tableName &&
!in_array($fieldName, $primaryKey) &&
$fieldName === $foreignKey
) {
$assoc = [
'alias' => $otherModel->getAlias(),
'foreignKey' => $fieldName,
Expand Down Expand Up @@ -564,6 +577,7 @@ public function findHasMany(Table $model, array $associations): array
foreach ($otherSchema->columns() as $fieldName) {
$assoc = false;
if (
$otherTableName !== $tableName &&
!in_array($fieldName, $primaryKey) &&
$fieldName === $foreignKey &&
!$this->hasUniqueConstraintFor($otherSchema, $fieldName)
Expand Down
76 changes: 76 additions & 0 deletions tests/TestCase/Command/ModelCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,82 @@ public function testApplyAssociationsConcreteClass(): void
$this->assertEquals($original, $new);
}

/**
* A non-foreign-key column matching the table name (e.g. `system_id` on
* `systems`) must not generate an invalid self-referencing association.
*
* @return void
*/
public function testGetAssociationsSkipsSelfReferencingKey(): void
{
$systems = $this->getTableLocator()->get('Systems');

$command = new ModelCommand();
$command->connection = 'test';
$args = new Arguments([], [], []);
$io = $this->createStub(ConsoleIo::class);
$result = $command->getAssociations($systems, $args, $io);

$aliases = array_merge(
array_column($result['belongsTo'], 'alias'),
array_column($result['hasMany'], 'alias'),
);
$this->assertNotContains('Systems', $aliases, 'Self-referencing association must not be generated.');

// Applying the associations must not throw.
$command->applyAssociations($systems, $result);
$this->assertSame([], $systems->associations()->keys());
}

/**
* A real self-referencing foreign key (constrained, not named `parent_id`)
* must still produce a belongsTo association.
*
* @return void
*/
public function testGetAssociationsKeepsConstrainedSelfReference(): void
{
$nodes = $this->getTableLocator()->get('Nodes');

$command = new ModelCommand();
$command->connection = 'test';
$args = new Arguments([], [], []);
$io = $this->createStub(ConsoleIo::class);
$result = $command->getAssociations($nodes, $args, $io);

$this->assertSame(['Nodes'], array_column($result['belongsTo'], 'alias'));

$command->applyAssociations($nodes, $result);
$this->assertSame(['Nodes'], $nodes->associations()->keys());
}

/**
* A unique non-foreign-key column matching the table name (e.g. a unique
* `gadget_id` on `gadgets`) must not generate a self-referencing hasOne.
*
* @return void
*/
public function testGetAssociationsSkipsUniqueSelfReferencingKey(): void
{
$gadgets = $this->getTableLocator()->get('Gadgets');

$command = new ModelCommand();
$command->connection = 'test';
$args = new Arguments([], [], []);
$io = $this->createStub(ConsoleIo::class);
$result = $command->getAssociations($gadgets, $args, $io);

$aliases = array_merge(
array_column($result['belongsTo'], 'alias'),
array_column($result['hasOne'], 'alias'),
array_column($result['hasMany'], 'alias'),
);
$this->assertNotContains('Gadgets', $aliases, 'Self-referencing association must not be generated.');

$command->applyAssociations($gadgets, $result);
$this->assertSame([], $gadgets->associations()->keys());
}

/**
* Test getAssociations
*
Expand Down
45 changes: 45 additions & 0 deletions tests/schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,51 @@
'unique_self_referencing_parent' => ['type' => 'unique', 'columns' => ['parent_id']],
],
],
// "systems" has a "system_id" column that is not a real foreign key.
// Bake must not generate a self-referencing Systems association for it.
[
'table' => 'systems',
'columns' => [
'id' => ['type' => 'integer'],
'system_id' => ['type' => 'string', 'length' => 255, 'null' => false],
'name' => ['type' => 'string', 'length' => 255, 'null' => true],
],
'constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
],
// "nodes" has a real self-referencing foreign key (node_id -> nodes.id)
// that is not named parent_id. The belongsTo association must still be kept.
[
'table' => 'nodes',
'columns' => [
'id' => ['type' => 'integer'],
'node_id' => ['type' => 'integer', 'null' => true],
'name' => ['type' => 'string', 'length' => 255, 'null' => true],
],
'constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']],
'node_id_fk' => [
'type' => 'foreign',
'columns' => ['node_id'],
'references' => ['nodes', 'id'],
'update' => 'cascade',
'delete' => 'cascade',
],
],
],
// "gadgets" has a unique "gadget_id" column that is not a real foreign key.
// Bake must not generate a self-referencing hasOne Gadgets association.
[
'table' => 'gadgets',
'columns' => [
'id' => ['type' => 'integer'],
'gadget_id' => ['type' => 'string', 'length' => 255, 'null' => false],
'name' => ['type' => 'string', 'length' => 255, 'null' => true],
],
'constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']],
'unique_gadget_id' => ['type' => 'unique', 'columns' => ['gadget_id']],
],
],
// "news" is both singular and plural - tests variable collision fix
[
'table' => 'news',
Expand Down
Loading