Passed
Pull Request — master (#20047)
by
unknown
09:40
created

FixtureController::confirmUnload()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 8
nop 2
dl 0
loc 21
ccs 13
cts 13
cp 1
crap 4
rs 9.8666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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\console\ExitCode;
16
use yii\helpers\Console;
17
use yii\helpers\FileHelper;
18
use yii\test\Fixture;
19
use yii\test\FixtureTrait;
20
21
/**
22
 * Manages fixture data loading and unloading.
23
 *
24
 * ```
25
 * #load fixtures from UsersFixture class with default namespace "tests\unit\fixtures"
26
 * yii fixture/load User
27
 *
28
 * #also a short version of this command (generate action is default)
29
 * yii fixture User
30
 *
31
 * #load all fixtures
32
 * yii fixture "*"
33
 *
34
 * #load all fixtures except User
35
 * yii fixture "*, -User"
36
 *
37
 * #load fixtures with different namespace.
38
 * yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
39
 * ```
40
 *
41
 * The `unload` sub-command can be used similarly to unload fixtures.
42
 *
43
 * @author Mark Jebri <[email protected]>
44
 * @since 2.0
45
 */
46
class FixtureController extends Controller
47
{
48
    use FixtureTrait;
0 ignored issues
show
Bug introduced by
The trait yii\test\FixtureTrait requires the property $depends which is not provided by yii\console\controllers\FixtureController.
Loading history...
49
50
    /**
51
     * @var string controller default action ID.
52
     */
53
    public $defaultAction = 'load';
54
    /**
55
     * @var string default namespace to search fixtures in
56
     */
57
    public $namespace = 'tests\unit\fixtures';
58
    /**
59
     * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture`
60
     * that disables and enables integrity check, so your data can be safely loaded.
61
     */
62
    public $globalFixtures = [
63
        'yii\test\InitDbFixture',
64
    ];
65
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function options($actionID)
71
    {
72
        return array_merge(parent::options($actionID), [
73
            'namespace', 'globalFixtures',
74
        ]);
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     * @since 2.0.8
80
     */
81
    public function optionAliases()
82
    {
83
        return array_merge(parent::optionAliases(), [
84
            'g' => 'globalFixtures',
85
            'n' => 'namespace',
86
        ]);
87
    }
88
89
    /**
90
     * Loads the specified fixture data.
91
     *
92
     * For example,
93
     *
94
     * ```
95
     * # load the fixture data specified by User and UserProfile.
96
     * # any existing fixture data will be removed first
97
     * yii fixture/load "User, UserProfile"
98
     *
99
     * # load all available fixtures found under 'tests\unit\fixtures'
100
     * yii fixture/load "*"
101
     *
102
     * # load all fixtures except User and UserProfile
103
     * yii fixture/load "*, -User, -UserProfile"
104
     * ```
105
     *
106
     * @param array $fixturesInput
107
     * @return int return code
108
     * @throws Exception if the specified fixture does not exist.
109
     */
110 8
    public function actionLoad(array $fixturesInput = [])
111
    {
112 8
        if ($fixturesInput === []) {
113
            $this->printHelpMessage();
114
            return ExitCode::OK;
115
        }
116
117 8
        $filtered = $this->filterFixtures($fixturesInput);
118 8
        $except = $filtered['except'];
119
120 8
        if (!$this->needToApplyAll($fixturesInput[0])) {
121 5
            $fixtures = $filtered['apply'];
122
123 5
            $foundFixtures = $this->findFixtures($fixtures);
124 5
            $notFoundFixtures = array_diff($fixtures, $foundFixtures);
125
126 5
            if ($notFoundFixtures !== []) {
127 5
                $this->notifyNotFound($notFoundFixtures);
128
            }
129
        } else {
130 3
            $foundFixtures = $this->findFixtures();
131
        }
132
133 8
        $fixturesToLoad = array_diff($foundFixtures, $except);
134
135 8
        if (!$foundFixtures) {
136 1
            throw new Exception(
137 1
                'No files were found for: "' . implode(', ', $fixturesInput) . "\".\n" .
138 1
                "Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . '".'
139
            );
140
        }
141
142 7
        if ($fixturesToLoad === []) {
143
            $this->notifyNothingToLoad($foundFixtures, $except);
144
            return ExitCode::OK;
145
        }
146
147 7
        if (!$this->confirmLoad($fixturesToLoad, $except)) {
148
            return ExitCode::OK;
149
        }
150
151 7
        $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToLoad));
152
153 7
        if (!$fixtures) {
154
            throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . '');
155
        }
156
157 7
        $fixturesObjects = $this->createFixtures($fixtures);
158
159 7
        $this->unloadFixtures($fixturesObjects);
160 7
        $this->loadFixtures($fixturesObjects);
161 7
        $this->notifyLoaded($fixturesObjects);
162
163 7
        return ExitCode::OK;
164
    }
165
166
    /**
167
     * Unloads the specified fixtures.
168
     *
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
        if ($fixturesInput === []) {
189
            $this->printHelpMessage();
190
            return ExitCode::OK;
191
        }
192
193 6
        $filtered = $this->filterFixtures($fixturesInput);
194 6
        $except = $filtered['except'];
195
196 6
        if (!$this->needToApplyAll($fixturesInput[0])) {
197 4
            $fixtures = $filtered['apply'];
198
199 4
            $foundFixtures = $this->findFixtures($fixtures);
200 4
            $notFoundFixtures = array_diff($fixtures, $foundFixtures);
201
202 4
            if ($notFoundFixtures !== []) {
203 4
                $this->notifyNotFound($notFoundFixtures);
204
            }
205
        } else {
206 2
            $foundFixtures = $this->findFixtures();
207
        }
208
209 6
        if ($foundFixtures === []) {
210 1
            throw new Exception(
211 1
                'No files were found for: "' . implode(', ', $fixturesInput) . "\".\n" .
212 1
                "Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . '".'
213
            );
214
        }
215
216 5
        $fixturesToUnload = array_diff($foundFixtures, $except);
217
218 5
        if ($fixturesToUnload === []) {
219
            $this->notifyNothingToUnload($foundFixtures, $except);
220
            return ExitCode::OK;
221
        }
222
223 5
        if (!$this->confirmUnload($fixturesToUnload, $except)) {
224
            return ExitCode::OK;
225
        }
226
227 5
        $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $fixturesToUnload));
228
229 5
        if ($fixtures === []) {
230
            throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".');
231
        }
232
233 5
        $this->unloadFixtures($this->createFixtures($fixtures));
234 5
        $this->notifyUnloaded($fixtures);
235 5
    }
236
237
    /**
238
     * Show help message.
239
     * @param array $fixturesInput
240
     */
241
    private function printHelpMessage()
242
    {
243
        $this->stdout($this->getHelpSummary() . "\n");
244
245
        $helpCommand = Console::ansiFormat('yii help fixture', [Console::FG_CYAN]);
246
        $this->stdout("Use $helpCommand to get usage info.\n");
247
    }
248
249
    /**
250
     * Notifies user that fixtures were successfully loaded.
251
     * @param Fixture[] $fixtures array of loaded fixtures
252
     */
253 7
    private function notifyLoaded($fixtures)
254
    {
255 7
        $this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW);
256 7
        $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
0 ignored issues
show
Bug introduced by
Are you sure Yii::getAlias($this->namespace) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

256
        $this->stdout("\t\"" . /** @scrutinizer ignore-type */ Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
Loading history...
257
258 7
        $fixtureClassNames = [];
259
260 7
        foreach ($fixtures as $fixture) {
261 7
            $fixtureClassNames[] = $fixture::className();
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

261
            $fixtureClassNames[] = /** @scrutinizer ignore-deprecated */ $fixture::className();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
262
        }
263
264 7
        $this->outputList($fixtureClassNames);
265 7
    }
266
267
    /**
268
     * Notifies user that there are no fixtures to load according input conditions.
269
     * @param array $foundFixtures array of found fixtures
270
     * @param array $except array of names of fixtures that should not be loaded
271
     */
272
    public function notifyNothingToLoad($foundFixtures, $except)
273
    {
274
        $this->stdout("Fixtures to load could not be found according given conditions:\n\n", Console::FG_RED);
275
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
276
        $this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
277
278
        if (count($foundFixtures)) {
279
            $this->stdout("\nFixtures founded under the namespace:\n\n", Console::FG_YELLOW);
280
            $this->outputList($foundFixtures);
281
        }
282
283
        if (count($except)) {
284
            $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
285
            $this->outputList($except);
286
        }
287
    }
288
289
    /**
290
     * Notifies user that there are no fixtures to unload according input conditions.
291
     * @param array $foundFixtures array of found fixtures
292
     * @param array $except array of names of fixtures that should not be loaded
293
     */
294
    public function notifyNothingToUnload($foundFixtures, $except)
295
    {
296
        $this->stdout("Fixtures to unload could not be found according to given conditions:\n\n", Console::FG_RED);
297
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
298
        $this->stdout("\t" . $this->namespace . "\n", Console::FG_GREEN);
299
300
        if (count($foundFixtures)) {
301
            $this->stdout("\nFixtures found under the namespace:\n\n", Console::FG_YELLOW);
302
            $this->outputList($foundFixtures);
303
        }
304
305
        if (count($except)) {
306
            $this->stdout("\nFixtures that will NOT be unloaded: \n\n", Console::FG_YELLOW);
307
            $this->outputList($except);
308
        }
309
    }
310
311
    /**
312
     * Notifies user that fixtures were successfully unloaded.
313
     * @param array $fixtures
314
     */
315 5
    private function notifyUnloaded($fixtures)
316
    {
317 5
        $this->stdout("\nFixtures were successfully unloaded from namespace: ", Console::FG_YELLOW);
318 5
        $this->stdout(Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
0 ignored issues
show
Bug introduced by
Are you sure Yii::getAlias($this->namespace) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

318
        $this->stdout(/** @scrutinizer ignore-type */ Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
Loading history...
319 5
        $this->outputList($fixtures);
320 5
    }
321
322
    /**
323
     * Notifies user that fixtures were not found under fixtures path.
324
     * @param array $fixtures
325
     */
326 2
    private function notifyNotFound($fixtures)
327
    {
328 2
        $this->stdout("Some fixtures were not found under path:\n", Console::BG_RED);
329 2
        $this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN);
330 2
        $this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED);
331 2
        $this->outputList($fixtures);
332 2
        $this->stdout("\n");
333 2
    }
334
335
    /**
336
     * Prompts user with confirmation if fixtures should be loaded.
337
     * @param array $fixtures
338
     * @param array $except
339
     * @return bool
340
     */
341 7
    private function confirmLoad($fixtures, $except)
342
    {
343 7
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
344 7
        $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
345
346 7
        if (count($this->globalFixtures)) {
347 2
            $this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
348 2
            $this->outputList($this->globalFixtures);
349
        }
350
351 7
        if (count($fixtures)) {
352 7
            $this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW);
353 7
            $this->outputList($fixtures);
354
        }
355
356 7
        if (count($except)) {
357 3
            $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
358 3
            $this->outputList($except);
359
        }
360
361 7
        $this->stdout("\nBe aware that:\n", Console::BOLD);
362 7
        $this->stdout("Applying leads to purging of certain data in the database!\n", Console::FG_RED);
363
364 7
        return $this->confirm("\nLoad above fixtures?");
365
    }
366
367
    /**
368
     * Prompts user with confirmation for fixtures that should be unloaded.
369
     * @param array $fixtures
370
     * @param array $except
371
     * @return bool
372
     */
373 5
    private function confirmUnload($fixtures, $except)
374
    {
375 5
        $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
376 5
        $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);
377
378 5
        if (count($this->globalFixtures)) {
379 1
            $this->stdout("Global fixtures will be used:\n\n", Console::FG_YELLOW);
380 1
            $this->outputList($this->globalFixtures);
381
        }
382
383 5
        if (count($fixtures)) {
384 5
            $this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW);
385 5
            $this->outputList($fixtures);
386
        }
387
388 5
        if (count($except)) {
389 3
            $this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW);
390 3
            $this->outputList($except);
391
        }
392
393 5
        return $this->confirm("\nUnload fixtures?");
394
    }
395
396
    /**
397
     * Outputs data to the console as a list.
398
     * @param array $data
399
     */
400 14
    private function outputList($data)
401
    {
402 14
        foreach ($data as $index => $item) {
403 14
            $this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN);
404
        }
405 14
    }
406
407
    /**
408
     * Checks if needed to apply all fixtures.
409
     * @param string $fixture
410
     * @return bool
411
     */
412 14
    public function needToApplyAll($fixture)
413
    {
414 14
        return $fixture === '*';
415
    }
416
417
    /**
418
     * Finds fixtures to be loaded, for example "User", if no fixtures were specified then all of them
419
     * will be searching by suffix "Fixture.php".
420
     * @param array $fixtures fixtures to be loaded
421
     * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists.
422
     */
423 14
    private function findFixtures(array $fixtures = [])
424
    {
425 14
        $fixturesPath = $this->getFixturePath();
426
427 14
        $filesToSearch = ['*Fixture.php'];
428 14
        $findAll = ($fixtures === []);
429
430 14
        if (!$findAll) {
431 9
            $filesToSearch = [];
432
433 9
            foreach ($fixtures as $fileName) {
434 9
                $filesToSearch[] = $fileName . 'Fixture.php';
435
            }
436
        }
437
438 14
        $files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]);
439 14
        $foundFixtures = [];
440
441 14
        foreach ($files as $fixture) {
442 12
            $foundFixtures[] = $this->getFixtureRelativeName($fixture);
443
        }
444
445 14
        return $foundFixtures;
446
    }
447
448
    /**
449
     * Calculates fixture's name
450
     * Basically, strips [[getFixturePath()]] and `Fixture.php' suffix from fixture's full path.
451
     * @see getFixturePath()
452
     * @param string $fullFixturePath Full fixture path
453
     * @return string Relative fixture name
454
     */
455 12
    private function getFixtureRelativeName($fullFixturePath)
456
    {
457 12
        $fixturesPath = FileHelper::normalizePath($this->getFixturePath());
458 12
        $fullFixturePath = FileHelper::normalizePath($fullFixturePath);
459
460 12
        $relativeName = substr($fullFixturePath, strlen($fixturesPath) + 1);
461 12
        $relativeDir = dirname($relativeName) === '.' ? '' : dirname($relativeName) . '/';
462
463 12
        return $relativeDir . basename($fullFixturePath, 'Fixture.php');
464
    }
465
466
    /**
467
     * Returns valid fixtures config that can be used to load them.
468
     * @param array $fixtures fixtures to configure
469
     * @return array
470
     */
471 12
    private function getFixturesConfig($fixtures)
472
    {
473 12
        $config = [];
474
475 12
        foreach ($fixtures as $fixture) {
476 12
            $isNamespaced = (strpos($fixture, '\\') !== false);
477
            // replace linux' path slashes to namespace backslashes, in case if $fixture is non-namespaced relative path
478 12
            $fixture = str_replace('/', '\\', $fixture);
479 12
            $fullClassName = $isNamespaced ? $fixture : $this->namespace . '\\' . $fixture;
480
481 12
            if (class_exists($fullClassName)) {
482 1
                $config[] = $fullClassName;
483 12
            } elseif (class_exists($fullClassName . 'Fixture')) {
484 12
                $config[] = $fullClassName . 'Fixture';
485
            } else {
486
                throw new Exception('Neither fixture "' . $fullClassName . '" nor "' . $fullClassName . 'Fixture" was found.');
487
            }
488
        }
489
490 12
        return $config;
491
    }
492
493
    /**
494
     * Filters fixtures by splitting them in two categories: one that should be applied and not.
495
     *
496
     * If fixture is prefixed with "-", for example "-User", that means that fixture should not be loaded,
497
     * if it is not prefixed it is considered as one to be loaded. Returns array:
498
     *
499
     * ```php
500
     * [
501
     *     'apply' => [
502
     *         'User',
503
     *         ...
504
     *     ],
505
     *     'except' => [
506
     *         'Custom',
507
     *         ...
508
     *     ],
509
     * ]
510
     * ```
511
     * @param array $fixtures
512
     * @return array fixtures array with 'apply' and 'except' elements.
513
     */
514 14
    private function filterFixtures($fixtures)
515
    {
516
        $filtered = [
517 14
            'apply' => [],
518
            'except' => [],
519
        ];
520
521 14
        foreach ($fixtures as $fixture) {
522 14
            if (mb_strpos($fixture, '-') !== false) {
523 6
                $filtered['except'][] = str_replace('-', '', $fixture);
524
            } else {
525 14
                $filtered['apply'][] = $fixture;
526
            }
527
        }
528
529 14
        return $filtered;
530
    }
531
532
    /**
533
     * Returns fixture path that determined on fixtures namespace.
534
     * @throws InvalidConfigException if fixture namespace is invalid
535
     * @return string fixture path
536
     */
537 14
    private function getFixturePath()
538
    {
539
        try {
540 14
            return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace));
0 ignored issues
show
Bug Best Practice introduced by
The expression return Yii::getAlias('@'...'/', $this->namespace)) could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
541
        } catch (InvalidParamException $e) {
542
            throw new InvalidConfigException('Invalid fixture namespace: "' . $this->namespace . '". Please, check your FixtureController::namespace parameter');
543
        }
544
    }
545
}
546