1 | <?php |
---|
2 | /* |
---|
3 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
---|
4 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
---|
5 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
---|
6 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
---|
7 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
---|
8 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
---|
9 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
---|
10 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
---|
11 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
---|
12 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
---|
13 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
14 | * |
---|
15 | * This software consists of voluntary contributions made by many individuals |
---|
16 | * and is licensed under the LGPL. For more information, see |
---|
17 | * <http://www.doctrine-project.org>. |
---|
18 | */ |
---|
19 | |
---|
20 | namespace Doctrine\ORM\Proxy; |
---|
21 | |
---|
22 | use Doctrine\ORM\EntityManager, |
---|
23 | Doctrine\ORM\Mapping\ClassMetadata, |
---|
24 | Doctrine\Common\Util\ClassUtils; |
---|
25 | |
---|
26 | /** |
---|
27 | * This factory is used to create proxy objects for entities at runtime. |
---|
28 | * |
---|
29 | * @author Roman Borschel <roman@code-factory.org> |
---|
30 | * @author Giorgio Sironi <piccoloprincipeazzurro@gmail.com> |
---|
31 | * @since 2.0 |
---|
32 | */ |
---|
33 | class ProxyFactory |
---|
34 | { |
---|
35 | /** The EntityManager this factory is bound to. */ |
---|
36 | private $_em; |
---|
37 | /** Whether to automatically (re)generate proxy classes. */ |
---|
38 | private $_autoGenerate; |
---|
39 | /** The namespace that contains all proxy classes. */ |
---|
40 | private $_proxyNamespace; |
---|
41 | /** The directory that contains all proxy classes. */ |
---|
42 | private $_proxyDir; |
---|
43 | |
---|
44 | /** |
---|
45 | * Used to match very simple id methods that don't need |
---|
46 | * to be proxied since the identifier is known. |
---|
47 | * |
---|
48 | * @var string |
---|
49 | */ |
---|
50 | const PATTERN_MATCH_ID_METHOD = '((public\s)?(function\s{1,}%s\s?\(\)\s{1,})\s{0,}{\s{0,}return\s{0,}\$this->%s;\s{0,}})i'; |
---|
51 | |
---|
52 | /** |
---|
53 | * Initializes a new instance of the <tt>ProxyFactory</tt> class that is |
---|
54 | * connected to the given <tt>EntityManager</tt>. |
---|
55 | * |
---|
56 | * @param EntityManager $em The EntityManager the new factory works for. |
---|
57 | * @param string $proxyDir The directory to use for the proxy classes. It must exist. |
---|
58 | * @param string $proxyNs The namespace to use for the proxy classes. |
---|
59 | * @param boolean $autoGenerate Whether to automatically generate proxy classes. |
---|
60 | */ |
---|
61 | public function __construct(EntityManager $em, $proxyDir, $proxyNs, $autoGenerate = false) |
---|
62 | { |
---|
63 | if ( ! $proxyDir) { |
---|
64 | throw ProxyException::proxyDirectoryRequired(); |
---|
65 | } |
---|
66 | if ( ! $proxyNs) { |
---|
67 | throw ProxyException::proxyNamespaceRequired(); |
---|
68 | } |
---|
69 | $this->_em = $em; |
---|
70 | $this->_proxyDir = $proxyDir; |
---|
71 | $this->_autoGenerate = $autoGenerate; |
---|
72 | $this->_proxyNamespace = $proxyNs; |
---|
73 | } |
---|
74 | |
---|
75 | /** |
---|
76 | * Gets a reference proxy instance for the entity of the given type and identified by |
---|
77 | * the given identifier. |
---|
78 | * |
---|
79 | * @param string $className |
---|
80 | * @param mixed $identifier |
---|
81 | * @return object |
---|
82 | */ |
---|
83 | public function getProxy($className, $identifier) |
---|
84 | { |
---|
85 | $fqn = ClassUtils::generateProxyClassName($className, $this->_proxyNamespace); |
---|
86 | |
---|
87 | if (! class_exists($fqn, false)) { |
---|
88 | $fileName = $this->getProxyFileName($className); |
---|
89 | if ($this->_autoGenerate) { |
---|
90 | $this->_generateProxyClass($this->_em->getClassMetadata($className), $fileName, self::$_proxyClassTemplate); |
---|
91 | } |
---|
92 | require $fileName; |
---|
93 | } |
---|
94 | |
---|
95 | if ( ! $this->_em->getMetadataFactory()->hasMetadataFor($fqn)) { |
---|
96 | $this->_em->getMetadataFactory()->setMetadataFor($fqn, $this->_em->getClassMetadata($className)); |
---|
97 | } |
---|
98 | |
---|
99 | $entityPersister = $this->_em->getUnitOfWork()->getEntityPersister($className); |
---|
100 | |
---|
101 | return new $fqn($entityPersister, $identifier); |
---|
102 | } |
---|
103 | |
---|
104 | /** |
---|
105 | * Generate the Proxy file name |
---|
106 | * |
---|
107 | * @param string $className |
---|
108 | * @param string $baseDir Optional base directory for proxy file name generation. |
---|
109 | * If not specified, the directory configured on the Configuration of the |
---|
110 | * EntityManager will be used by this factory. |
---|
111 | * @return string |
---|
112 | */ |
---|
113 | private function getProxyFileName($className, $baseDir = null) |
---|
114 | { |
---|
115 | $proxyDir = $baseDir ?: $this->_proxyDir; |
---|
116 | |
---|
117 | return $proxyDir . DIRECTORY_SEPARATOR . '__CG__' . str_replace('\\', '', $className) . '.php'; |
---|
118 | } |
---|
119 | |
---|
120 | /** |
---|
121 | * Generates proxy classes for all given classes. |
---|
122 | * |
---|
123 | * @param array $classes The classes (ClassMetadata instances) for which to generate proxies. |
---|
124 | * @param string $toDir The target directory of the proxy classes. If not specified, the |
---|
125 | * directory configured on the Configuration of the EntityManager used |
---|
126 | * by this factory is used. |
---|
127 | * @return int Number of generated proxies. |
---|
128 | */ |
---|
129 | public function generateProxyClasses(array $classes, $toDir = null) |
---|
130 | { |
---|
131 | $proxyDir = $toDir ?: $this->_proxyDir; |
---|
132 | $proxyDir = rtrim($proxyDir, DIRECTORY_SEPARATOR); |
---|
133 | $num = 0; |
---|
134 | |
---|
135 | foreach ($classes as $class) { |
---|
136 | /* @var $class ClassMetadata */ |
---|
137 | if ($class->isMappedSuperclass || $class->reflClass->isAbstract()) { |
---|
138 | continue; |
---|
139 | } |
---|
140 | |
---|
141 | $proxyFileName = $this->getProxyFileName($class->name, $proxyDir); |
---|
142 | |
---|
143 | $this->_generateProxyClass($class, $proxyFileName, self::$_proxyClassTemplate); |
---|
144 | $num++; |
---|
145 | } |
---|
146 | |
---|
147 | return $num; |
---|
148 | } |
---|
149 | |
---|
150 | /** |
---|
151 | * Generates a proxy class file. |
---|
152 | * |
---|
153 | * @param $class |
---|
154 | * @param $proxyClassName |
---|
155 | * @param $file The path of the file to write to. |
---|
156 | */ |
---|
157 | private function _generateProxyClass($class, $fileName, $file) |
---|
158 | { |
---|
159 | $methods = $this->_generateMethods($class); |
---|
160 | $sleepImpl = $this->_generateSleep($class); |
---|
161 | $cloneImpl = $class->reflClass->hasMethod('__clone') ? 'parent::__clone();' : ''; // hasMethod() checks case-insensitive |
---|
162 | |
---|
163 | $placeholders = array( |
---|
164 | '<namespace>', |
---|
165 | '<proxyClassName>', '<className>', |
---|
166 | '<methods>', '<sleepImpl>', '<cloneImpl>' |
---|
167 | ); |
---|
168 | |
---|
169 | $className = ltrim($class->name, '\\'); |
---|
170 | $proxyClassName = ClassUtils::generateProxyClassName($class->name, $this->_proxyNamespace); |
---|
171 | $parts = explode('\\', strrev($proxyClassName), 2); |
---|
172 | $proxyClassNamespace = strrev($parts[1]); |
---|
173 | $proxyClassName = strrev($parts[0]); |
---|
174 | |
---|
175 | $replacements = array( |
---|
176 | $proxyClassNamespace, |
---|
177 | $proxyClassName, |
---|
178 | $className, |
---|
179 | $methods, |
---|
180 | $sleepImpl, |
---|
181 | $cloneImpl |
---|
182 | ); |
---|
183 | |
---|
184 | $file = str_replace($placeholders, $replacements, $file); |
---|
185 | |
---|
186 | $parentDirectory = dirname($fileName); |
---|
187 | |
---|
188 | if ( ! is_dir($parentDirectory)) { |
---|
189 | if (false === @mkdir($parentDirectory, 0775, true)) { |
---|
190 | throw ProxyException::proxyDirectoryNotWritable(); |
---|
191 | } |
---|
192 | } else if ( ! is_writable($parentDirectory)) { |
---|
193 | throw ProxyException::proxyDirectoryNotWritable(); |
---|
194 | } |
---|
195 | |
---|
196 | $tmpFileName = $fileName . '.' . uniqid("", true); |
---|
197 | file_put_contents($tmpFileName, $file); |
---|
198 | rename($tmpFileName, $fileName); |
---|
199 | } |
---|
200 | |
---|
201 | /** |
---|
202 | * Generates the methods of a proxy class. |
---|
203 | * |
---|
204 | * @param ClassMetadata $class |
---|
205 | * @return string The code of the generated methods. |
---|
206 | */ |
---|
207 | private function _generateMethods(ClassMetadata $class) |
---|
208 | { |
---|
209 | $methods = ''; |
---|
210 | |
---|
211 | $methodNames = array(); |
---|
212 | foreach ($class->reflClass->getMethods() as $method) { |
---|
213 | /* @var $method ReflectionMethod */ |
---|
214 | if ($method->isConstructor() || in_array(strtolower($method->getName()), array("__sleep", "__clone")) || isset($methodNames[$method->getName()])) { |
---|
215 | continue; |
---|
216 | } |
---|
217 | $methodNames[$method->getName()] = true; |
---|
218 | |
---|
219 | if ($method->isPublic() && ! $method->isFinal() && ! $method->isStatic()) { |
---|
220 | $methods .= "\n" . ' public function '; |
---|
221 | if ($method->returnsReference()) { |
---|
222 | $methods .= '&'; |
---|
223 | } |
---|
224 | $methods .= $method->getName() . '('; |
---|
225 | $firstParam = true; |
---|
226 | $parameterString = $argumentString = ''; |
---|
227 | |
---|
228 | foreach ($method->getParameters() as $param) { |
---|
229 | if ($firstParam) { |
---|
230 | $firstParam = false; |
---|
231 | } else { |
---|
232 | $parameterString .= ', '; |
---|
233 | $argumentString .= ', '; |
---|
234 | } |
---|
235 | |
---|
236 | // We need to pick the type hint class too |
---|
237 | if (($paramClass = $param->getClass()) !== null) { |
---|
238 | $parameterString .= '\\' . $paramClass->getName() . ' '; |
---|
239 | } else if ($param->isArray()) { |
---|
240 | $parameterString .= 'array '; |
---|
241 | } |
---|
242 | |
---|
243 | if ($param->isPassedByReference()) { |
---|
244 | $parameterString .= '&'; |
---|
245 | } |
---|
246 | |
---|
247 | $parameterString .= '$' . $param->getName(); |
---|
248 | $argumentString .= '$' . $param->getName(); |
---|
249 | |
---|
250 | if ($param->isDefaultValueAvailable()) { |
---|
251 | $parameterString .= ' = ' . var_export($param->getDefaultValue(), true); |
---|
252 | } |
---|
253 | } |
---|
254 | |
---|
255 | $methods .= $parameterString . ')'; |
---|
256 | $methods .= "\n" . ' {' . "\n"; |
---|
257 | if ($this->isShortIdentifierGetter($method, $class)) { |
---|
258 | $identifier = lcfirst(substr($method->getName(), 3)); |
---|
259 | |
---|
260 | $cast = in_array($class->fieldMappings[$identifier]['type'], array('integer', 'smallint')) ? '(int) ' : ''; |
---|
261 | |
---|
262 | $methods .= ' if ($this->__isInitialized__ === false) {' . "\n"; |
---|
263 | $methods .= ' return ' . $cast . '$this->_identifier["' . $identifier . '"];' . "\n"; |
---|
264 | $methods .= ' }' . "\n"; |
---|
265 | } |
---|
266 | $methods .= ' $this->__load();' . "\n"; |
---|
267 | $methods .= ' return parent::' . $method->getName() . '(' . $argumentString . ');'; |
---|
268 | $methods .= "\n" . ' }' . "\n"; |
---|
269 | } |
---|
270 | } |
---|
271 | |
---|
272 | return $methods; |
---|
273 | } |
---|
274 | |
---|
275 | /** |
---|
276 | * Check if the method is a short identifier getter. |
---|
277 | * |
---|
278 | * What does this mean? For proxy objects the identifier is already known, |
---|
279 | * however accessing the getter for this identifier usually triggers the |
---|
280 | * lazy loading, leading to a query that may not be necessary if only the |
---|
281 | * ID is interesting for the userland code (for example in views that |
---|
282 | * generate links to the entity, but do not display anything else). |
---|
283 | * |
---|
284 | * @param ReflectionMethod $method |
---|
285 | * @param ClassMetadata $class |
---|
286 | * @return bool |
---|
287 | */ |
---|
288 | private function isShortIdentifierGetter($method, $class) |
---|
289 | { |
---|
290 | $identifier = lcfirst(substr($method->getName(), 3)); |
---|
291 | $cheapCheck = ( |
---|
292 | $method->getNumberOfParameters() == 0 && |
---|
293 | substr($method->getName(), 0, 3) == "get" && |
---|
294 | in_array($identifier, $class->identifier, true) && |
---|
295 | $class->hasField($identifier) && |
---|
296 | (($method->getEndLine() - $method->getStartLine()) <= 4) |
---|
297 | && in_array($class->fieldMappings[$identifier]['type'], array('integer', 'bigint', 'smallint', 'string')) |
---|
298 | ); |
---|
299 | |
---|
300 | if ($cheapCheck) { |
---|
301 | $code = file($method->getDeclaringClass()->getFileName()); |
---|
302 | $code = trim(implode(" ", array_slice($code, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1))); |
---|
303 | |
---|
304 | $pattern = sprintf(self::PATTERN_MATCH_ID_METHOD, $method->getName(), $identifier); |
---|
305 | |
---|
306 | if (preg_match($pattern, $code)) { |
---|
307 | return true; |
---|
308 | } |
---|
309 | } |
---|
310 | return false; |
---|
311 | } |
---|
312 | |
---|
313 | /** |
---|
314 | * Generates the code for the __sleep method for a proxy class. |
---|
315 | * |
---|
316 | * @param $class |
---|
317 | * @return string |
---|
318 | */ |
---|
319 | private function _generateSleep(ClassMetadata $class) |
---|
320 | { |
---|
321 | $sleepImpl = ''; |
---|
322 | |
---|
323 | if ($class->reflClass->hasMethod('__sleep')) { |
---|
324 | $sleepImpl .= "return array_merge(array('__isInitialized__'), parent::__sleep());"; |
---|
325 | } else { |
---|
326 | $sleepImpl .= "return array('__isInitialized__', "; |
---|
327 | $first = true; |
---|
328 | |
---|
329 | foreach ($class->getReflectionProperties() as $name => $prop) { |
---|
330 | if ($first) { |
---|
331 | $first = false; |
---|
332 | } else { |
---|
333 | $sleepImpl .= ', '; |
---|
334 | } |
---|
335 | |
---|
336 | $sleepImpl .= "'" . $name . "'"; |
---|
337 | } |
---|
338 | |
---|
339 | $sleepImpl .= ');'; |
---|
340 | } |
---|
341 | |
---|
342 | return $sleepImpl; |
---|
343 | } |
---|
344 | |
---|
345 | /** Proxy class code template */ |
---|
346 | private static $_proxyClassTemplate = |
---|
347 | '<?php |
---|
348 | |
---|
349 | namespace <namespace>; |
---|
350 | |
---|
351 | /** |
---|
352 | * THIS CLASS WAS GENERATED BY THE DOCTRINE ORM. DO NOT EDIT THIS FILE. |
---|
353 | */ |
---|
354 | class <proxyClassName> extends \<className> implements \Doctrine\ORM\Proxy\Proxy |
---|
355 | { |
---|
356 | private $_entityPersister; |
---|
357 | private $_identifier; |
---|
358 | public $__isInitialized__ = false; |
---|
359 | public function __construct($entityPersister, $identifier) |
---|
360 | { |
---|
361 | $this->_entityPersister = $entityPersister; |
---|
362 | $this->_identifier = $identifier; |
---|
363 | } |
---|
364 | /** @private */ |
---|
365 | public function __load() |
---|
366 | { |
---|
367 | if (!$this->__isInitialized__ && $this->_entityPersister) { |
---|
368 | $this->__isInitialized__ = true; |
---|
369 | |
---|
370 | if (method_exists($this, "__wakeup")) { |
---|
371 | // call this after __isInitialized__to avoid infinite recursion |
---|
372 | // but before loading to emulate what ClassMetadata::newInstance() |
---|
373 | // provides. |
---|
374 | $this->__wakeup(); |
---|
375 | } |
---|
376 | |
---|
377 | if ($this->_entityPersister->load($this->_identifier, $this) === null) { |
---|
378 | throw new \Doctrine\ORM\EntityNotFoundException(); |
---|
379 | } |
---|
380 | unset($this->_entityPersister, $this->_identifier); |
---|
381 | } |
---|
382 | } |
---|
383 | |
---|
384 | /** @private */ |
---|
385 | public function __isInitialized() |
---|
386 | { |
---|
387 | return $this->__isInitialized__; |
---|
388 | } |
---|
389 | |
---|
390 | <methods> |
---|
391 | |
---|
392 | public function __sleep() |
---|
393 | { |
---|
394 | <sleepImpl> |
---|
395 | } |
---|
396 | |
---|
397 | public function __clone() |
---|
398 | { |
---|
399 | if (!$this->__isInitialized__ && $this->_entityPersister) { |
---|
400 | $this->__isInitialized__ = true; |
---|
401 | $class = $this->_entityPersister->getClassMetadata(); |
---|
402 | $original = $this->_entityPersister->load($this->_identifier); |
---|
403 | if ($original === null) { |
---|
404 | throw new \Doctrine\ORM\EntityNotFoundException(); |
---|
405 | } |
---|
406 | foreach ($class->reflFields AS $field => $reflProperty) { |
---|
407 | $reflProperty->setValue($this, $reflProperty->getValue($original)); |
---|
408 | } |
---|
409 | unset($this->_entityPersister, $this->_identifier); |
---|
410 | } |
---|
411 | <cloneImpl> |
---|
412 | } |
---|
413 | }'; |
---|
414 | } |
---|