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])) |
|
|
|
|
100
|
|
|
{ |
101
|
|
|
$aOptions = array_merge($aOptions, $aDirectoryOptions[$sNamespace]['*']); |
|
|
|
|
102
|
|
|
} |
103
|
|
View Code Duplication |
if(key_exists($sClassName, $aDirectoryOptions[$sNamespace])) |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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 = ''; |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|
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.