ComponentOptions::afterMethods()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
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_unique;
23
use function array_values;
24
use function count;
25
use function explode;
26
use function in_array;
27
use function is_array;
28
use function is_string;
29
use function json_encode;
30
use function substr;
31
use function str_replace;
32
use function trim;
33
34
class ComponentOptions
35
{
36
    /**
37
     * Check if the js code for this object must be generated
38
     *
39
     * @var bool
40
     */
41
    private $bExcluded = false;
42
43
    /**
44
     * The character to use as separator in javascript class names
45
     *
46
     * @var string
47
     */
48
    private $sSeparator = '.';
49
50
    /**
51
     * A list of methods of the user registered callable object the library can export to javascript
52
     *
53
     * @var array
54
     */
55
    private $aPublicMethods = [];
56
57
    /**
58
     * The methods in the export attributes
59
     *
60
     * @var array
61
     */
62
    private $aExportMethods = ['except' => []];
63
64
    /**
65
     * A list of methods to call before processing the request
66
     *
67
     * @var array
68
     */
69
    private $aBeforeMethods = [];
70
71
    /**
72
     * A list of methods to call after processing the request
73
     *
74
     * @var array
75
     */
76
    private $aAfterMethods = [];
77
78
    /**
79
     * The javascript class options
80
     *
81
     * @var array
82
     */
83
    private $aJsOptions = [];
84
85
    /**
86
     * The DI options
87
     *
88
     * @var array
89
     */
90
    private $aDiOptions = [];
91
92
    /**
93
     * The constructor
94
     *
95
     * @param array $aMethods
96
     * @param array $aOptions
97
     * @param Metadata|null $xMetadata
98
     */
99
    public function __construct(array $aMethods, array $aOptions, Metadata|null $xMetadata)
100
    {
101
        $this->bExcluded = ($xMetadata?->isExcluded() ?? false) ||
102
            (bool)($aOptions['excluded'] ?? false);
103
104
        // Options from the config.
105
        $sSeparator = $aOptions['separator'] ?? '.';
106
        $this->sSeparator = $sSeparator === '_' ? '_' : '.';
107
        $this->addProtectedMethods($aOptions['protected'] ?? []);
108
        $this->setExportMethods($aOptions['export'] ?? []);
109
        foreach($aOptions['functions'] ?? [] as $sNames => $aFunctionOptions)
110
        {
111
            // Names are in a comma-separated list.
112
            $this->setFunctionProperties(explode(',', $sNames), $aFunctionOptions);
113
        }
114
115
        // Options from the attributes or annotations.
116
        if($xMetadata !== null)
117
        {
118
            $this->setExportMethods($xMetadata->getExportMethods());
119
            foreach($xMetadata->getProperties() as $sFunctionName => $aFunctionOptions)
120
            {
121
                $this->setFunctionProperties([$sFunctionName], $aFunctionOptions);
122
            }
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
        $aMethods = is_string($xMethods) ? [trim($xMethods)] :
0 ignored issues
show
introduced by
The condition is_string($xMethods) is always false.
Loading history...
136
            array_map(fn($sMethod) => trim((string)$sMethod), $xMethods);
137
        $this->aExportMethods['except'] = [...$this->aExportMethods['except'], ...$aMethods];
138
    }
139
140
    /**
141
     * @param array $aExportMethods
142
     *
143
     * @return void
144
     */
145
    private function setExportMethods(array $aExportMethods): void
146
    {
147
        foreach(['base', 'only', 'except'] as $sKey)
148
        {
149
            if(is_array($aExportMethods[$sKey] ?? false))
150
            {
151
                $this->aExportMethods[$sKey] = array_unique([
152
                    ...($this->aExportMethods[$sKey] ?? []),
153
                    ...$aExportMethods[$sKey]
154
                ]);
155
            }
156
        }
157
    }
158
159
    /**
160
     * @param array<string> $aFunctionNames
161
     * @param array $aFunctionOptions
162
     *
163
     * @return void
164
     */
165
    private function setFunctionProperties(array $aFunctionNames, array $aFunctionOptions): void
166
    {
167
        foreach($aFunctionNames as $sFunctionName)
168
        {
169
            $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
170
        }
171
    }
172
173
    /**
174
     * @param array $aMethods
175
     *
176
     * @return array
177
     */
178
    private function filterPublicMethods(array $aMethods): array
179
    {
180
        if($this->bExcluded || in_array('*', $this->aExportMethods['except']))
181
        {
182
            return [];
183
        }
184
185
        $aBaseMethods = $aMethods[1];
186
        $aNoMethods = $aMethods[2];
187
        $aMethods = $aMethods[0];
188
        if(isset($this->aExportMethods['only']))
189
        {
190
            $aMethods = array_intersect($aMethods, $this->aExportMethods['only']);
191
        }
192
        $aMethods = array_diff($aMethods, $this->aExportMethods['except']);
193
        if(count($aBaseMethods) > 0 && isset($this->aExportMethods['base']))
194
        {
195
            $aBaseMethods = array_diff($aBaseMethods, $this->aExportMethods['base']);
196
        }
197
198
        return array_values(array_diff($aMethods, $aBaseMethods, $aNoMethods));
199
    }
200
201
    /**
202
     * @return array
203
     */
204
    public function getPublicMethods(): array
205
    {
206
        return $this->aPublicMethods;
207
    }
208
209
    /**
210
     * @param string $sMethodName
211
     *
212
     * @return bool
213
     */
214
    public function isPublicMethod(string $sMethodName): bool
215
    {
216
        return in_array($sMethodName, $this->aPublicMethods);
217
    }
218
219
    /**
220
     * Check if the js code for this object must be generated
221
     *
222
     * @return bool
223
     */
224
    public function excluded(): bool
225
    {
226
        return $this->bExcluded;
227
    }
228
229
    /**
230
     * @return string
231
     */
232
    public function separator(): string
233
    {
234
        return $this->sSeparator;
235
    }
236
237
    /**
238
     * @return array
239
     */
240
    public function beforeMethods(): array
241
    {
242
        return $this->aBeforeMethods;
243
    }
244
245
    /**
246
     * @return array
247
     */
248
    public function afterMethods(): array
249
    {
250
        return $this->aAfterMethods;
251
    }
252
253
    /**
254
     * @return array
255
     */
256
    public function diOptions(): array
257
    {
258
        return $this->aDiOptions;
259
    }
260
261
    /**
262
     * @return array
263
     */
264
    public function jsOptions(): array
265
    {
266
        return $this->aJsOptions;
267
    }
268
269
    /**
270
     * Set hook methods
271
     *
272
     * @param array $aHookMethods    The array of hook methods
273
     * @param string|array $xValue    The value of the configuration option
274
     *
275
     * @return void
276
     */
277
    private function setHookMethods(array &$aHookMethods, $xValue): void
278
    {
279
        foreach($xValue as $sCalledMethod => $xMethodToCall)
280
        {
281
            if(!isset($aHookMethods[$sCalledMethod]))
282
            {
283
                $aHookMethods[$sCalledMethod] = [];
284
            }
285
            if(is_array($xMethodToCall))
286
            {
287
                $aHookMethods[$sCalledMethod] = [...$aHookMethods[$sCalledMethod], ...$xMethodToCall];
288
                continue;
289
            }
290
            if(is_string($xMethodToCall))
291
            {
292
                $aHookMethods[$sCalledMethod][] = $xMethodToCall;
293
            }
294
        }
295
    }
296
297
    /**
298
     * @param array $aDiOptions
299
     */
300
    private function addDiOption(array $aDiOptions): void
301
    {
302
        $this->aDiOptions = [...$this->aDiOptions, ...$aDiOptions];
303
    }
304
305
    /**
306
     * Set configuration options / call options for each method
307
     *
308
     * @param string $sName    The name of the configuration option
309
     * @param string|array $xValue    The value of the configuration option
310
     *
311
     * @return void
312
     */
313
    private function addOption(string $sName, $xValue): void
314
    {
315
        switch($sName)
316
        {
317
        // Set the methods to call before processing the request
318
        case '__before':
319
            $this->setHookMethods($this->aBeforeMethods, $xValue);
320
            break;
321
        // Set the methods to call after processing the request
322
        case '__after':
323
            $this->setHookMethods($this->aAfterMethods, $xValue);
324
            break;
325
        // Set the attributes to inject in the callable object
326
        case '__di':
327
            $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

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