Post   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

Changes 0
Metric Value
wmc 33
lcom 2
cbo 3
dl 0
loc 250
rs 9.76
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getId() 0 4 1
A getTitle() 0 4 1
A setTitle() 0 8 2
A getSlug() 0 4 1
A postfixSlug() 0 10 2
A getContent() 0 4 1
A setContent() 0 4 1
A getPublishedAt() 0 4 1
A setPublishedAt() 0 9 2
A getAuthorId() 0 4 1
A setAuthorId() 0 4 1
A getComments() 0 4 1
A addComment() 0 7 2
A removeComment() 0 8 2
A getSummary() 0 4 1
A setSummary() 0 4 1
A addTag() 0 8 3
A removeTag() 0 6 2
A getTags() 0 10 2
A clearTags() 0 10 2
A contains() 0 5 1
A getKey() 0 5 1
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
0 ignored issues
show
Documentation Bug introduced by
It seems like $publishedAt instanceof ...ishedAt) : $publishedAt can also be of type object<DateTimeInterface>. However, the property $publishedAt is declared as type object<DateTimeImmutable>. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<Acme\App\Core\Com...\Blog\Domain\Post\Post>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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