Container   C
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 639
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 12

Test Coverage

Coverage 65.36%

Importance

Changes 0
Metric Value
wmc 66
c 0
b 0
f 0
lcom 3
cbo 12
dl 0
loc 639
ccs 168
cts 257
cp 0.6536
rs 5.4842

28 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 0 132 10
A get() 0 4 1
A lookup() 0 10 3
B resolve() 0 33 4
A set() 0 4 1
A helperExec() 0 10 2
A path() 0 7 2
A fn() 0 10 3
A method() 0 4 1
A decl() 0 14 3
A notice() 0 4 1
A evaluate() 0 11 1
A has() 0 9 2
A isEmpty() 0 9 3
A call() 0 17 4
A setOutputPrefix() 0 8 2
B exec() 0 22 5
A cmd() 0 10 2
A value() 0 7 2
A str() 0 13 4
A addPlugin() 0 5 1
A addCommand() 0 4 1
A getCommands() 0 4 1
A getValues() 0 4 1
A __clone() 0 3 1
A isDebug() 0 4 1
A push() 0 11 3
A pop() 0 4 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * @author Gerard van Helden <[email protected]>
4
 * @copyright Zicht Online <http://zicht.nl>
5
 */
6
7
namespace Zicht\Tool\Container;
8
9
use Symfony\Component\Console\Command\Command;
10
use Symfony\Component\Console\Output\NullOutput;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Zicht\Tool\Debug;
13
use Zicht\Tool\Output\PrefixFormatter;
14
use Zicht\Tool\PropertyPath\PropertyAccessor;
15
use Zicht\Tool\PluginInterface;
16
use Zicht\Tool\Script\Compiler as ScriptCompiler;
17
use Zicht\Tool\Util;
18
use Zicht\Tool\Script\Parser\Expression as ExpressionParser;
19
use Zicht\Tool\Script\Tokenizer\Expression as ExpressionTokenizer;
20
use UnexpectedValueException;
21
22
/**
23
 * Service container
24
 */
25
class Container
26
{
27
    /**
28
     * Exit code used by commands to identify that they should abort the entire script
29
     */
30
    const ABORT_EXIT_CODE = 42;
31
32
    protected $commands = array();
33
    protected $values = array();
34
35
    private $resolutionStack = array();
36
    private $varStack = array();
37
    private $scope = array();
0 ignored issues
show
Unused Code introduced by
The property $scope is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
38
39
    public $output;
40
    public $executor;
41
    public $plugins = array();
42
43
    /**
44
     * Construct the container with the specified values as services/values.
45
     *
46
     * @param Executor $executor
47
     * @param OutputInterface $output
48
     */
49 11
    public function __construct(Executor $executor = null, $output = null)
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
50
    {
51 11
        $this->executor = $executor ?: new Executor($this);
52 11
        $this->output = $output ?: new NullOutput();
53
54 11
        $this->values = array(
55
            'SHELL'         => function ($z) {
56 1
                return '/bin/bash -e' . ($z->has('DEBUG') && $z->get('DEBUG') ? 'x' : '');
57 11
            },
58 11
            'TIMEOUT'       => null,
59 11
            'INTERACTIVE'   => false,
60
        );
61
        // gather the options for nested z calls.
62 11
        $this->set(
63 11
            array('z', 'opts'),
64
            function ($z) {
65 1
                $opts = array();
66 1
                foreach (array('FORCE', 'VERBOSE', 'EXPLAIN', 'DEBUG') as $opt) {
67 1
                    if ($z->has($opt) && $z->get($opt)) {
68 1
                        $opts[] = '--' . strtolower($opt);
69 1
                    }
70 1
                }
71 1
                return join(' ', $opts);
72
            }
73 11
        );
74 11
        $this->set(array('z', 'cmd'), $_SERVER['argv'][0]);
75 11
        $this->decl(
76 11
            'STDIN',
77
            function () {
78
                return stream_get_contents(STDIN);
79
            }
80 11
        );
81
        $this->fn('confirm', function () {
82
            return false;
83 11
        });
84 11
        $this->set('cwd', getcwd());
85 11
        $this->set('user', getenv('USER'));
86
87
88
        // -----------------------------------------------------------------
89
        // string functions
90 11
        $this->fn(
91 11
            'cat',
92
            function () {
93 1
                return join('', func_get_args());
94
            }
95 11
        );
96 11
        $this->fn('trim');
97 11
        $this->fn('str_replace', 'str_replace');
98 11
        $this->fn(
99 11
            'sha1',
100
            function () {
101
                return sha1(join("", func_get_args()));
102
            }
103 11
        );
104 11
        $this->fn('ltrim');
105 11
        $this->fn('rtrim');
106 11
        $this->fn('sprintf');
107
        $this->fn(array('safename'), function ($fn) {
108
            return preg_replace('/[^a-z0-9]+/', '-', $fn);
109 11
        });
110
        
111
        // -----------------------------------------------------------------
112
        // I/O functions
113 11
        $this->fn('basename');
114 11
        $this->fn('dirname');
115 11
        $this->fn('is_file');
116 11
        $this->fn('is_dir');
117 11
        $this->fn('mtime', 'filemtime');
118 11
        $this->fn('atime', 'fileatime');
119 11
        $this->fn('ctime', 'filectime');
120
        $this->fn('escape', function ($value) {
121
            if (is_array($value)) {
122
                return array_map('escapeshellarg', $value);
123
            }
124
            return escapeshellarg($value);
125 11
        });
126 11
        $this->fn(
127 11
            'path',
128
            function () {
129
                return join(
130
                    "/",
131
                    array_map(
132
                        function ($el) {
133
                            return rtrim($el, "/");
134
                        },
135
                        array_filter(func_get_args())
136
                    )
137
                );
138
            }
139 11
        );
140
        
141
        // -----------------------------------------------------------------
142
        // array functions
143 11
        $this->fn('join', 'implode');
144 11
        $this->fn('keys', 'array_keys');
145 11
        $this->fn('values', 'array_values');
146
        $this->fn('range', function () {
147
            if (func_num_args() > 1) {
148
                return range(func_get_arg(1), func_get_arg(0));
149
            }
150
            return range(1, func_get_arg(0));
151 11
        });
152 11
        $this->fn('slice', 'array_slice');
153
154
155
        // -----------------------------------------------------------------
156
        // encoding / decoding
157
        $this->fn('json_encode', function ($v) {
158
            return json_encode($v, JSON_UNESCAPED_SLASHES);
159 11
        });
160 11
        $this->fn('json_decode');
161
162
        // -----------------------------------------------------------------
163
        // other functions
164 11
        $this->fn('sh', array($this, 'helperExec'));
165 11
        $this->fn('str', array($this, 'str'));
166 11
        $this->fn(
167 11
            array('url', 'host'),
168
            function ($url) {
169
                return parse_url($url, PHP_URL_HOST);
170
            }
171 11
        );
172
        $this->decl(array('now'), function () {
173
            return date('YmdHis');
174 11
        });
175
176 11
        $exitCode = self::ABORT_EXIT_CODE;
177
        $this->decl(array('abort'), function () use($exitCode) {
178
            return 'exit ' . $exitCode;
179 11
        });
180 11
    }
181
182
183
    /**
184
     * Return the raw context value at the specified path.
185
     *
186
     * @param array|string $path
187
     * @return mixed
188
     */
189 3
    public function get($path)
190
    {
191 3
        return $this->lookup($this->values, $path, true);
0 ignored issues
show
Bug introduced by
It seems like $path defined by parameter $path on line 189 can also be of type string; however, Zicht\Tool\Container\Container::lookup() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
192
    }
193
194
195
    /**
196
     * Looks up a path in the specified context
197
     *
198
     * @param array $context
199
     * @param array $path
200
     * @param bool $require
201
     * @return string
202
     *
203
     * @throws \RuntimeException
204
     * @throws \InvalidArgumentException
205
     */
206 12
    public function lookup($context, $path, $require = false)
207
    {
208 12
        if (empty($path)) {
209 1
            throw new \InvalidArgumentException("Passed lookup path is empty.");
210
        }
211 11
        if (empty($context)) {
212
            throw new \InvalidArgumentException("Specified context is empty while resolving path " . json_encode($path));
213
        }
214 11
        return PropertyAccessor::getByPath($context, $this->path($path), $require);
215
    }
216
217
    /**
218
     * Resolve the specified path. If the resulting value is a Closure, it's assumed a declaration and therefore
219
     * executed
220
     *
221
     * @param array|string $id
222
     * @param bool $required
223
     * @return string
224
     *
225
     * @throws \RuntimeException
226
     * @throws CircularReferenceException
227
     */
228 9
    public function resolve($id, $required = false)
229
    {
230 9
        $id = $this->path($id);
231
232
        try {
233 9
            if (in_array($id, $this->resolutionStack)) {
234 1
                $path = array_map(
235
                    function ($a) {
236 1
                        return join('.', $a);
237 1
                    },
238 1
                    $this->resolutionStack
239 1
                );
240
241 1
                throw new CircularReferenceException(
242 1
                    sprintf(
243 1
                        "Circular reference detected: %s -> %s",
244 1
                        implode(' -> ', $path),
245 1
                        join('.', $id)
246 1
                    )
247 1
                );
248
            }
249
250 9
            array_push($this->resolutionStack, $id);
251 9
            $ret = $this->value($this->lookup($this->values, $id, $required));
252 7
            array_pop($this->resolutionStack);
253 7
            return $ret;
254 2
        } catch (\Exception $e) {
255 2
            if ($e instanceof CircularReferenceException) {
256 1
                throw $e;
257
            }
258 1
            throw new \RuntimeException("While resolving value " . join(".", $id), 0, $e);
259
        }
260
    }
261
262
263
    /**
264
     * Set the value at the specified path
265
     *
266
     * @param array|string $path
267
     * @param mixed $value
268
     * @return void
269
     *
270
     * @throws \UnexpectedValueException
271
     */
272 10
    public function set($path, $value)
273
    {
274 10
        PropertyAccessor::setByPath($this->values, $this->path($path), $value);
275 10
    }
276
277
    /**
278
     * This is useful for commands that need the shell regardless of the 'explain' value setting.
279
     *
280
     * @param string $cmd
281
     * @return mixed
282
     */
283 1
    public function helperExec($cmd)
284
    {
285 1
        if ($this->resolve('EXPLAIN')) {
286 1
            $this->output->writeln("# Task needs the following helper command:");
287 1
            $this->output->writeln("# " . str_replace("\n", "\\n", $cmd));
288 1
        }
289 1
        $ret = '';
290 1
        $this->executor->execute($cmd, $ret);
291 1
        return $ret;
292
    }
293
294
    /**
295
     * Wrapper for converting string paths to arrays.
296
     *
297
     * @param mixed $path
298
     * @return array
299
     *
300
     * @throws \InvalidArgumentException
301
     */
302 10
    private function path($path)
303
    {
304 10
        if (is_string($path)) {
305 10
            $path = explode('.', $path);
306 10
        }
307 10
        return $path;
308
    }
309
310
311
    /**
312
     * Set a function at the specified path.
313
     *
314
     * @param array|string $id
315
     * @param callable $callable
316
     * @param bool $needsContainer
317
     * @return void
318
     *
319
     * @throws \InvalidArgumentException
320
     */
321 8
    public function fn($id, $callable = null, $needsContainer = false)
322
    {
323 8
        if ($callable === null) {
324 8
            $callable = $id;
325 8
        }
326 8
        if (!is_callable($callable)) {
327
            throw new \InvalidArgumentException("Not callable");
328
        }
329 8
        $this->set($id, array($callable, $needsContainer));
330 8
    }
331
332
333
    /**
334
     * Creates a method-type function, i.e. the first parameter will always be the container.
335
     *
336
     * @param array $id
337
     * @param callable $callable
338
     * @return void
339
     */
340
    public function method($id, $callable)
341
    {
342
        $this->fn($id, $callable, true);
343
    }
344
345
346
    /**
347
     * Does a declaration, i.e., the first time the declaration is called, it's resulting value overwrites the
348
     * declaration.
349
     *
350
     * @param array|string $id
351
     * @param callable $callable
352
     * @return void
353
     *
354
     * @throws \InvalidArgumentException
355
     */
356 10
    public function decl($id, $callable)
357
    {
358 10
        if (!is_callable($callable)) {
359
            throw new \InvalidArgumentException("Passed declaration is not callable");
360
        }
361
        $this->set($id, function (Container $c) use($callable, $id) {
362 2
            Debug::enterScope(join('.', (array)$id));
363 2
            if (null !== ($value = call_user_func($callable, $c))) {
364 2
                $c->set($id, $value);
365 2
            }
366 2
            Debug::exitScope(join('.', (array)$id));
367 2
            return $value;
368 10
        });
369 10
    }
370
371
    /**
372
     * Output a notice
373
     *
374
     * @param string $message
375
     * @return void
376
     */
377
    public function notice($message)
378
    {
379
        $this->output->writeln("<comment>NOTICE: $message</comment>");
380
    }
381
382
383
    /**
384
     * Does an on-the-fly evaluation of the specified expression.
385
     * The compilation result will be stored in $code.
386
     *
387
     * @param string $expression
388
     * @param string &$code
389
     *
390
     * @return string
391
     */
392
    public function evaluate($expression, &$code = null)
393
    {
394
        $exprcompiler = new ScriptCompiler(new ExpressionParser(), new ExpressionTokenizer());
395
396
        $z = $this;
397
        $_value = null;
0 ignored issues
show
Unused Code introduced by
$_value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
398
        $code = '$z->set(array(\'_\'), ' . $exprcompiler->compile($expression) . ');';
399
        eval($code);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
400
401
        return $z->resolve(array('_'));
402
    }
403
404
405
    /**
406
     * Checks for existence of the specified path.
407
     *
408
     * @param string $id
409
     * @return string
410
     */
411 1
    public function has($id)
412
    {
413
        try {
414 1
            $existing = $this->get($id);
415 1
        } catch (\OutOfBoundsException $e) {
416 1
            return false;
417
        }
418
        return Util::typeOf($existing);
419
    }
420
421
422
    /**
423
     * Checks if a value is empty.
424
     *
425
     * @param mixed $path
426
     * @return bool
427
     */
428
    public function isEmpty($path)
429
    {
430
        try {
431
            $value = $this->get($path);
432
        } catch (\OutOfBoundsException $e) {
433
            return true;
434
        }
435
        return '' === $value || null === $value;
436
    }
437
438
439
    /**
440
     * Separate helper for calling a service as a function.
441
     *
442
     * @return mixed
443
     *
444
     * @throws \InvalidArgumentException
445
     */
446
    public function call()
447
    {
448
        $args = func_get_args();
449
        $service = array_shift($args);
450
        if (!is_array($service)) {
451
            throw new \RuntimeException("Expected an array");
452
        }
453
        if (!is_callable($service[0])) {
454
            throw new \InvalidArgumentException("Can not use service '{$service[0]}' as a function, it is not callable");
455
        }
456
457
        // if the service needs the container, it is specified in the decl() call as the second param:
458
        if ($service[1]) {
459
            array_unshift($args, $this);
460
        }
461
        return call_user_func_array($service[0], $args);
462
    }
463
464
    /**
465
     * Helper to set prefix if the output if PrefixFormatter
466
     *
467
     * @param string $prefix
468
     * @return void
469
     */
470 2
    private function setOutputPrefix($prefix)
471
    {
472 2
        if (!($this->output->getFormatter() instanceof PrefixFormatter)) {
473 2
            return;
474
        }
475
476
        $this->output->getFormatter()->prefix = $prefix;
477
    }
478
479
480
    /**
481
     * Executes a script snippet using the 'executor' service.
482
     *
483
     * @param string $cmd
484
     * @return void
485
     */
486 2
    public function exec($cmd)
487
    {
488 2
        if (trim($cmd)) {
489 2
            $this->setOutputPrefix('');
490
491 2
            if ($this->resolve('DEBUG')) {
492
                $this->output->writeln('<comment># ' . join('::', Debug::$scope) . "</comment>");
493
            }
494
495 2
            if ($this->resolve('EXPLAIN')) {
496 1
                if ($this->resolve('INTERACTIVE')) {
497
                    $this->notice('interactive shell:');
498
                    $line = '( /bin/bash -c \'' . trim($cmd) . '\' )';
499
                } else {
500 1
                    $line = 'echo ' . escapeshellarg(trim($cmd)) . ' | ' . $this->resolve(array('SHELL'));
501
                }
502 1
                $this->output->writeln($line);
503 1
            } else {
504 1
                $this->executor->execute($cmd);
505
            }
506 2
        }
507 2
    }
508
509
    /**
510
     * Execute a command. This is a wrapper for 'exec', so that a task prefixed with '@' can be passed as well.
511
     *
512
     * @param string $cmd
513
     * @return string|null
514
     */
515 3
    public function cmd($cmd)
516
    {
517 3
        $cmd = ltrim($cmd);
518 3
        if (substr($cmd, 0, 1) === '@') {
519 1
            return $this->resolve(array_merge(array('tasks'), explode('.', substr($cmd, 1))));
520
        } else {
521 2
            $this->exec($cmd);
522 2
            return null;
523
        }
524
    }
525
526
527
    /**
528
     * Returns the value representation of the requested variable
529
     *
530
     * @param string $value
531
     * @return string
532
     *
533
     * @throws \UnexpectedValueException
534
     */
535 7
    public function value($value)
536
    {
537 7
        if ($value instanceof \Closure) {
538 5
            $value = call_user_func($value, $this);
539 3
        }
540 5
        return $value;
541
    }
542
543
544
    /**
545
     * Convert the value to a string.
546
     *
547
     * @param mixed $value
548
     * @return string
549
     * @throws \UnexpectedValueException
550
     */
551 2
    public function str($value)
552
    {
553 2
        if (is_array($value)) {
554 2
            $allScalar = function ($a, $b) {
555 2
                return $a && is_scalar($b);
556 2
            };
557 2
            if (!array_reduce($value, $allScalar, true)) {
558 1
                throw new UnexpectedValueException("Unexpected complex type " . Util::toPhp($value));
559
            }
560 1
            return join(' ', $value);
561
        }
562
        return (string)$value;
563
    }
564
565
566
    /**
567
     * Register a plugin
568
     *
569
     * @param \Zicht\Tool\PluginInterface $plugin
570
     * @return void
571
     */
572
    public function addPlugin(PluginInterface $plugin)
573
    {
574
        $this->plugins[] = $plugin;
575
        $plugin->setContainer($this);
576
    }
577
578
579
    /**
580
     * Register a command
581
     *
582
     * @param \Symfony\Component\Console\Command\Command $command
583
     * @return void
584
     */
585
    public function addCommand(Command $command)
586
    {
587
        $this->commands[] = $command;
588
    }
589
590
591
    /**
592
     * Returns the registered commands
593
     *
594
     * @return Task[]
595
     */
596
    public function getCommands()
597
    {
598
        return $this->commands;
599
    }
600
601
    /**
602
     * Returns the values.
603
     *
604
     * @return array
605
     */
606 1
    public function getValues()
607
    {
608 1
        return $this->values;
609
    }
610
611
612
    /**
613
     * Can not be cloned
614
     *
615
     * @return void
616
     */
617
    private function __clone()
618
    {
619
    }
620
621
622
    /**
623
     * Check whether we're in debug mode or not.
624
     *
625
     * @return bool
626
     */
627
    public function isDebug()
628
    {
629
        return $this->get('DEBUG') === true;
630
    }
631
632
633
    /**
634
     * Push a var on a local stack by it's name.
635
     * 
636
     * @param string $varName
637
     * @param string $tail
638
     * @return void
639
     */
640
    public function push($varName, $tail)
641
    {
642
        if (false === $this->has($varName)) {
643
            $this->set($varName, null);
644
        }
645
        if (!isset($this->varStack[json_encode($varName)])) {
646
            $this->varStack[json_encode($varName)] = array();
647
        }
648
        array_push($this->varStack[json_encode($varName)], $this->get($varName));
649
        $this->set($varName, $tail);
650
    }
651
652
653
    /**
654
     * Pop a var from a local var stack.
655
     *
656
     * @param string $varName
657
     * @return void
658
     */
659
    public function pop($varName)
660
    {
661
        $this->set($varName, array_pop($this->varStack[json_encode($varName)]));
662
    }
663
}
664