PHPTAL_Php_TalesInternal   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 453
Duplicated Lines 8.61 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 39
loc 453
rs 2.88
c 0
b 0
f 0
wmc 69
lcom 1
cbo 4

16 Methods

Rating   Name   Duplication   Size   Complexity  
A true() 0 4 1
A not() 0 4 1
D path() 4 75 19
A checkExpressionPart() 0 5 1
A string() 0 4 1
F parseString() 31 115 23
A php() 0 4 1
A phptal_internal_php_block() 0 15 2
A exists() 0 6 2
A number() 0 5 2
A json() 0 4 1
A urlencode() 0 4 1
A compileToPHPExpression() 0 8 2
A convertExpressionsToExpression() 0 10 4
A compileToPHPExpressions() 4 30 4
A verifyPHPExpressions() 0 12 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PHPTAL_Php_TalesInternal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PHPTAL_Php_TalesInternal, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * PHPTAL templating engine
4
 *
5
 * PHP Version 5
6
 *
7
 * @category HTML
8
 * @package  PHPTAL
9
 * @author   Laurent Bedubourg <[email protected]>
10
 * @author   Moritz Bechler <[email protected]>
11
 * @author   Kornel Lesiński <[email protected]>
12
 * @license  http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
13
 * @version  SVN: $Id$
14
 * @link     http://phptal.org/
15
 */
16
17
18
/**
19
 * TALES Specification 1.3
20
 *
21
 *      Expression  ::= [type_prefix ':'] String
22
 *      type_prefix ::= Name
23
 *
24
 * Examples:
25
 *
26
 *      a/b/c
27
 *      path:a/b/c
28
 *      nothing
29
 *      path:nothing
30
 *      python: 1 + 2
31
 *      string:Hello, ${username}
32
 *
33
 *
34
 * Builtin Names in Page Templates (for PHPTAL)
35
 *
36
 *      * nothing - special singleton value used by TAL to represent a
37
 *        non-value (e.g. void, None, Nil, NULL).
38
 *
39
 *      * default - special singleton value used by TAL to specify that
40
 *        existing text should not be replaced.
41
 *
42
 *      * repeat - the repeat variables (see RepeatVariable).
43
 *
44
 *
45
 */
46
47
/**
48
 * @package PHPTAL
49
 * @subpackage Php
50
 */
51
class PHPTAL_Php_TalesInternal implements PHPTAL_Tales
52
{
53
    const DEFAULT_KEYWORD = 'new PHPTAL_DefaultKeyword';
54
    const NOTHING_KEYWORD = 'new PHPTAL_NothingKeyword';
55
56
    static public function true($src, $nothrow)
0 ignored issues
show
Unused Code introduced by
The parameter $nothrow is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
57
    {
58
        return 'phptal_true(' . self::compileToPHPExpression($src, true) . ')';
59
    }
60
61
    /**
62
     * not:
63
     *
64
     *      not: Expression
65
     *
66
     * evaluate the expression string (recursively) as a full expression,
67
     * and returns the boolean negation of its value
68
     *
69
     * return boolean based on the following rules:
70
     *
71
     *     1. integer 0 is false
72
     *     2. integer > 0 is true
73
     *     3. an empty string or other sequence is false
74
     *     4. a non-empty string or other sequence is true
75
     *     5. a non-value (e.g. void, None, Nil, NULL, etc) is false
76
     *     6. all other values are implementation-dependent.
77
     *
78
     * Examples:
79
     *
80
     *      not: exists: foo/bar/baz
81
     *      not: php: object.hasChildren()
82
     *      not: string:${foo}
83
     *      not: foo/bar/booleancomparable
84
     */
85
    static public function not($expression, $nothrow)
86
    {
87
        return '!phptal_true(' . self::compileToPHPExpression($expression, $nothrow) . ')';
88
    }
89
90
91
    /**
92
     * path:
93
     *
94
     *      PathExpr  ::= Path [ '|' Path ]*
95
     *      Path      ::= variable [ '/' URL_Segment ]*
96
     *      variable  ::= Name
97
     *
98
     * Examples:
99
     *
100
     *      path: username
101
     *      path: user/name
102
     *      path: object/method/10/method/member
103
     *      path: object/${dynamicmembername}/method
104
     *      path: maybethis | path: maybethat | path: default
105
     *
106
     * PHPTAL:
107
     *
108
     * 'default' may lead to some 'difficult' attributes implementation
109
     *
110
     * For example, the tal:content will have to insert php code like:
111
     *
112
     * if (isset($ctx->maybethis)) {
113
     *     echo $ctx->maybethis;
114
     * }
115
     * elseif (isset($ctx->maybethat) {
116
     *     echo $ctx->maybethat;
117
     * }
118
     * else {
119
     *     // process default tag content
120
     * }
121
     *
122
     * @returns string or array
123
     */
124
    static public function path($expression, $nothrow=false)
125
    {
126
        $expression = trim($expression);
127
        if ($expression == 'default') return self::DEFAULT_KEYWORD;
128
        if ($expression == 'nothing') return self::NOTHING_KEYWORD;
129
        if ($expression == '')        return self::NOTHING_KEYWORD;
130
131
        // split OR expressions terminated by a string
132
        if (preg_match('/^(.*?)\s*\|\s*?(string:.*)$/sm', $expression, $m)) {
133
            list(, $expression, $string) = $m;
134
        }
135
        // split OR expressions terminated by a 'fast' string
136 View Code Duplication
        elseif (preg_match('/^(.*?)\s*\|\s*\'((?:[^\'\\\\]|\\\\.)*)\'\s*$/sm', $expression, $m)) {
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...
137
            list(, $expression, $string) = $m;
138
            $string = 'string:'.stripslashes($string);
139
        }
140
141
        // split OR expressions
142
        $exps = preg_split('/\s*\|\s*/sm', $expression);
143
144
        // if (many expressions) or (expressions or terminating string) found then
145
        // generate the array of sub expressions and return it.
146
        if (count($exps) > 1 || isset($string)) {
147
            $result = array();
148
            foreach ($exps as $i=>$exp) {
149
                if(isset($string) || $i < count($exps) - 1) {
150
                    $result[] = self::compileToPHPExpressions(trim($exp), true);
151
                }
152
                else {
153
                    // the last expression can thorw exception.
154
                    $result[] = self::compileToPHPExpressions(trim($exp), false);
155
                }
156
            }
157
            if (isset($string)) {
158
                $result[] = self::compileToPHPExpressions($string, true);
159
            }
160
            return $result;
161
        }
162
163
164
        // see if there are subexpressions, but skip interpolated parts, i.e. ${a/b}/c is 2 parts
165
        if (preg_match('/^((?:[^$\/]+|\$\$|\${[^}]+}|\$))\/(.+)$/s', $expression, $m))
166
        {
167
            if (!self::checkExpressionPart($m[1])) {
168
                throw new PHPTAL_ParserException("Invalid TALES path: '$expression', expected '{$m[1]}' to be variable name");
169
            }
170
171
            $next = self::string($m[1]);
172
            $expression = self::string($m[2]);
173
        } else {
174
            if (!self::checkExpressionPart($expression)) {
175
                throw new PHPTAL_ParserException("Invalid TALES path: '$expression', expected variable name. Complex expressions need php: modifier.");
176
            }
177
178
            $next = self::string($expression);
179
            $expression = null;
180
        }
181
182
        if ($nothrow) {
183
            return '$ctx->path($ctx, ' . $next . ($expression === null ? '' : '."/".'.$expression) . ', true)';
184
        }
185
186
        if (preg_match('/^\'[a-z][a-z0-9_]*\'$/i', $next)) $next = substr($next, 1, -1); else $next = '{'.$next.'}';
187
188
        // if no sub part for this expression, just optimize the generated code
189
        // and access the $ctx->var
190
        if ($expression === null) {
191
            return '$ctx->'.$next;
192
        }
193
194
        // otherwise we have to call PHPTAL_Context::path() to resolve the path at runtime
195
        // extract the first part of the expression (it will be the PHPTAL_Context::path()
196
        // $base and pass the remaining of the path to PHPTAL_Context::path()
197
        return '$ctx->path($ctx->'.$next.', '.$expression.')';
198
    }
199
200
    /**
201
     * check if part of exprssion (/foo/ or /foo${bar}/) is alphanumeric
202
     */
203
    private static function checkExpressionPart($expression)
204
    {
205
        $expression = preg_replace('/\${[^}]+}/', 'a', $expression); // pretend interpolation is done
206
        return preg_match('/^[a-z_][a-z0-9_]*$/i', $expression);
207
    }
208
209
    /**
210
     * string:
211
     *
212
     *      string_expression ::= ( plain_string | [ varsub ] )*
213
     *      varsub            ::= ( '$' Path ) | ( '${' Path '}' )
214
     *      plain_string      ::= ( '$$' | non_dollar )*
215
     *      non_dollar        ::= any character except '$'
216
     *
217
     * Examples:
218
     *
219
     *      string:my string
220
     *      string:hello, $username how are you
221
     *      string:hello, ${user/name}
222
     *      string:you have $$130 in your bank account
223
     */
224
    static public function string($expression, $nothrow=false)
225
    {
226
        return self::parseString($expression, $nothrow, '');
227
    }
228
229
    /**
230
     * @param string $tales_prefix prefix added to all TALES in the string
231
     */
232
    static public function parseString($expression, $nothrow, $tales_prefix)
0 ignored issues
show
Unused Code introduced by
The parameter $nothrow is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
233
    {
234
        // This is a simple parser which evaluates ${foo} inside
235
        // 'string:foo ${foo} bar' expressions, it returns the php code which will
236
        // print the string with correct interpollations.
237
        // Nothing special there :)
238
239
        $inPath = false;
240
        $inAccoladePath = false;
241
        $lastWasDollar = false;
242
        $result = '';
243
        $len = strlen($expression);
244
        for ($i=0; $i<$len; $i++) {
245
            $c = $expression[$i];
246
            switch ($c) {
247 View Code Duplication
                case '$':
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...
248
                    if ($lastWasDollar) {
249
                        $lastWasDollar = false;
250
                    } elseif ($inAccoladePath) {
251
                        $subPath .= $c;
0 ignored issues
show
Bug introduced by
The variable $subPath does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
252
                        $c = '';
253
                    } else {
254
                        $lastWasDollar = true;
255
                        $c = '';
256
                    }
257
                    break;
258
259
                case '\\':
260
                    if ($inAccoladePath) {
261
                        $subPath .= $c;
262
                        $c = '';
263
                    }
264
                    else {
265
                        $c = '\\\\';
266
                    }
267
                    break;
268
269
                case '\'':
270
                    if ($inAccoladePath) {
271
                        $subPath .= $c;
272
                        $c = '';
273
                    }
274
                    else {
275
                        $c = '\\\'';
276
                    }
277
                    break;
278
279 View Code Duplication
                case '{':
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...
280
                    if ($inAccoladePath) {
281
                        $subPath .= $c;
282
                        $c = '';
283
                    } elseif ($lastWasDollar) {
284
                        $lastWasDollar = false;
285
                        $inAccoladePath = true;
286
                        $subPath = '';
287
                        $c = '';
288
                    }
289
                    break;
290
291
                case '}':
292
                    if ($inAccoladePath) {
293
                        $inAccoladePath = false;
294
                        $subEval = self::compileToPHPExpression($tales_prefix.$subPath,false);
295
                        $result .= "'.(" . $subEval . ").'";
296
                        $subPath = '';
297
                        $lastWasDollar = false;
298
                        $c = '';
299
                    }
300
                    break;
301
302
                default:
303
                    if ($lastWasDollar) {
304
                        $lastWasDollar = false;
305
                        $inPath = true;
306
                        $subPath = $c;
307
                        $c = '';
308
                    } elseif ($inAccoladePath) {
309
                        $subPath .= $c;
310
                        $c = '';
311
                    } elseif ($inPath) {
312
                        $t = strtolower($c);
313
                        if (($t >= 'a' && $t <= 'z') || ($t >= '0' && $t <= '9') || ($t == '_')) {
314
                            $subPath .= $c;
315
                            $c = '';
316 View Code Duplication
                        } else {
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...
317
                            $inPath = false;
318
                            $subEval = self::compileToPHPExpression($tales_prefix.$subPath,false);
319
                            $result .= "'.(" . $subEval . ").'";
320
                            }
321
                        }
322
                    break;
323
            }
324
            $result .= $c;
325
        }
326 View Code Duplication
        if ($inPath) {
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...
327
            $subEval = self::compileToPHPExpression($tales_prefix.$subPath, false);
328
            $result .= "'.(" . $subEval . ").'";
329
        }
330
331
        // optimize ''.foo.'' to foo
332
        $result = preg_replace("/^(?:''\.)?(.*?)(?:\.'')?$/", '\1', '\''.$result.'\'');
333
334
        /*
335
            The following expression (with + in first alternative):
336
            "/^\(((?:[^\(\)]+|\([^\(\)]*\))*)\)$/"
337
338
            did work properly for (aaaaaaa)aa, but not for (aaaaaaaaaaaaaaaaaaaaa)aa
339
            WTF!?
340
        */
341
342
        // optimize (foo()) to foo()
343
        $result = preg_replace("/^\(((?:[^\(\)]|\([^\(\)]*\))*)\)$/", '\1', $result);
344
345
        return $result;
346
    }
347
348
    /**
349
     * php: modifier.
350
     *
351
     * Transform the expression into a regular PHP expression.
352
     */
353
    static public function php($src)
354
    {
355
        return PHPTAL_Php_Transformer::transform($src, '$ctx->');
356
    }
357
358
    /**
359
     * phptal-internal-php-block: modifier for emulation of <?php ?> in attributes.
360
     *
361
     * Please don't use it in the templates!
362
     */
363
    static public function phptal_internal_php_block($src)
364
    {
365
        $src = rawurldecode($src);
366
367
        // Simple echo can be supported via regular method
368
        if (preg_match('/^\s*echo\s+((?:[^;]+|"[^"\\\\]*"|\'[^\'\\\\]*\'|\/\*.*?\*\/)+);*\s*$/s',$src,$m))
369
        {
370
            return $m[1];
371
        }
372
373
        // <?php block expects statements, but modifiers must return expressions.
374
        // unfortunately this ugliness is the only way to support it currently.
375
        // ? > keeps semicolon optional
376
        return "eval(".self::string($src.'?>').")";
377
    }
378
379
    /**
380
     * exists: modifier.
381
     *
382
     * Returns the code required to invoke Context::exists() on specified path.
383
     */
384
    static public function exists($src, $nothrow)
0 ignored issues
show
Unused Code introduced by
The parameter $nothrow is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
385
    {
386
        $src = trim($src);
387
        if (ctype_alnum($src)) return 'isset($ctx->'.$src.')';
388
        return '(null !== ' . self::compileToPHPExpression($src, true) . ')';
389
    }
390
391
    /**
392
     * number: modifier.
393
     *
394
     * Returns the number as is.
395
     */
396
    static public function number($src, $nothrow)
0 ignored issues
show
Unused Code introduced by
The parameter $nothrow is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
397
    {
398
        if (!is_numeric(trim($src))) throw new PHPTAL_ParserException("'$src' is not a number");
399
        return trim($src);
400
    }
401
402
    /**
403
     * json: modifier. Serializes anything as JSON.
404
     */
405
    static public function json($src, $nothrow)
406
    {
407
        return 'json_encode('.phptal_tale($src,$nothrow).')';
408
    }
409
410
    /**
411
     * urlencode: modifier. Escapes a string.
412
     */
413
    static public function urlencode($src, $nothrow)
414
    {
415
        return 'rawurlencode('.phptal_tale($src,$nothrow).')';
416
    }
417
418
    /**
419
     * translates TALES expression with alternatives into single PHP expression.
420
     * Identical to compileToPHPExpressions() for singular expressions.
421
     *
422
     * @see PHPTAL_Php_TalesInternal::compileToPHPExpressions()
423
     * @return string
424
     */
425
    public static function compileToPHPExpression($expression, $nothrow=false)
426
    {
427
        $r = self::compileToPHPExpressions($expression, $nothrow);
428
        if (!is_array($r)) return $r;
429
430
        // this weird ternary operator construct is to execute noThrow inside the expression
431
        return '($ctx->noThrow(true)||1?'.self::convertExpressionsToExpression($r, $nothrow).':"")';
432
    }
433
434
    /*
435
     * helper function for compileToPHPExpression
436
     * @access private
437
     */
438
    private static function convertExpressionsToExpression(array $array, $nothrow)
439
    {
440
        if (count($array)==1) return '($ctx->noThrow('.($nothrow?'true':'false').')||1?('.
441
            ($array[0]==self::NOTHING_KEYWORD?'null':$array[0]).
442
            '):"")';
443
444
        $expr = array_shift($array);
445
446
        return "(!phptal_isempty(\$_tmp5=$expr) && (\$ctx->noThrow(false)||1)?\$_tmp5:".self::convertExpressionsToExpression($array, $nothrow).')';
447
    }
448
449
    /**
450
     * returns PHP code that will evaluate given TALES expression.
451
     * e.g. "string:foo${bar}" may be transformed to "'foo'.phptal_escape($ctx->bar)"
452
     *
453
     * Expressions with alternatives ("foo | bar") will cause it to return array
454
     * Use PHPTAL_Php_TalesInternal::compileToPHPExpression() if you always want string.
455
     *
456
     * @param bool $nothrow if true, invalid expression will return NULL (at run time) rather than throwing exception
457
     *
458
     * @return string or array
459
     */
460
    public static function compileToPHPExpressions($expression, $nothrow=false)
461
    {
462
        $expression = trim($expression);
463
464
        // Look for tales modifier (string:, exists:, Namespaced\Tale:, etc...)
465
        if (preg_match('/^([a-z](?:[a-z0-9._\\\\-]*[a-z0-9])?):(.*)$/si', $expression, $m)) {
466
            list(, $typePrefix, $expression) = $m;
467
        }
468
        // may be a 'string'
469 View Code Duplication
        elseif (preg_match('/^\'((?:[^\']|\\\\.)*)\'$/s', $expression, $m)) {
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...
470
            $expression = stripslashes($m[1]);
471
            $typePrefix = 'string';
472
        }
473
        // failback to path:
474
        else {
475
            $typePrefix = 'path';
476
        }
477
478
        // is a registered TALES expression modifier
479
        $callback = PHPTAL_TalesRegistry::getInstance()->getCallback($typePrefix);
480
        if ($callback !== NULL)
481
        {
482
            $result = call_user_func($callback, $expression, $nothrow);
483
            self::verifyPHPExpressions($typePrefix, $result);
484
            return $result;
485
        }
486
487
        $func = 'phptal_tales_'.str_replace('-', '_', $typePrefix);
488
        throw new PHPTAL_UnknownModifierException("Unknown phptal modifier '$typePrefix'. Function '$func' does not exist", $typePrefix);
489
    }
490
491
    private static function verifyPHPExpressions($typePrefix,$expressions)
492
    {
493
        if (!is_array($expressions)) {
494
            $expressions = array($expressions);
495
        }
496
497
        foreach($expressions as $expr) {
498
            if (preg_match('/;\s*$/', $expr)) {
499
                throw new PHPTAL_ParserException("Modifier $typePrefix generated PHP statement rather than expression (don't add semicolons)");
500
            }
501
        }
502
    }
503
}
504