registerComponentsInDirectories()   B
last analyzed

Complexity

Conditions 8
Paths 6

Size

Total Lines 33
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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