Completed
Pull Request — master (#1787)
by Stefano
21:31
created

DefaultPersistentCollectionGenerator::loadClass()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 12.8936

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 14
cts 22
cp 0.6364
rs 7.7404
c 0
b 0
f 0
cc 9
nc 9
nop 2
crap 12.8936
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\PersistentCollection;
6
7
use Doctrine\ODM\MongoDB\Configuration;
8
use const DIRECTORY_SEPARATOR;
9
use function array_map;
10
use function array_pop;
11
use function class_exists;
12
use function dirname;
13
use function explode;
14
use function file_exists;
15
use function file_put_contents;
16
use function implode;
17
use function interface_exists;
18
use function is_dir;
19
use function is_writable;
20
use function method_exists;
21
use function mkdir;
22
use function rename;
23
use function str_replace;
24
use function strtolower;
25
use function substr;
26
use function uniqid;
27
use function var_export;
28
29
/**
30
 * Default generator for custom PersistentCollection classes.
31
 *
32
 */
33
final class DefaultPersistentCollectionGenerator implements PersistentCollectionGenerator
34
{
35
    /**
36
     * The namespace that contains all persistent collection classes.
37
     *
38
     * @var string
39
     */
40
    private $collectionNamespace;
41
42
    /**
43
     * The directory that contains all persistent collection classes.
44
     *
45
     * @var string
46
     */
47
    private $collectionDir;
48
49
    /**
50
     * @param string $collectionDir
51
     * @param string $collectionNs
52
     */
53 11
    public function __construct($collectionDir, $collectionNs)
54
    {
55 11
        $this->collectionDir = $collectionDir;
56 11
        $this->collectionNamespace = $collectionNs;
57 11
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function generateClass($class, $dir)
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($collectionClass, $autoGenerate)
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 6
    private function generateCollectionClass($for, $targetFqcn, $fileName)
114
    {
115 6
        $exploded = explode('\\', $targetFqcn);
116 6
        $class = array_pop($exploded);
117 6
        $namespace = implode('\\', $exploded);
118
        $code = <<<CODE
119
<?php
120
121 6
namespace $namespace;
122
123
use Doctrine\Common\Collections\Collection as BaseCollection;
124
use Doctrine\ODM\MongoDB\DocumentManager;
125
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
126
use Doctrine\ODM\MongoDB\MongoDBException;
127
use Doctrine\ODM\MongoDB\UnitOfWork;
128
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
129
130
/**
131
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE\'S PERSISTENT COLLECTION GENERATOR
132
 */
133 6
class $class extends \\$for implements \\Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface
134
{
135
    use \\Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionTrait;
136
137
    /**
138
     * @param BaseCollection \$coll
139
     * @param DocumentManager \$dm
140
     * @param UnitOfWork \$uow
141
     */
142
    public function __construct(BaseCollection \$coll, DocumentManager \$dm, UnitOfWork \$uow)
143
    {
144
        \$this->coll = \$coll;
145
        \$this->dm = \$dm;
146
        \$this->uow = \$uow;
147
    }
148
149
CODE;
150 6
        $rc = new \ReflectionClass($for);
151 6
        $rt = new \ReflectionClass('Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionTrait');
152 6
        foreach ($rc->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
153 6
            if ($rt->hasMethod($method->name) ||
154 6
                $method->isConstructor() ||
155 6
                $method->isFinal() ||
156 6
                $method->isStatic()
157
            ) {
158 6
                continue;
159
            }
160 6
            $code .= $this->generateMethod($method);
161
        }
162 6
        $code .= "}\n";
163
164 6
        if ($fileName === false) {
165 3
            if (! class_exists($targetFqcn)) {
166 3
                eval(substr($code, 5));
167
            }
168
        } else {
169 3
            $parentDirectory = dirname($fileName);
170
171 3
            if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
172
                throw PersistentCollectionException::directoryNotWritable();
173
            }
174
175 3
            if (! is_writable($parentDirectory)) {
176
                throw PersistentCollectionException::directoryNotWritable();
177
            }
178
179 3
            $tmpFileName = $fileName . '.' . uniqid('', true);
180 3
            file_put_contents($tmpFileName, $code);
181 3
            rename($tmpFileName, $fileName);
182
        }
183 6
    }
184
185 6
    private function generateMethod(\ReflectionMethod $method)
186
    {
187 6
        $parametersString = $this->buildParametersString($method);
188 6
        $callParamsString = implode(', ', $this->getParameterNamesForDecoratedCall($method->getParameters()));
189
190
        $method = <<<CODE
191
192
    /**
193
     * {@inheritDoc}
194
     */
195 6
    public function {$method->name}($parametersString){$this->getMethodReturnType($method)}
196
    {
197
        \$this->initialize();
198
        if (\$this->needsSchedulingForDirtyCheck()) {
199
            \$this->changed();
200
        }
201 6
        return \$this->coll->{$method->name}($callParamsString);
202
    }
203
204
CODE;
205 6
        return $method;
206
    }
207
208
    /**
209
     *
210
     * @return string
211
     */
212 6
    private function buildParametersString(\ReflectionMethod $method)
213
    {
214 6
        $parameters = $method->getParameters();
215 6
        $parameterDefinitions = [];
216
217
        /** @var \ReflectionParameter $param */
218 6
        foreach ($parameters as $param) {
219 6
            $parameterDefinition = '';
220 6
            $parameterType = $this->getParameterType($param);
0 ignored issues
show
Bug introduced by
It seems like $param defined by $param on line 218 can also be of type string; however, Doctrine\ODM\MongoDB\Per...tor::getParameterType() does only seem to accept object<ReflectionParameter>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
221
222 6
            if ($parameterType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parameterType of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
223 6
                $parameterDefinition .= $parameterType . ' ';
224
            }
225
226 6
            if ($param->isPassedByReference()) {
227
                $parameterDefinition .= '&';
228
            }
229
230 6
            if (method_exists($param, 'isVariadic')) {
231 6
                if ($param->isVariadic()) {
232
                    $parameterDefinition .= '...';
233
                }
234
            }
235
236 6
            $parameters[]     = '$' . $param->name;
237 6
            $parameterDefinition .= '$' . $param->name;
238
239 6
            if ($param->isDefaultValueAvailable()) {
240
                $parameterDefinition .= ' = ' . var_export($param->getDefaultValue(), true);
241
            }
242
243 6
            $parameterDefinitions[] = $parameterDefinition;
244
        }
245
246 6
        return implode(', ', $parameterDefinitions);
247
    }
248
249
    /**
250
     *
251
     * @return string|null
252
     */
253 6
    private function getParameterType(\ReflectionParameter $parameter)
254
    {
255
        // We need to pick the type hint class too
256 6
        if ($parameter->isArray()) {
257
            return 'array';
258
        }
259
260 6
        if (method_exists($parameter, 'isCallable') && $parameter->isCallable()) {
261
            return 'callable';
262
        }
263
264
        try {
265 6
            $parameterClass = $parameter->getClass();
266
267 6
            if ($parameterClass) {
268 6
                return '\\' . $parameterClass->name;
269
            }
270
        } catch (\ReflectionException $previous) {
271
            // @todo ProxyGenerator throws specialized exceptions
272
            throw $previous;
273
        }
274
275 2
        return null;
276
    }
277
278
    /**
279
     * @param \ReflectionParameter[] $parameters
280
     *
281
     * @return string[]
282
     */
283 6
    private function getParameterNamesForDecoratedCall(array $parameters)
284
    {
285 6
        return array_map(
286
            function (\ReflectionParameter $parameter) {
287 6
                $name = '';
288
289 6
                if (method_exists($parameter, 'isVariadic')) {
290 6
                    if ($parameter->isVariadic()) {
291
                        $name .= '...';
292
                    }
293
                }
294
295 6
                $name .= '$' . $parameter->name;
296
297 6
                return $name;
298 6
            },
299 6
            $parameters
300
        );
301
    }
302
303
    /**
304
     *
305
     * @return string
306
     *
307
     * @see \Doctrine\Common\Proxy\ProxyGenerator::getMethodReturnType()
308
     */
309 6
    private function getMethodReturnType(\ReflectionMethod $method)
310
    {
311 6
        if (! method_exists($method, 'hasReturnType') || ! $method->hasReturnType()) {
312 6
            return '';
313
        }
314 2
        return ': ' . $this->formatType($method->getReturnType(), $method);
315
    }
316
317
    /**
318
     *
319
     * @return string
320
     *
321
     * @see \Doctrine\Common\Proxy\ProxyGenerator::formatType()
322
     */
323 2
    private function formatType(
324
        \ReflectionType $type,
325
        \ReflectionMethod $method,
326
        ?\ReflectionParameter $parameter = null
327
    ) {
328 2
        $name = method_exists($type, 'getName') ? $type->getName() : (string) $type;
329 2
        $nameLower = strtolower($name);
330 2
        if ($nameLower === 'self') {
331
            $name = $method->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
332
        }
333 2
        if ($nameLower === 'parent') {
334
            $name = $method->getDeclaringClass()->getParentClass()->getName();
335
        }
336 2
        if (! $type->isBuiltin() && ! class_exists($name) && ! interface_exists($name)) {
337
            if ($parameter !== null) {
338
                throw PersistentCollectionException::invalidParameterTypeHint(
339
                    $method->getDeclaringClass()->getName(),
0 ignored issues
show
introduced by
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
340
                    $method->getName(),
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
341
                    $parameter->getName()
342
                );
343
            }
344
            throw PersistentCollectionException::invalidReturnTypeHint(
345
                $method->getDeclaringClass()->getName(),
0 ignored issues
show
introduced by
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
346
                $method->getName()
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
347
            );
348
        }
349 2
        if (! $type->isBuiltin()) {
350 2
            $name = '\\' . $name;
351
        }
352 2
        if ($type->allowsNull()
353 2
            && ($parameter === null || ! $parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== null)
354
        ) {
355 1
            $name = '?' . $name;
356
        }
357 2
        return $name;
358
    }
359
}
360