Completed
Push — master ( 84dbb5...744962 )
by Thierry
01:43
created

CallableRepository::getOptionsFromClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 8
rs 10
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
class CallableRepository
20
{
21
    use \Jaxon\Utils\Traits\Config;
22
    use \Jaxon\Utils\Traits\Template;
23
24
    /**
25
     * The registered namespaces
26
     *
27
     * These are the namespaces specified when registering directories.
28
     *
29
     * @var array
30
     */
31
    protected $aNamespaceOptions = [];
32
33
    /**
34
     * The registered classes
35
     *
36
     * These are registered classes, and classes in directories registered without a namespace.
37
     *
38
     * @var array
39
     */
40
    protected $aClassOptions = [];
41
42
    /**
43
     * The namespaces
44
     *
45
     * These are all the namespaces found in registered directories
46
     *
47
     * @var array
48
     */
49
    protected $aNamespaces = [];
50
51
    /**
52
     * The created callable objects
53
     *
54
     * @var array
55
     */
56
    protected $aCallableObjects = [];
57
58
    /**
59
     * The options to be applied to callable objects
60
     *
61
     * @var array
62
     */
63
    protected $aCallableOptions = [];
64
65
    /**
66
     *
67
     * @param string        $sClassName     The name of the class being registered
68
     * @param array|string  $aOptions       The associated options
69
     *
70
     * @return void
71
     */
72
    public function addClass($sClassName, $aOptions)
73
    {
74
        // Todo: if there's a namespace, register with '_' as separator
75
        $sClassName = trim($sClassName, '\\');
76
        $this->aClassOptions[$sClassName] = $aOptions;
77
    }
78
79
    /**
80
     * Get a given class options from specified directory options
81
     *
82
     * @param string        $sClassName         The name of the class
83
     * @param array         $aDirectoryOptions  The directory options
84
     * @param array         $aDefaultOptions    The default options
85
     *
86
     * @return array
87
     */
88
    private function getClassOptions($sClassName, array $aDirectoryOptions, array $aDefaultOptions = [])
89
    {
90
        $aOptions = $aDefaultOptions;
91
        if(key_exists('separator', $aDirectoryOptions))
92
        {
93
            $aOptions['separator'] = $aDirectoryOptions['separator'];
94
        }
95
        if(key_exists('protected', $aDirectoryOptions))
96
        {
97
            $aOptions['protected'] = $aDirectoryOptions['protected'];
98
        }
99 View Code Duplication
        if(key_exists('*', $aDirectoryOptions[$sNamespace]))
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...
100
        {
101
            $aOptions = array_merge($aOptions, $aDirectoryOptions[$sNamespace]['*']);
0 ignored issues
show
Bug introduced by
The variable $sNamespace does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
102
        }
103 View Code Duplication
        if(key_exists($sClassName, $aDirectoryOptions[$sNamespace]))
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...
104
        {
105
            $aOptions = array_merge($aOptions, $aDirectoryOptions[$sNamespace][$sClassName]);
106
        }
107
108
        return $aOptions;
109
    }
110
111
    /**
112
     *
113
     * @param string        $sDirectory     The directory being registered
114
     * @param array         $aOptions       The associated options
115
     *
116
     * @return void
117
     */
118
    public function addDirectory($sDirectory, $aOptions)
119
    {
120
        $itDir = new RecursiveDirectoryIterator($sDirectory);
121
        $itFile = new RecursiveIteratorIterator($itDir);
122
        // Iterate on dir content
123
        foreach($itFile as $xFile)
124
        {
125
            // skip everything except PHP files
126
            if(!$xFile->isFile() || $xFile->getExtension() != 'php')
127
            {
128
                continue;
129
            }
130
131
            $aClassOptions = [];
132
            // No more classmap autoloading. The file will be included when needed.
133
            if(($aOptions['autoload']))
134
            {
135
                $aClassOptions['include'] = $xFile->getPathname();
136
            }
137
138
            $sClassName = $xFile->getBasename('.php');
139
            $aClassOptions = $this->getClassOptions($sClassName, $aOptions, $aClassOptions);
140
            $this->addClass($sClassName, $aClassOptions);
141
        }
142
    }
143
144
    /**
145
     *
146
     * @param string        $sNamespace     The namespace of the directory being registered
147
     * @param array         $aOptions       The associated options
148
     *
149
     * @return void
150
     */
151
    public function addNamespace($sNamespace, $aOptions)
152
    {
153
        $this->aNamespaceOptions[$sNamespace] = $aOptions;
154
    }
155
156
    /**
157
     * Find a class name is register with Jaxon::CALLABLE_CLASS type
158
     *
159
     * @param string        $sClassName            The class name of the callable object
160
     *
161
     * @return array|null
162
     */
163
    private function getOptionsFromClass($sClassName)
164
    {
165
        if(!key_exists($sClassName, $this->aClassOptions))
166
        {
167
            return null; // Class not registered
168
        }
169
        return $this->aClassOptions[$sClassName];
170
    }
171
172
    /**
173
     * Find a class name is register with Jaxon::CALLABLE_DIR type
174
     *
175
     * @param string        $sClassName            The class name of the callable object
176
     *
177
     * @return array|null
178
     */
179
    private function getOptionsFromNamespace($sClassName)
180
    {
181
        // Find the corresponding namespace
182
        $sNamespace = null;
183
        foreach(array_keys($this->aNamespaceOptions) as $_sNamespace)
184
        {
185
            if(substr($sClassName, 0, strlen($_sNamespace) + 1) == $_sNamespace . '\\')
186
            {
187
                $sNamespace = $_sNamespace;
188
                break;
189
            }
190
        }
191
        if($sNamespace == null)
192
        {
193
            return null; // Class not registered
194
        }
195
196
        // Get the class options
197
        return $this->getClassOptions($sClassName, $this->aNamespaceOptions[$sNamespace]);
198
    }
199
200
    /**
201
     * Find a callable object by class name
202
     *
203
     * @param string        $sClassName            The class name of the callable object
204
     *
205
     * @return object
206
     */
207
    public function getCallableObject($sClassName)
208
    {
209
        // Replace all separators ('.' and '_') with antislashes, and remove the antislashes
210
        // at the beginning and the end of the class name.
211
        $sClassName = trim(str_replace(['.', '_'], ['\\', '\\'], (string)$sClassName), '\\');
212
213
        if(key_exists($sClassName, $this->aCallableObjects))
214
        {
215
            return $this->aCallableObjects[$sClassName];
216
        }
217
218
        $aOptions = $this->getOptionsFromClass($sClassName);
219
        if($aOptions === null)
220
        {
221
            $aOptions = $this->getOptionsFromNamespace($sClassName);
222
        }
223
        if($aOptions === null)
224
        {
225
            return null;
226
        }
227
228
        // Make sure the registered class exists
229
        if(key_exists('include', $aOptions))
230
        {
231
            require_once($aOptions['include']);
232
        }
233
        if(!class_exists('\\' . $sClassName))
234
        {
235
            return null;
236
        }
237
238
        // Create the callable object
239
        $xCallableObject = new \Jaxon\Request\Support\CallableObject($sClassName);
240
        $this->aCallableOptions[$sClassName] = [];
241
        foreach($aOptions as $sName => $xValue)
242
        {
243
            if($sName == 'separator' || $sName == 'protected')
244
            {
245
                $xCallableObject->configure($sName, $xValue);
246
            }
247
            elseif(is_array($xValue) && $sName != 'include')
248
            {
249
                // These options are to be included in javascript code.
250
                $this->aCallableOptions[$sClassName][$sName] = $xValue;
251
            }
252
        }
253
        $this->aCallableObjects[$sClassName] = $xCallableObject;
254
255
        // Register the request factory for this callable object
256
        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...
257
            $xCallableObject = $this->aCallableObjects[$sClassName];
258
            return new \Jaxon\Factory\Request\Portable($xCallableObject);
259
        });
260
        // Register the paginator factory for this callable object
261
        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...
262
            $xCallableObject = $this->aCallableObjects[$sClassName];
263
            return new \Jaxon\Factory\Request\Paginator($xCallableObject);
264
        });
265
266
        return $xCallableObject;
267
    }
268
269
    /**
270
     * Create callable objects for all registered namespaces
271
     *
272
     * @return void
273
     */
274
    private function createCallableObjects()
275
    {
276
        // Create callable objects for registered classes
277
        foreach(array_keys($this->aClassOptions) as $sClassName)
278
        {
279
            $this->getCallableObject($sClassName);
280
        }
281
282
        // Create callable objects for registered namespaces
283
        $sDS = DIRECTORY_SEPARATOR;
284
        foreach($this->aNamespaceOptions as $sNamespace => $aOptions)
285
        {
286
            if(key_exists($sNamespace, $this->aNamespaces))
287
            {
288
                continue;
289
            }
290
291
            $this->aNamespaces[$sNamespace] = $sNamespace;
292
293
            // Iterate on dir content
294
            $sDirectory = $aOptions['directory'];
295
            $itDir = new RecursiveDirectoryIterator($sDirectory);
296
            $itFile = new RecursiveIteratorIterator($itDir);
297
            foreach($itFile as $xFile)
298
            {
299
                // skip everything except PHP files
300
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
301
                {
302
                    continue;
303
                }
304
305
                // Find the class path (the same as the class namespace)
306
                $sClassPath = $sNamespace;
307
                $sRelativePath = substr($xFile->getPath(), strlen($sDirectory));
308
                $sRelativePath = trim(str_replace($sDS, '\\', $sRelativePath), '\\');
309
                if($sRelativePath != '')
310
                {
311
                    $sClassPath .= '\\' . $sRelativePath;
312
                }
313
314
                $this->aNamespaces[$sClassPath] = ['separator' => $aOptions['separator']];
315
                $sClassName = $xFile->getBasename('.php');
316
                $this->getCallableObject($sClassPath . '\\' . $sClassName);
317
            }
318
        }
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
    protected 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
     * Generate a hash for the registered callable objects
337
     *
338
     * @return string
339
     */
340
    public function generateHash()
341
    {
342
        return $this->createCallableObjects();
343
344
        $sHash = '';
0 ignored issues
show
Unused Code introduced by
$sHash = ''; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
345
        foreach($this->aNamespaces as $sNamespace => $aOptions)
346
        {
347
            $sHash .= $sNamespace . $aOptions['separator'];
348
        }
349
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
350
        {
351
            $sHash .= $sClassName . implode('|', $xCallableObject->getMethods());
352
        }
353
354
        return md5($sHash);
355
    }
356
357
    /**
358
     * Generate client side javascript code for the registered callable objects
359
     *
360
     * @return string
361
     */
362
    public function getScript()
363
    {
364
        $this->createCallableObjects();
365
366
        $sPrefix = $this->getOption('core.prefix.class');
367
368
        $aJsClasses = [];
369
        $sCode = '';
370
        foreach(array_keys($this->sNamespaces) as $sNamespace)
0 ignored issues
show
Bug introduced by
The property sNamespaces does not seem to exist. Did you mean aNamespaces?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
371
        {
372
            $offset = 0;
373
            $sJsNamespace = str_replace('\\', '.', $sNamespace);
374
            $sJsNamespace .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
375
            while(($dotPosition = strpos($sJsNamespace, '.', $offset)) !== false)
376
            {
377
                $sJsClass = substr($sJsNamespace, 0, $dotPosition);
378
                // Generate code for this object
379
                if(!key_exists($sJsClass, $aJsClasses))
380
                {
381
                    $sCode .= "$sPrefix$sJsClass = {};\n";
382
                    $aJsClasses[$sJsClass] = $sJsClass;
383
                }
384
                $offset = $dotPosition + 1;
385
            }
386
        }
387
388
        foreach($this->aCallableObjects as $sClassName => $xCallableObject)
389
        {
390
            $aConfig = $this->aCallableOptions[$sClassName];
391
            $aCommonConfig = key_exists('*', $aConfig) ? $aConfig['*'] : [];
392
393
            $aMethods = [];
394
            foreach($xCallableObject->getMethods() as $sMethodName)
395
            {
396
                // Specific options for this method
397
                $aMethodConfig = key_exists($sMethodName, $aConfig) ?
398
                    array_merge($aCommonConfig, $aConfig[$sMethodName]) : $aCommonConfig;
399
                $aMethods[] = [
400
                    'name' => $sMethodName,
401
                    'config' => $aMethodConfig,
402
                ];
403
            }
404
405
            $sCode .= $this->render('jaxon::support/object.js', [
406
                'sPrefix' => $sPrefix,
407
                'sClass' => $xCallableObject->getJsName(),
408
                'aMethods' => $aMethods,
409
            ]);
410
        }
411
412
        return $sCode;
413
    }
414
}
415