Completed
Pull Request — master (#10)
by Todd
03:18
created

Ebi   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Test Coverage

Coverage 84.21%

Importance

Changes 0
Metric Value
wmc 57
lcom 2
cbo 4
dl 0
loc 475
ccs 160
cts 190
cp 0.8421
rs 6.433
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 45 2
A defineFunction() 0 8 2
A mb() 0 3 2
A write() 0 16 3
A lookup() 0 16 3
A componentExists() 0 9 3
A componentKey() 0 6 2
A loadComponent() 0 17 3
A cacheKeyExists() 0 4 1
A compile() 0 11 1
A includeComponent() 0 14 3
B filePutContents() 0 20 5
A requireFile() 0 3 1
A defineComponent() 0 3 1
A render() 0 13 2
A setErrorReporting() 0 4 1
A call() 0 7 2
B attributeClass() 0 17 6
B formatDate() 0 21 7
A getMeta() 0 3 2
A setMeta() 0 4 1
A getTemplateLoader() 0 3 1
A setTemplateLoader() 0 4 1
A getMetaArray() 0 3 1
A setMetaArray() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Ebi 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 Ebi, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Ebi;
9
10
11
class Ebi {
12
    /**
13
     * @var string
14
     */
15
    protected $cachePath;
16
    /**
17
     * @var callable[]
18
     */
19
    protected $functions;
20
    /**
21
     * @var TemplateLoaderInterface
22
     */
23
    private $templateLoader;
24
    /**
25
     * @var CompilerInterface
26
     */
27
    private $compiler;
28
    /**
29
     * @var callable[]
30
     */
31
    private $components = [];
32
    /**
33
     * @var array
34
     */
35
    private $meta;
36
37
    /**
38
     * Ebi constructor.
39
     *
40
     * @param TemplateLoaderInterface $templateLoader Used to load template sources from component names.
41
     * @param string $cachePath The path to cache compiled templates.
42
     * @param CompilerInterface $compiler The compiler used to compile templates.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $compiler not be null|CompilerInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
43
     */
44 43
    public function __construct(TemplateLoaderInterface $templateLoader, $cachePath, CompilerInterface $compiler = null) {
45 43
        $this->templateLoader = $templateLoader;
46 43
        $this->cachePath = $cachePath;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
47 43
        $this->compiler = $compiler ?: new Compiler();
0 ignored issues
show
Documentation Bug introduced by
It seems like $compiler ?: new \Ebi\Compiler() can also be of type object<Ebi\Compiler>. However, the property $compiler is declared as type object<Ebi\CompilerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
48
49 43
        $this->defineFunction('abs');
50 43
        $this->defineFunction('ceil');
51 43
        $this->defineFunction('componentExists', [$this, 'componentExists']);
52 43
        $this->defineFunction('count');
53 43
        $this->defineFunction('formatDate', [$this, 'formatDate']);
54 43
        $this->defineFunction('empty');
55 43
        $this->defineFunction('floor');
56 43
        $this->defineFunction('htmlEncode', 'htmlspecialchars');
57 43
        $this->defineFunction('join');
58 43
        $this->defineFunction('lcase', $this->mb('strtolower'));
59 43
        $this->defineFunction('lcfirst');
60 43
        $this->defineFunction('ltrim');
61 43
        $this->defineFunction('max');
62 43
        $this->defineFunction('min');
63 43
        $this->defineFunction('queryEncode', 'http_build_query');
64 43
        $this->defineFunction('round');
65 43
        $this->defineFunction('rtrim');
66 43
        $this->defineFunction('sprintf');
67 43
        $this->defineFunction('strlen', $this->mb('strlen'));
68 43
        $this->defineFunction('substr', $this->mb('substr'));
69 43
        $this->defineFunction('trim');
70 43
        $this->defineFunction('ucase', $this->mb('strtoupper'));
71 43
        $this->defineFunction('ucfirst');
72 43
        $this->defineFunction('ucwords');
73 43
        $this->defineFunction('urlencode', 'rawurlencode');
74
75 43
        $this->defineFunction('@class', [$this, 'attributeClass']);
76
77
        // Define a simple component not found component to help troubleshoot.
78
        $this->defineComponent('@component-not-found', function ($props) {
79 1
            echo '<!-- Component "'.htmlspecialchars($props['component']).'" not found. -->';
80 43
        });
81
82
        // Define a simple component exception.
83 43
        $this->defineComponent('@exception', function ($props) {
84 1
            echo "\n<!--\nException in component \"".htmlspecialchars($props['component'])."\"\n".
85 1
                htmlspecialchars($props['message'])."\n-->\n";
86
87 43
        });
88 43
    }
89
90
    /**
91
     * Register a runtime function.
92
     *
93
     * @param string $name The name of the function.
94
     * @param callable $function The function callback.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $function not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
95
     */
96 43
    public function defineFunction($name, $function = null) {
97 43
        if ($function === null) {
98 43
            $function = $name;
99 43
        }
100
101 43
        $this->functions[strtolower($name)] = $function;
102 43
        $this->compiler->defineFunction($name, $function);
0 ignored issues
show
Bug introduced by
The method defineFunction() does not seem to exist on object<Ebi\CompilerInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
103 43
    }
104
105 43
    private function mb($func) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
106 43
        return function_exists("mb_$func") ? "mb_$func" : $func;
107
    }
108
109
    /**
110
     * Write a component to the output buffer.
111
     *
112
     * @param string $component The name of the component.
113
     * @param array ...$args
114
     */
115 11
    public function write($component, ...$args) {
116 11
        $component = strtolower($component);
117
118
        try {
119 11
            $callback = $this->lookup($component);
120
121 11
            if (is_callable($callback)) {
122 11
                call_user_func($callback, ...$args);
123 11
            } else {
124 1
                $this->write('@component-not-found', ['component' => $component]);
125
            }
126 11
        } catch (\Exception $ex) {
127 1
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
128 1
            return;
129
        }
130 11
    }
131
132
    /**
133
     * Lookup a component with a given name.
134
     *
135
     * @param string $component The component to lookup.
136
     * @return callable|null Returns the component function or **null** if the component is not found.
137
     */
138 38
    public function lookup($component) {
139 38
        $component = strtolower($component);
140 38
        $key = $this->componentKey($component);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
141
142 38
        if (!array_key_exists($key, $this->components)) {
143 36
            $this->loadComponent($component);
144 36
        }
145
146 38
        if (isset($this->components[$key])) {
147 37
            return $this->components[$key];
148
        } else {
149
            // Mark a tombstone to the component array so it doesn't keep getting loaded.
150 2
            $this->components[$key] = null;
151 2
            return null;
152
        }
153
    }
154
155
    /**
156
     * Check to see if a component exists.
157
     *
158
     * @param string $component The name of the component.
159
     * @param bool $loader Whether or not to use the component loader or just look in the component cache.
160
     * @return bool Returns **true** if the component exists or **false** otherwise.
161
     */
162 2
    public function componentExists($component, $loader = true) {
163 2
        $componentKey = $this->componentKey($component);
164 2
        if (array_key_exists($componentKey, $this->components)) {
165 1
            return $this->components[$componentKey] !== null;
166 2
        } elseif ($loader) {
167 2
            return !empty($this->templateLoader->cacheKey($component));
168
        }
169 1
        return false;
170
    }
171
172
    /**
173
     * Strip the namespace off a component name to get the component key.
174
     *
175
     * @param string $component The full name of the component with a possible namespace.
176
     * @return string Returns the component key.
177
     */
178 40
    protected function componentKey($component) {
179 40
        if (false !== $pos = strpos($component, ':')) {
180 1
            $component = substr($component, $pos + 1);
181 1
        }
182 40
        return strtolower($component);
183
    }
184
185
    /**
186
     * Load a component.
187
     *
188
     * @param string $component The name of the component to load.
189
     * @return callable|null Returns the component or **null** if the component isn't found.
190
     */
191 36
    protected function loadComponent($component) {
192 36
        $cacheKey = $this->templateLoader->cacheKey($component);
193
        // The template loader can tell us a template doesn't exist when giving the cache key.
194 36
        if (empty($cacheKey)) {
195 2
            return null;
196
        }
197
198 34
        $cachePath = "{$this->cachePath}/$cacheKey.php";
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
199 34
        $componentKey = $this->componentKey($component);
200
201 34
        if (!file_exists($cachePath)) {
202 34
            $src = $this->templateLoader->load($component);
203 34
            return $this->compile($componentKey, $src, $cacheKey);
204
        } else {
205
            return $this->includeComponent($componentKey, $cachePath);
206
        }
207
    }
208
209
    /**
210
     * Check to see if a specific cache key exists in the cache.
211
     *
212
     * @param string $cacheKey The cache key to check.
213
     * @return bool Returns **true** if there is a cache key at the file or **false** otherwise.
214
     */
215 1
    public function cacheKeyExists($cacheKey) {
216 1
        $cachePath = "{$this->cachePath}/$cacheKey.php";
217 1
        return file_exists($cachePath);
218
    }
219
220
    /**
221
     * Compile a component from source, cache it and include it.
222
     *
223
     * @param string $component The name of the component.
224
     * @param string $src The component source.
225
     * @param string $cacheKey The cache key of the component.
226
     * @return callable|null Returns the compiled component closure.
227
     */
228 35
    public function compile($component, $src, $cacheKey) {
229 35
        $cachePath = "{$this->cachePath}/$cacheKey.php";
230 35
        $component = strtolower($component);
231
232 35
        $php = $this->compiler->compile($src, ['basename' => $component]);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
233 35
        $comment = "/*\n".str_replace('*/', '❄/', trim($src))."\n*/";
234
235 35
        $this->filePutContents($cachePath, "<?php\n$comment\n$php");
236
237 35
        return $this->includeComponent($component, $cachePath);
238
    }
239
240
    /**
241
     * Include a cached component.
242
     *
243
     * @param string $component The component key.
244
     * @param string $cachePath The path to the component.
245
     * @return callable|null Returns the component function or **null** if the component wasn't properly defined.
246
     */
247 35
    private function includeComponent($component, $cachePath) {
248 35
        unset($this->components[$component]);
249 35
        $fn = $this->requireFile($cachePath);
250
251 35
        if (isset($this->components[$component])) {
252 35
            return $this->components[$component];
253
        } elseif (is_callable($fn)) {
254
            $this->defineComponent($component, $fn);
255
            return $fn;
256
        } else {
257
            $this->components[$component] = null;
258
            return null;
259
        }
260
    }
261
262
    /**
263
     * A safe version of {@link file_put_contents()} that also clears op caches.
264
     *
265
     * @param string $path The path to save to.
266
     * @param string $contents The contents of the file.
267
     * @return bool Returns **true** on success or **false** on failure.
268
     */
269 35
    private function filePutContents($path, $contents) {
270 35
        if (!file_exists(dirname($path))) {
271 3
            mkdir(dirname($path), 0777, true);
272 3
        }
273 35
        $tmpPath = tempnam(dirname($path), 'ebi-');
274 35
        $r = false;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
275 35
        if (file_put_contents($tmpPath, $contents) !== false) {
276 35
            chmod($tmpPath, 0664);
277 35
            $r = rename($tmpPath, $path);
278 35
        }
279
280 35
        if (function_exists('apc_delete_file')) {
281
            // This fixes a bug with some configurations of apc.
282
            @apc_delete_file($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
283 35
        } elseif (function_exists('opcache_invalidate')) {
284 35
            @opcache_invalidate($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
285 35
        }
286
287 35
        return $r;
288
    }
289
290
    /**
291
     * Include a file.
292
     *
293
     * This is method is useful for including a file bound to this object instance.
294
     *
295
     * @param string $path The path to the file to include.
296
     * @return mixed Returns the result of the include.
297
     */
298 35
    public function requireFile($path) {
299 35
        return require $path;
300
    }
301
302
    /**
303
     * Register a component.
304
     *
305
     * @param string $name The name of the component to register.
306
     * @param callable $component The component function.
307
     */
308 43
    public function defineComponent($name, callable $component) {
309 43
        $this->components[$name] = $component;
310 43
    }
311
312
    /**
313
     * Render a component to a string.
314
     *
315
     * @param string $component The name of the component to render.
316
     * @param array ...$args Arguments to pass to the component.
317
     * @return string|null Returns the rendered component or **null** if the component was not found.
318
     */
319 34
    public function render($component, ...$args) {
320 34
        if ($callback = $this->lookup($component)) {
321 34
            ob_start();
322 34
            $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
323 34
            call_user_func($callback, ...$args);
324 34
            error_reporting($errs);
325 34
            $str = ob_get_clean();
326 34
            return $str;
327
        } else {
328
            trigger_error("Could not find component $component.", E_USER_NOTICE);
329
            return null;
330
        }
331
    }
332
333
    /**
334
     * Set the error reporting appropriate for template rendering.
335
     *
336
     * @return int Returns the previous error level.
337
     */
338
    public function setErrorReporting() {
339
        $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
340
        return $errs;
341
    }
342
343
    /**
344
     * Call a function registered with **defineFunction()**.
345
     *
346
     * If a static or global function is registered then it's simply rendered in the compiled template.
347
     * This method is for closures or callbacks.
348
     *
349
     * @param string $name The name of the registered function.
350
     * @param array ...$args The function's argument.
351
     * @return mixed Returns the result of the function
352
     * @throws RuntimeException Throws an exception when the function isn't found.
353
     */
354 2
    public function call($name, ...$args) {
355 2
        if (!isset($this->functions[$name])) {
356 1
            throw new RuntimeException("Call to undefined function $name.", 500);
357
        } else {
358 1
            return $this->functions[$name](...$args);
359
        }
360
    }
361
362
    /**
363
     * Render a variable appropriately for CSS.
364
     *
365
     * This is a convenience runtime function.
366
     *
367
     * @param string|array $expr A CSS class, an array of CSS classes, or an associative array where the keys are class
368
     * names and the values are truthy conditions to include the class (or not).
369
     * @return string Returns a space-delimited CSS class string.
370
     */
371 4
    public function attributeClass($expr) {
372 4
        if (is_array($expr)) {
373 3
            $classes = [];
374 3
            foreach ($expr as $i => $val) {
375 3
                if (is_array($val)) {
376 1
                    $classes[] = $this->attributeClass($val);
377 3
                } elseif (is_int($i)) {
378 1
                    $classes[] = $val;
379 3
                } elseif (!empty($val)) {
380 2
                    $classes[] = $i;
381 2
                }
382 3
            }
383 3
            return implode(' ', $classes);
384
        } else {
385 1
            return (string)$expr;
386
        }
387
    }
388
389
    /**
390
     * Format a data.
391
     *
392
     * @param mixed $date The date to format. This can be a string data, a timestamp or an instance of **DateTimeInterface**.
393
     * @param string $format The format of the date.
394
     * @return string Returns the formatted data.
395
     * @see date_format()
396
     */
397 1
    public function formatDate($date, $format = 'c') {
398 1
        if (is_string($date)) {
399
            try {
400 1
                $date = new \DateTimeImmutable($date);
401 1
            } catch (\Exception $ex) {
402
                return '#error#';
403
            }
404 1
        } elseif (empty($date)) {
405
            return '';
406
        } elseif (is_int($date)) {
407
            try {
408
                $date = new \DateTimeImmutable('@'.$date);
409
            } catch (\Exception $ex) {
410
                return '#error#';
411
            }
412
        } elseif (!$date instanceof \DateTimeInterface) {
413
            return '#error#';
414
        }
415
416 1
        return $date->format($format);
417
    }
418
419
    /**
420
     * Get a single item from the meta array.
421
     *
422
     * @param string $name The key to get from.
423
     * @param mixed $default The default value if no item at the key exists.
424
     * @return mixed Returns the meta value.
425
     */
426
    public function getMeta($name, $default = null) {
427
        return isset($this->meta[$name]) ? $this->meta[$name] : $default;
428
    }
429
430
    /**
431
     * Set a single item to the meta array.
432
     *
433
     * @param string $name The key to set.
434
     * @param mixed $value The new value.
435
     * @return $this
436
     */
437 1
    public function setMeta($name, $value) {
438 1
        $this->meta[$name] = $value;
439 1
        return $this;
440
    }
441
442
    /**
443
     * Get the template loader.
444
     *
445
     * The template loader translates component names into template contents.
446
     *
447
     * @return TemplateLoaderInterface Returns the template loader.
448
     */
449 1
    public function getTemplateLoader() {
450 1
        return $this->templateLoader;
451
    }
452
453
    /**
454
     * Set the template loader.
455
     *
456
     * The template loader translates component names into template contents.
457
     *
458
     * @param TemplateLoaderInterface $templateLoader The new template loader.
459
     * @return $this
460
     */
461
    public function setTemplateLoader($templateLoader) {
462
        $this->templateLoader = $templateLoader;
463
        return $this;
464
    }
465
466
    /**
467
     * Get the entire meta array.
468
     *
469
     * @return array Returns the meta.
470
     */
471
    public function getMetaArray() {
472
        return $this->meta;
473
    }
474
475
    /**
476
     * Set the entire meta array.
477
     *
478
     * @param array $meta The new meta array.
479
     * @return $this
480
     */
481
    public function setMetaArray(array $meta) {
482
        $this->meta = $meta;
483
        return $this;
484
    }
485
}
486