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