Completed
Pull Request — master (#1523)
by
unknown
10:53
created

DocumentGenerator::generateDocumentClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 9
cts 9
cp 1
rs 9.3142
c 0
b 0
f 0
cc 1
eloc 15
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
91
    private static $getMethodTemplate =
92
'/**
93
 * <description>
94
 *
95
 * @return <variableType>$<variableName>
96
 */
97
public function <methodName>()
98
{
99
<spaces>return $this-><fieldName>;
100
}';
101
102
    private static $setMethodTemplate =
103
'/**
104
 * <description>
105
 *
106
 * @param <variableType>$<variableName>
107
 * @return $this
108
 */
109
public function <methodName>(<methodTypeHint>$<variableName><variableDefault>)
110
{
111
<spaces>$this-><fieldName> = $<variableName>;
112
<spaces>return $this;
113
}';
114
115
    private static $addMethodTemplate =
116
'/**
117
 * <description>
118
 *
119
 * @param <variableType>$<variableName>
120
 */
121
public function <methodName>(<methodTypeHint>$<variableName>)
122
{
123
<spaces>$this-><fieldName>[] = $<variableName>;
124
}';
125
126
    private static $removeMethodTemplate =
127
'/**
128
 * <description>
129
 *
130
 * @param <variableType>$<variableName>
131
 */
132
public function <methodName>(<methodTypeHint>$<variableName>)
133
{
134
<spaces>$this-><fieldName>->removeElement($<variableName>);
135
}';
136
137
    private static $lifecycleCallbackMethodTemplate =
138
'<comment>
139
public function <methodName>()
140
{
141
<spaces>// Add your code here
142
}';
143
144
    private static $constructorMethodTemplate =
145
'public function __construct()
146
{
147
<collections>
148
}
149
';
150
151
    /**
152
     * Generate and write document classes for the given array of ClassMetadataInfo instances
153
     *
154
     * @param array $metadatas
155
     * @param string $outputDirectory
156
     * @return void
157
     */
158
    public function generate(array $metadatas, $outputDirectory)
159
    {
160
        foreach ($metadatas as $metadata) {
161
            $this->writeDocumentClass($metadata, $outputDirectory);
162
        }
163
    }
164
165
    /**
166
     * Generated and write document class to disk for the given ClassMetadataInfo instance
167
     *
168
     * @param ClassMetadataInfo $metadata
169
     * @param string $outputDirectory
170
     * @throws \RuntimeException
171
     * @return void
172
     */
173 9
    public function writeDocumentClass(ClassMetadataInfo $metadata, $outputDirectory)
174
    {
175 9
        $path = $outputDirectory . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $metadata->name) . $this->extension;
176 9
        $dir = dirname($path);
177
178 9
        if ( ! is_dir($dir)) {
179
            mkdir($dir, 0775, true);
180
        }
181
182 9
        $this->isNew = ! file_exists($path) || (file_exists($path) && $this->regenerateDocumentIfExists);
183
184 9
        if ( ! $this->isNew) {
185 2
            $this->parseTokensInDocumentFile($path);
186
        }
187
188 9
        if ($this->backupExisting && file_exists($path)) {
189 2
            $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . '~';
190 2
            if ( ! copy($path, $backupPath)) {
191
                throw new \RuntimeException('Attempt to backup overwritten document file but copy operation failed.');
192
            }
193
        }
194
195
        // If document doesn't exist or we're re-generating the documents entirely
196 9
        if ($this->isNew) {
197 8
            file_put_contents($path, $this->generateDocumentClass($metadata));
198
199
        // If document exists and we're allowed to update the document class
200 2
        } elseif ( ! $this->isNew && $this->updateDocumentIfExists) {
201 2
            file_put_contents($path, $this->generateUpdatedDocumentClass($metadata, $path));
202
        }
203 9
        chmod($path, 0664);
204 9
    }
205
206
    /**
207
     * Generate a PHP5 Doctrine 2 document class from the given ClassMetadataInfo instance
208
     *
209
     * @param ClassMetadataInfo $metadata
210
     * @return string $code
211
     */
212 8
    public function generateDocumentClass(ClassMetadataInfo $metadata)
213
    {
214
        $placeHolders = array(
215 8
            '<namespace>',
216
            '<imports>',
217
            '<documentAnnotation>',
218
            '<documentClassName>',
219
            '<documentBody>'
220
        );
221
222
        $replacements = array(
223 8
            $this->generateDocumentNamespace($metadata),
224 8
            $this->generateDocumentImports(),
225 8
            $this->generateDocumentDocBlock($metadata),
226 8
            $this->generateDocumentClassName($metadata),
227 8
            $this->generateDocumentBody($metadata)
228
        );
229
230 8
        $code = str_replace($placeHolders, $replacements, self::$classTemplate);
231 8
        return str_replace('<spaces>', $this->spaces, $code);
232
    }
233
234
    /**
235
     * Generate the updated code for the given ClassMetadataInfo and document at path
236
     *
237
     * @param ClassMetadataInfo $metadata
238
     * @param string $path
239
     * @return string $code;
240
     */
241 2
    public function generateUpdatedDocumentClass(ClassMetadataInfo $metadata, $path)
242
    {
243 2
        $currentCode = file_get_contents($path);
244
245 2
        $body = $this->generateDocumentBody($metadata);
246 2
        $body = str_replace('<spaces>', $this->spaces, $body);
247 2
        $last = strrpos($currentCode, '}');
248
249 2
        return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : '') . "}\n";
250
    }
251
252
    /**
253
     * Set the number of spaces the exported class should have
254
     *
255
     * @param integer $numSpaces
256
     * @return void
257
     */
258
    public function setNumSpaces($numSpaces)
259
    {
260
        $this->spaces = str_repeat(' ', $numSpaces);
261
        $this->numSpaces = $numSpaces;
262
    }
263
264
    /**
265
     * Set the extension to use when writing php files to disk
266
     *
267
     * @param string $extension
268
     * @return void
269
     */
270
    public function setExtension($extension)
271
    {
272
        $this->extension = $extension;
273
    }
274
275
    /**
276
     * Set the name of the class the generated classes should extend from
277
     *
278
     * @param string $classToExtend Class name.
279
     * @return void
280
     */
281 1
    public function setClassToExtend($classToExtend)
282
    {
283 1
        $this->classToExtend = $classToExtend;
284 1
    }
285
286
    /**
287
     * Set whether or not to generate annotations for the document
288
     *
289
     * @param bool $bool
290
     * @return void
291
     */
292 9
    public function setGenerateAnnotations($bool)
293
    {
294 9
        $this->generateAnnotations = $bool;
295 9
    }
296
297
    /**
298
     * Set whether or not to try and update the document if it already exists
299
     *
300
     * @param bool $bool
301
     * @return void
302
     */
303 9
    public function setUpdateDocumentIfExists($bool)
304
    {
305 9
        $this->updateDocumentIfExists = $bool;
306 9
    }
307
308
    /**
309
     * Set whether or not to regenerate the document if it exists
310
     *
311
     * @param bool $bool
312
     * @return void
313
     */
314 9
    public function setRegenerateDocumentIfExists($bool)
315
    {
316 9
        $this->regenerateDocumentIfExists = $bool;
317 9
    }
318
319
    /**
320
     * Set whether or not to generate stub methods for the document
321
     *
322
     * @param bool $bool
323
     * @return void
324
     */
325 9
    public function setGenerateStubMethods($bool)
326
    {
327 9
        $this->generateDocumentStubMethods = $bool;
328 9
    }
329
330
    /**
331
     * Sets a value indicating whether existing documents will be backed up.
332
     *
333
     * @param bool $bool True to backup existing document, false to overwrite.
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
        }
365
366 9
        if ($associationMappingProperties) {
367 6
            $code[] = $associationMappingProperties;
368
        }
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
        }
375
376 9
        if ($lifecycleCallbackMethods) {
377 6
            $code[] = "\n" . $lifecycleCallbackMethods;
378
        }
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 9
                $collections[] = '$this->' . $mapping['fieldName'] . ' = new \Doctrine\Common\Collections\ArrayCollection();';
393
            }
394
        }
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
                    }
422
                }
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
                } elseif ($tokens[$i + 2][0] == '&' && $tokens[$i + 3][0] == T_STRING) {
431 1
                    $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
            }
436
        }
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
        }
449
450 8
        foreach ($this->getTraits($metadata) as $trait) {
451 2
            if ($trait->hasProperty($property)) {
452 2
                return true;
453
            }
454
        }
455
456
        return (
457 8
            isset($this->staticReflection[$metadata->name]) &&
458 8
            in_array($property, $this->staticReflection[$metadata->name]['properties'])
459
        );
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
        }
472
473 9
        foreach ($this->getTraits($metadata) as $trait) {
474 2
            if ($trait->hasMethod($method)) {
475 2
                return true;
476
            }
477
        }
478
479
        return (
480 9
            isset($this->staticReflection[$metadata->name]) &&
481 9
            in_array($method, $this->staticReflection[$metadata->name]['methods'])
482
        );
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
            }
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
            } else {
558 8
                $lines[] = ' * @ODM\\Document';
559
            }
560
561 8
            $document = array();
562 8
            if ( ! $metadata->isMappedSuperclass && ! $metadata->isEmbeddedDocument) {
563 8
                if ($metadata->collection) {
564 8
                    $document[] = ' *     collection="' . $metadata->collection . '"';
565
                }
566 8
                if ($metadata->customRepositoryClassName) {
567 6
                    $document[] = ' *     repositoryClass="' . $metadata->customRepositoryClassName . '"';
568
                }
569
            }
570 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...
571
                $indexes = array();
572
                $indexLines = array();
573
                $indexLines[] = ' *     indexes={';
574
                foreach ($metadata->indexes as $index) {
575
                    $keys = array();
576 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...
577
                        $keys[] = '"' . $key . '"="' . $value . '"';
578
                    }
579
                    $options = array();
580 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...
581
                        $options[] = '"' . $key . '"="' . $value . '"';
582
                    }
583
                    $indexes[] = '@ODM\\Index(keys={' . implode(', ', $keys) . '}, options={' . implode(', ', $options) . '})';
584
                }
585
                $indexLines[] = "\n *         " . implode(",\n *         ", $indexes);
586
                $indexLines[] = "\n *     }";
587
588
                $document[] = implode(null, $indexLines);
589
            }
590
591 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...
592 8
                $lines[count($lines) - 1] .= '(';
593 8
                $lines[] = implode(",\n", $document);
594 8
                $lines[] = ' * )';
595
            }
596
597 8
            if ( ! empty($metadata->lifecycleCallbacks)) {
598 6
                $lines[] = ' * @ODM\HasLifecycleCallbacks';
599
            }
600
601
            $methods = array(
602 8
                'generateInheritanceAnnotation',
603
                'generateDiscriminatorFieldAnnotation',
604
                'generateDiscriminatorMapAnnotation',
605
                'generateDefaultDiscriminatorValueAnnotation',
606
                'generateChangeTrackingPolicyAnnotation'
607
            );
608
609 8
            foreach ($methods as $method) {
610 8
                if ($code = $this->$method($metadata)) {
611 8
                    $lines[] = ' * ' . $code;
612
                }
613
            }
614
        }
615
616 8
        $lines[] = ' */';
617 8
        return implode("\n", $lines);
618
    }
619
620 8
    private function generateInheritanceAnnotation(ClassMetadataInfo $metadata)
621
    {
622 8
        if ($metadata->inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
623
            return '@ODM\\InheritanceType("' . $this->getInheritanceTypeString($metadata->inheritanceType) . '")';
624
        }
625 8
    }
626
627 8
    private function generateDiscriminatorFieldAnnotation(ClassMetadataInfo $metadata)
628
    {
629 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
630
            return '@ODM\\DiscriminatorField(name="' . $metadata->discriminatorField . '")';
631
        }
632 8
    }
633
634 8
    private function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata)
635
    {
636 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
637
            $inheritanceClassMap = array();
638
639
            foreach ($metadata->discriminatorMap as $type => $class) {
640
                $inheritanceClassMap[] .= '"' . $type . '" = "' . $class . '"';
641
            }
642
643
            return '@ODM\\DiscriminatorMap({' . implode(', ', $inheritanceClassMap) . '})';
644
        }
645 8
    }
646
647 8
    private function generateDefaultDiscriminatorValueAnnotation(ClassMetadataInfo $metadata)
648
    {
649 8
        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION && isset($metadata->defaultDiscriminatorValue)) {
650
            return '@ODM\\DefaultDiscriminatorValue("' . $metadata->defaultDiscriminatorValue . '")';
651
        }
652 8
    }
653
654 8
    private function generateChangeTrackingPolicyAnnotation(ClassMetadataInfo $metadata)
655
    {
656 8
        return '@ODM\\ChangeTrackingPolicy("' . $this->getChangeTrackingPolicyString($metadata->changeTrackingPolicy) . '")';
657
    }
658
659 9
    private function generateDocumentStubMethods(ClassMetadataInfo $metadata)
660
    {
661 9
        $methods = array();
662
663 9
        foreach ($metadata->fieldMappings as $fieldMapping) {
664 9
            $typeHint = isset($fieldMapping['fieldTypeName']) ? $fieldMapping['fieldTypeName'] : $fieldMapping['type'];
665
            
666 9
            if (isset($fieldMapping['id'])) {
667 9 View Code Duplication
                if ($metadata->generatorType == 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...
668
                    if ($code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $typeHint)) {
669
                        $methods[] = $code;
670
                    }
671
                }
672 9 View Code Duplication
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $typeHint)) {
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...
673 9
                    $methods[] = $code;
674
                }
675 9
            } elseif ( ! isset($fieldMapping['association'])) {
676 9 View Code Duplication
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $typeHint)) {
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...
677 9
                    $methods[] = $code;
678
                }
679 9 View Code Duplication
                if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $typeHint)) {
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...
680 9
                    $methods[] = $code;
681
                }
682 8
            } 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
                }
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 8
                    $methods[] = $code;
689
                }
690 6
            } 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
                }
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
                }
697 6
                if ($code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], '\Doctrine\Common\Collections\Collection')) {
698 9
                    $methods[] = $code;
699
                }
700
            }
701
        }
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
                }
729
            }
730
        }
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);
749 6
            $lines[] = $this->spaces . 'protected $' . $fieldMapping['fieldName']
750 6
                . ($fieldMapping['type'] === ClassMetadataInfo::MANY ? ' = array()' : null) . ";\n";
751
        }
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
        }
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 6
            ? 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]) ? (mb_substr($typeHint, 0, 1) != '\\' ? '\\' : '') . $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
        );
806
807 9
        $templateVar = sprintf('%sMethodTemplate', $type);
808
809 9
        $method = str_replace(
810
            array_keys($replacements),
811
            array_values($replacements),
812 9
            self::$$templateVar
813
        );
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
        );
828
829 6
        $method = str_replace(
830
            array_keys($replacements),
831
            array_values($replacements),
832 6
            self::$lifecycleCallbackMethodTemplate
833
        );
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
            }
862 6
            $typeOptions = array();
863
864 6
            if (isset($fieldMapping['targetDocument'])) {
865 6
                $typeOptions[] = 'targetDocument="' . $fieldMapping['targetDocument'] . '"';
866
            }
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
        }
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 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...
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 7
                $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName'];
904
            }
905
        } else {
906 7
            if (isset($fieldMapping['fieldTypeName'])){
907
                $lines[] = $this->spaces . ' * @var ' . $fieldMapping['fieldTypeName'] . ' $' . $fieldMapping['fieldName'];
908
            } else {
909 7
                $lines[] = $this->spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName'];
910
            }
911
        }
912
913 7
        if ($this->generateAnnotations) {
914 7
            $lines[] = $this->spaces . ' *';
915
916 7
            $field = array();
917 7
            if (isset($fieldMapping['id']) && $fieldMapping['id']) {
918 7
                if (isset($fieldMapping['strategy'])) {
919 7
                    $field[] = 'strategy="' . $this->getIdGeneratorTypeString($metadata->generatorType) . '"';
920
                }
921 7
                $lines[] = $this->spaces . ' * @ODM\\Id(' . implode(', ', $field) . ')';
922
            } else {
923 7
                if (isset($fieldMapping['name'])) {
924 7
                    $field[] = 'name="' . $fieldMapping['name'] . '"';
925
                }
926
927 7
                if (isset($fieldMapping['type'])) {
928 7
                    $field[] = 'type="' . $fieldMapping['type'] . '"';
929
                }
930
931 7
                if (isset($fieldMapping['nullable']) && $fieldMapping['nullable'] === true) {
932
                    $field[] = 'nullable=' . var_export($fieldMapping['nullable'], true);
933
                }
934 7
                if (isset($fieldMapping['options'])) {
935 1
                    $options = array();
936 1
                    foreach ($fieldMapping['options'] as $key => $value) {
937
                        $options[] = '"' . $key . '" = "' . $value . '"';
938
                    }
939 1
                    $field[] = 'options={' . implode(', ', $options) . '}';
940
                }
941 7
                $lines[] = $this->spaces . ' * @ODM\\Field(' . implode(', ', $field) . ')';
942
            }
943
944 7
            if (isset($fieldMapping['version']) && $fieldMapping['version']) {
945
                $lines[] = $this->spaces . ' * @ODM\\Version';
946
            }
947
        }
948
949 7
        $lines[] = $this->spaces . ' */';
950
951 7
        return implode("\n", $lines);
952
    }
953
954 9
    private function prefixCodeWithSpaces($code, $num = 1)
955
    {
956 9
        $lines = explode("\n", $code);
957
958 9
        foreach ($lines as $key => $value) {
959 9
            $lines[$key] = str_repeat($this->spaces, $num) . $lines[$key];
960
        }
961
962 9
        return implode("\n", $lines);
963
    }
964
965
    private function getInheritanceTypeString($type)
966
    {
967
        switch ($type) {
968
            case ClassMetadataInfo::INHERITANCE_TYPE_NONE:
969
                return 'NONE';
970
971
            case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION:
972
                return 'SINGLE_COLLECTION';
973
974
            case ClassMetadataInfo::INHERITANCE_TYPE_COLLECTION_PER_CLASS:
975
                return 'COLLECTION_PER_CLASS';
976
977
            default:
978
                throw new \InvalidArgumentException('Invalid provided InheritanceType: ' . $type);
979
        }
980
    }
981
982 8
    private function getChangeTrackingPolicyString($policy)
983
    {
984
        switch ($policy) {
985 8
            case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT:
986 8
                return 'DEFERRED_IMPLICIT';
987
988
            case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT:
989
                return 'DEFERRED_EXPLICIT';
990
991
            case ClassMetadataInfo::CHANGETRACKING_NOTIFY:
992
                return 'NOTIFY';
993
994
            default:
995
                throw new \InvalidArgumentException('Invalid provided ChangeTrackingPolicy: ' . $policy);
996
        }
997
    }
998
999 7
    private function getIdGeneratorTypeString($type)
1000
    {
1001
        switch ($type) {
1002 7
            case ClassMetadataInfo::GENERATOR_TYPE_AUTO:
1003 1
                return 'AUTO';
1004
1005 6
            case ClassMetadataInfo::GENERATOR_TYPE_INCREMENT:
1006
                return 'INCREMENT';
1007
1008 6
            case ClassMetadataInfo::GENERATOR_TYPE_UUID:
1009
                return 'UUID';
1010
1011 6
            case ClassMetadataInfo::GENERATOR_TYPE_ALNUM:
1012
                return 'ALNUM';
1013
1014 6
            case ClassMetadataInfo::GENERATOR_TYPE_CUSTOM:
1015 6
                return 'CUSTOM';
1016
1017
            case ClassMetadataInfo::GENERATOR_TYPE_NONE:
1018
                return 'NONE';
1019
1020
            default:
1021
                throw new \InvalidArgumentException('Invalid provided IdGeneratorType: ' . $type);
1022
        }
1023
    }
1024
}
1025