Completed
Pull Request — master (#1749)
by Gabriel
11:34
created

generateMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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