Evaluator::extract_value()   C
last analyzed

Complexity

Conditions 9
Paths 10

Size

Total Lines 48
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 19
c 3
b 0
f 0
nc 10
nop 3
dl 0
loc 48
rs 5.5102
1
<?php
2
3
/*
4
 * This file is part of the Patron package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Patron;
13
14
use BlueTihi\Context;
15
16
/**
17
 * Evaluate expression relative to a context.
18
 */
19
class Evaluator
20
{
21
	const TOKEN_TYPE = 1;
22
	const TOKEN_TYPE_FUNCTION = 2;
23
	const TOKEN_TYPE_IDENTIFIER = 3;
24
	const TOKEN_VALUE = 4;
25
	const TOKEN_ARGS = 5;
26
	const TOKEN_ARGS_EVALUATE = 6;
27
28
	/**
29
	 * @var Engine
30
	 */
31
	private $engine;
32
33
	/**
34
	 * @param Engine $engine
35
	 */
36
	public function __construct(Engine $engine)
37
	{
38
		$this->engine = $engine;
39
	}
40
41
	/**
42
	 * Evaluate an expression relative to a context.
43
	 *
44
	 * @param mixed $context
45
	 * @param string $expression
46
	 * @param bool $silent `true` to suppress errors, `false` otherwise.
47
	 *
48
	 * @return mixed
49
	 */
50
	public function __invoke($context, $expression, $silent = false)
51
	{
52
		$tokens = $this->tokenize($expression);
53
54
		return $this->evaluate($context, $expression, $tokens, $silent);
55
	}
56
57
	/**
58
	 * Tokenize Javascript style function chain into an array of identifiers and functions
59
	 *
60
	 * @param string $str
61
	 *
62
	 * @return array
63
	 */
64
	protected function tokenize($str)
65
	{
66
		if ($str{0} == '@')
67
		{
68
			$str = 'this.' . substr($str, 1);
69
		}
70
71
		$str .= '.';
72
73
		$length = strlen($str);
74
75
		$quote = null;
76
		$quote_closed = null;
77
		$part = null;
78
		$escape = false;
79
80
		$function = null;
81
		$args = [];
82
		$args_evaluate = [];
83
		$args_count = 0;
84
85
		$parts = [];
86
87
		for ($i = 0 ; $i < $length ; $i++)
88
		{
89
			$c = $str{$i};
90
91
			if ($escape)
92
			{
93
				$part .= $c;
94
95
				$escape = false;
96
97
				continue;
98
			}
99
100
			if ($c == '\\')
101
			{
102
				$escape = true;
103
104
				continue;
105
			}
106
107
			if ($c == '"' || $c == '\'' || $c == '`')
108
			{
109
				if ($quote && $quote == $c)
0 ignored issues
show
Bug Best Practice introduced by
The expression $quote of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
110
				{
111
					$quote = null;
112
					$quote_closed = $c;
113
114
					if ($function)
0 ignored issues
show
Bug Best Practice introduced by
The expression $function of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
115
					{
116
						continue;
117
					}
118
				}
119
				else if (!$quote)
0 ignored issues
show
Bug Best Practice introduced by
The expression $quote of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
120
				{
121
					$quote = $c;
122
123
					if ($function)
0 ignored issues
show
Bug Best Practice introduced by
The expression $function of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
124
					{
125
						continue;
126
					}
127
				}
128
			}
129
130
			if ($quote)
0 ignored issues
show
Bug Best Practice introduced by
The expression $quote of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
131
			{
132
				$part .= $c;
133
134
				continue;
135
			}
136
137
			#
138
			# we are not in a quote
139
			#
140
141
			if ($c == '.')
142
			{
143
				if (strlen($part))
144
				{
145
					$parts[] = [
146
147
						self::TOKEN_TYPE => self::TOKEN_TYPE_IDENTIFIER,
148
						self::TOKEN_VALUE => $part
149
150
					];
151
				}
152
153
				$part = null;
154
155
				continue;
156
			}
157
158
			if ($c == '(')
159
			{
160
				$function = $part;
161
162
				$args = [];
163
				$args_count = 0;
164
165
				$part = null;
166
167
				continue;
168
			}
169
170
			if (($c == ',' || $c == ')') && $function)
0 ignored issues
show
Bug Best Practice introduced by
The expression $function of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
171
			{
172
				if ($part !== null)
173
				{
174
					if ($quote_closed == '`')
175
					{
176
						$args_evaluate[] = $args_count;
177
					}
178
179
					if (!$quote_closed)
0 ignored issues
show
Bug Best Practice introduced by
The expression $quote_closed of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
180
					{
181
						#
182
						# end of an unquoted part.
183
						# it might be an integer, a float, or maybe a constant !
184
						#
185
186
						switch ($part)
187
						{
188
							case 'true':
189
							case 'TRUE':
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
190
							{
191
								$part = true;
192
							}
193
							break;
194
195
							case 'false':
196
							case 'FALSE':
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
197
							{
198
								$part = false;
199
							}
200
							break;
201
202
							case 'null':
203
							case 'NULL':
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
204
							{
205
								$part = null;
206
							}
207
							break;
208
209
							default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
210
							{
211
								if (is_numeric($part))
212
								{
213
									$part = (int) $part;
214
								}
215
								else if (is_float($part))
216
								{
217
									$part = (float) $part;
218
								}
219
								else
220
								{
221
									$part = constant($part);
222
								}
223
							}
224
							break;
225
						}
226
					}
227
228
					$args[] = $part;
229
					$args_count++;
230
231
					$part = null;
232
				}
233
234
				$quote_closed = null;
235
236
				if ($c != ')')
237
				{
238
					continue;
239
				}
240
			}
241
242
			if ($c == ')' && $function)
0 ignored issues
show
Bug Best Practice introduced by
The expression $function of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
243
			{
244
				$parts[] = [
245
246
					self::TOKEN_TYPE => self::TOKEN_TYPE_FUNCTION,
247
					self::TOKEN_VALUE => $function,
248
					self::TOKEN_ARGS => $args,
249
					self::TOKEN_ARGS_EVALUATE => $args_evaluate
250
251
				];
252
253
				continue;
254
			}
255
256
			if ($c == ' ' && $function)
0 ignored issues
show
Bug Best Practice introduced by
The expression $function of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
257
			{
258
				continue;
259
			}
260
261
			$part .= $c;
262
		}
263
264
		return $parts;
265
	}
266
267
	protected function evaluate($context, $expression, $tokens, $silent)
268
	{
269
		$expression_path = [];
270
271
		foreach ($tokens as $i => $part)
272
		{
273
			$identifier = $part[self::TOKEN_VALUE];
274
275
			$expression_path[] = $identifier;
276
277
			switch ($part[self::TOKEN_TYPE])
278
			{
279
				case self::TOKEN_TYPE_IDENTIFIER:
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
280
				{
281
					if (!is_array($context) && !is_object($context))
282
					{
283
						throw new \InvalidArgumentException(\ICanBoogie\format
284
						(
285
							'Unexpected variable type: %type (%value) for %identifier in expression %expression, should be either an array or an object', [
286
287
								'%type' => gettype($context),
288
								'%value' => $context,
289
								'%identifier' => $identifier,
290
								'%expression' => $expression
291
292
							]
293
						));
294
					}
295
296
					$exists = false;
297
					$next_value = $this->extract_value($context, $identifier, $exists);
298
299
					if (!$exists)
300
					{
301
						if ($silent)
302
						{
303
							return null;
304
						}
305
306
						throw new ReferenceError(\ICanBoogie\format('Reference to undefined property %path of expression %expression (defined: :keys) in: :value', [
307
308
							'path' => implode('.', $expression_path),
309
							'expression' => $expression,
310
							'keys' => implode(', ', $context instanceof Context ? $context->keys() : array_keys((array) $context)),
311
							'value' => \ICanBoogie\dump($context)
312
313
						]));
314
					}
315
316
					$context = $next_value;
317
				}
318
				break;
319
320
				case self::TOKEN_TYPE_FUNCTION:
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
321
				{
322
					$method = $identifier;
323
					$args = $part[self::TOKEN_ARGS];
324
					$args_evaluate = $part[self::TOKEN_ARGS_EVALUATE];
325
326
					if ($args_evaluate)
327
					{
328
						$this->engine->error('we should evaluate %eval', [ '%eval' => $args_evaluate ]);
329
					}
330
331
					#
332
					# if value is an object, we check if the object has the method
333
					#
334
335 View Code Duplication
					if (is_object($context) && method_exists($context, $method))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
336
					{
337
						$context = call_user_func_array([ $context, $method ], $args);
338
339
						break;
340
					}
341
342
					#
343
					# well, the object didn't have the method,
344
					# we check internal functions
345
					#
346
347
					$callback = $this->engine->functions->find($method);
348
349
					#
350
					# if no internal function matches, we try string and array functions
351
					# depending on the type of the value
352
					#
353
354
					if (!$callback)
355
					{
356
						if (is_string($context))
357
						{
358 View Code Duplication
							if (function_exists('str' . $method))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
359
							{
360
								$callback = 'str' . $method;
361
							}
362
							else if (function_exists('str_' . $method))
363
							{
364
								$callback = 'str_' . $method;
365
							}
366
						}
367
						else if (is_array($context) || is_object($context))
368
						{
369 View Code Duplication
							if (function_exists('ICanBoogie\array_' . $method))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
							{
371
								$callback = 'ICanBoogie\array_' . $method;
372
							}
373
							else if (function_exists('array_' . $method))
374
							{
375
								$callback = 'array_' . $method;
376
							}
377
						}
378
					}
379
380
					#
381
					# our last hope is to try the function "as is"
382
					#
383
384
					if (!$callback)
0 ignored issues
show
Bug Best Practice introduced by
The expression $callback of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
385
					{
386
						if (function_exists($method))
387
						{
388
							$callback = $method;
389
						}
390
					}
391
392 View Code Duplication
					if (!$callback)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
393
					{
394
						if (is_object($context) && method_exists($context, '__call'))
395
						{
396
							$context = call_user_func_array([ $context, $method ], $args);
397
398
							break;
399
						}
400
					}
401
402
					#
403
					#
404
					#
405
406
					if (!$callback)
407
					{
408
						throw new \Exception(\ICanBoogie\format('Unknown method %method for expression %expression.', [
409
410
							'%method' => $method,
411
							'%expression' => $expression
412
413
						]));
414
					}
415
416
					#
417
					# create evaluation
418
					#
419
420
					array_unshift($args, $context);
421
422
					if (PHP_MAJOR_VERSION > 5 || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 2))
423
					{
424
						if ($callback == 'array_shift')
425
						{
426
							$context = array_shift($context);
427
						}
428
						else
429
						{
430
							$context = call_user_func_array($callback, $args);
431
						}
432
					}
433
					else
434
					{
435
						$context = call_user_func_array($callback, $args);
436
					}
437
				}
438
				break;
439
			}
440
		}
441
442
		return $context;
443
	}
444
445
	/**
446
	 * Extract a value from a container.
447
	 *
448
	 * @param mixed $container A value can be extracted from the following containers, in that
449
	 * order:
450
	 *
451
	 * - An array, where the `$identifier` key exists.
452
	 * - An object implementing the `$identifier` property.
453
	 * - An object implementing `has_property()` which is used to determine if the object
454
	 * implements the property.
455
	 * - An object implementing `ArrayAccess`, where the `$identifier` offset exists.
456
	 * - Finaly, an object implementing `__get()`.
457
	 *
458
	 * @param string $identifier The identifier of the value to extract.
459
	 * @param bool $exists `true` when the value was extracted, `false` otherwise.
460
	 *
461
	 * @return mixed The extracted value.
462
	 */
463
	protected function extract_value($container, $identifier, &$exists=false)
464
	{
465
		$exists = false;
466
467
		# array
468
469
		if (is_array($container))
470
		{
471
			$exists = array_key_exists($identifier, $container);
472
473
			return $exists ? $container[$identifier] : null;
474
		}
475
476
		# object
477
478
		$exists = property_exists($container, $identifier);
479
480
		if ($exists)
481
		{
482
			return $container->$identifier;
483
		}
484
485
		if (method_exists($container, 'has_property'))
486
		{
487
			$exists = $container->has_property($identifier);
488
489
			return $exists ? $container->$identifier : null;
490
		}
491
492
		if ($container instanceof \ArrayAccess)
493
		{
494
			$exists = $container->offsetExists($identifier);
495
496
			if ($exists)
497
			{
498
				return $container[$identifier];
499
			}
500
		}
501
502
		if (method_exists($container, '__get'))
503
		{
504
			$exists = true;
505
506
			return $container->$identifier;
507
		}
508
509
		return null;
510
	}
511
}
512