ClassContainer::useUnderscore()   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
 * ClassContainer.php
5
 *
6
 * Jaxon DI container. Provide container service for the registered classes.
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\Di;
16
17
use Jaxon\App\AbstractCallable;
18
use Jaxon\App\Config\ConfigManager;
19
use Jaxon\App\I18n\Translator;
20
use Jaxon\App\Pagination;
21
use Jaxon\Exception\SetupException;
22
use Jaxon\Plugin\Request\CallableClass\CallableClassHelper;
23
use Jaxon\Plugin\Request\CallableClass\CallableObject;
24
use Jaxon\Plugin\Request\CallableClass\CallableRegistry;
25
use Jaxon\Plugin\Response\Dialog\DialogCommand;
26
use Jaxon\Request\Handler\CallbackManager;
27
use Jaxon\Script\JxnCall;
28
use Jaxon\Script\JxnClass;
29
use Pimple\Container as PimpleContainer;
30
use Closure;
31
use ReflectionClass;
32
use ReflectionException;
33
use ReflectionNamedType;
34
use ReflectionParameter;
35
36
use function array_map;
37
use function str_replace;
38
use function trim;
39
40
class ClassContainer
41
{
42
    /**
43
     * If the underscore is used as separator in js class names.
44
     *
45
     * @var bool
46
     */
47
    private $bUsingUnderscore = false;
48
49
    /**
50
     * The Dependency Injection Container for registered classes
51
     *
52
     * @var PimpleContainer
53
     */
54
    private $xContainer;
55
56
    /**
57
     * The classes
58
     *
59
     * These are all the classes, both registered and found in registered directories.
60
     *
61
     * @var array
62
     */
63
    protected $aClasses = [];
64
65
    /**
66
     * The class constructor
67
     *
68
     * @param Container $di
69
     * @param Translator $xTranslator
70
     */
71
    public function __construct(private Container $di, private Translator $xTranslator)
72
    {
73
        $this->xContainer = new PimpleContainer();
74
        $this->val(ClassContainer::class, $this);
75
76
        // Register the call factory for registered functions
77
        $this->set($this->getRequestFactoryKey(JxnCall::class), function() {
78
            return new JxnCall($this->di->g(DialogCommand::class),
79
                $this->di->g(ConfigManager::class)->getOption('core.prefix.function', ''));
80
        });
81
    }
82
83
    /**
84
     * @return void
85
     */
86
    public function useUnderscore()
87
    {
88
        $this->bUsingUnderscore = true;
89
    }
90
91
    /**
92
     * Check if a class is defined in the container
93
     *
94
     * @param string $sClass    The full class name
95
     *
96
     * @return bool
97
     */
98
    public function has(string $sClass): bool
99
    {
100
        return $this->xContainer->offsetExists($sClass);
101
    }
102
103
    /**
104
     * Get a class instance
105
     *
106
     * @param string $sClass    The full class name
107
     *
108
     * @return mixed
109
     */
110
    public function get(string $sClass)
111
    {
112
        return $this->xContainer->offsetGet($sClass);
113
    }
114
115
    /**
116
     * Save a closure in the container
117
     *
118
     * @param string $sClass    The full class name
119
     * @param Closure $xClosure    The closure
120
     *
121
     * @return void
122
     */
123
    public function set(string $sClass, Closure $xClosure)
124
    {
125
       $this->xContainer->offsetSet($sClass, function() use($xClosure) {
126
            return $xClosure($this);
127
        });
128
    }
129
130
    /**
131
     * Save a value in the container
132
     *
133
     * @param string $sKey    The key
134
     * @param mixed $xValue    The value
135
     *
136
     * @return void
137
     */
138
    public function val(string $sKey, $xValue)
139
    {
140
       $this->xContainer->offsetSet($sKey, $xValue);
141
    }
142
143
    /**
144
     *
145
     * @param string $sClassName    The class name
146
     * @param array $aOptions    The class options
147
     *
148
     * @return void
149
     */
150
    public function registerClass(string $sClassName, array $aOptions = [])
151
    {
152
        try
153
        {
154
            // Make sure the registered class exists
155
            isset($aOptions['include']) && require_once($aOptions['include']);
156
            $xReflectionClass = new ReflectionClass($sClassName);
157
            // Check if the class is registrable
158
            if($xReflectionClass->isInstantiable() &&
159
                !$xReflectionClass->isSubclassOf(Pagination::class))
160
            {
161
                $this->aClasses[$sClassName] = $aOptions;
162
                $this->val($this->getReflectionClassKey($sClassName), $xReflectionClass);
163
            }
164
        }
165
        catch(ReflectionException $e)
166
        {
167
            throw new SetupException($this->xTranslator->trans('errors.class.invalid',
168
                ['name' => $sClassName]));
169
        }
170
    }
171
172
    /**
173
     * Find the options associated with a registered class name
174
     *
175
     * @param string $sClassName The class name
176
     *
177
     * @return void
178
     * @throws SetupException
179
     */
180
    private function registerClassOptions(string $sClassName)
181
    {
182
        if(!isset($this->aClasses[$sClassName]))
183
        {
184
            // Find options for a class registered with namespace.
185
            /** @var CallableRegistry */
186
            $xRegistry = $this->di->g(CallableRegistry::class);
187
            $xRegistry->registerClassFromNamespace($sClassName);
188
            if(!isset($this->aClasses[$sClassName]))
189
            {
190
                // Find options for a class registered without namespace.
191
                // We need to parse all the classes to be able to find one.
192
                $xRegistry->parseDirectories();
193
            }
194
        }
195
        if(!isset($this->aClasses[$sClassName]))
196
        {
197
            throw new SetupException($this->xTranslator->trans('errors.class.invalid',
198
                ['name' => $sClassName]));
199
        }
200
    }
201
202
    /**
203
     * Get callable objects for known classes
204
     *
205
     * @return array
206
     * @throws SetupException
207
     */
208
    public function getCallableObjects(): array
209
    {
210
        $aCallableObjects = [];
211
        foreach($this->aClasses as $sClassName => $_)
212
        {
213
            $this->registerCallableClass($sClassName);
214
            $aCallableObjects[$sClassName] = $this->getCallableObject($sClassName);
215
        }
216
        return $aCallableObjects;
217
    }
218
219
    /**
220
     * @param ReflectionClass $xClass
221
     * @param ReflectionParameter $xParameter
222
     *
223
     * @return mixed
224
     * @throws SetupException
225
     */
226
    protected function getParameter(ReflectionClass $xClass, ReflectionParameter $xParameter)
0 ignored issues
show
Unused Code introduced by
The parameter $xClass is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

226
    protected function getParameter(/** @scrutinizer ignore-unused */ ReflectionClass $xClass, ReflectionParameter $xParameter)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
227
    {
228
        $xType = $xParameter->getType();
229
        // Check the parameter class first.
230
        if($xType instanceof ReflectionNamedType)
231
        {
232
            // Check the class + the name
233
            if($this->di->has($xType->getName() . ' $' . $xParameter->getName()))
234
            {
235
                return $this->di->get($xType->getName() . ' $' . $xParameter->getName());
236
            }
237
            // Check the class only
238
            if($this->di->has($xType->getName()))
239
            {
240
                return $this->di->get($xType->getName());
241
            }
242
        }
243
        // Check the name only
244
        return $this->di->get('$' . $xParameter->getName());
245
    }
246
247
    /**
248
     * Create an instance of a class, getting the constructor parameters from the DI container
249
     *
250
     * @param string|ReflectionClass $xClass The class name or the reflection class
251
     *
252
     * @return object|null
253
     * @throws ReflectionException
254
     * @throws SetupException
255
     */
256
    public function make($xClass)
257
    {
258
        if(is_string($xClass))
259
        {
260
            $xClass = new ReflectionClass($xClass); // Create the reflection class instance
261
        }
262
        if(!($xClass instanceof ReflectionClass))
0 ignored issues
show
introduced by
$xClass is always a sub-type of ReflectionClass.
Loading history...
263
        {
264
            return null;
265
        }
266
        // Use the Reflection class to get the parameters of the constructor
267
        if(($constructor = $xClass->getConstructor()) === null)
268
        {
269
            return $xClass->newInstance();
270
        }
271
        $aParameterInstances = array_map(function($xParameter) use($xClass) {
272
            return $this->getParameter($xClass, $xParameter);
273
        }, $constructor->getParameters());
274
275
        return $xClass->newInstanceArgs($aParameterInstances);
276
    }
277
278
    /**
279
     * Create an instance of a class by automatically fetching the dependencies in the constructor.
280
     *
281
     * @param string $sClass    The class name
282
     *
283
     * @return void
284
     */
285
    public function auto(string $sClass)
286
    {
287
        $this->set($sClass, function() use ($sClass) {
288
            return $this->make($sClass);
289
        });
290
    }
291
292
    /**
293
     * @param string $sClassName The callable class name
294
     *
295
     * @return string
296
     */
297
    private function getCallableObjectKey(string $sClassName): string
298
    {
299
        return $sClassName . '_CallableObject';
300
    }
301
302
    /**
303
     * @param string $sClassName The callable class name
304
     *
305
     * @return string
306
     */
307
    private function getCallableHelperKey(string $sClassName): string
308
    {
309
        return $sClassName . '_CallableHelper';
310
    }
311
312
    /**
313
     * @param string $sClassName The callable class name
314
     *
315
     * @return string
316
     */
317
    private function getReflectionClassKey(string $sClassName): string
318
    {
319
        return $sClassName . '_ReflectionClass';
320
    }
321
322
    /**
323
     * Register a callable class
324
     *
325
     * @param string $sClassName The callable class name
326
     *
327
     * @return void
328
     * @throws SetupException
329
     */
330
    private function registerCallableClass(string $sClassName)
331
    {
332
        $sCallableObject = $this->getCallableObjectKey($sClassName);
333
        // Prevent duplication. It's important not to use the class name here.
334
        if($this->has($sCallableObject))
335
        {
336
            return;
337
        }
338
339
        // Register the helper class
340
        $this->set($this->getCallableHelperKey($sClassName), function() use($sClassName) {
341
            $xFactory = $this->di->getCallFactory();
342
            return new CallableClassHelper($this, $xFactory->rq($sClassName),
0 ignored issues
show
Bug introduced by
It seems like $xFactory->rq($sClassName) can also be of type null; however, parameter $xJxnCall of Jaxon\Plugin\Request\Cal...ssHelper::__construct() does only seem to accept Jaxon\Script\JxnCall, 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

342
            return new CallableClassHelper($this, /** @scrutinizer ignore-type */ $xFactory->rq($sClassName),
Loading history...
343
                $xFactory, $this->di->getViewRenderer(),
344
                $this->di->getLogger(), $this->di->getSessionManager(),
345
                $this->di->getStash(), $this->di->getUploadHandler());
346
        });
347
348
        $this->registerClassOptions($sClassName);
349
        $aOptions = $this->aClasses[$sClassName];
350
351
        // Register the callable object
352
        $this->set($sCallableObject, function() use($sClassName, $aOptions) {
353
            $xReflectionClass = $this->get($this->getReflectionClassKey($sClassName));
354
            $xRegistry = $this->di->g(CallableRegistry::class);
355
            $aProtectedMethods = $xRegistry->getProtectedMethods($sClassName);
356
357
            return new CallableObject($this, $this->di, $xReflectionClass, $aOptions, $aProtectedMethods);
358
        });
359
360
        // Register the user class, but only if the user didn't already.
361
        if(!$this->has($sClassName))
362
        {
363
            $this->set($sClassName, function() use($sClassName) {
364
                return $this->make($this->get($this->getReflectionClassKey($sClassName)));
365
            });
366
        }
367
        // Initialize the user class instance
368
        $this->xContainer->extend($sClassName, function($xClassInstance) use($sClassName) {
369
            if($xClassInstance instanceof AbstractCallable)
370
            {
371
                $xClassInstance->_initCallable($this->di, $this->get($this->getCallableHelperKey($sClassName)));
372
            }
373
374
            // Run the callbacks for class initialisation
375
            $this->di->g(CallbackManager::class)->onInit($xClassInstance);
376
377
            // Set attributes from the DI container.
378
            // The class level DI options are set when creating the object instance.
379
            // The method level DI options are set only when calling the method in the ajax request.
380
            /** @var CallableObject */
381
            $xCallableObject = $this->get($this->getCallableObjectKey($sClassName));
382
            $xCallableObject->setDiClassAttributes($xClassInstance);
383
384
            return $xClassInstance;
385
        });
386
    }
387
388
    /**
389
     * Get the callable object for a given class
390
     *
391
     * @param string $sClassName
392
     *
393
     * @return CallableObject
394
     */
395
    public function getCallableObject(string $sClassName): CallableObject
396
    {
397
        return $this->get($sClassName . '_CallableObject');
398
    }
399
400
    /**
401
     * Check if a callable object is already in the DI, and register if not
402
     *
403
     * @param string $sClassName The class name of the callable object
404
     *
405
     * @return string
406
     * @throws SetupException
407
     */
408
    private function checkCallableObject(string $sClassName): string
409
    {
410
        // Replace all separators ('.' and '_') with antislashes, and remove the antislashes
411
        // at the beginning and the end of the class name.
412
        $sClassName = trim(str_replace('.', '\\', $sClassName), '\\');
413
        if($this->bUsingUnderscore)
414
        {
415
            $sClassName = trim(str_replace('_', '\\', $sClassName), '\\');
416
        }
417
418
        // Register the class.
419
        $this->registerCallableClass($sClassName);
420
        return $sClassName;
421
    }
422
423
    /**
424
     * Get the callable object for a given class
425
     * The callable object is registered if it is not already in the DI.
426
     *
427
     * @param string $sClassName The class name of the callable object
428
     *
429
     * @return CallableObject|null
430
     * @throws SetupException
431
     */
432
    public function makeCallableObject(string $sClassName): ?CallableObject
433
    {
434
        return $this->getCallableObject($this->checkCallableObject($sClassName));
435
    }
436
437
    /**
438
     * Get an instance of a Jaxon class by name
439
     *
440
     * @param string $sClassName the class name
441
     *
442
     * @return mixed
443
     * @throws SetupException
444
     */
445
    public function makeRegisteredObject(string $sClassName)
446
    {
447
        $xCallableObject = $this->makeCallableObject($sClassName);
448
        return !$xCallableObject ? null : $xCallableObject->getRegisteredObject();
0 ignored issues
show
introduced by
$xCallableObject is of type Jaxon\Plugin\Request\CallableClass\CallableObject, thus it always evaluated to true.
Loading history...
449
    }
450
451
    /**
452
     * Get the callable registry
453
     *
454
     * @return CallableRegistry
455
     */
456
    public function getCallableRegistry(): CallableRegistry
457
    {
458
        return $this->di->g(CallableRegistry::class);
459
    }
460
461
    /**
462
     * @param string $sClassName The callable class name
463
     *
464
     * @return string
465
     */
466
    private function getRequestFactoryKey(string $sClassName): string
467
    {
468
        return $sClassName . '_RequestFactory';
469
    }
470
471
    /**
472
     * @param string $sClassName
473
     * @param string $sFactoryKey
474
     *
475
     * @return void
476
     */
477
    private function registerRequestFactory(string $sClassName, string $sFactoryKey)
478
    {
479
        $this->xContainer->offsetSet($sFactoryKey, function() use($sClassName) {
480
            if(!($xCallable = $this->makeCallableObject($sClassName)))
481
            {
482
                return null;
483
            }
484
            $xConfigManager = $this->di->g(ConfigManager::class);
485
            $sJsObject = $xConfigManager->getOption('core.prefix.class', '') . $xCallable->getJsName();
486
            return new JxnClass($this->di->g(DialogCommand::class), $sJsObject);
487
        });
488
    }
489
490
    /**
491
     * Get a factory for a js function call.
492
     *
493
     * @param string $sClassName
494
     *
495
     * @return JxnCall|null
496
     */
497
    public function getRequestFactory(string $sClassName = ''): ?JxnCall
498
    {
499
        $sClassName = trim($sClassName, " \t") ?: JxnCall::class;
500
        $sFactoryKey = $this->getRequestFactoryKey($sClassName);
501
        if(!$this->has($sFactoryKey))
502
        {
503
            $this->registerRequestFactory($sClassName, $sFactoryKey);
504
        }
505
        return $this->get($sFactoryKey);
506
    }
507
}
508