Completed
Push — master ( bd434e...913de5 )
by
unknown
34s queued 15s
created

Calendar   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 0
Metric Value
wmc 61
lcom 2
cbo 10
dl 0
loc 315
rs 3.52
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A deserializeDateTime() 0 14 3
B __construct() 0 39 11
A getType() 0 4 1
A serialize() 0 27 5
C deserialize() 0 32 12
A getStartDate() 0 17 5
A getEndDate() 0 17 5
A getOpeningHours() 0 4 1
A getTimestamps() 0 4 1
A getEventStatusType() 0 26 5
B toJsonLd() 0 35 7
A sameAs() 0 4 1
A fromUdb3ModelCalendar() 0 34 4

How to fix   Complexity   

Complex Class

Complex classes like Calendar 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 Calendar, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace CultuurNet\UDB3;
4
5
use Broadway\Serializer\SerializableInterface;
6
use CultuurNet\UDB3\Calendar\OpeningHour;
7
use CultuurNet\UDB3\Event\ValueObjects\EventStatusType;
8
use CultuurNet\UDB3\Model\ValueObject\Calendar\Calendar as Udb3ModelCalendar;
9
use CultuurNet\UDB3\Model\ValueObject\Calendar\CalendarWithDateRange;
10
use CultuurNet\UDB3\Model\ValueObject\Calendar\CalendarWithOpeningHours;
11
use CultuurNet\UDB3\Model\ValueObject\Calendar\CalendarWithSubEvents;
12
use CultuurNet\UDB3\Model\ValueObject\Calendar\DateRange;
13
use CultuurNet\UDB3\Model\ValueObject\Calendar\OpeningHours\OpeningHour as Udb3ModelOpeningHour;
14
use DateTime;
15
use DateTimeInterface;
16
use DateTimeZone;
17
use InvalidArgumentException;
18
19
final class Calendar implements CalendarInterface, JsonLdSerializableInterface, SerializableInterface
20
{
21
    /**
22
     * @var CalendarType
23
     */
24
    protected $type;
25
26
    /**
27
     * @var DateTimeInterface
28
     */
29
    protected $startDate;
30
31
    /**
32
     * @var DateTimeInterface
33
     */
34
    protected $endDate;
35
36
    /**
37
     * @var Timestamp[]
38
     */
39
    protected $timestamps = [];
40
41
    /**
42
     * @var OpeningHour[]
43
     */
44
    protected $openingHours = [];
45
46
    /**
47
     * @param CalendarType $type
48
     * @param DateTimeInterface|null $startDate
49
     * @param DateTimeInterface|null $endDate
50
     * @param Timestamp[] $timestamps
51
     * @param OpeningHour[] $openingHours
52
     */
53
    public function __construct(
54
        CalendarType $type,
55
        ?DateTimeInterface $startDate = null,
56
        ?DateTimeInterface $endDate = null,
57
        array $timestamps = [],
58
        array $openingHours = []
59
    ) {
60
        if (empty($timestamps) && ($type->is(CalendarType::SINGLE()) || $type->is(CalendarType::MULTIPLE()))) {
61
            throw new \UnexpectedValueException('A single or multiple calendar should have timestamps.');
62
        }
63
64
        if (($startDate === null || $endDate === null) && $type->is(CalendarType::PERIODIC())) {
65
            throw new \UnexpectedValueException('A period should have a start- and end-date.');
66
        }
67
68
        foreach ($timestamps as $timestamp) {
69
            if (!is_a($timestamp, Timestamp::class)) {
70
                throw new \InvalidArgumentException('Timestamps should have type TimeStamp.');
71
            }
72
        }
73
74
        foreach ($openingHours as $openingHour) {
75
            if (!is_a($openingHour, OpeningHour::class)) {
76
                throw new \InvalidArgumentException('OpeningHours should have type OpeningHour.');
77
            }
78
        }
79
80
        $this->type = $type->toNative();
81
        $this->startDate = $startDate;
82
        $this->endDate = $endDate;
83
        $this->openingHours = $openingHours;
84
85
        usort($timestamps, function (Timestamp $timestamp, Timestamp $otherTimestamp) {
86
            return $timestamp->getStartDate() <=> $otherTimestamp->getStartDate();
87
        });
88
89
        $this->timestamps = $timestamps;
90
91
    }
92
93
    public function getType(): CalendarType
94
    {
95
        return CalendarType::fromNative($this->type);
96
    }
97
98
    public function serialize(): array
99
    {
100
        $serializedTimestamps = array_map(
101
            function (Timestamp $timestamp) {
102
                return $timestamp->serialize();
103
            },
104
            $this->timestamps
105
        );
106
107
        $serializedOpeningHours = array_map(
108
            function (OpeningHour $openingHour) {
109
                return $openingHour->serialize();
110
            },
111
            $this->openingHours
112
        );
113
114
        $calendar = [
115
            'type' => $this->type,
116
        ];
117
118
        empty($this->startDate) ?: $calendar['startDate'] = $this->startDate->format(DateTime::ATOM);
119
        empty($this->endDate) ?: $calendar['endDate'] = $this->endDate->format(DateTime::ATOM);
120
        empty($serializedTimestamps) ?: $calendar['timestamps'] = $serializedTimestamps;
121
        empty($serializedOpeningHours) ?: $calendar['openingHours'] = $serializedOpeningHours;
122
123
        return $calendar;
124
    }
125
126
    public static function deserialize(array $data): Calendar
127
    {
128
        $calendarType = CalendarType::fromNative($data['type']);
129
130
        // Backwards compatibility for serialized single or multiple calendar types that are missing timestamps but do
131
        // have a start and end date.
132
        $defaultTimeStamps = [];
133
        if ($calendarType->sameValueAs(CalendarType::SINGLE()) || $calendarType->sameValueAs(CalendarType::MULTIPLE())) {
134
            $defaultTimeStampStartDate = !empty($data['startDate']) ? self::deserializeDateTime($data['startDate']) : null;
135
            $defaultTimeStampEndDate = !empty($data['endDate']) ? self::deserializeDateTime($data['endDate']) : $defaultTimeStampStartDate;
136
            $defaultTimeStamp = $defaultTimeStampStartDate && $defaultTimeStampEndDate ? new Timestamp($defaultTimeStampStartDate, $defaultTimeStampEndDate) : null;
137
            $defaultTimeStamps = $defaultTimeStamp ? [$defaultTimeStamp] : [];
138
        }
139
140
        return new self(
141
            $calendarType,
142
            !empty($data['startDate']) ? self::deserializeDateTime($data['startDate']) : null,
143
            !empty($data['endDate']) ? self::deserializeDateTime($data['endDate']) : null,
144
            !empty($data['timestamps']) ? array_map(
145
                function ($timestamp) {
146
                    return Timestamp::deserialize($timestamp);
147
                },
148
                $data['timestamps']
149
            ) : $defaultTimeStamps,
150
            !empty($data['openingHours']) ? array_map(
151
                function ($openingHour) {
152
                    return OpeningHour::deserialize($openingHour);
153
                },
154
                $data['openingHours']
155
            ) : []
156
        );
157
    }
158
159
    /**
160
     * This deserialization function takes into account old data that might be missing a timezone.
161
     * It will fall back to creating a DateTime object and assume Brussels.
162
     * If this still fails an error will be thrown.
163
     */
164
    private static function deserializeDateTime(string $dateTimeData): DateTime
165
    {
166
        $dateTime = DateTime::createFromFormat(DateTime::ATOM, $dateTimeData);
167
168
        if ($dateTime === false) {
169
            $dateTime = DateTime::createFromFormat('Y-m-d\TH:i:s', $dateTimeData, new DateTimeZone('Europe/Brussels'));
170
171
            if (!$dateTime) {
172
                throw new InvalidArgumentException('Invalid date string provided for timestamp, ISO8601 expected!');
173
            }
174
        }
175
176
        return $dateTime;
177
    }
178
179
    public function getStartDate(): ?DateTimeInterface
180
    {
181
        $timestamps = $this->getTimestamps();
182
183
        if (empty($timestamps)) {
184
            return $this->startDate;
185
        }
186
187
        $startDate = null;
188
        foreach ($timestamps as $timestamp) {
189
            if ($startDate === null || $timestamp->getStartDate() < $startDate) {
190
                $startDate = $timestamp->getStartDate();
191
            }
192
        }
193
194
        return $startDate;
195
    }
196
197
    public function getEndDate(): ?DateTimeInterface
198
    {
199
        $timestamps = $this->getTimestamps();
200
201
        if (empty($timestamps)) {
202
            return $this->endDate;
203
        }
204
205
        $endDate = null;
206
        foreach ($this->getTimestamps() as $timestamp) {
207
            if ($endDate === null || $timestamp->getEndDate() > $endDate) {
208
                $endDate = $timestamp->getEndDate();
209
            }
210
        }
211
212
        return $endDate;
213
    }
214
215
    /**
216
     * @return array|OpeningHour[]
217
     */
218
    public function getOpeningHours(): array
219
    {
220
        return $this->openingHours;
221
    }
222
223
    /**
224
     * @return array|Timestamp[]
225
     */
226
    public function getTimestamps(): array
227
    {
228
        return $this->timestamps;
229
    }
230
231
    public function getEventStatusType(): EventStatusType
232
    {
233
        $eventStatusTypeCounts = [];
234
        $eventStatusTypeCounts[EventStatusType::scheduled()->toNative()] = 0;
235
        $eventStatusTypeCounts[EventStatusType::postponed()->toNative()] = 0;
236
        $eventStatusTypeCounts[EventStatusType::cancelled()->toNative()] = 0;
237
238
        foreach ($this->timestamps as $timestamp) {
239
            ++$eventStatusTypeCounts[$timestamp->getEventStatus()->getEventStatusType()->toNative()];
240
        }
241
242
        if ($eventStatusTypeCounts[EventStatusType::scheduled()->toNative()] > 0) {
243
            return EventStatusType::scheduled();
244
        }
245
246
        if ($eventStatusTypeCounts[EventStatusType::postponed()->toNative()] > 0) {
247
            return EventStatusType::postponed();
248
        }
249
250
        if ($eventStatusTypeCounts[EventStatusType::cancelled()->toNative()] > 0) {
251
            return EventStatusType::cancelled();
252
        }
253
254
        // This extra return is needed for events with calendar type of permanent or periodic.
255
        return EventStatusType::scheduled();
256
    }
257
258
    public function toJsonLd(): array
259
    {
260
        $jsonLd = [];
261
262
        $jsonLd['calendarType'] = $this->getType()->toNative();
263
264
        $startDate = $this->getStartDate();
265
        $endDate = $this->getEndDate();
266
        if ($startDate !== null) {
267
            $jsonLd['startDate'] = $startDate->format(DateTime::ATOM);
268
        }
269
        if ($endDate !== null) {
270
            $jsonLd['endDate'] = $endDate->format(DateTime::ATOM);
271
        }
272
273
        $jsonLd['eventStatus'] = $this->getEventStatusType()->toNative();
274
275
        $timestamps = $this->getTimestamps();
276
        if (!empty($timestamps)) {
277
            $jsonLd['subEvent'] = [];
278
            foreach ($timestamps as $timestamp) {
279
                $jsonLd['subEvent'][] = $timestamp->toJsonLd();
280
            }
281
        }
282
283
        $openingHours = $this->getOpeningHours();
284
        if (!empty($openingHours)) {
285
            $jsonLd['openingHours'] = [];
286
            foreach ($openingHours as $openingHour) {
287
                $jsonLd['openingHours'][] = $openingHour->serialize();
288
            }
289
        }
290
291
        return $jsonLd;
292
    }
293
294
    public function sameAs(Calendar $otherCalendar): bool
295
    {
296
        return $this->toJsonLd() === $otherCalendar->toJsonLd();
297
    }
298
299
    public static function fromUdb3ModelCalendar(Udb3ModelCalendar $calendar): Calendar
300
    {
301
        $type = CalendarType::fromNative($calendar->getType()->toString());
302
303
        $startDate = null;
304
        $endDate = null;
305
        $timestamps = [];
306
        $openingHours = [];
307
308
        if ($calendar instanceof CalendarWithDateRange) {
309
            $startDate = $calendar->getStartDate();
310
            $endDate = $calendar->getEndDate();
311
        }
312
313
        if ($calendar instanceof CalendarWithSubEvents) {
314
            $timestamps = array_map(
315
                function (DateRange $dateRange) {
316
                    return Timestamp::fromUdb3ModelDateRange($dateRange);
317
                },
318
                $calendar->getSubEvents()->toArray()
319
            );
320
        }
321
322
        if ($calendar instanceof CalendarWithOpeningHours) {
323
            $openingHours = array_map(
324
                function (Udb3ModelOpeningHour $openingHour) {
325
                    return OpeningHour::fromUdb3ModelOpeningHour($openingHour);
326
                },
327
                $calendar->getOpeningHours()->toArray()
328
            );
329
        }
330
331
        return new self($type, $startDate, $endDate, $timestamps, $openingHours);
332
    }
333
}
334