Completed
Push — master ( 2f2306...4db6d8 )
by Nathan
02:13
created

HtmlElement::load()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
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
     * Load element on dynamic calls.
58
     *
59
     * @param string $name      The element name
60
     * @param array  $arguments The arguments array to set:
61
     *                          [0] = parameters (array)
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 function __call($name, $arguments)
70
    {
71
        switch (count($arguments)) {
72
            case 0:
73
                return $this->load($name);
74
            case 1:
75
                return $this->load($name, $arguments[0]);
76
            case 2:
77
                return $this->load($name, $arguments[0], $arguments[1]);
78
            case 3:
79
                return $this->load($name, $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 function getMap()
93
    {
94
        return $this->map;
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100
    public function setMap(array $map)
101
    {
102
        $this->map = $map;
103
104
        return $this;
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    public function addManyToMap(array $elements)
111
    {
112
        foreach ($elements as $name => $element) {
113
            $this->addOneToMap($name, $element);
114
        }
115
116
        return $this;
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function addOneToMap($name, array $element)
123
    {
124
        $this->map[$name] = $element;
125
126
        return $this;
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function getBranchValidator()
133
    {
134
        return $this->branchValidator;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function setBranchValidator(BranchValidatorInterface $branchValidator)
141
    {
142
        $this->branchValidator = $branchValidator;
143
144
        return $this;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function getEscaper()
151
    {
152
        return $this->escaper;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function setEscaper(EscaperInterface $escaper)
159
    {
160
        $this->escaper = $escaper;
161
162
        return $this;
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function load($name, array $parameters = array(), array $attributes = array(), array $children = array())
169
    {
170
        $element = $this->getInstance($name, $parameters, true);
171
172
        $element->addAttributes($this->escaper->escapeAttributes($attributes));
173
174
        foreach ($children as $child) {
175
            $element->addChild($this->escaper->escape($child));
176
        }
177
178
        return $element;
179
    }
180
181
    /**
182
     * Get the element instance.
183
     *
184
     * @param string $name       The element name
185
     * @param array  $parameters The parameters to replace in element
186
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
187
     *
188
     * @return ElementInterface
189
     *
190
     * @throws InvalidElementException If the current instance doesn't implement ElementInterface
191
     */
192
    private function getInstance($name, array $parameters, $mainCall = false)
193
    {
194
        $element = $this->resolveElement($name, $parameters, $mainCall);
195
196
        $class = $element['class'];
197
        $type = $element['type'];
198
        $text = $element['text'];
199
        $attributes = $element['attr'];
200
201
        $instance = new $class($type, $text, $attributes);
202
203
        if (!$instance instanceof ElementInterface) {
204
            throw new InvalidElementException(sprintf(
205
                'The element "%s" does not implement the %s',
206
                get_class($instance),
207
                ElementInterface::class
208
            ));
209
        }
210
211
        $children = array();
212
        foreach ((array) $element['children'] as $child) {
213
            $children[] = $this->getInstance($child, $parameters);
214
        }
215
216
        $instance->setChildren($children);
217
218
        if (null !== $element['parent']) {
219
            $parent = $this->getInstance($element['parent'], $parameters);
220
221
            $parent->addChild($instance);
222
        }
223
224
        return $this->escaper->escape($instance);
225
    }
226
227
    /**
228
     * Get the resolved element representation.
229
     *
230
     * @param string $name       The current element name
231
     * @param array  $parameters The parameters to replace in element
232
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
233
     *
234
     * @return array
235
     */
236
    private function resolveElement($name, array $parameters, $mainCall = false)
237
    {
238
        $current = $this->getCurrentElement($name);
239
240
        $name = $current['name'];
241
242
        if ($this->alreadyResolved($name)) {
243
            return $this->resolved[$name];
244
        }
245
246
        if ($mainCall) {
247
            $this->branchValidator->validateBranch($name);
248
        }
249
250
        foreach ($this->defaults as $default => $value) {
251
            if (!isset($current[$default])) {
252
                $current[$default] = $value;
253
            }
254
        }
255
256
        $current = $this->replaceParameters($current, $parameters);
257
258
        foreach ((array) $current['extends'] as $extend) {
259
            $extend = $this->resolveElement($extend, $parameters);
260
            $current = $this->extendElement($extend, $current);
261
        }
262
263
        $this->resolved[$name] = $current;
264
265
        return $current;
266
    }
267
268
    /**
269
     * Check if an element has been already resolved.
270
     *
271
     * @param string $name
272
     *
273
     * @return bool
274
     */
275
    private function alreadyResolved($name)
276
    {
277
        return array_key_exists($name, $this->resolved);
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function exists($name)
284
    {
285
        return array_key_exists(lcfirst($name), $this->map);
286
    }
287
288
    /**
289
     * Get the current element representation.
290
     *
291
     * @param string $name The element name
292
     *
293
     * @return array
294
     *
295
     * @throws InvalidElementException   If the current element is defined dynamically and doesn't define a name
296
     * @throws UndefinedElementException If the current element doesn't exist
297
     */
298
    public function getCurrentElement($name)
299
    {
300
        if (is_array($name)) {
301
            if (!isset($name['name'])) {
302
                throw new InvalidElementException(sprintf(
303
                    'Elements defined dynamically in parent or children must define a name.'
304
                ));
305
            }
306
307
            return $name;
308
        }
309
310
        if (!$this->exists($name)) {
311
            throw new UndefinedElementException(sprintf('The element with name "%s" does not exist.', $name));
312
        }
313
314
        $current = $this->map[lcfirst($name)];
315
        $current['name'] = $name;
316
317
        return $current;
318
    }
319
320
    /**
321
     * Replace the parameters of the element.
322
     *
323
     * @param array $element    The element with the parameters to replace
324
     * @param array $parameters The array of parameters values
325
     *
326
     * @return array
327
     */
328
    private function replaceParameters(array $element, array $parameters)
329
    {
330
        foreach ($element as $key => $value) {
331
            if (is_array($value)) {
332
                $element[$key] = $this->replaceParameters($value, $parameters);
333
            }
334
335
            if (is_string($value)) {
336
                foreach ($parameters as $parameter => $replace) {
337
                    if(in_array($key, $this->escaper->getUrlsAttributes()) && $this->escaper->isEscapeUrl()){
0 ignored issues
show
Bug introduced by
The method isEscapeUrl() does not exist on NatePage\EasyHtmlElement\EscaperInterface. Did you maybe mean escape()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
338
                        $replace = $this->escaper->escapeUrl($replace);
0 ignored issues
show
Bug introduced by
The method escapeUrl() does not exist on NatePage\EasyHtmlElement\EscaperInterface. Did you maybe mean escape()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
339
                    }
340
341
                    $value = str_replace('%'.$parameter.'%', $replace, $value);
342
                }
343
344
                $element[$key] = $value;
345
            }
346
        }
347
348
        return $element;
349
    }
350
351
    /**
352
     * Extend element from another one.
353
     *
354
     * @param array $extend  The array of the element to extend
355
     * @param array $current The current element which extends
356
     *
357
     * @return array
358
     */
359
    private function extendElement($extend, $current)
360
    {
361
        $current['class'] = $extend['class'];
362
363
        $current['attr'] = $this->extendAttributes($extend['attr'], $current['attr']);
364
365
        return $current;
366
    }
367
368
    /**
369
     * Extend attributes from another element.
370
     *
371
     * @param array $from The array of attributes to extend
372
     * @param array $to   The array of attributes which extends
373
     *
374
     * @return array
375
     */
376
    private function extendAttributes(array $from, array $to)
377
    {
378
        foreach ($from as $key => $value) {
379
            if (in_array($key, $this->mergeableAttributes) && isset($to[$key])) {
380
                $to[$key] = array_merge((array) $to[$key], (array) $value);
381
            } elseif (!isset($to[$key])) {
382
                $to[$key] = $value;
383
            } elseif (is_array($value)) {
384
                $to[$key] = $this->extendAttributes($value, $to[$key]);
385
            }
386
        }
387
388
        return $to;
389
    }
390
}
391