CallableClassPlugin   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 292
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 91
c 2
b 0
f 0
dl 0
loc 292
rs 9.36
wmc 38

15 Methods

Rating   Name   Duplication   Size   Complexity  
A renderMethod() 0 12 3
A register() 0 5 1
A getCallable() 0 3 1
A addCallable() 0 22 5
A checkOptions() 0 15 4
A processRequest() 0 34 5
A renderCallable() 0 20 3
A getScript() 0 22 3
A throwException() 0 7 2
A getName() 0 3 1
A renderChild() 0 10 1
A __construct() 0 5 1
A getHash() 0 4 1
A canProcessRequest() 0 6 6
A setTarget() 0 4 1
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\Request\Target;
30
use Jaxon\Request\Validator;
31
use Jaxon\Utils\Template\TemplateEngine;
32
use Psr\Http\Message\ServerRequestInterface;
33
use Psr\Log\LoggerInterface;
34
use ReflectionException;
35
36
use function array_map;
37
use function array_merge;
38
use function count;
39
use function explode;
40
use function implode;
41
use function is_array;
42
use function is_string;
43
use function md5;
44
use function str_repeat;
45
use function trim;
46
47
class CallableClassPlugin extends AbstractRequestPlugin
48
{
49
    /**
50
     * @var array<CallableObject>
51
     */
52
    private array $aCallableObjects = [];
53
54
    /**
55
     * @var array<string>
56
     */
57
    private array $aCallableNames = [];
58
59
    /**
60
     * The class constructor
61
     *
62
     * @param string $sPrefix
63
     * @param LoggerInterface $xLogger
64
     * @param ComponentContainer $cdi
65
     * @param ComponentRegistry $xRegistry
66
     * @param Translator $xTranslator
67
     * @param TemplateEngine $xTemplateEngine
68
     * @param Validator $xValidator
69
     */
70
    public function __construct(private string $sPrefix,
71
        private LoggerInterface $xLogger, private ComponentContainer $cdi,
72
        private ComponentRegistry $xRegistry, private Translator $xTranslator,
73
        private TemplateEngine $xTemplateEngine, private Validator $xValidator)
74
    {}
75
76
    /**
77
     * @inheritDoc
78
     */
79
    public function getName(): string
80
    {
81
        return Jaxon::CALLABLE_CLASS;
82
    }
83
84
    /**
85
     * @inheritDoc
86
     * @throws SetupException
87
     */
88
    public function checkOptions(string $sCallable, $xOptions): array
89
    {
90
        if(!$this->xValidator->validateClass(trim($sCallable)))
91
        {
92
            throw new SetupException($this->xTranslator->trans('errors.objects.invalid-declaration'));
93
        }
94
        if(is_string($xOptions))
95
        {
96
            $xOptions = ['include' => $xOptions];
97
        }
98
        elseif(!is_array($xOptions))
99
        {
100
            throw new SetupException($this->xTranslator->trans('errors.objects.invalid-declaration'));
101
        }
102
        return $xOptions;
103
    }
104
105
    /**
106
     * @inheritDoc
107
     */
108
    public function register(string $sType, string $sCallable, array $aOptions): bool
109
    {
110
        $sClassName = trim($sCallable);
111
        $this->xRegistry->registerComponent($sClassName, $aOptions);
112
        return true;
113
    }
114
115
    /**
116
     * @inheritDoc
117
     * @throws SetupException
118
     */
119
    public function getCallable(string $sCallable): CallableObject|null
120
    {
121
        return $this->cdi->makeCallableObject($sCallable);
122
    }
123
124
    /**
125
     * @inheritDoc
126
     */
127
    public function getHash(): string
128
    {
129
        $this->xRegistry->registerAllComponents();
130
        return md5($this->xRegistry->getHash());
131
    }
132
133
    /**
134
     * Add a callable object to the script generator
135
     *
136
     * @param CallableObject $xCallableObject
137
     *
138
     * @return void
139
     */
140
    private function addCallable(CallableObject $xCallableObject): void
141
    {
142
        $aCallableMethods = $xCallableObject->getCallableMethods();
143
        if($xCallableObject->excluded() || count($aCallableMethods) === 0)
144
        {
145
            return;
146
        }
147
148
        $aCallableObject = &$this->aCallableObjects;
149
        $sJsName = $xCallableObject->getJsName();
150
        foreach(explode('.', $sJsName) as $sName)
151
        {
152
            if(!isset($aCallableObject['children'][$sName]))
153
            {
154
                $aCallableObject['children'][$sName] = [];
155
            }
156
            $aCallableObject = &$aCallableObject['children'][$sName];
157
        }
158
159
        $aCallableObject['methods'] = $aCallableMethods;
160
        $aCallableObject['index'] = count($this->aCallableNames);
161
        $this->aCallableNames[] = $sJsName;
162
    }
163
164
    /**
165
     * @param string $sIndent
166
     * @param array $aTemplateVars
167
     *
168
     * @return string
169
     */
170
    private function renderMethod(string $sIndent, array $aTemplateVars): string
171
    {
172
        $aOptions = [];
173
        foreach($aTemplateVars['aMethod']['options'] as $sKey => $sValue)
174
        {
175
            $aOptions[] = "$sKey: $sValue";
176
        }
177
        $aTemplateVars['sArguments'] = count($aOptions) === 0 ? 'args' :
178
            'args, { ' . implode(', ', $aOptions) . ' }';
179
180
        return $sIndent . trim($this->xTemplateEngine
181
            ->render('jaxon::callables/method.js', $aTemplateVars));
182
    }
183
184
    /**
185
     * @param string $sJsClass
186
     * @param array $aCallable
187
     * @param int $nIndent
188
     *
189
     * @return string
190
     */
191
    private function renderCallable(string $sJsClass, array $aCallable, int $nIndent): string
192
    {
193
        $nIndent += 2; // Indentation.
194
        $sIndent = str_repeat(' ', $nIndent);
195
196
        $fMethodCallback = fn($aMethod) => $this->renderMethod($sIndent, [
197
            'aMethod' => $aMethod,
198
            'nIndex' => $aCallable['index'] ?? 0,
199
        ]);
200
        $aMethods = !isset($aCallable['methods']) ? [] :
201
            array_map($fMethodCallback, $aCallable['methods']);
202
203
        $aChildren = [];
204
        foreach($aCallable['children'] ?? [] as $sName => $aChild)
205
        {
206
            $aChildren[] = $this->renderChild("$sName:", "$sJsClass.$sName",
207
                $aChild, $nIndent) . ',';
208
        }
209
210
        return implode("\n", array_merge($aMethods, $aChildren));
211
    }
212
213
    /**
214
     * @param string $sJsVar
215
     * @param string $sJsClass
216
     * @param array $aCallable
217
     * @param int $nIndent
218
     *
219
     * @return string
220
     */
221
    private function renderChild(string $sJsVar, string $sJsClass,
222
        array $aCallable, int $nIndent = 0): string
223
    {
224
        $sIndent = str_repeat(' ', $nIndent);
225
        $sScript = $this->renderCallable($sJsClass, $aCallable, $nIndent);
226
227
        return <<<CODE
228
$sIndent$sJsVar {
229
$sScript
230
$sIndent}
231
CODE;
232
    }
233
234
    /**
235
     * Generate client side javascript code for the registered callable objects
236
     *
237
     * @return string
238
     * @throws SetupException
239
     */
240
    public function getScript(): string
241
    {
242
        $this->xRegistry->registerAllComponents();
243
244
        $this->aCallableNames = [];
245
        $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...
246
        foreach($this->cdi->getCallableObjects() as $xCallableObject)
247
        {
248
            $this->addCallable($xCallableObject);
249
        }
250
251
        $aScripts = [
252
            $this->xTemplateEngine ->render('jaxon::callables/objects.js', [
253
                'aCallableNames' => $this->aCallableNames,
254
            ])
255
        ];
256
        foreach($this->aCallableObjects['children'] as $sJsClass => $aCallable)
257
        {
258
            $aScripts[] = $this->renderChild("{$this->sPrefix}$sJsClass =",
259
                $sJsClass, $aCallable) . ';';
260
        }
261
        return implode("\n", $aScripts) . "\n";
262
    }
263
264
    /**
265
     * @inheritDoc
266
     */
267
    public static function canProcessRequest(ServerRequestInterface $xRequest): bool
268
    {
269
        $aCall = $xRequest->getAttribute('jxncall');
270
        return $aCall !== null && ($aCall['type'] ?? '') === 'class' &&
271
            isset($aCall['name']) && isset($aCall['method']) &&
272
            is_string($aCall['name']) && is_string($aCall['method']);
273
    }
274
275
    /**
276
     * @inheritDoc
277
     */
278
    public function setTarget(ServerRequestInterface $xRequest): Target
279
    {
280
        $this->xTarget = Target::makeClass($xRequest->getAttribute('jxncall'));
281
        return $this->xTarget;
282
    }
283
284
    /**
285
     * @param string $sExceptionMessage
286
     * @param string $sErrorCode
287
     * @param array $aErrorParams
288
     *
289
     * @throws RequestException
290
     * @return void
291
     */
292
    private function throwException(string $sExceptionMessage,
293
        string $sErrorCode, array $aErrorParams = []): void
294
    {
295
        $sMessage = $this->xTranslator->trans($sErrorCode, $aErrorParams) .
296
            (!$sExceptionMessage ? '' : "\n$sExceptionMessage");
297
        $this->xLogger->error($sMessage);
298
        throw new RequestException($sMessage);
299
    }
300
301
    /**
302
     * @inheritDoc
303
     * @throws RequestException
304
     */
305
    public function processRequest(): void
306
    {
307
        $sClassName = $this->xTarget->getClassName();
308
        $sMethodName = $this->xTarget->getMethodName();
309
        // Will be used to print a translated error message.
310
        $aErrorParams = ['class' => $sClassName, 'method' => $sMethodName];
311
312
        if(!$this->xValidator->validateJsObject($sClassName) ||
313
            !$this->xValidator->validateMethod($sMethodName))
314
        {
315
            // Unable to find the requested object or method
316
            $this->throwException('', 'errors.objects.invalid', $aErrorParams);
317
        }
318
319
        // Call the requested method
320
        try
321
        {
322
            $sError = 'errors.objects.find';
323
            /** @var CallableObject */
324
            $xCallableObject = $this->getCallable($sClassName);
325
326
            if($xCallableObject->excluded($sMethodName))
327
            {
328
                // Unable to find the requested class or method
329
                $this->throwException('', 'errors.objects.excluded', $aErrorParams);
330
            }
331
332
            $sError = 'errors.objects.call';
333
            $xCallableObject->call($this->xTarget);
334
        }
335
        catch(ReflectionException|SetupException $e)
336
        {
337
            // Unable to execute the requested class or method
338
            $this->throwException($e->getMessage(), $sError, $aErrorParams);
339
        }
340
    }
341
}
342