Passed
Push — master ( 9dbdd9...d5a428 )
by Alexander
04:15
created

console/controllers/FixtureController.php (1 issue)

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