Passed
Push — main ( fd5484...10601e )
by Thierry
05:13
created

CallableClassPlugin::renderChild()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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