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: 13 additions & 8 deletions src/Relation/HasMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,18 @@ protected function getReferenceScope(Node $node): ?array
*/
protected function calcDeleted(iterable $related, iterable $original): array
{
$related = $this->extract($related);
$original = $this->extract($original);
return \array_udiff(
$original ?? [],
$related,
// static fn(object $a, object $b): int => strcmp(spl_object_hash($a), spl_object_hash($b))
static fn(object $a, object $b): int => (int) ($a === $b) - 1,
);
$relatedIds = [];
foreach ($this->extract($related) as $item) {
$relatedIds[\spl_object_id($item)] = true;
}

$deleted = [];
foreach ($this->extract($original) as $item) {
if (!isset($relatedIds[\spl_object_id($item)])) {
$deleted[] = $item;
}
}

return $deleted;
}
}
137 changes: 137 additions & 0 deletions tests/ORM/Functional/Driver/Common/Integration/Case575/CaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575;

use Cycle\ORM\Heap\Heap;
use Cycle\ORM\Select;
use Cycle\ORM\Tests\Functional\Driver\Common\BaseTest;
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\IntegrationTestTrait;
use Cycle\ORM\Tests\Traits\TableTrait;

/**
* Changing the order of a nullable HasMany collection must not detach the children that stay in it.
*/
abstract class CaseTest extends BaseTest
{
use IntegrationTestTrait;
use TableTrait;

public function testReorderKeepsChildren(): void
{
$user = $this->fetchUser();
$user->comments = \array_reverse($user->comments);

$this->captureWriteQueries();
$this->save($user);
$this->assertOwnedComments(['comment 1', 'comment 2', 'comment 3']);
$this->assertNumWrites(0);
}

public function testPrependKeepsChildren(): void
{
$user = $this->fetchUser();
$comment = new Entity\Comment('comment 4');
$comment->user = $user;
\array_unshift($user->comments, $comment);

$this->captureWriteQueries();
$this->save($user);
$this->assertOwnedComments(['comment 1', 'comment 2', 'comment 3', 'comment 4']);
$this->assertNumWrites(1);
}

public function testReplaceInTheMiddleDetachesOnlyReplacedChild(): void
{
$user = $this->fetchUser();
$user->comments[1]->user = null;
$comment = new Entity\Comment('comment 4');
$comment->user = $user;
$user->comments[1] = $comment;

$this->captureWriteQueries();
$this->save($user);
$this->assertOwnedComments(['comment 1', 'comment 3', 'comment 4']);
$this->assertNumWrites(2);
$this->assertSame(
['comment 2'],
$this->getDatabase()->table('comment')
->select('content')
->where('user_id', null)
->fetchAll(\PDO::FETCH_COLUMN),
);
}

public function setUp(): void
{
// Init DB
parent::setUp();
$this->makeTables();
$this->fillData();

$this->loadSchema(__DIR__ . '/schema.php');
}

private function fetchUser(): Entity\User
{
$user = (new Select($this->orm, Entity\User::class))
->load('comments')
->wherePK(1)
->fetchOne();

$this->assertInstanceOf(Entity\User::class, $user);
$this->assertCount(3, $user->comments);

return $user;
}

/**
* @param list<non-empty-string> $expected Contents of the user's comments, in id order.
*/
private function assertOwnedComments(array $expected): void
{
$user = (new Select($this->orm->withHeap(new Heap()), Entity\User::class))
->load('comments')
->wherePK(1)
->fetchOne();

$this->assertSame(
$expected,
\array_map(static fn(Entity\Comment $comment): string => $comment->content, $user->comments),
);
}

private function makeTables(): void
{
$this->makeTable(Entity\User::ROLE, [
'id' => 'primary',
'login' => 'string',
]);

$this->makeTable(Entity\Comment::ROLE, [
'id' => 'primary',
'content' => 'string',
'user_id' => 'int,nullable',
]);
$this->makeFK(Entity\Comment::ROLE, 'user_id', Entity\User::ROLE, 'id', 'NO ACTION', 'NO ACTION');
}

private function fillData(): void
{
$this->getDatabase()->table('user')->insertMultiple(
['login'],
[
['user-1'],
],
);
$this->getDatabase()->table('comment')->insertMultiple(
['user_id', 'content'],
[
[1, 'comment 1'],
[1, 'comment 2'],
[1, 'comment 3'],
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\Entity;

class Comment
{
public const ROLE = 'comment';

public ?int $id = null;
public string $content;
public ?User $user = null;
public ?int $user_id = null;

public function __construct(string $content)
{
$this->content = $content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\Entity;

class User
{
public const ROLE = 'user';

public ?int $id = null;
public string $login;

/** @var list<Comment> */
public array $comments = [];

public function __construct(string $login)
{
$this->login = $login;
}
}
81 changes: 81 additions & 0 deletions tests/ORM/Functional/Driver/Common/Integration/Case575/schema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

use Cycle\ORM\Mapper\Mapper;
use Cycle\ORM\Relation;
use Cycle\ORM\SchemaInterface as Schema;
use Cycle\ORM\Select\Repository;
use Cycle\ORM\Select\Source;
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\Entity\Comment;
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\Entity\User;

return [
User::ROLE => [
Schema::ENTITY => User::class,
Schema::MAPPER => Mapper::class,
Schema::SOURCE => Source::class,
Schema::REPOSITORY => Repository::class,
Schema::DATABASE => 'default',
Schema::TABLE => 'user',
Schema::PRIMARY_KEY => ['id'],
Schema::FIND_BY_KEYS => ['id'],
Schema::COLUMNS => [
'id' => 'id',
'login' => 'login',
],
Schema::RELATIONS => [
'comments' => [
Relation::TYPE => Relation::HAS_MANY,
Relation::TARGET => Comment::ROLE,
Relation::COLLECTION_TYPE => 'array',
Relation::LOAD => Relation::LOAD_PROMISE,
Relation::SCHEMA => [
Relation::CASCADE => true,
Relation::NULLABLE => true,
Relation::WHERE => [],
Relation::ORDER_BY => ['id' => 'ASC'],
Relation::INNER_KEY => ['id'],
Relation::OUTER_KEY => 'user_id',
],
],
],
Schema::TYPECAST => [
'id' => 'int',
],
Schema::SCHEMA => [],
],
Comment::ROLE => [
Schema::ENTITY => Comment::class,
Schema::MAPPER => Mapper::class,
Schema::SOURCE => Source::class,
Schema::REPOSITORY => Repository::class,
Schema::DATABASE => 'default',
Schema::TABLE => 'comment',
Schema::PRIMARY_KEY => ['id'],
Schema::FIND_BY_KEYS => ['id'],
Schema::COLUMNS => [
'id' => 'id',
'content' => 'content',
'user_id' => 'user_id',
],
Schema::RELATIONS => [
'user' => [
Relation::TYPE => Relation::BELONGS_TO,
Relation::TARGET => User::ROLE,
Relation::LOAD => Relation::LOAD_PROMISE,
Relation::SCHEMA => [
Relation::CASCADE => true,
Relation::NULLABLE => true,
Relation::INNER_KEY => 'user_id',
Relation::OUTER_KEY => ['id'],
],
],
],
Schema::TYPECAST => [
'id' => 'int',
'user_id' => 'int',
],
Schema::SCHEMA => [],
],
];
17 changes: 17 additions & 0 deletions tests/ORM/Functional/Driver/MySQL/Integration/Case575/CaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\MySQL\Integration\Case575;

// phpcs:ignore
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\CaseTest as CommonClass;

/**
* @group driver
* @group driver-mysql
*/
class CaseTest extends CommonClass
{
public const DRIVER = 'mysql';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Postgres\Integration\Case575;

// phpcs:ignore
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\CaseTest as CommonClass;

/**
* @group driver
* @group driver-postgres
*/
class CaseTest extends CommonClass
{
public const DRIVER = 'postgres';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\SQLServer\Integration\Case575;

// phpcs:ignore
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\CaseTest as CommonClass;

/**
* @group driver
* @group driver-sqlserver
*/
class CaseTest extends CommonClass
{
public const DRIVER = 'sqlserver';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\SQLite\Integration\Case575;

// phpcs:ignore
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case575\CaseTest as CommonClass;

/**
* @group driver
* @group driver-sqlite
*/
class CaseTest extends CommonClass
{
public const DRIVER = 'sqlite';
}
Loading