Completed
Push — master ( 5d7b0f...8d92ea )
by Thierry
03:20
created

CallableRepository::getCallableObject()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 1
dl 0
loc 23
rs 9.552
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * CallableRepository.php - Jaxon callable object repository
5
 *
6
 * This class stores all the callable object already created.
7
 *
8
 * @package jaxon-core
9
 * @author Thierry Feuzeu <[email protected]>
10
 * @copyright 2019 Thierry Feuzeu <[email protected]>
11
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
12
 * @link https://github.com/jaxon-php/jaxon-core
13
 */
14
15
namespace Jaxon\Request\Support;
16
17
use Jaxon\Request\Request;
18
19
use RecursiveDirectoryIterator;
20
use RecursiveIteratorIterator;
21
// use RegexIterator;
22
// use RecursiveRegexIterator;
23
24
class CallableRepository
25
{
26
    use \Jaxon\Utils\Traits\Config;
27
    use \Jaxon\Utils\Traits\Template;
28
29
    /**
30
     * The registered namespaces
31
     *
32
     * These are the namespaces specified when registering directories.
33
     *
34
     * @var array
35
     */
36
    protected $aNamespaceOptions = [];
37
38
    /**
39
     * The registered classes
40
     *
41
     * These are registered classes, and classes in directories registered without a namespace.
42
     *
43
     * @var array
44
     */
45
    protected $aClassOptions = [];
46
47
    /**
48
     * The namespaces
49
     *
50
     * These are all the namespaces found in registered directories
51
     *
52
     * @var array
53
     */
54
    protected $aNamespaces = [];
55
56
    /**
57
     * The created callable objects
58
     *
59
     * @var array
60
     */
61
    protected $aCallableObjects = [];
62
63
    /**
64
     * The options to be applied to callable objects
65
     *
66
     * @var array
67
     */
68
    protected $aCallableOptions = [];
69
70
    /**
71
     *
72
     * @param string        $sClassName     The name of the class being registered
73
     * @param array|string  $aOptions       The associated options
74
     *
75
     * @return void
76
     */
77
    public function addClass($sClassName, $aOptions)
78
    {
79
        // Todo: if there's a namespace, register with '_' as separator
80
        $sClassName = trim($sClassName, '\\');
81
        $this->aClassOptions[$sClassName] = $aOptions;
82
    }
83
84
    /**
85
     * Get a given class options from specified directory options
86
     *
87
     * @param string        $sClassName         The name of the class
88
     * @param array         $aDirectoryOptions  The directory options
89
     * @param array         $aDefaultOptions    The default options
90
     *
91
     * @return array
92
     */
93
    private function getClassOptions($sClassName, array $aDirectoryOptions, array $aDefaultOptions = [])
94
    {
95
        $aOptions = $aDefaultOptions;
96
        if(key_exists('separator', $aDirectoryOptions))
97
        {
98
            $aOptions['separator'] = $aDirectoryOptions['separator'];
99
        }
100
        if(key_exists('protected', $aDirectoryOptions))
101
        {
102
            $aOptions['protected'] = $aDirectoryOptions['protected'];
103
        }
104
        if(key_exists('*', $aDirectoryOptions))
105
        {
106
            $aOptions = array_merge($aOptions, $aDirectoryOptions['*']);
107
        }
108
        if(key_exists($sClassName, $aDirectoryOptions))
109
        {
110
            $aOptions = array_merge($aOptions, $aDirectoryOptions[$sClassName]);
111
        }
112
113
        return $aOptions;
114
    }
115
116
    /**
117
     *
118
     * @param string        $sDirectory     The directory being registered
119
     * @param array         $aOptions       The associated options
120
     *
121
     * @return void
122
     */
123
    public function addDirectory($sDirectory, $aOptions)
124
    {
125
        $itDir = new RecursiveDirectoryIterator($sDirectory);
126
        $itFile = new RecursiveIteratorIterator($itDir);
127
        // Iterate on dir content
128
        foreach($itFile as $xFile)
129
        {
130
            // skip everything except PHP files
131
            if(!$xFile->isFile() || $xFile->getExtension() != 'php')
132
            {
133
                continue;
134
            }
135
136
            $aClassOptions = [];
137
            // No more classmap autoloading. The file will be included when needed.
138
            if(($aOptions['autoload']))
139
            {
140
                $aClassOptions['include'] = $xFile->getPathname();
141
            }
142
143
            $sClassName = $xFile->getBasename('.php');
144
            $aClassOptions = $this->getClassOptions($sClassName, $aOptions, $aClassOptions);
145
            $this->addClass($sClassName, $aClassOptions);
146
        }
147
    }
148
149
    /**
150
     *
151
     * @param string        $sNamespace     The namespace of the directory being registered
152
     * @param array         $aOptions       The associated options
153
     *
154
     * @return void
155
     */
156
    public function addNamespace($sNamespace, array $aOptions)
157
    {
158
        // Separator default value
159
        if(!key_exists('separator', $aOptions))
160
        {
161
            $aOptions['separator'] = '.';
162
        }
163
        $this->aNamespaceOptions[$sNamespace] = $aOptions;
164
    }
165
166
    /**
167
     * Find a class name is register with Jaxon::CALLABLE_CLASS type
168
     *
169
     * @param string        $sClassName            The class name of the callable object
170
     *
171
     * @return array|null
172
     */
173
    private function getOptionsFromClass($sClassName)
174
    {
175
        if(!key_exists($sClassName, $this->aClassOptions))
176
        {
177
            return null; // Class not registered
178
        }
179
        return $this->aClassOptions[$sClassName];
180
    }
181
182
    /**
183
     * Find a class name is register with Jaxon::CALLABLE_DIR type
184
     *
185
     * @param string        $sClassName            The class name of the callable object
186
     * @param string|null   $sNamespace            The namespace
187
     *
188
     * @return array|null
189
     */
190
    private function getOptionsFromNamespace($sClassName, $sNamespace = null)
191
    {
192
        // Find the corresponding namespace
193
        if($sNamespace == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $sNamespace of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
194
        {
195
            foreach(array_keys($this->aNamespaceOptions) as $_sNamespace)
196
            {
197
                if(substr($sClassName, 0, strlen($_sNamespace) + 1) == $_sNamespace . '\\')
198
                {
199
                    $sNamespace = $_sNamespace;
200
                    break;
201
                }
202
            }
203
        }
204
        if($sNamespace == null)
205
        {
206
            return null; // Class not registered
207
        }
208
209
        // Get the class options
210
        $aOptions = $this->aNamespaceOptions[$sNamespace];
211
        $aDefaultOptions = []; // ['namespace' => $aOptions['namespace']];
212
        if(key_exists('separator', $aOptions))
213
        {
214
            $aDefaultOptions['separator'] = $aOptions['separator'];
215
        }
216
        return $this->getClassOptions($sClassName, $aOptions, $aDefaultOptions);
217
    }
218
219
    /**
220
     * Find a callable object by class name
221
     *
222
     * @param string        $sClassName            The class name of the callable object
223
     * @param array         $aOptions              The callable object options
224
     *
225
     * @return object
226
     */
227
    protected function _getCallableObject($sClassName, array $aOptions)
228
    {
229
        // Make sure the registered class exists
230
        if(key_exists('include', $aOptions))
231
        {
232
            require_once($aOptions['include']);
233
        }
234
        if(!class_exists($sClassName))
235
        {
236
            return null;
237
        }
238
239
        // Create the callable object
240
        $xCallableObject = new \Jaxon\Request\Support\CallableObject($sClassName);
241
        $this->aCallableOptions[$sClassName] = [];
242
        foreach($aOptions as $sName => $xValue)
243
        {
244
            if($sName == 'separator' || $sName == 'protected')
245
            {
246
                $xCallableObject->configure($sName, $xValue);
247
            }
248
            elseif(is_array($xValue) && $sName != 'include')
249
            {
250
                // These options are to be included in javascript code.
251
                $this->aCallableOptions[$sClassName][$sName] = $xValue;
252
            }
253
        }
254
        $this->aCallableObjects[$sClassName] = $xCallableObject;
255
256
        // Register the request factory for this callable object
257
        jaxon()->di()->set($sClassName . '_Factory_Rq', function () use ($sClassName) {
258
            $xCallableObject = $this->aCallableObjects[$sClassName];
259
            return new \Jaxon\Factory\Request\Portable($xCallableObject);
260
        });
261
        // Register the paginator factory for this callable object
262
        jaxon()->di()->set($sClassName . '_Factory_Pg', function () use ($sClassName) {
263
            $xCallableObject = $this->aCallableObjects[$sClassName];
264
            return new \Jaxon\Factory\Request\Paginator($xCallableObject);
265
        });
266
267
        return $xCallableObject;
268
    }
269
270
    /**
271
     * Find a callable object by class name
272
     *
273
     * @param string        $sClassName            The class name of the callable object
274
     *
275
     * @return object
276
     */
277
    public function getCallableObject($sClassName)
278
    {
279
        // Replace all separators ('.' and '_') with antislashes, and remove the antislashes
280
        // at the beginning and the end of the class name.
281
        $sClassName = trim(str_replace(['.', '_'], ['\\', '\\'], (string)$sClassName), '\\');
282
283
        if(key_exists($sClassName, $this->aCallableObjects))
284
        {
285
            return $this->aCallableObjects[$sClassName];
286
        }
287
288
        $aOptions = $this->getOptionsFromClass($sClassName);
289
        if($aOptions === null)
290
        {
291
            $aOptions = $this->getOptionsFromNamespace($sClassName);
292
        }
293
        if($aOptions === null)
294
        {
295
            return null;
296
        }
297
298
        return $this->_getCallableObject($sClassName, $aOptions);
299
    }
300
301
    /**
302
     * Create callable objects for all registered namespaces
303
     *
304
     * @return void
305
     */
306
    private function createCallableObjects()
307
    {
308
        // Create callable objects for registered classes
309
        foreach($this->aClassOptions as $sClassName => $aClassOptions)
310
        {
311
            if(!key_exists($sClassName, $this->aCallableObjects))
312
            {
313
                $this->_getCallableObject($sClassName, $aClassOptions);
314
            }
315
        }
316
317
        // Create callable objects for registered namespaces
318
        $sDS = DIRECTORY_SEPARATOR;
319
        foreach($this->aNamespaceOptions as $sNamespace => $aOptions)
320
        {
321
            if(key_exists($sNamespace, $this->aNamespaces))
322
            {
323
                continue;
324
            }
325
326
            $this->aNamespaces[$sNamespace] = $sNamespace;
327
328
            // Iterate on dir content
329
            $sDirectory = $aOptions['directory'];
330
            $itDir = new RecursiveDirectoryIterator($sDirectory);
331
            $itFile = new RecursiveIteratorIterator($itDir);
332
            foreach($itFile as $xFile)
333
            {
334
                // skip everything except PHP files
335
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
336
                {
337
                    continue;
338
                }
339
340
                // Find the class path (the same as the class namespace)
341
                $sClassPath = $sNamespace;
342
                $sRelativePath = substr($xFile->getPath(), strlen($sDirectory));
343
                $sRelativePath = trim(str_replace($sDS, '\\', $sRelativePath), '\\');
344
                if($sRelativePath != '')
345
                {
346
                    $sClassPath .= '\\' . $sRelativePath;
347
                }
348
349
                $this->aNamespaces[$sClassPath] = ['separator' => $aOptions['separator']];
350
                $sClassName = $sClassPath . '\\' . $xFile->getBasename('.php');
351
352
                if(!key_exists($sClassName, $this->aCallableObjects))
353
                {
354
                    $aClassOptions = $this->getOptionsFromNamespace($sClassName, $sNamespace);
355
                    $this->_getCallableObject($sClassName, $aClassOptions);
0 ignored issues
show
Bug introduced by
It seems like $aClassOptions defined by $this->getOptionsFromNam...ClassName, $sNamespace) on line 354 can also be of type null; however, Jaxon\Request\Support\Ca...y::_getCallableObject() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
356
                }
357
            }
358
        }
359
    }
360
361
    /**
362
     * Find a user registered callable object by class name
363
     *
364
     * @param string        $sClassName            The class name of the callable object
365
     *
366
     * @return object
367
     */
368
    protected function getRegisteredObject($sClassName)
369
    {
370
        // Get the corresponding callable object
371
        $xCallableObject = $this->getCallableObject($sClassName);
372
        return ($xCallableObject) ? $xCallableObject->getRegisteredObject() : null;
373
    }
374
375
    /**
376
     * Generate a hash for the registered callable objects
377
     *
378
     * @return string
379
     */
380
    public function generateHash()
381
    {
382
        $this->createCallableObjects();
383
384
        $sHash = '';
385
        foreach($this->aNamespaces as $sNamespace => $aOptions)
386
        {
387
            $sHash .= $sNamespace . $aOptions['separator'];
388
        }
389
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
390
        {
391
            $sHash .= $sClassName . implode('|', $xCallableObject->getMethods());
392
        }
393
394
        return md5($sHash);
395
    }
396
397
    /**
398
     * Generate client side javascript code for the registered callable objects
399
     *
400
     * @return string
401
     */
402
    public function getScript()
403
    {
404
        $this->createCallableObjects();
405
406
        $sPrefix = $this->getOption('core.prefix.class');
407
408
        $aJsClasses = [];
409
        $sCode = '';
410
        foreach(array_keys($this->aNamespaces) as $sNamespace)
411
        {
412
            $offset = 0;
413
            $sJsNamespace = str_replace('\\', '.', $sNamespace);
414
            $sJsNamespace .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
415
            while(($dotPosition = strpos($sJsNamespace, '.', $offset)) !== false)
416
            {
417
                $sJsClass = substr($sJsNamespace, 0, $dotPosition);
418
                // Generate code for this object
419
                if(!key_exists($sJsClass, $aJsClasses))
420
                {
421
                    $sCode .= "$sPrefix$sJsClass = {};\n";
422
                    $aJsClasses[$sJsClass] = $sJsClass;
423
                }
424
                $offset = $dotPosition + 1;
425
            }
426
        }
427
428
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
429
        {
430
            $aConfig = $this->aCallableOptions[$sClassName];
431
            $aCommonConfig = key_exists('*', $aConfig) ? $aConfig['*'] : [];
432
433
            $aMethods = [];
434
            foreach($xCallableObject->getMethods() as $sMethodName)
435
            {
436
                // Specific options for this method
437
                $aMethodConfig = key_exists($sMethodName, $aConfig) ?
438
                    array_merge($aCommonConfig, $aConfig[$sMethodName]) : $aCommonConfig;
439
                $aMethods[] = [
440
                    'name' => $sMethodName,
441
                    'config' => $aMethodConfig,
442
                ];
443
            }
444
445
            $sCode .= $this->render('jaxon::support/object.js', [
446
                'sPrefix' => $sPrefix,
447
                'sClass' => $xCallableObject->getJsName(),
448
                'aMethods' => $aMethods,
449
            ]);
450
        }
451
452
        return $sCode;
453
    }
454
}
455