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
21 changes: 21 additions & 0 deletions src/Definition/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ final class Field

private ?int $generated = null;
private bool $referenced = false;
private bool $relationGenerated = false;
private ?string $entityClass = null;

public function __construct()
Expand Down Expand Up @@ -161,6 +162,26 @@ public function isReferenced(): bool
return $this->referenced;
}

/**
* Marks the field as created by a relation rather than declared by the user.
*
* @internal
*/
public function setRelationGenerated(bool $relationGenerated): self
{
$this->relationGenerated = $relationGenerated;

return $this;
}

/**
* @internal
*/
public function isRelationGenerated(): bool
{
return $this->relationGenerated;
}

public function getEntityClass(): ?string
{
return $this->entityClass;
Expand Down
10 changes: 9 additions & 1 deletion src/Relation/Traits/FieldTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,19 @@ protected function ensureField(Entity $target, string $fieldName, Field $outerFi
$outerField->setReferenced(true);

if ($target->getFields()->has($fieldName)) {
// field already exists and defined by the user
$field = $target->getFields()->get($fieldName);

// A column shared by several relations (HasMany and its BelongsTo) must not depend
// on which relation is generated first; user-declared fields are kept as they are.
if ($nullable && $field->isRelationGenerated()) {
$field->getOptions()->set(Column::OPT_NULLABLE, true);
}

return;
}

$field = new Field();
$field->setRelationGenerated(true);
$field->setEntityClass($target->getClass());
$field->setColumn($fieldName);
$field->setTypecast($outerField->getTypecast());
Expand Down
70 changes: 70 additions & 0 deletions tests/Schema/Relation/HasManyRelationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

namespace Cycle\Schema\Tests\Relation;

use Cycle\Database\Schema\AbstractTable;
use Cycle\ORM\Collection\ArrayCollectionFactory;
use Cycle\ORM\Relation;
use Cycle\ORM\Schema;
use Cycle\Schema\Compiler;
use Cycle\Schema\Definition\Entity;
use Cycle\Schema\Definition\Field;
use Cycle\Schema\Definition\Relation as RelationDefinition;
use Cycle\Schema\Exception\RegistryException;
use Cycle\Schema\Exception\SchemaException;
use Cycle\Schema\Generator\GenerateRelations;
Expand Down Expand Up @@ -304,4 +308,70 @@ public function testRenderWithIndex(): void
$table = $this->getDriver()->getSchema('plain');
$this->assertTrue($table->hasIndex(['user_p_id']));
}

public function belongsToNullableOrders(): iterable
{
yield 'hasMany source first' => [true];
yield 'belongsTo source first' => [false];
}

/**
* @dataProvider belongsToNullableOrders
*/
public function testNullableBelongsToMakesSharedColumnNullable(bool $userFirst): void
{
$e = Plain::define();
$u = User::define();

$u->getRelations()->get('plain')->setType('hasMany');
$e->getRelations()->set(
'user',
(new RelationDefinition())->setTarget('user')->setType('belongsTo'),
);
$e->getRelations()->get('user')->getOptions()->set('nullable', true);

$table = $this->renderPlainWithUser($e, $u, $userFirst);

$this->assertTrue($table->column('user_p_id')->isNullable());
}

/**
* @dataProvider belongsToNullableOrders
*/
public function testNullableBelongsToKeepsUserDefinedColumn(bool $userFirst): void
{
$e = Plain::define();
$u = User::define();

$e->getFields()->set('user_p_id', (new Field())->setType('int')->setColumn('user_p_id'));
$u->getRelations()->get('plain')->setType('hasMany');
$e->getRelations()->set(
'user',
(new RelationDefinition())->setTarget('user')->setType('belongsTo'),
);
$e->getRelations()->get('user')->getOptions()->set('nullable', true);

$table = $this->renderPlainWithUser($e, $u, $userFirst);

$this->assertFalse($table->column('user_p_id')->isNullable());
}

private function renderPlainWithUser(Entity $plain, Entity $user, bool $userFirst): AbstractTable
{
$r = new Registry($this->dbal);
$entities = $userFirst ? [[$user, 'user'], [$plain, 'plain']] : [[$plain, 'plain'], [$user, 'user']];
foreach ($entities as [$entity, $table]) {
$r->register($entity)->linkTable($entity, 'default', $table);
}

(new Compiler())->compile($r, [
new GenerateRelations(['hasMany' => new HasMany(), 'belongsTo' => new BelongsTo()]),
$t = new RenderTables(),
new RenderRelations(),
]);

$t->getReflector()->run();

return $this->getDriver()->getSchema('plain');
}
}
34 changes: 34 additions & 0 deletions tests/Schema/Relation/Traits/FieldTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,40 @@ public function testEnsureFieldIfFieldExistsItShouldNotBeCreated(): void
$this->assertCount(1, $target->getFields());
}

/**
* @dataProvider nullableOrders
*/
public function testEnsureFieldMergesNullableOfRelationGeneratedField(bool $first, bool $second): void
{
$target = new Entity();
$outer = (new Field())->setColumn('id')->setType('primary');

$this->ensureField($target, 'user_id', $outer, $first);
$this->ensureField($target, 'user_id', $outer, $second);

$field = $target->getFields()->get('user_id');
$this->assertTrue($field->isRelationGenerated());
$this->assertTrue($field->getOptions()->get(Column::OPT_NULLABLE));
}

public function nullableOrders(): iterable
{
yield 'not nullable first' => [false, true];
yield 'nullable first' => [true, false];
}

public function testEnsureFieldDoesNotChangeUserDefinedField(): void
{
$target = new Entity();
$outer = (new Field())->setColumn('id')->setType('primary');
$target->getFields()->set('user_id', $userField = (new Field())->setColumn('user_id')->setType('int'));

$this->ensureField($target, 'user_id', $outer, true);

$this->assertFalse($userField->isRelationGenerated());
$this->assertFalse($userField->getOptions()->has(Column::OPT_NULLABLE));
}

/**
* @dataProvider outerFieldTypes
*/
Expand Down
Loading