1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace steevanb\DoctrineReadOnlyHydrator\Hydrator; |
4
|
|
|
|
5
|
|
|
use Doctrine\Common\Proxy\AbstractProxyFactory; |
6
|
|
|
use Doctrine\Common\Proxy\ProxyGenerator; |
7
|
|
|
use Doctrine\ORM\Mapping\ClassMetadata; |
8
|
|
|
use steevanb\DoctrineReadOnlyHydrator\Entity\ReadOnlyEntityInterface; |
9
|
|
|
use steevanb\DoctrineReadOnlyHydrator\Exception\PrivateMethodShouldNotAccessPropertiesException; |
10
|
|
|
|
11
|
|
|
class ReadOnlyHydrator extends SimpleObjectHydrator |
12
|
|
|
{ |
13
|
|
|
const HYDRATOR_NAME = 'readOnly'; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* @param ClassMetadata $classMetaData |
17
|
|
|
* @param array $data |
18
|
|
|
* @return mixed |
19
|
|
|
* @throws \Exception |
20
|
|
|
*/ |
21
|
|
|
protected function createEntity(ClassMetadata $classMetaData, array $data) |
22
|
|
|
{ |
23
|
|
|
$className = $this->getEntityClassName($classMetaData, $data); |
24
|
|
|
// $this->generateProxyFile($classMetaData, $data); |
|
|
|
|
25
|
|
|
|
26
|
|
|
require_once($this->getProxyFilePath($className)); |
27
|
|
|
$proxyClassName = $this->getProxyNamespace($className) . '\\' . $this->getProxyClassName($className); |
28
|
|
|
$entity = new $proxyClassName(array_keys($data)); |
29
|
|
|
|
30
|
|
|
return $entity; |
31
|
|
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @param ClassMetadata $classMetaData |
35
|
|
|
* @param array $data |
36
|
|
|
* @return $this |
37
|
|
|
*/ |
38
|
|
|
protected function generateProxyFile(ClassMetadata $classMetaData, array $data) |
39
|
|
|
{ |
40
|
|
|
$entityClassName = $this->getEntityClassName($classMetaData, $data); |
41
|
|
|
$proxyMethodsCode = implode("\n\n", $this->getPhpForProxyMethods($classMetaData, $entityClassName)); |
42
|
|
|
$proxyNamespace = $this->getProxyNamespace($entityClassName); |
43
|
|
|
$proxyClassName = $this->getProxyClassName($entityClassName); |
44
|
|
|
$generator = static::class; |
45
|
|
|
$readOnlyInterface = ReadOnlyEntityInterface::class; |
46
|
|
|
|
47
|
|
|
$php = <<<PHP |
48
|
|
|
<?php |
49
|
|
|
|
50
|
|
|
namespace $proxyNamespace; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* DO NOT EDIT THIS FILE - IT WAS CREATED BY $generator |
54
|
|
|
*/ |
55
|
|
|
class $proxyClassName extends \\$entityClassName implements \\$readOnlyInterface |
56
|
|
|
{ |
57
|
|
|
protected \$loadedProperties; |
58
|
|
|
|
59
|
|
|
public function __construct(array \$loadedProperties) |
60
|
|
|
{ |
61
|
|
|
\$this->loadedProperties = \$loadedProperties; |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
$proxyMethodsCode |
65
|
|
|
|
66
|
|
|
protected function assertReadOnlyPropertiesAreLoaded(array \$properties) |
67
|
|
|
{ |
68
|
|
|
foreach (\$properties as \$property) { |
69
|
|
|
if (in_array(\$property, \$this->loadedProperties) === false) { |
70
|
|
|
throw new \steevanb\DoctrineReadOnlyHydrator\Exception\PropertyNotLoadedException(\$this, \$property); |
71
|
|
|
} |
72
|
|
|
} |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
PHP; |
76
|
|
|
file_put_contents($this->getProxyFilePath($entityClassName), $php); |
77
|
|
|
|
78
|
|
|
return $this; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @param string $entityClassName |
83
|
|
|
* @return string |
84
|
|
|
*/ |
85
|
|
|
protected function getProxyFilePath($entityClassName) |
86
|
|
|
{ |
87
|
|
|
$fileName = str_replace('\\', '_', $entityClassName) . '.php'; |
88
|
|
|
|
89
|
|
|
return $this->getProxyDirectory() . DIRECTORY_SEPARATOR . $fileName; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @param string $entityClassName |
94
|
|
|
* @return string |
95
|
|
|
*/ |
96
|
|
|
protected function getProxyNamespace($entityClassName) |
97
|
|
|
{ |
98
|
|
|
return 'ReadOnlyProxies\\' . substr($entityClassName, 0, strrpos($entityClassName, '\\')); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @param string $entityClassName |
103
|
|
|
* @return string |
104
|
|
|
*/ |
105
|
|
|
protected function getProxyClassName($entityClassName) |
106
|
|
|
{ |
107
|
|
|
return substr($entityClassName, strrpos($entityClassName, '\\') + 1); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* As Doctrine\ORM\EntityManager::newHydrator() call new FooHydrator($this), we can't set parameters to Hydrator. |
112
|
|
|
* So, we will use proxyDirectory from Doctrine\Common\Proxy\AbstractProxyFactory. |
113
|
|
|
* It's directory used by Doctrine\ORM\Internal\Hydration\ObjectHydrator. |
114
|
|
|
* |
115
|
|
|
* @return string |
116
|
|
|
*/ |
117
|
|
|
protected function getProxyDirectory() |
118
|
|
|
{ |
119
|
|
|
/** @var ProxyGenerator $proxyGenerator */ |
120
|
|
|
$proxyGenerator = $this->getPrivatePropertyValue( |
121
|
|
|
AbstractProxyFactory::class, |
122
|
|
|
'proxyGenerator', |
123
|
|
|
$this->_em->getProxyFactory() |
124
|
|
|
); |
125
|
|
|
|
126
|
|
|
$directory = $this->getPrivatePropertyValue(get_class($proxyGenerator), 'proxyDirectory', $proxyGenerator); |
127
|
|
|
$readOnlyDirectory = $directory . DIRECTORY_SEPARATOR . 'ReadOnly'; |
128
|
|
|
if (is_dir($readOnlyDirectory) === false) { |
129
|
|
|
mkdir($readOnlyDirectory); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
return $readOnlyDirectory; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* @param \ReflectionMethod $reflectionMethod |
137
|
|
|
* @param array $properties |
138
|
|
|
* @return string|false |
139
|
|
|
*/ |
140
|
|
|
protected function getUsedProperties(\ReflectionMethod $reflectionMethod, $properties) |
141
|
|
|
{ |
142
|
|
|
$classLines = file($reflectionMethod->getFileName()); |
143
|
|
|
$methodLines = array_slice( |
144
|
|
|
$classLines, |
145
|
|
|
$reflectionMethod->getStartLine() - 1, |
146
|
|
|
$reflectionMethod->getEndLine() - $reflectionMethod->getStartLine() + 1 |
147
|
|
|
); |
148
|
|
|
$code = '<?php' . "\n" . implode("\n", $methodLines) . "\n" . '?>'; |
149
|
|
|
|
150
|
|
|
$return = array(); |
151
|
|
|
$nextStringIsProperty = false; |
152
|
|
|
foreach (token_get_all($code) as $token) { |
153
|
|
|
if (is_array($token)) { |
154
|
|
|
if ($token[0] === T_VARIABLE && $token[1] === '$this') { |
155
|
|
|
$nextStringIsProperty = true; |
156
|
|
|
} elseif ($nextStringIsProperty && $token[0] === T_STRING) { |
157
|
|
|
$nextStringIsProperty = false; |
158
|
|
|
if (in_array($token[1], $properties)) { |
159
|
|
|
$return[$token[1]] = true; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
return array_keys($return); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* @param ClassMetadata $classMetaData |
170
|
|
|
* @param string $entityClassName |
171
|
|
|
* @return array |
172
|
|
|
* @throws PrivateMethodShouldNotAccessPropertiesException |
173
|
|
|
*/ |
174
|
|
|
protected function getPhpForProxyMethods(ClassMetadata $classMetaData, $entityClassName) |
175
|
|
|
{ |
176
|
|
|
$return = array(); |
177
|
|
|
$reflectionClass = new \ReflectionClass($entityClassName); |
178
|
|
|
$properties = array_merge($classMetaData->getFieldNames(), array_keys($classMetaData->associationMappings)); |
179
|
|
|
foreach ($reflectionClass->getMethods() as $method) { |
180
|
|
|
if ($method->getName() === '__construct') { |
|
|
|
|
181
|
|
|
continue; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$usedProperties = $this->getUsedProperties($method, $properties); |
185
|
|
|
if (count($usedProperties) > 0) { |
186
|
|
|
if ($method->isPrivate()) { |
187
|
|
|
throw new PrivateMethodShouldNotAccessPropertiesException( |
188
|
|
|
$entityClassName, |
189
|
|
|
$method->getName(), |
|
|
|
|
190
|
|
|
$usedProperties |
191
|
|
|
); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$return[] = $this->getPhpForMethod($method, $usedProperties); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
return $return; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @param \ReflectionMethod $reflectionMethod |
203
|
|
|
* @param array $properties |
204
|
|
|
* @return string |
205
|
|
|
*/ |
206
|
|
|
protected function getPhpForMethod(\ReflectionMethod $reflectionMethod, array $properties) |
207
|
|
|
{ |
208
|
|
|
if ($reflectionMethod->isPublic()) { |
209
|
|
|
$signature = 'public'; |
210
|
|
|
} else { |
211
|
|
|
$signature = 'protected'; |
212
|
|
|
} |
213
|
|
|
$signature .= ' function ' . $reflectionMethod->getName() . '('; |
|
|
|
|
214
|
|
|
$parameters = array(); |
215
|
|
|
foreach ($reflectionMethod->getParameters() as $parameter) { |
216
|
|
|
$parameters[] = $this->getPhpForParameter($parameter); |
217
|
|
|
} |
218
|
|
|
$signature .= implode(', ', $parameters) . ')'; |
219
|
|
|
|
220
|
|
|
$method = $reflectionMethod->getName(); |
|
|
|
|
221
|
|
|
|
222
|
|
|
array_walk($properties, function(&$name) { |
223
|
|
|
$name = "'" . $name . "'"; |
224
|
|
|
}); |
225
|
|
|
$propertiesToAssert = implode(', ', $properties); |
226
|
|
|
|
227
|
|
|
$php = <<<PHP |
228
|
|
|
$signature |
229
|
|
|
{ |
230
|
|
|
\$this->assertReadOnlyPropertiesAreLoaded(array($propertiesToAssert)); |
231
|
|
|
|
232
|
|
|
return call_user_func_array(array('parent', '$method'), func_get_args()); |
233
|
|
|
} |
234
|
|
|
PHP; |
235
|
|
|
|
236
|
|
|
return $php; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* @param \ReflectionParameter $parameter |
241
|
|
|
* @return string |
242
|
|
|
*/ |
243
|
|
|
protected function getPhpForParameter(\ReflectionParameter $parameter) |
244
|
|
|
{ |
245
|
|
|
$php = null; |
246
|
|
|
if ($parameter->getClass() instanceof \ReflectionClass) { |
247
|
|
|
$php .= '\\' . $parameter->getClass()->getName() . ' '; |
|
|
|
|
248
|
|
|
} elseif ($parameter->isCallable()) { |
249
|
|
|
$php .= 'callable '; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
if ($parameter->isPassedByReference()) { |
253
|
|
|
$php .= '&'; |
254
|
|
|
} |
255
|
|
|
$php .= '$' . $parameter->getName(); |
|
|
|
|
256
|
|
|
|
257
|
|
|
if ($parameter->isDefaultValueAvailable()) { |
258
|
|
|
if ($parameter->isDefaultValueConstant()) { |
259
|
|
|
$defaultValue = $parameter->getDefaultValueConstantName(); |
|
|
|
|
260
|
|
|
} elseif ($parameter->getDefaultValue() === null) { |
261
|
|
|
$defaultValue = 'null'; |
262
|
|
|
} elseif (is_string($parameter->getDefaultValue())) { |
263
|
|
|
$defaultValue = '\'' . $parameter->getDefaultValue() . '\''; |
264
|
|
|
} else { |
265
|
|
|
$defaultValue = $parameter->getDefaultValue(); |
266
|
|
|
} |
267
|
|
|
$php .= ' = ' . $defaultValue; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
return $php; |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.