Completed
Pull Request — master (#478)
by Luc
02:35
created

Calendar   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 8

Importance

Changes 0
Metric Value
wmc 59
lcom 2
cbo 8
dl 0
loc 301
rs 4.08
c 0
b 0
f 0

13 Methods

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