Passed
Push — master ( 9d2fb0...79d8ff )
by Jean
02:00
created

DeferredCallChain   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 55
eloc 135
c 5
b 0
f 0
dl 0
loc 369
rs 6

14 Methods

Rating   Name   Duplication   Size   Complexity  
A offsetSet() 0 4 1
B toString() 0 27 7
B checkTarget() 0 36 10
A __toString() 0 3 1
A offsetGet() 0 11 3
A exceptionTrownFromMagicCall() 0 15 6
B __invoke() 0 54 8
A __call() 0 12 3
A checkMethodIsReallyCallable() 0 26 3
A offsetUnset() 0 4 1
A offsetExists() 0 4 1
B varExport() 0 29 8
A jsonSerialize() 0 3 1
A __construct() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like DeferredCallChain 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.

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 DeferredCallChain, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * DeferredCallChain
4
 *
5
 * @package php-deferred-callchain
6
 * @author  Jean Claveau
7
 */
8
namespace JClaveau\Async;
9
use       JClaveau\Async\Exceptions\BadTargetClassException;
10
use       JClaveau\Async\Exceptions\BadTargetTypeException;
11
use       JClaveau\Async\Exceptions\UndefinedTargetClassException;
12
use       JClaveau\Async\Exceptions\BadTargetInterfaceException;
13
use       JClaveau\Async\Exceptions\TargetAlreadyDefinedException;
14
use       JClaveau\VisibilityViolator\VisibilityViolator;
15
use       BadMethodCallException;
16
17
/**
18
 * This class stores an arbitrary stack of calls (methods or array entries access)
19
 * that will be callable on any future variable.
20
 */
21
class DeferredCallChain implements \JsonSerializable, \ArrayAccess
22
{
23
    use \JClaveau\Traits\Fluent\New_;
24
    use FunctionCallTrait;
25
    
26
    /** @var array $stack The stack of deferred calls */
27
    protected $stack = [];
28
29
    /** @var mixed $expectedTarget The stack of deferred calls */
30
    protected $expectedTarget;
31
32
    /**
33
     * Constructor 
34
     * 
35
     * @param string $class_type_interface_or_instance The expected target class/type/interface/instance
36
     */
37
    public function __construct($class_type_interface_or_instance=null)
38
    {
39
        if ($class_type_interface_or_instance) {
40
            $this->expectedTarget = $class_type_interface_or_instance;
41
        }
42
    }
43
44
    /**
45
     * ArrayAccess interface
46
     *
47
     * @param string $key The entry to acces
48
     */
49
    public function &offsetGet($key)
50
    {
51
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
52
        
53
        $this->stack[] = [
54
            'entry' => $key,
55
            'file'  => isset($caller['file']) ? $caller['file'] : null,
56
            'line'  => isset($caller['line']) ? $caller['line'] : null,
57
        ];
58
59
        return $this;
60
    }
61
62
    /**
63
     * Stores any call in the the stack.
64
     *
65
     * @param  string $method
66
     * @param  array  $arguments
67
     *
68
     * @return $this
69
     */
70
    public final function __call($method, array $arguments)
71
    {
72
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
73
        
74
        $this->stack[] = [
75
            'method'    => $method,
76
            'arguments' => $arguments,
77
            'file'      => isset($caller['file']) ? $caller['file'] : null,
78
            'line'      => isset($caller['line']) ? $caller['line'] : null,
79
        ];
80
81
        return $this;
82
    }
83
84
    /**
85
     * For implementing JsonSerializable interface.
86
     *
87
     * @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php
88
     */
89
    public function jsonSerialize()
90
    {
91
        return $this->stack;
92
    }
93
94
    /**
95
     * Outputs the PHP code producing the current call chain while it's casted
96
     * as a string.
97
     *
98
     * @return string The PHP code corresponding to this call chain
99
     */
100
    public function __toString()
101
    {
102
        return $this->toString();
103
    }
104
105
    /**
106
     * Outputs the PHP code producing the current call chain while it's casted
107
     * as a string.
108
     *
109
     * @return string The PHP code corresponding to this call chain
110
     */
111
    protected function toString(array $options=[])
112
    {
113
        $target = isset($options['target']) ? $options['target'] : $this->expectedTarget;
114
        
115
        $string = '(new ' . get_called_class();
116
        $target && $string .= '(' . static::varExport($target, ['short_objects']) . ')';
117
        $string .= ')';
118
119
        foreach ($this->stack as $i => $call) {
120
            if (isset($call['method'])) {
121
                $string .= '->';
122
                $string .= $call['method'].'(';
123
                $string .= implode(', ', array_map(function($argument) {
124
                    return static::varExport($argument, ['short_objects']);
125
                }, $call['arguments']));
126
                $string .= ')';
127
            }
128
            else {
129
                $string .= '[' . static::varExport($call['entry'], ['short_objects']) . ']';
130
            }
131
            
132
            if (! empty($options['limit']) && $options['limit'] == $i) {
133
                break;
134
            }
135
        }
136
137
        return $string;
138
    }
139
    
140
    /**
141
     * Enhanced var_export() required for dumps.
142
     * 
143
     * @param  mixed  $variable
144
     * @param  array  $options max_length | alias_instances
145
     * @return string The PHP code of the variable
146
     */
147
    protected static function varExport($variable, array $options=[])
148
    {
149
        $options['max_length']    = isset($options['max_length']) ? $options['max_length'] : 512;
150
        $options['short_objects'] = ! empty($options['short_objects']) || in_array('short_objects', $options);
151
        
152
        $export = var_export($variable, true);
153
        
154
        if ($options['short_objects']) {
155
            if (is_object($variable)) {
156
                $export = ' ' . get_class($variable) . ' #' . spl_object_id($variable) . ' ';
157
            }
158
        }
159
        
160
        if (strlen($export) > $options['max_length']) {
161
            
162
            if (is_object($variable)) {
163
                $export = get_class($variable) . ' #' . spl_object_id($variable);
164
            }
165
            elseif (is_string($variable)) {
166
                $keep_length = ceil(($options['max_length'] - 5) / 2);
167
                
168
                $export = substr($variable, 0, $keep_length)
0 ignored issues
show
Bug introduced by
$keep_length of type double is incompatible with the type integer expected by parameter $length of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
                $export = substr($variable, 0, /** @scrutinizer ignore-type */ $keep_length)
Loading history...
169
                    . ' ... '
170
                    . substr($variable, -$keep_length)
0 ignored issues
show
Bug introduced by
-$keep_length of type double is incompatible with the type integer expected by parameter $start of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
                    . substr($variable, /** @scrutinizer ignore-type */ -$keep_length)
Loading history...
171
                    ;
172
            }
173
        }
174
        
175
        return $export;
176
    }
177
178
    /**
179
     * Checks that the provided target matches the type/class/interface
180
     * given during construction.
181
     * 
182
     * @param  mixed $target
183
     * @return mixed $target Checked
184
     */
185
    protected function checkTarget($target)
186
    {
187
        if (is_object($this->expectedTarget)) {
188
            if ($target) {
189
                throw new TargetAlreadyDefinedException($this, $this->expectedTarget, $target);
190
            }
191
            
192
            $out = $this->expectedTarget;
193
        }
194
        elseif (is_string($this->expectedTarget)) {
195
            if (class_exists($this->expectedTarget)) {
196
                if (! $target instanceof $this->expectedTarget) {
197
                    throw new BadTargetClassException($this, $this->expectedTarget, $target);
198
                }
199
            }
200
            elseif (interface_exists($this->expectedTarget)) {
201
                if (! $target instanceof $this->expectedTarget) {
202
                    throw new BadTargetInterfaceException($this, $this->expectedTarget, $target);
203
                }
204
            }
205
            elseif (type_exists($this->expectedTarget)) {
206
                if (gettype($target) != $this->expectedTarget) {
207
                    throw new BadTargetTypeException($this, $this->expectedTarget, $target);
208
                }
209
            }
210
            else {
211
                throw new UndefinedTargetClassException($this, $this->expectedTarget);
212
            }
213
            
214
            $out = $target;
215
        }
216
        else {
217
            $out = $target;
218
        }
219
        
220
        return $out;
221
    }
222
223
    /**
224
     * Calling a method coded inside a magic __call can produce a 
225
     * BadMethodCallException and thus not be a callable.
226
     * 
227
     * @param mixed  $current_chained_subject
228
     * @param string $method_name
229
     * @param array  $arguments
230
     * 
231
     * @return bool $is_called
232
     */
233
    protected function checkMethodIsReallyCallable(
234
        &$current_chained_subject, 
235
        $method_name,
236
        $arguments
237
    ) {
238
        $is_called = true;
239
        try {
240
            $current_chained_subject = call_user_func_array(
241
                [$current_chained_subject, $method_name], 
242
                $arguments
243
            );
244
        }
245
        catch (\BadMethodCallException $e) {
246
            if ($this->exceptionTrownFromMagicCall(
247
                $e->getTrace(),
248
                $current_chained_subject,
249
                $method_name
250
            )) {
251
                $is_called = false;
252
            }
253
            else {
254
                throw $e;
255
            }
256
        }
257
        
258
        return $is_called;
259
    }
260
261
    /**
262
     * Checks if the exception having $trace is thrown from à __call
263
     * magic method.
264
     * 
265
     * @param  array  $trace
266
     * @param  object $current_chained_subject
267
     * @param  string $method_name
268
     * 
269
     * @return bool Whether or not the exception having the $trace has been
270
     *              thrown from a __call() method.
271
     */
272
    protected function exceptionTrownFromMagicCall(
273
        $trace, 
274
        $current_chained_subject,
275
        $method_name
276
    ) {
277
        // Before PHP 7, there is a raw for the non existing method called
278
        $call_user_func_array_position = PHP_VERSION_ID < 70000 ? 2 : 1;
279
        
280
        return  
281
                $trace[0]['function'] == '__call'
282
            &&  $trace[0]['class']    == get_class($current_chained_subject)
283
            &&  $trace[0]['args'][0]  == $method_name
284
            && (
285
                    $trace[$call_user_func_array_position]['file'] == __FILE__
286
                &&  $trace[$call_user_func_array_position]['function'] == 'call_user_func_array'
287
            )
288
            ;
289
    }
290
291
    /**
292
     * Invoking the instance produces the call of the stack
293
     *
294
     * @param  mixed $target The target to apply the callchain on
295
     * @return mixed The value returned once the call chain is called uppon $target
296
     */
297
    public function __invoke($target=null)
298
    {
299
        $out = $this->checkTarget($target);
300
        
301
        foreach ($this->stack as $i => $call) {
302
            $is_called = false;
303
            try {
304
                if (isset($call['method'])) {
305
                    if (is_callable([$out, $call['method']])) {
306
                        $is_called = $this->checkMethodIsReallyCallable(
307
                            $out,
308
                            $call['method'],
309
                            $call['arguments']
310
                        );
311
                    }
312
                    
313
                    if (! $is_called && is_callable($call['method'])) {
314
                        $arguments = $this->prepareArgs($call['arguments'], $out);
315
                        $out = call_user_func_array($call['method'], $arguments);
316
                        $is_called = true;
317
                    }
318
                    
319
                    if (! $is_called) {
320
                        throw new \BadMethodCallException(
321
                            $call['method'] . "() is neither a method of " . get_class($out)
322
                            . " nor a function"
323
                        );
324
                    }
325
                }
326
                else {
327
                    $out = $out[ $call['entry'] ];
328
                }
329
            }
330
            catch (\Exception $e) {
331
                
332
                $callchain_description = $this->toString([
333
                    'target' => $target,
334
                    'limit'  => $i,
335
                ]);
336
                
337
                VisibilityViolator::setHiddenProperty(
338
                    $e,
339
                    'message',
340
                    $e->getMessage()
341
                    . "\nWhen applying $callchain_description called in "
342
                    . $call['file'] . ':' . $call['line']
343
                );
344
                
345
                // Throw $e with the good stack (usage exception)
346
                throw $e;
347
            }
348
        }
349
350
        return $out;
351
    }
352
353
    /**
354
     * Unused part of the ArrayAccess interface
355
     *
356
     * @param  $offset
357
     * @param  $value
358
     * @throws \BadMethodCallException
359
     */
360
    public function offsetSet($offset, $value)
361
    {
362
        throw new BadMethodCallException(
363
            "not implemented"
364
        );
365
    }
366
367
    /**
368
     * Unused part of the ArrayAccess interface
369
     *
370
     * @param  $offset
371
     * @throws \BadMethodCallException
372
     */
373
    public function offsetExists($offset)
374
    {
375
        throw new BadMethodCallException(
376
            "not implemented"
377
        );
378
    }
379
380
    /**
381
     * Unused part of the ArrayAccess interface
382
     *
383
     * @param  $offset
384
     * @throws \BadMethodCallException
385
     */
386
    public function offsetUnset($offset)
387
    {
388
        throw new BadMethodCallException(
389
            "not implemented"
390
        );
391
    }
392
393
    /**/
394
}
395