Completed
Pull Request — master (#1263)
by Andreas
13:28
created

DocumentGenerator::setRegenerateDocumentIfExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
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
 */
41
class DocumentGenerator
42
{
43
    /**
44
     * @var bool
45
     */
46
    private $backupExisting = true;
47
48
    /** The extension to use for written php files */
49
    private $extension = '.php';
50
51
    /** Whether or not the current ClassMetadataInfo instance is new or old */
52
    private $isNew = true;
53
54
    private $staticReflection = array();
55
56
    /** Number of spaces to use for indention in generated code */
57
    private $numSpaces = 4;
58
59
    /** The actual spaces to use for indention */
60
    private $spaces = '    ';
61
62
    /** The class all generated documents should extend */
63
    private $classToExtend;
64
65
    /** Whether or not to generate annotations */
66
    private $generateAnnotations = false;
67
68
    /** Whether or not to generate stub methods */
69
    private $generateDocumentStubMethods = false;
70
71
    /** Whether or not to update the document class if it exists already */
72
    private $updateDocumentIfExists = false;
73
74
    /** Whether or not to re-generate document class if it exists already */
75
    private $regenerateDocumentIfExists = false;
76
77
    private static $classTemplate =
78
'<?php
79
80
<namespace>
81
82
<imports>
83
84
<documentAnnotation>
85
<documentClassName>
86
{
87
<documentBody>
88
}';
89
90
    private static $getMethodTemplate =
91
'/**
92
 * <description>
93
 *
94
 * @return <variableType>$<variableName>
95
 */
96
public function <methodName>()
97
{
98
<spaces>return $this-><fieldName>;
99
}';
100
101
    private static $setMethodTemplate =
102
'/**
103
 * <description>
104
 *
105
 * @param <variableType>$<variableName>
106
 * @return $this
107
 */
108
public function <methodName>(<methodTypeHint>$<variableName><variableDefault>)
109
{
110
<spaces>$this-><fieldName> = $<variableName>;
111
<spaces>return $this;
112
}';
113
114
    private static $addMethodTemplate =
115
'/**
116
 * <description>
117
 *
118
 * @param <variableType>$<variableName>
119
 */
120
public function <methodName>(<methodTypeHint>$<variableName>)
121
{
122
<spaces>$this-><fieldName>[] = $<variableName>;
123
}';
124
125
    private static $removeMethodTemplate =
126
'/**
127
 * <description>
128
 *
129
 * @param <variableType>$<variableName>
130
 */
131
public function <methodName>(<methodTypeHint>$<variableName>)
132
{
133
<spaces>$this-><fieldName>->removeElement($<variableName>);
134
}';
135
136
    private static $lifecycleCallbackMethodTemplate =
137
'<comment>
138
public function <methodName>()
139
{
140
<spaces>// Add your code here
141
}';
142
143
    private static $constructorMethodTemplate =
144
'public function __construct()
145
{
146
<collections>
147
}
148
';
149
150
    /**
151
     * Generate and write document classes for the given array of ClassMetadataInfo instances
152
     *
153
     * @param array $metadatas
154
     * @param string $outputDirectory
155
     * @return void
156
     */
157
    public function generate(array $metadatas, $outputDirectory)
158
    {
159
        foreach ($metadatas as $metadata) {
160
            $this->writeDocumentClass($metadata, $outputDirectory);
161
        }
162
    }
163
164
    /**
165
     * Generated and write document class to disk for the given ClassMetadataInfo instance
166
     *
167
     * @param ClassMetadataInfo $metadata
168
     * @param string $outputDirectory
169
     * @throws \RuntimeException
170
     * @return void
171
     */
172 9
    public function writeDocumentClass(ClassMetadataInfo $metadata, $outputDirectory)
173
    {
174 9
        $path = $outputDirectory . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $metadata->name) . $this->extension;
175 9
        $dir = dirname($path);
176
177 9
        if ( ! is_dir($dir)) {
178
            mkdir($dir, 0775, true);
179
        }
180
181 9
        $this->isNew = ! file_exists($path) || (file_exists($path) && $this->regenerateDocumentIfExists);
182
183 9
        if ( ! $this->isNew) {
184 2
            $this->parseTokensInDocumentFile($path);
185
        }
186
187 9
        if ($this->backupExisting && file_exists($path)) {
188 2
            $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . '~';
189 2
            if ( ! copy($path, $backupPath)) {
190
                throw new \RuntimeException('Attempt to backup overwritten document file but copy operation failed.');
191
            }
192
        }
193
194
        // If document doesn't exist or we're re-generating the documents entirely
195 9
        if ($this->isNew) {
196 8
            file_put_contents($path, $this->generateDocumentClass($metadata));
197
198
        // If document exists and we're allowed to update the document class
199 2
        } elseif ( ! $this->isNew && $this->updateDocumentIfExists) {
200 2
            file_put_contents($path, $this->generateUpdatedDocumentClass($metadata, $path));
201
        }
202 9
        chmod($path, 0664);
203 9
    }
204
205
    /**
206
     * Generate a PHP5 Doctrine 2 document class from the given ClassMetadataInfo instance
207
     *
208
     * @param ClassMetadataInfo $metadata
209
     * @return string $code
210
     */
211 8
    public function generateDocumentClass(ClassMetadataInfo $metadata)
212
    {
213
        $placeHolders = array(
214 8
            '<namespace>',
215
            '<imports>',
216
            '<documentAnnotation>',
217
            '<documentClassName>',
218
            '<documentBody>'
219
        );
220
221
        $replacements = array(
222 8
            $this->generateDocumentNamespace($metadata),
223 8
            $this->generateDocumentImports(),
224 8
            $this->generateDocumentDocBlock($metadata),
225 8
            $this->generateDocumentClassName($metadata),
226 8
            $this->generateDocumentBody($metadata)
227
        );
228
229 8
        $code = str_replace($placeHolders, $replacements, self::$classTemplate);
230 8
        return str_replace('<spaces>', $this->spaces, $code);
231
    }
232
233
    /**
234
     * Generate the updated code for the given ClassMetadataInfo and document at path
235
     *
236
     * @param ClassMetadataInfo $metadata
237
     * @param string $path
238
     * @return string $code;
239
     */
240 2
    public function generateUpdatedDocumentClass(ClassMetadataInfo $metadata, $path)
241
    {
242 2
        $currentCode = file_get_contents($path);
243
244 2
        $body = $this->generateDocumentBody($metadata);
245 2
        $body = str_replace('<spaces>', $this->spaces, $body);
246 2
        $last = strrpos($currentCode, '}');
247
248 2
        return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : '') . "}\n";
249
    }
250
251
    /**
252
     * Set the number of spaces the exported class should have
253
     *
254
     * @param integer $numSpaces
255
     * @return void
256
     */
257
    public function setNumSpaces($numSpaces)
258
    {
259
        $this->spaces = str_repeat(' ', $numSpaces);
260
        $this->numSpaces = $numSpaces;
261
    }
262
263
    /**
264
     * Set the extension to use when writing php files to disk
265
     *
266
     * @param string $extension
267
     * @return void
268
     */
269
    public function setExtension($extension)
270
    {
271
        $this->extension = $extension;
272
    }
273
274
    /**
275
     * Set the name of the class the generated classes should extend from
276
     *
277
     * @param string $classToExtend Class name.
278
     * @return void
279
     */
280 1
    public function setClassToExtend($classToExtend)
281
    {
282 1
        $this->classToExtend = $classToExtend;
283 1
    }
284
285
    /**
286
     * Set whether or not to generate annotations for the document
287
     *
288
     * @param bool $bool
289
     * @return void
290
     */
291 9
    public function setGenerateAnnotations($bool)
292
    {
293 9
        $this->generateAnnotations = $bool;
294 9
    }
295
296
    /**
297
     * Set whether or not to try and update the document if it already exists
298
     *
299
     * @param bool $bool
300
     * @return void
301
     */
302 9
    public function setUpdateDocumentIfExists($bool)
303
    {
304 9
        $this->updateDocumentIfExists = $bool;
305 9
    }
306
307
    /**
308
     * Set whether or not to regenerate the document if it exists
309
     *
310
     * @param bool $bool
311
     * @return void
312
     */
313 9
    public function setRegenerateDocumentIfExists($bool)
314
    {
315 9
        $this->regenerateDocumentIfExists = $bool;
316 9
    }
317
318
    /**
319
     * Set whether or not to generate stub methods for the document
320
     *
321
     * @param bool $bool
322
     * @return void
323
     */
324 9
    public function setGenerateStubMethods($bool)
325
    {
326 9
        $this->generateDocumentStubMethods = $bool;
327 9
    }
328
329
    /**
330
     * Sets a value indicating whether existing documents will be backed up.
331
     *
332
     * @param bool $bool True to backup existing document, false to overwrite.
333
     */
334
    public function setBackupExisting($bool)
335
    {
336
        $this->backupExisting = $bool;
337
    }
338
339 8
    private function generateDocumentNamespace(ClassMetadataInfo $metadata)
340
    {
341 8
        if ($this->hasNamespace($metadata)) {
342 8
            return 'namespace ' . $this->getNamespace($metadata) . ';';
343
        }
344
    }
345
346 8
    private function generateDocumentClassName(ClassMetadataInfo $metadata)
347
    {
348 8
        return 'class ' . $this->getClassName($metadata) .
349 8
            ($this->extendsClass() ? ' extends ' . $this->getClassToExtendName() : null);
350
    }
351
352 9
    private function generateDocumentBody(ClassMetadataInfo $metadata)
353
    {
354 9
        $fieldMappingProperties = $this->generateDocumentFieldMappingProperties($metadata);
355 9
        $associationMappingProperties = $this->generateDocumentAssociationMappingProperties($metadata);
356 9
        $stubMethods = $this->generateDocumentStubMethods ? $this->generateDocumentStubMethods($metadata) : null;
357 9
        $lifecycleCallbackMethods = $this->generateDocumentLifecycleCallbackMethods($metadata);
358
359 9
        $code = array();
360
361 9
        if ($fieldMappingProperties) {
362 7
            $code[] = $fieldMappingProperties;
363
        }
364
365 9
        if ($associationMappingProperties) {
366 6
            $code[] = $associationMappingProperties;
367
        }
368
369 9
        $code[] = $this->generateDocumentConstructor($metadata);
370
371 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...
372 9
            $code[] = $stubMethods;
373
        }
374
375 9
        if ($lifecycleCallbackMethods) {
376 6
            $code[] = "\n" . $lifecycleCallbackMethods;
377
        }
378
379 9
        return implode("\n", $code);
380
    }
381
382 9
    private function generateDocumentConstructor(ClassMetadataInfo $metadata)
383
    {
384 9
        if ($this->hasMethod('__construct', $metadata)) {
385 1
            return '';
386
        }
387
388 9
        $collections = array();
389 9
        foreach ($metadata->fieldMappings AS $mapping) {
390 9
            if ($mapping['type'] === ClassMetadataInfo::MANY) {
391 9
                $collections[] = '$this->' . $mapping['fieldName'] . ' = new \Doctrine\Common\Collections\ArrayCollection();';
392
            }
393
        }
394 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...
395 6
            return $this->prefixCodeWithSpaces(str_replace('<collections>', $this->spaces . implode("\n" . $this->spaces, $collections), self::$constructorMethodTemplate));
396
        }
397 3
        return '';
398
    }
399
400
    /**
401
     * @todo this won't work if there is a namespace in brackets and a class outside of it.
402
     * @param string $path
403
     */
404 2
    private function parseTokensInDocumentFile($path)
405
    {
406 2
        $tokens = token_get_all(file_get_contents($path));
407 2
        $lastSeenNamespace = '';
408 2
        $lastSeenClass = false;
409
410 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...
411 2
            $token = $tokens[$i];
412 2
            if ($token[0] == T_NAMESPACE) {
413 2
                $peek = $i;
414 2
                $lastSeenNamespace = '';
415 2
                while (isset($tokens[++$peek])) {
416 2
                    if (';' == $tokens[$peek]) {
417 2
                        break;
418 2
                    } elseif (is_array($tokens[$peek]) && in_array($tokens[$peek][0], array(T_STRING, T_NS_SEPARATOR))) {
419 2
                        $lastSeenNamespace .= $tokens[$peek][1];
420
                    }
421
                }
422 2
            } elseif ($token[0] == T_CLASS) {
423 2
                $lastSeenClass = $lastSeenNamespace . '\\' . $tokens[$i + 2][1];
424 2
                $this->staticReflection[$lastSeenClass]['properties'] = array();
425 2
                $this->staticReflection[$lastSeenClass]['methods'] = array();
426 2
            } elseif ($token[0] == T_FUNCTION) {
427 1
                if ($tokens[$i + 2][0] == T_STRING) {
428 1
                    $this->staticReflection[$lastSeenClass]['methods'][] = $tokens[$i + 2][1];
429
                } elseif ($tokens[$i + 2][0] == '&' && $tokens[$i + 3][0] == T_STRING) {
430 1
                    $this->staticReflection[$lastSeenClass]['methods'][] = $tokens[$i + 3][1];
431
                }
432 2
            } elseif (in_array($token[0], array(T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED)) && $tokens[$i + 2][0] != T_FUNCTION) {
433 2
                $this->staticReflection[$lastSeenClass]['properties'][] = substr($tokens[$i + 2][1], 1);
434
            }
435
        }
436 2
    }
437
438 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...
439
    {
440 9
        if ($this->extendsClass() || class_exists($metadata->name)) {
441
            // don't generate property if its already on the base class.
442 2
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
443
444 2
            if ($reflClass->hasProperty($property)) {
445 1
                return true;
446
            }
447
        }
448
449 8
        foreach ($this->getTraits($metadata) as $trait) {
450 2
            if ($trait->hasProperty($property)) {
451 2
                return true;
452
            }
453
        }
454
455
        return (
456 8
            isset($this->staticReflection[$metadata->name]) &&
457 8
            in_array($property, $this->staticReflection[$metadata->name]['properties'])
458
        );
459
    }
460
461 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...
462
    {
463 9
        if ($this->extendsClass() || class_exists($metadata->name)) {
464
            // don't generate method if its already on the base class.
465 2
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
466
467 2
            if ($reflClass->hasMethod($method)) {
468 1
                return true;
469
            }
470
        }
471
472 9
        foreach ($this->getTraits($metadata) as $trait) {
473 2
            if ($trait->hasMethod($method)) {
474 2
                return true;
475
            }
476
        }
477
478
        return (
479 9
            isset($this->staticReflection[$metadata->name]) &&
480 9
            in_array($method, $this->staticReflection[$metadata->name]['methods'])
481
        );
482
    }
483
484 8
    private function hasNamespace(ClassMetadataInfo $metadata)
485
    {
486 8
        return strpos($metadata->name, '\\') ? true : false;
487
    }
488
489 9
    private function extendsClass()
490
    {
491 9
        return $this->classToExtend ? true : false;
492
    }
493
494 2
    private function getClassToExtend()
495
    {
496 2
        return $this->classToExtend;
497
    }
498
499 1
    private function getClassToExtendName()
500
    {
501 1
        $refl = new \ReflectionClass($this->getClassToExtend());
502
503 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...
504
    }
505
506 8
    private function getClassName(ClassMetadataInfo $metadata)
507
    {
508 8
        return ($pos = strrpos($metadata->name, '\\'))
509 8
            ? substr($metadata->name, $pos + 1, strlen($metadata->name)) : $metadata->name;
510
    }
511
512 8
    private function getNamespace(ClassMetadataInfo $metadata)
513
    {
514 8
        return substr($metadata->name, 0, strrpos($metadata->name, '\\'));
515
    }
516
517
    /**
518
     * @param ClassMetadataInfo $metadata
519
     *
520
     * @return array
521
     */
522 9
    protected function getTraits(ClassMetadataInfo $metadata)
523
    {
524 9
        if ($metadata->reflClass !== null || class_exists($metadata->name)) {
525 3
            $reflClass = $metadata->reflClass === null ? new \ReflectionClass($metadata->name) : $metadata->reflClass;
526 3
            $traits = array();
527 3
            while ($reflClass !== false) {
528 3
                $traits = array_merge($traits, $reflClass->getTraits());
529 3
                $reflClass = $reflClass->getParentClass();
530
            }
531 3
            return $traits;
532
        }
533 6
        return array();
534
    }
535
536 8
    private function generateDocumentImports()
537
    {
538 8
        if ($this->generateAnnotations) {
539 8
            return 'use Doctrine\\ODM\\MongoDB\\Mapping\\Annotations as ODM;';
540
        }
541
    }
542
543 8
    private function generateDocumentDocBlock(ClassMetadataInfo $metadata)
544
    {
545 8
        $lines = array();
546 8
        $lines[] = '/**';
547 8
        $lines[] = ' * ' . $metadata->name;
548
549 8
        if ($this->generateAnnotations) {
550 8
            $lines[] = ' *';
551
552 8
            if ($metadata->isMappedSuperclass) {
553
                $lines[] = ' * @ODM\\MappedSuperclass';
554 8
            } elseif ($metadata->isEmbeddedDocument) {
555
                $lines[] = ' * @ODM\\EmbeddedDocument';
556 8
            } elseif ($metadata->isAggregationResultDocument) {
557
                $lines[] = ' * @ODM\\AggregationResultDocument';
558
            } else {
559 8
                $lines[] = ' * @ODM\\Document';
560
            }
561
562 8
            $document = array();
563 8
            if ( ! $metadata->isMappedSuperclass && ! $metadata->isEmbeddedDocument && ! $metadata->isAggregationResultDocument) {
564 8
                if ($metadata->collection) {
565 8
                    $document[] = ' *     collection="' . $metadata->collection . '"';
566
                }
567 8
                if ($metadata->customRepositoryClassName) {
568 6
                    $document[] = ' *     repositoryClass="' . $metadata->customRepositoryClassName . '"';
569
                }
570
            }
571 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...
572
                $indexes = array();
573
                $indexLines = array();
574
                $indexLines[] = ' *     indexes={';
575
                foreach ($metadata->indexes as $index) {
576
                    $keys = array();
577 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...
578
                        $keys[] = '"' . $key . '"="' . $value . '"';
579
                    }
580
                    $options = array();
581 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...
582
                        $options[] = '"' . $key . '"="' . $value . '"';
583
                    }
584
                    $indexes[] = '@ODM\\Index(keys={' . implode(', ', $keys) . '}, options={' . implode(', ', $options) . '})';
585
                }
586
                $indexLines[] = "\n *         " . implode(",\n *         ", $indexes);
587
                $indexLines[] = "\n *     }";
588
589
                $document[] = implode(null, $indexLines);
590
            }
591
592 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...
593 8
                $lines[count($lines) - 1] .= '(';
594 8
                $lines[] = implode(",\n", $document);
595 8
                $lines[] = ' * )';
596
            }
597
598 8
            if ( ! empty($metadata->lifecycleCallbacks)) {
599 6
                $lines[] = ' * @ODM\HasLifecycleCallbacks';
600
            }
601
602
            $methods = array(
603 8
                'generateInheritanceAnnotation',
604
                'generateDiscriminatorFieldAnnotation',
605
                'generateDiscriminatorMapAnnotation',
606
                'generateDefaultDiscriminatorValueAnnotation',
607
                'generateChangeTrackingPolicyAnnotation'
608
            );
609
610 8
            foreach ($methods as $method) {
611 8
                if ($code = $this->$method($metadata)) {
612 8
                    $lines[] = ' * ' . $code;
613
                }
614
            }
615
        }
616
617 8
        $lines[] = ' */';
618 8
        return implode("\n", $lines);
619
    }
620
621 8
    private function generateInheritanceAnnotation(ClassMetadataInfo $metadata)
622
    {
623 8
        if ($metadata->inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
624
            return '@ODM\\InheritanceType("' . $this->getInheritanceTypeString($metadata->inheritanceType) . '")';
625
        }
626 8
    }
627
628 8
    private function generateDiscriminatorFieldAnnotation(ClassMetadataInfo $metadata)
629
    {
630 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
631
            return '@ODM\\DiscriminatorField(name="' . $metadata->discriminatorField . '")';
632
        }
633 8
    }
634
635 8
    private function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata)
636
    {
637 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
638
            $inheritanceClassMap = array();
639
640
            foreach ($metadata->discriminatorMap as $type => $class) {
641
                $inheritanceClassMap[] .= '"' . $type . '" = "' . $class . '"';
642
            }
643
644
            return '@ODM\\DiscriminatorMap({' . implode(', ', $inheritanceClassMap) . '})';
645
        }
646 8
    }
647
648 8
    private function generateDefaultDiscriminatorValueAnnotation(ClassMetadataInfo $metadata)
649
    {
650 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION && isset($metadata->defaultDiscriminatorValue)) {
651
            return '@ODM\\DefaultDiscriminatorValue("' . $metadata->defaultDiscriminatorValue . '")';
652
        }
653 8
    }
654
655 8
    private function generateChangeTrackingPolicyAnnotation(ClassMetadataInfo $metadata)
656
    {
657 8
        return '@ODM\\ChangeTrackingPolicy("' . $this->getChangeTrackingPolicyString($metadata->changeTrackingPolicy) . '")';
658
    }
659
660 9
    private function generateDocumentStubMethods(ClassMetadataInfo $metadata)
661
    {
662 9
        $methods = array();
663
664 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
665 9
            if (isset($fieldMapping['id'])) {
666 9
                if ($metadata->generatorType == ClassMetadataInfo::GENERATOR_TYPE_NONE) {
667
                    if ($code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) {
668
                        $methods[] = $code;
669
                    }
670
                }
671 9
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) {
672 9
                    $methods[] = $code;
673
                }
674 9
            } elseif ( ! isset($fieldMapping['association'])) {
675 9
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) {
676 9
                    $methods[] = $code;
677
                }
678 9
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) {
679 9
                    $methods[] = $code;
680
                }
681 8
            } elseif ($fieldMapping['type'] === ClassMetadataInfo::ONE) {
682 8
                $nullable = $this->isAssociationNullable($fieldMapping) ? 'null' : null;
683 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...
684 6
                    $methods[] = $code;
685
                }
686 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...
687 8
                    $methods[] = $code;
688
                }
689 6
            } elseif ($fieldMapping['type'] === ClassMetadataInfo::MANY) {
690 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...
691 6
                    $methods[] = $code;
692
                }
693 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...
694 6
                    $methods[] = $code;
695
                }
696 6
                if ($code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], '\Doctrine\Common\Collections\Collection')) {
697 9
                    $methods[] = $code;
698
                }
699
            }
700
        }
701
702 9
        return implode("\n\n", $methods);
703
    }
704
705
    /**
706
     * @param array $fieldMapping
707
     *
708
     * @return bool
709
     */
710 8
    protected function isAssociationNullable($fieldMapping)
711
    {
712 8
        return isset($fieldMapping['nullable']) && $fieldMapping['nullable'];
713
    }
714
715 9
    private function generateDocumentLifecycleCallbackMethods(ClassMetadataInfo $metadata)
716
    {
717 9
        if (empty($metadata->lifecycleCallbacks)) {
718 3
            return '';
719
        }
720
721 6
        $methods = array();
722
723 6
        foreach ($metadata->lifecycleCallbacks as $event => $callbacks) {
724 6
            foreach ($callbacks as $callback) {
725 6
                if ($code = $this->generateLifecycleCallbackMethod($event, $callback, $metadata)) {
726 6
                    $methods[] = $code;
727
                }
728
            }
729
        }
730
731 6
        return implode("\n\n", $methods);
732
    }
733
734 9
    private function generateDocumentAssociationMappingProperties(ClassMetadataInfo $metadata)
735
    {
736 9
        $lines = array();
737
738 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
739 9
            if ($this->hasProperty($fieldMapping['fieldName'], $metadata) ||
740 9
                $metadata->isInheritedField($fieldMapping['fieldName'])) {
741 4
                continue;
742
            }
743 7
            if ( ! isset($fieldMapping['association'])) {
744 7
                continue;
745
            }
746
747 6
            $lines[] = $this->generateAssociationMappingPropertyDocBlock($fieldMapping);
748 6
            $lines[] = $this->spaces . 'protected $' . $fieldMapping['fieldName']
749 6
                . ($fieldMapping['type'] === ClassMetadataInfo::MANY ? ' = array()' : null) . ";\n";
750
        }
751
752 9
        return implode("\n", $lines);
753
    }
754
755 9
    private function generateDocumentFieldMappingProperties(ClassMetadataInfo $metadata)
756
    {
757 9
        $lines = array();
758
759 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
760 9
            if ($this->hasProperty($fieldMapping['fieldName'], $metadata) ||
761 9
                $metadata->isInheritedField($fieldMapping['fieldName'])) {
762 4
                continue;
763
            }
764 7
            if (isset($fieldMapping['association']) && $fieldMapping['association']) {
765 6
                continue;
766
            }
767
768 7
            $lines[] = $this->generateFieldMappingPropertyDocBlock($fieldMapping, $metadata);
769 7
            $lines[] = $this->spaces . 'protected $' . $fieldMapping['fieldName']
770 7
                . (isset($fieldMapping['default']) ? ' = ' . var_export($fieldMapping['default'], true) : null) . ";\n";
771
        }
772
773 9
        return implode("\n", $lines);
774
    }
775
776 9
    private function generateDocumentStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null)
777
    {
778
        // Add/remove methods should use the singular form of the field name
779 9
        $formattedFieldName = in_array($type, array('add', 'remove'))
780 6
            ? Inflector::singularize($fieldName)
781 9
            : $fieldName;
782
783 9
        $methodName = $type . Inflector::classify($formattedFieldName);
784 9
        $variableName = Inflector::camelize($formattedFieldName);
785
786 9
        if ($this->hasMethod($methodName, $metadata)) {
787 4
            return;
788
        }
789
790 9
        $description = ucfirst($type) . ' ' . $variableName;
791
792 9
        $types = Type::getTypesMap();
793 9
        $methodTypeHint = $typeHint && ! isset($types[$typeHint]) ? '\\' . $typeHint . ' ' : null;
794 9
        $variableType = $typeHint ? $typeHint . ' ' : null;
795
796
        $replacements = array(
797 9
            '<description>'         => $description,
798 9
            '<methodTypeHint>'      => $methodTypeHint,
799 9
            '<variableType>'        => $variableType,
800 9
            '<variableName>'        => $variableName,
801 9
            '<methodName>'          => $methodName,
802 9
            '<fieldName>'           => $fieldName,
803 9
            '<variableDefault>'     => ($defaultValue !== null ) ? (' = ' . $defaultValue) : '',
804
        );
805
806 9
        $templateVar = sprintf('%sMethodTemplate', $type);
807
808 9
        $method = str_replace(
809
            array_keys($replacements),
810
            array_values($replacements),
811 9
            self::$$templateVar
812
        );
813
814 9
        return $this->prefixCodeWithSpaces($method);
815
    }
816
817 6
    private function generateLifecycleCallbackMethod($name, $methodName, ClassMetadataInfo $metadata)
818
    {
819 6
        if ($this->hasMethod($methodName, $metadata)) {
820 1
            return;
821
        }
822
823
        $replacements = array(
824 6
            '<comment>'    => $this->generateAnnotations ? '/** @ODM\\' . ucfirst($name) . ' */' : '',
825 6
            '<methodName>' => $methodName,
826
        );
827
828 6
        $method = str_replace(
829
            array_keys($replacements),
830
            array_values($replacements),
831 6
            self::$lifecycleCallbackMethodTemplate
832
        );
833
834 6
        return $this->prefixCodeWithSpaces($method);
835
    }
836
837 6
    private function generateAssociationMappingPropertyDocBlock(array $fieldMapping)
838
    {
839 6
        $lines = array();
840 6
        $lines[] = $this->spaces . '/**';
841 6
        $lines[] = $this->spaces . ' * @var ' . (isset($fieldMapping['targetDocument']) ? $fieldMapping['targetDocument'] : 'object');
842
843 6
        if ($this->generateAnnotations) {
844 6
            $lines[] = $this->spaces . ' *';
845
846 6
            $type = null;
847 6
            switch ($fieldMapping['association']) {
848 6
                case ClassMetadataInfo::EMBED_ONE:
849
                    $type = 'EmbedOne';
850
                    break;
851 6
                case ClassMetadataInfo::EMBED_MANY:
852
                    $type = 'EmbedMany';
853
                    break;
854 6
                case ClassMetadataInfo::REFERENCE_ONE:
855 6
                    $type = 'ReferenceOne';
856 6
                    break;
857 6
                case ClassMetadataInfo::REFERENCE_MANY:
858 6
                    $type = 'ReferenceMany';
859 6
                    break;
860
            }
861 6
            $typeOptions = array();
862
863 6
            if (isset($fieldMapping['targetDocument'])) {
864 6
                $typeOptions[] = 'targetDocument="' . $fieldMapping['targetDocument'] . '"';
865
            }
866
867 6
            if (isset($fieldMapping['cascade']) && $fieldMapping['cascade']) {
868
                $cascades = array();
869
870
                if ($fieldMapping['isCascadePersist']) $cascades[] = '"persist"';
871
                if ($fieldMapping['isCascadeRemove']) $cascades[] = '"remove"';
872
                if ($fieldMapping['isCascadeDetach']) $cascades[] = '"detach"';
873
                if ($fieldMapping['isCascadeMerge']) $cascades[] = '"merge"';
874
                if ($fieldMapping['isCascadeRefresh']) $cascades[] = '"refresh"';
875
876
                $typeOptions[] = 'cascade={' . implode(',', $cascades) . '}';
877
            }
878
879 6
            $lines[] = $this->spaces . ' * @ODM\\' . $type . '(' . implode(', ', $typeOptions) . ')';
880
        }
881
882 6
        $lines[] = $this->spaces . ' */';
883
884 6
        return implode("\n", $lines);
885
    }
886
887 7
    private function generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata)
888
    {
889 7
        $lines = array();
890 7
        $lines[] = $this->spaces . '/**';
891 7
        if (isset($fieldMapping['id']) && $fieldMapping['id']) {
892 7
            $fieldMapping['strategy'] = isset($fieldMapping['strategy']) ? $fieldMapping['strategy'] : ClassMetadataInfo::GENERATOR_TYPE_AUTO;
893 7
            if ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_AUTO) {
894 6
                $lines[] = $this->spaces . ' * @var MongoId $' . $fieldMapping['fieldName'];
895 1 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...
896
                $lines[] = $this->spaces . ' * @var integer $' . $fieldMapping['fieldName'];
897 1
            } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_UUID) {
898
                $lines[] = $this->spaces . ' * @var string $' . $fieldMapping['fieldName'];
899 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...
900
                $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName'];
901
            } else {
902 7
                $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName'];
903
            }
904
        } else {
905 7
            $lines[] = $this->spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName'];
906
        }
907
908 7
        if ($this->generateAnnotations) {
909 7
            $lines[] = $this->spaces . ' *';
910
911 7
            $field = array();
912 7
            if (isset($fieldMapping['id']) && $fieldMapping['id']) {
913 7
                if (isset($fieldMapping['strategy'])) {
914 7
                    $field[] = 'strategy="' . $this->getIdGeneratorTypeString($metadata->generatorType) . '"';
915
                }
916 7
                $lines[] = $this->spaces . ' * @ODM\\Id(' . implode(', ', $field) . ')';
917
            } else {
918 7
                if (isset($fieldMapping['name'])) {
919 7
                    $field[] = 'name="' . $fieldMapping['name'] . '"';
920
                }
921
922 7
                if (isset($fieldMapping['type'])) {
923 7
                    $field[] = 'type="' . $fieldMapping['type'] . '"';
924
                }
925
926 7
                if (isset($fieldMapping['nullable']) && $fieldMapping['nullable'] === true) {
927
                    $field[] = 'nullable=' . var_export($fieldMapping['nullable'], true);
928
                }
929 7
                if (isset($fieldMapping['options'])) {
930 1
                    $options = array();
931 1
                    foreach ($fieldMapping['options'] as $key => $value) {
932
                        $options[] = '"' . $key . '" = "' . $value . '"';
933
                    }
934 1
                    $field[] = 'options={' . implode(', ', $options) . '}';
935
                }
936 7
                $lines[] = $this->spaces . ' * @ODM\\Field(' . implode(', ', $field) . ')';
937
            }
938
939 7
            if (isset($fieldMapping['version']) && $fieldMapping['version']) {
940
                $lines[] = $this->spaces . ' * @ODM\\Version';
941
            }
942
        }
943
944 7
        $lines[] = $this->spaces . ' */';
945
946 7
        return implode("\n", $lines);
947
    }
948
949 9
    private function prefixCodeWithSpaces($code, $num = 1)
950
    {
951 9
        $lines = explode("\n", $code);
952
953 9
        foreach ($lines as $key => $value) {
954 9
            $lines[$key] = str_repeat($this->spaces, $num) . $lines[$key];
955
        }
956
957 9
        return implode("\n", $lines);
958
    }
959
960
    private function getInheritanceTypeString($type)
961
    {
962
        switch ($type) {
963
            case ClassMetadataInfo::INHERITANCE_TYPE_NONE:
964
                return 'NONE';
965
966
            case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION:
967
                return 'SINGLE_COLLECTION';
968
969
            case ClassMetadataInfo::INHERITANCE_TYPE_COLLECTION_PER_CLASS:
970
                return 'COLLECTION_PER_CLASS';
971
972
            default:
973
                throw new \InvalidArgumentException('Invalid provided InheritanceType: ' . $type);
974
        }
975
    }
976
977 8
    private function getChangeTrackingPolicyString($policy)
978
    {
979
        switch ($policy) {
980 8
            case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT:
981 8
                return 'DEFERRED_IMPLICIT';
982
983
            case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT:
984
                return 'DEFERRED_EXPLICIT';
985
986
            case ClassMetadataInfo::CHANGETRACKING_NOTIFY:
987
                return 'NOTIFY';
988
989
            default:
990
                throw new \InvalidArgumentException('Invalid provided ChangeTrackingPolicy: ' . $policy);
991
        }
992
    }
993
994 7
    private function getIdGeneratorTypeString($type)
995
    {
996
        switch ($type) {
997 7
            case ClassMetadataInfo::GENERATOR_TYPE_AUTO:
998 1
                return 'AUTO';
999
1000 6
            case ClassMetadataInfo::GENERATOR_TYPE_INCREMENT:
1001
                return 'INCREMENT';
1002
1003 6
            case ClassMetadataInfo::GENERATOR_TYPE_UUID:
1004
                return 'UUID';
1005
1006 6
            case ClassMetadataInfo::GENERATOR_TYPE_ALNUM:
1007
                return 'ALNUM';
1008
1009 6
            case ClassMetadataInfo::GENERATOR_TYPE_CUSTOM:
1010 6
                return 'CUSTOM';
1011
1012
            case ClassMetadataInfo::GENERATOR_TYPE_NONE:
1013
                return 'NONE';
1014
1015
            default:
1016
                throw new \InvalidArgumentException('Invalid provided IdGeneratorType: ' . $type);
1017
        }
1018
    }
1019
}
1020