Completed
Push — development ( 8981a4...d3a488 )
by Stephen
18s
created

Elk_Autoloader::instance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.1481

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 9
ccs 4
cts 6
cp 0.6667
crap 2.1481
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Used to auto load class files given a class name
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * @version 1.1 Release Candidate 1
11
 */
12
13
/**
14
 * ElkArte autoloader
15
 *
16
 * What it does:
17
 *
18
 * - Automatically finds and includes the files for a given class name
19
 * - Follows controller naming conventions to find the right file to load
20
 */
21
class Elk_Autoloader
22
{
23
	/**
24
	 * Instance manager
25
	 * @var Elk_Autoloader
26
	 */
27
	private static $_instance;
28
29
	/**
30
	 * Stores whether the autoloader has been initialized
31
	 * @var boolean
32
	 */
33
	protected $_setup = false;
34
35
	/**
36
	 * Stores whether the autoloader verifies file existence or not
37
	 * @var boolean
38
	 */
39
	protected $_strict = false;
40
41
	/**
42
	 * Stores whether the autoloader verifies file existence or not for each
43
	 * namespace separately
44
	 * @var boolean
45
	 */
46
	protected $_strict_namespace = array();
47
48
	/**
49
	 * Path to directory containing ElkArte
50
	 * @var string
51
	 */
52
	protected $_dir = '.';
53
54
	/**
55
	 * Holds the name of the exploded class name
56
	 * @var array
57
	 */
58
	protected $_name;
59
60
	/**
61
	 * Holds the class name ahead of any _ if any
62
	 * @var string
63
	 */
64
	protected $_surname;
65
66
	/**
67
	 * Holds the class name after the first _ if any
68
	 * @var string
69
	 */
70
	protected $_givenname;
71
72
	/**
73
	 * The current namespace
74
	 * @var string|false
75
	 */
76
	protected $_current_namespace;
77
78
	/**
79
	 * Holds the name full file name of the file to load (require)
80
	 * @var string|boolean
81
	 */
82
	protected $_file_name = false;
83
84
	/**
85
	 * Holds the pairs namespace => paths
86
	 * @var array
87
	 */
88
	protected $_paths;
89
90
	/**
91
	 * Constructor, not used, instead use getInstance()
92
	 */
93
	protected function __construct()
94
	{
95
	}
96
97
	/**
98
	 * Setup the autoloader environment
99
	 *
100
	 * @param string|string[] $dir
101
	 */
102
	public function setupAutoloader($dir)
103
	{
104
		if (!is_array($dir))
105
		{
106
			$dir = array($dir);
107
		}
108
109
		foreach ($dir as $path)
110
		{
111
			$this->register($path, '\\' . strtr($path, array(BOARDDIR => 'ElkArte', '/' => '\\')));
112
		}
113
	}
114
115
	/**
116
	 * Registers new paths for the autoloader
117
	 *
118
	 * @param string $dir
119
	 * @param string|null $namespace
120
	 * @param bool $strict
121
	 */
122 22
	public function register($dir, $namespace = null, $strict = false)
123
	{
124 22
		if ($namespace === null)
125 22
		{
126
			$namespace = 0;
127
		}
128
129 22
		if (!isset($this->_paths[$namespace]))
130 22
		{
131 1
			$this->_paths[$namespace] = array();
132 1
		}
133
134 22
		$this->_paths[$namespace][] = $dir;
135 22
		$this->_paths[$namespace] = array_unique($this->_paths[$namespace]);
136 22
		$this->_strict_namespace[$namespace] = $strict;
137
138 22
		$this->_buildPaths((array) $dir);
139 22
	}
140
141
	/**
142
	 * Build the directory path names to search for files to autoload
143
	 *
144
	 * @param array $dir
145
	 */
146 22
	protected function _buildPaths($dir)
147
	{
148
		// Build the paths where we are going to look for files
149 22
		foreach ($dir as $include)
150
		{
151 22
			$this->_dir .= $include . PATH_SEPARATOR;
152 22
		}
153
154
		// Initialize
155 22
		$this->_setupAutoloader();
156 22
		$this->_setup = true;
157 22
	}
158
159
	/**
160
	 * Method that actually registers the autoloader.
161
	 */
162 22
	protected function _setupAutoloader()
163
	{
164
		// Make sure our paths are in the include path
165 22
		set_include_path($this->_dir . '.' . (!@ini_get('open_basedir') ? PATH_SEPARATOR . get_include_path() : ''));
166
167
		// The autoload "magic"
168 22
		if (!$this->_setup)
169 22
		{
170
			spl_autoload_register(array($this, 'elk_autoloader'));
171
		}
172 22
	}
173
174
	/**
175
	 * Callback for the spl_autoload_register, loads the requested class
176
	 *
177
	 * @param string $class
178
	 */
179 19
	public function elk_autoloader($class)
180
	{
181
		// Break the class name in to its parts
182 19
		if (!$this->_string_to_class($class))
183 19
		{
184
			return false;
185
		}
186
187
		// If passed a namespace, /name/space/class
188 19
		if ($this->_current_namespace !== false)
189 19
		{
190 10
			$this->_handle_namespaces();
191 10
		}
192
193
		// Basic cases like Util.class, Action.class, Request.class
194 19
		if ($this->_file_name === false)
195 19
		{
196 13
			$this->_handle_basic_cases();
197 13
		}
198
199
		// All the rest
200 19
		if ($this->_file_name === false)
201 19
		{
202 11
			$this->_handle_other_cases();
203 11
		}
204
205 19
		$file = $this->_file_name;
206
207
		// Start fresh for the next one
208 19
		$this->_file_name = false;
209
210
		// Well do we have something to do?
211 19
		if (!empty($file))
212 19
		{
213
			// Are we going to validate the file exists?
214 18
			if ($this->_strict)
215 18
			{
216
				if (stream_resolve_include_path($file))
217
				{
218
					require_once($file);
219
				}
220
			}
221
			else
222
			{
223 18
				require_once($file);
224
			}
225
226 18
			$this->_strict = false;
227 18
		}
228
229 19
		return true;
230
	}
231
232
	/**
233
	 * Resolves a class name to an autoload name
234
	 *
235
	 * @param string $class - Name of class to autoload
236
	 */
237 19
	private function _string_to_class($class)
238
	{
239 19
		$namespaces = explode('\\', ltrim($class, '\\'));
240 19
		$prefix = '';
241
242 19
		if (isset($namespaces[1]))
243 19
		{
244 10
			$class = array_pop($namespaces);
245 10
			$full_namespace = '\\' . implode('\\', $namespaces);
246 10
			$found = false;
247
			do
248
			{
249 10
				$this->_current_namespace = '\\' . implode('\\', $namespaces);
250
251 10
				if (isset($this->_paths[$this->_current_namespace]))
252 10
				{
253 8
					$found = true;
254 8
					break;
255
				}
256
257 5
				$prefix .= array_pop($namespaces) . '/';
258 5
			} while (!empty($namespaces));
259
260 10
			if (!$found)
261 10
			{
262 2
				$this->_current_namespace = $full_namespace;
263 2
				if (!isset($this->_paths[$this->_current_namespace]))
264 2
				{
265 2
					$this->register($this->_current_namespace, strtr($this->_current_namespace, array('\\' => DIRECTORY_SEPARATOR, 'ElkArte' => BOARDDIR)));
266 2
				}
267 2
			}
268 10
		}
269
		else
270
		{
271 13
			$this->_current_namespace = false;
272
		}
273
274
		// The name must be letters, numbers and _ only
275 19
		if (preg_match('~[^a-z0-9_]~i', $class))
276 19
		{
277
			return false;
278
		}
279
280 19
		$this->_name = explode('_', $class);
281 19
		$this->_surname = array_pop($this->_name);
282 19
		$this->_givenname = $prefix . implode('', $this->_name);
283
284 19
		return true;
285
	}
286
287
	/**
288
	 * This handles any case where a namespace is present.
289
	 *
290
	 * @return boolean|null false if the namespace was found, but the file not, true otherwise
291
	 */
292 10
	protected function _handle_namespaces()
293
	{
294 10
		if (isset($this->_paths[$this->_current_namespace]))
295 10
		{
296 8
			foreach ($this->_paths[$this->_current_namespace] as $possible_dir)
297
			{
298 8
				$file = $possible_dir . '/' . $this->_givenname . $this->_surname . '.php';
299
300 8
				if (file_exists($file))
301 8
				{
302 8
					$this->_file_name = $file;
303
304 8
					return;
305
				}
306
			}
307
308
			if ($this->_strict_namespace[$this->_current_namespace])
309
			{
310
				$this->_strict = true;
311
			}
312
		}
313 2
	}
314
315
	/**
316
	 * This handles the simple cases, mostly single word class names.
317
	 *
318
	 * - Bypasses db classes as those are done elsewhere
319
	 */
320 13
	private function _handle_basic_cases()
321
	{
322 13
		switch ($this->_givenname)
323
		{
324 13
			case 'VerificationControls':
325
				$this->_file_name = SUBSDIR . '/VerificationControls.class.php';
326
				break;
327 13
			case 'AdminSettings':
328
				$this->_file_name = SUBSDIR . '/AdminSettingsSearch.class.php';
329
				break;
330
			// We don't handle these with the autoloader
331 13
			case 'Database':
332 13
			case 'DbSearch':
333 13
			case 'DbTable':
334
				$this->_file_name = '';
335
				break;
336
			// Simple one word class names like Util.class, Action.class, Request.class ...
337 13
			case '':
338 6
				$this->_file_name = $this->_surname;
339
340 6
				if (!empty($this->_current_namespace))
341 6
						$this->_file_name = $this->_current_namespace . '/' . $this->_file_name;
342
343
				// validate the file since it can vary
344 6 View Code Duplication
				if (stream_resolve_include_path($this->_file_name . '.class.php'))
345 6
				{
346 4
					$this->_file_name = $this->_file_name . '.class.php';
347 4
				}
348 2
				elseif (stream_resolve_include_path($this->_file_name . '.php'))
349
				{
350 2
					$this->_file_name = $this->_file_name . '.php';
351 2
				}
352
				else
353
				{
354 1
					$this->_file_name = '';
355
				}
356 6
				break;
357 11
			default:
358 11
				$this->_file_name = false;
359 13
		}
360 13
	}
361
362
	/**
363
	 * This handles Some_Controller style classes
364
	 */
365 11
	private function _handle_other_cases()
366
	{
367 11
		switch ($this->_surname)
368
		{
369
			// Some_Controller => Some.controller.php
370 11
			case 'Controller':
371 2
				$this->_file_name = $this->_givenname . '.controller.php';
372
373
				// Try source, controller, admin, then addons
374 2
				if (!stream_resolve_include_path($this->_file_name))
375 2
				{
376 1
					$this->_file_name = '';
377 1
				}
378 2
				break;
379
			// Some_Thing_Exception => /Exception/SomeThingException.class.php
380 9
			case 'Exception':
381 2
				$this->_file_name = SUBSDIR . '/Exception/' . $this->_givenname . $this->_surname . '.class.php';
382 2
				break;
383
			// Some_Cache => SomeCache.class.php
384 7
			case 'Integrate':
385
				$this->_file_name = SUBSDIR . '/' . $this->_givenname . '.integrate.php';
386
				if (!stream_resolve_include_path($this->_file_name))
387
				{
388
					$this->_file_name = ADDONSDIR . '/' . $this->_givenname . '/' . $this->_givenname . '.integrate.php';
389
					if (!stream_resolve_include_path($this->_file_name))
390
					{
391
						$this->_file_name = '';
392
					}
393
				}
394
				break;
395
			// Some_Display => Subscriptions-Some.class.php
396 7
			case 'Display':
397 7
			case 'Payment':
398
				if (!empty($this->_name))
399
				{
400
					$this->_file_name = SUBSDIR . '/Subscriptions-' . implode('_', $this->_name) . '.class.php';
401
				}
402
				break;
403 7
			case 'Module':
404
				if (file_exists(SOURCEDIR . '/modules/' . $this->_name[0] . '/' . $this->_givenname . 'Module.class.php'))
405
				{
406
					$this->_file_name = SOURCEDIR . '/modules/' . $this->_name[0] . '/' . $this->_givenname . 'Module.class.php';
407
				}
408
				break;
409 7
			case 'Interface':
410 7
			case 'Abstract':
411
				if ($this->_surname == 'Interface')
412
				{
413
					$this->_file_name = $this->_givenname . '.interface.php';
414
				}
415
				else
416
				{
417
					$this->_file_name = $this->_givenname . 'Abstract.class.php';
418
				}
419
420
				if (file_exists(SUBSDIR . '/' . $this->_file_name))
421
				{
422
					$this->_file_name = SUBSDIR . '/' . $this->_file_name;
423
				}
424
				else
425
				{
426
					$dir = SUBSDIR . '/' . $this->_givenname;
427
428
					if (file_exists($dir . '/' . $this->_file_name))
429
					{
430
						$this->_file_name = $dir . '/' . $this->_file_name;
431
					}
432
					elseif (!empty($this->_name[1]) && $this->_name[1] == 'Module')
433
					{
434
						$this->_file_name = SOURCEDIR . '/modules/' . $this->_name[0] . '/' . $this->_file_name;
435
					}
436
					// Not knowing what it is, better leave it empty
437
					else
438
					{
439
						$this->_file_name = '';
440
					}
441
				}
442
				break;
443
			// All the rest, like Browser_Detector, Template_Layers, Site_Dispatcher ...
444 7
			default:
445 7
				$this->_file_name = $this->_givenname . $this->_surname;
446
447 7 View Code Duplication
				if (stream_resolve_include_path($this->_file_name . '.class.php'))
448 7
				{
449 7
					$this->_file_name = $this->_file_name . '.class.php';
450 7
				}
451 2
				elseif (stream_resolve_include_path($this->_file_name . '.php'))
452
				{
453 2
					$this->_file_name = $this->_file_name . '.php';
454 2
				}
455
				else
456
				{
457
					$this->_file_name = '';
458
				}
459 11
		}
460 11
	}
461
462
	/**
463
	 * Returns the instance of the autoloader
464
	 *
465
	 * - Uses final definition to prevent child classes from overriding this method
466
	 */
467 20
	final public static function instance()
468
	{
469 20
		if (!self::$_instance)
470 20
		{
471
			self::$_instance = new self();
472
		}
473
474 20
		return self::$_instance;
475
	}
476
477
	/**
478
	 * Manually sets the autoloader instance.
479
	 *
480
	 * - Use this to inject a modified version.
481
	 *
482
	 * @param Elk_Autoloader|null $loader
483
	 */
484
	public static function setInstance(Elk_Autoloader $loader = null)
485
	{
486
		self::$_instance = $loader;
487
	}
488
}
489