Completed
Push — master ( 1e3ab4...deb186 )
by Nathan
02:09
created

HtmlElement::replaceParametersInAttributes()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 10
nc 7
nop 2
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);
0 ignored issues
show
Documentation introduced by
$element['parent'] is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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->replaceParameters($this->resolved[$name], $parameters);
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
        foreach ((array) $current['extends'] as $extend) {
257
            $extend = $this->resolveElement($extend, $parameters);
258
            $current = $this->extendElement($extend, $current);
259
        }
260
261
        $this->resolved[$name] = $current;
262
263
        $current = $this->replaceParameters($current, $parameters);
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 parameters in text and attr.
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 ($parameters as $parameter => $replace) {
331
            $element['text'] = str_replace('%'.$parameter.'%', $replace, (string) $element['text']);
332
        }
333
334
        $element['attr'] = $this->replaceParametersInAttributes($element['attr'], $parameters);
335
336
        return $element;
337
    }
338
339
    /**
340
     * Replace parameters in attr.
341
     *
342
     * @param array $attributes The attributes
343
     * @param array $parameters The parameters
344
     *
345
     * @return array
346
     */
347
    private function replaceParametersInAttributes(array $attributes, array $parameters)
348
    {
349
        foreach ($attributes as $key => $value) {
350
            if (is_array($value)) {
351
                $attributes[$key] = $this->replaceParametersInAttributes($value, $parameters);
352
            }
353
354
            foreach ($parameters as $parameter => $replace) {
355
                if (in_array($key, $this->escaper->getUrlsAttributes()) && $this->escaper->isEscapeUrl()) {
356
                    $replace = $this->escaper->escapeUrlParameter($replace);
357
                }
358
359
                $value = str_replace('%'.$parameter.'%', $replace, (string) $value);
360
            }
361
362
            $attributes[$key] = $value;
363
        }
364
365
        return $attributes;
366
    }
367
368
    /**
369
     * Extend element from another one.
370
     *
371
     * @param array $extend  The array of the element to extend
372
     * @param array $current The current element which extends
373
     *
374
     * @return array
375
     */
376
    private function extendElement($extend, $current)
377
    {
378
        foreach ($this->defaults as $default => $value) {
379
            if (!in_array($default, array('attr', 'children')) && $current[$default] === $value) {
380
                $current[$default] = $extend[$default];
381
            }
382
        }
383
384
        $current['attr'] = $this->extendAttributes($extend['attr'], $current['attr']);
385
386
        foreach ($extend['children'] as $child) {
387
            $current['children'][] = $child;
388
        }
389
390
        return $current;
391
    }
392
393
    /**
394
     * Extend attributes from another element.
395
     *
396
     * @param array $from The array of attributes to extend
397
     * @param array $to   The array of attributes which extends
398
     *
399
     * @return array
400
     */
401
    private function extendAttributes(array $from, array $to)
402
    {
403
        foreach ($from as $key => $value) {
404
            if (in_array($key, $this->mergeableAttributes) && isset($to[$key])) {
405
                $to[$key] = $this->extendMergeableAttributes($value, $to[$key], $key);
406
            } elseif (!isset($to[$key])) {
407
                $to[$key] = $value;
408
            } elseif (is_array($value)) {
409
                $to[$key] = $this->extendAttributes($value, $to[$key]);
410
            }
411
        }
412
413
        return $to;
414
    }
415
416
    /**
417
     * Extend mergeable attributes from another element.
418
     *
419
     * @param string|array $from The attribute to extend
420
     * @param string|array $to   The attribute which extends
421
     * @param string       $attr The attribute name
422
     *
423
     * @return string
424
     */
425
    private function extendMergeableAttributes($from, $to, $attr)
426
    {
427
        $value = array_merge((array) $to, (array) $from);
428
429
        switch ($attr) {
430
            case 'class':
431
                return implode(' ', $value);
432
            case 'style':
433
                return implode('; ', $value);
434
        }
435
    }
436
}
437