ComponentRegistry::setPackageConfig()   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
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * ComponentRegistry.php - Jaxon component registry
5
 *
6
 * This class is the entry point for class, directory and namespace registration.
7
 *
8
 * @package jaxon-core
9
 * @author Thierry Feuzeu <[email protected]>
10
 * @copyright 2019 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 Composer\Autoload\ClassLoader;
18
use Jaxon\Config\Config;
19
use Jaxon\Di\ComponentContainer;
20
21
use function array_merge;
22
use function file_exists;
23
use function is_string;
24
use function str_replace;
25
use function strlen;
26
use function strncmp;
27
use function substr;
28
use function trim;
29
30
class ComponentRegistry
31
{
32
    /**
33
     * The namespace options
34
     *
35
     * These are the options of the registered namespaces.
36
     *
37
     * @var array
38
     */
39
    protected $aNamespaceOptions = [];
40
41
    /**
42
     * The directory options
43
     *
44
     * These are the options of the registered directories.
45
     *
46
     * @var array
47
     */
48
    protected $aDirectoryOptions = [];
49
50
    /**
51
     * The namespaces
52
     *
53
     * These are all the namespaces found in registered directories.
54
     *
55
     * @var array
56
     */
57
    protected $aNamespaces = [];
58
59
    /**
60
     * A config from the package providing the class or directory being registered.
61
     *
62
     * @var Config|null
63
     */
64
    protected $xPackageConfig = null;
65
66
    /**
67
     * The string that will be used to compute the js file hash
68
     *
69
     * @var string
70
     */
71
    protected $sHash = '';
72
73
    /**
74
     * @var array
75
     */
76
    private $aDefaultClassOptions = [
77
        'separator' => '.',
78
        'protected' => [],
79
        'functions' => [],
80
        'timestamp' => 0,
81
    ];
82
83
    /**
84
     * @var bool
85
     */
86
    protected $bDirectoriesParsed = false;
87
88
    /**
89
     * @var bool
90
     */
91
    protected $bNamespacesParsed = false;
92
93
    /**
94
     * The Composer autoloader
95
     *
96
     * @var ClassLoader
97
     */
98
    private $xAutoloader = null;
99
100
    /**
101
     * @var bool
102
     */
103
    private $bUpdateHash = true;
104
105
    /**
106
     * The class constructor
107
     *
108
     * @param ComponentContainer $cdi
109
     */
110
    public function __construct(protected ComponentContainer $cdi)
111
    {
112
        // Set the composer autoloader
113
        if(file_exists(($sAutoloadFile = __DIR__ . '/../../../../../../autoload.php')) ||
114
            file_exists(($sAutoloadFile = __DIR__ . '/../../../../../vendor/autoload.php')) ||
115
            file_exists(($sAutoloadFile = __DIR__ . '/../../../../vendor/autoload.php')))
116
        {
117
            $this->xAutoloader = require($sAutoloadFile);
118
        }
119
    }
120
121
    /**
122
     * @param Config $xConfig
123
     *
124
     * @return void
125
     */
126
    public function setPackageConfig(Config $xConfig): void
127
    {
128
        $this->xPackageConfig = $xConfig;
129
    }
130
131
    /**
132
     * @return void
133
     */
134
    public function unsetPackageConfig(): void
135
    {
136
        $this->xPackageConfig = null;
137
    }
138
139
    /**
140
     * Get all registered namespaces
141
     *
142
     * @return array
143
     */
144
    public function getNamespaces(): array
145
    {
146
        return $this->aNamespaces;
147
    }
148
149
    /**
150
     * Get the hash
151
     *
152
     * @return string
153
     */
154
    public function getHash(): string
155
    {
156
        return $this->sHash;
157
    }
158
159
    /**
160
     * Enable or disable hash calculation
161
     *
162
     * @param bool $bUpdateHash
163
     *
164
     * @return void
165
     */
166
    public function updateHash(bool $bUpdateHash): void
167
    {
168
        $this->bUpdateHash = $bUpdateHash;
169
    }
170
171
    /**
172
     * Get a given class options from specified directory options
173
     *
174
     * @param string $sClassName    The class name
175
     * @param array $aClassOptions    The default class options
176
     * @param array $aDirectoryOptions    The directory options
177
     *
178
     * @return array
179
     */
180
    private function makeClassOptions(string $sClassName, array $aClassOptions, array $aDirectoryOptions): array
181
    {
182
        foreach($this->aDefaultClassOptions as $sOption => $xValue)
183
        {
184
            if(!isset($aClassOptions[$sOption]))
185
            {
186
                $aClassOptions[$sOption] = $xValue;
187
            }
188
        }
189
        $aClassOptions['excluded'] = (bool)($aClassOptions['excluded'] ?? false); // Convert to bool.
190
        if(is_string($aClassOptions['protected']))
191
        {
192
            $aClassOptions['protected'] = [$aClassOptions['protected']]; // Convert to array.
193
        }
194
195
        $aDirectoryOptions['functions'] = []; // The 'functions' section is not allowed here.
196
        $aOptionGroups = [
197
            $aDirectoryOptions, // Options at directory level
198
            $aDirectoryOptions['classes']['*'] ?? [], // Options for all classes
199
            $aDirectoryOptions['classes'][$sClassName] ?? [], // Options for this specific class
200
        ];
201
        foreach($aOptionGroups as $aOptionGroup)
202
        {
203
            if(isset($aOptionGroup['separator']))
204
            {
205
                $aClassOptions['separator'] = (string)$aOptionGroup['separator'];
206
            }
207
            if(isset($aOptionGroup['excluded']))
208
            {
209
                $aClassOptions['excluded'] = (bool)$aOptionGroup['excluded'];
210
            }
211
            if(isset($aOptionGroup['protected']))
212
            {
213
                if(is_string($aOptionGroup['protected']))
214
                {
215
                    $aOptionGroup['protected'] = [$aOptionGroup['protected']]; // Convert to array.
216
                }
217
                $aClassOptions['protected'] = array_merge($aClassOptions['protected'], $aOptionGroup['protected']);
218
            }
219
            if(isset($aOptionGroup['functions']))
220
            {
221
                $aClassOptions['functions'] = array_merge($aClassOptions['functions'], $aOptionGroup['functions']);
222
            }
223
        }
224
        if(isset($aDirectoryOptions['config']) && !isset($aClassOptions['config']))
225
        {
226
            $aClassOptions['config'] = $aDirectoryOptions['config'];
227
        }
228
229
        return $aClassOptions;
230
    }
231
232
    /**
233
     * Register a component
234
     *
235
     * @param string $sClassName        The class name
236
     * @param array $aClassOptions      The default class options
237
     * @param array $aDirectoryOptions  The directory options
238
     *
239
     * @return void
240
     */
241
    private function _registerComponent(string $sClassName, array $aClassOptions,
242
        array $aDirectoryOptions = []): void
243
    {
244
        $aOptions = $this->makeClassOptions($sClassName, $aClassOptions, $aDirectoryOptions);
245
        $this->cdi->saveComponent($sClassName, $aOptions);
246
        if($this->bUpdateHash)
247
        {
248
            $this->sHash .= $sClassName . $aOptions['timestamp'];
249
        }
250
    }
251
252
    /**
253
     * Register a component
254
     *
255
     * @param string $sClassName    The class name
256
     * @param array $aClassOptions    The default class options
257
     *
258
     * @return void
259
     */
260
    public function registerComponent(string $sClassName, array $aClassOptions): void
261
    {
262
        // For classes, the underscore is used as separator.
263
        $aClassOptions['separator'] = '_';
264
        if($this->xPackageConfig !== null)
265
        {
266
            $aClassOptions['config'] = $this->xPackageConfig;
267
        }
268
        $this->_registerComponent($sClassName, $aClassOptions);
269
    }
270
271
    /**
272
     * Get the options of a component in a registered namespace
273
     *
274
     * @param string $sClassName    The class name
275
     *
276
     * @return array|null
277
     */
278
    public function getNamespaceComponentOptions(string $sClassName): ?array
279
    {
280
        // Find the corresponding namespace
281
        foreach($this->aNamespaceOptions as $sNamespace => $aDirectoryOptions)
282
        {
283
            // Check if the namespace matches the class.
284
            if(strncmp($sClassName, $sNamespace . '\\', strlen($sNamespace) + 1) === 0)
285
            {
286
                // Save the class options
287
                $aClassOptions = ['namespace' => $sNamespace];
288
                return $this->makeClassOptions($sClassName, $aClassOptions, $aDirectoryOptions);
289
            }
290
        }
291
        return null;
292
    }
293
294
    /**
295
     * Register a directory
296
     *
297
     * @param string $sDirectory    The directory being registered
298
     * @param array $aOptions    The associated options
299
     *
300
     * @return void
301
     */
302
    public function registerDirectory(string $sDirectory, array $aOptions): void
303
    {
304
        // For directories without namespace, the underscore is used as separator.
305
        $aOptions['separator'] = '_';
306
        // Set the autoload option default value
307
        if(!isset($aOptions['autoload']))
308
        {
309
            $aOptions['autoload'] = true;
310
        }
311
        if($this->xPackageConfig !== null)
312
        {
313
            $aOptions['config'] = $this->xPackageConfig;
314
        }
315
        $this->aDirectoryOptions[$sDirectory] = $aOptions;
316
    }
317
318
    /**
319
     * Add a namespace
320
     *
321
     * @param string $sNamespace    The namespace
322
     * @param array $aOptions    The associated options
323
     *
324
     * @return void
325
     */
326
    private function addNamespace(string $sNamespace, array $aOptions): void
327
    {
328
        $this->aNamespaces[] = $sNamespace;
329
        $this->sHash .= $sNamespace . $aOptions['separator'];
330
    }
331
332
    /**
333
     * Register a namespace
334
     *
335
     * @param string $sNamespace    The namespace of the directory being registered
336
     * @param array $aOptions    The associated options
337
     *
338
     * @return void
339
     */
340
    public function registerNamespace(string $sNamespace, array $aOptions): void
341
    {
342
        // For namespaces, the dot is used as separator.
343
        $aOptions['separator'] = '.';
344
        // Set the autoload option default value
345
        if(!isset($aOptions['autoload']))
346
        {
347
            $aOptions['autoload'] = true;
348
        }
349
        if($this->xPackageConfig !== null)
350
        {
351
            $aOptions['config'] = $this->xPackageConfig;
352
        }
353
        // Register the dir with PSR4 autoloading
354
        if(($aOptions['autoload']) && $this->xAutoloader != null)
355
        {
356
            $this->xAutoloader->setPsr4($sNamespace . '\\', $aOptions['directory']);
357
        }
358
359
        $this->aNamespaceOptions[$sNamespace] = $aOptions;
360
    }
361
362
    /**
363
     * Read classes from directories registered with namespaces
364
     *
365
     * @return void
366
     */
367
    public function registerComponentsInNamespaces(): void
368
    {
369
        // This is to be done only once.
370
        if($this->bNamespacesParsed)
371
        {
372
            return;
373
        }
374
        $this->bNamespacesParsed = true;
375
376
        // Browse directories with namespaces and read all the files.
377
        $sDS = DIRECTORY_SEPARATOR;
378
        foreach($this->aNamespaceOptions as $sNamespace => $aDirectoryOptions)
379
        {
380
            $this->addNamespace($sNamespace, ['separator' => '.']);
381
382
            // Iterate on dir content
383
            $sDirectory = $aDirectoryOptions['directory'];
384
            $itFile = new SortedFileIterator($sDirectory);
385
            foreach($itFile as $xFile)
386
            {
387
                // skip everything except PHP files
388
                if(!$xFile->isFile() || $xFile->getExtension() !== 'php')
389
                {
390
                    continue;
391
                }
392
393
                // Find the class path (the same as the class namespace)
394
                $sClassPath = $sNamespace;
395
                $sRelativePath = substr($xFile->getPath(), strlen($sDirectory));
396
                $sRelativePath = trim(str_replace($sDS, '\\', $sRelativePath), '\\');
397
                if($sRelativePath !== '')
398
                {
399
                    $sClassPath .= '\\' . $sRelativePath;
400
                }
401
402
                $this->addNamespace($sClassPath, ['separator' => '.']);
403
404
                $sClassName = $sClassPath . '\\' . $xFile->getBasename('.php');
405
                $aClassOptions = [
406
                    'separator' => '.',
407
                    'namespace' => $sNamespace,
408
                    'timestamp' => $xFile->getMTime(),
409
                ];
410
                $this->_registerComponent($sClassName, $aClassOptions, $aDirectoryOptions);
411
            }
412
        }
413
    }
414
415
    /**
416
     * Read classes from directories registered without namespaces
417
     *
418
     * @return void
419
     */
420
    public function registerComponentsInDirectories(): void
421
    {
422
        // This is to be done only once.
423
        if($this->bDirectoriesParsed)
424
        {
425
            return;
426
        }
427
        $this->bDirectoriesParsed = true;
428
429
        // Browse directories without namespaces and read all the files.
430
        foreach($this->aDirectoryOptions as $sDirectory => $aDirectoryOptions)
431
        {
432
            $itFile = new SortedFileIterator($sDirectory);
433
            // Iterate on dir content
434
            foreach($itFile as $xFile)
435
            {
436
                // Skip everything except PHP files
437
                if(!$xFile->isFile() || $xFile->getExtension() !== 'php')
438
                {
439
                    continue;
440
                }
441
442
                $sClassName = $xFile->getBasename('.php');
443
                $aClassOptions = [
444
                    'separator' => '.',
445
                    'timestamp' => $xFile->getMTime(),
446
                ];
447
                if(($aDirectoryOptions['autoload']) && $this->xAutoloader !== null)
448
                {
449
                    // Set classmap autoloading. Must be done before registering the class.
450
                    $this->xAutoloader->addClassMap([$sClassName => $xFile->getPathname()]);
451
                }
452
                $this->_registerComponent($sClassName, $aClassOptions, $aDirectoryOptions);
453
            }
454
        }
455
    }
456
457
    /**
458
     * Register all the components
459
     *
460
     * @return void
461
     */
462
    public function registerAllComponents(): void
463
    {
464
        $this->registerComponentsInNamespaces();
465
        $this->registerComponentsInDirectories();
466
    }
467
}
468