Passed
Push — main ( 652a21...525366 )
by Thierry
03:56
created

CallableObject::callHookMethods()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
nc 4
nop 1
dl 0
loc 15
rs 9.9666
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * CallableObject.php
5
 *
6
 * Jaxon callable object
7
 *
8
 * This class stores a reference to a component whose methods can be called from
9
 * the client via a Jaxon request
10
 *
11
 * @package jaxon-core
12
 * @author Jared White
13
 * @author J. Max Wilson
14
 * @author Joseph Woolley
15
 * @author Steffen Konerow
16
 * @author Thierry Feuzeu <[email protected]>
17
 * @copyright Copyright (c) 2005-2007 by Jared White & J. Max Wilson
18
 * @copyright Copyright (c) 2008-2010 by Joseph Woolley, Steffen Konerow, Jared White  & J. Max Wilson
19
 * @copyright 2016 Thierry Feuzeu <[email protected]>
20
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
21
 * @link https://github.com/jaxon-php/jaxon-core
22
 */
23
24
namespace Jaxon\Plugin\Request\CallableClass;
25
26
use Jaxon\Di\ComponentContainer;
27
use Jaxon\Di\Container;
28
use Jaxon\Exception\SetupException;
29
use Jaxon\Request\Target;
30
use Closure;
31
use ReflectionClass;
32
use ReflectionException;
33
34
use function array_map;
35
use function array_merge;
36
use function call_user_func;
37
use function is_array;
38
use function is_string;
39
use function json_encode;
40
use function str_replace;
41
42
class CallableObject
43
{
44
    /**
45
     * The user registered component
46
     *
47
     * @var mixed
48
     */
49
    private $xComponent = null;
50
51
    /**
52
     * The target of the Jaxon call
53
     *
54
     * @var Target
55
     */
56
    private $xTarget;
57
58
    /**
59
     * The class constructor
60
     *
61
     * @param ComponentContainer $cdi
62
     * @param Container $di
63
     * @param ReflectionClass $xReflectionClass
64
     * @param ComponentOptions $xOptions
65
     */
66
    public function __construct(protected ComponentContainer $cdi,
67
        protected Container $di, private ReflectionClass $xReflectionClass,
68
        private ComponentOptions $xOptions)
69
    {}
70
71
    /**
72
     * @param string|null $sMethodName
73
     *
74
     * @return bool
75
     */
76
    public function excluded(?string $sMethodName = null): bool
77
    {
78
        return $sMethodName === null ? $this->xOptions->excluded() :
79
            $this->xOptions->isProtectedMethod($sMethodName);
80
    }
81
82
    /**
83
     * Get the name of the registered PHP class
84
     *
85
     * @return class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
86
     */
87
    public function getClassName(): string
88
    {
89
        return $this->xReflectionClass->getName();
90
    }
91
92
    /**
93
     * Get the name of the corresponding javascript class
94
     *
95
     * @return string
96
     */
97
    public function getJsName(): string
98
    {
99
        return str_replace('\\', $this->xOptions->separator(), $this->getClassName());
100
    }
101
102
    /**
103
     * Get the js options of the component
104
     *
105
     * @return array
106
     */
107
    public function getOptions(): array
108
    {
109
        return $this->xOptions->jsOptions();
110
    }
111
112
    /**
113
     * @param string $sMethodName
114
     *
115
     * @return array
116
     */
117
    private function getMethodOptions(string $sMethodName): array
118
    {
119
        return array_map(fn($xOption) =>
120
            is_array($xOption) ? json_encode($xOption) : $xOption,
121
            $this->xOptions->getMethodOptions($sMethodName));
122
    }
123
124
    /**
125
     * Return a list of methods of the component to export to javascript
126
     *
127
     * @return array
128
     */
129
    public function getCallableMethods(): array
130
    {
131
        // Get the method options, and convert each of them to
132
        // a string to be displayed in the js script template.
133
        $aMethods = array_filter($this->cdi->getPublicMethods($this->xReflectionClass),
134
            fn($sMethodName) => !$this->xOptions->isProtectedMethod($sMethodName));
135
136
        return array_map(fn($sMethodName) => [
137
            'name' => $sMethodName,
138
            'options' => $this->getMethodOptions($sMethodName),
139
        ], $aMethods);
140
    }
141
142
    /**
143
     * Check if the specified method name is one of the methods of the component
144
     *
145
     * @param string $sMethod    The name of the method to check
146
     *
147
     * @return bool
148
     */
149
    public function hasMethod(string $sMethod): bool
150
    {
151
        return $this->xReflectionClass->hasMethod($sMethod);
152
    }
153
154
    /**
155
     * Call the specified method of the component using the specified array of arguments
156
     *
157
     * @param string $sMethod    The method name
158
     * @param array $aArgs    The method arguments
159
     * @param bool $bAccessible    If false, only calls to public method are allowed
160
     *
161
     * @return void
162
     * @throws ReflectionException
163
     */
164
    private function callMethod(string $sMethod, array $aArgs, bool $bAccessible): void
165
    {
166
        $reflectionMethod = $this->xReflectionClass->getMethod($sMethod);
167
        // Make it possible to call protected methods
168
        $reflectionMethod->setAccessible($bAccessible);
169
        $reflectionMethod->invokeArgs($this->xComponent, $aArgs);
170
    }
171
172
    /**
173
     * Call the specified method of the component using the specified array of arguments
174
     *
175
     * @param array $aHookMethods    The method config options
176
     *
177
     * @return void
178
     * @throws ReflectionException
179
     */
180
    private function callHookMethods(array $aHookMethods): void
181
    {
182
        $sMethod = $this->xTarget->getMethodName();
183
        // The hooks defined at method level are merged with those defined at class level.
184
        $aMethods = array_merge($aHookMethods['*'] ?? [], $aHookMethods[$sMethod] ?? []);
185
        foreach($aMethods as $xKey => $xValue)
186
        {
187
            $sHookName = $xValue;
188
            $aHookArgs = [];
189
            if(is_string($xKey))
190
            {
191
                $sHookName = $xKey;
192
                $aHookArgs = is_array($xValue) ? $xValue : [$xValue];
193
            }
194
            $this->callMethod($sHookName, $aHookArgs, true);
195
        }
196
    }
197
198
    /**
199
     * @param object $xComponent
200
     * @param string $sAttr
201
     * @param object $xDiValue
202
     * @param-closure-this object $cSetter
203
     *
204
     * @return void
205
     */
206
    private function setDiAttribute($xComponent, string $sAttr, $xDiValue, Closure $cSetter): void
207
    {
208
        // Allow the setter to access protected attributes.
209
        $cSetter = $cSetter->bindTo($xComponent, $xComponent);
210
        call_user_func($cSetter, $sAttr, $xDiValue);
211
    }
212
213
    /**
214
     * @param mixed $xComponent
215
     * @param array $aDiOptions
216
     *
217
     * @return void
218
     */
219
    private function setDiAttributes($xComponent, array $aDiOptions): void
220
    {
221
        // Set the protected attributes of the object
222
        $cSetter = function($sAttr, $xDiValue) {
223
            // $this here is related to the registered object instance.
224
            // Warning: dynamic properties will be deprecated in PHP8.2.
225
            $this->$sAttr = $xDiValue;
226
        };
227
        foreach($aDiOptions as $sAttr => $sClass)
228
        {
229
            $this->setDiAttribute($xComponent, $sAttr, $this->di->get($sClass), $cSetter);
230
        }
231
    }
232
233
    /**
234
     * @param mixed $xComponent
235
     *
236
     * @return void
237
     */
238
    public function setDiClassAttributes($xComponent): void
239
    {
240
        $aDiOptions = $this->xOptions->diOptions();
241
        $this->setDiAttributes($xComponent, $aDiOptions['*'] ?? []);
242
    }
243
244
    /**
245
     * @param mixed $xComponent
246
     * @param string $sMethodName
247
     *
248
     * @return void
249
     */
250
    public function setDiMethodAttributes($xComponent, string $sMethodName): void
251
    {
252
        $aDiOptions = $this->xOptions->diOptions();
253
        $this->setDiAttributes($xComponent, $aDiOptions[$sMethodName] ?? []);
254
    }
255
256
    /**
257
     * Call the specified method of the component using the specified array of arguments
258
     *
259
     * @param Target $xTarget The target of the Jaxon call
260
     *
261
     * @return void
262
     * @throws ReflectionException
263
     * @throws SetupException
264
     */
265
    public function call(Target $xTarget): void
266
    {
267
        $this->xTarget = $xTarget;
268
        $this->xComponent = $this->cdi->getTargetComponent($this->getClassName(), $xTarget);
269
270
        // Methods to call before processing the request
271
        $this->callHookMethods($this->xOptions->beforeMethods());
272
273
        // Call the request method
274
        $this->callMethod($xTarget->getMethodName(), $this->xTarget->args(), false);
275
276
        // Methods to call after processing the request
277
        $this->callHookMethods($this->xOptions->afterMethods());
278
    }
279
}
280