Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
131 / 131
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
XmlParser
100.00% covered (success)
100.00%
131 / 131
100.00% covered (success)
100.00%
10 / 10
35
100.00% covered (success)
100.00%
1 / 1
 load
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 parseXml
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
4
 parseGenerator
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 parseConferenceColor
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 parseTracks
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 parseEvents
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
9
 parseRecording
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 getFirstXpathContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getListFromSequence
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getSchedule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Helpers\Schedule;
6
7use Carbon\Carbon;
8use SimpleXMLElement;
9
10class XmlParser
11{
12    protected SimpleXMLElement $scheduleXML;
13
14    protected Schedule $schedule;
15
16    public function load(string $xml): bool
17    {
18        $scheduleXML = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOWARNING | LIBXML_NOERROR);
19        if (!$scheduleXML) {
20            return false;
21        }
22
23        $this->scheduleXML = $scheduleXML;
24        $this->parseXml();
25        return true;
26    }
27
28    /**
29     * Parse the predefined XML content
30     *
31     * See also https://c3voc.de/wiki/schedule
32     *
33     * According to https://github.com/voc/schedule/blob/master/validator/xsd/schedule.xml.xsd
34     */
35    protected function parseXml(): void
36    {
37        $version = $this->getFirstXpathContent('version');
38        $generator = $this->parseGenerator($this->scheduleXML);
39        $color = $this->parseConferenceColor($this->scheduleXML);
40        $tracks = $this->parseTracks($this->scheduleXML);
41
42        $conference = new Conference(
43            $this->getFirstXpathContent('conference/title'),
44            $this->getFirstXpathContent('conference/acronym'),
45            $this->getFirstXpathContent('conference/start'),
46            $this->getFirstXpathContent('conference/end'),
47            (int) $this->getFirstXpathContent('conference/days'),
48            $this->getFirstXpathContent('conference/timeslot_duration'),
49            $this->getFirstXpathContent('conference/base_url'),
50            $this->getFirstXpathContent('conference/logo'),
51            $this->getFirstXpathContent('conference/url'),
52            $this->getFirstXpathContent('conference/time_zone_name'),
53            $color,
54            $tracks,
55        );
56
57        $days = [];
58        foreach ($this->scheduleXML->xpath('day') as $day) {
59            $rooms = [];
60
61            foreach ($day->xpath('room') as $roomElement) {
62                $guid = (string) $roomElement->attributes()['guid'];
63                $room = new Room(
64                    (string) $roomElement->attributes()['name'],
65                    !empty($guid) ? $guid : null
66                );
67
68                $events = $this->parseEvents($roomElement->xpath('event'), $room, $tracks);
69                $room->setEvents($events);
70                $rooms[] = $room;
71            }
72
73            $data = $day->attributes();
74            $days[] = new Day(
75                (string) $data['date'],
76                new Carbon((string) $data['start']),
77                new Carbon((string) $data['end']),
78                (int) $data['index'],
79                $rooms
80            );
81        }
82
83        $this->schedule = new Schedule(
84            $version,
85            $conference,
86            $days,
87            $generator
88        );
89    }
90
91    protected function parseGenerator(SimpleXMLElement $scheduleXML): ?ScheduleGenerator
92    {
93        $generatorData = $scheduleXML->xpath('generator');
94        if (!isset($generatorData[0])) {
95            return null;
96        }
97
98        $data = $generatorData[0]->attributes();
99        return new ScheduleGenerator(
100            (string) $data['name'] ?? null,
101            (string) $data['version'] ?? null,
102        );
103    }
104
105    protected function parseConferenceColor(SimpleXMLElement $scheduleXML): ?ConferenceColor
106    {
107        $conferenceColorData = $scheduleXML->xpath('conference/color');
108        if (!isset($conferenceColorData[0])) {
109            return null;
110        }
111
112        $data = collect($conferenceColorData[0]->attributes())->map(fn($value) => (string) $value);
113        $additionalData = $data->collect()->forget(['primary', 'background']);
114        return new ConferenceColor(
115            $data['primary'] ?? null,
116            $data['background'] ?? null,
117            $additionalData->isNotEmpty() ? $additionalData->toArray() : []
118        );
119    }
120
121    /**
122     * @return ConferenceTrack[]
123     */
124    protected function parseTracks(SimpleXMLElement $scheduleXML): array
125    {
126        $tracksData = $scheduleXML->xpath('conference/track');
127        if (!isset($tracksData[0])) {
128            return [];
129        }
130
131        $tracks = [];
132        foreach ($tracksData as $trackData) {
133            $data = $trackData->attributes();
134            $tracks[] = new ConferenceTrack(
135                (string) $data['name'],
136                (string) $data['color'] ?? null,
137                (string) $data['slug'] ?? null,
138            );
139        }
140        return $tracks;
141    }
142
143    /**
144     * @param SimpleXMLElement[] $eventElements
145     * @param ConferenceTrack[] $tracks
146     */
147    protected function parseEvents(array $eventElements, Room $room, array $tracks): array
148    {
149        $events = [];
150
151        foreach ($eventElements as $event) {
152            $persons = $this->getListFromSequence($event, 'persons', 'person', 'id');
153            $links = $this->getListFromSequence($event, 'links', 'link', 'href');
154            $attachments = $this->getListFromSequence($event, 'attachments', 'attachment', 'href');
155
156            $recording = $this->parseRecording($event);
157            $trackName = $this->getFirstXpathContent('track', $event);
158            $track = collect($tracks)->where('name', $trackName)->first() ?: new ConferenceTrack($trackName);
159
160            $events[] = new Event(
161                (string) $event->attributes()['guid'],
162                (int) $event->attributes()['id'],
163                $room,
164                $this->getFirstXpathContent('title', $event),
165                $this->getFirstXpathContent('subtitle', $event),
166                $this->getFirstXpathContent('type', $event),
167                new Carbon($this->getFirstXpathContent('date', $event)),
168                $this->getFirstXpathContent('start', $event),
169                $this->getFirstXpathContent('duration', $event),
170                $this->getFirstXpathContent('abstract', $event),
171                $this->getFirstXpathContent('slug', $event),
172                $track,
173                $this->getFirstXpathContent('logo', $event) ?: null,
174                $persons,
175                $this->getFirstXpathContent('language', $event) ?: null,
176                $this->getFirstXpathContent('description', $event) ?: null,
177                $recording,
178                $links,
179                $attachments,
180                $this->getFirstXpathContent('url', $event) ?: null,
181                $this->getFirstXpathContent('video_download_url', $event) ?: null,
182                $this->getFirstXpathContent('feedback_url', $event) ?: null,
183            );
184        }
185
186        return $events;
187    }
188
189    protected function parseRecording(SimpleXMLElement $event): ?EventRecording
190    {
191        $recordingElement = $event->xpath('recording');
192        if (!isset($recordingElement[0])) {
193            return null;
194        }
195
196        $element = $recordingElement[0];
197        return new EventRecording(
198            $this->getFirstXpathContent('license', $element) ?: '',
199            $this->getFirstXpathContent('optout', $element) != 'false',
200            $this->getFirstXpathContent('url', $element) ?: null,
201            $this->getFirstXpathContent('link', $element) ?: null,
202        );
203    }
204
205    protected function getFirstXpathContent(string $path, ?SimpleXMLElement $xml = null): string
206    {
207        $element = ($xml ?: $this->scheduleXML)->xpath($path);
208
209        return $element ? (string) $element[0] : '';
210    }
211
212    /**
213     * Resolves a list from a sequence of elements
214     */
215    protected function getListFromSequence(
216        SimpleXMLElement $element,
217        string $firstElement,
218        string $secondElement,
219        string $idAttribute
220    ): array {
221        $items = [];
222
223        foreach ($element->xpath($firstElement) as $element) {
224            foreach ($element->xpath($secondElement) as $item) {
225                $items[(string) $item->attributes()[$idAttribute]] = (string) $item;
226            }
227        }
228
229        return $items;
230    }
231
232    public function getSchedule(): Schedule
233    {
234        return $this->schedule;
235    }
236}