ComponentTrait::getPublicMethods()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 19
rs 9.9
1
<?php
2
3
/**
4
 * ComponentTrait.php
5
 *
6
 * Functions for the component container.
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\Traits;
16
17
use Jaxon\Di\Container;
18
use Jaxon\App\FuncComponent;
19
use Jaxon\App\NodeComponent;
20
use Jaxon\App\I18n\Translator;
21
use Jaxon\App\Metadata\InputData;
22
use Jaxon\App\Metadata\Metadata;
23
use Jaxon\Config\Config;
24
use Jaxon\Exception\SetupException;
25
use Jaxon\Plugin\Request\CallableClass\CallableObject;
26
use Jaxon\Plugin\Request\CallableClass\ComponentOptions;
27
use Jaxon\Plugin\Request\CallableClass\ComponentRegistry;
28
use ReflectionClass;
29
use ReflectionMethod;
30
use ReflectionProperty;
31
32
use function array_filter;
33
use function array_map;
34
use function in_array;
35
use function str_replace;
36
use function substr;
37
38
trait ComponentTrait
39
{
40
    /**
41
     * The classes, both registered and found in registered directories.
42
     *
43
     * @var array
44
     */
45
    protected $aComponents = [];
46
47
    /**
48
     * The classes, both registered and found in registered directories.
49
     *
50
     * @var array
51
     */
52
    protected $aComponentPublicMethods = [];
53
54
    /**
55
     * The container for parameters
56
     *
57
     * @return Container
58
     */
59
    abstract protected function cn(): Container;
60
61
    /**
62
     * @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...
63
     *
64
     * @return CallableObject|null
65
     * @throws SetupException
66
     */
67
    abstract public function makeCallableObject(string $sClassName): ?CallableObject;
68
69
    /**
70
     * @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...
71
     * @param array $aOptions    The class options
72
     *
73
     * @return void
74
     */
75
    abstract public function saveComponent(string $sClassName, array $aOptions): void;
76
77
    /**
78
     * @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...
79
     *
80
     * @return string
81
     */
82
    private function getCallableObjectKey(string $sClassName): string
83
    {
84
        return "{$sClassName}_CallableObject";
85
    }
86
87
    /**
88
     * @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...
89
     *
90
     * @return string
91
     */
92
    private function getCallableHelperKey(string $sClassName): string
93
    {
94
        return "{$sClassName}_CallableHelper";
95
    }
96
97
    /**
98
     * @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...
99
     *
100
     * @return string
101
     */
102
    private function getReflectionClassKey(string $sClassName): string
103
    {
104
        return "{$sClassName}_ReflectionClass";
105
    }
106
107
    /**
108
     * @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...
109
     *
110
     * @return string
111
     */
112
    private function getRequestFactoryKey(string $sClassName): string
113
    {
114
        return "{$sClassName}_RequestFactory";
115
    }
116
117
    /**
118
     * @param string $sClassName
119
     * @param array $aOptions
120
     *
121
     * @return void
122
     */
123
    private function _saveClassOptions(string $sClassName, array $aOptions): void
124
    {
125
        $sOptionsId = str_replace('\\', $aOptions['separator'], $sClassName);
126
        $this->aComponents[$sOptionsId] = $aOptions;
127
    }
128
129
    /**
130
     * @param string $sClassName
131
     *
132
     * @return array
133
     */
134
    private function _getClassOptions(string $sClassName): array
135
    {
136
        return $this->aComponents[str_replace('\\', '.', $sClassName)] ??
137
            $this->aComponents[str_replace('\\', '_', $sClassName)];
138
    }
139
140
    /**
141
     * Find a component amongst the registered namespaces and directories.
142
     *
143
     * @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...
144
     *
145
     * @return void
146
     * @throws SetupException
147
     */
148
    private function discoverComponent(string $sClassName): void
149
    {
150
        $xRegistry = $this->cn()->g(ComponentRegistry::class);
151
        $xRegistry->updateHash(false); // Disable hash calculation.
152
153
        $sComponentId = str_replace('\\', '.', $sClassName);
154
        if(!isset($this->aComponents[$sComponentId]))
155
        {
156
            $aOptions = $xRegistry->getNamespaceComponentOptions($sClassName);
157
            if($aOptions !== null)
158
            {
159
                $this->saveComponent($sClassName, $aOptions);
160
            }
161
        }
162
        if(isset($this->aComponents[$sComponentId]))
163
        {
164
            return; // The component is found.
165
        }
166
167
        // The component was not found in a registered namespace. We need to parse all
168
        // the directories to be able to find a component registered without a namespace.
169
        $sComponentId = str_replace('\\', '_', $sClassName);
170
        if(!isset($this->aComponents[$sComponentId]))
171
        {
172
            $xRegistry->registerComponentsInDirectories();
173
        }
174
        if(isset($this->aComponents[$sComponentId]))
175
        {
176
            return; // The component is found.
177
        }
178
179
        throw new SetupException($this->cn()->g(Translator::class)
180
            ->trans('errors.class.invalid', ['name' => $sClassName]));
181
    }
182
183
    /**
184
     * Get callable objects for known classes
185
     *
186
     * @return array
187
     * @throws SetupException
188
     */
189
    public function getCallableObjects(): array
190
    {
191
        $aCallableObjects = [];
192
        foreach($this->aComponents as $sComponentId => $_)
193
        {
194
            $aCallableObjects[$sComponentId] = $this->makeCallableObject($sComponentId);
195
        }
196
        return $aCallableObjects;
197
    }
198
199
    /**
200
     * @param string $sKey
201
     * @param string $sClass
202
     * @param array $aNeverExported
203
     *
204
     * @return void
205
     */
206
    private function setComponentPublicMethods(string $sKey, string $sClass,
207
        array $aNeverExported): void
208
    {
209
        if(isset($this->aComponentPublicMethods[$sKey]))
210
        {
211
            return;
212
        }
213
214
        $xReflectionClass = new ReflectionClass($sClass);
215
        $aMethods = $xReflectionClass->getMethods(ReflectionMethod::IS_PUBLIC);
216
        $this->aComponentPublicMethods[$sKey] = [
217
            array_map(fn($xMethod) => $xMethod->getName(), $aMethods),
218
            $aNeverExported,
219
        ];
220
    }
221
222
    /**
223
     * Get the public methods of the callable object
224
     *
225
     * @param ReflectionClass $xReflectionClass
226
     *
227
     * @return array
228
     */
229
    private function getPublicMethods(ReflectionClass $xReflectionClass): array
230
    {
231
        $aMethods = array_map(fn($xMethod) => $xMethod->getShortName(),
232
            $xReflectionClass->getMethods(ReflectionMethod::IS_PUBLIC));
233
        // Don't take the magic __call, __construct, __destruct methods.
234
        $aMethods = array_filter($aMethods, fn($sMethodName) =>
235
            substr($sMethodName, 0, 2) !== '__');
236
237
        // Don't take the public methods of the Component base classes.
238
        // And also return the methods that must never be exported.
239
        $aBaseMethods = match(true) {
240
            $xReflectionClass->isSubclassOf(NodeComponent::class) =>
241
                $this->aComponentPublicMethods['node'],
242
            $xReflectionClass->isSubclassOf(FuncComponent::class) =>
243
                $this->aComponentPublicMethods['func'],
244
            default => [[], []],
245
        };
246
247
        return [$aMethods, ...$aBaseMethods];
248
    }
249
250
    /**
251
     * @param ReflectionClass $xReflectionClass
252
     * @param array $aMethods
253
     * @param array $aOptions
254
     *
255
     * @return Metadata|null
256
     */
257
    private function getComponentMetadata(ReflectionClass $xReflectionClass,
258
        array $aMethods, array $aOptions): Metadata|null
259
    {
260
        /** @var Config|null */
261
        $xPackageConfig = $aOptions['config'] ?? null;
262
        if($xPackageConfig === null || (bool)($aOptions['excluded'] ?? false))
263
        {
264
            return null;
265
        }
266
        $sMetadataFormat = $xPackageConfig->getOption('metadata.format');
267
        if(!in_array($sMetadataFormat, ['attributes', 'annotations']))
268
        {
269
            return null;
270
        }
271
272
        // Try to get the class metadata from the cache.
273
        $di = $this->cn();
274
        $xMetadata = null;
275
        $xMetadataCache = null;
276
        $xConfig = $di->config();
277
        if($xConfig->getAppOption('metadata.cache.enabled', false))
278
        {
279
            if(!$di->h('jaxon_metadata_cache_dir'))
280
            {
281
                $sCacheDir = $xConfig->getAppOption('metadata.cache.dir');
282
                $di->val('jaxon_metadata_cache_dir', $sCacheDir);
283
            }
284
            $xMetadataCache = $di->getMetadataCache();
285
            $xMetadata = $xMetadataCache->read($xReflectionClass->getName());
286
            if($xMetadata !== null)
287
            {
288
                return $xMetadata;
289
            }
290
        }
291
292
        $aProperties = array_map(fn($xProperty) => $xProperty->getName(),
293
            $xReflectionClass->getProperties(ReflectionProperty::IS_PUBLIC |
294
                ReflectionProperty::IS_PROTECTED));
295
296
        $xMetadataReader = $di->getMetadataReader($sMetadataFormat);
297
        $xInput = new InputData($xReflectionClass, $aMethods, $aProperties);
298
        $xMetadata = $xMetadataReader->getAttributes($xInput);
299
300
        // Try to save the metadata in the cache
301
        if($xMetadataCache !== null)
302
        {
303
            $xMetadataCache->save($xReflectionClass->getName(), $xMetadata);
304
        }
305
306
        return $xMetadata;
307
    }
308
309
    /**
310
     * @param ReflectionClass $xReflectionClass
311
     * @param array $aOptions
312
     *
313
     * @return ComponentOptions
314
     */
315
    public function getComponentOptions(ReflectionClass $xReflectionClass,
316
        array $aOptions): ComponentOptions
317
    {
318
        $aMethods = $this->getPublicMethods($xReflectionClass);
319
        $xMetadata = $this->getComponentMetadata($xReflectionClass, $aMethods[0], $aOptions);
320
321
        return new ComponentOptions($aMethods, $aOptions, $xMetadata);
322
    }
323
}
324