Skip to content

Commit fa7000f

Browse files
committed
feat: add ArticleCategory and ArticleCategoryCollection
1 parent 8b9a7b4 commit fa7000f

File tree

5 files changed

+218
-29
lines changed

5 files changed

+218
-29
lines changed

contexts/ArticlePublishing/Domain/Models/Article.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ private function __construct(
1515
private string $title,
1616
private string $body,
1717
private ArticleStatus $status,
18+
private ArticleCategoryCollection $categories,
1819
private ?CarbonImmutable $created_at = null,
1920
private ?CarbonImmutable $updated_at = null
2021
) {
@@ -25,13 +26,15 @@ public function revise(
2526
?string $newTitle,
2627
?string $newBody,
2728
?ArticleStatus $newStatus,
29+
?ArticleCategoryCollection $categories,
2830
?CarbonImmutable $newCreatedAt = null,
2931
) {
3032
$this->title = $newTitle ?? $this->title;
3133
$this->body = $newBody ?? $this->body;
3234
if ($newStatus && ! $this->status->equals($newStatus)) {
3335
$this->transitionStatus($newStatus);
3436
}
37+
$this->categories = $categories ?? $this->categories;
3538
$this->created_at = $newCreatedAt ?? $this->created_at;
3639
}
3740

@@ -40,11 +43,12 @@ public static function reconstitute(
4043
string $title,
4144
string $body,
4245
ArticleStatus $status,
46+
ArticleCategoryCollection $categories,
4347
?CarbonImmutable $created_at = null,
4448
?CarbonImmutable $updated_at = null,
4549
array $events = []
4650
): self {
47-
$article = new self($id, $title, $body, $status, $created_at, $updated_at);
51+
$article = new self($id, $title, $body, $status, $categories, $created_at, $updated_at);
4852
foreach ($events as $event) {
4953
$article->recordEvent($event);
5054
}
@@ -56,20 +60,22 @@ public static function createDraft(
5660
ArticleId $id,
5761
string $title,
5862
string $body,
63+
ArticleCategoryCollection $categories,
5964
?CarbonImmutable $created_at = null,
6065
?CarbonImmutable $updated_at = null
6166
): self {
62-
return new self($id, $title, $body, ArticleStatus::draft(), $created_at, $updated_at);
67+
return new self($id, $title, $body, ArticleStatus::draft(), $categories, $created_at, $updated_at);
6368
}
6469

6570
public static function createPublished(
6671
ArticleId $id,
6772
string $title,
6873
string $body,
74+
ArticleCategoryCollection $categories,
6975
?CarbonImmutable $created_at = null,
7076
?CarbonImmutable $updated_at = null
7177
): self {
72-
$article = new self($id, $title, $body, ArticleStatus::published(), $created_at, $updated_at);
78+
$article = new self($id, $title, $body, ArticleStatus::published(), $categories, $created_at, $updated_at);
7379
$article->recordEvent(new ArticlePublishedEvent($article->id));
7480

7581
return $article;
@@ -115,6 +121,16 @@ public function getbody(): string
115121
return $this->body;
116122
}
117123

124+
public function getStatus(): ArticleStatus
125+
{
126+
return $this->status;
127+
}
128+
129+
public function getCategories(): ArticleCategoryCollection
130+
{
131+
return $this->categories;
132+
}
133+
118134
public function getCreatedAt(): CarbonImmutable
119135
{
120136
return $this->created_at;
@@ -124,9 +140,4 @@ public function getUpdatedAt(): ?CarbonImmutable
124140
{
125141
return $this->updated_at;
126142
}
127-
128-
public function getStatus(): ArticleStatus
129-
{
130-
return $this->status;
131-
}
132143
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Contexts\ArticlePublishing\Domain\Models;
6+
7+
class ArticleCategory
8+
{
9+
private int $id;
10+
11+
private string $label;
12+
13+
public function __construct(int $id, string $label)
14+
{
15+
$this->id = $id;
16+
$this->label = $label;
17+
}
18+
19+
public function getId(): int
20+
{
21+
return $this->id;
22+
}
23+
24+
public function getLabel(): string
25+
{
26+
return $this->label;
27+
}
28+
29+
public function equals(ArticleCategory $category): bool
30+
{
31+
return $this->id === $category->getId();
32+
}
33+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Contexts\ArticlePublishing\Domain\Models;
6+
7+
use App\Exceptions\BizException;
8+
use Illuminate\Support\Collection;
9+
10+
class ArticleCategoryCollection
11+
{
12+
private Collection $items;
13+
14+
public function __construct(array $categories = [])
15+
{
16+
$this->items = new Collection($categories);
17+
$this->validateCategories();
18+
}
19+
20+
private function validateCategories(): void
21+
{
22+
if ($this->items->isEmpty()) {
23+
throw BizException::make('Article categories cannot be empty');
24+
}
25+
26+
$this->items->each(function ($category) {
27+
if (! $category instanceof ArticleCategory) {
28+
throw BizException::make('Invalid article category')->logContext(['category' => $category]);
29+
}
30+
});
31+
}
32+
33+
public function getIdsArray(): array
34+
{
35+
return $this->items->map(fn (ArticleCategory $category) => $category->getId())->toArray();
36+
}
37+
38+
/**
39+
* @template T
40+
*
41+
* @param callable(ArticleCategory): T $callback
42+
* @return Collection<int, T>
43+
*/
44+
public function map(callable $callback): Collection
45+
{
46+
return $this->items->map($callback);
47+
}
48+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Exceptions\BizException;
6+
use Contexts\ArticlePublishing\Domain\Models\ArticleCategory;
7+
use Contexts\ArticlePublishing\Domain\Models\ArticleCategoryCollection;
8+
use Illuminate\Support\Collection;
9+
use Mockery\MockInterface;
10+
11+
beforeEach(function () {
12+
$this->mockCategory = Mockery::mock(ArticleCategory::class);
13+
$this->mockCategory->shouldReceive('getId')->andReturn(1);
14+
});
15+
16+
afterEach(function () {
17+
Mockery::close();
18+
});
19+
20+
it('throws exception when categories array is empty', function () {
21+
expect(fn () => new ArticleCategoryCollection([]))
22+
->toThrow(BizException::class, 'Article categories cannot be empty');
23+
});
24+
25+
it('throws exception when category is not an ArticleCategory instance', function () {
26+
expect(fn () => new ArticleCategoryCollection(['invalid']))
27+
->toThrow(BizException::class, 'Invalid article category');
28+
});
29+
30+
it('creates collection with valid categories', function () {
31+
$collection = new ArticleCategoryCollection([$this->mockCategory]);
32+
33+
expect($collection)
34+
->toBeInstanceOf(ArticleCategoryCollection::class);
35+
});
36+
37+
it('returns array of category IDs', function () {
38+
/** @var MockInterface&ArticleCategory $mockCategory2 */
39+
$mockCategory2 = Mockery::mock(ArticleCategory::class);
40+
$mockCategory2->shouldReceive('getId')->andReturn(2);
41+
42+
$collection = new ArticleCategoryCollection([$this->mockCategory, $mockCategory2]);
43+
44+
expect($collection->getIdsArray())
45+
->toBe([1, 2])
46+
->toBeArray();
47+
});
48+
49+
it('maps categories with callback', function () {
50+
/** @var MockInterface&ArticleCategory $mockCategory2 */
51+
$mockCategory2 = Mockery::mock(ArticleCategory::class);
52+
53+
$collection = new ArticleCategoryCollection([$this->mockCategory, $mockCategory2]);
54+
55+
$result = $collection->map(fn ($category) => 'mapped_'.spl_object_id($category));
56+
57+
expect($result)
58+
->toBeInstanceOf(Collection::class)
59+
->toHaveCount(2)
60+
->each->toContain('mapped_');
61+
});

0 commit comments

Comments
 (0)