Completed
Push — master ( 26495e...cf8e72 )
by Nathan
02:19
created

HtmlElement::validCircularReferences()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 2
nop 4
1
<?php
2
3
namespace NatePage\EasyHtmlElement;
4
5
use NatePage\EasyHtmlElement\Exception\InvalidArgumentsNumberException;
6
use NatePage\EasyHtmlElement\Exception\InvalidElementException;
7
use NatePage\EasyHtmlElement\Exception\UndefinedElementException;
8
9
class HtmlElement implements HtmlElementInterface
10
{
11
    /** @var array */
12
    private $map;
13
14
    /** @var EscaperInterface */
15
    private $escaper;
16
17
    /** @var BranchValidatorInterface */
18
    private $branchValidator;
19
20
    /** @var array The already resolved elements */
21
    private $resolved = array();
22
23
    /** @var array The default values of element options */
24
    private $defaults = array(
25
        'parent' => null,
26
        'children' => array(),
27
        'extends' => array(),
28
        'attr' => array(),
29
        'text' => null,
30
        'type' => null,
31
        'class' => Element::class
32
    );
33
34
    /** @var array The mergeable attributes */
35
    private $mergeableAttributes = array('class', 'style');
36
37
    /**
38
     * HtmlElement constructor.
39
     *
40
     * @param array                         $map             The elements map
41
     * @param BranchValidatorInterface|null $branchValidator The branch validator
42
     * @param EscaperInterface|null         $escaper         The escaper, by default ZendFramework/Escaper is used
43
     * @param string                        $encoding        The encoding used for escaping, by default utf-8 is used
44
     */
45
    public function __construct(
46
        array $map = array(),
47
        BranchValidatorInterface $branchValidator = null,
48
        EscaperInterface $escaper = null,
49
        $encoding = 'utf-8')
50
    {
51
        $this->map = $map;
52
        $this->branchValidator = null !== $branchValidator ? $branchValidator : new BranchValidator($this);
53
        $this->escaper = null !== $escaper ? $escaper : new Escaper($encoding);
54
    }
55
56
    /**
57
     * Create element on static calls.
58
     *
59
     * @param string $type      The element type
60
     * @param array  $arguments The arguments array to set:
61
     *                          [0] = text (string)
62
     *                          [1] = attributes (array)
63
     *                          [2] = children (array)
64
     *
65
     * @return ElementInterface
66
     *
67
     * @throws InvalidArgumentsNumberException If the arguments length is more than 3
68
     */
69
    public static function __callStatic($type, $arguments)
70
    {
71
        switch (count($arguments)) {
72
            case 0:
73
                return self::create($type);
74
            case 1:
75
                return self::create($type, $arguments[0]);
76
            case 2:
77
                return self::create($type, $arguments[0], $arguments[1]);
78
            case 3:
79
                return self::create($type, $arguments[0], $arguments[1], $arguments[2]);
80
            default:
81
                throw new InvalidArgumentsNumberException(sprintf(
82
                    'Maximum numbers of arguments is %d, [%d] given.',
83
                    3,
84
                    count($arguments)
85
                ));
86
        }
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public static function create($type = null, $text = null, array $attributes = array(), array $children = array())
93
    {
94
        $htmlElement = new HtmlElement();
95
        $escaper = $htmlElement->getEscaper();
96
97
        $attributes = $escaper->escapeAttributes($attributes);
98
99
        foreach ($children as $key => $child) {
100
            $children[$key] = $escaper->escape($child);
101
        }
102
103
        return $escaper->escape(new Element($type, $text, $attributes, $children));
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function getMap()
110
    {
111
        return $this->map;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function setMap(array $map)
118
    {
119
        $this->map = $map;
120
121
        return $this;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function addManyToMap(array $elements)
128
    {
129
        foreach ($elements as $name => $element) {
130
            $this->addOneToMap($name, $element);
131
        }
132
133
        return $this;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    public function addOneToMap($name, array $element)
140
    {
141
        $this->map[$name] = $element;
142
143
        return $this;
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function getBranchValidator()
150
    {
151
        return $this->branchValidator;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function setBranchValidator(BranchValidatorInterface $branchValidator)
158
    {
159
        $this->branchValidator = $branchValidator;
160
161
        return $this;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function getEscaper()
168
    {
169
        return $this->escaper;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175
    public function setEscaper(EscaperInterface $escaper)
176
    {
177
        $this->escaper = $escaper;
178
179
        return $this;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function load($name, array $parameters = array(), array $attributes = array(), array $children = array())
186
    {
187
        $element = $this->getInstance($name, $parameters, true);
188
189
        $element->addAttributes($this->escaper->escapeAttributes($attributes));
190
191
        foreach ($children as $child) {
192
            $element->addChild($this->escaper->escape($child));
193
        }
194
195
        return $element;
196
    }
197
198
    /**
199
     * Get the element instance.
200
     *
201
     * @param string $name       The element name
202
     * @param array  $parameters The parameters to replace in element
203
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
204
     *
205
     * @return ElementInterface
206
     *
207
     * @throws InvalidElementException If the current instance doesn't implement ElementInterface
208
     */
209
    private function getInstance($name, array $parameters, $mainCall = false)
210
    {
211
        $element = $this->resolveElement($name, $parameters, $mainCall);
212
213
        $class = $element['class'];
214
        $type = $element['type'];
215
        $text = $element['text'];
216
        $attributes = $element['attr'];
217
218
        $instance = new $class($type, $text, $attributes);
219
220
        if (!$instance instanceof ElementInterface) {
221
            throw new InvalidElementException(sprintf(
222
                'The element "%s" does not implement the %s',
223
                get_class($instance),
224
                ElementInterface::class
225
            ));
226
        }
227
228
        $children = array();
229
        foreach ((array) $element['children'] as $child) {
230
            $children[] = $this->getInstance($child, $parameters);
231
        }
232
233
        $instance->setChildren($children);
234
235
        if (null !== $element['parent']) {
236
            $parent = $this->getInstance($element['parent'], $parameters);
237
238
            $parent->addChild($instance);
239
        }
240
241
        return $this->escaper->escape($instance);
242
    }
243
244
    /**
245
     * Get the resolved element representation.
246
     *
247
     * @param string $name       The current element name
248
     * @param array  $parameters The parameters to replace in element
249
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
250
     *
251
     * @return array
252
     */
253
    private function resolveElement($name, array $parameters, $mainCall = false)
254
    {
255
        $current = $this->getCurrentElement($name);
256
257
        $name = $current['name'];
258
259
        if ($this->alreadyResolved($name)) {
260
            return $this->resolved[$name];
261
        }
262
263
        if ($mainCall) {
264
            $this->branchValidator->validateBranch($name);
265
        }
266
267
        foreach ($this->defaults as $default => $value) {
268
            if (!isset($current[$default])) {
269
                $current[$default] = $value;
270
            }
271
        }
272
273
        $current = $this->replaceParameters($current, $parameters);
274
275
        foreach ((array) $current['extends'] as $extend) {
276
            $extend = $this->resolveElement($extend, $parameters);
277
            $current = $this->extendElement($extend, $current);
278
        }
279
280
        $this->resolved[$name] = $current;
281
282
        return $current;
283
    }
284
285
    /**
286
     * Check if an element has been already resolved.
287
     *
288
     * @param string $name
289
     *
290
     * @return bool
291
     */
292
    private function alreadyResolved($name)
293
    {
294
        return array_key_exists($name, $this->resolved);
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300
    public function exists($name)
301
    {
302
        return array_key_exists(lcfirst($name), $this->map);
303
    }
304
305
    /**
306
     * Get the current element representation.
307
     *
308
     * @param string $name The element name
309
     *
310
     * @return array
311
     *
312
     * @throws InvalidElementException   If the current element is defined dynamically and doesn't define a name
313
     * @throws UndefinedElementException If the current element doesn't exist
314
     */
315
    public function getCurrentElement($name)
316
    {
317
        if (is_array($name)) {
318
            if (!isset($name['name'])) {
319
                throw new InvalidElementException(sprintf(
320
                    'Elements defined dynamically in parent or children must define a name.'
321
                ));
322
            }
323
324
            return $name;
325
        }
326
327
        if (!$this->exists($name)) {
328
            throw new UndefinedElementException(sprintf('The element with name "%s" does not exist.', $name));
329
        }
330
331
        $current = $this->map[lcfirst($name)];
332
        $current['name'] = $name;
333
334
        return $current;
335
    }
336
337
    /**
338
     * Replace the parameters of the element.
339
     *
340
     * @param array $element    The element with the parameters to replace
341
     * @param array $parameters The array of parameters values
342
     *
343
     * @return array
344
     */
345
    private function replaceParameters(array $element, array $parameters)
346
    {
347
        foreach ($element as $key => $value) {
348
            if (is_array($value)) {
349
                $element[$key] = $this->replaceParameters($value, $parameters);
350
            }
351
352
            if (is_string($value)) {
353
                foreach ($parameters as $parameter => $replace) {
354
                    $value = str_replace('%'.$parameter.'%', $replace, $value);
355
                }
356
357
                $element[$key] = $value;
358
            }
359
        }
360
361
        return $element;
362
    }
363
364
    /**
365
     * Extend element from another one.
366
     *
367
     * @param array $extend  The array of the element to extend
368
     * @param array $current The current element which extends
369
     *
370
     * @return array
371
     */
372
    private function extendElement($extend, $current)
373
    {
374
        $current['class'] = $extend['class'];
375
376
        $current['attr'] = $this->extendAttributes($extend['attr'], $current['attr']);
377
378
        return $current;
379
    }
380
381
    /**
382
     * Extend attributes from another element.
383
     *
384
     * @param array $from The array of attributes to extend
385
     * @param array $to   The array of attributes which extends
386
     *
387
     * @return array
388
     */
389
    private function extendAttributes(array $from, array $to)
390
    {
391
        foreach ($from as $key => $value) {
392
            if (in_array($key, $this->mergeableAttributes) && isset($to[$key])) {
393
                $to[$key] = array_merge((array) $to[$key], (array) $value);
394
            } elseif (!isset($to[$key])) {
395
                $to[$key] = $value;
396
            } elseif (is_array($value)) {
397
                $to[$key] = $this->extendAttributes($value, $to[$key]);
398
            }
399
        }
400
401
        return $to;
402
    }
403
}
404