Completed
Push — master ( 658918...bcc56d )
by Dmitry
28:01 queued 24:50
created

FixtureController::getFixturePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.5

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 2
cts 4
cp 0.5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
crap 2.5
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\base\InvalidConfigException;
12
use yii\base\InvalidParamException;
13
use yii\console\Controller;
14
use yii\console\Exception;
15
use yii\helpers\Console;
16
use yii\helpers\FileHelper;
17
use yii\test\FixtureTrait;
18
19
/**
20
 * Manages fixture data loading and unloading.
21
 *
22
 * ```
23
 * #load fixtures from UsersFixture class with default namespace "tests\unit\fixtures"
24
 * yii fixture/load User
25
 *
26
 * #also a short version of this command (generate action is default)
27
 * yii fixture User
28
 *
29
 * #load all fixtures
30
 * yii fixture "*"
31
 *
32
 * #load all fixtures except User
33
 * yii fixture "*, -User"
34
 *
35
 * #load fixtures with different namespace.
36
 * yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
37
 * ```
38
 *
39
 * The `unload` sub-command can be used similarly to unload fixtures.
40
 *
41
 * @author Mark Jebri <[email protected]>
42
 * @since 2.0
43
 */
44
class FixtureController extends Controller
45
{
46
    use FixtureTrait;
47
48
    /**
49
     * @var string controller default action ID.
50
     */
51
    public $defaultAction = 'load';
52
    /**
53
     * @var string default namespace to search fixtures in
54
     */
55
    public $namespace = 'tests\unit\fixtures';
56
    /**
57
     * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture`
58
     * that disables and enables integrity check, so your data can be safely loaded.
59
     */
60
    public $globalFixtures = [
61
        'yii\test\InitDb',
62
    ];
63
64
65
    /**
66
     * @inheritdoc
67
     */
68
    public function options($actionID)
69
    {
70
        return array_merge(parent::options($actionID), [
71
            'namespace', 'globalFixtures'
72
        ]);
73
    }
74
75
    /**
76
     * @inheritdoc
77
     * @since 2.0.8
78
     */
79
    public function optionAliases()
80
    {
81
        return array_merge(parent::optionAliases(), [
82
            'g' => 'globalFixtures',
83
            'n' => 'namespace',
84
        ]);
85
    }
86
87
    /**
88
     * Loads the specified fixture data.
89
     * For example,
90
     *
91
     * ```
92
     * # load the fixture data specified by User and UserProfile.
93
     * # any existing fixture data will be removed first
94
     * yii fixture/load "User, UserProfile"
95
     *
96
     * # load all available fixtures found under 'tests\unit\fixtures'
97
     * yii fixture/load "*"
98
     *
99
     * # load all fixtures except User and UserProfile
100
     * yii fixture/load "*, -User, -UserProfile"
101
     * ```
102
     *
103
     * @param array $fixturesInput
104
     * @return int return code
105
     * @throws Exception if the specified fixture does not exist.
106
     */
107 6
    public function actionLoad(array $fixturesInput = [])
108
    {
109 6
        if ($fixturesInput === []) {
110
            $this->stdout($this->getHelpSummary() . "\n");
111
112
            $helpCommand = Console::ansiFormat('yii help fixture', [Console::FG_CYAN]);
113
            $this->stdout("Use $helpCommand to get usage info.\n");
114
115
            return self::EXIT_CODE_NORMAL;
116
        }
117
118 6
        $filtered = $this->filterFixtures($fixturesInput);
119 6
        $except = $filtered['except'];
120
121 6
        if (!$this->needToApplyAll($fixturesInput[0])) {
122 4
            $fixtures = $filtered['apply'];
123
124 4
            $foundFixtures = $this->findFixtures($fixtures);
125 4
            $notFoundFixtures = array_diff($fixtures, $foundFixtures);
126
127 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...
128 1
                $this->notifyNotFound($notFoundFixtures);
129 1
            }
130 4
        } else {
131 2
            $foundFixtures = $this->findFixtures();
132
        }
133
134 6
        $fixturesToLoad = array_diff($foundFixtures, $except);
135
136 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...
137 1
            throw new Exception(
138 1
                "No files were found for: \"" . implode(', ', $fixturesInput) . "\".\n" .
139 1
                "Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . "\"."
140 1
            );
141
        }
142
143 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...
144 1
            $this->notifyNothingToLoad($foundFixtures, $except);
145 1
            return static::EXIT_CODE_NORMAL;
146
        }
147
148 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...
149
            return static::EXIT_CODE_NORMAL;
150
        }
151
152 4
        $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToLoad));
153
154 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...
155
            throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . '');
156
        }
157
158 4
        $fixturesObjects = $this->createFixtures($fixtures);
159
160 4
        $this->unloadFixtures($fixturesObjects);
161 4
        $this->loadFixtures($fixturesObjects);
162 4
        $this->notifyLoaded($fixtures);
163
164 4
        return static::EXIT_CODE_NORMAL;
165
    }
166
167
    /**
168
     * Unloads the specified fixtures.
169
     * For example,
170
     *
171
     * ```
172
     * # unload the fixture data specified by User and UserProfile.
173
     * yii fixture/unload "User, UserProfile"
174
     *
175
     * # unload all fixtures found under 'tests\unit\fixtures'
176
     * yii fixture/unload "*"
177
     *
178
     * # unload all fixtures except User and UserProfile
179
     * yii fixture/unload "*, -User, -UserProfile"
180
     * ```
181
     *
182
     * @param array $fixturesInput
183
     * @return int return code
184
     * @throws Exception if the specified fixture does not exist.
185
     */
186 6
    public function actionUnload(array $fixturesInput = [])
187
    {
188 6
        $filtered = $this->filterFixtures($fixturesInput);
189 6
        $except = $filtered['except'];
190
191 6
        if (!$this->needToApplyAll($fixturesInput[0])) {
192 4
            $fixtures = $filtered['apply'];
193
194 4
            $foundFixtures = $this->findFixtures($fixtures);
195 4
            $notFoundFixtures = array_diff($fixtures, $foundFixtures);
196
197 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...
198 1
                $this->notifyNotFound($notFoundFixtures);
199 1
            }
200 4
        } else {
201 2
            $foundFixtures = $this->findFixtures();
202
        }
203
204 6
        $fixturesToUnload = array_diff($foundFixtures, $except);
205
206 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...
207 1
            throw new Exception(
208 1
                "No files were found for: \"" . implode(', ', $fixturesInput) . "\".\n" .
209 1
                "Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . "\"."
210 1
            );
211
        }
212
213 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...
214 1
            $this->notifyNothingToUnload($foundFixtures, $except);
215 1
            return static::EXIT_CODE_NORMAL;
216
        }
217
218 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...
219
            return static::EXIT_CODE_NORMAL;
220
        }
221
222 4
        $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToUnload));
223
224 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...
225
            throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".');
226
        }
227
228 4
        $this->unloadFixtures($this->createFixtures($fixtures));
229 4
        $this->notifyUnloaded($fixtures);
230 4
    }
231
232
    /**
233
     * Notifies user that fixtures were successfully loaded.
234
     * @param array $fixtures
235
     */
236 4
    private function notifyLoaded($fixtures)
237
    {
238 4
        $this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW);
239 4
        $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
240 4
        $this->outputList($fixtures);
241 4
    }
242
243
    /**
244
     * Notifies user that there are no fixtures to load according input conditions
245
     * @param array $foundFixtures array of found fixtures
246
     * @param array $except array of names of fixtures that should not be loaded
247
     */
248 1
    public function notifyNothingToLoad($foundFixtures, $except)
249
    {
250 1
        $this->stdout("Fixtures to load could not be found according given conditions:\n\n", Console::FG_RED);
251 1
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
252 1
        $this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
253
254 1
        if (count($foundFixtures)) {
255 1
            $this->stdout("\nFixtures founded under the namespace:\n\n", Console::FG_YELLOW);
256 1
            $this->outputList($foundFixtures);
257 1
        }
258
259 1
        if (count($except)) {
260 1
            $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
261 1
            $this->outputList($except);
262 1
        }
263 1
    }
264
265
    /**
266
     * Notifies user that there are no fixtures to unload according input conditions
267
     * @param array $foundFixtures array of found fixtures
268
     * @param array $except array of names of fixtures that should not be loaded
269
     */
270 1
    public function notifyNothingToUnload($foundFixtures, $except)
271
    {
272 1
        $this->stdout("Fixtures to unload could not be found according to given conditions:\n\n", Console::FG_RED);
273 1
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
274 1
        $this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
275
276 1
        if (count($foundFixtures)) {
277 1
            $this->stdout("\nFixtures found under the namespace:\n\n", Console::FG_YELLOW);
278 1
            $this->outputList($foundFixtures);
279 1
        }
280
281 1
        if (count($except)) {
282 1
            $this->stdout("\nFixtures that will NOT be unloaded: \n\n", Console::FG_YELLOW);
283 1
            $this->outputList($except);
284 1
        }
285 1
    }
286
287
    /**
288
     * Notifies user that fixtures were successfully unloaded.
289
     * @param array $fixtures
290
     */
291 4
    private function notifyUnloaded($fixtures)
292
    {
293 4
        $this->stdout("\nFixtures were successfully unloaded from namespace: ", Console::FG_YELLOW);
294 4
        $this->stdout(Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
295 4
        $this->outputList($fixtures);
296 4
    }
297
298
    /**
299
     * Notifies user that fixtures were not found under fixtures path.
300
     * @param array $fixtures
301
     */
302 2
    private function notifyNotFound($fixtures)
303
    {
304 2
        $this->stdout("Some fixtures were not found under path:\n", Console::BG_RED);
305 2
        $this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN);
306 2
        $this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED);
307 2
        $this->outputList($fixtures);
308 2
        $this->stdout("\n");
309 2
    }
310
311
    /**
312
     * Prompts user with confirmation if fixtures should be loaded.
313
     * @param array $fixtures
314
     * @param array $except
315
     * @return bool
316
     */
317 4
    private function confirmLoad($fixtures, $except)
318
    {
319 4
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
320 4
        $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
321
322 4
        if (count($this->globalFixtures)) {
323 1
            $this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
324 1
            $this->outputList($this->globalFixtures);
325 1
        }
326
327 4
        if (count($fixtures)) {
328 4
            $this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW);
329 4
            $this->outputList($fixtures);
330 4
        }
331
332 4
        if (count($except)) {
333 2
            $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
334 2
            $this->outputList($except);
335 2
        }
336
337 4
        $this->stdout("\nBe aware that:\n", Console::BOLD);
338 4
        $this->stdout("Applying leads to purging of certain data in the database!\n", Console::FG_RED);
339
340 4
        return $this->confirm("\nLoad above fixtures?");
341
    }
342
343
    /**
344
     * Prompts user with confirmation for fixtures that should be unloaded.
345
     * @param array $fixtures
346
     * @param array $except
347
     * @return bool
348
     */
349 4
    private function confirmUnload($fixtures, $except)
350
    {
351 4
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
352 4
        $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
353
354 4
        if (count($this->globalFixtures)) {
355 1
            $this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
356 1
            $this->outputList($this->globalFixtures);
357 1
        }
358
359 4
        if (count($fixtures)) {
360 4
            $this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW);
361 4
            $this->outputList($fixtures);
362 4
        }
363
364 4
        if (count($except)) {
365 2
            $this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW);
366 2
            $this->outputList($except);
367 2
        }
368
369 4
        return $this->confirm("\nUnload fixtures?");
370
    }
371
372
    /**
373
     * Outputs data to the console as a list.
374
     * @param array $data
375
     */
376 12
    private function outputList($data)
377
    {
378 12
        foreach ($data as $index => $item) {
379 12
            $this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN);
380 12
        }
381 12
    }
382
383
    /**
384
     * Checks if needed to apply all fixtures.
385
     * @param string $fixture
386
     * @return bool
387
     */
388 12
    public function needToApplyAll($fixture)
389
    {
390 12
        return $fixture === '*';
391
    }
392
393
    /**
394
     * Finds fixtures to be loaded, for example "User", if no fixtures were specified then all of them
395
     * will be searching by suffix "Fixture.php".
396
     * @param array $fixtures fixtures to be loaded
397
     * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists.
398
     */
399 12
    private function findFixtures(array $fixtures = [])
400
    {
401 12
        $fixturesPath = $this->getFixturePath();
402
403 12
        $filesToSearch = ['*Fixture.php'];
404 12
        $findAll = ($fixtures === []);
405
406 12
        if (!$findAll) {
407 8
            $filesToSearch = [];
408
409 8
            foreach ($fixtures as $fileName) {
410 8
                $filesToSearch[] = $fileName . 'Fixture.php';
411 8
            }
412 8
        }
413
414 12
        $files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]);
0 ignored issues
show
Bug introduced by
It seems like $fixturesPath defined by $this->getFixturePath() on line 401 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...
415 12
        $foundFixtures = [];
416
417 12
        foreach ($files as $fixture) {
418 10
            $foundFixtures[] = basename($fixture, 'Fixture.php');
419 12
        }
420
421 12
        return $foundFixtures;
422
    }
423
424
    /**
425
     * Returns valid fixtures config that can be used to load them.
426
     * @param array $fixtures fixtures to configure
427
     * @return array
428
     */
429 8
    private function getFixturesConfig($fixtures)
430
    {
431 8
        $config = [];
432
433 8
        foreach ($fixtures as $fixture) {
434 8
            $isNamespaced = (strpos($fixture, '\\') !== false);
435 8
            $fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture';
436
437 8
            if (class_exists($fullClassName)) {
438 8
                $config[] = $fullClassName;
439 8
            }
440 8
        }
441
442 8
        return $config;
443
    }
444
445
    /**
446
     * Filters fixtures by splitting them in two categories: one that should be applied and not.
447
     * If fixture is prefixed with "-", for example "-User", that means that fixture should not be loaded,
448
     * if it is not prefixed it is considered as one to be loaded. Returns array:
449
     *
450
     * ```php
451
     * [
452
     *     'apply' => [
453
     *         'User',
454
     *         ...
455
     *     ],
456
     *     'except' => [
457
     *         'Custom',
458
     *         ...
459
     *     ],
460
     * ]
461
     * ```
462
     * @param array $fixtures
463
     * @return array fixtures array with 'apply' and 'except' elements.
464
     */
465 12
    private function filterFixtures($fixtures)
466
    {
467
        $filtered = [
468 12
            'apply' => [],
469 12
            'except' => [],
470 12
        ];
471
472 12
        foreach ($fixtures as $fixture) {
473 12
            if (mb_strpos($fixture, '-') !== false) {
474 6
                $filtered['except'][] = str_replace('-', '', $fixture);
475 6
            } else {
476 12
                $filtered['apply'][] = $fixture;
477
            }
478 12
        }
479
480 12
        return $filtered;
481
    }
482
483
    /**
484
     * Returns fixture path that determined on fixtures namespace.
485
     * @throws InvalidConfigException if fixture namespace is invalid
486
     * @return string fixture path
487
     */
488 12
    private function getFixturePath()
489
    {
490
        try {
491 12
            return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace));
492
        } catch (InvalidParamException $e) {
493
            throw new InvalidConfigException('Invalid fixture namespace: "' . $this->namespace . '". Please, check your FixtureController::namespace parameter');
494
        }
495
    }
496
}
497