DataFixturesSorter::orderFixturesByNumber()   C
last analyzed

Complexity

Conditions 9
Paths 1

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 6.412
cc 9
eloc 14
nc 1
nop 0
1
<?php
2
3
namespace RDV\Bundle\MigrationBundle\Migration\Sorter;
4
5
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
6
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
7
use Doctrine\Common\DataFixtures\Exception\CircularReferenceException;
8
9
/**
10
 * Basically code of this class comes from origin \Doctrine\Common\DataFixtures\Loader.
11
 * Issue solved is notices during fixtures sorting
12
 *
13
 * @TODO could be removed when https://github.com/doctrine/data-fixtures/issues/148 will be resolved
14
 */
15
class DataFixturesSorter
16
{
17
    /** @var array */
18
    protected $orderedFixtures = [];
19
20
    /** @var array */
21
    protected $fixtures = [];
22
23
    /**
24
     * Returns the array of data fixtures to execute.
25
     *
26
     * @param array $fixtures
27
     *
28
     * @return array $fixtures
29
     */
30
    public function sort(array $fixtures)
31
    {
32
        $this->fixtures        = $fixtures;
33
        $this->orderedFixtures = [];
34
35
        $usePrioritySorting     = $this->usePrioritySorting($fixtures);
36
        $useDependenciesSorting = $this->useDependenciesSorting($fixtures);
37
38
39
        if ($usePrioritySorting) {
40
            $this->orderFixturesByNumber();
41
        }
42
43
        if ($useDependenciesSorting) {
44
            $this->orderFixturesByDependencies($usePrioritySorting);
45
        }
46
47
        if (!($usePrioritySorting || $useDependenciesSorting)) {
48
            $this->orderedFixtures = $fixtures;
49
        }
50
51
        return $this->orderedFixtures;
52
    }
53
54
    /**
55
     * Order fixtures by priority
56
     *
57
     * @return array
58
     */
59
    protected function orderFixturesByNumber()
60
    {
61
        $this->orderedFixtures = $this->fixtures;
62
        usort(
63
            $this->orderedFixtures,
64
            function ($a, $b) {
65
                if ($a instanceof OrderedFixtureInterface && $b instanceof OrderedFixtureInterface) {
66
                    if ($a->getOrder() === $b->getOrder()) {
67
                        return 0;
68
                    }
69
70
                    return $a->getOrder() < $b->getOrder() ? -1 : 1;
71
                } elseif ($a instanceof OrderedFixtureInterface) {
72
                    return $a->getOrder() === 0 ? 0 : 1;
73
                } elseif ($b instanceof OrderedFixtureInterface) {
74
                    return $b->getOrder() === 0 ? 0 : -1;
75
                }
76
77
                return 0;
78
            }
79
        );
80
    }
81
82
    /**
83
     * @param bool $usedPrioritySorting
84
     *
85
     * @return array
86
     * @throws CircularReferenceException
87
     *
88
     * @SuppressWarnings(PHPMD.NPathComplexity)
89
     */
90
    protected function orderFixturesByDependencies($usedPrioritySorting)
91
    {
92
        $sequenceForClasses = $orderedFixtures = [];
93
94
        // If fixtures were already ordered by number then we need
95
        // to remove classes which are not instances of OrderedFixtureInterface
96
        // in case fixtures implementing DependentFixtureInterface exist.
97
        // This is because, in that case, the method orderFixturesByDependencies
98
        // will handle all fixtures which are not instances of
99
        // OrderedFixtureInterface
100
        if ($usedPrioritySorting) {
101
            $this->orderedFixtures = array_filter(
102
                $this->orderedFixtures,
103
                function ($fixture) {
104
                    return $fixture instanceof OrderedFixtureInterface;
105
                }
106
            );
107
        }
108
109
        // First we determine which classes has dependencies and which don't
110
        foreach ($this->fixtures as $fixture) {
111
            $fixtureClass = get_class($fixture);
112
113
            if ($fixture instanceof OrderedFixtureInterface) {
114
                continue;
115
            } elseif ($fixture instanceof DependentFixtureInterface) {
116
                $dependenciesClasses = $fixture->getDependencies();
117
118
                $this->validateDependencies($fixtureClass, $dependenciesClasses);
119
120
                // We mark this class as unsequenced
121
                $sequenceForClasses[$fixtureClass] = -1;
122
            } else {
123
                // This class has no dependencies, so we assign 0
124
                $sequenceForClasses[$fixtureClass] = 0;
125
            }
126
        }
127
128
        // Now we order fixtures by sequence
129
        $sequence  = 1;
130
        $lastCount = -1;
131
132
        while (($count = count($unsequencedClasses = $this->getUnsequencedClasses($sequenceForClasses))) > 0
133
            && $count !== $lastCount) {
134
            foreach ($unsequencedClasses as $key => $class) {
135
                $fixture                 = $this->fixtures[$class];
136
                $dependencies            = $fixture->getDependencies();
137
                $unsequencedDependencies = $this->getUnsequencedClasses($sequenceForClasses, $dependencies);
138
139
                if (count($unsequencedDependencies) === 0) {
140
                    $sequenceForClasses[$class] = $sequence++;
141
                }
142
            }
143
144
            $lastCount = $count;
145
        }
146
147
        // If there're fixtures unsequenced left and they couldn't be sequenced,
148
        // it means we have a circular reference
149
        if ($count > 0) {
150
            $msg = 'Classes "%s" have produced a CircularReferenceException. ';
151
            $msg .= 'An example of this problem would be the following: Class C has class B as its dependency. ';
152
            $msg .= 'Then, class B has class A has its dependency. Finally, class A has class C as its dependency. ';
153
            $msg .= 'This case would produce a CircularReferenceException.';
154
155
            throw new CircularReferenceException(sprintf($msg, implode(',', $unsequencedClasses)));
156
        } else {
157
            // We order the classes by sequence
158
            asort($sequenceForClasses);
159
160
            foreach ($sequenceForClasses as $class => $sequence) {
161
                // If fixtures were ordered
162
                $orderedFixtures[] = $this->fixtures[$class];
163
            }
164
        }
165
166
        $this->orderedFixtures = array_merge($this->orderedFixtures, $orderedFixtures);
167
    }
168
169
    /**
170
     * @param string $fixtureClass
171
     * @param mixed  $dependenciesClasses
172
     *
173
     * @return bool
174
     */
175
    protected function validateDependencies($fixtureClass, $dependenciesClasses)
176
    {
177
        if (!is_array($dependenciesClasses) || empty($dependenciesClasses)) {
178
            throw new \InvalidArgumentException(
179
                sprintf(
180
                    'Method "%s" in class "%s" must return an array of classes which are'
181
                    . ' dependencies for the fixture, and it must be NOT empty.',
182
                    'getDependencies',
183
                    $fixtureClass
184
                )
185
            );
186
        }
187
188
        if (in_array($fixtureClass, $dependenciesClasses)) {
189
            throw new \InvalidArgumentException(
190
                sprintf('Class "%s" can\'t have itself as a dependency', $fixtureClass)
191
            );
192
        }
193
194
        $loadedFixtureClasses = array_keys($this->fixtures);
195
        foreach ($dependenciesClasses as $class) {
196
            if (!in_array($class, $loadedFixtureClasses)) {
197
                throw new \RuntimeException(
198
                    sprintf(
199
                        'Fixture "%s" was declared as a dependency, but it should be added in fixture loader first.',
200
                        $class
201
                    )
202
                );
203
            }
204
        }
205
206
        return true;
207
    }
208
209
    /**
210
     * @param array      $sequences
211
     * @param null|array $classes
212
     *
213
     * @return array
214
     */
215
    protected function getUnsequencedClasses(array $sequences, array $classes = null)
216
    {
217
        $unsequencedClasses = array();
218
219
        if (is_null($classes)) {
220
            $classes = array_keys($sequences);
221
        }
222
223
        foreach ($classes as $class) {
224
            // might not be set if depends on ordered fixture
225
            if (isset($sequences[$class]) && $sequences[$class] === -1) {
226
                $unsequencedClasses[] = $class;
227
            }
228
        }
229
230
        return $unsequencedClasses;
231
    }
232
233
    /**
234
     * @param array $fixtures
235
     *
236
     * @return bool
237
     */
238
    protected function usePrioritySorting($fixtures)
239
    {
240
        foreach ($fixtures as $fixture) {
241
            if ($fixture instanceof OrderedFixtureInterface) {
242
                return true;
243
            }
244
        }
245
246
        return false;
247
    }
248
249
    /**
250
     * @param array $fixtures
251
     *
252
     * @return bool
253
     */
254
    protected function useDependenciesSorting($fixtures)
255
    {
256
        foreach ($fixtures as $fixture) {
257
            if ($fixture instanceof DependentFixtureInterface) {
258
                return true;
259
            }
260
        }
261
262
        return false;
263
    }
264
}
265