Passed
Push — master ( 04b495...4a17b9 )
by Jean
01:53
created

DeferredCallChain   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 32
eloc 90
c 5
b 0
f 0
dl 0
loc 220
rs 9.84

6 Methods

Rating   Name   Duplication   Size   Complexity  
B checkTarget() 0 36 10
B __invoke() 0 54 8
A exceptionTrownFromMagicCall() 0 15 6
A __call() 0 12 3
A checkMethodIsReallyCallable() 0 26 3
A __construct() 0 4 2
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
    use ArrayAccessTrait;
26
    use ExportTrait;
27
    
28
    /** @var array $stack The stack of deferred calls */
29
    protected $stack = [];
30
31
    /** @var mixed $expectedTarget The stack of deferred calls */
32
    protected $expectedTarget;
33
34
    /**
35
     * Constructor 
36
     * 
37
     * @param string $class_type_interface_or_instance The expected target class/type/interface/instance
38
     */
39
    public function __construct($class_type_interface_or_instance=null)
40
    {
41
        if ($class_type_interface_or_instance) {
42
            $this->expectedTarget = $class_type_interface_or_instance;
43
        }
44
    }
45
46
    /**
47
     * Stores any call in the the stack.
48
     *
49
     * @param  string $method
50
     * @param  array  $arguments
51
     *
52
     * @return $this
53
     */
54
    public final function __call($method, array $arguments)
55
    {
56
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
57
        
58
        $this->stack[] = [
59
            'method'    => $method,
60
            'arguments' => $arguments,
61
            'file'      => isset($caller['file']) ? $caller['file'] : null,
62
            'line'      => isset($caller['line']) ? $caller['line'] : null,
63
        ];
64
65
        return $this;
66
    }
67
68
    /**
69
     * Checks that the provided target matches the type/class/interface
70
     * given during construction.
71
     * 
72
     * @param  mixed $target
73
     * @return mixed $target Checked
74
     */
75
    protected function checkTarget($target)
76
    {
77
        if (is_object($this->expectedTarget)) {
78
            if ($target) {
79
                throw new TargetAlreadyDefinedException($this, $this->expectedTarget, $target);
80
            }
81
            
82
            $out = $this->expectedTarget;
83
        }
84
        elseif (is_string($this->expectedTarget)) {
85
            if (class_exists($this->expectedTarget)) {
86
                if (! $target instanceof $this->expectedTarget) {
87
                    throw new BadTargetClassException($this, $this->expectedTarget, $target);
88
                }
89
            }
90
            elseif (interface_exists($this->expectedTarget)) {
91
                if (! $target instanceof $this->expectedTarget) {
92
                    throw new BadTargetInterfaceException($this, $this->expectedTarget, $target);
93
                }
94
            }
95
            elseif (type_exists($this->expectedTarget)) {
96
                if (gettype($target) != $this->expectedTarget) {
97
                    throw new BadTargetTypeException($this, $this->expectedTarget, $target);
98
                }
99
            }
100
            else {
101
                throw new UndefinedTargetClassException($this, $this->expectedTarget);
102
            }
103
            
104
            $out = $target;
105
        }
106
        else {
107
            $out = $target;
108
        }
109
        
110
        return $out;
111
    }
112
113
    /**
114
     * Calling a method coded inside a magic __call can produce a 
115
     * BadMethodCallException and thus not be a callable.
116
     * 
117
     * @param mixed  $current_chained_subject
118
     * @param string $method_name
119
     * @param array  $arguments
120
     * 
121
     * @return bool $is_called
122
     */
123
    protected function checkMethodIsReallyCallable(
124
        &$current_chained_subject, 
125
        $method_name,
126
        $arguments
127
    ) {
128
        $is_called = true;
129
        try {
130
            $current_chained_subject = call_user_func_array(
131
                [$current_chained_subject, $method_name], 
132
                $arguments
133
            );
134
        }
135
        catch (\BadMethodCallException $e) {
136
            if ($this->exceptionTrownFromMagicCall(
137
                $e->getTrace(),
138
                $current_chained_subject,
139
                $method_name
140
            )) {
141
                $is_called = false;
142
            }
143
            else {
144
                throw $e;
145
            }
146
        }
147
        
148
        return $is_called;
149
    }
150
151
    /**
152
     * Checks if the exception having $trace is thrown from à __call
153
     * magic method.
154
     * 
155
     * @param  array  $trace
156
     * @param  object $current_chained_subject
157
     * @param  string $method_name
158
     * 
159
     * @return bool Whether or not the exception having the $trace has been
160
     *              thrown from a __call() method.
161
     */
162
    protected function exceptionTrownFromMagicCall(
163
        $trace, 
164
        $current_chained_subject,
165
        $method_name
166
    ) {
167
        // Before PHP 7, there is a raw for the non existing method called
168
        $call_user_func_array_position = PHP_VERSION_ID < 70000 ? 2 : 1;
169
        
170
        return  
171
                $trace[0]['function'] == '__call'
172
            &&  $trace[0]['class']    == get_class($current_chained_subject)
173
            &&  $trace[0]['args'][0]  == $method_name
174
            && (
175
                    $trace[$call_user_func_array_position]['file'] == __FILE__
176
                &&  $trace[$call_user_func_array_position]['function'] == 'call_user_func_array'
177
            )
178
            ;
179
    }
180
181
    /**
182
     * Invoking the instance produces the call of the stack
183
     *
184
     * @param  mixed $target The target to apply the callchain on
185
     * @return mixed The value returned once the call chain is called uppon $target
186
     */
187
    public function __invoke($target=null)
188
    {
189
        $out = $this->checkTarget($target);
190
        
191
        foreach ($this->stack as $i => $call) {
192
            $is_called = false;
193
            try {
194
                if (isset($call['method'])) {
195
                    if (is_callable([$out, $call['method']])) {
196
                        $is_called = $this->checkMethodIsReallyCallable(
197
                            $out,
198
                            $call['method'],
199
                            $call['arguments']
200
                        );
201
                    }
202
                    
203
                    if (! $is_called && is_callable($call['method'])) {
204
                        $arguments = $this->prepareArgs($call['arguments'], $out);
205
                        $out = call_user_func_array($call['method'], $arguments);
206
                        $is_called = true;
207
                    }
208
                    
209
                    if (! $is_called) {
210
                        throw new \BadMethodCallException(
211
                            $call['method'] . "() is neither a method of " . get_class($out)
212
                            . " nor a function"
213
                        );
214
                    }
215
                }
216
                else {
217
                    $out = $out[ $call['entry'] ];
218
                }
219
            }
220
            catch (\Exception $e) {
221
                
222
                $callchain_description = $this->toString([
223
                    'target' => $target,
224
                    'limit'  => $i,
225
                ]);
226
                
227
                VisibilityViolator::setHiddenProperty(
228
                    $e,
229
                    'message',
230
                    $e->getMessage()
231
                    . "\nWhen applying $callchain_description called in "
232
                    . $call['file'] . ':' . $call['line']
233
                );
234
                
235
                // Throw $e with the good stack (usage exception)
236
                throw $e;
237
            }
238
        }
239
240
        return $out;
241
    }
242
243
    /**/
244
}
245