Passed
Push — main ( 87fed9...27211a )
by Thierry
04:04
created

ComponentOptions::separator()   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
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 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
     * A list of methods of the user registered callable object the library must not export to javascript
59
     *
60
     * @var array
61
     */
62
    private $aProtectedMethods = [];
63
64
    /**
65
     * The methods in the export attributes
66
     *
67
     * @var array
68
     */
69
    private $aExportMethods = [];
70
71
    /**
72
     * A list of methods to call before processing the request
73
     *
74
     * @var array
75
     */
76
    private $aBeforeMethods = [];
77
78
    /**
79
     * A list of methods to call after processing the request
80
     *
81
     * @var array
82
     */
83
    private $aAfterMethods = [];
84
85
    /**
86
     * The javascript class options
87
     *
88
     * @var array
89
     */
90
    private $aJsOptions = [];
91
92
    /**
93
     * The DI options
94
     *
95
     * @var array
96
     */
97
    private $aDiOptions = [];
98
99
    /**
100
     * The constructor
101
     *
102
     * @param array $aMethods
103
     * @param array $aOptions
104
     * @param Metadata|null $xMetadata
105
     */
106
    public function __construct(array $aMethods, array $aOptions, Metadata|null $xMetadata)
107
    {
108
        $this->bExcluded = ($xMetadata?->isExcluded() ?? false) ||
109
            (bool)($aOptions['excluded'] ?? false);
110
        if($this->bExcluded)
111
        {
112
            return;
113
        }
114
115
        $sSeparator = $aOptions['separator'];
116
        if($sSeparator === '_' || $sSeparator === '.')
117
        {
118
            $this->sSeparator = $sSeparator;
119
        }
120
        $this->addProtectedMethods($aOptions['protected']);
121
122
        foreach($aOptions['functions'] as $sNames => $aFunctionOptions)
123
        {
124
            // Names are in a comma-separated list.
125
            $aFunctionNames = explode(',', $sNames);
126
            foreach($aFunctionNames as $sFunctionName)
127
            {
128
                $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
129
            }
130
        }
131
132
        if($xMetadata !== null)
133
        {
134
            $this->aExportMethods = $xMetadata->getExportMethods();
135
            $this->addProtectedMethods($xMetadata->getProtectedMethods());
136
            foreach($xMetadata->getProperties() as $sFunctionName => $aFunctionOptions)
137
            {
138
                $this->addFunctionOptions($sFunctionName, $aFunctionOptions);
139
            }
140
        }
141
142
        $this->aPublicMethods = $this->filterPublicMethods($aMethods);
143
    }
144
145
    /**
146
     * @param array|string $xMethods
147
     *
148
     * @return void
149
     */
150
    private function addProtectedMethods(array|string $xMethods): void
151
    {
152
        $this->aProtectedMethods = array_merge($this->aProtectedMethods,
153
            !is_array($xMethods) ? [trim((string)$xMethods)] :
0 ignored issues
show
introduced by
The condition is_array($xMethods) is always true.
Loading history...
154
            array_map(fn($sMethod) => trim((string)$sMethod), $xMethods));
155
    }
156
157
    /**
158
     * @param array $aMethods
159
     *
160
     * @return array
161
     */
162
    private function filterPublicMethods(array $aMethods): array
163
    {
164
        if($this->bExcluded || in_array('*', $this->aProtectedMethods))
165
        {
166
            return [];
167
        }
168
169
        $aBaseMethods = $aMethods[1];
170
        $aMethods = $aMethods[0];
171
172
        if(isset($this->aExportMethods['only']))
173
        {
174
            $aMethods = array_intersect($aMethods, $this->aExportMethods['only']);
175
        }
176
        $aMethods = array_diff($aMethods, $this->aProtectedMethods,
177
            $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_diff($aMethods, $aBaseMethods);
184
    }
185
186
    /**
187
     * @param string $sMethodName
188
     *
189
     * @return bool
190
     */
191
    public function isPublicMethod(string $sMethodName): bool
192
    {
193
        return in_array($sMethodName, $this->aPublicMethods);
194
    }
195
196
    /**
197
     * Check if the js code for this object must be generated
198
     *
199
     * @return bool
200
     */
201
    public function excluded(): bool
202
    {
203
        return $this->bExcluded;
204
    }
205
206
    /**
207
     * @return string
208
     */
209
    public function separator(): string
210
    {
211
        return $this->sSeparator;
212
    }
213
214
    /**
215
     * @return array
216
     */
217
    public function beforeMethods(): array
218
    {
219
        return $this->aBeforeMethods;
220
    }
221
222
    /**
223
     * @return array
224
     */
225
    public function afterMethods(): array
226
    {
227
        return $this->aAfterMethods;
228
    }
229
230
    /**
231
     * @return array
232
     */
233
    public function diOptions(): array
234
    {
235
        return $this->aDiOptions;
236
    }
237
238
    /**
239
     * @return array
240
     */
241
    public function jsOptions(): array
242
    {
243
        return $this->aJsOptions;
244
    }
245
246
    /**
247
     * Set hook methods
248
     *
249
     * @param array $aHookMethods    The array of hook methods
250
     * @param string|array $xValue    The value of the configuration option
251
     *
252
     * @return void
253
     */
254
    private function setHookMethods(array &$aHookMethods, $xValue): void
255
    {
256
        foreach($xValue as $sCalledMethod => $xMethodToCall)
257
        {
258
            if(!isset($aHookMethods[$sCalledMethod]))
259
            {
260
                $aHookMethods[$sCalledMethod] = [];
261
            }
262
            if(is_array($xMethodToCall))
263
            {
264
                $aHookMethods[$sCalledMethod] = array_merge($aHookMethods[$sCalledMethod], $xMethodToCall);
265
            }
266
            elseif(is_string($xMethodToCall))
267
            {
268
                $aHookMethods[$sCalledMethod][] = $xMethodToCall;
269
            }
270
        }
271
    }
272
273
    /**
274
     * @param array $aDiOptions
275
     */
276
    private function addDiOption(array $aDiOptions): void
277
    {
278
        $this->aDiOptions = array_merge($this->aDiOptions, $aDiOptions);
279
    }
280
281
    /**
282
     * Set configuration options / call options for each method
283
     *
284
     * @param string $sName    The name of the configuration option
285
     * @param string|array $xValue    The value of the configuration option
286
     *
287
     * @return void
288
     */
289
    private function addOption(string $sName, $xValue): void
290
    {
291
        switch($sName)
292
        {
293
        // Set the methods to call before processing the request
294
        case '__before':
295
            $this->setHookMethods($this->aBeforeMethods, $xValue);
296
            break;
297
        // Set the methods to call after processing the request
298
        case '__after':
299
            $this->setHookMethods($this->aAfterMethods, $xValue);
300
            break;
301
        // Set the attributes to inject in the callable object
302
        case '__di':
303
            $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

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