Passed
Push — main ( 7f530d...7aae9b )
by Thierry
05:54
created

ComponentOptions::getMethodOptions()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 3
b 0
f 0
nc 6
nop 1
dl 0
loc 22
rs 9.6111
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
105
        // Options from the config.
106
        $sSeparator = $aOptions['separator'] ?? '.';
107
        $this->sSeparator = $sSeparator === '_' ? '_' : '.';
108
        $this->addProtectedMethods($aOptions['protected'] ?? []);
109
        foreach($aOptions['functions'] ?? [] as $sNames => $aFunctionOptions)
110
        {
111
            // Names are in a comma-separated list.
112
            $aFunctionNames = explode(',', $sNames);
113
            foreach($aFunctionNames as $sFunctionName)
114
            {
115
                $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
116
            }
117
        }
118
119
        // Options from the attributes or annotations.
120
        if($xMetadata !== null)
121
        {
122
            $this->readMetadataOptions($xMetadata);
123
        }
124
125
        $this->aPublicMethods = $this->filterPublicMethods($aMethods);
126
    }
127
128
    /**
129
     * @param array|string $xMethods
130
     *
131
     * @return void
132
     */
133
    private function addProtectedMethods(array|string $xMethods): void
134
    {
135
        $this->aExportMethods['except'] = array_merge($this->aExportMethods['except'],
136
            !is_array($xMethods) ? [trim((string)$xMethods)] :
0 ignored issues
show
introduced by
The condition is_array($xMethods) is always true.
Loading history...
137
            array_map(fn($sMethod) => trim((string)$sMethod), $xMethods));
138
    }
139
140
    /**
141
     * @param Metadata $xMetadata
142
     *
143
     * @return void
144
     */
145
    private function readMetadataOptions(Metadata $xMetadata): void
146
    {
147
        // Excluded methods must be merged with the existing ones.
148
        $aExportMethods = $xMetadata->getExportMethods();
149
        $aExportMethods['except'] = array_unique(array_merge(
150
            $aExportMethods['except'] ?? [], $this->aExportMethods['except']));
151
        $this->aExportMethods = $aExportMethods;
152
        foreach($xMetadata->getProperties() as $sFunctionName => $aFunctionOptions)
153
        {
154
            $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
155
        }
156
    }
157
158
    /**
159
     * @param array $aMethods
160
     *
161
     * @return array
162
     */
163
    private function filterPublicMethods(array $aMethods): array
164
    {
165
        if($this->bExcluded || in_array('*', $this->aExportMethods['except']))
166
        {
167
            return [];
168
        }
169
170
        $aBaseMethods = $aMethods[1];
171
        $aNoMethods = $aMethods[2];
172
        $aMethods = $aMethods[0];
173
        if(isset($this->aExportMethods['only']))
174
        {
175
            $aMethods = array_intersect($aMethods, $this->aExportMethods['only']);
176
        }
177
        $aMethods = array_diff($aMethods, $this->aExportMethods['except']);
178
        if(count($aBaseMethods) > 0 && isset($this->aExportMethods['base']))
179
        {
180
            $aBaseMethods = array_diff($aBaseMethods, $this->aExportMethods['base']);
181
        }
182
183
        return array_values(array_diff($aMethods, $aBaseMethods, $aNoMethods));
184
    }
185
186
    /**
187
     * @return array
188
     */
189
    public function getPublicMethods(): array
190
    {
191
        return $this->aPublicMethods;
192
    }
193
194
    /**
195
     * @param string $sMethodName
196
     *
197
     * @return bool
198
     */
199
    public function isPublicMethod(string $sMethodName): bool
200
    {
201
        return in_array($sMethodName, $this->aPublicMethods);
202
    }
203
204
    /**
205
     * Check if the js code for this object must be generated
206
     *
207
     * @return bool
208
     */
209
    public function excluded(): bool
210
    {
211
        return $this->bExcluded;
212
    }
213
214
    /**
215
     * @return string
216
     */
217
    public function separator(): string
218
    {
219
        return $this->sSeparator;
220
    }
221
222
    /**
223
     * @return array
224
     */
225
    public function beforeMethods(): array
226
    {
227
        return $this->aBeforeMethods;
228
    }
229
230
    /**
231
     * @return array
232
     */
233
    public function afterMethods(): array
234
    {
235
        return $this->aAfterMethods;
236
    }
237
238
    /**
239
     * @return array
240
     */
241
    public function diOptions(): array
242
    {
243
        return $this->aDiOptions;
244
    }
245
246
    /**
247
     * @return array
248
     */
249
    public function jsOptions(): array
250
    {
251
        return $this->aJsOptions;
252
    }
253
254
    /**
255
     * Set hook methods
256
     *
257
     * @param array $aHookMethods    The array of hook methods
258
     * @param string|array $xValue    The value of the configuration option
259
     *
260
     * @return void
261
     */
262
    private function setHookMethods(array &$aHookMethods, $xValue): void
263
    {
264
        foreach($xValue as $sCalledMethod => $xMethodToCall)
265
        {
266
            if(!isset($aHookMethods[$sCalledMethod]))
267
            {
268
                $aHookMethods[$sCalledMethod] = [];
269
            }
270
            if(is_array($xMethodToCall))
271
            {
272
                $aHookMethods[$sCalledMethod] =
273
                    array_merge($aHookMethods[$sCalledMethod], $xMethodToCall);
274
                continue;
275
            }
276
            if(is_string($xMethodToCall))
277
            {
278
                $aHookMethods[$sCalledMethod][] = $xMethodToCall;
279
            }
280
        }
281
    }
282
283
    /**
284
     * @param array $aDiOptions
285
     */
286
    private function addDiOption(array $aDiOptions): void
287
    {
288
        $this->aDiOptions = array_merge($this->aDiOptions, $aDiOptions);
289
    }
290
291
    /**
292
     * Set configuration options / call options for each method
293
     *
294
     * @param string $sName    The name of the configuration option
295
     * @param string|array $xValue    The value of the configuration option
296
     *
297
     * @return void
298
     */
299
    private function addOption(string $sName, $xValue): void
300
    {
301
        switch($sName)
302
        {
303
        // Set the methods to call before processing the request
304
        case '__before':
305
            $this->setHookMethods($this->aBeforeMethods, $xValue);
306
            break;
307
        // Set the methods to call after processing the request
308
        case '__after':
309
            $this->setHookMethods($this->aAfterMethods, $xValue);
310
            break;
311
        // Set the attributes to inject in the callable object
312
        case '__di':
313
            $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

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