CallableClassPlugin::setTarget()   A
last analyzed

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
eloc 3
dl 0
loc 5
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 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\ClassContainer;
26
use Jaxon\Di\Container;
27
use Jaxon\Exception\RequestException;
28
use Jaxon\Exception\SetupException;
29
use Jaxon\Plugin\AbstractRequestPlugin;
30
use Jaxon\Request\Target;
31
use Jaxon\Request\Validator;
32
use Jaxon\Response\AbstractResponse;
33
use Jaxon\Utils\Template\TemplateEngine;
34
use Psr\Http\Message\ServerRequestInterface;
35
use Exception;
36
use ReflectionException;
37
38
use function array_reduce;
39
use function is_array;
40
use function is_string;
41
use function md5;
42
use function str_replace;
43
use function strpos;
44
use function strlen;
45
use function substr;
46
use function trim;
47
use function uksort;
48
49
class CallableClassPlugin extends AbstractRequestPlugin
50
{
51
    /**
52
     * The class constructor
53
     *
54
     * @param string $sPrefix
55
     * @param bool $bDebug
56
     * @param Container $di
57
     * @param ClassContainer $cls
58
     * @param CallableRegistry $xRegistry
59
     * @param TemplateEngine $xTemplateEngine
60
     * @param Translator $xTranslator
61
     * @param Validator $xValidator
62
     */
63
    public function __construct(private string $sPrefix, private bool $bDebug,
64
        private Container $di, private ClassContainer $cls,
65
        private CallableRegistry $xRegistry, private TemplateEngine $xTemplateEngine,
66
        private Translator $xTranslator, private Validator $xValidator)
67
    {}
68
69
    /**
70
     * @inheritDoc
71
     */
72
    public function getName(): string
73
    {
74
        return Jaxon::CALLABLE_CLASS;
75
    }
76
77
    /**
78
     * @inheritDoc
79
     * @throws SetupException
80
     */
81
    public function checkOptions(string $sCallable, $xOptions): array
82
    {
83
        if(!$this->xValidator->validateClass(trim($sCallable)))
84
        {
85
            throw new SetupException($this->xTranslator->trans('errors.objects.invalid-declaration'));
86
        }
87
        if(is_string($xOptions))
88
        {
89
            $xOptions = ['include' => $xOptions];
90
        }
91
        elseif(!is_array($xOptions))
92
        {
93
            throw new SetupException($this->xTranslator->trans('errors.objects.invalid-declaration'));
94
        }
95
        return $xOptions;
96
    }
97
98
    /**
99
     * @inheritDoc
100
     */
101
    public function register(string $sType, string $sCallable, array $aOptions): bool
102
    {
103
        $sClassName = trim($sCallable);
104
        $this->xRegistry->registerClass($sClassName, $aOptions);
105
        return true;
106
    }
107
108
    /**
109
     * @inheritDoc
110
     * @throws SetupException
111
     */
112
    public function getCallable(string $sCallable)
113
    {
114
        return $this->cls->makeCallableObject($sCallable);
115
    }
116
117
    /**
118
     * @inheritDoc
119
     */
120
    public function getHash(): string
121
    {
122
        $this->xRegistry->parseCallableClasses();
123
        return md5($this->xRegistry->getHash());
124
    }
125
126
    /**
127
     * Generate client side javascript code for namespaces
128
     *
129
     * @return string
130
     */
131
    private function getNamespacesScript(): string
132
    {
133
        $sCode = '';
134
        $aJsClasses = [];
135
        foreach($this->xRegistry->getNamespaces() as $sNamespace)
136
        {
137
            $offset = 0;
138
            $sJsNamespace = str_replace('\\', '.', $sNamespace);
139
            $sJsNamespace .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
140
            while(($dotPosition = strpos($sJsNamespace, '.', $offset)) !== false)
141
            {
142
                $sJsClass = substr($sJsNamespace, 0, $dotPosition);
143
                // Generate code for this object
144
                if(!isset($aJsClasses[$sJsClass]))
145
                {
146
                    $sCode .= $this->sPrefix . "$sJsClass = {};\n";
147
                    $aJsClasses[$sJsClass] = $sJsClass;
148
                }
149
                $offset = $dotPosition + 1;
150
            }
151
        }
152
        return $sCode;
153
    }
154
155
    /**
156
     * Generate client side javascript code for a callable class
157
     *
158
     * @param CallableObject $xCallableObject The corresponding callable object
159
     *
160
     * @return string
161
     */
162
    private function getCallableScript(CallableObject $xCallableObject): string
163
    {
164
        if($xCallableObject->excluded())
165
        {
166
            return '';
167
        }
168
169
        return $this->xTemplateEngine->render('jaxon::callables/object.js', [
170
            'sPrefix' => $this->sPrefix,
171
            'sClass' => $xCallableObject->getJsName(),
172
            'aMethods' => $xCallableObject->getCallableMethods(),
173
        ]);
174
    }
175
176
    /**
177
     * Generate client side javascript code for the registered callable objects
178
     *
179
     * @return string
180
     * @throws SetupException
181
     */
182
    public function getScript(): string
183
    {
184
        $this->xRegistry->parseCallableClasses();
185
        $aCallableObjects = $this->cls->getCallableObjects();
186
        // Sort the options by key length asc
187
        uksort($aCallableObjects, function($name1, $name2) {
188
            return strlen($name1) - strlen($name2);
189
        });
190
191
        return array_reduce($aCallableObjects, function($sCode, $xCallableObject) {
192
            return $sCode . $this->getCallableScript($xCallableObject);
193
        }, $this->getNamespacesScript());
194
    }
195
196
    /**
197
     * @inheritDoc
198
     */
199
    public static function canProcessRequest(ServerRequestInterface $xRequest): bool
200
    {
201
        $aCall = $xRequest->getAttribute('jxncall');
202
        return $aCall !== null && ($aCall['type'] ?? '') === 'class' &&
203
            isset($aCall['name']) && isset($aCall['method']);
204
    }
205
206
    /**
207
     * @inheritDoc
208
     */
209
    public function setTarget(ServerRequestInterface $xRequest): Target
210
    {
211
        $aCall = $xRequest->getAttribute('jxncall');
212
        $this->xTarget = Target::makeClass(trim($aCall['name']), trim($aCall['method']));
213
        return $this->xTarget;
214
    }
215
216
    /**
217
     * @param Exception $xException
218
     * @param string $sErrorMessage
219
     *
220
     * @throws RequestException
221
     * @return void
222
     */
223
    private function throwException(Exception $xException, string $sErrorMessage): void
224
    {
225
        $this->di->getLogger()->error($xException->getMessage());
226
        throw new RequestException($sErrorMessage . (!$this->bDebug ? '' :
227
            "\n" . $xException->getMessage()));
228
    }
229
230
    /**
231
     * @inheritDoc
232
     * @throws RequestException
233
     */
234
    public function processRequest(): ?AbstractResponse
235
    {
236
        $sClassName = $this->xTarget->getClassName();
237
        $sMethodName = $this->xTarget->getMethodName();
238
239
        if(!$this->xValidator->validateClass($sClassName) ||
240
            !$this->xValidator->validateMethod($sMethodName))
241
        {
242
            // Unable to find the requested object or method
243
            throw new RequestException($this->xTranslator->trans('errors.objects.invalid',
244
                ['class' => $sClassName, 'method' => $sMethodName]));
245
        }
246
247
        // Call the requested method
248
        try
249
        {
250
            /** @var CallableObject */
251
            $xCallableObject = $this->getCallable($sClassName);
252
        }
253
        catch(ReflectionException|SetupException $e)
254
        {
255
            // Unable to find the requested class or method
256
            $this->throwException($e, $this->xTranslator->trans('errors.objects.invalid',
257
                ['class' => $sClassName, 'method' => $sMethodName]));
258
        }
259
        try
260
        {
261
            return $xCallableObject->call($this->xTarget);
262
        }
263
        catch(ReflectionException|SetupException $e)
264
        {
265
            // Unable to execute the requested class or method
266
            $this->throwException($e, $this->xTranslator->trans('errors.objects.call',
267
                ['class' => $sClassName, 'method' => $sMethodName]));
268
        }
269
    }
270
}
271