Completed
Pull Request — master (#453)
by
unknown
03:05
created

Organizer   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 521
Duplicated Lines 9.6 %

Coupling/Cohesion

Components 1
Dependencies 30

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 30
dl 50
loc 521
c 0
b 0
f 0
rs 6

34 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 removeAddress() 0 10 2
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 applyAddressRemoved() 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
A hasAddress() 0 4 1

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\AddressRemoved;
18
use CultuurNet\UDB3\Organizer\Events\AddressTranslated;
19
use CultuurNet\UDB3\Organizer\Events\AddressUpdated;
20
use CultuurNet\UDB3\Organizer\Events\ContactPointUpdated;
21
use CultuurNet\UDB3\Organizer\Events\GeoCoordinatesUpdated;
22
use CultuurNet\UDB3\Organizer\Events\LabelAdded;
23
use CultuurNet\UDB3\Organizer\Events\LabelRemoved;
24
use CultuurNet\UDB3\Organizer\Events\LabelsImported;
25
use CultuurNet\UDB3\Organizer\Events\OrganizerCreated;
26
use CultuurNet\UDB3\Organizer\Events\OrganizerCreatedWithUniqueWebsite;
27
use CultuurNet\UDB3\Organizer\Events\OrganizerDeleted;
28
use CultuurNet\UDB3\Organizer\Events\OrganizerImportedFromUDB2;
29
use CultuurNet\UDB3\Organizer\Events\OrganizerUpdatedFromUDB2;
30
use CultuurNet\UDB3\Organizer\Events\TitleTranslated;
31
use CultuurNet\UDB3\Organizer\Events\TitleUpdated;
32
use CultuurNet\UDB3\Organizer\Events\WebsiteUpdated;
33
use CultuurNet\UDB3\Title;
34
use ValueObjects\Web\Url;
35
36
class Organizer extends EventSourcedAggregateRoot implements UpdateableWithCdbXmlInterface, LabelAwareAggregateRoot
37
{
38
    /**
39
     * The actor id.
40
     *
41
     * @var string
42
     */
43
    protected $actorId;
44
45
    /**
46
     * @var Language
47
     */
48
    private $mainLanguage;
49
50
    /**
51
     * @var Url
52
     */
53
    private $website;
54
55
    /**
56
     * @var Title[]
57
     */
58
    private $titles;
59
60
    /**
61
     * @var Address[]|null
62
     */
63
    private $addresses;
64
65
    /**
66
     * @var ContactPoint
67
     */
68
    private $contactPoint;
69
70
    /**
71
     * @var LabelCollection|Label[]
72
     */
73
    private $labels;
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function getAggregateRootId()
79
    {
80
        return $this->actorId;
81
    }
82
83
    public function __construct()
84
    {
85
        // Contact points can be empty, but we only want to start recording
86
        // ContactPointUpdated events as soon as the organizer is updated
87
        // with a non-empty contact point. To enforce this we initialize the
88
        // aggregate state with an empty contact point.
89
        $this->contactPoint = new ContactPoint();
90
        $this->labels = new LabelCollection();
91
    }
92
93
    /**
94
     * Import from UDB2.
95
     *
96
     * @param string $actorId
97
     *   The actor id.
98
     * @param string $cdbXml
99
     *   The cdb xml.
100
     * @param string $cdbXmlNamespaceUri
101
     *   The cdb xml namespace uri.
102
     *
103
     * @return Organizer
104
     *   The actor.
105
     */
106
    public static function importFromUDB2(
107
        $actorId,
108
        $cdbXml,
109
        $cdbXmlNamespaceUri
110
    ) {
111
        $organizer = new static();
112
        $organizer->apply(
113
            new OrganizerImportedFromUDB2(
114
                $actorId,
115
                $cdbXml,
116
                $cdbXmlNamespaceUri
117
            )
118
        );
119
120
        return $organizer;
121
    }
122
123
    /**
124
     * Factory method to create a new Organizer.
125
     *
126
     * @param string $id
127
     * @param Language $mainLanguage
128
     * @param Url $website
129
     * @param Title $title
130
     * @return Organizer
131
     */
132
    public static function create(
133
        $id,
134
        Language $mainLanguage,
135
        Url $website,
136
        Title $title
137
    ) {
138
        $organizer = new self();
139
140
        $organizer->apply(
141
            new OrganizerCreatedWithUniqueWebsite($id, $mainLanguage, $website, $title)
142
        );
143
144
        return $organizer;
145
    }
146
147
    /**
148
     * @inheritdoc
149
     */
150
    public function updateWithCdbXml($cdbXml, $cdbXmlNamespaceUri)
151
    {
152
        $this->apply(
153
            new OrganizerUpdatedFromUDB2(
154
                $this->actorId,
155
                $cdbXml,
156
                $cdbXmlNamespaceUri
157
            )
158
        );
159
    }
160
161
    /**
162
     * @param Url $website
163
     */
164
    public function updateWebsite(Url $website)
165
    {
166
        if (is_null($this->website) || !$this->website->sameValueAs($website)) {
167
            $this->apply(
168
                new WebsiteUpdated(
169
                    $this->actorId,
170
                    $website
171
                )
172
            );
173
        }
174
    }
175
176
    /**
177
     * @param Title $title
178
     * @param Language $language
179
     */
180 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...
181
        Title $title,
182
        Language $language
183
    ) {
184
        if ($this->isTitleChanged($title, $language)) {
185
            if ($language->getCode() !== $this->mainLanguage->getCode()) {
186
                $event = new TitleTranslated(
187
                    $this->actorId,
188
                    $title,
189
                    $language
190
                );
191
            } else {
192
                $event = new TitleUpdated(
193
                    $this->actorId,
194
                    $title
195
                );
196
            }
197
198
            $this->apply($event);
199
        }
200
    }
201
202
    /**
203
     * @param Address $address
204
     * @param Language $language
205
     */
206 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...
207
        Address $address,
208
        Language $language
209
    ) {
210
        if ($this->isAddressChanged($address, $language)) {
211
            if ($language->getCode() !== $this->mainLanguage->getCode()) {
212
                $event = new AddressTranslated(
213
                    $this->actorId,
214
                    $address,
215
                    $language
216
                );
217
            } else {
218
                $event = new AddressUpdated(
219
                    $this->actorId,
220
                    $address
221
                );
222
            }
223
224
            $this->apply($event);
225
        }
226
    }
227
228
    public function removeAddress()
229
    {
230
        if ($this->hasAddress()) {
231
            return;
232
        }
233
234
        $this->apply(
235
            new AddressRemoved($this->actorId)
236
        );
237
    }
238
239
    /**
240
     * @param ContactPoint $contactPoint
241
     */
242
    public function updateContactPoint(ContactPoint $contactPoint)
243
    {
244
        if (!$this->contactPoint->sameAs($contactPoint)) {
245
            $this->apply(
246
                new ContactPointUpdated($this->actorId, $contactPoint)
247
            );
248
        }
249
    }
250
251
    public function updateGeoCoordinates(Coordinates $coordinate)
252
    {
253
        $this->apply(
254
            new GeoCoordinatesUpdated(
255
                $this->actorId,
256
                $coordinate
257
            )
258
        );
259
    }
260
261
    /**
262
     * @inheritdoc
263
     */
264
    public function addLabel(Label $label)
265
    {
266
        if (!$this->labels->contains($label)) {
267
            $this->apply(new LabelAdded($this->actorId, $label));
268
        }
269
    }
270
271
    /**
272
     * @inheritdoc
273
     */
274
    public function removeLabel(Label $label)
275
    {
276
        if ($this->labels->contains($label)) {
277
            $this->apply(new LabelRemoved($this->actorId, $label));
278
        }
279
    }
280
281
    /**
282
     * @param Labels $labels
283
     */
284
    public function importLabels(Labels $labels, Labels $labelsToKeepIfAlreadyOnOrganizer)
285
    {
286
        $convertLabelClass = function (\CultuurNet\UDB3\Model\ValueObject\Taxonomy\Label\Label $label) {
287
            return new Label(
288
                $label->getName()->toString(),
289
                $label->isVisible()
290
            );
291
        };
292
293
        // Convert the imported labels to label collection.
294
        $importLabelsCollection = new LabelCollection(
295
            array_map($convertLabelClass, $labels->toArray())
296
        );
297
298
        // Convert the labels to keep if already applied.
299
        $keepLabelsCollection = new LabelCollection(
300
            array_map($convertLabelClass, $labelsToKeepIfAlreadyOnOrganizer->toArray())
301
        );
302
303
        // What are the added labels?
304
        // Labels which are not inside the internal state but inside the imported labels
305
        $addedLabels = new LabelCollection();
306
        foreach ($importLabelsCollection->asArray() as $label) {
307
            if (!$this->labels->contains($label)) {
308
                $addedLabels = $addedLabels->with($label);
309
            }
310
        }
311
312
        // Fire a LabelsImported for all new labels.
313
        $importLabels = new Labels();
314 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...
315
            $importLabels = $importLabels->with(
316
                new \CultuurNet\UDB3\Model\ValueObject\Taxonomy\Label\Label(
317
                    new LabelName((string) $addedLabel),
318
                    $addedLabel->isVisible()
319
                )
320
            );
321
        }
322
        if ($importLabels->count() > 0) {
323
            $this->apply(new LabelsImported(
324
                $this->actorId,
325
                $importLabels
326
            ));
327
        }
328
329
        // For each added label fire a LabelAdded event.
330
        foreach ($addedLabels->asArray() as $label) {
331
            $this->apply(new LabelAdded($this->actorId, $label));
332
        }
333
334
        // What are the deleted labels?
335
        // Labels which are inside the internal state but not inside imported labels.
336
        // For each deleted label fire a LabelDeleted event.
337
        foreach ($this->labels->asArray() as $label) {
338
            if (!$importLabelsCollection->contains($label) && !$keepLabelsCollection->contains($label)) {
339
                $this->apply(new LabelRemoved($this->actorId, $label));
340
            }
341
        }
342
    }
343
344
    public function delete()
345
    {
346
        $this->apply(
347
            new OrganizerDeleted($this->getAggregateRootId())
348
        );
349
    }
350
351
352
    /**
353
     * Apply the organizer created event.
354
     * @param OrganizerCreated $organizerCreated
355
     */
356
    protected function applyOrganizerCreated(OrganizerCreated $organizerCreated)
357
    {
358
        $this->actorId = $organizerCreated->getOrganizerId();
359
360
        $this->mainLanguage = new Language('nl');
361
362
        $this->setTitle($organizerCreated->getTitle(), $this->mainLanguage);
363
    }
364
365
    /**
366
     * Apply the organizer created event.
367
     * @param OrganizerCreatedWithUniqueWebsite $organizerCreated
368
     */
369
    protected function applyOrganizerCreatedWithUniqueWebsite(OrganizerCreatedWithUniqueWebsite $organizerCreated)
370
    {
371
        $this->actorId = $organizerCreated->getOrganizerId();
372
373
        $this->mainLanguage = $organizerCreated->getMainLanguage();
374
375
        $this->website = $organizerCreated->getWebsite();
376
377
        $this->setTitle($organizerCreated->getTitle(), $this->mainLanguage);
378
    }
379
380
    /**
381
     * @param OrganizerImportedFromUDB2 $organizerImported
382
     * @throws \CultureFeed_Cdb_ParseException
383
     */
384
    protected function applyOrganizerImportedFromUDB2(
385
        OrganizerImportedFromUDB2 $organizerImported
386
    ) {
387
        $this->actorId = (string) $organizerImported->getActorId();
388
389
        // On import from UDB2 the default main language is 'nl'.
390
        $this->mainLanguage = new Language('nl');
391
392
        $actor = ActorItemFactory::createActorFromCdbXml(
393
            $organizerImported->getCdbXmlNamespaceUri(),
394
            $organizerImported->getCdbXml()
395
        );
396
397
        $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...
398
399
        $this->labels = LabelCollection::fromKeywords($actor->getKeywords(true));
400
    }
401
402
    /**
403
     * @param OrganizerUpdatedFromUDB2 $organizerUpdatedFromUDB2
404
     * @throws \CultureFeed_Cdb_ParseException
405
     */
406
    protected function applyOrganizerUpdatedFromUDB2(
407
        OrganizerUpdatedFromUDB2 $organizerUpdatedFromUDB2
408
    ) {
409
        // Note: never change the main language on update from UDB2.
410
411
        $actor = ActorItemFactory::createActorFromCdbXml(
412
            $organizerUpdatedFromUDB2->getCdbXmlNamespaceUri(),
413
            $organizerUpdatedFromUDB2->getCdbXml()
414
        );
415
416
        $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...
417
418
        $this->labels = LabelCollection::fromKeywords($actor->getKeywords(true));
419
    }
420
421
    /**
422
     * @param WebsiteUpdated $websiteUpdated
423
     */
424
    protected function applyWebsiteUpdated(WebsiteUpdated $websiteUpdated)
425
    {
426
        $this->website = $websiteUpdated->getWebsite();
427
    }
428
429
    /**
430
     * @param TitleUpdated $titleUpdated
431
     */
432
    protected function applyTitleUpdated(TitleUpdated $titleUpdated)
433
    {
434
        $this->setTitle($titleUpdated->getTitle(), $this->mainLanguage);
435
    }
436
437
    /**
438
     * @param TitleTranslated $titleTranslated
439
     */
440
    protected function applyTitleTranslated(TitleTranslated $titleTranslated)
441
    {
442
        $this->setTitle($titleTranslated->getTitle(), $titleTranslated->getLanguage());
443
    }
444
445
    /**
446
     * @param AddressUpdated $addressUpdated
447
     */
448
    protected function applyAddressUpdated(AddressUpdated $addressUpdated)
449
    {
450
        $this->setAddress($addressUpdated->getAddress(), $this->mainLanguage);
451
    }
452
453
    /**
454
     * @param AddressRemoved $addressRemoved
455
     */
456
    protected function applyAddressRemoved(AddressRemoved $addressRemoved)
0 ignored issues
show
Unused Code introduced by
The parameter $addressRemoved is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
457
    {
458
        $this->addresses = null;
459
    }
460
461
    /**
462
     * @param AddressTranslated $addressTranslated
463
     */
464
    protected function applyAddressTranslated(AddressTranslated $addressTranslated)
465
    {
466
        $this->setAddress($addressTranslated->getAddress(), $addressTranslated->getLanguage());
467
    }
468
469
    /**
470
     * @param ContactPointUpdated $contactPointUpdated
471
     */
472
    protected function applyContactPointUpdated(ContactPointUpdated $contactPointUpdated)
473
    {
474
        $this->contactPoint = $contactPointUpdated->getContactPoint();
475
    }
476
477
    /**
478
     * @param LabelAdded $labelAdded
479
     */
480
    protected function applyLabelAdded(LabelAdded $labelAdded)
481
    {
482
        $this->labels = $this->labels->with($labelAdded->getLabel());
483
    }
484
485
    /**
486
     * @param LabelRemoved $labelRemoved
487
     */
488
    protected function applyLabelRemoved(LabelRemoved $labelRemoved)
489
    {
490
        $this->labels = $this->labels->without($labelRemoved->getLabel());
491
    }
492
493
    /**
494
     * @param \CultureFeed_Cdb_Item_Actor $actor
495
     * @return null|Title
496
     */
497
    private function getTitle(\CultureFeed_Cdb_Item_Actor $actor)
498
    {
499
        $details = $actor->getDetails();
500
        $details->rewind();
501
502
        // The first language detail found will be used to retrieve
503
        // properties from which in UDB3 are not any longer considered
504
        // to be language specific.
505
        if ($details->valid()) {
506
            return new Title($details->current()->getTitle());
507
        } else {
508
            return null;
509
        }
510
    }
511
512
    /**
513
     * @param Title $title
514
     * @param Language $language
515
     */
516
    private function setTitle(Title $title, Language $language)
517
    {
518
        $this->titles[$language->getCode()] = $title;
519
    }
520
521
    /**
522
     * @param Title $title
523
     * @param Language $language
524
     * @return bool
525
     */
526
    private function isTitleChanged(Title $title, Language $language)
527
    {
528
        return !isset($this->titles[$language->getCode()]) ||
529
            !$title->sameValueAs($this->titles[$language->getCode()]);
530
    }
531
532
    /**
533
     * @param Address $address
534
     * @param Language $language
535
     */
536
    private function setAddress(Address $address, Language $language)
537
    {
538
        $this->addresses[$language->getCode()] = $address;
539
    }
540
541
    /**
542
     * @param Address $address
543
     * @param Language $language
544
     * @return bool
545
     */
546
    private function isAddressChanged(Address $address, Language $language)
547
    {
548
        return !isset($this->addresses[$language->getCode()]) ||
549
            !$address->sameAs($this->addresses[$language->getCode()]);
550
    }
551
552
    private function hasAddress(): bool
553
    {
554
        return $this->addresses === null;
555
    }
556
}
557