Completed
Pull Request — master (#436)
by
unknown
03:28
created

Organizer   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 496
Duplicated Lines 10.08 %

Coupling/Cohesion

Components 1
Dependencies 29

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 29
dl 50
loc 496
rs 7.92
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A getAggregateRootId() 0 4 1
A __construct() 0 9 1
A importFromUDB2() 0 16 1
A create() 0 14 1
A updateWithCdbXml() 0 10 1
A updateWebsite() 0 11 3
A updateTitle() 21 21 3
A updateAddress() 21 21 3
A updateContactPoint() 0 8 2
A updateGeoCoordinates() 0 9 1
A addLabel() 0 6 2
A removeLabel() 0 6 2
B importLabels() 8 59 9
A delete() 0 6 1
A applyOrganizerCreated() 0 8 1
A applyOrganizerCreatedWithUniqueWebsite() 0 10 1
A applyOrganizerImportedFromUDB2() 0 17 1
A applyOrganizerUpdatedFromUDB2() 0 14 1
A applyWebsiteUpdated() 0 4 1
A applyTitleUpdated() 0 4 1
A applyTitleTranslated() 0 4 1
A applyAddressUpdated() 0 4 1
A applyAddressTranslated() 0 4 1
A applyContactPointUpdated() 0 4 1
A applyLabelAdded() 0 4 1
A applyLabelRemoved() 0 4 1
A getTitle() 0 14 2
A setTitle() 0 4 1
A isTitleChanged() 0 5 2
A setAddress() 0 4 1
A isAddressChanged() 0 5 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Organizer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Organizer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace CultuurNet\UDB3\Organizer;
4
5
use Broadway\EventSourcing\EventSourcedAggregateRoot;
6
use CultuurNet\Geocoding\Coordinate\Coordinates;
7
use CultuurNet\UDB3\Address\Address;
8
use CultuurNet\UDB3\Cdb\ActorItemFactory;
9
use CultuurNet\UDB3\Cdb\UpdateableWithCdbXmlInterface;
10
use CultuurNet\UDB3\ContactPoint;
11
use CultuurNet\UDB3\Label;
12
use CultuurNet\UDB3\LabelAwareAggregateRoot;
13
use CultuurNet\UDB3\LabelCollection;
14
use CultuurNet\UDB3\Language;
15
use CultuurNet\UDB3\Model\ValueObject\Taxonomy\Label\LabelName;
16
use CultuurNet\UDB3\Model\ValueObject\Taxonomy\Label\Labels;
17
use CultuurNet\UDB3\Organizer\Events\AddressTranslated;
18
use CultuurNet\UDB3\Organizer\Events\AddressUpdated;
19
use CultuurNet\UDB3\Organizer\Events\ContactPointUpdated;
20
use CultuurNet\UDB3\Organizer\Events\GeoCoordinatesUpdated;
21
use CultuurNet\UDB3\Organizer\Events\LabelAdded;
22
use CultuurNet\UDB3\Organizer\Events\LabelRemoved;
23
use CultuurNet\UDB3\Organizer\Events\LabelsImported;
24
use CultuurNet\UDB3\Organizer\Events\OrganizerCreated;
25
use CultuurNet\UDB3\Organizer\Events\OrganizerCreatedWithUniqueWebsite;
26
use CultuurNet\UDB3\Organizer\Events\OrganizerDeleted;
27
use CultuurNet\UDB3\Organizer\Events\OrganizerImportedFromUDB2;
28
use CultuurNet\UDB3\Organizer\Events\OrganizerUpdatedFromUDB2;
29
use CultuurNet\UDB3\Organizer\Events\TitleTranslated;
30
use CultuurNet\UDB3\Organizer\Events\TitleUpdated;
31
use CultuurNet\UDB3\Organizer\Events\WebsiteUpdated;
32
use CultuurNet\UDB3\Title;
33
use ValueObjects\Web\Url;
34
35
class Organizer extends EventSourcedAggregateRoot implements UpdateableWithCdbXmlInterface, LabelAwareAggregateRoot
36
{
37
    /**
38
     * The actor id.
39
     *
40
     * @var string
41
     */
42
    protected $actorId;
43
44
    /**
45
     * @var Language
46
     */
47
    private $mainLanguage;
48
49
    /**
50
     * @var Url
51
     */
52
    private $website;
53
54
    /**
55
     * @var Title[]
56
     */
57
    private $titles;
58
59
    /**
60
     * @var Address[]|null
61
     */
62
    private $addresses;
63
64
    /**
65
     * @var ContactPoint
66
     */
67
    private $contactPoint;
68
69
    /**
70
     * @var LabelCollection|Label[]
71
     */
72
    private $labels;
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function getAggregateRootId()
78
    {
79
        return $this->actorId;
80
    }
81
82
    public function __construct()
83
    {
84
        // Contact points can be empty, but we only want to start recording
85
        // ContactPointUpdated events as soon as the organizer is updated
86
        // with a non-empty contact point. To enforce this we initialize the
87
        // aggregate state with an empty contact point.
88
        $this->contactPoint = new ContactPoint();
89
        $this->labels = new LabelCollection();
90
    }
91
92
    /**
93
     * Import from UDB2.
94
     *
95
     * @param string $actorId
96
     *   The actor id.
97
     * @param string $cdbXml
98
     *   The cdb xml.
99
     * @param string $cdbXmlNamespaceUri
100
     *   The cdb xml namespace uri.
101
     *
102
     * @return Organizer
103
     *   The actor.
104
     */
105
    public static function importFromUDB2(
106
        $actorId,
107
        $cdbXml,
108
        $cdbXmlNamespaceUri
109
    ) {
110
        $organizer = new static();
111
        $organizer->apply(
112
            new OrganizerImportedFromUDB2(
113
                $actorId,
114
                $cdbXml,
115
                $cdbXmlNamespaceUri
116
            )
117
        );
118
119
        return $organizer;
120
    }
121
122
    /**
123
     * Factory method to create a new Organizer.
124
     *
125
     * @param string $id
126
     * @param Language $mainLanguage
127
     * @param Url $website
128
     * @param Title $title
129
     * @return Organizer
130
     */
131
    public static function create(
132
        $id,
133
        Language $mainLanguage,
134
        Url $website,
135
        Title $title
136
    ) {
137
        $organizer = new self();
138
139
        $organizer->apply(
140
            new OrganizerCreatedWithUniqueWebsite($id, $mainLanguage, $website, $title)
141
        );
142
143
        return $organizer;
144
    }
145
146
    /**
147
     * @inheritdoc
148
     */
149
    public function updateWithCdbXml($cdbXml, $cdbXmlNamespaceUri)
150
    {
151
        $this->apply(
152
            new OrganizerUpdatedFromUDB2(
153
                $this->actorId,
154
                $cdbXml,
155
                $cdbXmlNamespaceUri
156
            )
157
        );
158
    }
159
160
    /**
161
     * @param Url $website
162
     */
163
    public function updateWebsite(Url $website)
164
    {
165
        if (is_null($this->website) || !$this->website->sameValueAs($website)) {
166
            $this->apply(
167
                new WebsiteUpdated(
168
                    $this->actorId,
169
                    $website
170
                )
171
            );
172
        }
173
    }
174
175
    /**
176
     * @param Title $title
177
     * @param Language $language
178
     */
179 View Code Duplication
    public function updateTitle(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
180
        Title $title,
181
        Language $language
182
    ) {
183
        if ($this->isTitleChanged($title, $language)) {
184
            if ($language->getCode() !== $this->mainLanguage->getCode()) {
185
                $event = new TitleTranslated(
186
                    $this->actorId,
187
                    $title,
188
                    $language
189
                );
190
            } else {
191
                $event = new TitleUpdated(
192
                    $this->actorId,
193
                    $title
194
                );
195
            }
196
197
            $this->apply($event);
198
        }
199
    }
200
201
    /**
202
     * @param Address $address
203
     * @param Language $language
204
     */
205 View Code Duplication
    public function updateAddress(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
206
        Address $address,
207
        Language $language
208
    ) {
209
        if ($this->isAddressChanged($address, $language)) {
210
            if ($language->getCode() !== $this->mainLanguage->getCode()) {
211
                $event = new AddressTranslated(
212
                    $this->actorId,
213
                    $address,
214
                    $language
215
                );
216
            } else {
217
                $event = new AddressUpdated(
218
                    $this->actorId,
219
                    $address
220
                );
221
            }
222
223
            $this->apply($event);
224
        }
225
    }
226
227
    /**
228
     * @param ContactPoint $contactPoint
229
     */
230
    public function updateContactPoint(ContactPoint $contactPoint)
231
    {
232
        if (!$this->contactPoint->sameAs($contactPoint)) {
233
            $this->apply(
234
                new ContactPointUpdated($this->actorId, $contactPoint)
235
            );
236
        }
237
    }
238
239
    public function updateGeoCoordinates(Coordinates $coordinate)
240
    {
241
        $this->apply(
242
            new GeoCoordinatesUpdated(
243
                $this->actorId,
244
                $coordinate
245
            )
246
        );
247
    }
248
249
    /**
250
     * @inheritdoc
251
     */
252
    public function addLabel(Label $label)
253
    {
254
        if (!$this->labels->contains($label)) {
255
            $this->apply(new LabelAdded($this->actorId, $label));
256
        }
257
    }
258
259
    /**
260
     * @inheritdoc
261
     */
262
    public function removeLabel(Label $label)
263
    {
264
        if ($this->labels->contains($label)) {
265
            $this->apply(new LabelRemoved($this->actorId, $label));
266
        }
267
    }
268
269
    /**
270
     * @param Labels $labels
271
     */
272
    public function importLabels(Labels $labels, Labels $labelsToKeepIfAlreadyOnOrganizer)
273
    {
274
        $convertLabelClass = function (\CultuurNet\UDB3\Model\ValueObject\Taxonomy\Label\Label $label) {
275
            return new Label(
276
                $label->getName()->toString(),
277
                $label->isVisible()
278
            );
279
        };
280
281
        // Convert the imported labels to label collection.
282
        $importLabelsCollection = new LabelCollection(
283
            array_map($convertLabelClass, $labels->toArray())
284
        );
285
286
        // Convert the labels to keep if already applied.
287
        $keepLabelsCollection = new LabelCollection(
288
            array_map($convertLabelClass, $labelsToKeepIfAlreadyOnOrganizer->toArray())
289
        );
290
291
        // What are the added labels?
292
        // Labels which are not inside the internal state but inside the imported labels
293
        $addedLabels = new LabelCollection();
294
        foreach ($importLabelsCollection->asArray() as $label) {
295
            if (!$this->labels->contains($label)) {
296
                $addedLabels = $addedLabels->with($label);
297
            }
298
        }
299
300
        // Fire a LabelsImported for all new labels.
301
        $importLabels = new Labels();
302 View Code Duplication
        foreach ($addedLabels->asArray() as $addedLabel) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
303
            $importLabels = $importLabels->with(
304
                new \CultuurNet\UDB3\Model\ValueObject\Taxonomy\Label\Label(
305
                    new LabelName((string) $addedLabel),
306
                    $addedLabel->isVisible()
307
                )
308
            );
309
        }
310
        if ($importLabels->count() > 0) {
311
            $this->apply(new LabelsImported(
312
                $this->actorId,
313
                $importLabels
314
            ));
315
        }
316
317
        // For each added label fire a LabelAdded event.
318
        foreach ($addedLabels->asArray() as $label) {
319
            $this->apply(new LabelAdded($this->actorId, $label));
320
        }
321
322
        // What are the deleted labels?
323
        // Labels which are inside the internal state but not inside imported labels.
324
        // For each deleted label fire a LabelDeleted event.
325
        foreach ($this->labels->asArray() as $label) {
326
            if (!$importLabelsCollection->contains($label) && !$keepLabelsCollection->contains($label)) {
327
                $this->apply(new LabelRemoved($this->actorId, $label));
328
            }
329
        }
330
    }
331
332
    public function delete()
333
    {
334
        $this->apply(
335
            new OrganizerDeleted($this->getAggregateRootId())
336
        );
337
    }
338
339
    /**
340
     * Apply the organizer created event.
341
     * @param OrganizerCreated $organizerCreated
342
     */
343
    protected function applyOrganizerCreated(OrganizerCreated $organizerCreated)
344
    {
345
        $this->actorId = $organizerCreated->getOrganizerId();
346
347
        $this->mainLanguage = new Language('nl');
348
349
        $this->setTitle($organizerCreated->getTitle(), $this->mainLanguage);
350
    }
351
352
    /**
353
     * Apply the organizer created event.
354
     * @param OrganizerCreatedWithUniqueWebsite $organizerCreated
355
     */
356
    protected function applyOrganizerCreatedWithUniqueWebsite(OrganizerCreatedWithUniqueWebsite $organizerCreated)
357
    {
358
        $this->actorId = $organizerCreated->getOrganizerId();
359
360
        $this->mainLanguage = $organizerCreated->getMainLanguage();
361
362
        $this->website = $organizerCreated->getWebsite();
363
364
        $this->setTitle($organizerCreated->getTitle(), $this->mainLanguage);
365
    }
366
367
    /**
368
     * @param OrganizerImportedFromUDB2 $organizerImported
369
     * @throws \CultureFeed_Cdb_ParseException
370
     */
371
    protected function applyOrganizerImportedFromUDB2(
372
        OrganizerImportedFromUDB2 $organizerImported
373
    ) {
374
        $this->actorId = (string) $organizerImported->getActorId();
375
376
        // On import from UDB2 the default main language is 'nl'.
377
        $this->mainLanguage = new Language('nl');
378
379
        $actor = ActorItemFactory::createActorFromCdbXml(
380
            $organizerImported->getCdbXmlNamespaceUri(),
381
            $organizerImported->getCdbXml()
382
        );
383
384
        $this->setTitle($this->getTitle($actor), $this->mainLanguage);
0 ignored issues
show
Bug introduced by
It seems like $this->getTitle($actor) can be null; however, setTitle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
385
386
        $this->labels = LabelCollection::fromKeywords($actor->getKeywords(true));
387
    }
388
389
    /**
390
     * @param OrganizerUpdatedFromUDB2 $organizerUpdatedFromUDB2
391
     * @throws \CultureFeed_Cdb_ParseException
392
     */
393
    protected function applyOrganizerUpdatedFromUDB2(
394
        OrganizerUpdatedFromUDB2 $organizerUpdatedFromUDB2
395
    ) {
396
        // Note: never change the main language on update from UDB2.
397
398
        $actor = ActorItemFactory::createActorFromCdbXml(
399
            $organizerUpdatedFromUDB2->getCdbXmlNamespaceUri(),
400
            $organizerUpdatedFromUDB2->getCdbXml()
401
        );
402
403
        $this->setTitle($this->getTitle($actor), $this->mainLanguage);
0 ignored issues
show
Bug introduced by
It seems like $this->getTitle($actor) can be null; however, setTitle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
404
405
        $this->labels = LabelCollection::fromKeywords($actor->getKeywords(true));
406
    }
407
408
    /**
409
     * @param WebsiteUpdated $websiteUpdated
410
     */
411
    protected function applyWebsiteUpdated(WebsiteUpdated $websiteUpdated)
412
    {
413
        $this->website = $websiteUpdated->getWebsite();
414
    }
415
416
    /**
417
     * @param TitleUpdated $titleUpdated
418
     */
419
    protected function applyTitleUpdated(TitleUpdated $titleUpdated)
420
    {
421
        $this->setTitle($titleUpdated->getTitle(), $this->mainLanguage);
422
    }
423
424
    /**
425
     * @param TitleTranslated $titleTranslated
426
     */
427
    protected function applyTitleTranslated(TitleTranslated $titleTranslated)
428
    {
429
        $this->setTitle($titleTranslated->getTitle(), $titleTranslated->getLanguage());
430
    }
431
432
    /**
433
     * @param AddressUpdated $addressUpdated
434
     */
435
    protected function applyAddressUpdated(AddressUpdated $addressUpdated)
436
    {
437
        $this->setAddress($addressUpdated->getAddress(), $this->mainLanguage);
438
    }
439
440
    /**
441
     * @param AddressTranslated $addressTranslated
442
     */
443
    protected function applyAddressTranslated(AddressTranslated $addressTranslated)
444
    {
445
        $this->setAddress($addressTranslated->getAddress(), $addressTranslated->getLanguage());
446
    }
447
448
    /**
449
     * @param ContactPointUpdated $contactPointUpdated
450
     */
451
    protected function applyContactPointUpdated(ContactPointUpdated $contactPointUpdated)
452
    {
453
        $this->contactPoint = $contactPointUpdated->getContactPoint();
454
    }
455
456
    /**
457
     * @param LabelAdded $labelAdded
458
     */
459
    protected function applyLabelAdded(LabelAdded $labelAdded)
460
    {
461
        $this->labels = $this->labels->with($labelAdded->getLabel());
462
    }
463
464
    /**
465
     * @param LabelRemoved $labelRemoved
466
     */
467
    protected function applyLabelRemoved(LabelRemoved $labelRemoved)
468
    {
469
        $this->labels = $this->labels->without($labelRemoved->getLabel());
470
    }
471
472
    /**
473
     * @param \CultureFeed_Cdb_Item_Actor $actor
474
     * @return null|Title
475
     */
476
    private function getTitle(\CultureFeed_Cdb_Item_Actor $actor)
477
    {
478
        $details = $actor->getDetails();
479
        $details->rewind();
480
481
        // The first language detail found will be used to retrieve
482
        // properties from which in UDB3 are not any longer considered
483
        // to be language specific.
484
        if ($details->valid()) {
485
            return new Title($details->current()->getTitle());
486
        } else {
487
            return null;
488
        }
489
    }
490
491
    /**
492
     * @param Title $title
493
     * @param Language $language
494
     */
495
    private function setTitle(Title $title, Language $language)
496
    {
497
        $this->titles[$language->getCode()] = $title;
498
    }
499
500
    /**
501
     * @param Title $title
502
     * @param Language $language
503
     * @return bool
504
     */
505
    private function isTitleChanged(Title $title, Language $language)
506
    {
507
        return !isset($this->titles[$language->getCode()]) ||
508
            !$title->sameValueAs($this->titles[$language->getCode()]);
509
    }
510
511
    /**
512
     * @param Address $address
513
     * @param Language $language
514
     */
515
    private function setAddress(Address $address, Language $language)
516
    {
517
        $this->addresses[$language->getCode()] = $address;
518
    }
519
520
    /**
521
     * @param Address $address
522
     * @param Language $language
523
     * @return bool
524
     */
525
    private function isAddressChanged(Address $address, Language $language)
526
    {
527
        return !isset($this->addresses[$language->getCode()]) ||
528
            !$address->sameAs($this->addresses[$language->getCode()]);
529
    }
530
}
531