Completed
Push — master ( 8d92ea...75c398 )
by Thierry
02:10
created

CallableRepository::createCallableObjects()   C

Complexity

Conditions 11
Paths 30

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 30
nop 0
dl 0
loc 57
rs 6.7915
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
22
class CallableRepository
23
{
24
    use \Jaxon\Utils\Traits\Config;
25
    use \Jaxon\Utils\Traits\Template;
26
27
    /**
28
     * The registered namespaces
29
     *
30
     * These are the namespaces specified when registering directories.
31
     *
32
     * @var array
33
     */
34
    protected $aNamespaceOptions = [];
35
36
    /**
37
     * The registered classes
38
     *
39
     * These are registered classes, and classes in directories registered without a namespace.
40
     *
41
     * @var array
42
     */
43
    protected $aClassOptions = [];
44
45
    /**
46
     * The namespaces
47
     *
48
     * These are all the namespaces found in registered directories
49
     *
50
     * @var array
51
     */
52
    protected $aNamespaces = [];
53
54
    /**
55
     * The created callable objects
56
     *
57
     * @var array
58
     */
59
    protected $aCallableObjects = [];
60
61
    /**
62
     * The options to be applied to callable objects
63
     *
64
     * @var array
65
     */
66
    protected $aCallableOptions = [];
67
68
    /**
69
     *
70
     * @param string        $sClassName     The name of the class being registered
71
     * @param array|string  $aOptions       The associated options
72
     *
73
     * @return void
74
     */
75
    public function addClass($sClassName, $aOptions)
76
    {
77
        // Todo: if there's a namespace, register with '_' as separator
78
        $sClassName = trim($sClassName, '\\');
79
        $this->aClassOptions[$sClassName] = $aOptions;
80
    }
81
82
    /**
83
     * Get a given class options from specified directory options
84
     *
85
     * @param string        $sClassName         The name of the class
86
     * @param array         $aDirectoryOptions  The directory options
87
     * @param array         $aDefaultOptions    The default options
88
     *
89
     * @return array
90
     */
91
    private function getClassOptions($sClassName, array $aDirectoryOptions, array $aDefaultOptions = [])
92
    {
93
        $aOptions = $aDefaultOptions;
94
        if(key_exists('separator', $aDirectoryOptions))
95
        {
96
            $aOptions['separator'] = $aDirectoryOptions['separator'];
97
        }
98
        if(key_exists('protected', $aDirectoryOptions))
99
        {
100
            $aOptions['protected'] = $aDirectoryOptions['protected'];
101
        }
102
        if(key_exists('*', $aDirectoryOptions))
103
        {
104
            $aOptions = array_merge($aOptions, $aDirectoryOptions['*']);
105
        }
106
        if(key_exists($sClassName, $aDirectoryOptions))
107
        {
108
            $aOptions = array_merge($aOptions, $aDirectoryOptions[$sClassName]);
109
        }
110
111
        return $aOptions;
112
    }
113
114
    /**
115
     *
116
     * @param string        $sDirectory     The directory being registered
117
     * @param array         $aOptions       The associated options
118
     *
119
     * @return void
120
     */
121
    public function addDirectory($sDirectory, $aOptions)
122
    {
123
        $itDir = new RecursiveDirectoryIterator($sDirectory);
124
        $itFile = new RecursiveIteratorIterator($itDir);
125
        // Iterate on dir content
126
        foreach($itFile as $xFile)
127
        {
128
            // skip everything except PHP files
129
            if(!$xFile->isFile() || $xFile->getExtension() != 'php')
130
            {
131
                continue;
132
            }
133
134
            $aClassOptions = [];
135
            // No more classmap autoloading. The file will be included when needed.
136
            if(($aOptions['autoload']))
137
            {
138
                $aClassOptions['include'] = $xFile->getPathname();
139
            }
140
141
            $sClassName = $xFile->getBasename('.php');
142
            $aClassOptions = $this->getClassOptions($sClassName, $aOptions, $aClassOptions);
143
            $this->addClass($sClassName, $aClassOptions);
144
        }
145
    }
146
147
    /**
148
     *
149
     * @param string        $sNamespace     The namespace of the directory being registered
150
     * @param array         $aOptions       The associated options
151
     *
152
     * @return void
153
     */
154
    public function addNamespace($sNamespace, array $aOptions)
155
    {
156
        // Separator default value
157
        if(!key_exists('separator', $aOptions))
158
        {
159
            $aOptions['separator'] = '.';
160
        }
161
        $this->aNamespaceOptions[$sNamespace] = $aOptions;
162
    }
163
164
    /**
165
     * Find a class name is register with Jaxon::CALLABLE_CLASS type
166
     *
167
     * @param string        $sClassName            The class name of the callable object
168
     *
169
     * @return array|null
170
     */
171
    private function getOptionsFromClass($sClassName)
172
    {
173
        if(!key_exists($sClassName, $this->aClassOptions))
174
        {
175
            return null; // Class not registered
176
        }
177
        return $this->aClassOptions[$sClassName];
178
    }
179
180
    /**
181
     * Find a class name is register with Jaxon::CALLABLE_DIR type
182
     *
183
     * @param string        $sClassName            The class name of the callable object
184
     * @param string|null   $sNamespace            The namespace
185
     *
186
     * @return array|null
187
     */
188
    private function getOptionsFromNamespace($sClassName, $sNamespace = null)
189
    {
190
        // Find the corresponding namespace
191
        if($sNamespace === null)
192
        {
193
            foreach(array_keys($this->aNamespaceOptions) as $_sNamespace)
194
            {
195
                if(substr($sClassName, 0, strlen($_sNamespace) + 1) == $_sNamespace . '\\')
196
                {
197
                    $sNamespace = $_sNamespace;
198
                    break;
199
                }
200
            }
201
        }
202
        if($sNamespace === null)
203
        {
204
            return null; // Class not registered
205
        }
206
207
        // Get the class options
208
        $aOptions = $this->aNamespaceOptions[$sNamespace];
209
        $aDefaultOptions = []; // ['namespace' => $aOptions['namespace']];
210
        if(key_exists('separator', $aOptions))
211
        {
212
            $aDefaultOptions['separator'] = $aOptions['separator'];
213
        }
214
        return $this->getClassOptions($sClassName, $aOptions, $aDefaultOptions);
215
    }
216
217
    /**
218
     * Find a callable object by class name
219
     *
220
     * @param string        $sClassName            The class name of the callable object
221
     * @param array         $aOptions              The callable object options
222
     *
223
     * @return object
224
     */
225
    protected function _getCallableObject($sClassName, array $aOptions)
226
    {
227
        // Make sure the registered class exists
228
        if(key_exists('include', $aOptions))
229
        {
230
            require_once($aOptions['include']);
231
        }
232
        if(!class_exists($sClassName))
233
        {
234
            return null;
235
        }
236
237
        // Create the callable object
238
        $xCallableObject = new \Jaxon\Request\Support\CallableObject($sClassName);
239
        $this->aCallableOptions[$sClassName] = [];
240
        foreach($aOptions as $sName => $xValue)
241
        {
242
            if($sName == 'separator' || $sName == 'protected')
243
            {
244
                $xCallableObject->configure($sName, $xValue);
245
            }
246
            elseif(is_array($xValue) && $sName != 'include')
247
            {
248
                // These options are to be included in javascript code.
249
                $this->aCallableOptions[$sClassName][$sName] = $xValue;
250
            }
251
        }
252
        $this->aCallableObjects[$sClassName] = $xCallableObject;
253
254
        // Register the request factory for this callable object
255
        jaxon()->di()->set($sClassName . '_Factory_Rq', function () use ($sClassName) {
256
            $xCallableObject = $this->aCallableObjects[$sClassName];
257
            return new \Jaxon\Factory\Request\Portable($xCallableObject);
258
        });
259
        // Register the paginator factory for this callable object
260
        jaxon()->di()->set($sClassName . '_Factory_Pg', function () use ($sClassName) {
261
            $xCallableObject = $this->aCallableObjects[$sClassName];
262
            return new \Jaxon\Factory\Request\Paginator($xCallableObject);
263
        });
264
265
        return $xCallableObject;
266
    }
267
268
    /**
269
     * Find a callable object by class name
270
     *
271
     * @param string        $sClassName            The class name of the callable object
272
     *
273
     * @return object
274
     */
275
    public function getCallableObject($sClassName)
276
    {
277
        // Replace all separators ('.' and '_') with antislashes, and remove the antislashes
278
        // at the beginning and the end of the class name.
279
        $sClassName = trim(str_replace(['.', '_'], ['\\', '\\'], (string)$sClassName), '\\');
280
281
        if(key_exists($sClassName, $this->aCallableObjects))
282
        {
283
            return $this->aCallableObjects[$sClassName];
284
        }
285
286
        $aOptions = $this->getOptionsFromClass($sClassName);
287
        if($aOptions === null)
288
        {
289
            $aOptions = $this->getOptionsFromNamespace($sClassName);
290
        }
291
        if($aOptions === null)
292
        {
293
            return null;
294
        }
295
296
        return $this->_getCallableObject($sClassName, $aOptions);
297
    }
298
299
    /**
300
     * Create callable objects for all registered namespaces
301
     *
302
     * @return void
303
     */
304
    private function createCallableObjects()
305
    {
306
        // Create callable objects for registered classes
307
        foreach($this->aClassOptions as $sClassName => $aClassOptions)
308
        {
309
            if(!key_exists($sClassName, $this->aCallableObjects))
310
            {
311
                $this->_getCallableObject($sClassName, $aClassOptions);
312
            }
313
        }
314
315
        // Create callable objects for registered namespaces
316
        $sDS = DIRECTORY_SEPARATOR;
317
        foreach($this->aNamespaceOptions as $sNamespace => $aOptions)
318
        {
319
            if(key_exists($sNamespace, $this->aNamespaces))
320
            {
321
                continue;
322
            }
323
324
            $this->aNamespaces[$sNamespace] = $sNamespace;
325
326
            // Iterate on dir content
327
            $sDirectory = $aOptions['directory'];
328
            $itDir = new RecursiveDirectoryIterator($sDirectory);
329
            $itFile = new RecursiveIteratorIterator($itDir);
330
            foreach($itFile as $xFile)
331
            {
332
                // skip everything except PHP files
333
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
334
                {
335
                    continue;
336
                }
337
338
                // Find the class path (the same as the class namespace)
339
                $sClassPath = $sNamespace;
340
                $sRelativePath = substr($xFile->getPath(), strlen($sDirectory));
341
                $sRelativePath = trim(str_replace($sDS, '\\', $sRelativePath), '\\');
342
                if($sRelativePath != '')
343
                {
344
                    $sClassPath .= '\\' . $sRelativePath;
345
                }
346
347
                $this->aNamespaces[$sClassPath] = ['separator' => $aOptions['separator']];
348
                $sClassName = $sClassPath . '\\' . $xFile->getBasename('.php');
349
350
                if(!key_exists($sClassName, $this->aCallableObjects))
351
                {
352
                    $aClassOptions = $this->getOptionsFromNamespace($sClassName, $sNamespace);
353
                    if($aClassOptions !== null)
354
                    {
355
                        $this->_getCallableObject($sClassName, $aClassOptions);
356
                    }
357
                }
358
            }
359
        }
360
    }
361
362
    /**
363
     * Find a user registered callable object by class name
364
     *
365
     * @param string        $sClassName            The class name of the callable object
366
     *
367
     * @return object
368
     */
369
    protected function getRegisteredObject($sClassName)
370
    {
371
        // Get the corresponding callable object
372
        $xCallableObject = $this->getCallableObject($sClassName);
373
        return ($xCallableObject) ? $xCallableObject->getRegisteredObject() : null;
374
    }
375
376
    /**
377
     * Generate a hash for the registered callable objects
378
     *
379
     * @return string
380
     */
381
    public function generateHash()
382
    {
383
        $this->createCallableObjects();
384
385
        $sHash = '';
386
        foreach($this->aNamespaces as $sNamespace => $aOptions)
387
        {
388
            $sHash .= $sNamespace . $aOptions['separator'];
389
        }
390
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
391
        {
392
            $sHash .= $sClassName . implode('|', $xCallableObject->getMethods());
393
        }
394
395
        return md5($sHash);
396
    }
397
398
    /**
399
     * Generate client side javascript code for the registered callable objects
400
     *
401
     * @return string
402
     */
403
    public function getScript()
404
    {
405
        $this->createCallableObjects();
406
407
        $sPrefix = $this->getOption('core.prefix.class');
408
409
        $aJsClasses = [];
410
        $sCode = '';
411
        foreach(array_keys($this->aNamespaces) as $sNamespace)
412
        {
413
            $offset = 0;
414
            $sJsNamespace = str_replace('\\', '.', $sNamespace);
415
            $sJsNamespace .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
416
            while(($dotPosition = strpos($sJsNamespace, '.', $offset)) !== false)
417
            {
418
                $sJsClass = substr($sJsNamespace, 0, $dotPosition);
419
                // Generate code for this object
420
                if(!key_exists($sJsClass, $aJsClasses))
421
                {
422
                    $sCode .= "$sPrefix$sJsClass = {};\n";
423
                    $aJsClasses[$sJsClass] = $sJsClass;
424
                }
425
                $offset = $dotPosition + 1;
426
            }
427
        }
428
429
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
430
        {
431
            $aConfig = $this->aCallableOptions[$sClassName];
432
            $aCommonConfig = key_exists('*', $aConfig) ? $aConfig['*'] : [];
433
434
            $aMethods = [];
435
            foreach($xCallableObject->getMethods() as $sMethodName)
436
            {
437
                // Specific options for this method
438
                $aMethodConfig = key_exists($sMethodName, $aConfig) ?
439
                    array_merge($aCommonConfig, $aConfig[$sMethodName]) : $aCommonConfig;
440
                $aMethods[] = [
441
                    'name' => $sMethodName,
442
                    'config' => $aMethodConfig,
443
                ];
444
            }
445
446
            $sCode .= $this->render('jaxon::support/object.js', [
447
                'sPrefix' => $sPrefix,
448
                'sClass' => $xCallableObject->getJsName(),
449
                'aMethods' => $aMethods,
450
            ]);
451
        }
452
453
        return $sCode;
454
    }
455
}
456