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

ComponentTrait::getPublicMethods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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