MetadataGrapher   B
last analyzed

Complexity

Total Complexity 52

Size/Duplication

Total Lines 217
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 99.01%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 1
dl 0
loc 217
ccs 100
cts 101
cp 0.9901
rs 7.44
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
F getAssociationString() 0 42 21
B generateFromMetadata() 0 36 9
A getClassReverseAssociationName() 0 15 4
B getClassString() 0 30 6
A getClassByName() 0 13 4
A getParent() 0 11 3
A visitAssociation() 0 24 5

How to fix   Complexity   

Complex Class

Complex classes like MetadataGrapher often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataGrapher, and based on these observations, apply Extract Interface, too.

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