Passed
Push — main ( 652a21...525366 )
by Thierry
03:56
created

ComponentContainer   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 90
c 1
b 0
f 0
dl 0
loc 357
rs 10
wmc 28

17 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 3 1
A registerRequestFactory() 0 10 2
A useUnderscore() 0 3 1
A __construct() 0 14 1
A set() 0 4 1
A makeComponent() 0 5 1
A getClassName() 0 5 2
A cn() 0 3 1
A registerComponent() 0 30 5
A val() 0 3 1
A getFunctionRequestFactory() 0 3 1
A _registerComponent() 0 59 4
A makeCallableObject() 0 5 1
A getCallableObject() 0 3 1
A getTargetComponent() 0 8 1
A has() 0 3 1
A getComponentRequestFactory() 0 14 3
1
<?php
2
3
/**
4
 * ComponentContainer.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\Component\AbstractComponent;
18
use Jaxon\App\Component\Pagination;
19
use Jaxon\App\Config\ConfigManager;
20
use Jaxon\App\I18n\Translator;
21
use Jaxon\Exception\SetupException;
22
use Jaxon\Plugin\Request\CallableClass\CallableObject;
23
use Jaxon\Plugin\Request\CallableClass\ComponentHelper;
24
use Jaxon\Request\Handler\CallbackManager;
25
use Jaxon\Request\Target;
26
use Jaxon\Script\Call\JxnCall;
27
use Jaxon\Script\Call\JxnClassCall;
28
use Pimple\Container as PimpleContainer;
29
use Closure;
30
use ReflectionClass;
31
use ReflectionException;
32
33
use function call_user_func;
34
use function str_replace;
35
use function trim;
36
37
class ComponentContainer
38
{
39
    use Traits\DiAutoTrait;
40
    use Traits\ComponentTrait;
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
     * This will be set only when getting the object targetted by the ajax request.
58
     *
59
     * @var Target
60
     */
61
    private $xTarget = null;
62
63
    /**
64
     * The class constructor
65
     *
66
     * @param Container $di
67
     */
68
    public function __construct(private Container $di)
69
    {
70
        $this->xContainer = new PimpleContainer();
71
        $this->val(ComponentContainer::class, $this);
72
73
        // Register the call factory for registered functions
74
        $this->set($this->getRequestFactoryKey(JxnCall::class), fn() =>
75
            new JxnCall($this->di->g(ConfigManager::class)
76
                ->getOption('core.prefix.function', '')));
77
78
        // Register the pagination component, but do not export to js.
79
        $this->registerComponent(Pagination::class, [
80
            'excluded' => true,
81
            'namespace' => 'Jaxon\\App\\Component',
82
        ]);
83
    }
84
85
    /**
86
     * The container for parameters
87
     *
88
     * @return Container
89
     */
90
    protected function cn(): Container
91
    {
92
        return $this->di;
93
    }
94
95
96
    /**
97
     * @return void
98
     */
99
    public function useUnderscore()
100
    {
101
        $this->bUsingUnderscore = true;
102
    }
103
104
    /**
105
     * Check if a class is defined in the container
106
     *
107
     * @param class-string $sClass    The full class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
108
     *
109
     * @return bool
110
     */
111
    public function has(string $sClass): bool
112
    {
113
        return $this->xContainer->offsetExists($sClass);
114
    }
115
116
    /**
117
     * Save a closure in the container
118
     *
119
     * @param class-string $sClass    The full class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
120
     * @param Closure $xClosure    The closure
121
     *
122
     * @return void
123
     */
124
    public function set(string $sClass, Closure $xClosure)
125
    {
126
        $this->xContainer->offsetSet($sClass, function() use($xClosure) {
127
            return $xClosure($this);
128
        });
129
    }
130
131
    /**
132
     * Save a value in the container
133
     *
134
     * @param string|class-string $sKey    The key
0 ignored issues
show
Documentation Bug introduced by
The doc comment string|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in string|class-string.
Loading history...
135
     * @param mixed $xValue    The value
136
     *
137
     * @return void
138
     */
139
    public function val(string $sKey, $xValue)
140
    {
141
       $this->xContainer->offsetSet($sKey, $xValue);
142
    }
143
144
    /**
145
     * Get a class instance
146
     *
147
     * @template T
148
     * @param class-string<T> $sClass The full class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
149
     *
150
     * @return T
151
     */
152
    public function get(string $sClass)
153
    {
154
        return $this->xContainer->offsetGet($sClass);
155
    }
156
157
    /**
158
     * Get a component when one of its method needs to be called
159
     *
160
     * @template T
161
     * @param class-string<T> $sClassName the class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
162
     * @param Target $xTarget
163
     *
164
     * @return T|null
165
     */
166
    public function getTargetComponent(string $sClassName, Target $xTarget): mixed
167
    {
168
        // Set the target only when getting the object targetted by the ajax request.
169
        $this->xTarget = $xTarget;
170
        $xComponent = $this->get($sClassName);
171
        $this->xTarget = null;
172
173
        return $xComponent;
174
    }
175
176
    /**
177
     * Register a component and its options
178
     *
179
     * @param class-string $sClassName    The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
180
     * @param array $aOptions    The class options
181
     *
182
     * @return void
183
     */
184
    public function registerComponent(string $sClassName, array $aOptions = []): void
185
    {
186
        try
187
        {
188
            // Make sure the registered class exists
189
            if(isset($aOptions['include']))
190
            {
191
                require_once $aOptions['include'];
192
            }
193
            $xReflectionClass = new ReflectionClass($sClassName);
194
            // Check if the class is registrable
195
            if(!$xReflectionClass->isInstantiable())
196
            {
197
                return;
198
            }
199
200
            $this->_saveClassOptions($sClassName, $aOptions);
201
            $this->val($this->getReflectionClassKey($sClassName), $xReflectionClass);
202
            // Register the user class, but only if the user didn't already.
203
            if(!$this->has($sClassName))
204
            {
205
                $this->set($sClassName, function() use($sClassName) {
206
                    return $this->make($this->get($this->getReflectionClassKey($sClassName)));
207
                });
208
            }
209
        }
210
        catch(ReflectionException $e)
211
        {
212
            throw new SetupException($this->cn()->g(Translator::class)
213
                ->trans('errors.class.invalid', ['name' => $sClassName]));
214
        }
215
    }
216
217
    /**
218
     * Register a component
219
     *
220
     * @param class-string $sClassName The component name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
221
     *
222
     * @return void
223
     * @throws SetupException
224
     */
225
    private function _registerComponent(string $sClassName)
226
    {
227
        $sComponentObject = $this->getCallableObjectKey($sClassName);
228
        // Prevent duplication. It's important not to use the class name here.
229
        if($this->has($sComponentObject))
230
        {
231
            return;
232
        }
233
234
        // Register the helper class
235
        $this->set($this->getCallableHelperKey($sClassName), function() use($sClassName) {
236
            $xFactory = $this->di->getCallFactory();
237
            return new ComponentHelper($this, $xFactory->rq($sClassName),
238
                $xFactory, $this->di->getViewRenderer(),
239
                $this->di->getLogger(), $this->di->getSessionManager(),
240
                $this->di->getStash(), $this->di->getUploadHandler());
241
        });
242
243
        $this->discoverComponent($sClassName);
244
        $aOptions = $this->_getClassOptions($sClassName);
245
246
        // Register the callable object
247
        $this->set($sComponentObject, function() use($sClassName, $aOptions) {
248
            $xReflectionClass = $this->get($this->getReflectionClassKey($sClassName));
249
            $xOptions = $this->getComponentOptions($xReflectionClass, $aOptions);
250
            return new CallableObject($this, $this->di, $xReflectionClass, $xOptions);
251
        });
252
253
        // Initialize the user class instance
254
        $this->xContainer->extend($sClassName, function($xClassInstance) use($sClassName) {
255
            if($xClassInstance instanceof AbstractComponent)
256
            {
257
                $xHelper = $this->get($this->getCallableHelperKey($sClassName));
258
                $xHelper->xTarget = $this->xTarget;
259
260
                // Call the protected "initComponent()" method of the Component class.
261
                $cSetter = function($di, $xHelper) {
262
                    $this->initComponent($di, $xHelper);  // "$this" here refers to the Component class.
0 ignored issues
show
Bug introduced by
The method initComponent() does not exist on Jaxon\Di\ComponentContainer. ( Ignorable by Annotation )

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

262
                    $this->/** @scrutinizer ignore-call */ 
263
                           initComponent($di, $xHelper);  // "$this" here refers to the Component class.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
263
                };
264
                $cSetter = $cSetter->bindTo($xClassInstance, $xClassInstance);
265
                call_user_func($cSetter, $this->di, $xHelper);
266
            }
267
268
            // Run the callbacks for class initialisation
269
            $this->di->g(CallbackManager::class)->onInit($xClassInstance);
270
271
            // Set attributes from the DI container.
272
            // The class level DI options are set on any component.
273
            // The method level DI options are set only on the targetted component.
274
            /** @var CallableObject */
275
            $xCallableObject = $this->get($this->getCallableObjectKey($sClassName));
276
            $xCallableObject->setDiClassAttributes($xClassInstance);
277
            if($this->xTarget !== null)
278
            {
279
                $sMethodName = $this->xTarget->getMethodName();
280
                $xCallableObject->setDiMethodAttributes($xClassInstance, $sMethodName);
281
            }
282
283
            return $xClassInstance;
284
        });
285
    }
286
287
    /**
288
     * Get the callable object for a given class
289
     *
290
     * @param class-string $sClassName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
291
     *
292
     * @return CallableObject
293
     */
294
    public function getCallableObject(string $sClassName): CallableObject
295
    {
296
        return $this->get($this->getCallableObjectKey($sClassName));
297
    }
298
299
    /**
300
     * @param string $sClassName A class name, but possibly with dot or underscore as separator
301
     *
302
     * @return class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
303
     * @throws SetupException
304
     */
305
    private function getClassName(string $sClassName): string
306
    {
307
        // Replace all separators ('.' or '_') with antislashes, and trim the class name.
308
        $sSeparator = !$this->bUsingUnderscore ? '.' : '_';
309
        return trim(str_replace($sSeparator, '\\', $sClassName), '\\');
310
    }
311
312
    /**
313
     * Get the callable object for a given class
314
     * The callable object is registered if it is not already in the DI.
315
     *
316
     * @param class-string $sClassName The class name of the callable object
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
317
     *
318
     * @return CallableObject|null
319
     * @throws SetupException
320
     */
321
    public function makeCallableObject(string $sClassName): ?CallableObject
322
    {
323
        $sClassName = $this->getClassName($sClassName);
324
        $this->_registerComponent($sClassName);
325
        return $this->getCallableObject($sClassName);
326
    }
327
328
    /**
329
     * Get an instance of a component by name
330
     *
331
     * @template T
332
     * @param class-string<T> $sClassName the class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
333
     *
334
     * @return T|null
335
     * @throws SetupException
336
     */
337
    public function makeComponent(string $sClassName): mixed
338
    {
339
        $sClassName = $this->getClassName($sClassName);
340
        $this->_registerComponent($sClassName);
341
        return $this->get($sClassName);
342
    }
343
344
    /**
345
     * @param class-string $sClassName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
346
     * @param string $sFactoryKey
347
     *
348
     * @return void
349
     */
350
    private function registerRequestFactory(string $sClassName, string $sFactoryKey)
351
    {
352
        $this->xContainer->offsetSet($sFactoryKey, function() use($sClassName) {
353
            if(!($xCallable = $this->makeCallableObject($sClassName)))
354
            {
355
                return null;
356
            }
357
358
            $sPrefix = $this->di->g(ConfigManager::class)->getOption('core.prefix.class', '');
359
            return new JxnClassCall($sPrefix . $xCallable->getJsName());
360
        });
361
    }
362
363
    /**
364
     * Get a factory for a call to a registered function.
365
     *
366
     * @return JxnCall
367
     */
368
    public function getFunctionRequestFactory(): JxnCall
369
    {
370
        return $this->get($this->getRequestFactoryKey(JxnCall::class));
371
    }
372
373
    /**
374
     * Get a factory for a call to a registered component.
375
     *
376
     * @param class-string $sClassName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
377
     *
378
     * @return JxnCall|null
379
     */
380
    public function getComponentRequestFactory(string $sClassName): ?JxnCall
381
    {
382
        $sClassName = trim($sClassName, " \t");
383
        if($sClassName === '')
384
        {
385
            return null;
386
        }
387
388
        $sFactoryKey = $this->getRequestFactoryKey($sClassName);
389
        if(!$this->has($sFactoryKey))
390
        {
391
            $this->registerRequestFactory($sClassName, $sFactoryKey);
392
        }
393
        return $this->get($sFactoryKey);
394
    }
395
}
396