Passed
Push — main ( 82104f...ee6fb2 )
by Thierry
05:16
created

CallableClassPlugin::register()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 3
dl 0
loc 5
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * CallableClassPlugin.php - Jaxon callable class plugin
5
 *
6
 * This class registers user defined callable classes, and calls their methods on user request.
7
 *
8
 * @package jaxon-core
9
 * @author Jared White
10
 * @author J. Max Wilson
11
 * @author Joseph Woolley
12
 * @author Steffen Konerow
13
 * @author Thierry Feuzeu <[email protected]>
14
 * @copyright Copyright (c) 2005-2007 by Jared White & J. Max Wilson
15
 * @copyright Copyright (c) 2008-2010 by Joseph Woolley, Steffen Konerow, Jared White  & J. Max Wilson
16
 * @copyright 2016 Thierry Feuzeu <[email protected]>
17
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
18
 * @link https://github.com/jaxon-php/jaxon-core
19
 */
20
21
namespace Jaxon\Plugin\Request\CallableClass;
22
23
use Jaxon\Jaxon;
24
use Jaxon\App\I18n\Translator;
25
use Jaxon\Di\ComponentContainer;
26
use Jaxon\Exception\RequestException;
27
use Jaxon\Exception\SetupException;
28
use Jaxon\Plugin\AbstractRequestPlugin;
29
use Jaxon\Plugin\JsCode;
30
use Jaxon\Plugin\JsCodeGeneratorInterface;
31
use Jaxon\Request\Target;
32
use Jaxon\Request\Validator;
33
use Jaxon\Utils\Template\TemplateEngine;
34
use Psr\Http\Message\ServerRequestInterface;
35
use Psr\Log\LoggerInterface;
36
use ReflectionException;
37
38
use function array_map;
39
use function array_merge;
40
use function count;
41
use function explode;
42
use function implode;
43
use function is_array;
44
use function is_string;
45
use function md5;
46
use function str_repeat;
47
use function trim;
48
49
class CallableClassPlugin extends AbstractRequestPlugin implements JsCodeGeneratorInterface
50
{
51
    /**
52
     * @var array<CallableObject>
53
     */
54
    private array $aCallableObjects = [];
55
56
    /**
57
     * @var array<string>
58
     */
59
    private array $aCallableParams = [];
60
61
    /**
62
     * The class constructor
63
     *
64
     * @param string $sPrefix
65
     * @param LoggerInterface $xLogger
66
     * @param ComponentContainer $cdi
67
     * @param ComponentRegistry $xRegistry
68
     * @param Translator $xTranslator
69
     * @param TemplateEngine $xTemplateEngine
70
     * @param Validator $xValidator
71
     */
72
    public function __construct(private string $sPrefix,
73
        private LoggerInterface $xLogger, private ComponentContainer $cdi,
74
        private ComponentRegistry $xRegistry, private Translator $xTranslator,
75
        private TemplateEngine $xTemplateEngine, private Validator $xValidator)
76
    {}
77
78
    /**
79
     * @inheritDoc
80
     */
81
    public function getName(): string
82
    {
83
        return Jaxon::CALLABLE_CLASS;
84
    }
85
86
    /**
87
     * @inheritDoc
88
     * @throws SetupException
89
     */
90
    public function checkOptions(string $sCallable, $xOptions): array
91
    {
92
        if(!$this->xValidator->validateClass(trim($sCallable)))
93
        {
94
            throw new SetupException($this->xTranslator->trans('errors.objects.invalid-declaration'));
95
        }
96
        if(is_string($xOptions))
97
        {
98
            $xOptions = ['include' => $xOptions];
99
        }
100
        elseif(!is_array($xOptions))
101
        {
102
            throw new SetupException($this->xTranslator->trans('errors.objects.invalid-declaration'));
103
        }
104
        return $xOptions;
105
    }
106
107
    /**
108
     * @inheritDoc
109
     */
110
    public function register(string $sType, string $sCallable, array $aOptions): bool
111
    {
112
        $sClassName = trim($sCallable);
113
        $this->xRegistry->registerComponent($sClassName, $aOptions);
114
        return true;
115
    }
116
117
    /**
118
     * @inheritDoc
119
     * @throws SetupException
120
     */
121
    public function getCallable(string $sCallable): CallableObject|null
122
    {
123
        return $this->cdi->makeCallableObject($sCallable);
124
    }
125
126
    /**
127
     * @inheritDoc
128
     */
129
    public function getHash(): string
130
    {
131
        $this->xRegistry->registerAllComponents();
132
        return md5($this->xRegistry->getHash());
133
    }
134
135
    /**
136
     * Add a callable object to the script generator
137
     *
138
     * @param CallableObject $xCallableObject
139
     *
140
     * @return void
141
     */
142
    private function addCallable(CallableObject $xCallableObject): void
143
    {
144
        $aCallableMethods = $xCallableObject->getCallableMethods();
145
        if($xCallableObject->excluded() || count($aCallableMethods) === 0)
146
        {
147
            return;
148
        }
149
150
        $aCallableObject = &$this->aCallableObjects;
151
        $sJsName = $xCallableObject->getJsName();
152
        foreach(explode('.', $sJsName) as $sName)
153
        {
154
            if(!isset($aCallableObject['children'][$sName]))
155
            {
156
                $aCallableObject['children'][$sName] = [];
157
            }
158
            $aCallableObject = &$aCallableObject['children'][$sName];
159
        }
160
161
        $sJsParam = $xCallableObject->getJsParam();
162
163
        $aCallableObject['methods'] = $aCallableMethods;
164
        $aCallableObject['param'] = $sJsParam;
165
166
        // Add the js param to the list, if it is not already in.
167
        if(isset($this->aCallableParams[$sJsParam]))
168
        {
169
            $aCallableObject['index'] = $this->aCallableParams[$sJsParam];
170
            return;
171
        }
172
173
        $nIndex = count($this->aCallableParams);
174
        $this->aCallableParams[$sJsParam] = $nIndex;
175
        $aCallableObject['index'] = $nIndex;
176
    }
177
178
    /**
179
     * @param string $sIndent
180
     * @param array $aTemplateVars
181
     *
182
     * @return string
183
     */
184
    private function renderMethod(string $sIndent, array $aTemplateVars): string
185
    {
186
        $aOptions = [];
187
        foreach($aTemplateVars['aMethod']['options'] as $sKey => $sValue)
188
        {
189
            $aOptions[] = "$sKey: $sValue";
190
        }
191
        $aTemplateVars['sArguments'] = count($aOptions) === 0 ? 'args' :
192
            'args, { ' . implode(', ', $aOptions) . ' }';
193
194
        return $sIndent . trim($this->xTemplateEngine
195
            ->render('jaxon::callables/method.js', $aTemplateVars));
196
    }
197
198
    /**
199
     * @param string $sJsClass
200
     * @param array $aCallable
201
     * @param int $nIndent
202
     *
203
     * @return string
204
     */
205
    private function renderCallable(string $sJsClass, array $aCallable, int $nIndent): string
206
    {
207
        $nIndent += 2; // Indentation.
208
        $sIndent = str_repeat(' ', $nIndent);
209
210
        $fMethodCallback = fn($aMethod) => $this->renderMethod($sIndent, [
211
            'aMethod' => $aMethod,
212
            'nIndex' => $aCallable['index'] ?? 0,
213
        ]);
214
        $aMethods = !isset($aCallable['methods']) ? [] :
215
            array_map($fMethodCallback, $aCallable['methods']);
216
217
        $aChildren = [];
218
        foreach($aCallable['children'] ?? [] as $sName => $aChild)
219
        {
220
            $aChildren[] = $this->renderChild("$sName:", "$sJsClass.$sName",
221
                $aChild, $nIndent) . ',';
222
        }
223
224
        return implode("\n", array_merge($aMethods, $aChildren));
225
    }
226
227
    /**
228
     * @param string $sJsVar
229
     * @param string $sJsClass
230
     * @param array $aCallable
231
     * @param int $nIndent
232
     *
233
     * @return string
234
     */
235
    private function renderChild(string $sJsVar, string $sJsClass,
236
        array $aCallable, int $nIndent = 0): string
237
    {
238
        $sIndent = str_repeat(' ', $nIndent);
239
        $sScript = $this->renderCallable($sJsClass, $aCallable, $nIndent);
240
241
        return <<<CODE
242
$sIndent$sJsVar {
243
$sScript
244
$sIndent}
245
CODE;
246
    }
247
248
    /**
249
     * Generate client side javascript code for the registered callable objects
250
     *
251
     * @return string
252
     * @throws SetupException
253
     */
254
    public function getJsCode(): JsCode
255
    {
256
        $this->xRegistry->registerAllComponents();
257
258
        $this->aCallableParams = [];
259
        $this->aCallableObjects = ['children' => []];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('children' => array()) of type array<string,array> is incompatible with the declared type Jaxon\Plugin\Request\Cal...eClass\CallableObject[] of property $aCallableObjects.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
260
        foreach($this->cdi->getCallableObjects() as $xCallableObject)
261
        {
262
            $this->addCallable($xCallableObject);
263
        }
264
265
        $aScripts = [
266
            $this->xTemplateEngine ->render('jaxon::callables/objects.js', [
267
                'aCallableParams' => $this->aCallableParams,
268
            ])
269
        ];
270
        foreach($this->aCallableObjects['children'] as $sJsClass => $aCallable)
271
        {
272
            $aScripts[] = $this->renderChild("{$this->sPrefix}$sJsClass =",
273
                $sJsClass, $aCallable) . ';';
274
        }
275
        return new JsCode(implode("\n", $aScripts) . "\n");
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Jaxon\Plugin\...(' ', $aScripts) . ' ') returns the type Jaxon\Plugin\JsCode which is incompatible with the documented return type string.
Loading history...
276
    }
277
278
    /**
279
     * @inheritDoc
280
     */
281
    public static function canProcessRequest(ServerRequestInterface $xRequest): bool
282
    {
283
        $aCall = $xRequest->getAttribute('jxncall');
284
        return $aCall !== null && ($aCall['type'] ?? '') === 'class' &&
285
            isset($aCall['name']) && isset($aCall['method']) &&
286
            is_string($aCall['name']) && is_string($aCall['method']);
287
    }
288
289
    /**
290
     * @inheritDoc
291
     */
292
    public function setTarget(ServerRequestInterface $xRequest): Target
293
    {
294
        $this->xTarget = Target::makeClass($xRequest->getAttribute('jxncall'));
295
        return $this->xTarget;
296
    }
297
298
    /**
299
     * @param string $sExceptionMessage
300
     * @param string $sErrorCode
301
     * @param array $aErrorParams
302
     *
303
     * @throws RequestException
304
     * @return void
305
     */
306
    private function throwException(string $sExceptionMessage,
307
        string $sErrorCode, array $aErrorParams = []): void
308
    {
309
        $sMessage = $this->xTranslator->trans($sErrorCode, $aErrorParams) .
310
            (!$sExceptionMessage ? '' : "\n$sExceptionMessage");
311
        $this->xLogger->error($sMessage);
312
        throw new RequestException($sMessage);
313
    }
314
315
    /**
316
     * @inheritDoc
317
     * @throws RequestException
318
     */
319
    public function processRequest(): void
320
    {
321
        $sClassName = $this->xTarget->getClassName();
322
        $sMethodName = $this->xTarget->getMethodName();
323
        // Will be used to print a translated error message.
324
        $aErrorParams = ['class' => $sClassName, 'method' => $sMethodName];
325
326
        if(!$this->xValidator->validateJsObject($sClassName) ||
327
            !$this->xValidator->validateMethod($sMethodName))
328
        {
329
            // Unable to find the requested object or method
330
            $this->throwException('', 'errors.objects.invalid', $aErrorParams);
331
        }
332
333
        // Call the requested method
334
        try
335
        {
336
            $sError = 'errors.objects.find';
337
            /** @var CallableObject */
338
            $xCallableObject = $this->getCallable($sClassName);
339
340
            if($xCallableObject->excluded($sMethodName))
341
            {
342
                // Unable to find the requested class or method
343
                $this->throwException('', 'errors.objects.excluded', $aErrorParams);
344
            }
345
346
            $sError = 'errors.objects.call';
347
            $xCallableObject->call($this->xTarget);
348
        }
349
        catch(ReflectionException|SetupException $e)
350
        {
351
            // Unable to execute the requested class or method
352
            $this->throwException($e->getMessage(), $sError, $aErrorParams);
353
        }
354
    }
355
}
356