Passed
Push — main ( fd5484...10601e )
by Thierry
05:13
created

ComponentContainer::saveComponent()   A

Complexity

Conditions 5
Paths 22

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
c 0
b 0
f 0
nc 22
nop 2
dl 0
loc 30
rs 9.4888
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
     * The Dependency Injection Container for registered classes
44
     *
45
     * @var PimpleContainer
46
     */
47
    private $xContainer;
48
49
    /**
50
     * This will be set only when getting the object targetted by the ajax request.
51
     *
52
     * @var Target
53
     */
54
    private $xTarget = null;
55
56
    /**
57
     * The class constructor
58
     *
59
     * @param Container $di
60
     */
61
    public function __construct(private Container $di)
62
    {
63
        $this->xContainer = new PimpleContainer();
64
        $this->val(ComponentContainer::class, $this);
65
66
        // Register the call factory for registered functions
67
        $this->set($this->getRequestFactoryKey(JxnCall::class), fn() =>
68
            new JxnCall($this->di->g(ConfigManager::class)
69
                ->getOption('core.prefix.function', '')));
70
71
        // Register the pagination component, but do not export to js.
72
        $this->saveComponent(Pagination::class, [
73
            'excluded' => true,
74
            'separator' => '.',
75
            'namespace' => 'Jaxon\\App\\Component',
76
        ]);
77
    }
78
79
    /**
80
     * The container for parameters
81
     *
82
     * @return Container
83
     */
84
    protected function cn(): Container
85
    {
86
        return $this->di;
87
    }
88
89
    /**
90
     * Check if a class is defined in the container
91
     *
92
     * @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...
93
     *
94
     * @return bool
95
     */
96
    public function has(string $sClass): bool
97
    {
98
        return $this->xContainer->offsetExists($sClass);
99
    }
100
101
    /**
102
     * Save a closure in the container
103
     *
104
     * @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...
105
     * @param Closure $xClosure    The closure
106
     *
107
     * @return void
108
     */
109
    public function set(string $sClass, Closure $xClosure)
110
    {
111
        $this->xContainer->offsetSet($sClass, fn() => $xClosure($this));
112
    }
113
114
    /**
115
     * Save a value in the container
116
     *
117
     * @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...
118
     * @param mixed $xValue    The value
119
     *
120
     * @return void
121
     */
122
    public function val(string $sKey, $xValue)
123
    {
124
       $this->xContainer->offsetSet($sKey, $xValue);
125
    }
126
127
    /**
128
     * Get a class instance
129
     *
130
     * @template T
131
     * @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...
132
     *
133
     * @return T
134
     */
135
    public function get(string $sClass)
136
    {
137
        return $this->xContainer->offsetGet($sClass);
138
    }
139
140
    /**
141
     * Get a component when one of its method needs to be called
142
     *
143
     * @template T
144
     * @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...
145
     * @param Target $xTarget
146
     *
147
     * @return T|null
148
     */
149
    public function getTargetComponent(string $sClassName, Target $xTarget): mixed
150
    {
151
        // Set the target only when getting the object targetted by the ajax request.
152
        $this->xTarget = $xTarget;
153
        $xComponent = $this->get($sClassName);
154
        $this->xTarget = null;
155
156
        return $xComponent;
157
    }
158
159
    /**
160
     * Register a component and its options
161
     *
162
     * @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...
163
     * @param array $aOptions    The class options
164
     *
165
     * @return void
166
     */
167
    public function saveComponent(string $sClassName, array $aOptions): void
168
    {
169
        try
170
        {
171
            // Make sure the registered class exists
172
            if(isset($aOptions['include']))
173
            {
174
                require_once $aOptions['include'];
175
            }
176
            $xReflectionClass = new ReflectionClass($sClassName);
177
            // Check if the class is registrable
178
            if(!$xReflectionClass->isInstantiable())
179
            {
180
                return;
181
            }
182
183
            $this->_saveClassOptions($sClassName, $aOptions);
184
185
            $sClassKey = $this->getReflectionClassKey($sClassName);
186
            $this->val($sClassKey, $xReflectionClass);
187
            // Register the user class, but only if the user didn't already.
188
            if(!$this->has($sClassName))
189
            {
190
                $this->set($sClassName, fn() => $this->make($this->get($sClassKey)));
191
            }
192
        }
193
        catch(ReflectionException $e)
194
        {
195
            throw new SetupException($this->cn()->g(Translator::class)
196
                ->trans('errors.class.invalid', ['name' => $sClassName]));
197
        }
198
    }
199
200
    /**
201
     * Register a component
202
     *
203
     * @param string $sComponentId The component name
204
     *
205
     * @return string
206
     * @throws SetupException
207
     */
208
    private function _registerComponent(string $sComponentId): string
209
    {
210
        // Replace all separators ('.' or '_') with antislashes, and trim the class name.
211
        $sClassName = trim(str_replace(['.', '_'], '\\', $sComponentId), '\\');
212
213
        $sComponentObject = $this->getCallableObjectKey($sClassName);
214
        // Prevent duplication. It's important not to use the class name here.
215
        if($this->has($sComponentObject))
216
        {
217
            return $sClassName;
218
        }
219
220
        // Register the helper class
221
        $this->set($this->getCallableHelperKey($sClassName), function() use($sClassName) {
222
            $xFactory = $this->di->getCallFactory();
223
            return new ComponentHelper($this, $xFactory->rq($sClassName),
224
                $xFactory, $this->di->getViewRenderer(),
225
                $this->di->getLogger(), $this->di->getSessionManager(),
226
                $this->di->getStash(), $this->di->getUploadHandler());
227
        });
228
229
        $this->discoverComponent($sClassName);
230
231
        // Register the callable object
232
        $this->set($sComponentObject, function() use($sComponentId, $sClassName) {
233
            $aOptions = $this->_getClassOptions($sComponentId);
234
            $xReflectionClass = $this->get($this->getReflectionClassKey($sClassName));
235
            $xOptions = $this->getComponentOptions($xReflectionClass, $aOptions);
236
            return new CallableObject($this, $this->di, $xReflectionClass, $xOptions);
237
        });
238
239
        // Initialize the user class instance
240
        $this->xContainer->extend($sClassName, function($xClassInstance) use($sClassName) {
241
            if($xClassInstance instanceof AbstractComponent)
242
            {
243
                $xHelper = $this->get($this->getCallableHelperKey($sClassName));
244
                $xHelper->xTarget = $this->xTarget;
245
246
                // Call the protected "initComponent()" method of the Component class.
247
                $cSetter = function($di, $xHelper) {
248
                    $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

248
                    $this->/** @scrutinizer ignore-call */ 
249
                           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...
249
                };
250
                $cSetter = $cSetter->bindTo($xClassInstance, $xClassInstance);
251
                call_user_func($cSetter, $this->di, $xHelper);
252
            }
253
254
            // Run the callbacks for class initialisation
255
            $this->di->g(CallbackManager::class)->onInit($xClassInstance);
256
257
            // Set attributes from the DI container.
258
            // The class level DI options are set on any component.
259
            // The method level DI options are set only on the targetted component.
260
            /** @var CallableObject */
261
            $xCallableObject = $this->get($this->getCallableObjectKey($sClassName));
262
            $xCallableObject->setDiClassAttributes($xClassInstance);
263
            if($this->xTarget !== null)
264
            {
265
                $sMethodName = $this->xTarget->getMethodName();
266
                $xCallableObject->setDiMethodAttributes($xClassInstance, $sMethodName);
267
            }
268
269
            return $xClassInstance;
270
        });
271
272
        return $sClassName;
273
    }
274
275
    /**
276
     * Get the callable object for a given class
277
     * The callable object is registered if it is not already in the DI.
278
     *
279
     * @param string $sComponentId
280
     *
281
     * @return CallableObject|null
282
     * @throws SetupException
283
     */
284
    public function makeCallableObject(string $sComponentId): ?CallableObject
285
    {
286
        $sClassName = $this->_registerComponent($sComponentId);
287
        return $this->get($this->getCallableObjectKey($sClassName));
288
    }
289
290
    /**
291
     * Get an instance of a component by name
292
     *
293
     * @template T
294
     * @param string<T> $sClassName the class name
295
     *
296
     * @return T|null
297
     * @throws SetupException
298
     */
299
    public function makeComponent(string $sClassName): mixed
300
    {
301
        $sComponentId = str_replace('\\', '.', $sClassName);
302
        $sClassName = $this->_registerComponent($sComponentId);
303
        return $this->get($sClassName);
304
    }
305
306
    /**
307
     * Get a factory for a call to a registered function.
308
     *
309
     * @return JxnCall
310
     */
311
    public function getFunctionRequestFactory(): JxnCall
312
    {
313
        return $this->get($this->getRequestFactoryKey(JxnCall::class));
314
    }
315
316
    /**
317
     * Get a factory for a call to a registered component.
318
     *
319
     * @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...
320
     *
321
     * @return JxnCall|null
322
     */
323
    public function getComponentRequestFactory(string $sClassName): ?JxnCall
324
    {
325
        $sClassName = trim($sClassName, " \t");
326
        if($sClassName === '')
327
        {
328
            return null;
329
        }
330
331
        $sFactoryKey = $this->getRequestFactoryKey($sClassName);
332
        if(!$this->has($sFactoryKey))
333
        {
334
            $this->xContainer->offsetSet($sFactoryKey, function() use($sClassName) {
335
                $sComponentId = str_replace('\\', '.', $sClassName);
336
                if(!($xCallable = $this->makeCallableObject($sComponentId)))
337
                {
338
                    return null;
339
                }
340
341
                $xConfigManager = $this->di->g(ConfigManager::class);
342
                $sPrefix = $xConfigManager->getOption('core.prefix.class', '');
343
                return new JxnClassCall($sPrefix . $xCallable->getJsName());
344
            });
345
        }
346
        return $this->get($sFactoryKey);
347
    }
348
}
349