Completed
Push — master ( dbc4d9...4d3a24 )
by Thierry
01:44
created

CallableDir::createCallableObjects()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 8
nop 0
dl 0
loc 44
rs 7.9715
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * CallableDir.php - Jaxon callable dir plugin
5
 *
6
 * This class registers directories containing user defined callable classes,
7
 * and generates client side javascript code.
8
 *
9
 * @package jaxon-core
10
 * @author Jared White
11
 * @author J. Max Wilson
12
 * @author Joseph Woolley
13
 * @author Steffen Konerow
14
 * @author Thierry Feuzeu <[email protected]>
15
 * @copyright Copyright (c) 2005-2007 by Jared White & J. Max Wilson
16
 * @copyright Copyright (c) 2008-2010 by Joseph Woolley, Steffen Konerow, Jared White  & J. Max Wilson
17
 * @copyright 2016 Thierry Feuzeu <[email protected]>
18
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
19
 * @link https://github.com/jaxon-php/jaxon-core
20
 */
21
22
namespace Jaxon\Request\Plugin;
23
24
use Jaxon\Jaxon;
25
use Jaxon\Plugin\Request as RequestPlugin;
26
27
class CallableDir extends RequestPlugin
28
{
29
    use \Jaxon\Utils\Traits\Config;
30
    use \Jaxon\Utils\Traits\Manager;
31
    use \Jaxon\Utils\Traits\Validator;
32
    use \Jaxon\Utils\Traits\Translator;
33
34
    /**
35
     * The registered namespaces with their directories
36
     *
37
     * @var array
38
     */
39
    protected $aNamespaces = [];
40
41
    /**
42
     * The classes of the registered callable objects
43
     *
44
     * @var array
45
     */
46
    protected $aClassNames = [];
47
48
    /**
49
     * The registered callable objects
50
     *
51
     * @var array
52
     */
53
    protected $aCallableObjects = [];
54
55
    /**
56
     * True if the Composer autoload is enabled
57
     *
58
     * @var boolean
59
     */
60
    private $bAutoloadEnabled = true;
61
62
    /**
63
     * The Composer autoloader
64
     *
65
     * @var Autoloader
66
     */
67
    private $xAutoloader = null;
68
69
    /**
70
     * The value of the class parameter of the incoming Jaxon request
71
     *
72
     * @var string
73
     */
74
    protected $sRequestedClass = null;
75
76
    /**
77
     * The value of the method parameter of the incoming Jaxon request
78
     *
79
     * @var string
80
     */
81
    protected $sRequestedMethod = null;
82
83 View Code Duplication
    public function __construct()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
84
    {
85
        if(!empty($_GET['jxncls']))
86
        {
87
            $this->sRequestedClass = $_GET['jxncls'];
88
        }
89
        if(!empty($_GET['jxnmthd']))
90
        {
91
            $this->sRequestedMethod = $_GET['jxnmthd'];
92
        }
93
        if(!empty($_POST['jxncls']))
94
        {
95
            $this->sRequestedClass = $_POST['jxncls'];
96
        }
97
        if(!empty($_POST['jxnmthd']))
98
        {
99
            $this->sRequestedMethod = $_POST['jxnmthd'];
100
        }
101
    }
102
103
    /**
104
     * Return the name of this plugin
105
     *
106
     * @return string
107
     */
108
    public function getName()
109
    {
110
        return Jaxon::CALLABLE_DIR;
111
    }
112
113
    /**
114
     * Use the Composer autoloader
115
     *
116
     * @return void
117
     */
118
    public function useComposerAutoloader()
119
    {
120
        $this->bAutoloadEnabled = true;
121
        $this->xAutoloader = require(__DIR__ . '/../../../../autoload.php');
122
    }
123
124
    /**
125
     * Disable the autoloader in the library
126
     *
127
     * The user shall provide an alternative autoload system.
128
     *
129
     * @return void
130
     */
131
    public function disableAutoload()
132
    {
133
        $this->bAutoloadEnabled = false;
134
        $this->xAutoloader = null;
135
    }
136
137
    /**
138
     * Register a callable class
139
     *
140
     * @param string        $sType          The type of request handler being registered
141
     * @param string        $sDirectory     The name of the class being registered
142
     * @param array|string  $aOptions       The associated options
143
     *
144
     * @return boolean
145
     */
146
    public function register($sType, $sDirectory, $aOptions)
147
    {
148
        if($sType != $this->getName())
149
        {
150
            return false;
151
        }
152
153
        if(!is_string($sDirectory) || !is_dir($sDirectory))
154
        {
155
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid-declaration'));
156
        }
157
        $sDirectory = trim($sDirectory, DIRECTORY_SEPARATOR);
158
159
        if(is_string($aOptions))
160
        {
161
            $aOptions = ['namespace' => $aOptions];
162
        }
163
        if(!is_array($aOptions))
164
        {
165
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid-declaration'));
166
        }
167
168
        if(!is_dir(($sDirectory = trim($sDirectory))))
169
        {
170
            return false;
171
        }
172
        $aOptions['directory'] = $sDirectory;
173
174
        $aProtected = key_exists('protected', $aOptions) ? $aOptions['protected'] : [];
175
        if(!is_array($aProtected))
176
        {
177
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid-declaration'));
178
        }
179
        $aOptions['protected'] = $aProtected;
180
181
        $sSeparator = key_exists('separator', $aOptions) ? $aOptions['separator'] : '.';
182
        // Only '.' and '_' are allowed to be used as separator. Any other value is ignored and '.' is used instead.
183
        if(($sSeparator = trim($sSeparator)) != '_')
184
        {
185
            $sSeparator = '.';
186
        }
187
        $aOptions['separator'] = $sSeparator;
188
189
        $sNamespace = key_exists('namespace', $aOptions) ? $aOptions['namespace'] : '';
190
        if(!($sNamespace = trim($sNamespace, ' \\')))
191
        {
192
            $sNamespace = '';
193
        }
194
        $aOptions['namespace'] = $sNamespace;
195
196
        // Todo: Change the keys in $aOptions['classes'] to have "\" as separator
197
        // $aNewOptions = [];
198
        // foreach($aOptions['classes'] as $sClass => $aOption)
199
        // {
200
        //     $sClass = trim(str_replace(['.', '_'], ['\\', '\\'], $sClass), ' \\');
201
        //     $aNewOptions[$sClass] = $aOption;
202
        // }
203
        // $aOptions['classes'] = $aNewOptions;
204
205
        if(($sNamespace))
206
        {
207
            // Register the dir with PSR4 autoloading
208
            if(($this->xAutoloader))
209
            {
210
                $this->xAutoloader->setPsr4($sNamespace . '\\', $sDirectory);
211
            }
212
213
            $this->aNamespaces[$sNamespace] = $aOptions;
214
        }
215
        else
216
        {
217
            // Get the callable class plugin
218
            $callableClassPlugin = $this->getPluginManager()->getRequestPlugin(Jaxon::CALLABLE_CLASS);
219
220
            // Register the dir with classmap autoloading
221
            $itDir = new RecursiveDirectoryIterator($sDirectory);
222
            $itFile = new RecursiveIteratorIterator($itDir);
223
            // Iterate on dir content
224
            foreach($itFile as $xFile)
225
            {
226
                // skip everything except PHP files
227
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
228
                {
229
                    continue;
230
                }
231
232
                $sClassName = $xFile->getBasename('.php');
233
                if(($this->xAutoloader))
234
                {
235
                    $this->xAutoloader->addClassMap([$sClassName => $xFile->getPathname()]);
236
                }
237
                elseif(!class_exists($sClassName))
238
                {
239
                    $aOptions['include'] = $xFile->getPathname();
240
                }
241
242
                $callableClassPlugin->register(Jaxon::CALLABLE_CLASS, $sClassName, $aOptions);
243
            }
244
        }
245
246
        return true;
247
    }
248
249
    /**
250
     * Find a callable object by class name
251
     *
252
     * @param string        $sClassName            The class name of the callable object
253
     *
254
     * @return object
255
     */
256
    public function getCallableObject($sClassName)
257
    {
258
        // Replace all separators ('.' and '_') with antislashes, and remove the antislashes
259
        // at the beginning and the end of the class name.
260
        $sClassName = trim(str_replace(['.', '_'], ['\\', '\\'], (string)$sClassName), '\\');
261
262
        // Make sure the registered class exists
263
        if(!class_exists('\\' . $sClassName))
264
        {
265
            return null;
266
        }
267
268
        if(key_exists($sClassName, $this->aCallableObjects))
269
        {
270
            return $this->aCallableObjects[$sClassName];
271
        }
272
273
        // Find the corresponding namespace
274
        $sNamespace = null;
275
        foreach(array_keys($this->aNamespaces) as $_sNamespace)
276
        {
277
            if(substr($sClassName, 0, strlen($_sNamespace)) == $_sNamespace)
278
            {
279
                $sNamespace = $_sNamespace;
280
                break;
281
            }
282
        }
283
        if($sNamespace == null)
284
        {
285
            return null; // Class not registered
286
        }
287
288
        // Create the callable object
289
        $xCallableObject = new \Jaxon\Request\Support\CallableObject($sClassName);
290
        $aOptions = $this->aNamespaces[$sNamespace];
291
        foreach($aOptions as $sMethod => $aValue)
292
        {
293
            foreach($aValue as $sName => $sValue)
294
            {
295
                $xCallableObject->configure($sMethod, $sName, $sValue);
296
            }
297
        }
298
299
        $this->aCallableObjects[$sClassName] = $xCallableObject;
300
        // Register the request factory for this callable object
301
        jaxon()->di()->set($sClassName . '\Factory\Rq', function ($di) use ($sClassName) {
0 ignored issues
show
Unused Code introduced by
The parameter $di is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
302
            $xCallableObject = $this->aCallableObjects[$sClassName];
303
            return new \Jaxon\Factory\Request\Portable($xCallableObject);
304
        });
305
        // Register the paginator factory for this callable object
306
        jaxon()->di()->set($sClassName . '\Factory\Pg', function ($di) use ($sClassName) {
0 ignored issues
show
Unused Code introduced by
The parameter $di is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
307
            $xCallableObject = $this->aCallableObjects[$sClassName];
308
            return new \Jaxon\Factory\Request\Paginator($xCallableObject);
309
        });
310
311
        return $xCallableObject;
312
    }
313
314
    /**
315
     * Find a user registered callable object by class name
316
     *
317
     * @param string        $sClassName            The class name of the callable object
318
     *
319
     * @return object
320
     */
321
    public function getRegisteredObject($sClassName)
322
    {
323
        // Get the corresponding callable object
324
        $xCallableObject = $this->getCallableObject($sClassName);
325
        return ($xCallableObject) ? $xCallableObject->getRegisteredObject() : null;
326
    }
327
328
    /**
329
     * Create callable objects for all registered namespaces
330
     *
331
     * @return void
332
     */
333
    private function createCallableObjects()
334
    {
335
        $sDS = DIRECTORY_SEPARATOR;
336
337
        foreach($this->aNamespaces as $sNamespace => $aOptions)
338
        {
339
            if(key_exists($sNamespace, $this->aClassNames))
340
            {
341
                continue;
342
            }
343
344
            $this->aClassNames[$sNamespace] = [];
345
346
            // Iterate on dir content
347
            $sDirectory = $aOptions['directory'];
348
            $itDir = new RecursiveDirectoryIterator($sDirectory);
349
            $itFile = new RecursiveIteratorIterator($itDir);
350
            foreach($itFile as $xFile)
351
            {
352
                // skip everything except PHP files
353
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
354
                {
355
                    continue;
356
                }
357
358
                // Find the class path (the same as the class namespace)
359
                $sClassPath = $sNamespace;
360
                $sRelativePath = substr($xFile->getPath(), strlen($sDirectory));
0 ignored issues
show
Unused Code introduced by
$sRelativePath is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
361
                $sRelativePath = trim(str_replace($sDS, '\\', $sClassPath), '\\');
362
                if(($sRelativePath))
363
                {
364
                    $sClassPath .= '\\' . $sRelativePath;
365
                }
366
                if(!key_exists($sClassPath, $this->aClassNames))
367
                {
368
                    $this->aClassNames[$sClassPath] = [];
369
                }
370
371
                $sClassName = $xFile->getBasename('.php');
372
                $this->aClassNames[$sClassPath][] = $sClassName;
373
                $this->getCallableObject($sNamespace . '\\' . $sClass);
0 ignored issues
show
Bug introduced by
The variable $sClass does not exist. Did you mean $sClassPath?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
374
            }
375
        }
376
    }
377
378
    /**
379
     * Generate a hash for the registered callable objects
380
     *
381
     * @return string
382
     */
383
    public function generateHash()
384
    {
385
        if(count($this->aNamespaces) == 0)
386
        {
387
            return '';
388
        }
389
390
        $this->createCallableObjects();
391
392
        $sHash = '';
393
        foreach($this->aNamespaces as $sNamespace => $aOptions)
394
        {
395
            $sHash .= $sNamespace . $aOptions['directory'] . $aOptions['separator'];
396
        }
397
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
398
        {
399
            $sHash .= $sClassName . implode('|', $xCallableObject->getMethods());
400
        }
401
402
        return md5($sHash);
403
    }
404
405
    /**
406
     * Generate client side javascript code for the registered callable objects
407
     *
408
     * @return string
409
     */
410
    public function getScript()
411
    {
412
        $this->createCallableObjects();
413
414
        // Generate code for javascript objects declaration
415
        $sJaxonPrefix = $this->getOption('core.prefix.class');
416
        $aJsClasses = [];
417
        $sCode = '';
418
        foreach(array_keys($this->aClassNames) as $sNamespace)
419
        {
420
            // if(key_exists('separator', $aOptions) && $aOptions['separator'] != '.')
421
            // {
422
            //     continue;
423
            // }
424
            $offset = 0;
425
            $sJsClasses = str_replace('\\', '.', $sNamespace);
426
            $sJsClasses .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
427 View Code Duplication
            while(($dotPosition = strpos($sJsClasses, '.', $offset)) !== false)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
428
            {
429
                $sJsClass = substr($sJsClasses, 0, $dotPosition);
430
                // Generate code for this object
431
                if(!key_exists($sJsClass, $aJsClasses))
432
                {
433
                    $sCode .= "$sJaxonPrefix$sJsClass = {};\n";
434
                    $aJsClasses[$sJsClass] = $sJsClass;
435
                }
436
                $offset = $dotPosition + 1;
437
            }
438
        }
439
        foreach($this->aCallableObjects as $xCallableObject)
440
        {
441
            $sCode .= $xCallableObject->getScript();
442
        }
443
444
        return $sCode;
445
    }
446
447
    /**
448
     * Check if this plugin can process the incoming Jaxon request
449
     *
450
     * @return boolean
451
     */
452
    public function canProcessRequest()
453
    {
454
        // Check the validity of the class name
455 View Code Duplication
        if(($this->sRequestedClass) && !$this->validateClass($this->sRequestedClass))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
456
        {
457
            $this->sRequestedClass = null;
458
            $this->sRequestedMethod = null;
459
        }
460
        // Check the validity of the method name
461 View Code Duplication
        if(($this->sRequestedMethod) && !$this->validateMethod($this->sRequestedMethod))
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sRequestedMethod of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
462
        {
463
            $this->sRequestedClass = null;
464
            $this->sRequestedMethod = null;
465
        }
466
        return ($this->sRequestedClass != null && $this->sRequestedMethod != null);
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $this->sRequestedClass of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
Bug introduced by
It seems like you are loosely comparing $this->sRequestedMethod of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
467
    }
468
469
    /**
470
     * Process the incoming Jaxon request
471
     *
472
     * @return boolean
473
     */
474 View Code Duplication
    public function processRequest()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
475
    {
476
        if(!$this->canProcessRequest())
477
        {
478
            return false;
479
        }
480
481
        $aArgs = $this->getRequestManager()->process();
482
483
        // Find the requested method
484
        $xCallableObject = $this->getCallableObject($this->sRequestedClass);
485
        if(!$xCallableObject || !$xCallableObject->hasMethod($this->sRequestedMethod))
486
        {
487
            // Unable to find the requested object or method
488
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid',
489
                ['class' => $this->sRequestedClass, 'method' => $this->sRequestedMethod]));
490
        }
491
492
        // Call the requested method
493
        $xCallableObject->call($this->sRequestedMethod, $aArgs);
494
        return true;
495
    }
496
}
497