1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of the Explicit Architecture POC, |
7
|
|
|
* which is created on top of the Symfony Demo application. |
8
|
|
|
* |
9
|
|
|
* (c) Herberto Graça <[email protected]> |
10
|
|
|
* |
11
|
|
|
* For the full copyright and license information, please view the LICENSE |
12
|
|
|
* file that was distributed with this source code. |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace Acme\App\Core\Component\Blog\Domain\Post; |
16
|
|
|
|
17
|
|
|
use Acme\App\Core\Component\Blog\Domain\Post\Comment\Comment; |
18
|
|
|
use Acme\App\Core\Component\Blog\Domain\Post\Tag\Tag; |
19
|
|
|
use Acme\App\Core\SharedKernel\Component\User\Domain\User\UserId; |
20
|
|
|
use Acme\PhpExtension\DateTime\DateTimeGenerator; |
21
|
|
|
use Acme\PhpExtension\String\Slugger; |
22
|
|
|
use DateTime; |
23
|
|
|
use DateTimeImmutable; |
24
|
|
|
use DateTimeInterface; |
25
|
|
|
use function is_array; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Defines the properties of the Post entity to represent the blog posts. |
29
|
|
|
* |
30
|
|
|
* See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class |
31
|
|
|
* |
32
|
|
|
* Tip: if you have an existing database, you can generate these entity class automatically. |
33
|
|
|
* See https://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html |
34
|
|
|
* |
35
|
|
|
* @author Ryan Weaver <[email protected]> |
36
|
|
|
* @author Javier Eguiluz <[email protected]> |
37
|
|
|
* @author Yonel Ceruto <[email protected]> |
38
|
|
|
* @author Herberto Graca <[email protected]> |
39
|
|
|
*/ |
40
|
|
|
class Post |
41
|
|
|
{ |
42
|
|
|
/** |
43
|
|
|
* Use constants to define configuration options that rarely change instead |
44
|
|
|
* of specifying them under parameters section in config/services.yaml file. |
45
|
|
|
* |
46
|
|
|
* See https://symfony.com/doc/current/best_practices/configuration.html#constants-vs-configuration-options |
47
|
|
|
*/ |
48
|
|
|
const NUM_ITEMS = 10; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var PostId |
52
|
|
|
*/ |
53
|
|
|
private $id; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
private $title; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
private $slug; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var string |
67
|
|
|
*/ |
68
|
|
|
private $summary; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var string |
72
|
|
|
*/ |
73
|
|
|
private $content; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @var DateTimeImmutable |
77
|
|
|
*/ |
78
|
|
|
private $publishedAt; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @var UserId |
82
|
|
|
*/ |
83
|
|
|
private $authorId; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @var Comment[] |
87
|
|
|
*/ |
88
|
|
|
private $comments = []; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* We don't want to have any reference to Doctrine in the Domain, so we remove the Collection type hint from here. |
92
|
|
|
* |
93
|
|
|
* @var Tag[] |
94
|
|
|
*/ |
95
|
|
|
private $tags = []; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @var bool |
99
|
|
|
*/ |
100
|
|
|
private $isNewPost = false; |
101
|
|
|
|
102
|
|
|
public function __construct() |
103
|
|
|
{ |
104
|
|
|
$this->publishedAt = DateTimeGenerator::generate(); |
105
|
|
|
$this->id = new PostId(); |
106
|
|
|
$this->isNewPost = true; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
public function getId(): PostId |
110
|
|
|
{ |
111
|
|
|
return $this->id; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
public function getTitle(): ?string |
115
|
|
|
{ |
116
|
|
|
return $this->title; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
public function setTitle(string $title): void |
120
|
|
|
{ |
121
|
|
|
$this->title = $title; |
122
|
|
|
|
123
|
|
|
if ($this->isNewPost) { |
124
|
|
|
$this->slug = Slugger::slugify($this->getTitle()); |
125
|
|
|
} |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
public function getSlug(): ?string |
129
|
|
|
{ |
130
|
|
|
return $this->slug; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
public function postfixSlug(string $suffix): void |
134
|
|
|
{ |
135
|
|
|
if (!$this->isNewPost) { |
136
|
|
|
throw new SlugIsImmutableException(); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$suffix = '-' . ltrim($suffix, '-'); |
140
|
|
|
|
141
|
|
|
$this->slug = $this->slug . $suffix; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
public function getContent(): ?string |
145
|
|
|
{ |
146
|
|
|
return $this->content; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
public function setContent(string $content): void |
150
|
|
|
{ |
151
|
|
|
$this->content = $content; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
public function getPublishedAt(): DateTimeImmutable |
155
|
|
|
{ |
156
|
|
|
return $this->publishedAt; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
public function setPublishedAt(DateTimeInterface $publishedAt): void |
160
|
|
|
{ |
161
|
|
|
/* |
162
|
|
|
* We need this check here because Symfony/Form 4.0 can not create DateTimeImmutable, but 4.1 will |
163
|
|
|
*/ |
164
|
|
|
$this->publishedAt = $publishedAt instanceof DateTime |
|
|
|
|
165
|
|
|
? DateTimeImmutable::createFromMutable($publishedAt) |
166
|
|
|
: $publishedAt; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
public function getAuthorId(): UserId |
170
|
|
|
{ |
171
|
|
|
return $this->authorId; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
public function setAuthorId(UserId $authorId): void |
175
|
|
|
{ |
176
|
|
|
$this->authorId = $authorId; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* We don't want to have here any reference to doctrine, so we remove the Collection type hint from everywhere. |
181
|
|
|
* The safest is to treat it as an array but we can't type hint it with 'array' because we might actually |
182
|
|
|
* return an Collection. |
183
|
|
|
* |
184
|
|
|
* @return Comment[] |
185
|
|
|
*/ |
186
|
|
|
public function getComments() |
187
|
|
|
{ |
188
|
|
|
return $this->comments; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
public function addComment(Comment $comment): void |
192
|
|
|
{ |
193
|
|
|
$comment->setPost($this); |
194
|
|
|
if (!$this->contains($comment, $this->comments)) { |
195
|
|
|
$this->comments[] = $comment; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* This method is not used, but I will leave it here as an example |
201
|
|
|
*/ |
202
|
|
|
public function removeComment(Comment $comment): void |
203
|
|
|
{ |
204
|
|
|
$comment->setPost(null); |
|
|
|
|
205
|
|
|
|
206
|
|
|
if ($key = $this->getKey($comment, $this->comments)) { |
207
|
|
|
unset($this->comments[$key]); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
public function getSummary(): ?string |
212
|
|
|
{ |
213
|
|
|
return $this->summary; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
public function setSummary(string $summary): void |
217
|
|
|
{ |
218
|
|
|
$this->summary = $summary; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
public function addTag(Tag ...$tags): void |
222
|
|
|
{ |
223
|
|
|
foreach ($tags as $tag) { |
224
|
|
|
if (!$this->contains($tag, $this->tags)) { |
225
|
|
|
$this->tags[] = $tag; |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* This method is not used, but I will leave it here as an example |
232
|
|
|
*/ |
233
|
|
|
public function removeTag(Tag $tag): void |
234
|
|
|
{ |
235
|
|
|
if ($key = $this->getKey($tag, $this->tags)) { |
236
|
|
|
unset($this->tags[$key]); |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* @return Tag[] |
242
|
|
|
*/ |
243
|
|
|
public function getTags(): array |
244
|
|
|
{ |
245
|
|
|
// Since we don't type hint `tags` as a Doctrine collection, the `toArray` method is not recognized, |
246
|
|
|
// however, we do know it's a doctrine collection. |
247
|
|
|
// If Doctrine would allow us to define our own custom collections, this wouldn't be a problem. |
248
|
|
|
// As that is not the case, unfortunately we have here a hidden dependency. |
249
|
|
|
return is_array($this->tags) |
250
|
|
|
? $this->tags |
251
|
|
|
: $this->tags->toArray(); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Since we don't type hint `tags` as a Doctrine collection, the `clear()` method below is |
256
|
|
|
* not recognized by the IDE, as it is not even possible to call a method on an array. |
257
|
|
|
* |
258
|
|
|
* So we create this method here to encapsulate that operation, and minimize the issue, treating the `Post` entity |
259
|
|
|
* as an aggregate root. |
260
|
|
|
* |
261
|
|
|
* It is also a good practise to encapsulate these chained operations, |
262
|
|
|
* from an object calisthenics point of view. |
263
|
|
|
*/ |
264
|
|
|
public function clearTags(): void |
265
|
|
|
{ |
266
|
|
|
// Since we don't type hint `tags` as a Doctrine collection, the `clear` method is not recognized, |
267
|
|
|
// however, we do know it's a doctrine collection. |
268
|
|
|
// If Doctrine would allow us to define our own custom collections, this wouldn't be a problem. |
269
|
|
|
// As that is not the case, unfortunately we have here a hidden dependency. |
270
|
|
|
is_array($this->tags) |
271
|
|
|
? $this->tags = [] |
272
|
|
|
: $this->tags->clear(); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
private function contains($item, $list): bool |
276
|
|
|
{ |
277
|
|
|
// we need to cast the list to array because it might just actually be a doctrine collection |
278
|
|
|
return \in_array($item, (array) $list, true); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* @return false|int|string |
283
|
|
|
*/ |
284
|
|
|
private function getKey($item, $list) |
285
|
|
|
{ |
286
|
|
|
// we need to cast the list to array because it might just actually be a doctrine collection |
287
|
|
|
return \array_search($item, (array) $list, true); |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.