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] = text (string|null) |
62
|
|
|
* [1] = attributes (array) |
63
|
|
|
* [2] = parameters (array) |
64
|
|
|
* [3] = extras (array) |
65
|
|
|
* [4] = children (array) |
66
|
|
|
* |
67
|
|
|
* @return ElementInterface |
68
|
|
|
* |
69
|
|
|
* @throws InvalidArgumentsNumberException If the arguments length is more than 3 |
70
|
|
|
*/ |
71
|
|
|
public function __call($name, $arguments) |
72
|
|
|
{ |
73
|
|
|
array_unshift($arguments, $name); |
74
|
|
|
|
75
|
|
|
return $this->load(...$arguments); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* {@inheritdoc} |
80
|
|
|
*/ |
81
|
|
|
public function getMap(): array |
82
|
|
|
{ |
83
|
|
|
return $this->map; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* {@inheritdoc} |
88
|
|
|
*/ |
89
|
|
|
public function setMap(array $map): HtmlElementInterface |
90
|
|
|
{ |
91
|
|
|
$this->map = $map; |
92
|
|
|
|
93
|
|
|
return $this; |
|
|
|
|
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* {@inheritdoc} |
98
|
|
|
*/ |
99
|
|
|
public function addManyToMap(array $elements): HtmlElementInterface |
100
|
|
|
{ |
101
|
|
|
foreach ($elements as $name => $element) { |
102
|
|
|
$this->addOneToMap($name, $element); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
return $this; |
|
|
|
|
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* {@inheritdoc} |
110
|
|
|
*/ |
111
|
|
|
public function addOneToMap(string $name, array $element): HtmlElementInterface |
112
|
|
|
{ |
113
|
|
|
$this->map[$name] = $element; |
114
|
|
|
|
115
|
|
|
return $this; |
|
|
|
|
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* {@inheritdoc} |
120
|
|
|
*/ |
121
|
|
|
public function getBranchValidator(): BranchValidatorInterface |
122
|
|
|
{ |
123
|
|
|
return $this->branchValidator; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* {@inheritdoc} |
128
|
|
|
*/ |
129
|
|
|
public function setBranchValidator(BranchValidatorInterface $branchValidator): HtmlElementInterface |
130
|
|
|
{ |
131
|
|
|
$this->branchValidator = $branchValidator; |
132
|
|
|
|
133
|
|
|
return $this; |
|
|
|
|
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* {@inheritdoc} |
138
|
|
|
*/ |
139
|
|
|
public function getEscaper(): EscaperInterface |
140
|
|
|
{ |
141
|
|
|
return $this->escaper; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* {@inheritdoc} |
146
|
|
|
*/ |
147
|
|
|
public function setEscaper(EscaperInterface $escaper): HtmlElementInterface |
148
|
|
|
{ |
149
|
|
|
$this->escaper = $escaper; |
150
|
|
|
|
151
|
|
|
return $this; |
|
|
|
|
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* {@inheritdoc} |
156
|
|
|
*/ |
157
|
|
|
public function load( |
158
|
|
|
$name, |
159
|
|
|
$text = null, |
160
|
|
|
array $attributes = array(), |
161
|
|
|
array $parameters = array(), |
162
|
|
|
array $extras = array(), |
163
|
|
|
array $children = array() |
164
|
|
|
): ElementInterface |
165
|
|
|
{ |
166
|
|
|
$element = $this->getInstance($name, $text, $extras, $parameters, true); |
167
|
|
|
|
168
|
|
|
$element->addAttributes($this->escaper->escapeAttributes($attributes)); |
169
|
|
|
|
170
|
|
|
foreach ($children as $child) { |
171
|
|
|
$element->addChild($this->escaper->escape($child->getRoot())); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
return $element; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Get the element instance. |
179
|
|
|
* |
180
|
|
|
* @param string|array $name The element name |
181
|
|
|
* @param string|null $text The element text |
182
|
|
|
* @param array $extras The element extras |
183
|
|
|
* @param array $parameters The parameters to replace in element |
184
|
|
|
* @param bool $mainCall Determine if it's the main(first) call of the method |
185
|
|
|
* |
186
|
|
|
* @return ElementInterface |
187
|
|
|
* |
188
|
|
|
* @throws InvalidElementException If the current instance doesn't implement ElementInterface |
189
|
|
|
*/ |
190
|
|
|
private function getInstance( |
191
|
|
|
$name, $text, |
192
|
|
|
array $extras, |
193
|
|
|
array $parameters, |
194
|
|
|
bool $mainCall = false |
195
|
|
|
): ElementInterface |
196
|
|
|
{ |
197
|
|
|
$element = $this->resolveElement($name, $parameters, $mainCall); |
198
|
|
|
|
199
|
|
|
$class = $element['class']; |
200
|
|
|
$type = $element['type']; |
201
|
|
|
$text = $text !== null ? $text : $element['text']; |
202
|
|
|
$attributes = $element['attr']; |
203
|
|
|
|
204
|
|
|
$children = array(); |
205
|
|
|
foreach ((array) $element['children'] as $child) { |
206
|
|
|
$children[] = $this->getInstance($child, null, array(), $parameters); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
$instance = new $class($type, $text, $attributes, $extras, $children); |
210
|
|
|
|
211
|
|
|
if (!$instance instanceof ElementInterface) { |
212
|
|
|
throw new InvalidElementException(sprintf( |
213
|
|
|
'The element "%s" does not implement the %s', |
214
|
|
|
get_class($instance), |
215
|
|
|
ElementInterface::class |
216
|
|
|
)); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
if (null !== $element['parent']) { |
220
|
|
|
$parent = $this->getInstance($element['parent'], null, array(), $parameters); |
221
|
|
|
|
222
|
|
|
$parent->addChild($instance->getRoot()); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
return $this->escaper->escape($instance); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Get the resolved element representation. |
230
|
|
|
* |
231
|
|
|
* @param string|array $name The current element name |
232
|
|
|
* @param array $parameters The parameters to replace in element |
233
|
|
|
* @param bool $mainCall Determine if it's the main(first) call of the method |
234
|
|
|
* |
235
|
|
|
* @return array |
236
|
|
|
*/ |
237
|
|
|
private function resolveElement($name, array $parameters, bool $mainCall = false): array |
238
|
|
|
{ |
239
|
|
|
$current = $this->getCurrentElement($name); |
240
|
|
|
|
241
|
|
|
$name = $current['name']; |
242
|
|
|
|
243
|
|
|
if ($this->alreadyResolved($name)) { |
244
|
|
|
return $this->replaceParameters($this->resolved[$name], $parameters); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
if ($mainCall) { |
248
|
|
|
$this->branchValidator->validateBranch($name); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
foreach ($this->defaults as $default => $value) { |
252
|
|
|
if (!isset($current[$default])) { |
253
|
|
|
$current[$default] = $value; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
foreach ((array) $current['extends'] as $extend) { |
258
|
|
|
$extend = $this->resolveElement($extend, $parameters); |
259
|
|
|
$current = $this->extendElement($extend, $current); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$this->resolved[$name] = $current; |
263
|
|
|
|
264
|
|
|
$current = $this->replaceParameters($current, $parameters); |
265
|
|
|
|
266
|
|
|
return $current; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Check if an element has been already resolved. |
271
|
|
|
* |
272
|
|
|
* @param string $name |
273
|
|
|
* |
274
|
|
|
* @return bool |
275
|
|
|
*/ |
276
|
|
|
private function alreadyResolved(string $name): bool |
277
|
|
|
{ |
278
|
|
|
return array_key_exists($name, $this->resolved); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* {@inheritdoc} |
283
|
|
|
*/ |
284
|
|
|
public function exists(string $name): bool |
285
|
|
|
{ |
286
|
|
|
return array_key_exists(lcfirst($name), $this->map); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Get the current element representation. |
291
|
|
|
* |
292
|
|
|
* @param string|array $name The element name |
293
|
|
|
* |
294
|
|
|
* @return array |
295
|
|
|
* |
296
|
|
|
* @throws InvalidElementException If the current element is defined dynamically and doesn't define a name |
297
|
|
|
* @throws UndefinedElementException If the current element doesn't exist |
298
|
|
|
*/ |
299
|
|
|
public function getCurrentElement($name): array |
300
|
|
|
{ |
301
|
|
|
if (is_array($name)) { |
302
|
|
|
if (!isset($name['name'])) { |
303
|
|
|
throw new InvalidElementException(sprintf( |
304
|
|
|
'Elements defined dynamically in parent or children must define a name.' |
305
|
|
|
)); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $name; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
if (!$this->exists($name)) { |
312
|
|
|
throw new UndefinedElementException(sprintf('The element with name "%s" does not exist.', $name)); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
$current = $this->map[lcfirst($name)]; |
316
|
|
|
$current['name'] = $name; |
317
|
|
|
|
318
|
|
|
return $current; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Replace parameters in text and attr. |
323
|
|
|
* |
324
|
|
|
* @param array $element The element with the parameters to replace |
325
|
|
|
* @param array $parameters The array of parameters values |
326
|
|
|
* |
327
|
|
|
* @return array |
328
|
|
|
*/ |
329
|
|
|
private function replaceParameters(array $element, array $parameters): array |
330
|
|
|
{ |
331
|
|
|
foreach ($parameters as $parameter => $replace) { |
332
|
|
|
$element['text'] = str_replace('%'.$parameter.'%', $replace, (string) $element['text']); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$element['attr'] = $this->replaceParametersInAttributes($element['attr'], $parameters); |
336
|
|
|
|
337
|
|
|
return $element; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Replace parameters in attr. |
342
|
|
|
* |
343
|
|
|
* @param array $attributes The attributes |
344
|
|
|
* @param array $parameters The parameters |
345
|
|
|
* |
346
|
|
|
* @return array |
347
|
|
|
*/ |
348
|
|
|
private function replaceParametersInAttributes(array $attributes, array $parameters): array |
349
|
|
|
{ |
350
|
|
|
foreach ($attributes as $key => $value) { |
351
|
|
|
if (is_array($value)) { |
352
|
|
|
$attributes[$key] = $this->replaceParametersInAttributes($value, $parameters); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
foreach ($parameters as $parameter => $replace) { |
356
|
|
|
if (in_array($key, $this->escaper->getUrlsAttributes()) && $this->escaper->isEscapeUrl()) { |
357
|
|
|
$replace = $this->escaper->escapeUrlParameter($replace); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
$value = str_replace('%'.$parameter.'%', $replace, (string) $value); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
$attributes[$key] = $value; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
return $attributes; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Extend element from another one. |
371
|
|
|
* |
372
|
|
|
* @param array $extend The array of the element to extend |
373
|
|
|
* @param array $current The current element which extends |
374
|
|
|
* |
375
|
|
|
* @return array |
376
|
|
|
*/ |
377
|
|
|
private function extendElement(array $extend, array $current): array |
378
|
|
|
{ |
379
|
|
|
foreach ($this->defaults as $default => $value) { |
380
|
|
|
if (!in_array($default, array('attr', 'children')) && $current[$default] === $value) { |
381
|
|
|
$current[$default] = $extend[$default]; |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
$current['attr'] = $this->extendAttributes($extend['attr'], $current['attr']); |
386
|
|
|
|
387
|
|
|
foreach ($extend['children'] as $child) { |
388
|
|
|
$current['children'][] = $child; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
return $current; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* Extend attributes from another element. |
396
|
|
|
* |
397
|
|
|
* @param array $from The array of attributes to extend |
398
|
|
|
* @param array $to The array of attributes which extends |
399
|
|
|
* |
400
|
|
|
* @return array |
401
|
|
|
*/ |
402
|
|
|
private function extendAttributes(array $from, array $to): array |
403
|
|
|
{ |
404
|
|
|
foreach ($from as $key => $value) { |
405
|
|
|
if (in_array($key, $this->mergeableAttributes) && isset($to[$key])) { |
406
|
|
|
$to[$key] = $this->extendMergeableAttributes($value, $to[$key], $key); |
407
|
|
|
} elseif (!isset($to[$key])) { |
408
|
|
|
$to[$key] = $value; |
409
|
|
|
} elseif (is_array($value)) { |
410
|
|
|
$to[$key] = $this->extendAttributes($value, $to[$key]); |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
return $to; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Extend mergeable attributes from another element. |
419
|
|
|
* |
420
|
|
|
* @param string|array $from The attribute to extend |
421
|
|
|
* @param string|array $to The attribute which extends |
422
|
|
|
* @param string $attr The attribute name |
423
|
|
|
* |
424
|
|
|
* @return string |
425
|
|
|
*/ |
426
|
|
|
private function extendMergeableAttributes($from, $to, string $attr): string |
427
|
|
|
{ |
428
|
|
|
$value = array_merge((array) $to, (array) $from); |
429
|
|
|
|
430
|
|
|
switch ($attr) { |
431
|
|
|
case 'class': |
432
|
|
|
return implode(' ', $value); |
433
|
|
|
case 'style': |
434
|
|
|
return implode('; ', $value); |
435
|
|
|
} |
436
|
|
|
} |
437
|
|
|
} |
438
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.