Completed
Push — master ( 39246a...48ecd1 )
by Thierry
02:57
created

CallableDir::useComposerAutoloader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
rs 10
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 $sClass => $aClassOptions)
292
        {
293
            if($sClass == '*' || trim(str_replace(['.', '_'], ['\\', '\\'], $sClass)) == $sClassName)
294
            {
295
                foreach($aClassOptions as $sMethod => $aValue)
296
                {
297
                    foreach($aValue as $sName => $sValue)
298
                    {
299
                        $xCallableObject->configure($sMethod, $sName, $sValue);
300
                    }
301
                }
302
            }
303
        }
304
305
        $this->aCallableObjects[$sClassName] = $xCallableObject;
306
        // jaxon()->di()->set($sClassName, $xCallableObject);
307
        // Register the request factory for this callable object
308
        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...
309
            $xCallableObject = $this->aCallableObjects[$sClassName];
310
            return new \Jaxon\Factory\Request\Portable($xCallableObject);
311
        });
312
        // Register the paginator factory for this callable object
313
        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...
314
            $xCallableObject = $this->aCallableObjects[$sClassName];
315
            return new \Jaxon\Factory\Request\Paginator($xCallableObject);
316
        });
317
318
        return $xCallableObject;
319
    }
320
321
    /**
322
     * Find a user registered callable object by class name
323
     *
324
     * @param string        $sClassName            The class name of the callable object
325
     *
326
     * @return object
327
     */
328
    public function getRegisteredObject($sClassName)
329
    {
330
        // Get the corresponding callable object
331
        $xCallableObject = $this->getCallableObject($sClassName);
332
        return ($xCallableObject) ? $xCallableObject->getRegisteredObject() : null;
333
    }
334
335
    /**
336
     * Create callable objects for all registered namespaces
337
     *
338
     * @return void
339
     */
340
    private function createCallableObjects()
341
    {
342
        $sDS = DIRECTORY_SEPARATOR;
343
344
        foreach($this->aNamespaces as $sNamespace => $aOptions)
345
        {
346
            if(key_exists($sNamespace, $this->aClassNames))
347
            {
348
                continue;
349
            }
350
351
            $this->aClassNames[$sNamespace] = [];
352
353
            // Iterate on dir content
354
            $sDirectory = $aOptions['directory'];
355
            $itDir = new RecursiveDirectoryIterator($sDirectory);
356
            $itFile = new RecursiveIteratorIterator($itDir);
357
            foreach($itFile as $xFile)
358
            {
359
                // skip everything except PHP files
360
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
361
                {
362
                    continue;
363
                }
364
365
                // Find the class path (the same as the class namespace)
366
                $sClassPath = $sNamespace;
367
                $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...
368
                $sRelativePath = trim(str_replace($sDS, '\\', $sClassPath), '\\');
369
                if(($sRelativePath))
370
                {
371
                    $sClassPath .= '\\' . $sRelativePath;
372
                }
373
                if(!key_exists($sClassPath, $this->aClassNames))
374
                {
375
                    $this->aClassNames[$sClassPath] = [];
376
                }
377
378
                $sClassName = $xFile->getBasename('.php');
379
                $this->aClassNames[$sClassPath][] = $sClassName;
380
                $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...
381
            }
382
        }
383
    }
384
385
    /**
386
     * Generate a hash for the registered callable objects
387
     *
388
     * @return string
389
     */
390
    public function generateHash()
391
    {
392
        if(count($this->aNamespaces) == 0)
393
        {
394
            return '';
395
        }
396
397
        $this->createCallableObjects();
398
399
        $sHash = '';
400
        foreach($this->aNamespaces as $sNamespace => $aOptions)
401
        {
402
            $sHash .= $sNamespace . $aOptions['directory'] . $aOptions['separator'];
403
        }
404
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
405
        {
406
            $sHash .= $sClassName . implode('|', $xCallableObject->getMethods());
407
        }
408
409
        return md5($sHash);
410
    }
411
412
    /**
413
     * Generate client side javascript code for the registered callable objects
414
     *
415
     * @return string
416
     */
417
    public function getScript()
418
    {
419
        $this->createCallableObjects();
420
421
        // Generate code for javascript objects declaration
422
        $sJaxonPrefix = $this->getOption('core.prefix.class');
423
        $aJsClasses = [];
424
        $sCode = '';
425
        foreach(array_keys($this->aClassNames) as $sNamespace)
426
        {
427
            // if(key_exists('separator', $aOptions) && $aOptions['separator'] != '.')
428
            // {
429
            //     continue;
430
            // }
431
            $offset = 0;
432
            $sJsClasses = str_replace('\\', '.', $sNamespace);
433
            $sJsClasses .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
434 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...
435
            {
436
                $sJsClass = substr($sJsClasses, 0, $dotPosition);
437
                // Generate code for this object
438
                if(!key_exists($sJsClass, $aJsClasses))
439
                {
440
                    $sCode .= "$sJaxonPrefix$sJsClass = {};\n";
441
                    $aJsClasses[$sJsClass] = $sJsClass;
442
                }
443
                $offset = $dotPosition + 1;
444
            }
445
        }
446
        foreach($this->aCallableObjects as $xCallableObject)
447
        {
448
            $sCode .= $xCallableObject->getScript();
449
        }
450
451
        return $sCode;
452
    }
453
454
    /**
455
     * Check if this plugin can process the incoming Jaxon request
456
     *
457
     * @return boolean
458
     */
459
    public function canProcessRequest()
460
    {
461
        // Check the validity of the class name
462 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...
463
        {
464
            $this->sRequestedClass = null;
465
            $this->sRequestedMethod = null;
466
        }
467
        // Check the validity of the method name
468 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...
469
        {
470
            $this->sRequestedClass = null;
471
            $this->sRequestedMethod = null;
472
        }
473
        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...
474
    }
475
476
    /**
477
     * Process the incoming Jaxon request
478
     *
479
     * @return boolean
480
     */
481 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...
482
    {
483
        if(!$this->canProcessRequest())
484
        {
485
            return false;
486
        }
487
488
        $aArgs = $this->getRequestManager()->process();
489
490
        // Find the requested method
491
        $xCallableObject = $this->getCallableObject($this->sRequestedClass);
492
        if(!$xCallableObject || !$xCallableObject->hasMethod($this->sRequestedMethod))
493
        {
494
            // Unable to find the requested object or method
495
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid',
496
                ['class' => $this->sRequestedClass, 'method' => $this->sRequestedMethod]));
497
        }
498
499
        // Call the requested method
500
        $xCallableObject->call($this->sRequestedMethod, $aArgs);
501
        return true;
502
    }
503
}
504