Completed
Pull Request — master (#1263)
by Andreas
10:33
created

DocumentGenerator::generateInheritanceAnnotation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625
Metric Value
dl 0
loc 6
ccs 3
cts 4
cp 0.75
rs 9.4286
cc 2
eloc 3
nc 2
nop 1
crap 2.0625
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 MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Tools;
21
22
use Doctrine\Common\Inflector\Inflector;
23
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
24
use Doctrine\ODM\MongoDB\Types\Type;
25
26
/**
27
 * Generic class used to generate PHP5 document classes from ClassMetadataInfo instances
28
 *
29
 *     [php]
30
 *     $classes = $dm->getClassMetadataInfoFactory()->getAllMetadata();
31
 *
32
 *     $generator = new \Doctrine\ODM\MongoDB\Tools\DocumentGenerator();
33
 *     $generator->setGenerateAnnotations(true);
34
 *     $generator->setGenerateStubMethods(true);
35
 *     $generator->setRegenerateDocumentIfExists(false);
36
 *     $generator->setUpdateDocumentIfExists(true);
37
 *     $generator->generate($classes, '/path/to/generate/documents');
38
 *
39
 * @since   1.0
40
 * @author  Benjamin Eberlei <[email protected]>
41
 * @author  Guilherme Blanco <[email protected]>
42
 * @author  Jonathan Wage <[email protected]>
43
 * @author  Roman Borschel <[email protected]>
44
 */
45
class DocumentGenerator
46
{
47
    /**
48
     * @var bool
49
     */
50
    private $backupExisting = true;
51
52
    /** The extension to use for written php files */
53
    private $extension = '.php';
54
55
    /** Whether or not the current ClassMetadataInfo instance is new or old */
56
    private $isNew = true;
57
58
    private $staticReflection = array();
59
60
    /** Number of spaces to use for indention in generated code */
61
    private $numSpaces = 4;
62
63
    /** The actual spaces to use for indention */
64
    private $spaces = '    ';
65
66
    /** The class all generated documents should extend */
67
    private $classToExtend;
68
69
    /** Whether or not to generate annotations */
70
    private $generateAnnotations = false;
71
72
    /** Whether or not to generate stub methods */
73
    private $generateDocumentStubMethods = false;
74
75
    /** Whether or not to update the document class if it exists already */
76
    private $updateDocumentIfExists = false;
77
78
    /** Whether or not to re-generate document class if it exists already */
79
    private $regenerateDocumentIfExists = false;
80
81
    private static $classTemplate =
82
'<?php
83
84
<namespace>
85
86
<imports>
87
88
<documentAnnotation>
89
<documentClassName>
90
{
91
<documentBody>
92
}';
93
94
    private static $getMethodTemplate =
95
'/**
96
 * <description>
97
 *
98
 * @return <variableType>$<variableName>
99
 */
100
public function <methodName>()
101
{
102
<spaces>return $this-><fieldName>;
103
}';
104
105
    private static $setMethodTemplate =
106
'/**
107
 * <description>
108
 *
109
 * @param <variableType>$<variableName>
110
 * @return self
111
 */
112
public function <methodName>(<methodTypeHint>$<variableName><variableDefault>)
113
{
114
<spaces>$this-><fieldName> = $<variableName>;
115
<spaces>return $this;
116
}';
117
118
    private static $addMethodTemplate =
119
'/**
120
 * <description>
121
 *
122
 * @param <variableType>$<variableName>
123
 */
124
public function <methodName>(<methodTypeHint>$<variableName>)
125
{
126
<spaces>$this-><fieldName>[] = $<variableName>;
127
}';
128
129
    private static $removeMethodTemplate =
130
'/**
131
 * <description>
132
 *
133
 * @param <variableType>$<variableName>
134
 */
135
public function <methodName>(<methodTypeHint>$<variableName>)
136
{
137
<spaces>$this-><fieldName>->removeElement($<variableName>);
138
}';
139
140
    private static $lifecycleCallbackMethodTemplate =
141
'<comment>
142
public function <methodName>()
143
{
144
<spaces>// Add your code here
145
}';
146
147
    private static $constructorMethodTemplate =
148
'public function __construct()
149
{
150
<collections>
151
}
152
';
153
154
    /**
155
     * Generate and write document classes for the given array of ClassMetadataInfo instances
156
     *
157
     * @param array $metadatas
158
     * @param string $outputDirectory
159
     * @return void
160
     */
161
    public function generate(array $metadatas, $outputDirectory)
162
    {
163
        foreach ($metadatas as $metadata) {
164
            $this->writeDocumentClass($metadata, $outputDirectory);
165
        }
166
    }
167
168
    /**
169
     * Generated and write document class to disk for the given ClassMetadataInfo instance
170
     *
171
     * @param ClassMetadataInfo $metadata
172
     * @param string $outputDirectory
173
     * @throws \RuntimeException
174
     * @return void
175
     */
176 9
    public function writeDocumentClass(ClassMetadataInfo $metadata, $outputDirectory)
177
    {
178 9
        $path = $outputDirectory . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $metadata->name) . $this->extension;
179 9
        $dir = dirname($path);
180
181 9
        if ( ! is_dir($dir)) {
182
            mkdir($dir, 0775, true);
183
        }
184
185 9
        $this->isNew = ! file_exists($path) || (file_exists($path) && $this->regenerateDocumentIfExists);
186
187 9
        if ( ! $this->isNew) {
188 2
            $this->parseTokensInDocumentFile($path);
189 2
        }
190
191 9
        if ($this->backupExisting && file_exists($path)) {
192 2
            $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . '~';
193 2
            if ( ! copy($path, $backupPath)) {
194
                throw new \RuntimeException("Attempt to backup overwritten document file but copy operation failed.");
195
            }
196 2
        }
197
198
        // If document doesn't exist or we're re-generating the documents entirely
199 9
        if ($this->isNew) {
200 8
            file_put_contents($path, $this->generateDocumentClass($metadata));
201
202
        // If document exists and we're allowed to update the document class
203 9
        } elseif ( ! $this->isNew && $this->updateDocumentIfExists) {
204 2
            file_put_contents($path, $this->generateUpdatedDocumentClass($metadata, $path));
205 2
        }
206 9
        chmod($path, 0664);
207 9
    }
208
209
    /**
210
     * Generate a PHP5 Doctrine 2 document class from the given ClassMetadataInfo instance
211
     *
212
     * @param ClassMetadataInfo $metadata
213
     * @return string $code
214
     */
215 8
    public function generateDocumentClass(ClassMetadataInfo $metadata)
216
    {
217
        $placeHolders = array(
218 8
            '<namespace>',
219 8
            '<imports>',
220 8
            '<documentAnnotation>',
221 8
            '<documentClassName>',
222
            '<documentBody>'
223 8
        );
224
225
        $replacements = array(
226 8
            $this->generateDocumentNamespace($metadata),
227 8
            $this->generateDocumentImports($metadata),
0 ignored issues
show
Unused Code introduced by
The call to DocumentGenerator::generateDocumentImports() has too many arguments starting with $metadata.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
228 8
            $this->generateDocumentDocBlock($metadata),
229 8
            $this->generateDocumentClassName($metadata),
230 8
            $this->generateDocumentBody($metadata)
231 8
        );
232
233 8
        $code = str_replace($placeHolders, $replacements, self::$classTemplate);
234 8
        return str_replace('<spaces>', $this->spaces, $code);
235
    }
236
237
    /**
238
     * Generate the updated code for the given ClassMetadataInfo and document at path
239
     *
240
     * @param ClassMetadataInfo $metadata
241
     * @param string $path
242
     * @return string $code;
243
     */
244 2
    public function generateUpdatedDocumentClass(ClassMetadataInfo $metadata, $path)
245
    {
246 2
        $currentCode = file_get_contents($path);
247
248 2
        $body = $this->generateDocumentBody($metadata);
249 2
        $body = str_replace('<spaces>', $this->spaces, $body);
250 2
        $last = strrpos($currentCode, '}');
251
252 2
        return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : '') . "}\n";
253
    }
254
255
    /**
256
     * Set the number of spaces the exported class should have
257
     *
258
     * @param integer $numSpaces
259
     * @return void
260
     */
261
    public function setNumSpaces($numSpaces)
262
    {
263
        $this->spaces = str_repeat(' ', $numSpaces);
264
        $this->numSpaces = $numSpaces;
265
    }
266
267
    /**
268
     * Set the extension to use when writing php files to disk
269
     *
270
     * @param string $extension
271
     * @return void
272
     */
273
    public function setExtension($extension)
274
    {
275
        $this->extension = $extension;
276
    }
277
278
    /**
279
     * Set the name of the class the generated classes should extend from
280
     *
281
     * @return void
282
     */
283 1
    public function setClassToExtend($classToExtend)
284
    {
285 1
        $this->classToExtend = $classToExtend;
286 1
    }
287
288
    /**
289
     * Set whether or not to generate annotations for the document
290
     *
291
     * @param bool $bool
292
     * @return void
293
     */
294 9
    public function setGenerateAnnotations($bool)
295
    {
296 9
        $this->generateAnnotations = $bool;
297 9
    }
298
299
    /**
300
     * Set whether or not to try and update the document if it already exists
301
     *
302
     * @param bool $bool
303
     * @return void
304
     */
305 9
    public function setUpdateDocumentIfExists($bool)
306
    {
307 9
        $this->updateDocumentIfExists = $bool;
308 9
    }
309
310
    /**
311
     * Set whether or not to regenerate the document if it exists
312
     *
313
     * @param bool $bool
314
     * @return void
315
     */
316 9
    public function setRegenerateDocumentIfExists($bool)
317
    {
318 9
        $this->regenerateDocumentIfExists = $bool;
319 9
    }
320
321
    /**
322
     * Set whether or not to generate stub methods for the document
323
     *
324
     * @param bool $bool
325
     * @return void
326
     */
327 9
    public function setGenerateStubMethods($bool)
328
    {
329 9
        $this->generateDocumentStubMethods = $bool;
330 9
    }
331
332
    /**
333
     * Should an existing document be backed up if it already exists?
334
     */
335
    public function setBackupExisting($bool)
336
    {
337
        $this->backupExisting = $bool;
338
    }
339
340 8
    private function generateDocumentNamespace(ClassMetadataInfo $metadata)
341
    {
342 8
        if ($this->hasNamespace($metadata)) {
343 8
            return 'namespace ' . $this->getNamespace($metadata) . ';';
344
        }
345
    }
346
347 8
    private function generateDocumentClassName(ClassMetadataInfo $metadata)
348
    {
349 8
        return 'class ' . $this->getClassName($metadata) .
350 8
            ($this->extendsClass() ? ' extends ' . $this->getClassToExtendName() : null);
351
    }
352
353 9
    private function generateDocumentBody(ClassMetadataInfo $metadata)
354
    {
355 9
        $fieldMappingProperties = $this->generateDocumentFieldMappingProperties($metadata);
356 9
        $associationMappingProperties = $this->generateDocumentAssociationMappingProperties($metadata);
357 9
        $stubMethods = $this->generateDocumentStubMethods ? $this->generateDocumentStubMethods($metadata) : null;
358 9
        $lifecycleCallbackMethods = $this->generateDocumentLifecycleCallbackMethods($metadata);
359
360 9
        $code = array();
361
362 9
        if ($fieldMappingProperties) {
363 7
            $code[] = $fieldMappingProperties;
364 7
        }
365
366 9
        if ($associationMappingProperties) {
367 6
            $code[] = $associationMappingProperties;
368 6
        }
369
370 9
        $code[] = $this->generateDocumentConstructor($metadata);
371
372 9
        if ($stubMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stubMethods 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...
373 9
            $code[] = $stubMethods;
374 9
        }
375
376 9
        if ($lifecycleCallbackMethods) {
377 6
            $code[] = "\n" . $lifecycleCallbackMethods;
378 6
        }
379
380 9
        return implode("\n", $code);
381
    }
382
383 9
    private function generateDocumentConstructor(ClassMetadataInfo $metadata)
384
    {
385 9
        if ($this->hasMethod('__construct', $metadata)) {
386 1
            return '';
387
        }
388
389 9
        $collections = array();
390 9
        foreach ($metadata->fieldMappings AS $mapping) {
391 9
            if ($mapping['type'] === ClassMetadataInfo::MANY) {
392 6
                $collections[] = '$this->' . $mapping['fieldName'] . ' = new \Doctrine\Common\Collections\ArrayCollection();';
393 6
            }
394 9
        }
395 9
        if ($collections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
396 6
            return $this->prefixCodeWithSpaces(str_replace("<collections>", $this->spaces . implode("\n" . $this->spaces, $collections), self::$constructorMethodTemplate));
397
        }
398 3
        return '';
399
    }
400
401
    /**
402
     * @todo this won't work if there is a namespace in brackets and a class outside of it.
403
     * @param string $path
404
     */
405 2
    private function parseTokensInDocumentFile($path)
406
    {
407 2
        $tokens = token_get_all(file_get_contents($path));
408 2
        $lastSeenNamespace = '';
409 2
        $lastSeenClass = false;
410
411 2
        for ($i = 0; $i < count($tokens); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
412 2
            $token = $tokens[$i];
413 2
            if ($token[0] == T_NAMESPACE) {
414 2
                $peek = $i;
415 2
                $lastSeenNamespace = '';
416 2
                while (isset($tokens[++$peek])) {
417 2
                    if (';' == $tokens[$peek]) {
418 2
                        break;
419 2
                    } elseif (is_array($tokens[$peek]) && in_array($tokens[$peek][0], array(T_STRING, T_NS_SEPARATOR))) {
420 2
                        $lastSeenNamespace .= $tokens[$peek][1];
421 2
                    }
422 2
                }
423 2
            } elseif ($token[0] == T_CLASS) {
424 2
                $lastSeenClass = $lastSeenNamespace . '\\' . $tokens[$i + 2][1];
425 2
                $this->staticReflection[$lastSeenClass]['properties'] = array();
426 2
                $this->staticReflection[$lastSeenClass]['methods'] = array();
427 2
            } elseif ($token[0] == T_FUNCTION) {
428 1
                if ($tokens[$i + 2][0] == T_STRING) {
429 1
                    $this->staticReflection[$lastSeenClass]['methods'][] = $tokens[$i + 2][1];
430 1
                } elseif ($tokens[$i + 2][0] == '&' && $tokens[$i + 3][0] == T_STRING) {
431
                    $this->staticReflection[$lastSeenClass]['methods'][] = $tokens[$i + 3][1];
432
                }
433 2
            } elseif (in_array($token[0], array(T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED)) && $tokens[$i + 2][0] != T_FUNCTION) {
434 2
                $this->staticReflection[$lastSeenClass]['properties'][] = substr($tokens[$i + 2][1], 1);
435 2
            }
436 2
        }
437 2
    }
438
439 9 View Code Duplication
    private function hasProperty($property, ClassMetadataInfo $metadata)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
440
    {
441 9
        if ($this->extendsClass() || class_exists($metadata->name)) {
442
            // don't generate property if its already on the base class.
443 2
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
444
445 2
            if ($reflClass->hasProperty($property)) {
446 1
                return true;
447
            }
448 1
        }
449
450 8
        foreach ($this->getTraits($metadata) as $trait) {
451 2
            if ($trait->hasProperty($property)) {
452 2
                return true;
453
            }
454 8
        }
455
456
        return (
457 8
            isset($this->staticReflection[$metadata->name]) &&
458 1
            in_array($property, $this->staticReflection[$metadata->name]['properties'])
459 8
        );
460
    }
461
462 9 View Code Duplication
    private function hasMethod($method, ClassMetadataInfo $metadata)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
463
    {
464 9
        if ($this->extendsClass() || class_exists($metadata->name)) {
465
            // don't generate method if its already on the base class.
466 2
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
467
468 2
            if ($reflClass->hasMethod($method)) {
469 1
                return true;
470
            }
471 2
        }
472
473 9
        foreach ($this->getTraits($metadata) as $trait) {
474 2
            if ($trait->hasMethod($method)) {
475 2
                return true;
476
            }
477 9
        }
478
479
        return (
480 9
            isset($this->staticReflection[$metadata->name]) &&
481 2
            in_array($method, $this->staticReflection[$metadata->name]['methods'])
482 9
        );
483
    }
484
485 8
    private function hasNamespace(ClassMetadataInfo $metadata)
486
    {
487 8
        return strpos($metadata->name, '\\') ? true : false;
488
    }
489
490 9
    private function extendsClass()
491
    {
492 9
        return $this->classToExtend ? true : false;
493
    }
494
495 2
    private function getClassToExtend()
496
    {
497 2
        return $this->classToExtend;
498
    }
499
500 1
    private function getClassToExtendName()
501
    {
502 1
        $refl = new \ReflectionClass($this->getClassToExtend());
503
504 1
        return '\\' . $refl->getName();
0 ignored issues
show
Bug introduced by
Consider using $refl->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
505
    }
506
507 8
    private function getClassName(ClassMetadataInfo $metadata)
508
    {
509 8
        return ($pos = strrpos($metadata->name, '\\'))
510 8
            ? substr($metadata->name, $pos + 1, strlen($metadata->name)) : $metadata->name;
511
    }
512
513 8
    private function getNamespace(ClassMetadataInfo $metadata)
514
    {
515 8
        return substr($metadata->name, 0, strrpos($metadata->name, '\\'));
516
    }
517
518
    /**
519
     * @param ClassMetadataInfo $metadata
520
     *
521
     * @return array
522
     */
523 9
    protected function getTraits(ClassMetadataInfo $metadata)
524
    {
525 9
        if ($metadata->reflClass !== null || class_exists($metadata->name)) {
526 3
            $reflClass = $metadata->reflClass === null ? new \ReflectionClass($metadata->name) : $metadata->reflClass;
527 3
            $traits = array();
528 3
            while ($reflClass !== false) {
529 3
                $traits = array_merge($traits, $reflClass->getTraits());
530 3
                $reflClass = $reflClass->getParentClass();
531 3
            }
532 3
            return $traits;
533
        }
534 6
        return array();
535
    }
536
537 8
    private function generateDocumentImports()
538
    {
539 8
        if ($this->generateAnnotations) {
540 8
            return 'use Doctrine\\ODM\\MongoDB\\Mapping\\Annotations as ODM;';
541
        }
542
    }
543
544 8
    private function generateDocumentDocBlock(ClassMetadataInfo $metadata)
545
    {
546 8
        $lines = array();
547 8
        $lines[] = '/**';
548 8
        $lines[] = ' * ' . $metadata->name;
549
550 8
        if ($this->generateAnnotations) {
551 8
            $lines[] = ' *';
552
553 8
            if ($metadata->isMappedSuperclass) {
554
                $lines[] = ' * @ODM\\MappedSuperclass';
555 8
            } elseif ($metadata->isEmbeddedDocument) {
556
                $lines[] = ' * @ODM\\EmbeddedDocument';
557 8
            } elseif ($metadata->isAggregationResultDocument) {
558
                $lines[] = ' * @ODM\\AggregationResultDocument';
559
            } else {
560 8
                $lines[] = ' * @ODM\\Document';
561
            }
562
563 8
            $document = array();
564 8
            if ( ! $metadata->isMappedSuperclass && ! $metadata->isEmbeddedDocument && ! $metadata->isAggregationResultDocument) {
565 8
                if ($metadata->collection) {
566 8
                    $document[] = ' *     collection="' . $metadata->collection . '"';
567 8
                }
568 8
                if ($metadata->customRepositoryClassName) {
569 6
                    $document[] = ' *     repositoryClass="' . $metadata->customRepositoryClassName . '"';
570 6
                }
571 8
            }
572 8
            if ($metadata->indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->indexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
573
                $indexes = array();
574
                $indexLines = array();
575
                $indexLines[] = " *     indexes={";
576
                foreach ($metadata->indexes as $index) {
577
                    $keys = array();
578 View Code Duplication
                    foreach ($index['keys'] as $key => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
579
                        $keys[] = '"' . $key . '"="' . $value . '"';
580
                    }
581
                    $options = array();
582 View Code Duplication
                    foreach ($index['options'] as $key => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
583
                        $options[] = '"' . $key . '"="' . $value . '"';
584
                    }
585
                    $indexes[] = '@ODM\\Index(keys={' . implode(', ', $keys) . '}, options={' . implode(', ', $options) . '})';
586
                }
587
                $indexLines[] = "\n *         " . implode(",\n *         ", $indexes);
588
                $indexLines[] = "\n *     }";
589
590
                $document[] = implode(null, $indexLines);
591
            }
592
593 8
            if ($document) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $document of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
594 8
                $lines[count($lines) - 1] .= '(';
595 8
                $lines[] = implode(",\n", $document);
596 8
                $lines[] = ' * )';
597 8
            }
598
599 8
            if ( ! empty($metadata->lifecycleCallbacks)) {
600 6
                $lines[] = ' * @ODM\HasLifecycleCallbacks';
601 6
            }
602
603
            $methods = array(
604 8
                'generateInheritanceAnnotation',
605 8
                'generateDiscriminatorFieldAnnotation',
606 8
                'generateDiscriminatorMapAnnotation',
607 8
                'generateDefaultDiscriminatorValueAnnotation',
608
                'generateChangeTrackingPolicyAnnotation'
609 8
            );
610
611 8
            foreach ($methods as $method) {
612 8
                if ($code = $this->$method($metadata)) {
613 8
                    $lines[] = ' * ' . $code;
614 8
                }
615 8
            }
616 8
        }
617
618 8
        $lines[] = ' */';
619 8
        return implode("\n", $lines);
620
    }
621
622 8
    private function generateInheritanceAnnotation(ClassMetadataInfo $metadata)
623
    {
624 8
        if ($metadata->inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
625
            return '@ODM\\InheritanceType("' . $this->getInheritanceTypeString($metadata->inheritanceType) . '")';
626
        }
627 8
    }
628
629 8
    private function generateDiscriminatorFieldAnnotation(ClassMetadataInfo $metadata)
630
    {
631 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
632
            return '@ODM\\DiscriminatorField(name="' . $metadata->discriminatorField . '")';
633
        }
634 8
    }
635
636 8
    private function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata)
637
    {
638 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
639
            $inheritanceClassMap = array();
640
641
            foreach ($metadata->discriminatorMap as $type => $class) {
642
                $inheritanceClassMap[] .= '"' . $type . '" = "' . $class . '"';
643
            }
644
645
            return '@ODM\\DiscriminatorMap({' . implode(', ', $inheritanceClassMap) . '})';
646
        }
647 8
    }
648
649 8
    private function generateDefaultDiscriminatorValueAnnotation(ClassMetadataInfo $metadata)
650
    {
651 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION && isset($metadata->defaultDiscriminatorValue)) {
652
            return '@ODM\\DefaultDiscriminatorValue("' . $metadata->defaultDiscriminatorValue . '")';
653
        }
654 8
    }
655
656 8
    private function generateChangeTrackingPolicyAnnotation(ClassMetadataInfo $metadata)
657
    {
658 8
        return '@ODM\\ChangeTrackingPolicy("' . $this->getChangeTrackingPolicyString($metadata->changeTrackingPolicy) . '")';
659
    }
660
661 9
    private function generateDocumentStubMethods(ClassMetadataInfo $metadata)
662
    {
663 9
        $methods = array();
664
665 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
666 9
            if (isset($fieldMapping['id'])) {
667 9
                if ($metadata->generatorType == ClassMetadataInfo::GENERATOR_TYPE_NONE) {
668
                    if ($code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) {
669
                        $methods[] = $code;
670
                    }
671
                }
672 9
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) {
673 8
                    $methods[] = $code;
674 8
                }
675 9
            } elseif ( ! isset($fieldMapping['association'])) {
676 9
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) {
677 9
                    $methods[] = $code;
678 9
                }
679 9
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) {
680 9
                    $methods[] = $code;
681 9
                }
682 9
            } elseif ($fieldMapping['type'] === ClassMetadataInfo::ONE) {
683 8
                $nullable = $this->isAssociationNullable($fieldMapping) ? 'null' : null;
684 8 View Code Duplication
                if ($code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], isset($fieldMapping['targetDocument']) ? $fieldMapping['targetDocument'] : null, $nullable)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
685 6
                    $methods[] = $code;
686 6
                }
687 8 View Code Duplication
                if ($code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], isset($fieldMapping['targetDocument']) ? $fieldMapping['targetDocument'] : null)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
688 6
                    $methods[] = $code;
689 6
                }
690 8
            } elseif ($fieldMapping['type'] === ClassMetadataInfo::MANY) {
691 6 View Code Duplication
                if ($code = $this->generateDocumentStubMethod($metadata, 'add', $fieldMapping['fieldName'], isset($fieldMapping['targetDocument']) ? $fieldMapping['targetDocument'] : null)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
692 6
                    $methods[] = $code;
693 6
                }
694 6 View Code Duplication
                if ($code = $this->generateDocumentStubMethod($metadata, 'remove', $fieldMapping['fieldName'], isset($fieldMapping['targetDocument']) ? $fieldMapping['targetDocument'] : null)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
695 6
                    $methods[] = $code;
696 6
                }
697 6
                if ($code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], '\Doctrine\Common\Collections\Collection')) {
698 6
                    $methods[] = $code;
699 6
                }
700 6
            }
701 9
        }
702
703 9
        return implode("\n\n", $methods);
704
    }
705
706
    /**
707
     * @param array $fieldMapping
708
     *
709
     * @return bool
710
     */
711 8
    protected function isAssociationNullable($fieldMapping)
712
    {
713 8
        return isset($fieldMapping['nullable']) && $fieldMapping['nullable'];
714
    }
715
716 9
    private function generateDocumentLifecycleCallbackMethods(ClassMetadataInfo $metadata)
717
    {
718 9
        if (empty($metadata->lifecycleCallbacks)) {
719 3
            return '';
720
        }
721
722 6
        $methods = array();
723
724 6
        foreach ($metadata->lifecycleCallbacks as $event => $callbacks) {
725 6
            foreach ($callbacks as $callback) {
726 6
                if ($code = $this->generateLifecycleCallbackMethod($event, $callback, $metadata)) {
727 6
                    $methods[] = $code;
728 6
                }
729 6
            }
730 6
        }
731
732 6
        return implode("\n\n", $methods);
733
    }
734
735 9
    private function generateDocumentAssociationMappingProperties(ClassMetadataInfo $metadata)
736
    {
737 9
        $lines = array();
738
739 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
740 9
            if ($this->hasProperty($fieldMapping['fieldName'], $metadata) ||
741 9
                $metadata->isInheritedField($fieldMapping['fieldName'])) {
742 4
                continue;
743
            }
744 7
            if ( ! isset($fieldMapping['association'])) {
745 7
                continue;
746
            }
747
748 6
            $lines[] = $this->generateAssociationMappingPropertyDocBlock($fieldMapping, $metadata);
0 ignored issues
show
Unused Code introduced by
The call to DocumentGenerator::gener...ppingPropertyDocBlock() has too many arguments starting with $metadata.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
749 6
            $lines[] = $this->spaces . 'protected $' . $fieldMapping['fieldName']
750 6
                . ($fieldMapping['type'] === ClassMetadataInfo::MANY ? ' = array()' : null) . ";\n";
751 9
        }
752
753 9
        return implode("\n", $lines);
754
    }
755
756 9
    private function generateDocumentFieldMappingProperties(ClassMetadataInfo $metadata)
757
    {
758 9
        $lines = array();
759
760 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
761 9
            if ($this->hasProperty($fieldMapping['fieldName'], $metadata) ||
762 9
                $metadata->isInheritedField($fieldMapping['fieldName'])) {
763 4
                continue;
764
            }
765 7
            if (isset($fieldMapping['association']) && $fieldMapping['association']) {
766 6
                continue;
767
            }
768
769 7
            $lines[] = $this->generateFieldMappingPropertyDocBlock($fieldMapping, $metadata);
770 7
            $lines[] = $this->spaces . 'protected $' . $fieldMapping['fieldName']
771 7
                . (isset($fieldMapping['default']) ? ' = ' . var_export($fieldMapping['default'], true) : null) . ";\n";
772 9
        }
773
774 9
        return implode("\n", $lines);
775
    }
776
777 9
    private function generateDocumentStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null)
778
    {
779
        // Add/remove methods should use the singular form of the field name
780 9
        $formattedFieldName = in_array($type, array('add', 'remove'))
781 9
            ? Inflector::singularize($fieldName)
782 9
            : $fieldName;
783
784 9
        $methodName = $type . Inflector::classify($formattedFieldName);
785 9
        $variableName = Inflector::camelize($formattedFieldName);
786
787 9
        if ($this->hasMethod($methodName, $metadata)) {
788 4
            return;
789
        }
790
791 9
        $description = ucfirst($type) . ' ' . $variableName;
792
793 9
        $types = Type::getTypesMap();
794 9
        $methodTypeHint = $typeHint && ! isset($types[$typeHint]) ? '\\' . $typeHint . ' ' : null;
795 9
        $variableType = $typeHint ? $typeHint . ' ' : null;
796
797
        $replacements = array(
798 9
            '<description>'         => $description,
799 9
            '<methodTypeHint>'      => $methodTypeHint,
800 9
            '<variableType>'        => $variableType,
801 9
            '<variableName>'        => $variableName,
802 9
            '<methodName>'          => $methodName,
803 9
            '<fieldName>'           => $fieldName,
804 9
            '<variableDefault>'     => ($defaultValue !== null ) ? (' = ' . $defaultValue) : '',
805 9
        );
806
807 9
        $templateVar = sprintf('%sMethodTemplate', $type);
808
809 9
        $method = str_replace(
810 9
            array_keys($replacements),
811 9
            array_values($replacements),
812
            self::$$templateVar
813 9
        );
814
815 9
        return $this->prefixCodeWithSpaces($method);
816
    }
817
818 6
    private function generateLifecycleCallbackMethod($name, $methodName, ClassMetadataInfo $metadata)
819
    {
820 6
        if ($this->hasMethod($methodName, $metadata)) {
821 1
            return;
822
        }
823
824
        $replacements = array(
825 6
            '<comment>'    => $this->generateAnnotations ? '/** @ODM\\' . ucfirst($name) . ' */' : '',
826 6
            '<methodName>' => $methodName,
827 6
        );
828
829 6
        $method = str_replace(
830 6
            array_keys($replacements),
831 6
            array_values($replacements),
832
            self::$lifecycleCallbackMethodTemplate
833 6
        );
834
835 6
        return $this->prefixCodeWithSpaces($method);
836
    }
837
838 6
    private function generateAssociationMappingPropertyDocBlock(array $fieldMapping)
839
    {
840 6
        $lines = array();
841 6
        $lines[] = $this->spaces . '/**';
842 6
        $lines[] = $this->spaces . ' * @var ' . (isset($fieldMapping['targetDocument']) ? $fieldMapping['targetDocument'] : 'object');
843
844 6
        if ($this->generateAnnotations) {
845 6
            $lines[] = $this->spaces . ' *';
846
847 6
            $type = null;
848 6
            switch ($fieldMapping['association']) {
849 6
                case ClassMetadataInfo::EMBED_ONE:
850
                    $type = 'EmbedOne';
851
                    break;
852 6
                case ClassMetadataInfo::EMBED_MANY:
853
                    $type = 'EmbedMany';
854
                    break;
855 6
                case ClassMetadataInfo::REFERENCE_ONE:
856 6
                    $type = 'ReferenceOne';
857 6
                    break;
858 6
                case ClassMetadataInfo::REFERENCE_MANY:
859 6
                    $type = 'ReferenceMany';
860 6
                    break;
861 6
            }
862 6
            $typeOptions = array();
863
864 6
            if (isset($fieldMapping['targetDocument'])) {
865 6
                $typeOptions[] = 'targetDocument="' . $fieldMapping['targetDocument'] . '"';
866 6
            }
867
868 6
            if (isset($fieldMapping['cascade']) && $fieldMapping['cascade']) {
869
                $cascades = array();
870
871
                if ($fieldMapping['isCascadePersist']) $cascades[] = '"persist"';
872
                if ($fieldMapping['isCascadeRemove']) $cascades[] = '"remove"';
873
                if ($fieldMapping['isCascadeDetach']) $cascades[] = '"detach"';
874
                if ($fieldMapping['isCascadeMerge']) $cascades[] = '"merge"';
875
                if ($fieldMapping['isCascadeRefresh']) $cascades[] = '"refresh"';
876
877
                $typeOptions[] = 'cascade={' . implode(',', $cascades) . '}';
878
            }
879
880 6
            $lines[] = $this->spaces . ' * @ODM\\' . $type . '(' . implode(', ', $typeOptions) . ')';
881 6
        }
882
883 6
        $lines[] = $this->spaces . ' */';
884
885 6
        return implode("\n", $lines);
886
    }
887
888 7
    private function generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata)
889
    {
890 7
        $lines = array();
891 7
        $lines[] = $this->spaces . '/**';
892 7
        if (isset($fieldMapping['id']) && $fieldMapping['id']) {
893 7
            $fieldMapping['strategy'] = isset($fieldMapping['strategy']) ? $fieldMapping['strategy'] : ClassMetadataInfo::GENERATOR_TYPE_AUTO;
894 7
            if ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_AUTO) {
895 6
                $lines[] = $this->spaces . ' * @var MongoId $' . $fieldMapping['fieldName'];
896 7 View Code Duplication
            } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_INCREMENT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
897
                $lines[] = $this->spaces . ' * @var integer $' . $fieldMapping['fieldName'];
898 1
            } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_UUID) {
899
                $lines[] = $this->spaces . ' * @var string $' . $fieldMapping['fieldName'];
900 1 View Code Duplication
            } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_NONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
901
                $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName'];
902
            } else {
903 1
                $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName'];
904
            }
905 7
        } else {
906 7
            $lines[] = $this->spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName'];
907
        }
908
909 7
        if ($this->generateAnnotations) {
910 7
            $lines[] = $this->spaces . ' *';
911
912 7
            $field = array();
913 7
            if (isset($fieldMapping['id']) && $fieldMapping['id']) {
914 7
                if (isset($fieldMapping['strategy'])) {
915 7
                    $field[] = 'strategy="' . $this->getIdGeneratorTypeString($metadata->generatorType) . '"';
916 7
                }
917 7
                $lines[] = $this->spaces . ' * @ODM\\Id(' . implode(', ', $field) . ')';
918 7
            } else {
919 7
                if (isset($fieldMapping['name'])) {
920 7
                    $field[] = 'name="' . $fieldMapping['name'] . '"';
921 7
                }
922
923 7
                if (isset($fieldMapping['type'])) {
924 7
                    $field[] = 'type="' . $fieldMapping['type'] . '"';
925 7
                }
926
927 7
                if (isset($fieldMapping['nullable']) && $fieldMapping['nullable'] === true) {
928
                    $field[] = 'nullable=' . var_export($fieldMapping['nullable'], true);
929
                }
930 7
                if (isset($fieldMapping['options'])) {
931 1
                    $options = array();
932 1
                    foreach ($fieldMapping['options'] as $key => $value) {
933
                        $options[] = '"' . $key . '" = "' . $value . '"';
934 1
                    }
935 1
                    $field[] = "options={" . implode(', ', $options) . "}";
936 1
                }
937 7
                $lines[] = $this->spaces . ' * @ODM\\Field(' . implode(', ', $field) . ')';
938
            }
939
940 7
            if (isset($fieldMapping['version']) && $fieldMapping['version']) {
941
                $lines[] = $this->spaces . ' * @ODM\\Version';
942
            }
943 7
        }
944
945 7
        $lines[] = $this->spaces . ' */';
946
947 7
        return implode("\n", $lines);
948
    }
949
950 9
    private function prefixCodeWithSpaces($code, $num = 1)
951
    {
952 9
        $lines = explode("\n", $code);
953
954 9
        foreach ($lines as $key => $value) {
955 9
            $lines[$key] = str_repeat($this->spaces, $num) . $lines[$key];
956 9
        }
957
958 9
        return implode("\n", $lines);
959
    }
960
961
    private function getInheritanceTypeString($type)
962
    {
963
        switch ($type) {
964
            case ClassMetadataInfo::INHERITANCE_TYPE_NONE:
965
                return 'NONE';
966
967
            case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION:
968
                return 'SINGLE_COLLECTION';
969
970
            case ClassMetadataInfo::INHERITANCE_TYPE_COLLECTION_PER_CLASS:
971
                return 'COLLECTION_PER_CLASS';
972
973
            default:
974
                throw new \InvalidArgumentException('Invalid provided InheritanceType: ' . $type);
975
        }
976
    }
977
978 8
    private function getChangeTrackingPolicyString($policy)
979
    {
980
        switch ($policy) {
981 8
            case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT:
982 8
                return 'DEFERRED_IMPLICIT';
983
984
            case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT:
985
                return 'DEFERRED_EXPLICIT';
986
987
            case ClassMetadataInfo::CHANGETRACKING_NOTIFY:
988
                return 'NOTIFY';
989
990
            default:
991
                throw new \InvalidArgumentException('Invalid provided ChangeTrackingPolicy: ' . $policy);
992
        }
993
    }
994
995 7
    private function getIdGeneratorTypeString($type)
996
    {
997
        switch ($type) {
998 7
            case ClassMetadataInfo::GENERATOR_TYPE_AUTO:
999 1
                return 'AUTO';
1000
1001 6
            case ClassMetadataInfo::GENERATOR_TYPE_INCREMENT:
1002
                return 'INCREMENT';
1003
1004 6
            case ClassMetadataInfo::GENERATOR_TYPE_UUID:
1005
                return 'UUID';
1006
1007 6
            case ClassMetadataInfo::GENERATOR_TYPE_ALNUM:
1008
                return 'ALNUM';
1009
1010 6
            case ClassMetadataInfo::GENERATOR_TYPE_CUSTOM:
1011 6
                return 'CUSTOM';
1012
1013
            case ClassMetadataInfo::GENERATOR_TYPE_NONE:
1014
                return 'NONE';
1015
1016
            default:
1017
                throw new \InvalidArgumentException('Invalid provided IdGeneratorType: ' . $type);
1018
        }
1019
    }
1020
}
1021