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['protected']))
213
            {
214
                if(is_string($aOptionGroup['protected']))
215
                {
216
                    $aOptionGroup['protected'] = [$aOptionGroup['protected']]; // Convert to array.
217
                }
218
                $aClassOptions['protected'] = array_merge($aClassOptions['protected'], $aOptionGroup['protected']);
219
            }
220
            if(isset($aOptionGroup['functions']))
221
            {
222
                $aClassOptions['functions'] = array_merge($aClassOptions['functions'], $aOptionGroup['functions']);
223
            }
224
        }
225
        if(isset($aDirectoryOptions['config']) && !isset($aClassOptions['config']))
226
        {
227
            $aClassOptions['config'] = $aDirectoryOptions['config'];
228
        }
229
230
        return $aClassOptions;
231
    }
232
233
    /**
234
     * Register a component
235
     *
236
     * @param string $sClassName        The class name
237
     * @param array $aClassOptions      The default class options
238
     * @param array $aDirectoryOptions  The directory options
239
     *
240
     * @return void
241
     */
242
    private function _registerComponent(string $sClassName, array $aClassOptions,
243
        array $aDirectoryOptions = []): void
244
    {
245
        $aOptions = $this->makeClassOptions($sClassName, $aClassOptions, $aDirectoryOptions);
246
        $this->cdi->saveComponent($sClassName, $aOptions);
247
        if($this->bUpdateHash)
248
        {
249
            $this->sHash .= $sClassName . $aOptions['timestamp'];
250
        }
251
    }
252
253
    /**
254
     * Register a component
255
     *
256
     * @param string $sClassName    The class name
257
     * @param array $aClassOptions    The default class options
258
     *
259
     * @return void
260
     */
261
    public function registerComponent(string $sClassName, array $aClassOptions): void
262
    {
263
        // For classes, the underscore is used as separator.
264
        $aClassOptions['separator'] = '_';
265
        if($this->xPackageConfig !== null)
266
        {
267
            $aClassOptions['config'] = $this->xPackageConfig;
268
        }
269
        $this->_registerComponent($sClassName, $aClassOptions);
270
    }
271
272
    /**
273
     * Get the options of a component in a registered namespace
274
     *
275
     * @param string $sClassName    The class name
276
     *
277
     * @return array|null
278
     */
279
    public function getNamespaceComponentOptions(string $sClassName): ?array
280
    {
281
        // Find the corresponding namespace
282
        foreach($this->aNamespaceOptions as $sNamespace => $aDirectoryOptions)
283
        {
284
            // Check if the namespace matches the class.
285
            if(strncmp($sClassName, $sNamespace . '\\', strlen($sNamespace) + 1) === 0)
286
            {
287
                // Save the class options
288
                $aClassOptions = ['namespace' => $sNamespace];
289
                return $this->makeClassOptions($sClassName, $aClassOptions, $aDirectoryOptions);
290
            }
291
        }
292
        return null;
293
    }
294
295
    /**
296
     * Register a directory
297
     *
298
     * @param string $sDirectory    The directory being registered
299
     * @param array $aOptions    The associated options
300
     *
301
     * @return void
302
     */
303
    public function registerDirectory(string $sDirectory, array $aOptions): void
304
    {
305
        // For directories without namespace, the underscore is used as separator.
306
        $aOptions['separator'] = '_';
307
        // Set the autoload option default value
308
        if(!isset($aOptions['autoload']))
309
        {
310
            $aOptions['autoload'] = true;
311
        }
312
        if($this->xPackageConfig !== null)
313
        {
314
            $aOptions['config'] = $this->xPackageConfig;
315
        }
316
        $this->aDirectoryOptions[$sDirectory] = $aOptions;
317
    }
318
319
    /**
320
     * Add a namespace
321
     *
322
     * @param string $sNamespace    The namespace
323
     * @param array $aOptions    The associated options
324
     *
325
     * @return void
326
     */
327
    private function addNamespace(string $sNamespace, array $aOptions): void
328
    {
329
        $this->aNamespaces[] = $sNamespace;
330
        $this->sHash .= $sNamespace . $aOptions['separator'];
331
    }
332
333
    /**
334
     * Register a namespace
335
     *
336
     * @param string $sNamespace    The namespace of the directory being registered
337
     * @param array $aOptions    The associated options
338
     *
339
     * @return void
340
     */
341
    public function registerNamespace(string $sNamespace, array $aOptions): void
342
    {
343
        // For namespaces, the dot is used as separator.
344
        $aOptions['separator'] = '.';
345
        // Set the autoload option default value
346
        if(!isset($aOptions['autoload']))
347
        {
348
            $aOptions['autoload'] = true;
349
        }
350
        if($this->xPackageConfig !== null)
351
        {
352
            $aOptions['config'] = $this->xPackageConfig;
353
        }
354
        // Register the dir with PSR4 autoloading
355
        if(($aOptions['autoload']) && $this->xAutoloader != null)
356
        {
357
            $this->xAutoloader->setPsr4($sNamespace . '\\', $aOptions['directory']);
358
        }
359
360
        $this->aNamespaceOptions[$sNamespace] = $aOptions;
361
    }
362
363
    /**
364
     * Read classes from directories registered with namespaces
365
     *
366
     * @return void
367
     */
368
    public function registerComponentsInNamespaces(): void
369
    {
370
        // This is to be done only once.
371
        if($this->bNamespacesParsed)
372
        {
373
            return;
374
        }
375
        $this->bNamespacesParsed = true;
376
377
        // Browse directories with namespaces and read all the files.
378
        $sDS = DIRECTORY_SEPARATOR;
379
        foreach($this->aNamespaceOptions as $sNamespace => $aDirectoryOptions)
380
        {
381
            $this->addNamespace($sNamespace, ['separator' => '.']);
382
383
            // Iterate on dir content
384
            $sDirectory = $aDirectoryOptions['directory'];
385
            $itFile = new SortedFileIterator($sDirectory);
386
            foreach($itFile as $xFile)
387
            {
388
                // skip everything except PHP files
389
                if(!$xFile->isFile() || $xFile->getExtension() !== 'php')
390
                {
391
                    continue;
392
                }
393
394
                // Find the class path (the same as the class namespace)
395
                $sClassPath = $sNamespace;
396
                $sRelativePath = substr($xFile->getPath(), strlen($sDirectory));
397
                $sRelativePath = trim(str_replace($sDS, '\\', $sRelativePath), '\\');
398
                if($sRelativePath !== '')
399
                {
400
                    $sClassPath .= '\\' . $sRelativePath;
401
                }
402
403
                $this->addNamespace($sClassPath, ['separator' => '.']);
404
405
                $sClassName = $sClassPath . '\\' . $xFile->getBasename('.php');
406
                $aClassOptions = [
407
                    'separator' => '.',
408
                    'namespace' => $sNamespace,
409
                    'timestamp' => $xFile->getMTime(),
410
                ];
411
                $this->_registerComponent($sClassName, $aClassOptions, $aDirectoryOptions);
412
            }
413
        }
414
    }
415
416
    /**
417
     * Read classes from directories registered without namespaces
418
     *
419
     * @return void
420
     */
421
    public function registerComponentsInDirectories(): void
422
    {
423
        // This is to be done only once.
424
        if($this->bDirectoriesParsed)
425
        {
426
            return;
427
        }
428
        $this->bDirectoriesParsed = true;
429
430
        // Browse directories without namespaces and read all the files.
431
        foreach($this->aDirectoryOptions as $sDirectory => $aDirectoryOptions)
432
        {
433
            $itFile = new SortedFileIterator($sDirectory);
434
            // Iterate on dir content
435
            foreach($itFile as $xFile)
436
            {
437
                // Skip everything except PHP files
438
                if(!$xFile->isFile() || $xFile->getExtension() !== 'php')
439
                {
440
                    continue;
441
                }
442
443
                $sClassName = $xFile->getBasename('.php');
444
                $aClassOptions = [
445
                    'separator' => '.',
446
                    'timestamp' => $xFile->getMTime(),
447
                ];
448
                if(($aDirectoryOptions['autoload']) && $this->xAutoloader !== null)
449
                {
450
                    // Set classmap autoloading. Must be done before registering the class.
451
                    $this->xAutoloader->addClassMap([$sClassName => $xFile->getPathname()]);
452
                }
453
                $this->_registerComponent($sClassName, $aClassOptions, $aDirectoryOptions);
454
            }
455
        }
456
    }
457
458
    /**
459
     * Register all the components
460
     *
461
     * @return void
462
     */
463
    public function registerAllComponents(): void
464
    {
465
        $this->registerComponentsInNamespaces();
466
        $this->registerComponentsInDirectories();
467
    }
468
}
469