Completed
Push — master ( 3c4315...26495e )
by Nathan
02:22
created

HtmlElement::validDefineHimself()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 3
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 array The already resolved elements */
18
    private $resolved = array();
19
20
    /** @var array The default values of element options */
21
    private $defaults = array(
22
        'parent' => null,
23
        'children' => array(),
24
        'extends' => array(),
25
        'attr' => array(),
26
        'text' => null,
27
        'type' => null,
28
        'class' => Element::class
29
    );
30
31
    /** @var array The options to check to valid a branch */
32
    private $checks = array('parent', 'extends', 'children');
33
34
    /** @var array The mergeable attributes */
35
    private $mergeableAttributes = array('class', 'style');
36
37
    /** @var array The special escaping types */
38
    private $specialEscapingTypes = array('script', 'style');
39
40
    /** @var bool Determine if html is escaped or not */
41
    private $escapeHtml = true;
42
43
    /** @var bool Determine if html attributes are escaped or not */
44
    private $escapeHtmlAttr = true;
45
46
    /** @var bool Determine if javascript is escaped or not */
47
    private $escapeJs = true;
48
49
    /** @var bool Determine if css is escaped or not */
50
    private $escapeCss = true;
51
52
    /** @var bool Determine if urls are escaped or not */
53
    private $escapeUrl = true;
54
55
    /**
56
     * HtmlElement constructor.
57
     *
58
     * @param array                 $map      The elements map
59
     * @param EscaperInterface|null $escaper  The escaper, by default ZendFramework/Escaper is used
60
     * @param string                $encoding The encoding used for escaping, by default utf-8 is used
61
     */
62
    public function __construct(array $map = array(), EscaperInterface $escaper = null, $encoding = 'utf-8')
63
    {
64
        $this->map = $map;
65
        $this->escaper = null !== $escaper ? $escaper : new Escaper($encoding);
66
    }
67
68
    /**
69
     * Create element on static calls.
70
     *
71
     * @param string $type      The element type
72
     * @param array  $arguments The arguments array to set:
73
     *                          [0] = text (string)
74
     *                          [1] = attributes (array)
75
     *                          [2] = children (array)
76
     *
77
     * @return ElementInterface
78
     *
79
     * @throws InvalidArgumentsNumberException If the arguments length is more than 3
80
     */
81
    public static function __callStatic($type, $arguments)
82
    {
83
        switch (count($arguments)) {
84
            case 0:
85
                return self::create($type);
86
            case 1:
87
                return self::create($type, $arguments[0]);
88
            case 2:
89
                return self::create($type, $arguments[0], $arguments[1]);
90
            case 3:
91
                return self::create($type, $arguments[0], $arguments[1], $arguments[2]);
92
            default:
93
                throw new InvalidArgumentsNumberException(sprintf(
94
                    'Maximum numbers of arguments is %d, [%d] given.',
95
                    3,
96
                    count($arguments)
97
                ));
98
        }
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public static function create($type = null, $text = null, array $attributes = array(), array $children = array())
105
    {
106
        $htmlElement = new HtmlElement();
107
108
        $attributes = $htmlElement->escapeAttributes($attributes);
109
110
        foreach ($children as $key => $child) {
111
            $children[$key] = $htmlElement->escape($child);
112
        }
113
114
        return $htmlElement->escape(new Element($type, $text, $attributes, $children));
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function escapeAttributes(array $attributes)
121
    {
122
        if ($this->escapeHtmlAttr || $this->escapeUrl) {
123
            foreach ($attributes as $attr => $value) {
124
                if ('href' == $attr) {
125
                    if ($this->escapeUrl) {
126
                        $value = $this->escaper->escapeUrl($value);
127
                    }
128
                } else {
129
                    if ($this->escapeHtmlAttr) {
130
                        $value = $this->escaper->escapeHtmlAttr($value);
131
                    }
132
                }
133
134
                $attributes[$attr] = $value;
135
            }
136
        }
137
138
        return $attributes;
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function escape(ElementInterface $element)
145
    {
146
        if ($this->escapeHtml && !in_array($element->getType(), $this->specialEscapingTypes)) {
147
            $element->setText($this->escaper->escapeHtml($element->getText()));
148
        }
149
150
        $element->setAttributes($this->escapeAttributes($element->getAttributes()));
151
152
        if ($this->escapeJs && 'script' == $element->getType()) {
153
            $element->setText($this->escaper->escapeJs($element->getText()));
154
        }
155
156
        if ($this->escapeCss && 'style' == $element->getType()) {
157
            $element->setText($this->escaper->escapeCss($element->getText()));
158
        }
159
160
        return $element;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function getMap()
167
    {
168
        return $this->map;
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function setMap(array $map)
175
    {
176
        $this->map = $map;
177
178
        return $this;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function addManyToMap(array $elements)
185
    {
186
        foreach ($elements as $name => $element) {
187
            $this->addOneToMap($name, $element);
188
        }
189
190
        return $this;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function addOneToMap($name, array $element)
197
    {
198
        $this->map[$name] = $element;
199
200
        return $this;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function getEscaper()
207
    {
208
        return $this->escaper;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function setEscaper(EscaperInterface $escaper)
215
    {
216
        $this->escaper = $escaper;
217
218
        return $this;
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224
    public function isEscapeHtml()
225
    {
226
        return $this->escapeHtml;
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    public function setEscapeHtml($escapeHtml = true)
233
    {
234
        $this->escapeHtml = $escapeHtml;
235
236
        return $this;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242
    public function isEscapeHtmlAttr()
243
    {
244
        return $this->escapeHtmlAttr;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function setEscapeHtmlAttr($escapeHtmlAttr = true)
251
    {
252
        $this->escapeHtmlAttr = $escapeHtmlAttr;
253
254
        return $this;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function isEscapeJs()
261
    {
262
        return $this->escapeJs;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function setEscapeJs($escapeJs = true)
269
    {
270
        $this->escapeJs = $escapeJs;
271
272
        return $this;
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    public function isEscapeCss()
279
    {
280
        return $this->escapeCss;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function setEscapeCss($escapeCss = true)
287
    {
288
        $this->escapeCss = $escapeCss;
289
290
        return $this;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function isEscapeUrl()
297
    {
298
        return $this->escapeUrl;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function setEscapeUrl($escapeUrl = true)
305
    {
306
        $this->escapeUrl = $escapeUrl;
307
308
        return $this;
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function load($name, array $parameters = array(), array $attributes = array(), array $children = array())
315
    {
316
        $element = $this->getInstance($name, $parameters, true);
317
318
        $element->addAttributes($this->escapeAttributes($attributes));
319
320
        foreach ($children as $child) {
321
            $element->addChild($this->escape($child));
322
        }
323
324
        return $element;
325
    }
326
327
    /**
328
     * Get the element instance.
329
     *
330
     * @param string $name       The element name
331
     * @param array  $parameters The parameters to replace in element
332
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
333
     *
334
     * @return ElementInterface
335
     *
336
     * @throws InvalidElementException If the current instance doesn't implement ElementInterface
337
     */
338
    private function getInstance($name, array $parameters, $mainCall = false)
339
    {
340
        $element = $this->resolveElement($name, $parameters, $mainCall);
341
342
        $class = $element['class'];
343
        $type = $element['type'];
344
        $text = $element['text'];
345
        $attributes = $element['attr'];
346
347
        $instance = new $class($type, $text, $attributes);
348
349
        if (!$instance instanceof ElementInterface) {
350
            throw new InvalidElementException(sprintf(
351
                'The element "%s" does not implement the %s',
352
                get_class($instance),
353
                ElementInterface::class
354
            ));
355
        }
356
357
        $children = array();
358
        foreach ((array) $element['children'] as $child) {
359
            $children[] = $this->getInstance($child, $parameters);
360
        }
361
362
        $instance->setChildren($children);
363
364
        if (null !== $element['parent']) {
365
            $parent = $this->getInstance($element['parent'], $parameters);
366
367
            $parent->addChild($instance);
368
        }
369
370
        return $this->escape($instance);
371
    }
372
373
    /**
374
     * Get the resolved element representation.
375
     *
376
     * @param string $name       The current element name
377
     * @param array  $parameters The parameters to replace in element
378
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
379
     *
380
     * @return array
381
     */
382
    private function resolveElement($name, array $parameters, $mainCall = false)
383
    {
384
        $current = $this->getCurrentElement($name);
385
386
        $name = $current['name'];
387
388
        if ($this->alreadyResolved($name)) {
389
            return $this->resolved[$name];
390
        }
391
392
        if ($mainCall) {
393
            $this->validBranch($name);
394
        }
395
396
        foreach ($this->defaults as $default => $value) {
397
            if (!isset($current[$default])) {
398
                $current[$default] = $value;
399
            }
400
        }
401
402
        $current = $this->replaceParameters($current, $parameters);
403
404
        foreach ((array) $current['extends'] as $extend) {
405
            $extend = $this->resolveElement($extend, $parameters);
406
            $current = $this->extendElement($extend, $current);
407
        }
408
409
        $this->resolved[$name] = $current;
410
411
        return $current;
412
    }
413
414
    /**
415
     * Check if an element has been already resolved.
416
     *
417
     * @param string $name
418
     *
419
     * @return bool
420
     */
421
    private function alreadyResolved($name)
422
    {
423
        return array_key_exists($name, $this->resolved);
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     */
429
    public function exists($name)
430
    {
431
        return array_key_exists(lcfirst($name), $this->map);
432
    }
433
434
    /**
435
     * Valid the current map branch.
436
     *
437
     * @param string $name     The current element name
438
     * @param array  $circular The array of elements names called in the current branch of map
439
     */
440
    private function validBranch($name, array $circular = array())
441
    {
442
        $current = $this->getCurrentElement($name);
443
444
        $circular[] = $current['name'];
445
446
        $this->validClass($current);
447
448
        foreach ($this->checks as $check) {
449
            if (isset($current[$check])) {
450
                $currentCheck = (array) $current[$check];
451
452
                $this->validDefineHimself($current['name'], $currentCheck, $check);
453
454
                foreach ($currentCheck as $cc) {
455
                    $this->validCircularReferences($current['name'], $cc, $check, $circular);
456
                    $this->validBranch($cc, $circular);
457
                }
458
            }
459
        }
460
    }
461
462
    /**
463
     * Validate the current element class.
464
     *
465
     * @param array $current The current element
466
     *
467
     * @throws InvalidElementException If the current element defines a class which doesn't exist
468
     */
469
    private function validClass(array $current)
470
    {
471
        if (isset($current['class']) && !class_exists($current['class'])) {
472
            throw new InvalidElementException(sprintf(
473
                'The element "%s" define a class which doesn\'t exist.',
474
                $current['name']
475
            ));
476
        }
477
    }
478
479
    /**
480
     * Validate himself references.
481
     *
482
     * @param string $name         The current element name
483
     * @param array  $currentCheck The current check context
484
     * @param string $check        The current check name
485
     *
486
     * @throws InvalidElementException If the current element defines himself as parent, children or extends
487
     */
488
    private function validDefineHimself($name, array $currentCheck, $check)
489
    {
490
        if (in_array($name, $currentCheck)) {
491
            throw new InvalidElementException(sprintf(
492
                'Element "%s" cannot define himself as %s.',
493
                $name,
494
                $check
495
            ));
496
        }
497
    }
498
499
    /**
500
     * Validate circular references.
501
     *
502
     * @param string       $name         The current element name
503
     * @param string|array $currentCheck The current check context
504
     * @param string       $check        The current check name
505
     * @param array        $circular     The names of the previous elements called
506
     *
507
     * @throws InvalidElementException If the current element defines a parent, child or extends which creates circular
508
     *                                 reference
509
     */
510
    private function validCircularReferences($name, $currentCheck, $check, array $circular)
511
    {
512
        if (!is_array($currentCheck) && in_array($currentCheck, $circular)) {
513
            $circular[] = $currentCheck;
514
515
            throw new InvalidElementException(sprintf(
516
                'Element "%s" cannot define "%s" as %s. It\'s a circular reference. [%s]',
517
                $name,
518
                $currentCheck,
519
                $check,
520
                implode(' -> ', $circular)
521
            ));
522
        }
523
    }
524
525
    /**
526
     * Get the current element representation.
527
     *
528
     * @param string $name The element name
529
     *
530
     * @return array
531
     *
532
     * @throws InvalidElementException   If the current element is defined dynamically and doesn't define a name
533
     * @throws UndefinedElementException If the current element doesn't exist
534
     */
535
    private function getCurrentElement($name)
536
    {
537
        if (is_array($name)) {
538
            if (!isset($name['name'])) {
539
                throw new InvalidElementException(sprintf(
540
                    'Elements defined dynamically in parent or children must define a name.'
541
                ));
542
            }
543
544
            return $name;
545
        }
546
547
        if (!$this->exists($name)) {
548
            throw new UndefinedElementException(sprintf('The element with name "%s" does not exist.', $name));
549
        }
550
551
        $current = $this->map[lcfirst($name)];
552
        $current['name'] = $name;
553
554
        return $current;
555
    }
556
557
    /**
558
     * Replace the parameters of the element.
559
     *
560
     * @param array $element    The element with the parameters to replace
561
     * @param array $parameters The array of parameters values
562
     *
563
     * @return array
564
     */
565
    private function replaceParameters(array $element, array $parameters)
566
    {
567
        foreach ($element as $key => $value) {
568
            if (is_array($value)) {
569
                $element[$key] = $this->replaceParameters($value, $parameters);
570
            }
571
572
            if (is_string($value)) {
573
                foreach ($parameters as $parameter => $replace) {
574
                    $value = str_replace('%'.$parameter.'%', $replace, $value);
575
                }
576
577
                $element[$key] = $value;
578
            }
579
        }
580
581
        return $element;
582
    }
583
584
    /**
585
     * Extend element from another one.
586
     *
587
     * @param array $extend  The array of the element to extend
588
     * @param array $current The current element which extends
589
     *
590
     * @return array
591
     */
592
    private function extendElement($extend, $current)
593
    {
594
        $current['class'] = $extend['class'];
595
596
        $current['attr'] = $this->extendAttributes($extend['attr'], $current['attr']);
597
598
        return $current;
599
    }
600
601
    /**
602
     * Extend attributes from another element.
603
     *
604
     * @param array $from The array of attributes to extend
605
     * @param array $to   The array of attributes which extends
606
     *
607
     * @return array
608
     */
609
    private function extendAttributes(array $from, array $to)
610
    {
611
        foreach ($from as $key => $value) {
612
            if (in_array($key, $this->mergeableAttributes) && isset($to[$key])) {
613
                $to[$key] = array_merge((array) $to[$key], (array) $value);
614
            } elseif (!isset($to[$key])) {
615
                $to[$key] = $value;
616
            } elseif (is_array($value)) {
617
                $to[$key] = $this->extendAttributes($value, $to[$key]);
618
            }
619
        }
620
621
        return $to;
622
    }
623
}
624