Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,12 @@ Also, you can use convenient methods to add, remove, and set entities in the rel
$author->books->add($book);
$author->books->remove($book);
$author->books->set([$book]);
$author->books->removeAll();

$book->tags->add($tag);
$book->tags->remove($tag);
$book->tags->set([$tag]);
$book->tags->removeAll();
```

The relationship property wrapper accepts both entity instances and an id (primary key value). If you pass an id, Orm will load the proper entity automatically. This behavior is available only if the entity is "attached" to the repository (fetched from storage, directly attached, or indirectly attached by another attached entity).
Expand Down
10 changes: 10 additions & 0 deletions src/Relationships/HasMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ public function has($entity): bool
}


public function removeAll(): bool
{
foreach ($this->getCollection() as $entity) {
$this->remove($entity);
}

return true;
}


public function set(array $data): bool
{
$wanted = [];
Expand Down
7 changes: 7 additions & 0 deletions src/Relationships/IRelationshipCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public function set(array $data): bool;
public function remove($entity): ?IEntity;


/**
* Removes all entities.
* @return bool
*/
public function removeAll(): bool;


/**
* @param E|string|int $entity
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

/**
* @testCase
* @dataProvider ../../../databases.ini
*/

namespace NextrasTests\Orm\Integration\Relationships;


use NextrasTests\Orm\Author;
use NextrasTests\Orm\DataTestCase;
use NextrasTests\Orm\Tag;
use NextrasTests\Orm\TagFollower;
use Tester\Assert;


require_once __DIR__ . '/../../../bootstrap.php';


class RelationshipOneHasManyRemoveAllTest extends DataTestCase
{

public function testRemoveAllItems(): void
{
$author = new Author();
$author->name = 'Stephen King';

$tagFollower = new TagFollower();
$tagFollower->author = $author;
$tagFollower->tag = new Tag('Horror');

$this->orm->authors->persistAndFlush($author);

Assert::same(1, $author->tagFollowers->count());
$author->tagFollowers->removeAll();
Assert::same(0, $author->tagFollowers->count());

$this->orm->authors->persistAndFlush($author);

Assert::same(0, $author->tagFollowers->count());
Assert::same(0, $author->tagFollowers->countStored());
}
}


$test = new RelationshipOneHasManyRemoveAllTest();
$test->run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
START TRANSACTION;
INSERT INTO "public"."authors" ("name", "born_on", "web", "favorite_author_id") VALUES ('Stephen King', '2021-03-21'::date, 'http://www.example.com', NULL);
SELECT CURRVAL('public.authors_id_seq');
INSERT INTO "tags" ("name", "is_global") VALUES ('Horror', 'y');
SELECT CURRVAL('public.tags_id_seq');
INSERT INTO "tag_followers" ("created_at", "author_id", "tag_id") VALUES ('2021-12-02 19:21:00.000000'::timestamptz, 3, 4);
COMMIT;
SELECT "tag_followers".* FROM "tag_followers" AS "tag_followers" WHERE "tag_followers"."author_id" IN (3);
SELECT "tag_followers".* FROM "tag_followers" AS "tag_followers" WHERE (NOT (("tag_followers"."author_id", "tag_followers"."tag_id") IN ((3, 4)))) AND ("tag_followers"."author_id" IN (3));
START TRANSACTION;
COMMIT;
Comment on lines +10 to +11
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no DELETE. Do you know why? @hrach

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:-O I do not. Hopefully, you have not discovered a bug.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks buggy to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no, this is a real bug that has been hidden for many years.

Expected behavior:

  • throw an exception, that (in this case) the TagFollowe::$author cannot be null, or
  • delete the TagFollower if and only if the relationship is marked with a correct, currently non-existing, cascade marker, e.g., removeOrphan.

The second option is tracked in #205 and #278. Sadly, the current behavior does not rigger exception (either missing unsupported removeOrphan || non-nullable).

Supporting removeOrphan is a bit more difficult - we need to rewrite PersistenceHelper & RemovalHelper -> join the implementation together -> since the wanted behavior is in RemovalHelper, but needs to be triggered from PersistenceHelper.

As we are in the 5.1 RC phase, I would rather incline to trigger the exception. The working workaround is to call the TagFollower repository's remove().

SELECT "tag_followers".* FROM "tag_followers" AS "tag_followers" WHERE "tag_followers"."author_id" IN (3);
SELECT "author_id", COUNT(DISTINCT "tag_followers"."tag_id") as "count" FROM "tag_followers" AS "tag_followers" WHERE "tag_followers"."author_id" IN (3) GROUP BY "author_id";
Loading