CallableObject::isProtectedMethod()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 4
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 8
rs 9.6111
1
<?php
2
3
/**
4
 * CallableObject.php
5
 *
6
 * Jaxon callable object
7
 *
8
 * This class stores a reference to an object whose methods can be called from
9
 * the client via a Jaxon request
10
 *
11
 * The Jaxon plugin manager will call <CallableObject->getClientScript> so that
12
 * stub functions can be generated and sent to the browser.
13
 *
14
 * @package jaxon-core
15
 * @author Jared White
16
 * @author J. Max Wilson
17
 * @author Joseph Woolley
18
 * @author Steffen Konerow
19
 * @author Thierry Feuzeu <[email protected]>
20
 * @copyright Copyright (c) 2005-2007 by Jared White & J. Max Wilson
21
 * @copyright Copyright (c) 2008-2010 by Joseph Woolley, Steffen Konerow, Jared White  & J. Max Wilson
22
 * @copyright 2016 Thierry Feuzeu <[email protected]>
23
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
24
 * @link https://github.com/jaxon-php/jaxon-core
25
 */
26
27
namespace Jaxon\Plugin\Request\CallableClass;
28
29
use Jaxon\App\AbstractCallable;
30
use Jaxon\App\Metadata\InputData;
31
use Jaxon\App\Metadata\MetadataInterface;
32
use Jaxon\App\Metadata\MetadataReaderInterface;
33
use Jaxon\Di\ClassContainer;
34
use Jaxon\Di\Container;
35
use Jaxon\Exception\SetupException;
36
use Jaxon\Request\Target;
37
use Jaxon\Response\AbstractResponse;
38
use Jaxon\Utils\Config\Config;
39
use ReflectionClass;
40
use ReflectionException;
41
use ReflectionMethod;
42
use ReflectionProperty;
43
44
use function array_fill_keys;
45
use function array_filter;
46
use function array_map;
47
use function array_merge;
48
use function call_user_func;
49
use function is_array;
50
use function is_string;
51
use function json_encode;
52
use function str_replace;
53
use function substr;
54
55
class CallableObject
56
{
57
    /**
58
     * The user registered callable object
59
     *
60
     * @var mixed
61
     */
62
    private $xRegisteredObject = null;
63
64
    /**
65
     * The target of the Jaxon call
66
     *
67
     * @var Target
68
     */
69
    private $xTarget;
70
71
    /**
72
     * The options of this callable object
73
     *
74
     * @var CallableObjectOptions|null
75
     */
76
    private $xOptions = null;
77
78
    /**
79
     * @var int
80
     */
81
    private $nPropertiesFilter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED;
82
83
    /**
84
     * @var int
85
     */
86
    private $nMethodsFilter = ReflectionMethod::IS_PUBLIC;
87
88
    /**
89
     * @var array
90
     */
91
    private $aProtectedMethods;
92
93
    /**
94
     * The class constructor
95
     *
96
     * @param ClassContainer $cls
97
     * @param Container $di
98
     * @param ReflectionClass $xReflectionClass
99
     * @param array $aOptions
100
     * @param array $aProtectedMethods
101
     */
102
    public function __construct(protected ClassContainer $cls, protected Container $di,
103
        private ReflectionClass $xReflectionClass, array $aOptions, array $aProtectedMethods)
104
    {
105
        $this->aProtectedMethods = array_fill_keys($aProtectedMethods, true);
106
107
        $xMetadata = $this->getAttributes($xReflectionClass, $aOptions);
108
        $this->xOptions = new CallableObjectOptions($aOptions, $xMetadata);
109
    }
110
111
    /**
112
     * @param ReflectionClass $xReflectionClass
113
     * @param array $aOptions
114
     *
115
     * @return MetadataInterface|null
116
     */
117
    private function getAttributes(ReflectionClass $xReflectionClass, array $aOptions): ?MetadataInterface
118
    {
119
        /** @var Config|null */
120
        $xConfig = $aOptions['config'] ?? null;
121
        if($xConfig === null)
122
        {
123
            return null;
124
        }
125
126
        /** @var MetadataReaderInterface */
127
        $xMetadataReader = $this->di->getMetadataReader($xConfig->getOption('metadata', ''));
128
        return $xMetadataReader->getAttributes(new InputData($xReflectionClass,
129
            $this->getPublicMethods(true), $this->getProperties()));
130
    }
131
132
    /**
133
     * Get the public and protected attributes of the callable object
134
     *
135
     * @return array
136
     */
137
    private function getProperties(): array
138
    {
139
        return array_map(function($xProperty) {
140
            return $xProperty->getName();
141
        }, $this->xReflectionClass->getProperties($this->nPropertiesFilter));
142
    }
143
144
    /**
145
     * @param string $sMethodName
146
     * @param bool $bTakeAll
147
     *
148
     * @return bool
149
     */
150
    private function isProtectedMethod(string $sMethodName, bool $bTakeAll): bool
151
    {
152
        // Don't take magic __call, __construct, __destruct methods
153
        // Don't take protected methods
154
        return substr($sMethodName, 0, 2) === '__' ||
155
            isset($this->aProtectedMethods[$sMethodName]) ||
156
            (!$bTakeAll && $this->xOptions !== null &&
157
                $this->xOptions->isProtectedMethod($this->xReflectionClass, $sMethodName));
158
    }
159
160
    /**
161
     * Get the public methods of the callable object
162
     *
163
     * @param bool $bTakeAll
164
     *
165
     * @return array
166
     */
167
    public function getPublicMethods(bool $bTakeAll): array
168
    {
169
        $aMethods = array_map(function($xMethod) {
170
            return $xMethod->getShortName();
171
        }, $this->xReflectionClass->getMethods($this->nMethodsFilter));
172
173
        return array_filter($aMethods, function($sMethodName) use($bTakeAll) {
174
            return !$this->isProtectedMethod($sMethodName, $bTakeAll);
175
        });
176
    }
177
178
    /**
179
     * @return bool
180
     */
181
    public function excluded(): bool
182
    {
183
        return $this->xOptions->excluded();
0 ignored issues
show
Bug introduced by
The method excluded() does not exist on null. ( Ignorable by Annotation )

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

183
        return $this->xOptions->/** @scrutinizer ignore-call */ excluded();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
184
    }
185
186
    /**
187
     * Get the name of the registered PHP class
188
     *
189
     * @return string
190
     */
191
    public function getClassName(): string
192
    {
193
        return $this->xReflectionClass->getName();
194
    }
195
196
    /**
197
     * Get the name of the corresponding javascript class
198
     *
199
     * @return string
200
     */
201
    public function getJsName(): string
202
    {
203
        return str_replace('\\', $this->xOptions->separator(), $this->getClassName());
204
    }
205
206
    /**
207
     * Get the js options of the callable class
208
     *
209
     * @return array
210
     */
211
    public function getOptions(): array
212
    {
213
        return $this->xOptions->jsOptions();
214
    }
215
216
    /**
217
     * Return a list of methods of the callable object to export to javascript
218
     *
219
     * @return array
220
     */
221
    public function getCallableMethods(): array
222
    {
223
        // Get the method options, and convert each of them to
224
        // a string to be displayed in the js script template.
225
        $fGetOption = function($sMethodName) {
226
            return array_map(function($xOption) {
227
                return is_array($xOption) ? json_encode($xOption) : $xOption;
228
            }, $this->xOptions->getMethodOptions($sMethodName));
229
        };
230
231
        return array_map(function($sMethodName) use($fGetOption) {
232
            return [
233
                'name' => $sMethodName,
234
                'config' => $fGetOption($sMethodName),
235
            ];
236
        }, $this->getPublicMethods(false));
237
    }
238
239
    /**
240
     * Check if the specified method name is one of the methods of the registered callable object
241
     *
242
     * @param string $sMethod    The name of the method to check
243
     *
244
     * @return bool
245
     */
246
    public function hasMethod(string $sMethod): bool
247
    {
248
        return $this->xReflectionClass->hasMethod($sMethod);
249
    }
250
251
    /**
252
     * Call the specified method of the registered callable object using the specified array of arguments
253
     *
254
     * @param string $sMethod    The method name
255
     * @param array $aArgs    The method arguments
256
     * @param bool $bAccessible    If false, only calls to public method are allowed
257
     *
258
     * @return mixed
259
     * @throws ReflectionException
260
     */
261
    private function callMethod(string $sMethod, array $aArgs, bool $bAccessible)
262
    {
263
        $reflectionMethod = $this->xReflectionClass->getMethod($sMethod);
264
        $reflectionMethod->setAccessible($bAccessible); // Make it possible to call protected methods
265
        return $reflectionMethod->invokeArgs($this->xRegisteredObject, $aArgs);
266
    }
267
268
    /**
269
     * Call the specified method of the registered callable object using the specified array of arguments
270
     *
271
     * @param array $aHookMethods    The method config options
272
     *
273
     * @return void
274
     * @throws ReflectionException
275
     */
276
    private function callHookMethods(array $aHookMethods)
277
    {
278
        $sMethod = $this->xTarget->getMethodName();
279
        // The hooks defined at method level are merged with those defined at class level.
280
        $aMethods = array_merge($aHookMethods['*'] ?? [], $aHookMethods[$sMethod] ?? []);
281
        foreach($aMethods as $xKey => $xValue)
282
        {
283
            $sHookName = $xValue;
284
            $aHookArgs = [];
285
            if(is_string($xKey))
286
            {
287
                $sHookName = $xKey;
288
                $aHookArgs = is_array($xValue) ? $xValue : [$xValue];
289
            }
290
            $this->callMethod($sHookName, $aHookArgs, true);
291
        }
292
    }
293
294
    /**
295
     * @param mixed $xRegisteredObject
296
     * @param array $aDiOptions
297
     *
298
     * @return void
299
     */
300
    private function setDiAttributes($xRegisteredObject, array $aDiOptions)
301
    {
302
        // Set the protected attributes of the object
303
        $cSetter = function($sName, $xInjectedObject) {
304
            // Warning: dynamic properties will be deprecated in PHP8.2.
305
            $this->$sName = $xInjectedObject;
306
        };
307
        foreach($aDiOptions as $sName => $sClass)
308
        {
309
            // The setter has access to protected attributes
310
            $_cSetter = $cSetter->bindTo($xRegisteredObject, $xRegisteredObject);
311
            call_user_func($_cSetter, $sName, $this->di->get($sClass));
312
        }
313
    }
314
315
    /**
316
     * @param mixed $xRegisteredObject
317
     *
318
     * @return void
319
     */
320
    public function setDiClassAttributes($xRegisteredObject)
321
    {
322
        $aDiOptions = $this->xOptions->diOptions();
323
        $this->setDiAttributes($xRegisteredObject, $aDiOptions['*'] ?? []);
324
    }
325
326
    /**
327
     * @param mixed $xRegisteredObject
328
     * @param string $sMethodName
329
     *
330
     * @return void
331
     */
332
    private function setDiMethodAttributes($xRegisteredObject, string $sMethodName)
333
    {
334
        $aDiOptions = $this->xOptions->diOptions();
335
        $this->setDiAttributes($xRegisteredObject, $aDiOptions[$sMethodName] ?? []);
336
    }
337
338
    /**
339
     * Get a callable object when one of its method needs to be called
340
     *
341
     * @param Target|null $xTarget
342
     *
343
     * @return mixed
344
     */
345
    public function getRegisteredObject(?Target $xTarget = null)
346
    {
347
        $xRegisteredObject = $this->cls->get($this->getClassName());
348
        if(!$xRegisteredObject || !$xTarget)
349
        {
350
            return $xRegisteredObject;
351
        }
352
353
        // Set attributes from the DI container.
354
        // The class level DI options were set when creating the object instance.
355
        // We now need to set the method level DI options.
356
        $this->setDiMethodAttributes($xRegisteredObject, $xTarget->getMethodName());
357
        // Set the Jaxon request target in the helper
358
        if($xRegisteredObject instanceof AbstractCallable)
359
        {
360
            // Set the protected attributes of the object
361
            $sAttr = 'xHelper';
362
            $cSetter = function() use($xTarget, $sAttr) {
363
                $this->$sAttr->xTarget = $xTarget;
364
            };
365
            // Can now access protected attributes
366
            call_user_func($cSetter->bindTo($xRegisteredObject, $xRegisteredObject));
367
        }
368
        return $xRegisteredObject;
369
    }
370
371
    /**
372
     * Call the specified method of the registered callable object using the specified array of arguments
373
     *
374
     * @param Target $xTarget The target of the Jaxon call
375
     *
376
     * @return null|AbstractResponse
377
     * @throws ReflectionException
378
     * @throws SetupException
379
     */
380
    public function call(Target $xTarget): ?AbstractResponse
381
    {
382
        $this->xTarget = $xTarget;
383
        $this->xRegisteredObject = $this->getRegisteredObject($xTarget);
384
385
        // Methods to call before processing the request
386
        $this->callHookMethods($this->xOptions->beforeMethods());
387
388
        // Call the request method
389
        $sMethod = $xTarget->getMethodName();
390
        $xResponse = $this->callMethod($sMethod, $this->xTarget->args(), false);
391
392
        // Methods to call after processing the request
393
        $this->callHookMethods($this->xOptions->afterMethods());
394
        return $xResponse;
395
    }
396
}
397