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