|
1 <?php |
|
2 |
|
3 /* |
|
4 * This file is part of the Symfony package. |
|
5 * |
|
6 * (c) Fabien Potencier <fabien@symfony.com> |
|
7 * |
|
8 * For the full copyright and license information, please view the LICENSE |
|
9 * file that was distributed with this source code. |
|
10 */ |
|
11 |
|
12 namespace Symfony\Component\ClassLoader; |
|
13 |
|
14 /** |
|
15 * ClassCollectionLoader. |
|
16 * |
|
17 * @author Fabien Potencier <fabien@symfony.com> |
|
18 */ |
|
19 class ClassCollectionLoader |
|
20 { |
|
21 static private $loaded; |
|
22 |
|
23 /** |
|
24 * Loads a list of classes and caches them in one big file. |
|
25 * |
|
26 * @param array $classes An array of classes to load |
|
27 * @param string $cacheDir A cache directory |
|
28 * @param string $name The cache name prefix |
|
29 * @param Boolean $autoReload Whether to flush the cache when the cache is stale or not |
|
30 * @param Boolean $adaptive Whether to remove already declared classes or not |
|
31 * @param string $extension File extension of the resulting file |
|
32 * |
|
33 * @throws \InvalidArgumentException When class can't be loaded |
|
34 */ |
|
35 static public function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php') |
|
36 { |
|
37 // each $name can only be loaded once per PHP process |
|
38 if (isset(self::$loaded[$name])) { |
|
39 return; |
|
40 } |
|
41 |
|
42 self::$loaded[$name] = true; |
|
43 |
|
44 if ($adaptive) { |
|
45 // don't include already declared classes |
|
46 $classes = array_diff($classes, get_declared_classes(), get_declared_interfaces()); |
|
47 |
|
48 // the cache is different depending on which classes are already declared |
|
49 $name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5); |
|
50 } |
|
51 |
|
52 $cache = $cacheDir.'/'.$name.$extension; |
|
53 |
|
54 // auto-reload |
|
55 $reload = false; |
|
56 if ($autoReload) { |
|
57 $metadata = $cacheDir.'/'.$name.$extension.'.meta'; |
|
58 if (!file_exists($metadata) || !file_exists($cache)) { |
|
59 $reload = true; |
|
60 } else { |
|
61 $time = filemtime($cache); |
|
62 $meta = unserialize(file_get_contents($metadata)); |
|
63 |
|
64 if ($meta[1] != $classes) { |
|
65 $reload = true; |
|
66 } else { |
|
67 foreach ($meta[0] as $resource) { |
|
68 if (!file_exists($resource) || filemtime($resource) > $time) { |
|
69 $reload = true; |
|
70 |
|
71 break; |
|
72 } |
|
73 } |
|
74 } |
|
75 } |
|
76 } |
|
77 |
|
78 if (!$reload && file_exists($cache)) { |
|
79 require_once $cache; |
|
80 |
|
81 return; |
|
82 } |
|
83 |
|
84 $files = array(); |
|
85 $content = ''; |
|
86 foreach ($classes as $class) { |
|
87 if (!class_exists($class) && !interface_exists($class)) { |
|
88 throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class)); |
|
89 } |
|
90 |
|
91 $r = new \ReflectionClass($class); |
|
92 $files[] = $r->getFileName(); |
|
93 |
|
94 $c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($r->getFileName())); |
|
95 |
|
96 // add namespace declaration for global code |
|
97 if (!$r->inNamespace()) { |
|
98 $c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n"; |
|
99 } else { |
|
100 $c = self::fixNamespaceDeclarations('<?php '.$c); |
|
101 $c = preg_replace('/^\s*<\?php/', '', $c); |
|
102 } |
|
103 |
|
104 $content .= $c; |
|
105 } |
|
106 |
|
107 // cache the core classes |
|
108 if (!is_dir(dirname($cache))) { |
|
109 mkdir(dirname($cache), 0777, true); |
|
110 } |
|
111 self::writeCacheFile($cache, '<?php '.$content); |
|
112 |
|
113 if ($autoReload) { |
|
114 // save the resources |
|
115 self::writeCacheFile($metadata, serialize(array($files, $classes))); |
|
116 } |
|
117 } |
|
118 |
|
119 /** |
|
120 * Adds brackets around each namespace if it's not already the case. |
|
121 * |
|
122 * @param string $source Namespace string |
|
123 * |
|
124 * @return string Namespaces with brackets |
|
125 */ |
|
126 static public function fixNamespaceDeclarations($source) |
|
127 { |
|
128 if (!function_exists('token_get_all')) { |
|
129 return $source; |
|
130 } |
|
131 |
|
132 $output = ''; |
|
133 $inNamespace = false; |
|
134 $tokens = token_get_all($source); |
|
135 |
|
136 for ($i = 0, $max = count($tokens); $i < $max; $i++) { |
|
137 $token = $tokens[$i]; |
|
138 if (is_string($token)) { |
|
139 $output .= $token; |
|
140 } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { |
|
141 // strip comments |
|
142 continue; |
|
143 } elseif (T_NAMESPACE === $token[0]) { |
|
144 if ($inNamespace) { |
|
145 $output .= "}\n"; |
|
146 } |
|
147 $output .= $token[1]; |
|
148 |
|
149 // namespace name and whitespaces |
|
150 while (($t = $tokens[++$i]) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) { |
|
151 $output .= $t[1]; |
|
152 } |
|
153 if (is_string($t) && '{' === $t) { |
|
154 $inNamespace = false; |
|
155 --$i; |
|
156 } else { |
|
157 $output .= "\n{"; |
|
158 $inNamespace = true; |
|
159 } |
|
160 } else { |
|
161 $output .= $token[1]; |
|
162 } |
|
163 } |
|
164 |
|
165 if ($inNamespace) { |
|
166 $output .= "}\n"; |
|
167 } |
|
168 |
|
169 return $output; |
|
170 } |
|
171 |
|
172 /** |
|
173 * Writes a cache file. |
|
174 * |
|
175 * @param string $file Filename |
|
176 * @param string $content Temporary file content |
|
177 * |
|
178 * @throws \RuntimeException when a cache file cannot be written |
|
179 */ |
|
180 static private function writeCacheFile($file, $content) |
|
181 { |
|
182 $tmpFile = tempnam(dirname($file), basename($file)); |
|
183 if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { |
|
184 chmod($file, 0644); |
|
185 |
|
186 return; |
|
187 } |
|
188 |
|
189 throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file)); |
|
190 } |
|
191 |
|
192 /** |
|
193 * Removes comments from a PHP source string. |
|
194 * |
|
195 * We don't use the PHP php_strip_whitespace() function |
|
196 * as we want the content to be readable and well-formatted. |
|
197 * |
|
198 * @param string $source A PHP string |
|
199 * |
|
200 * @return string The PHP string with the comments removed |
|
201 */ |
|
202 static private function stripComments($source) |
|
203 { |
|
204 if (!function_exists('token_get_all')) { |
|
205 return $source; |
|
206 } |
|
207 |
|
208 $output = ''; |
|
209 foreach (token_get_all($source) as $token) { |
|
210 if (is_string($token)) { |
|
211 $output .= $token; |
|
212 } elseif (!in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { |
|
213 $output .= $token[1]; |
|
214 } |
|
215 } |
|
216 |
|
217 // replace multiple new lines with a single newline |
|
218 $output = preg_replace(array('/\s+$/Sm', '/\n+/S'), "\n", $output); |
|
219 |
|
220 return $output; |
|
221 } |
|
222 } |