Completed
Push — master ( 23291c...c26772 )
by Jean
01:56
created

DeferredCallChain::checkMethodIsReallyCallable()   C

Complexity

Conditions 12
Paths 3

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
c 0
b 0
f 0
dl 0
loc 39
rs 6.9666
cc 12
nc 3
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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       BadMethodCallException;
15
16
/**
17
 * This class stores an arbitrary stack of calls (methods or array entries access)
18
 * that will be callable on any future variable.
19
 */
20
class DeferredCallChain implements \JsonSerializable, \ArrayAccess
21
{
22
    use \JClaveau\Traits\Fluent\New_;
23
    use FunctionCallTrait;
24
    
25
    /** @var array $stack The stack of deferred calls */
26
    protected $stack = [];
27
28
    /** @var mixed $expectedTarget The stack of deferred calls */
29
    protected $expectedTarget;
30
31
    /**
32
     * Constructor 
33
     * 
34
     * @param string $key The entry to acces
35
     */
36
    public function __construct($class_type_or_instance=null)
37
    {
38
        if ($class_type_or_instance) {
39
            $this->expectedTarget = $class_type_or_instance;
40
        }
41
    }
42
43
    /**
44
     * ArrayAccess interface
45
     *
46
     * @param string $key The entry to acces
47
     */
48
    public function &offsetGet($key)
49
    {
50
        $this->stack[] = [
51
            'entry' => $key,
52
        ];
53
54
        return $this;
55
    }
56
57
    /**
58
     * Stores any call in the the stack.
59
     *
60
     * @param  string $method
61
     * @param  array  $arguments
62
     *
63
     * @return $this
64
     */
65
    public final function __call($method, array $arguments)
66
    {
67
        $this->stack[] = [
68
            'method'    => $method,
69
            'arguments' => $arguments,
70
        ];
71
72
        return $this;
73
    }
74
75
    /**
76
     * For implementing JsonSerializable interface.
77
     *
78
     * @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php
79
     */
80
    public function jsonSerialize()
81
    {
82
        return $this->stack;
83
    }
84
85
    /**
86
     * Outputs the PHP code producing the current call chain while it's casted
87
     * as a string.
88
     *
89
     * @return string The PHP code corresponding to this call chain
90
     */
91
    public function __toString()
92
    {
93
        $string = '(new ' . get_called_class();
94
        if (is_string($this->expectedTarget)) {
95
            $string .= '(' . var_export($this->expectedTarget, true) . ')';
96
        }
97
        elseif (is_object($this->expectedTarget)) {
98
            $string .= '( ' . get_class($this->expectedTarget) . '#' . spl_object_id($this->expectedTarget) . ' )';
99
        }
100
        $string .= ')';
101
102
        foreach ($this->stack as $i => $call) {
103
            if (isset($call['method'])) {
104
                $string .= '->';
105
                $string .= $call['method'].'(';
106
                $string .= implode(', ', array_map(function($argument) {
107
                    return var_export($argument, true);
108
                }, $call['arguments']));
109
                $string .= ')';
110
            }
111
            else {
112
                $string .= '[' . var_export($call['entry'], true) . ']';
113
            }
114
        }
115
116
        return $string;
117
    }
118
119
    /**
120
     * Checks that the provided target matches the type/class/interface
121
     * given during construction.
122
     * 
123
     * @param  mixed $target
124
     * @return mixed $target Checked
125
     */
126
    protected function checkTarget($target)
127
    {
128
        if (is_object($this->expectedTarget)) {
129
            if ($target) {
130
                throw new TargetAlreadyDefinedException($this, $this->expectedTarget, $target);
131
            }
132
            
133
            $out = $this->expectedTarget;
134
        }
135
        elseif (is_string($this->expectedTarget)) {
136
            if (class_exists($this->expectedTarget)) {
137
                if (! $target instanceof $this->expectedTarget) {
138
                    throw new BadTargetClassException($this, $this->expectedTarget, $target);
139
                }
140
            }
141
            elseif (interface_exists($this->expectedTarget)) {
142
                if (! $target instanceof $this->expectedTarget) {
143
                    throw new BadTargetInterfaceException($this, $this->expectedTarget, $target);
144
                }
145
            }
146
            elseif (type_exists($this->expectedTarget)) {
147
                if (gettype($target) != $this->expectedTarget) {
148
                    throw new BadTargetTypeException($this, $this->expectedTarget, $target);
149
                }
150
            }
151
            else {
152
                throw new UndefinedTargetClassException($this, $this->expectedTarget);
153
            }
154
            
155
            $out = $target;
156
        }
157
        else {
158
            $out = $target;
159
        }
160
        
161
        return $out;
162
    }
163
164
    /**
165
     * Calling a method coded inside a magic __call can produce a 
166
     * BadMethodCallException and thus not be a callable.
167
     * 
168
     * @param mixed  $current_chained_subject
169
     * @param string $method_name
170
     * @param array  $arguments
171
     * 
172
     * @return bool $is_called
173
     */
174
    protected function checkMethodIsReallyCallable(
175
        &$current_chained_subject, 
176
        $method_name,
177
        $arguments
178
    ) {
179
        $is_called = true;
180
        try {
181
            $current_chained_subject = call_user_func_array(
182
                [$current_chained_subject, $method_name], 
183
                $arguments
184
            );
185
        }
186
        catch (\BadMethodCallException $e) {
187
            if (
188
                (
189
                    (  PHP_VERSION_ID < 70000
190
                        && $e->getTrace()[2]['file'] == __FILE__
191
                        && $e->getTrace()[2]['function'] == 'call_user_func_array'
192
                        && $e->getTrace()[1]['function'] == $method_name
193
                        && $e->getTrace()[1]['class'] == get_class($current_chained_subject)
194
                    )
195
                    ||
196
                    (   PHP_VERSION_ID >= 70000
197
                        && $e->getTrace()[1]['file'] == __FILE__
198
                        && $e->getTrace()[1]['function'] == 'call_user_func_array'
199
                        // The magic method call doesn't exist in the stack with PHP 7
200
                    )
201
                )
202
                && $e->getTrace()[0]['function'] == '__call'
203
                && $e->getTrace()[0]['class'] == get_class($current_chained_subject)
204
            ) {
205
                $is_called = false;
206
            }
207
            else {
208
                throw $e;
209
            }
210
        }
211
        
212
        return $is_called;
213
    }
214
215
    /**
216
     * Invoking the instance produces the call of the stack
217
     *
218
     * @param  $target The target to apply the callchain on
0 ignored issues
show
Bug introduced by
The type JClaveau\Async\The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
219
     * @return The value returned once the call chain is called uppon $target
220
     */
221
    public function __invoke($target=null)
222
    {
223
        $out = $this->checkTarget($target);
224
        
225
        foreach ($this->stack as $i => $call) {
226
            $is_called = false;
227
            try {
228
                if (isset($call['method'])) {
229
                    if (is_callable([$out, $call['method']])) {
230
                        $is_called = $this->checkMethodIsReallyCallable(
231
                            $out,
232
                            $call['method'],
233
                            $call['arguments']
234
                        );
235
                    }
236
                    
237
                    if (! $is_called && is_callable($call['method'])) {
238
                        $arguments = $this->prepareArgs($call['arguments'], $out);
239
                        $out = call_user_func_array($call['method'], $arguments);
240
                        $is_called = true;
241
                    }
242
                    
243
                    if (! $is_called) {
244
                        throw new \BadMethodCallException(
245
                            $call['method'] . "() is neither a method of " . get_class($out)
246
                            . " nor a function"
247
                        );
248
                    }
249
                }
250
                else {
251
                    $out = $out[ $call['entry'] ];
252
                }
253
            }
254
            catch (\Exception $e) {
255
                // Throw $e with the good stack (usage exception)
256
                throw $e;
257
            }
258
        }
259
260
        return $out;
261
    }
262
263
    /**
264
     * Unused part of the ArrayAccess interface
265
     *
266
     * @param  $offset
267
     * @param  $value
268
     * @throws \BadMethodCallException
269
     */
270
    public function offsetSet($offset, $value)
271
    {
272
        throw new BadMethodCallException(
273
            "not implemented"
274
        );
275
    }
276
277
    /**
278
     * Unused part of the ArrayAccess interface
279
     *
280
     * @param  $offset
281
     * @throws \BadMethodCallException
282
     */
283
    public function offsetExists($offset)
284
    {
285
        throw new BadMethodCallException(
286
            "not implemented"
287
        );
288
    }
289
290
    /**
291
     * Unused part of the ArrayAccess interface
292
     *
293
     * @param  $offset
294
     * @throws \BadMethodCallException
295
     */
296
    public function offsetUnset($offset)
297
    {
298
        throw new BadMethodCallException(
299
            "not implemented"
300
        );
301
    }
302
303
    /**/
304
}
305