Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.43% covered (warning)
71.43%
30 / 42
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
LimitFieldLength
71.43% covered (warning)
71.43%
30 / 42
60.00% covered (warning)
60.00%
3 / 5
39.58
0.00% covered (danger)
0.00%
0 / 1
 postRun
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 patch
66.67% covered (warning)
66.67%
16 / 24
0.00% covered (danger)
0.00%
0 / 1
19.26
 fillInMissingStringLength
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 setNameArgument
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 iterateProperties
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2namespace Apie\DoctrineEntityConverter\CodeGenerators;
3
4use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface;
5use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
6use Doctrine\ORM\Mapping\Column;
7use Doctrine\ORM\Mapping\JoinColumn;
8use Doctrine\ORM\Mapping\Table;
9use Generator;
10use Nette\PhpGenerator\Attribute;
11use Nette\PhpGenerator\ClassType;
12use Nette\PhpGenerator\PromotedParameter;
13use Nette\PhpGenerator\Property;
14use ReflectionProperty;
15
16/**
17 * Find all doctrine column attributes set by AddDoctrineFields and checks if the generated column name
18 * is not too long.
19 *
20 * Most database vendors have a 64 character limit, Postgres has a 63 character limit.
21 * Foreign keys get a '_id' suffix, so any property name with more than 57 characters requires a different
22 * column name.
23 *
24 * We want to rename the column to only 57 characters and add 3 digits after it if this column name is already defined.
25 *
26 * @see AddDoctrineFields
27 */
28class LimitFieldLength implements PostRunGeneratedCodeContextInterface
29{
30    public function postRun(GeneratedCodeContext $generatedCodeContext): void
31    {
32        foreach ($generatedCodeContext->generatedCode->generatedCodeHashmap as $code) {
33            $this->patch($generatedCodeContext, $code);
34        }
35    }
36
37    private function patch(GeneratedCodeContext $generatedCodeContext, ClassType $classType): void
38    {
39        if (strlen($classType->getName()) > 60) {
40            $found = false;
41            $suggestedTableName = substr($classType->getName(), 0, 30) . '_' . md5($classType->getName());
42            foreach ($classType->getAttributes() as $attribute) {
43                if ($attribute->getName() === Table::class) {
44                    $found = true;
45                    $this->setNameArgument($attribute, $suggestedTableName);
46                }
47            }
48            if (!$found) {
49                $classType->addAttribute(Table::class, ['name' => $suggestedTableName]);
50            }
51        }
52        $alreadyDefined = [];
53        foreach ($this->iterateProperties($classType) as $property) {
54            foreach ($property->getAttributes() as $attribute) {
55                if (in_array($attribute->getName(), [Column::class])) {
56                    $this->fillInMissingStringLength($attribute);
57                }
58            }
59            $propertyName = $property->getName();
60            if (strlen($property->getName()) < 57 && !isset($alreadyDefined[$propertyName])) {
61                $alreadyDefined[$propertyName] = true;
62                continue;
63            }
64            $suggestedName = substr($propertyName, 0, 57);
65            for ($i = 0; !empty($alreadyDefined[$suggestedName]); $i++) {
66                $suggestedName = sprintf("%s%03u", substr($property->getName(), 0, 57), $i);
67            }
68            foreach ($property->getAttributes() as $attribute) {
69                if (in_array($attribute->getName(), [Column::class, JoinColumn::class])) {
70                    $this->setNameArgument($attribute, $suggestedName);
71                }
72            }
73        }
74    }
75
76    private function fillInMissingStringLength(Attribute $attribute): void
77    {
78        $arguments = $attribute->getArguments();
79        if (($arguments['type'] ?? 'string')=== 'string' || !isset($arguments['type'])) {
80            if (!isset($arguments['length'])) {
81                $arguments['length'] = 255;
82            }
83        }
84        $refl = new ReflectionProperty(Attribute::class, 'args');
85        $refl->setValue($attribute, $arguments);
86    }
87
88    private function setNameArgument(Attribute $attribute, string $suggestedName): void
89    {
90        $arguments = $attribute->getArguments();
91        $arguments['name'] = $suggestedName;
92        $refl = new ReflectionProperty(Attribute::class, 'args');
93        $refl->setValue($attribute, $arguments);
94    }
95
96    /**
97     * @return Generator<int, PromotedParameter|Property>
98     */
99    private function iterateProperties(ClassType $classType): Generator
100    {
101        foreach ($classType->getProperties() as $property) {
102            yield $property;
103        }
104        if ($classType->hasMethod('__construct')) {
105            foreach ($classType->getMethod('__construct')->getParameters() as $parameter) {
106                if ($parameter instanceof PromotedParameter) {
107                    yield $parameter;
108                }
109            }
110        }
111    }
112}