Completed
Push — master ( 55b06d...9f2a87 )
by Alexander
35:57
created

console/controllers/FixtureController.php (2 issues)

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) {
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...
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
        $fixturesToUnload = array_diff($foundFixtures, $except);
209
210 6
        if (!$foundFixtures) {
211 1
            throw new Exception(
212 1
                'No files were found for: "' . implode(', ', $fixturesInput) . "\".\n" .
213 1
                "Check that files exist under fixtures path: \n\"" . $this->getFixturePath() . '".'
214
            );
215
        }
216
217 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...
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) . DIRECTORY_SEPARATOR;
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));
531
        } catch (InvalidParamException $e) {
532
            throw new InvalidConfigException('Invalid fixture namespace: "' . $this->namespace . '". Please, check your FixtureController::namespace parameter');
533
        }
534
    }
535
}
536