Passed
Push — main ( 27211a...7f530d )
by Thierry
03:53
created

ComponentOptions::getPublicMethods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * ComponentOptions.php
5
 *
6
 * Options of a callable object.
7
 *
8
 * @package jaxon-core
9
 * @author Thierry Feuzeu <[email protected]>
10
 * @copyright 2024 Thierry Feuzeu <[email protected]>
11
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
12
 * @link https://github.com/jaxon-php/jaxon-core
13
 */
14
15
namespace Jaxon\Plugin\Request\CallableClass;
16
17
use Jaxon\App\Metadata\Metadata;
18
19
use function array_diff;
20
use function array_intersect;
21
use function array_map;
22
use function array_merge;
23
use function array_unique;
24
use function array_values;
25
use function count;
26
use function explode;
27
use function in_array;
28
use function is_array;
29
use function is_string;
30
use function json_encode;
31
use function substr;
32
use function str_replace;
33
use function trim;
34
35
class ComponentOptions
36
{
37
    /**
38
     * Check if the js code for this object must be generated
39
     *
40
     * @var bool
41
     */
42
    private $bExcluded = false;
43
44
    /**
45
     * The character to use as separator in javascript class names
46
     *
47
     * @var string
48
     */
49
    private $sSeparator = '.';
50
51
    /**
52
     * A list of methods of the user registered callable object the library can export to javascript
53
     *
54
     * @var array
55
     */
56
    private $aPublicMethods = [];
57
58
    /**
59
     * The methods in the export attributes
60
     *
61
     * @var array
62
     */
63
    private $aExportMethods = ['except' => []];
64
65
    /**
66
     * A list of methods to call before processing the request
67
     *
68
     * @var array
69
     */
70
    private $aBeforeMethods = [];
71
72
    /**
73
     * A list of methods to call after processing the request
74
     *
75
     * @var array
76
     */
77
    private $aAfterMethods = [];
78
79
    /**
80
     * The javascript class options
81
     *
82
     * @var array
83
     */
84
    private $aJsOptions = [];
85
86
    /**
87
     * The DI options
88
     *
89
     * @var array
90
     */
91
    private $aDiOptions = [];
92
93
    /**
94
     * The constructor
95
     *
96
     * @param array $aMethods
97
     * @param array $aOptions
98
     * @param Metadata|null $xMetadata
99
     */
100
    public function __construct(array $aMethods, array $aOptions, Metadata|null $xMetadata)
101
    {
102
        $this->bExcluded = ($xMetadata?->isExcluded() ?? false) ||
103
            (bool)($aOptions['excluded'] ?? false);
104
        if($this->bExcluded)
105
        {
106
            return;
107
        }
108
109
        // Options from the config.
110
        $sSeparator = $aOptions['separator'];
111
        $this->sSeparator = $sSeparator === '_' ? $sSeparator : '.';
112
113
        $this->addProtectedMethods($aOptions['protected']);
114
115
        foreach($aOptions['functions'] as $sNames => $aFunctionOptions)
116
        {
117
            // Names are in a comma-separated list.
118
            $aFunctionNames = explode(',', $sNames);
119
            foreach($aFunctionNames as $sFunctionName)
120
            {
121
                $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
122
            }
123
        }
124
125
        // Options from the attributes or annotations.
126
        if($xMetadata !== null)
127
        {
128
            // Excluded methods must be merged with the existing ones.
129
            $aExportMethods = $xMetadata->getExportMethods();
130
            $aExportMethods['except'] = array_unique(array_merge(
131
                $aExportMethods['except'] ?? [], $this->aExportMethods['except']));
132
            $this->aExportMethods = $aExportMethods;
133
134
            foreach($xMetadata->getProperties() as $sFunctionName => $aFunctionOptions)
135
            {
136
                $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
137
            }
138
        }
139
140
        $this->aPublicMethods = $this->filterPublicMethods($aMethods);
141
    }
142
143
    /**
144
     * @param array|string $xMethods
145
     *
146
     * @return void
147
     */
148
    private function addProtectedMethods(array|string $xMethods): void
149
    {
150
        $this->aExportMethods['except'] = array_merge($this->aExportMethods['except'],
151
            !is_array($xMethods) ? [trim((string)$xMethods)] :
0 ignored issues
show
introduced by
The condition is_array($xMethods) is always true.
Loading history...
152
            array_map(fn($sMethod) => trim((string)$sMethod), $xMethods));
153
    }
154
155
    /**
156
     * @param array $aMethods
157
     *
158
     * @return array
159
     */
160
    private function filterPublicMethods(array $aMethods): array
161
    {
162
        if($this->bExcluded || in_array('*', $this->aExportMethods['except']))
163
        {
164
            return [];
165
        }
166
167
        $aBaseMethods = $aMethods[1];
168
        $aNoMethods = $aMethods[2];
169
        $aMethods = $aMethods[0];
170
        if(isset($this->aExportMethods['only']))
171
        {
172
            $aMethods = array_intersect($aMethods, $this->aExportMethods['only']);
173
        }
174
        $aMethods = array_diff($aMethods, $this->aExportMethods['except']);
175
        if(count($aBaseMethods) > 0 && isset($this->aExportMethods['base']))
176
        {
177
            $aBaseMethods = array_diff($aBaseMethods, $this->aExportMethods['base']);
178
        }
179
180
        return array_values(array_diff($aMethods, $aBaseMethods, $aNoMethods));
181
    }
182
183
    /**
184
     * @return array
185
     */
186
    public function getPublicMethods(): array
187
    {
188
        return $this->aPublicMethods;
189
    }
190
191
    /**
192
     * @param string $sMethodName
193
     *
194
     * @return bool
195
     */
196
    public function isPublicMethod(string $sMethodName): bool
197
    {
198
        return in_array($sMethodName, $this->aPublicMethods);
199
    }
200
201
    /**
202
     * Check if the js code for this object must be generated
203
     *
204
     * @return bool
205
     */
206
    public function excluded(): bool
207
    {
208
        return $this->bExcluded;
209
    }
210
211
    /**
212
     * @return string
213
     */
214
    public function separator(): string
215
    {
216
        return $this->sSeparator;
217
    }
218
219
    /**
220
     * @return array
221
     */
222
    public function beforeMethods(): array
223
    {
224
        return $this->aBeforeMethods;
225
    }
226
227
    /**
228
     * @return array
229
     */
230
    public function afterMethods(): array
231
    {
232
        return $this->aAfterMethods;
233
    }
234
235
    /**
236
     * @return array
237
     */
238
    public function diOptions(): array
239
    {
240
        return $this->aDiOptions;
241
    }
242
243
    /**
244
     * @return array
245
     */
246
    public function jsOptions(): array
247
    {
248
        return $this->aJsOptions;
249
    }
250
251
    /**
252
     * Set hook methods
253
     *
254
     * @param array $aHookMethods    The array of hook methods
255
     * @param string|array $xValue    The value of the configuration option
256
     *
257
     * @return void
258
     */
259
    private function setHookMethods(array &$aHookMethods, $xValue): void
260
    {
261
        foreach($xValue as $sCalledMethod => $xMethodToCall)
262
        {
263
            if(!isset($aHookMethods[$sCalledMethod]))
264
            {
265
                $aHookMethods[$sCalledMethod] = [];
266
            }
267
            if(is_array($xMethodToCall))
268
            {
269
                $aHookMethods[$sCalledMethod] = array_merge($aHookMethods[$sCalledMethod], $xMethodToCall);
270
            }
271
            elseif(is_string($xMethodToCall))
272
            {
273
                $aHookMethods[$sCalledMethod][] = $xMethodToCall;
274
            }
275
        }
276
    }
277
278
    /**
279
     * @param array $aDiOptions
280
     */
281
    private function addDiOption(array $aDiOptions): void
282
    {
283
        $this->aDiOptions = array_merge($this->aDiOptions, $aDiOptions);
284
    }
285
286
    /**
287
     * Set configuration options / call options for each method
288
     *
289
     * @param string $sName    The name of the configuration option
290
     * @param string|array $xValue    The value of the configuration option
291
     *
292
     * @return void
293
     */
294
    private function addOption(string $sName, $xValue): void
295
    {
296
        switch($sName)
297
        {
298
        // Set the methods to call before processing the request
299
        case '__before':
300
            $this->setHookMethods($this->aBeforeMethods, $xValue);
301
            break;
302
        // Set the methods to call after processing the request
303
        case '__after':
304
            $this->setHookMethods($this->aAfterMethods, $xValue);
305
            break;
306
        // Set the attributes to inject in the callable object
307
        case '__di':
308
            $this->addDiOption($xValue);
0 ignored issues
show
Bug introduced by
It seems like $xValue can also be of type string; however, parameter $aDiOptions of Jaxon\Plugin\Request\Cal...tOptions::addDiOption() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

308
            $this->addDiOption(/** @scrutinizer ignore-type */ $xValue);
Loading history...
309
            break;
310
        default:
311
            break;
312
        }
313
    }
314
315
    /**
316
     * @param string $sFunctionName
317
     * @param string $sOptionName
318
     * @param mixed $xOptionValue
319
     *
320
     * @return void
321
     */
322
    private function _addJsArrayOption(string $sFunctionName, string $sOptionName, $xOptionValue): void
323
    {
324
        if(is_string($xOptionValue))
325
        {
326
            $xOptionValue = [$xOptionValue];
327
        }
328
        if(!is_array($xOptionValue))
329
        {
330
            return; // Do not save.
331
        }
332
333
        $aOptions = $this->aJsOptions[$sFunctionName][$sOptionName] ?? [];
334
        $this->aJsOptions[$sFunctionName][$sOptionName] = array_merge($aOptions, $xOptionValue);
335
    }
336
337
    /**
338
     * @param string $sFunctionName
339
     * @param string $sOptionName
340
     * @param mixed $xOptionValue
341
     *
342
     * @return void
343
     */
344
    private function _setJsOption(string $sFunctionName, string $sOptionName, $xOptionValue): void
345
    {
346
        $this->aJsOptions[$sFunctionName][$sOptionName] = $xOptionValue;
347
    }
348
349
    /**
350
     * @param string $sFunctionName
351
     * @param string $sOptionName
352
     * @param mixed $xOptionValue
353
     *
354
     * @return void
355
     */
356
    private function addJsOption(string $sFunctionName, string $sOptionName, $xOptionValue): void
357
    {
358
        switch($sOptionName)
359
        {
360
        case 'excluded':
361
            if((bool)$xOptionValue)
362
            {
363
                $this->addProtectedMethods($sFunctionName);
364
            }
365
            break;
366
        // For databags and callbacks, all the value are merged in a single array.
367
        case 'bags':
368
        case 'callback':
369
            $this->_addJsArrayOption($sFunctionName, $sOptionName, $xOptionValue);
370
            return;
371
        // For all the other options, only the last value is kept.
372
        default:
373
            $this->_setJsOption($sFunctionName, $sOptionName, $xOptionValue);
374
        }
375
    }
376
377
    /**
378
     * @param string $sFunctionName
379
     * @param array $aFunctionOptions
380
     *
381
     * @return void
382
     */
383
    private function addFunctionOptions(string $sFunctionName, array $aFunctionOptions): void
384
    {
385
        foreach($aFunctionOptions as $sOptionName => $xOptionValue)
386
        {
387
            substr($sOptionName, 0, 2) === '__' ?
388
                // Options for PHP classes. They start with "__".
389
                $this->addOption($sOptionName, [$sFunctionName => $xOptionValue]) :
390
                // Options for javascript code.
391
                $this->addJsOption($sFunctionName, $sOptionName, $xOptionValue);
392
        }
393
    }
394
395
    /**
396
     * @param string $sMethodName
397
     *
398
     * @return array
399
     */
400
    private function getMethodOptions(string $sMethodName): array
401
    {
402
        // First take the common options.
403
        $aOptions = array_merge($this->aJsOptions['*'] ?? []); // Clone the array
404
        // Then add the method options.
405
        $aMethodOptions = $this->aJsOptions[$sMethodName] ?? [];
406
        foreach($aMethodOptions as $sOptionName => $xOptionValue)
407
        {
408
            // For databags and callbacks, merge the values in a single array.
409
            // For all the other options, keep the last value.
410
            $aOptions[$sOptionName] = !in_array($sOptionName, ['bags', 'callback']) ?
411
                $xOptionValue :
412
                array_unique(array_merge($aOptions[$sOptionName] ?? [], $xOptionValue));
413
        }
414
        // Since callbacks are js object names, they need a special formatting.
415
        if(isset($aOptions['callback']))
416
        {
417
            $aOptions['callback'] = str_replace('"', '', json_encode($aOptions['callback']));
418
        }
419
420
        return array_map(fn($xOption) =>
421
            is_array($xOption) ? json_encode($xOption) : $xOption, $aOptions);
422
    }
423
424
    /**
425
     * Return a list of methods of the component to export to javascript
426
     *
427
     * @return array
428
     */
429
    public function getCallableMethods(): array
430
    {
431
        // Get the method options, and convert each of them to
432
        // a string to be displayed in the js script template.
433
        return array_map(fn($sMethodName) => [
434
            'name' => $sMethodName,
435
            'options' => $this->getMethodOptions($sMethodName),
436
        ], $this->aPublicMethods);
437
    }
438
}
439