Completed
Push — master ( 2f5cae...701262 )
by Joschi
03:21
created

RdfaLiteElementProcessor::addProperty()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 20
c 0
b 0
f 0
ccs 9
cts 9
cp 1
rs 9.4285
cc 3
eloc 8
nc 4
nop 4
crap 3
1
<?php
2
3
/**
4
 * rdfa-lite-microdata
5
 *
6
 * @category Jkphl
7
 * @package Jkphl\RdfaLiteMicrodata
8
 * @subpackage Jkphl\RdfaLiteMicrodata\Infrastructure
9
 * @author Joschi Kuphal <[email protected]> / @jkphl
10
 * @copyright Copyright © 2017 Joschi Kuphal <[email protected]> / @jkphl
11
 * @license http://opensource.org/licenses/MIT The MIT License (MIT)
12
 */
13
14
/***********************************************************************************
15
 *  The MIT License (MIT)
16
 *
17
 *  Copyright © 2017 Joschi Kuphal <[email protected]> / @jkphl
18
 *
19
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 *  this software and associated documentation files (the "Software"), to deal in
21
 *  the Software without restriction, including without limitation the rights to
22
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 *  the Software, and to permit persons to whom the Software is furnished to do so,
24
 *  subject to the following conditions:
25
 *
26
 *  The above copyright notice and this permission notice shall be included in all
27
 *  copies or substantial portions of the Software.
28
 *
29
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
31
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
32
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
 ***********************************************************************************/
36
37
namespace Jkphl\RdfaLiteMicrodata\Infrastructure\Parser;
38
39
use Jkphl\RdfaLiteMicrodata\Application\Contract\ElementProcessorInterface;
40
use Jkphl\RdfaLiteMicrodata\Application\Parser\Context;
41
use Jkphl\RdfaLiteMicrodata\Domain\Property\Property;
42
use Jkphl\RdfaLiteMicrodata\Domain\Thing\Thing;
43
use Jkphl\RdfaLiteMicrodata\Domain\Thing\ThingInterface;
44
use Jkphl\RdfaLiteMicrodata\Domain\Vocabulary\Vocabulary;
45
use Jkphl\RdfaLiteMicrodata\Domain\Vocabulary\VocabularyInterface;
46
use Jkphl\RdfaLiteMicrodata\Infrastructure\Exceptions\OutOfBoundsException;
47
use Jkphl\RdfaLiteMicrodata\Infrastructure\Exceptions\RuntimeException;
48
49
/**
50
 * RDFa Lite 1.1 element processor
51
 *
52
 * @package Jkphl\RdfaLiteMicrodata
53
 * @subpackage Jkphl\RdfaLiteMicrodata\Infrastructure
54
 */
55
class RdfaLiteElementProcessor implements ElementProcessorInterface
56
{
57
    /**
58
     * Tag name / attribute map
59
     *
60
     * @var array
61
     */
62
    protected static $tagNameAttributes = [
63
        'META' => 'content',
64
        'AUDIO' => 'src',
65
        'EMBED' => 'src',
66
        'IFRAME' => 'src',
67
        'IMG' => 'src',
68
        'SOURCE' => 'src',
69
        'TRACK' => 'src',
70
        'VIDEO' => 'src',
71
        'A' => 'href',
72
        'AREA' => 'href',
73
        'LINK' => 'href',
74
        'OBJECT' => 'data',
75
        'DATA' => 'value',
76
        'TIME' => 'datetime'
77
    ];
78
79
    /**
80
     * Process a DOM element
81
     *
82
     * @param \DOMElement $element DOM element
83
     * @param Context $context Inherited Context
84
     * @return Context Local context for this element
85
     */
86 8
    public function processElement(\DOMElement $element, Context $context)
87
    {
88
        // Process default vocabulary registrations
89 8
        $context = $this->processVocab($element, $context);
90
91
        // Register vocabulary prefixes
92 8
        $context = $this->processPrefix($element, $context);
93
94
        // Create properties
95 8
        $context = $this->processProperty($element, $context);
96 8
        return $context;
97
    }
98
99
    /**
100
     * Process changes of the default vocabulary
101
     *
102
     * @param \DOMElement $element DOM element
103
     * @param Context $context Inherited Context
104
     * @return Context Local context for this element
105
     */
106 8
    protected function processVocab(\DOMElement $element, Context $context)
107
    {
108 8
        if ($element->hasAttribute('vocab')) {
109 6
            $defaultVocabulary = new Vocabulary($element->getAttribute('vocab'));
110 6
            $context = $context->setDefaultVocabulary($defaultVocabulary);
111 6
        }
112
113 8
        return $context;
114
    }
115
116
    /**
117
     * Process vocabulary prefixes
118
     *
119
     * @param \DOMElement $element DOM element
120
     * @param Context $context Inherited Context
121
     * @return Context Local context for this element
122
     */
123 8
    protected function processPrefix(\DOMElement $element, Context $context)
124
    {
125 8
        if ($element->hasAttribute('prefix')) {
126 6
            $prefixes = preg_split('/\s+/', $element->getAttribute('prefix'));
127 6
            while (count($prefixes)) {
128 6
                $prefix = rtrim(array_shift($prefixes), ':');
129 6
                $uri = array_shift($prefixes);
130 6
                $context = $context->registerVocabulary($prefix, $uri);
131 6
            }
132 6
        }
133
134 8
        return $context;
135
    }
136
137
    /**
138
     * Create properties
139
     *
140
     * @param \DOMElement $element DOM element
141
     * @param Context $context Inherited Context
142
     * @return Context Local context for this element
143
     */
144 8
    protected function processProperty(\DOMElement $element, Context $context)
145
    {
146 8
        $processProperty = $element->hasAttribute('property') && ($context->getParentThing() instanceof ThingInterface);
147 8
        if ($processProperty) {
148 4
            list($prefix, $name) = $this->splitProperty($element->getAttribute('property'));
149 4
            $vocabulary = empty($prefix) ? $context->getDefaultVocabulary() : $context->getVocabulary($prefix);
150 4
            if ($vocabulary instanceof VocabularyInterface) {
151 4
                $context = $this->addProperty($element, $context, $name, $vocabulary);
152 4
            }
153 4
        }
154
155 8
        return $context;
156
    }
157
158
    /**
159
     * Split a property into prefix and name
160
     *
161
     * @param string $property Prefixed property
162
     * @return array Prefix and name
163
     */
164 4
    protected function splitProperty($property)
165
    {
166 4
        $property = explode(':', $property);
167 4
        $name = strval(array_pop($property));
168 4
        $prefix = strval(array_pop($property));
169 4
        return [$prefix, $name];
170
    }
171
172
    /**
173
     * Create properties
174
     *
175
     * @param \DOMElement $element DOM element
176
     * @param Context $context Inherited Context
177
     * @param string $name Property name
178
     * @param VocabularyInterface $vocabulary Property vocabulary
179
     * @return Context Local context for this element
180
     */
181 4
    protected function addProperty(\DOMElement $element, Context $context, $name, VocabularyInterface $vocabulary)
182
    {
183
        // Try to get a resource ID
184 4
        $resourceId = trim($element->getAttribute('resource')) ?: null;
185
186
        // Get the property value
187 4
        $propertyValue = $this->getPropertyValue($element, $context);
188 4
        $property = new Property($name, $vocabulary, $propertyValue, $resourceId);
189
190
        // Add the property to the current parent thing
191 4
        $context->getParentThing()->addProperty($property);
192
193
        // If the property value is a thing
194 4
        if ($propertyValue instanceof ThingInterface) {
195
            // Set the thing as parent thing for nested iterations
196 2
            $context = $context->setParentThing($propertyValue);
197 2
        }
198
199 4
        return $context;
200
    }
201
202
    /**
203
     * Return a property value (type and tag name dependent)
204
     *
205
     * @param \DOMElement $element DOM element
206
     * @param Context $context Context
207
     * @return ThingInterface|string Property value
208
     */
209 4
    protected function getPropertyValue(\DOMElement $element, Context $context)
210
    {
211
        // If the property creates a new type: Return the element itself
212 4
        if ($element->hasAttribute('typeof')) {
213 2
            return $this->getThing(
214 2
                $element->getAttribute('typeof'),
215 2
                trim($element->getAttribute('resource')) ?: null,
216
                $context
217 2
            );
218
        }
219
220
        // Return a string property value
221 4
        return $this->getPropertyStringValue($element);
222
    }
223
224
    /**
225
     * Return a thing by typeof value
226
     *
227
     * @param string $typeof Thing type
228
     * @param string $resourceId Resource ID
229
     * @param Context $context Context
230
     * @return ThingInterface Thing
231
     */
232 8
    protected function getThing($typeof, $resourceId, Context $context)
233
    {
234 8
        $typeof = explode(':', $typeof);
235 8
        $type = array_pop($typeof);
236 8
        $prefix = array_pop($typeof);
237
238 8
        return $this->getThingByPrefixType($prefix, $type, $resourceId, $context);
239
    }
240
241
    /**
242
     * Return a thing by prefix and type
243
     *
244
     * @param string $prefix Prefix
245
     * @param string $type Type
246
     * @param string $resourceId Resource ID
247
     * @param Context $context Context
248
     * @return ThingInterface Thing
249
     * @throws RuntimeException If the default vocabulary is empty
250
     * @throws OutOfBoundsException If the vocabulary prefix is unknown
251
     */
252 8
    protected function getThingByPrefixType($prefix, $type, $resourceId, Context $context)
253
    {
254
        // Determine the vocabulary to use
255
        try {
256 8
            $vocabulary = empty($prefix) ? $context->getDefaultVocabulary() : $context->getVocabulary($prefix);
257 6
            if ($vocabulary instanceof VocabularyInterface) {
258
                // Return a new thing
259 4
                return new Thing($type, $vocabulary, $resourceId);
260
            }
261
262
            // If the default vocabulary is empty
263 2
            if (empty($prefix)) {
264 2
                throw new RuntimeException(
265 2
                    RuntimeException::EMPTY_DEFAULT_VOCABULARY_STR,
266
                    RuntimeException::EMPTY_DEFAULT_VOCABULARY
267 2
                );
268
            }
269 4
        } catch (\Jkphl\RdfaLiteMicrodata\Application\Exceptions\OutOfBoundsException $e) {
270
            // Promote to client level exception
271
        }
272
273 2
        throw new OutOfBoundsException(
274 2
            sprintf(OutOfBoundsException::UNKNOWN_VOCABULARY_PREFIX_STR, $prefix),
275
            OutOfBoundsException::UNKNOWN_VOCABULARY_PREFIX
276 2
        );
277
    }
278
279
    /**
280
     * Return a property value (type and tag name dependent)
281
     *
282
     * @param \DOMElement $element DOM element
283
     * @return ThingInterface|string Property value
284
     */
285 4
    protected function getPropertyStringValue(\DOMElement $element)
286
    {
287 4
        $tagName = strtoupper($element->tagName);
288
289
        // Map to an attribute (if applicable)
290 4
        if (array_key_exists($tagName, self::$tagNameAttributes)) {
291 4
            $value = strval($element->getAttribute(self::$tagNameAttributes[$tagName]));
292 4
            if (($tagName != 'TIME') || !empty($value)) {
293 4
                return $value;
294
            }
295
        }
296
297
        // Return the text content
298
//        trigger_error(sprintf('RDFa Lite 1.1 element processor: Unhandled tag name "%s"', $element->tagName), E_USER_WARNING);
299 4
        return $element->textContent;
300
    }
301
302
    /**
303
     * Process a DOM element's children
304
     *
305
     * @param \DOMElement $element DOM element
306
     * @param Context $context Context
307
     * @return Context Context for children
308
     */
309 8
    public function processElementChildren(\DOMElement $element, Context $context)
310
    {
311
        // Process nested children
312 8
        return $this->processTypeof($element, $context);
313
    }
314
315
    /**
316
     * Create nested children
317
     *
318
     * @param \DOMElement $element DOM element
319
     * @param Context $context Context
320
     * @return Context Context for children
321
     */
322 8
    protected function processTypeof(\DOMElement $element, Context $context)
323
    {
324 8
        if ($element->hasAttribute('typeof') && empty($element->getAttribute('property'))) {
325 8
            $thing = $this->getThing(
326 8
                $element->getAttribute('typeof'),
327 8
                trim($element->getAttribute('resource')) ?: null,
328
                $context
329 8
            );
330
331
            // Add the new thing as a child to the current context
332
            // and set the thing as parent thing for nested iterations
333 4
            $context = $context->addChild($thing)->setParentThing($thing);
334 4
        }
335
336 8
        return $context;
337
    }
338
}
339