Completed
Pull Request — master (#602)
by Tom
07:01
created

MetadataGrapher::getClassString()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 17
cts 17
cp 1
rs 8.8177
c 0
b 0
f 0
cc 6
nc 16
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DoctrineORMModule\Yuml;
6
7
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
8
use Exception;
9
use function class_exists;
10
use function get_parent_class;
11
use function implode;
12
use function in_array;
13
use function str_replace;
14
15
/**
16
 * Utility to generate Yuml compatible strings from metadata graphs
17
 *
18
 * @link    http://www.doctrine-project.org/
19
 */
20
class MetadataGrapher
21
{
22
    /**
23
     * Temporary array where already visited collections are stored
24
     *
25
     * @var mixed[]
26
     */
27
    protected array $visitedAssociations = [];
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
28
29
    /** @var ClassMetadata[] */
30
    private array $metadata;
31
32
    /**
33
     * Temporary array where reverse association name are stored
34
     *
35
     * @var ClassMetadata[]
36
     */
37
    private array $classByNames = [];
38
39
    /**
40
     * Generate a YUML compatible `dsl_text` to describe a given array
41
     * of entities
42
     *
43
     * @param mixed[] $metadata \Doctrine\Common\Persistence\Mapping\ClassMetadata[]
44 21
     */
45
    public function generateFromMetadata(array $metadata) : string
46 21
    {
47 21
        $this->metadata            = $metadata;
48 21
        $this->visitedAssociations = [];
49
        $str                       = [];
50 21
51 21
        foreach ($metadata as $class) {
52
            $parent = $this->getParent($class);
53 21
54 3
            if ($parent) {
55
                $str[] = $this->getClassString($parent) . '^' . $this->getClassString($class);
56
            }
57 21
58
            $associations = $class->getAssociationNames();
59 21
60 3
            if (empty($associations) && ! isset($this->visitedAssociations[$class->getName()])) {
61
                $str[] = $this->getClassString($class);
62 3
63
                continue;
64
            }
65 19
66 17
            foreach ($associations as $associationName) {
67 1
                if ($parent && in_array($associationName, $parent->getAssociationNames())) {
68
                    continue;
69
                }
70 17
71 19
                if (! $this->visitAssociation($class->getName(), $associationName)) {
72
                    continue;
73
                }
74
75
                $str[] = $this->getAssociationString($class, $associationName);
76 21
            }
77
        }
78
79
        return implode(',', $str);
80
    }
81
82
    private function getAssociationString(ClassMetadata $class1, string $association) : string
83
    {
84 17
        $targetClassName = $class1->getAssociationTargetClass($association);
85
        $class2          = $this->getClassByName($targetClassName);
86 17
        $isInverse       = $class1->isAssociationInverseSide($association);
87 17
        $class1Count     = $class1->isCollectionValuedAssociation($association) ? 2 : 1;
88 17
89 17
        if ($class2 === null) {
90
            return $this->getClassString($class1)
91 17
                . ($isInverse ? '<' : '<>') . '-' . $association . ' '
92 2
                . ($class1Count > 1 ? '*' : ($class1Count ? '1' : ''))
93 2
                . ($isInverse ? '<>' : '>')
94 2
                . '[' . str_replace('\\', '.', $targetClassName) . ']';
95 2
        }
96 2
97
        $class1SideName = $association;
98
        $class2SideName = $this->getClassReverseAssociationName($class1, $class2);
99 15
        $class2Count    = 0;
100 15
        $bidirectional  = false;
101 15
102 15
        if ($class2SideName !== null) {
103
            if ($isInverse) {
104 15
                $class2Count   = $class2->isCollectionValuedAssociation($class2SideName) ? 2 : 1;
105 12
                $bidirectional = true;
106 5
            } elseif ($class2->isAssociationInverseSide($class2SideName)) {
107 5
                $class2Count   = $class2->isCollectionValuedAssociation($class2SideName) ? 2 : 1;
108 7
                $bidirectional = true;
109 7
            }
110 7
        }
111
112
        $this->visitAssociation($targetClassName, $class2SideName);
113
114 15
        return $this->getClassString($class1)
115
            . ($bidirectional ? ($isInverse ? '<' : '<>') : '') // class2 side arrow
116 15
            . ($class2SideName ? $class2SideName . ' ' : '')
117 15
            . ($class2Count > 1 ? '*' : ($class2Count ? '1' : '')) // class2 side single/multi valued
118 15
            . '-'
119 15
            . $class1SideName . ' '
120 15
            . ($class1Count > 1 ? '*' : ($class1Count ? '1' : '')) // class1 side single/multi valued
121 15
            . ($bidirectional && $isInverse ? '<>' : '>') // class1 side arrow
122 15
            . $this->getClassString($class2);
123 15
    }
124 15
125
    private function getClassReverseAssociationName(ClassMetadata $class1, ClassMetadata $class2) : ?string
126
    {
127
        foreach ($class2->getAssociationNames() as $class2Side) {
128
            $targetClass = $this->getClassByName($class2->getAssociationTargetClass($class2Side));
129
            if (! $targetClass) {
130
                throw new Exception('Invalid class name for AssociationTargetClass ' . $class2Side);
131
            }
132 15
133
            if ($class1->getName() === $targetClass->getName()) {
134 15
                return $class2Side;
135 12
            }
136 12
        }
137
138
        return null;
139 12
    }
140 12
141
    /**
142
     * Build the string representing the single graph item
143
     */
144 9
    private function getClassString(ClassMetadata $class) : string
145
    {
146
        $this->visitAssociation($class->getName());
147
148
        $className    = $class->getName();
149
        $classText    = '[' . str_replace('\\', '.', $className);
150
        $fields       = [];
151
        $parent       = $this->getParent($class);
152
        $parentFields = $parent ? $parent->getFieldNames() : [];
153
154 21
        foreach ($class->getFieldNames() as $fieldName) {
155
            if (in_array($fieldName, $parentFields)) {
156 21
                continue;
157
            }
158 21
159 21
            if ($class->isIdentifier($fieldName)) {
160 21
                $fields[] = '+' . $fieldName;
161 21
            } else {
162 21
                $fields[] = $fieldName;
163
            }
164 21
        }
165 2
166 1
        if (! empty($fields)) {
167
            $classText .= '|' . implode(';', $fields);
168
        }
169 2
170 1
        $classText .= ']';
171
172 2
        return $classText;
173
    }
174
175
    /**
176 21
     * Retrieve a class metadata instance by name from the given array
177 2
     */
178
    private function getClassByName(string $className) : ?ClassMetadata
179
    {
180 21
        if (! isset($this->classByNames[$className])) {
181
            foreach ($this->metadata as $class) {
182 21
                if ($class->getName() === $className) {
183
                    $this->classByNames[$className] = $class;
184
                    break;
185
                }
186
            }
187
        }
188
189
        return $this->classByNames[$className] ?? null;
190
    }
191
192 19
    /**
193
     * Retrieve a class metadata's parent class metadata
194 19
     */
195 19
    private function getParent(ClassMetadata $class) : ?ClassMetadata
196 19
    {
197 18
        $className = $class->getName();
198 19
199
        $parent = get_parent_class($className);
200
        if (! class_exists($className) || ! $parent) {
201
            return null;
202
        }
203 19
204
        return $this->getClassByName($parent);
205
    }
206
207
    /**
208
     * Visit a given association and mark it as visited
209
     *
210
     * @return bool true if the association was visited before
211
     */
212
    private function visitAssociation(string $className, ?string $association = null) : bool
213 21
    {
214
        if ($association === null) {
215 21
            if (isset($this->visitedAssociations[$className])) {
216
                return false;
217 21
            }
218 21
219
            $this->visitedAssociations[$className] = [];
220
221 3
            return true;
222
        }
223
224
        if (isset($this->visitedAssociations[$className][$association])) {
225
            return false;
226
        }
227
228
        if (! isset($this->visitedAssociations[$className])) {
229
            $this->visitedAssociations[$className] = [];
230
        }
231
232 21
        $this->visitedAssociations[$className][$association] = true;
233
234 21
        return true;
235 21
    }
236
}
237