Passed
Push — main ( d71b68...a743f7 )
by Dimitri
08:12 queued 04:09
created

ComponentLoader::getMethodParams()   A

Complexity

Conditions 4
Paths 10

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 1
b 0
f 0
nc 10
nop 3
dl 0
loc 20
ccs 6
cts 6
cp 1
crap 4
rs 9.9332
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\View\Components;
13
14
use BlitzPHP\Cache\CacheInterface;
15
use BlitzPHP\Container\Services;
16
use BlitzPHP\Exceptions\ViewException;
17
use DI\NotFoundException;
18
use ReflectionException;
19
use ReflectionMethod;
20
21
/**
22
 * Une classe simple qui peut appeler n'importe quelle autre classe qui peut être chargée, et afficher son résultat.
23
 * Destinée à afficher de petits blocs de contenu dans des vues qui peuvent être gérées par d'autres bibliothèques
24
 * et qui ne nécessitent pas d'être chargées dans le contrôleur.
25
 *
26
 * Utilisée avec la fonction d'aide, son utilisation sera la suivante :
27
 *
28
 *         component('\Some\Class::method', 'limit=5 sort=asc', 60, 'cache-name');
29
 *
30
 * Les paramètres sont mis en correspondance avec les arguments de la méthode de rappel portant le même nom :
31
 *
32
 *         class Class {
33
 *             function method($limit, $sort)
34
 *         }
35
 *
36
 * Sinon, les paramètres seront transmis à la méthode de callback sous la forme d'un simple tableau si les paramètres correspondants ne sont pas trouvés.
37
 *
38
 *         class Class {
39
 *             function method(array $params=null)
40
 *         }
41
 *
42
 * @credit <a href="http://www.codeigniter.com">CodeIgniter 4.5 - CodeIgniter\View\Cell</a>
43
 */
44
class ComponentLoader
45
{
46
    /**
47
     * @param CacheInterface $cache Instance du Cache
48
     */
49
    public function __construct(protected CacheInterface $cache)
50
    {
51
    }
52
53
    /**
54
     * Rendre un composant, en renvoyant son corps sous forme de chaîne.
55
     *
56
     * @param string            $library   Nom de la classe et de la méthode du composant.
57
     * @param array|string|null $params    Paramètres à passer à la méthode.
58
     * @param int               $ttl       Nombre de secondes pour la mise en cache de la cellule.
59
     * @param string|null       $cacheName Nom de l'élément mis en cache.
60
     *
61
     * @throws ReflectionException
62
     */
63
    public function render(string $library, null|array|string $params = null, int $ttl = 0, ?string $cacheName = null): string
64
    {
65 12
        [$instance, $method] = $this->determineClass($library);
66
67 12
        $class = is_object($instance) ? $instance::class : null;
68
69 12
        $params = $this->prepareParams($params);
70
71
        // Le résultat est-il mis en cache ??
72 12
        $cacheName ??= str_replace(['\\', '/'], '', $class) . $method . md5(serialize($params));
73
74
        if ($output = $this->cache->get($cacheName)) {
75 2
            return $output;
76
        }
77
78
        if (method_exists($instance, 'initialize')) {
79 2
            $instance->initialize(Services::request(), Services::response(), Services::logger());
80
        }
81
82
        if (! method_exists($instance, $method)) {
83 4
            throw ViewException::invalidComponentMethod($class, $method);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null; however, parameter $class of BlitzPHP\Exceptions\View...nvalidComponentMethod() does only seem to accept string, 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

83
            throw ViewException::invalidComponentMethod(/** @scrutinizer ignore-type */ $class, $method);
Loading history...
84
        }
85
86
        $output = $instance instanceof Component
87
            ? $this->renderComponent($instance, $method, $params)
88 10
            : $this->renderSimpleClass($instance, $method, $params, $class);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null; however, parameter $class of BlitzPHP\View\Components...er::renderSimpleClass() does only seem to accept string, 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

88
            : $this->renderSimpleClass($instance, $method, $params, /** @scrutinizer ignore-type */ $class);
Loading history...
89
90
        // Doit-on le mettre en cache?
91
        if ($ttl !== 0) {
92 2
            $this->cache->set($cacheName, $output, $ttl);
93
        }
94
95 10
        return $output;
96
    }
97
98
    /**
99
     * Analyse l'attribut params. S'il s'agit d'un tableau, il est renvoyé tel quel.
100
     * S'il s'agit d'une chaîne, elle doit être au format "clé1=valeur clé2=valeur".
101
     * Elle sera divisée et renvoyée sous forme de tableau.
102
     *
103
     * @param array<string, string>|string|null $params
104
     */
105
    public function prepareParams($params): array
106
    {
107
        if ($params === null || $params === '' || $params === [] || (! is_string($params) && ! is_array($params))) {
108 8
            return [];
109
        }
110
111
        if (is_string($params)) {
112 12
            $newParams = [];
113 12
            $separator = ' ';
114
115
            if (str_contains($params, ',')) {
116 12
                $separator = ',';
117
            }
118
119 12
            $params = explode($separator, $params);
120 12
            unset($separator);
121
122
            foreach ($params as $p) {
123
                if ($p !== '') {
124 12
                    [$key, $val] = explode('=', $p);
125
126 12
                    $newParams[trim($key)] = trim($val, ', ');
127
                }
128
            }
129
130 12
            $params = $newParams;
131 12
            unset($newParams);
132
        }
133
134
        if ($params === []) {
135 2
            return [];
136
        }
137
138 14
        return $params;
139
    }
140
141
    /**
142
     * Étant donné la chaîne de la bibliothèque, tente de déterminer la classe et la méthode à appeler.
143
     */
144
    protected function determineClass(string $library): array
145
    {
146
        //  Nous ne voulons pas appeler les méthodes statiques par défaut, c'est pourquoi nous convertissons tous les doubles points.
147 12
        $library = str_replace('::', ':', $library);
148
149
        //  Les composants contrôlées peuvent être appelées avec le seul nom de la classe, c'est pourquoi il faut ajouter une méthode par défaut
150
        if (! str_contains($library, ':')) {
151 2
            $library .= ':render';
152
        }
153
154 12
        [$class, $method] = explode(':', $library);
155
156
        if ($class === '') {
157 2
            throw ViewException::noComponentClass();
158
        }
159
160
        //  localise et renvoie une instance du composant
161
        try {
162 12
            $object = Services::container()->get($class);
163
        } catch (NotFoundException) {
164 4
            $locator = Services::locator();
165
166
            if (false === $path = $locator->locateFile($class, 'Components')) {
167 2
                throw ViewException::invalidComponentClass($class);
168
            }
169
            if (false === $_class = $locator->findQualifiedNameFromPath($path)) {
170 2
                throw ViewException::invalidComponentClass($class);
171
            }
172
173
            try {
174 2
                $object = Services::container()->get($_class);
175
            } catch (NotFoundException) {
176
                throw ViewException::invalidComponentClass($class);
177
            }
178
        }
179
180
        if (! is_object($object)) {
181 12
            throw ViewException::invalidComponentClass($class);
182
        }
183
184
        if ($method === '') {
185 2
            $method = 'index';
186
        }
187
188
        return [
189
            $object,
190
            $method,
191 12
        ];
192
    }
193
194
    /**
195
     * Rend un cellule qui étend la classe Component.
196
     */
197
    final protected function renderComponent(Component $instance, string $method, array $params): string
198
    {
199
        // Ne permet de définir que des propriétés publiques, ou des propriétés protégées/privées
200
        // qui ont une méthode pour les obtenir (get<Foo>Property()).
201 2
        $publicProperties  = $instance->getPublicProperties();
202 2
        $privateProperties = array_column($instance->getNonPublicProperties(['view']), 'name');
203 2
        $publicParams      = array_intersect_key($params, $publicProperties);
204
205
        foreach ($params as $key => $value) {
206 2
            $getter = 'get' . ucfirst((string) $key) . 'Property';
207
            if (in_array($key, $privateProperties, true) && method_exists($instance, $getter)) {
208 2
                $publicParams[$key] = $value;
209
            }
210
        }
211
212
        // Remplir toutes les propriétés publiques qui ont été passées,
213
        // mais seulement celles qui se trouvent dans le tableau $pulibcProperties.
214 2
        $instance = $instance->fill($publicParams);
215
216
        //  S'il existe des propriétés protégées/privées, nous devons les envoyer à la méthode mount().
217
        if (method_exists($instance, 'mount')) {
218
            //  si des $params ont des clés qui correspondent au nom d'un argument de la méthode mount,
219
            // passer ces variables à la méthode.
220 2
            $mountParams = $this->getMethodParams($instance, 'mount', $params);
221 2
            $instance->mount(...$mountParams);
222
        }
223
224 2
        return $instance->{$method}();
225
    }
226
227
    /**
228
     * Renvoie les valeurs de $params qui correspondent aux paramètres d'une méthode, dans l'ordre où ils sont définis.
229
     * Cela permet de les passer directement dans la méthode.
230
     */
231
    private function getMethodParams(Component $instance, string $method, array $params): array
232
    {
233 2
        $mountParams = [];
234
235
        try {
236 2
            $reflectionMethod = new ReflectionMethod($instance, $method);
237 2
            $reflectionParams = $reflectionMethod->getParameters();
238
239
            foreach ($reflectionParams as $reflectionParam) {
240 2
                $paramName = $reflectionParam->getName();
241
242
                if (array_key_exists($paramName, $params)) {
243 2
                    $mountParams[] = $params[$paramName];
244
                }
245
            }
246
        } catch (ReflectionException) {
247
            // ne rien faire
248
        }
249
250 2
        return $mountParams;
251
    }
252
253
    /**
254
     * Rend la classe non-Component, en passant les paramètres string/array.
255
     *
256
     * @todo Déterminer si cela peut être remanié pour utiliser $this-getMethodParams().
257
     *
258
     * @param object $instance
259
     */
260
    final protected function renderSimpleClass($instance, string $method, array $params, string $class): string
261
    {
262
        // Essayez de faire correspondre la liste de paramètres qui nous a été fournie avec le nom
263
        // du paramètre dans la méthode de callback.
264 10
        $refMethod  = new ReflectionMethod($instance, $method);
265 10
        $paramCount = $refMethod->getNumberOfParameters();
266 10
        $refParams  = $refMethod->getParameters();
267
268
        if ($paramCount === 0) {
269
            if ($params !== []) {
270 2
                throw ViewException::missingComponentParameters($class, $method);
271
            }
272
273 4
            $output = $instance->{$method}();
274
        } elseif (($paramCount === 1)
275
            && ((! array_key_exists($refParams[0]->name, $params))
276
            || (array_key_exists($refParams[0]->name, $params)
277
            && count($params) !== 1))
278
        ) {
279 6
            $output = $instance->{$method}($params);
280
        } else {
281 2
            $fireArgs     = [];
282 2
            $methodParams = [];
283
284
            foreach ($refParams as $arg) {
285 2
                $methodParams[$arg->name] = true;
286
                if (array_key_exists($arg->name, $params)) {
287 2
                    $fireArgs[$arg->name] = $params[$arg->name];
288
                }
289
            }
290
291
            foreach (array_keys($params) as $key) {
292
                if (! isset($methodParams[$key])) {
293 2
                    throw ViewException::invalidComponentParameter($key);
294
                }
295
            }
296
297 2
            $output = $instance->{$method}(...array_values($fireArgs));
298
        }
299
300 8
        return $output;
301
    }
302
}
303