Issues (15)

src/DeferredCallChain.php (1 issue)

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
     * ArrayAccess interface
70
     *
71
     * @param string $key The entry to acces
72
     */
73
    public function &offsetGet($key)
74
    {
75
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
76
        
77
        $this->stack[] = [
78
            'entry' => $key,
79
            'file'  => isset($caller['file']) ? $caller['file'] : null,
80
            'line'  => isset($caller['line']) ? $caller['line'] : null,
81
        ];
82
83
        return $this;
84
    }
85
86
    /**
87
     * Checks that the provided target matches the type/class/interface
88
     * given during construction.
89
     * 
90
     * @param  mixed $target
91
     * @return mixed $target Checked
92
     */
93
    protected function checkTarget($target)
94
    {
95
        if (is_object($this->expectedTarget)) {
96
            if ($target) {
97
                throw new TargetAlreadyDefinedException($this, $this->expectedTarget, $target);
98
            }
99
            
100
            $out = $this->expectedTarget;
101
        }
102
        elseif (is_string($this->expectedTarget)) {
103
            if (class_exists($this->expectedTarget)) {
104
                if (! $target instanceof $this->expectedTarget) {
105
                    throw new BadTargetClassException($this, $this->expectedTarget, $target);
106
                }
107
            }
108
            elseif (interface_exists($this->expectedTarget)) {
109
                if (! $target instanceof $this->expectedTarget) {
110
                    throw new BadTargetInterfaceException($this, $this->expectedTarget, $target);
111
                }
112
            }
113
            elseif (type_exists($this->expectedTarget)) {
114
                if (gettype($target) != $this->expectedTarget) {
115
                    throw new BadTargetTypeException($this, $this->expectedTarget, $target);
116
                }
117
            }
118
            else {
119
                throw new UndefinedTargetClassException($this, $this->expectedTarget);
120
            }
121
            
122
            $out = $target;
123
        }
124
        else {
125
            $out = $target;
126
        }
127
        
128
        return $out;
129
    }
130
131
    /**
132
     * Calling a method coded inside a magic __call can produce a 
133
     * BadMethodCallException and thus not be a callable.
134
     * 
135
     * @param string $method_type '->' or '::'
136
     * @param mixed  $current_chained_subject
137
     * @param string $method_name
138
     * @param array  $arguments
139
     * 
140
     * @return bool $is_called
141
     */
142
    protected function checkMethodIsReallyCallable(
143
        $method_type,
144
        &$current_chained_subject, 
145
        $method_name,
146
        $arguments
147
    ) {
148
        $is_called = true;
149
        try {
150
            if ($method_type == '->') {
151
                $callable = [$current_chained_subject, $method_name];
152
            }
153
            elseif ($method_type == '::') {
154
                if (is_object($current_chained_subject)) {
155
                    $class = get_class($current_chained_subject);
156
                }
157
                elseif (is_string($current_chained_subject)) {
158
                    $class = $current_chained_subject;
159
                }
160
                
161
                $callable = $class .'::'. $method_name;
162
            }
163
            
164
            $current_chained_subject = call_user_func_array(
165
                $callable, 
166
                $arguments
167
            );
168
        }
169
        catch (\BadMethodCallException $e) {
170
            if ($this->exceptionTrownFromMagicCall(
171
                $e->getTrace(),
172
                $current_chained_subject,
173
                $method_name
174
            )) {
175
                $is_called = false;
176
            }
177
            else {
178
                throw $e;
179
            }
180
        }
181
        
182
        return $is_called;
183
    }
184
185
    /**
186
     * Checks if the exception having $trace is thrown from à __call
187
     * magic method.
188
     * 
189
     * @param  array  $trace
190
     * @param  object $current_chained_subject
191
     * @param  string $method_name
192
     * 
193
     * @return bool Whether or not the exception having the $trace has been
194
     *              thrown from a __call() method.
195
     */
196
    protected function exceptionTrownFromMagicCall(
197
        $trace, 
198
        $current_chained_subject,
199
        $method_name
200
    ) {
201
        // Before PHP 7, there is a raw for the non existing method called
202
        $call_user_func_array_position = PHP_VERSION_ID < 70000 ? 2 : 1;
203
        
204
        return  
205
                ($trace[0]['function'] == '__call' || $trace[0]['function'] == '__callStatic')
206
            &&  $trace[0]['class']    == (is_string($current_chained_subject) 
0 ignored issues
show
The condition is_string($current_chained_subject) is always false.
Loading history...
207
                                       ? $current_chained_subject 
208
                                       : get_class($current_chained_subject))
209
            &&  $trace[0]['args'][0]  == $method_name
210
            && (
211
                    $trace[$call_user_func_array_position]['file'] == __FILE__
212
                &&  $trace[$call_user_func_array_position]['function'] == 'call_user_func_array'
213
            )
214
            ;
215
    }
216
217
    /**
218
     * Invoking the instance produces the call of the stack
219
     *
220
     * @param  mixed $target The target to apply the callchain on
221
     * @return mixed The value returned once the call chain is called uppon $target
222
     */
223
    public function __invoke($target=null)
224
    {
225
        $out = $this->checkTarget($target);
226
        
227
        foreach ($this->stack as $i => $call) {
228
            $is_called = false;
229
            try {
230
                if (isset($call['method'])) {
231
                    if (is_callable([$out, $call['method']])) {
232
                        $is_called = $this->checkMethodIsReallyCallable(
233
                            '->',
234
                            $out,
235
                            $call['method'],
236
                            $call['arguments']
237
                        );
238
                    }
239
                    
240
                    if (! $is_called && (
241
                                (is_string($out) && is_callable($out .'::'.$call['method']))
242
                            ||  (is_object($out) && is_callable(get_class($out) .'::'.$call['method']))
243
                        )
244
                    ) {
245
                        $is_called = $this->checkMethodIsReallyCallable(
246
                            '::',
247
                            $out,
248
                            $call['method'],
249
                            $call['arguments']
250
                        );
251
                    }
252
                    
253
                    if (! $is_called && is_callable($call['method'])) {
254
                        $arguments = $this->prepareArgs($call['arguments'], $out);
255
                        $out = call_user_func_array($call['method'], $arguments);
256
                        $is_called = true;
257
                    }
258
                    
259
                    if (! $is_called) {
260
                        throw new \BadMethodCallException(
261
                            $call['method'] . "() is neither a method of "
262
                            . (is_string($out) ? $out : get_class($out))
263
                            . " nor a function"
264
                        );
265
                    }
266
                }
267
                else {
268
                    $out = $out[ $call['entry'] ];
269
                }
270
            }
271
            catch (\Exception $e) {
272
                
273
                $callchain_description = $this->toString([
274
                    'target' => $target,
275
                    'limit'  => $i,
276
                ]);
277
                
278
                VisibilityViolator::setHiddenProperty(
279
                    $e,
280
                    'message',
281
                    $e->getMessage()
282
                    . "\nWhen applying $callchain_description defined at "
283
                    . $call['file'] . ':' . $call['line']
284
                );
285
                
286
                // Throw $e with the good stack (usage exception)
287
                throw $e;
288
            }
289
        }
290
291
        return $out;
292
    }
293
294
    /**/
295
}
296