Completed
Push — master ( 26ecbc...8c0c5d )
by Maciej
14s
created

DefaultPersistentCollectionGenerator.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\PersistentCollection;
6
7
use Doctrine\ODM\MongoDB\Configuration;
8
use ReflectionClass;
9
use ReflectionException;
10
use ReflectionMethod;
11
use ReflectionParameter;
12
use ReflectionType;
13
use const DIRECTORY_SEPARATOR;
14
use function array_map;
15
use function array_pop;
16
use function class_exists;
17
use function dirname;
18
use function explode;
19
use function file_exists;
20
use function file_put_contents;
21
use function implode;
22
use function interface_exists;
23
use function is_dir;
24
use function is_writable;
25
use function method_exists;
26
use function mkdir;
27
use function rename;
28
use function str_replace;
29
use function strtolower;
30
use function substr;
31
use function uniqid;
32
use function var_export;
33
34
/**
35
 * Default generator for custom PersistentCollection classes.
36
 */
37
final class DefaultPersistentCollectionGenerator implements PersistentCollectionGenerator
38
{
39
    /**
40
     * The namespace that contains all persistent collection classes.
41
     *
42
     * @var string
43
     */
44
    private $collectionNamespace;
45
46
    /**
47
     * The directory that contains all persistent collection classes.
48
     *
49
     * @var string
50
     */
51
    private $collectionDir;
52
53 11
    public function __construct(string $collectionDir, string $collectionNs)
54
    {
55 11
        $this->collectionDir       = $collectionDir;
56 11
        $this->collectionNamespace = $collectionNs;
57 11
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function generateClass(string $class, string $dir) : void
63
    {
64
        $collClassName = str_replace('\\', '', $class) . 'Persistent';
65
        $className     = $this->collectionNamespace . '\\' . $collClassName;
66
        $fileName      = $dir . DIRECTORY_SEPARATOR . $collClassName . '.php';
67
        $this->generateCollectionClass($class, $className, $fileName);
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 10
    public function loadClass(string $collectionClass, int $autoGenerate) : string
74
    {
75
        // These checks are not in __construct() because of BC and should be moved for 2.0
76 10
        if (! $this->collectionDir) {
77
            throw PersistentCollectionException::directoryRequired();
78
        }
79 10
        if (! $this->collectionNamespace) {
80
            throw PersistentCollectionException::namespaceRequired();
81
        }
82
83 10
        $collClassName = str_replace('\\', '', $collectionClass) . 'Persistent';
84 10
        $className     = $this->collectionNamespace . '\\' . $collClassName;
85 10
        if (! class_exists($className, false)) {
86 6
            $fileName = $this->collectionDir . DIRECTORY_SEPARATOR . $collClassName . '.php';
87 6
            switch ($autoGenerate) {
88
                case Configuration::AUTOGENERATE_NEVER:
89
                    require $fileName;
90
                    break;
91
92
                case Configuration::AUTOGENERATE_ALWAYS:
93 3
                    $this->generateCollectionClass($collectionClass, $className, $fileName);
94 3
                    require $fileName;
95 3
                    break;
96
97
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
98
                    if (! file_exists($fileName)) {
99
                        $this->generateCollectionClass($collectionClass, $className, $fileName);
100
                    }
101
                    require $fileName;
102
                    break;
103
104
                case Configuration::AUTOGENERATE_EVAL:
105 3
                    $this->generateCollectionClass($collectionClass, $className, false);
106 3
                    break;
107
            }
108
        }
109
110 10
        return $className;
111
    }
112
113
    /**
114
     * @param string|false $fileName Filename to write collection class code or false to eval it.
115
     */
116 6
    private function generateCollectionClass(string $for, string $targetFqcn, $fileName)
117
    {
118 6
        $exploded  = explode('\\', $targetFqcn);
119 6
        $class     = array_pop($exploded);
120 6
        $namespace = implode('\\', $exploded);
121
        $code      = <<<CODE
122
<?php
123
124 6
namespace $namespace;
125
126
use Doctrine\Common\Collections\Collection as BaseCollection;
127
use Doctrine\ODM\MongoDB\DocumentManager;
128
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
129
use Doctrine\ODM\MongoDB\MongoDBException;
130
use Doctrine\ODM\MongoDB\UnitOfWork;
131
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
132
133
/**
134
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE\'S PERSISTENT COLLECTION GENERATOR
135
 */
136 6
class $class extends \\$for implements \\Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface
137
{
138
    use \\Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionTrait;
139
140
    /**
141
     * @param BaseCollection \$coll
142
     * @param DocumentManager \$dm
143
     * @param UnitOfWork \$uow
144
     */
145
    public function __construct(BaseCollection \$coll, DocumentManager \$dm, UnitOfWork \$uow)
146
    {
147
        \$this->coll = \$coll;
148
        \$this->dm = \$dm;
149
        \$this->uow = \$uow;
150
    }
151
152
CODE;
153 6
        $rc        = new ReflectionClass($for);
154 6
        $rt        = new ReflectionClass('Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionTrait');
155 6
        foreach ($rc->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
156 6
            if ($rt->hasMethod($method->name) ||
157 6
                $method->isConstructor() ||
158 6
                $method->isFinal() ||
159 6
                $method->isStatic()
160
            ) {
161 6
                continue;
162
            }
163 6
            $code .= $this->generateMethod($method);
164
        }
165 6
        $code .= "}\n";
166
167 6
        if ($fileName === false) {
168 3
            if (! class_exists($targetFqcn)) {
169 3
                eval(substr($code, 5));
170
            }
171
        } else {
172 3
            $parentDirectory = dirname($fileName);
173
174 3
            if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
175
                throw PersistentCollectionException::directoryNotWritable();
176
            }
177
178 3
            if (! is_writable($parentDirectory)) {
179
                throw PersistentCollectionException::directoryNotWritable();
180
            }
181
182 3
            $tmpFileName = $fileName . '.' . uniqid('', true);
183 3
            file_put_contents($tmpFileName, $code);
184 3
            rename($tmpFileName, $fileName);
185
        }
186 6
    }
187
188 6
    private function generateMethod(ReflectionMethod $method) : string
189
    {
190 6
        $parametersString = $this->buildParametersString($method);
191 6
        $callParamsString = implode(', ', $this->getParameterNamesForDecoratedCall($method->getParameters()));
192
193
        return <<<CODE
194
195
    /**
196
     * {@inheritDoc}
197
     */
198 6
    public function {$method->name}($parametersString){$this->getMethodReturnType($method)}
199
    {
200
        \$this->initialize();
201
        if (\$this->needsSchedulingForDirtyCheck()) {
202
            \$this->changed();
203
        }
204 6
        return \$this->coll->{$method->name}($callParamsString);
205
    }
206
207
CODE;
208
    }
209
210 6
    private function buildParametersString(ReflectionMethod $method) : string
211
    {
212 6
        $parameters           = $method->getParameters();
213 6
        $parameterDefinitions = [];
214
215
        /** @var ReflectionParameter $param */
216 6
        foreach ($parameters as $param) {
217 6
            $parameterDefinition = '';
218 6
            $parameterType       = $this->getParameterType($param);
219
220 6
            if ($parameterType) {
221 6
                $parameterDefinition .= $parameterType . ' ';
222
            }
223
224 6
            if ($param->isPassedByReference()) {
225
                $parameterDefinition .= '&';
226
            }
227
228 6
            if (method_exists($param, 'isVariadic')) {
229 6
                if ($param->isVariadic()) {
230
                    $parameterDefinition .= '...';
231
                }
232
            }
233
234 6
            $parameters[]         = '$' . $param->name;
235 6
            $parameterDefinition .= '$' . $param->name;
236
237 6
            if ($param->isDefaultValueAvailable()) {
238
                $parameterDefinition .= ' = ' . var_export($param->getDefaultValue(), true);
239
            }
240
241 6
            $parameterDefinitions[] = $parameterDefinition;
242
        }
243
244 6
        return implode(', ', $parameterDefinitions);
245
    }
246
247 6
    private function getParameterType(ReflectionParameter $parameter) : ?string
248
    {
249
        // We need to pick the type hint class too
250 6
        if ($parameter->isArray()) {
251
            return 'array';
252
        }
253
254 6
        if (method_exists($parameter, 'isCallable') && $parameter->isCallable()) {
255
            return 'callable';
256
        }
257
258
        try {
259 6
            $parameterClass = $parameter->getClass();
260
261 6
            if ($parameterClass) {
262 6
                return '\\' . $parameterClass->name;
263
            }
264
        } catch (ReflectionException $previous) {
265
            // @todo ProxyGenerator throws specialized exceptions
266
            throw $previous;
267
        }
268
269 2
        return null;
270
    }
271
272
    /**
273
     * @param ReflectionParameter[] $parameters
274
     *
275
     * @return string[]
276
     */
277 6
    private function getParameterNamesForDecoratedCall(array $parameters) : array
278
    {
279 6
        return array_map(
280
            static function (ReflectionParameter $parameter) {
281 6
                $name = '';
282
283 6
                if (method_exists($parameter, 'isVariadic')) {
284 6
                    if ($parameter->isVariadic()) {
285
                        $name .= '...';
286
                    }
287
                }
288
289 6
                return $name . '$' . $parameter->name;
290 6
            },
291 6
            $parameters
292
        );
293
    }
294
295 6
    private function getMethodReturnType(ReflectionMethod $method) : string
296
    {
297 6
        $returnType = $method->getReturnType();
298 6
        if ($returnType === null) {
299 6
            return '';
300
        }
301
302 2
        return ': ' . $this->formatType($returnType, $method);
303
    }
304
305 2
    private function formatType(
306
        ReflectionType $type,
307
        ReflectionMethod $method,
308
        ?ReflectionParameter $parameter = null
309
    ) : string {
310 2
        $name      = method_exists($type, 'getName') ? $type->getName() : (string) $type;
311 2
        $nameLower = strtolower($name);
312 2
        if ($nameLower === 'self') {
313
            $name = $method->getDeclaringClass()->getName();
314
        }
315 2
        if ($nameLower === 'parent') {
316
            $parentClass = $method->getDeclaringClass()->getParentClass();
317
            if (! $parentClass) {
318
                throw PersistentCollectionException::parentClassRequired($method->getDeclaringClass()->getName(), $method->getName());
0 ignored issues
show
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
319
            }
320
321
            $name = $parentClass->getName();
322
        }
323 2
        if (! $type->isBuiltin() && ! class_exists($name) && ! interface_exists($name)) {
324
            if ($parameter !== null) {
325
                throw PersistentCollectionException::invalidParameterTypeHint(
326
                    $method->getDeclaringClass()->getName(),
327
                    $method->getName(),
328
                    $parameter->getName()
329
                );
330
            }
331
            throw PersistentCollectionException::invalidReturnTypeHint(
332
                $method->getDeclaringClass()->getName(),
333
                $method->getName()
334
            );
335
        }
336 2
        if (! $type->isBuiltin()) {
337 2
            $name = '\\' . $name;
338
        }
339 2
        if ($type->allowsNull()
340 2
            && ($parameter === null || ! $parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== null)
341
        ) {
342 1
            $name = '?' . $name;
343
        }
344 2
        return $name;
345
    }
346
}
347