Completed
Push — master ( 8840ae...28ce39 )
by Andreas
09:22
created

DocumentGenerator::generateDocumentImports()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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