Completed
Push — master ( 6bce7f...aeb439 )
by Jonas
15s queued 10s
created

Calendar::deriveStatusTypeFromSubEvents()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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