Completed
Pull Request — 2.1 (#11477)
by Angel
08:46
created

FixtureController::outputList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2
Metric Value
dl 0
loc 6
rs 9.4285
ccs 5
cts 5
cp 1
cc 2
eloc 3
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\console\Controller;
12
use yii\console\Exception;
13
use yii\helpers\Console;
14
use yii\helpers\FileHelper;
15
use yii\test\FixtureTrait;
16
use yii\test\InitDb;
17
18
/**
19
 * Manages fixture data loading and unloading.
20
 *
21
 * ```
22
 * #load fixtures from UsersFixture class with default namespace "tests\unit\fixtures"
23
 * yii fixture/load User
24
 *
25
 * #also a short version of this command (generate action is default)
26
 * yii fixture User
27
 *
28
 * #load all fixtures
29
 * yii fixture "*"
30
 *
31
 * #load all fixtures except User
32
 * yii fixture "*" -User
33
 *
34
 * #load fixtures with different namespace.
35
 * yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
36
 * ```
37
 *
38
 * The `unload` sub-command can be used similarly to unload fixtures.
39
 *
40
 * @author Mark Jebri <[email protected]>
41
 * @since 2.0
42
 */
43
class FixtureController extends Controller
44
{
45
    use FixtureTrait;
46
47
    /**
48
     * @var string controller default action ID.
49
     */
50
    public $defaultAction = 'load';
51
    /**
52
     * @var string default namespace to search fixtures in
53
     */
54
    public $namespace = 'tests\unit\fixtures';
55
    /**
56
     * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture`
57
     * that disables and enables integrity check, so your data can be safely loaded.
58
     */
59
    public $globalFixtures = [InitDb::class];
60
61
62
    /**
63
     * @inheritdoc
64
     */
65
    public function options($actionID)
66
    {
67
        return array_merge(parent::options($actionID), [
68
            'namespace', 'globalFixtures'
69
        ]);
70
    }
71
72
    /**
73
     * @inheritdoc
74
     * @since 2.0.8
75
     */
76
    public function optionAliases()
77
    {
78
        return array_merge(parent::optionAliases(), [
79
            'g' => 'globalFixtures',
80
            'n' => 'namespace'
81
        ]);
82
    }
83
84
    /**
85
     * Loads the specified fixture data.
86
     * For example,
87
     *
88
     * ```
89
     * # load the fixture data specified by User and UserProfile.
90
     * # any existing fixture data will be removed first
91
     * yii fixture/load User UserProfile
92
     *
93
     * # load all available fixtures found under 'tests\unit\fixtures'
94
     * yii fixture/load "*"
95
     *
96
     * # load all fixtures except User and UserProfile
97
     * yii fixture/load "*" -User -UserProfile
98
     * ```
99
     *
100
     * @throws Exception if the specified fixture does not exist.
101
     */
102 6
    public function actionLoad()
103
    {
104 6
        $fixturesInput = func_get_args();
105 6
        if ($fixturesInput === []) {
106
            $this->stdout($this->getHelpSummary() . "\n");
107
108
            $helpCommand = Console::ansiFormat('yii help fixture', [Console::FG_CYAN]);
109
            $this->stdout("Use $helpCommand to get usage info.\n");
110
111
            return self::EXIT_CODE_NORMAL;
112
        }
113
114 6
        $filtered = $this->filterFixtures($fixturesInput);
115 6
        $except = $filtered['except'];
116
117 6
        if (!$this->needToApplyAll($fixturesInput[0])) {
118 4
            $fixtures = $filtered['apply'];
119
120 4
            $foundFixtures = $this->findFixtures($fixtures);
121 4
            $notFoundFixtures = array_diff($fixtures, $foundFixtures);
122
123 4
            if ($notFoundFixtures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $notFoundFixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
124 1
                $this->notifyNotFound($notFoundFixtures);
125 1
            }
126 4
        } else {
127 2
            $foundFixtures = $this->findFixtures();
128
        }
129
130 6
        $fixturesToLoad = array_diff($foundFixtures, $except);
131
132 6
        if (!$foundFixtures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $foundFixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
133 1
            throw new Exception(
134 1
                "No files were found by name: \"" . implode(', ', $fixturesInput) . "\".\n" .
135 1
                "Check that files with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"."
136 1
            );
137
        }
138
139 5
        if (!$fixturesToLoad) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixturesToLoad of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
140 1
            $this->notifyNothingToLoad($foundFixtures, $except);
141 1
            return static::EXIT_CODE_NORMAL;
142
        }
143
144 4
        if (!$this->confirmLoad($fixturesToLoad, $except)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirmLoad($fixturesToLoad, $except) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
145
            return static::EXIT_CODE_NORMAL;
146
        }
147
148 4
        $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToLoad));
149
150 4
        if (!$fixtures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
151
            throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . '');
152
        }
153
154 4
        $fixturesObjects = $this->createFixtures($fixtures);
155
156 4
        $this->unloadFixtures($fixturesObjects);
157 4
        $this->loadFixtures($fixturesObjects);
158 4
        $this->notifyLoaded($fixtures);
159
160 4
        return static::EXIT_CODE_NORMAL;
161
    }
162
163
    /**
164
     * Unloads the specified fixtures.
165
     * For example,
166
     *
167
     * ```
168
     * # unload the fixture data specified by User and UserProfile.
169
     * yii fixture/unload User UserProfile
170
     *
171
     * # unload all fixtures found under 'tests\unit\fixtures'
172
     * yii fixture/unload "*"
173
     *
174
     * # unload all fixtures except User and UserProfile
175
     * yii fixture/unload "*" -User -UserProfile
176
     * ```
177
     *
178
     * @throws Exception if the specified fixture does not exist.
179
     */
180 6
    public function actionUnload()
181
    {
182 6
        $fixturesInput = func_get_args();
183 6
        $filtered = $this->filterFixtures($fixturesInput);
184 6
        $except = $filtered['except'];
185
186 6
        if (!$this->needToApplyAll($fixturesInput[0])) {
187 4
            $fixtures = $filtered['apply'];
188
189 4
            $foundFixtures = $this->findFixtures($fixtures);
190 4
            $notFoundFixtures = array_diff($fixtures, $foundFixtures);
191
192 4
            if ($notFoundFixtures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $notFoundFixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
193 1
                $this->notifyNotFound($notFoundFixtures);
194 1
            }
195 4
        } else {
196 2
            $foundFixtures = $this->findFixtures();
197
        }
198
199 6
        $fixturesToUnload = array_diff($foundFixtures, $except);
200
201 6
        if (!$foundFixtures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $foundFixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
202 1
            throw new Exception(
203 1
                "No files were found by name: \"" . implode(', ', $fixturesInput) . "\".\n" .
204 1
                "Check that files with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"."
205 1
            );
206
        }
207
208 5
        if (!$fixturesToUnload) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixturesToUnload of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
209 1
            $this->notifyNothingToUnload($foundFixtures, $except);
210 1
            return static::EXIT_CODE_NORMAL;
211
        }
212
213 4
        if (!$this->confirmUnload($fixturesToUnload, $except)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirmUnload($fixturesToUnload, $except) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
214
            return static::EXIT_CODE_NORMAL;
215
        }
216
217 4
        $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToUnload));
218
219 4
        if (!$fixtures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
220
            throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".');
221
        }
222
223 4
        $this->unloadFixtures($this->createFixtures($fixtures));
224 4
        $this->notifyUnloaded($fixtures);
225 4
    }
226
227
    /**
228
     * Notifies user that fixtures were successfully loaded.
229
     * @param array $fixtures
230
     */
231 4
    private function notifyLoaded($fixtures)
232
    {
233 4
        $this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW);
234 4
        $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
235 4
        $this->outputList($fixtures);
236 4
    }
237
238
    /**
239
     * Notifies user that there are no fixtures to load according input conditions
240
     * @param array $foundFixtures array of found fixtures
241
     * @param array $except array of names of fixtures that should not be loaded
242
     */
243 1
    public function notifyNothingToLoad($foundFixtures, $except)
244
    {
245 1
        $this->stdout("Fixtures to load could not be found according given conditions:\n\n", Console::FG_RED);
246 1
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
247 1
        $this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
248
249 1
        if (count($foundFixtures)) {
250 1
            $this->stdout("\nFixtures founded under the namespace:\n\n", Console::FG_YELLOW);
251 1
            $this->outputList($foundFixtures);
252 1
        }
253
254 1
        if (count($except)) {
255 1
            $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
256 1
            $this->outputList($except);
257 1
        }
258 1
    }
259
260
    /**
261
     * Notifies user that there are no fixtures to unload according input conditions
262
     * @param array $foundFixtures array of found fixtures
263
     * @param array $except array of names of fixtures that should not be loaded
264
     */
265 1
    public function notifyNothingToUnload($foundFixtures, $except)
266
    {
267 1
        $this->stdout("Fixtures to unload could not be found according given conditions:\n\n", Console::FG_RED);
268 1
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
269 1
        $this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
270
271 1
        if (count($foundFixtures)) {
272 1
            $this->stdout("\nFixtures founded under the namespace:\n\n", Console::FG_YELLOW);
273 1
            $this->outputList($foundFixtures);
274 1
        }
275
276 1
        if (count($except)) {
277 1
            $this->stdout("\nFixtures that will NOT be unloaded: \n\n", Console::FG_YELLOW);
278 1
            $this->outputList($except);
279 1
        }
280 1
    }
281
282
    /**
283
     * Notifies user that fixtures were successfully unloaded.
284
     * @param array $fixtures
285
     */
286 4
    private function notifyUnloaded($fixtures)
287
    {
288 4
        $this->stdout("\nFixtures were successfully unloaded from namespace: ", Console::FG_YELLOW);
289 4
        $this->stdout(Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
290 4
        $this->outputList($fixtures);
291 4
    }
292
293
    /**
294
     * Notifies user that fixtures were not found under fixtures path.
295
     * @param array $fixtures
296
     */
297 2
    private function notifyNotFound($fixtures)
298
    {
299 2
        $this->stdout("Some fixtures were not found under path:\n", Console::BG_RED);
300 2
        $this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN);
301 2
        $this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED);
302 2
        $this->outputList($fixtures);
303 2
        $this->stdout("\n");
304 2
    }
305
306
    /**
307
     * Prompts user with confirmation if fixtures should be loaded.
308
     * @param array $fixtures
309
     * @param array $except
310
     * @return boolean
311
     */
312 4
    private function confirmLoad($fixtures, $except)
313
    {
314 4
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
315 4
        $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
316
317 4
        if (count($this->globalFixtures)) {
318 1
            $this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
319 1
            $this->outputList($this->globalFixtures);
320 1
        }
321
322 4
        if (count($fixtures)) {
323 4
            $this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW);
324 4
            $this->outputList($fixtures);
325 4
        }
326
327 4
        if (count($except)) {
328 2
            $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
329 2
            $this->outputList($except);
330 2
        }
331
332 4
        $this->stdout("\nBe aware that:\n", Console::BOLD);
333 4
        $this->stdout("Applying leads to purging of certain data in the database!\n", Console::FG_RED);
334
335 4
        return $this->confirm("\nLoad above fixtures?");
336
    }
337
338
    /**
339
     * Prompts user with confirmation for fixtures that should be unloaded.
340
     * @param array $fixtures
341
     * @param array $except
342
     * @return boolean
343
     */
344 4
    private function confirmUnload($fixtures, $except)
345
    {
346 4
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
347 4
        $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
348
349 4
        if (count($this->globalFixtures)) {
350 1
            $this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
351 1
            $this->outputList($this->globalFixtures);
352 1
        }
353
354 4
        if (count($fixtures)) {
355 4
            $this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW);
356 4
            $this->outputList($fixtures);
357 4
        }
358
359 4
        if (count($except)) {
360 2
            $this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW);
361 2
            $this->outputList($except);
362 2
        }
363
364 4
        return $this->confirm("\nUnload fixtures?");
365
    }
366
367
    /**
368
     * Outputs data to the console as a list.
369
     * @param array $data
370
     */
371 12
    private function outputList($data)
372
    {
373 12
        foreach ($data as $index => $item) {
374 12
            $this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN);
375 12
        }
376 12
    }
377
378
    /**
379
     * Checks if needed to apply all fixtures.
380
     * @param string $fixture
381
     * @return boolean
382
     */
383 12
    public function needToApplyAll($fixture)
384
    {
385 12
        return $fixture === '*';
386
    }
387
388
    /**
389
     * Finds fixtures to be loaded, for example "User", if no fixtures were specified then all of them
390
     * will be searching by suffix "Fixture.php".
391
     * @param array $fixtures fixtures to be loaded
392
     * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists.
393
     */
394 12
    private function findFixtures(array $fixtures = [])
395
    {
396 12
        $fixturesPath = $this->getFixturePath();
397
398 12
        $filesToSearch = ['*Fixture.php'];
399 12
        $findAll = ($fixtures === []);
400
401 12
        if (!$findAll) {
402 8
            $filesToSearch = [];
403
404 8
            foreach ($fixtures as $fileName) {
405 8
                $filesToSearch[] = $fileName . 'Fixture.php';
406 8
            }
407 8
        }
408
409 12
        $files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]);
0 ignored issues
show
Bug introduced by
It seems like $fixturesPath defined by $this->getFixturePath() on line 396 can also be of type boolean; however, yii\helpers\BaseFileHelper::findFiles() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
410 12
        $foundFixtures = [];
411
412 12
        foreach ($files as $fixture) {
413 10
            $foundFixtures[] = basename($fixture, 'Fixture.php');
414 12
        }
415
416 12
        return $foundFixtures;
417
    }
418
419
    /**
420
     * Returns valid fixtures config that can be used to load them.
421
     * @param array $fixtures fixtures to configure
422
     * @return array
423
     */
424 8
    private function getFixturesConfig($fixtures)
425
    {
426 8
        $config = [];
427
428 8
        foreach ($fixtures as $fixture) {
429 8
            $isNamespaced = (strpos($fixture, '\\') !== false);
430 8
            $fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture';
431
432 8
            if (class_exists($fullClassName)) {
433 8
                $config[] = $fullClassName;
434 8
            }
435 8
        }
436
437 8
        return $config;
438
    }
439
440
    /**
441
     * Filters fixtures by splitting them in two categories: one that should be applied and not.
442
     * If fixture is prefixed with "-", for example "-User", that means that fixture should not be loaded,
443
     * if it is not prefixed it is considered as one to be loaded. Returns array:
444
     *
445
     * ```php
446
     * [
447
     *     'apply' => [
448
     *         'User',
449
     *         ...
450
     *     ],
451
     *     'except' => [
452
     *         'Custom',
453
     *         ...
454
     *     ],
455
     * ]
456
     * ```
457
     * @param array $fixtures
458
     * @return array fixtures array with 'apply' and 'except' elements.
459
     */
460 12
    private function filterFixtures($fixtures)
461
    {
462
        $filtered = [
463 12
            'apply' => [],
464 12
            'except' => [],
465 12
        ];
466
467 12
        foreach ($fixtures as $fixture) {
468 12
            if (mb_strpos($fixture, '-') !== false) {
469 6
                $filtered['except'][] = str_replace('-', '', $fixture);
470 6
            } else {
471 12
                $filtered['apply'][] = $fixture;
472
            }
473 12
        }
474
475 12
        return $filtered;
476
    }
477
478
    /**
479
     * Returns fixture path that determined on fixtures namespace.
480
     * @return string fixture path
481
     */
482 12
    private function getFixturePath()
483
    {
484 12
        return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace));
485
    }
486
}
487