Starter   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 11
Bugs 4 Features 2
Metric Value
eloc 130
c 11
b 4
f 2
dl 0
loc 347
ccs 0
cts 229
cp 0
rs 2.88
wmc 69

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getGoals() 0 3 1
A readConfig() 0 9 2
A findRootDir() 0 10 3
A passthru() 0 7 1
A includeGoals() 0 6 2
A loadGoals() 0 5 2
A needsComposerInstall() 0 10 3
A getRootDir() 0 7 2
A readYaml() 0 3 1
A updateDotHidev() 0 4 2
A buildRootPath() 0 3 1
A setRootDir() 0 3 1
A hasAlias() 0 5 2
A loadEnv() 0 5 3
A addAutoloader() 0 10 4
A noProject() 0 4 1
A startProject() 0 10 1
B getConfig() 0 34 11
B addAliases() 0 16 9
A includeAll() 0 8 1
A __construct() 0 10 2
A moreConfig() 0 6 3
B requireAll() 0 24 9
A createDotenv() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like Starter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Starter, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Automation tool mixed with code generator for easier continuous development
4
 *
5
 * @link      https://github.com/hiqdev/hidev
6
 * @package   hidev
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2015-2018, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hidev\base;
12
13
use Dotenv\Dotenv;
0 ignored issues
show
Bug introduced by
The type Dotenv\Dotenv was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use hidev\components\Request;
15
use hidev\helpers\ConfigPlugin;
16
use hidev\helpers\FileHelper;
17
use hidev\helpers\Helper;
18
use Symfony\Component\Yaml\Yaml;
19
use Yii;
20
use yii\base\InvalidParamException;
21
use yii\helpers\ArrayHelper;
22
23
/**
24
 * Application starter.
25
 * Chdirs to the project's root directory and loads dependencies and configs.
26
 *
27
 * XXX it's important to distinguish:
28
 * - goals definitions (hidev config) - YAML files
29
 * - application config - PHP files
30
 * @author Andrii Vasyliev <[email protected]>
31
 */
32
class Starter
33
{
34
    /**
35
     * @var string absolute path to the project root directory
36
     */
37
    private $_rootDir;
38
39
    /**
40
     * @var array goals definitions
41
     */
42
    private $goals = [];
43
44
    /**
45
     * @var array application config files
46
     */
47
    private $appFiles = ['@hidev/config/basis.php'];
48
49
    /**
50
     * Make action.
51
     */
52
    public function __construct()
53
    {
54
        $request = new Request();
55
        $this->scriptFile = $request->getScriptFile();
0 ignored issues
show
Bug Best Practice introduced by
The property scriptFile does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
56
        $route = reset($request->resolve());
57
        $id = reset(explode('/', $route, 2));
58
        if (in_array($id, ['init'], true)) {
59
            $this->noProject();
60
        } else {
61
            $this->startProject();
62
        }
63
    }
64
65
    public function noProject()
66
    {
67
        $this->setRootDir(getcwd());
68
        $this->addAliases();
69
    }
70
71
    public function startProject()
72
    {
73
        $this->getRootDir();
74
        $this->addAutoloader();
75
        $this->loadEnv();
76
        $this->loadGoals();
77
        $this->addAliases();
78
        $this->requireAll();
79
        $this->includeAll();
80
        $this->moreConfig();
81
    }
82
83
    public function getConfig()
84
    {
85
        $config = ArrayHelper::merge($this->readConfig(), [
86
            'components' => $this->goals,
87
        ]);
88
89
        $config['components']['request']['scriptFile'] = $this->scriptFile;
90
        unset($config['components']['include']);
91
        unset($config['components']['plugins']);
92
93
        foreach ($config['components'] as $id => $def) {
94
            if (empty($def['class']) && empty($def['__class'])) {
95
                unset($config['components'][$id]);
96
                $controllers[$id] = $def;
97
            }
98
        }
99
        if (!empty($controllers)) {
100
            $config = ArrayHelper::merge($config, [
101
                'controllerMap' => $controllers,
102
            ]);
103
        }
104
105
        if (!empty($config['controllerMap'])) {
106
            foreach ($config['controllerMap'] as &$def) {
107
                if (is_array($def) && empty($def['class']) && empty($def['__class'])) {
108
                    $def[Helper::isYii20() ? 'class' : '__class'] = \hidev\console\CommonController::class;
109
                }
110
            }
111
        }
112
113
        $interpolator = new Interpolator();
114
        $interpolator->interpolate($config);
115
116
        return $config;
117
    }
118
119
    public function readConfig()
120
    {
121
        $config = [];
122
        foreach ($this->appFiles as $file) {
123
            $path = Yii::getAlias($file);
124
            $config = ArrayHelper::merge($config, require $path);
125
        }
126
127
        return $config;
128
    }
129
130
    public function getGoals()
131
    {
132
        return $this->goals;
133
    }
134
135
    public function addAutoloader()
136
    {
137
        $autoloader = './vendor/autoload.php';
138
        if (file_exists($autoloader)) {
139
            if (Helper::isYii20()) {
140
                spl_autoload_unregister(['Yii', 'autoload']);
141
            }
142
            require $autoloader;
143
            if (Helper::isYii20()) {
144
                spl_autoload_register(['Yii', 'autoload'], true, true);
145
            }
146
        }
147
    }
148
149
    private function loadEnv()
150
    {
151
        if (file_exists('.env') && class_exists(Dotenv::class)) {
152
            $dotenv = $this->createDotenv('.');
153
            $dotenv->load();
154
        }
155
    }
156
157
    /**
158
     * Creates Dotenv object.
159
     * Supports both 2 and 3 version of `phpdotenv`
160
     * @param mixed $dir
161
     * @param mixed $file
162
     * @return Dotenv
163
     */
164
    private function createDotenv($dir)
165
    {
166
        if (method_exists(Dotenv::class, 'create')) {
167
            return Dotenv::create($dir);
168
        } else {
169
            return new Dotenv($dir);
170
        }
171
    }
172
173
    private function loadGoals()
174
    {
175
        $this->includeGoals('hidev.yml');
176
        if (file_exists('hidev-local.yml')) {
177
            $this->includeGoals('hidev-local.yml');
178
        }
179
    }
180
181
    private function includeGoals($paths)
182
    {
183
        foreach ((array) $paths as $path) {
184
            $this->goals = ArrayHelper::merge(
185
                $this->goals,
186
                $this->readYaml($path)
187
            );
188
        }
189
    }
190
191
    private function readYaml($path)
192
    {
193
        return Yaml::parse(FileHelper::read($path));
0 ignored issues
show
Bug introduced by
It seems like hidev\helpers\FileHelper::read($path) can also be of type false; however, parameter $input of Symfony\Component\Yaml\Yaml::parse() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

193
        return Yaml::parse(/** @scrutinizer ignore-type */ FileHelper::read($path));
Loading history...
194
    }
195
196
    /**
197
     * Adds aliases:
198
     * - @root alias to current project root dir
199
     * - @hidev own alias
200
     * - current package namespace for it could be used from hidev
201
     * - aliases listed in config.
202
     */
203
    private function addAliases()
204
    {
205
        Yii::setAlias('@root', $this->getRootDir());
206
        Yii::setAlias('@hidev', dirname(__DIR__));
207
208
        $package = $this->goals['package'];
209
        $alias  = isset($package['namespace']) ? strtr($package['namespace'], '\\', '/') : '';
210
        if ($alias && !Yii::getAlias('@' . $alias, false)) {
211
            $srcdir = Yii::getAlias('@root/' . ($package['src'] ?: 'src'));
212
            Yii::setAlias($alias, $srcdir);
213
        }
214
        $aliases = $this->goals['aliases'];
215
        if (!empty($aliases) && is_array($aliases)) {
216
            foreach ($aliases as $alias => $path) {
217
                if (!$this->hasAlias($alias)) {
218
                    Yii::setAlias($alias, $path);
219
                }
220
            }
221
        }
222
    }
223
224
    private function hasAlias($alias, $exact = true)
0 ignored issues
show
Unused Code introduced by
The parameter $exact is not used and could be removed. ( Ignorable by Annotation )

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

224
    private function hasAlias($alias, /** @scrutinizer ignore-unused */ $exact = true)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
225
    {
226
        $pos = strpos($alias, '/');
227
228
        return $pos === false ? isset(Yii::$aliases[$alias]) : isset(Yii::$aliases[substr($alias, 0, $pos)][$alias]);
229
    }
230
231
    /**
232
     * - install configured plugins and register their app config
233
     * - install project dependencies and register
234
     * - register application config files.
235
     */
236
    private function requireAll()
237
    {
238
        $vendors = [];
239
        $plugins = $this->goals['plugins'];
240
        if ($plugins) {
241
            $file = File::create('.hidev/composer.json');
242
            $data = ArrayHelper::merge($file->load(), ['require' => $plugins]);
243
            if ($file->save($data) || !is_dir('.hidev/vendor')) {
244
                $this->updateDotHidev();
245
            }
246
            $vendors[] = $this->buildRootPath('.hidev/vendor');
247
        }
248
        if ($this->needsComposerInstall()) {
249
            if ($this->passthru('composer', ['install', '--ansi'])) {
250
                throw new InvalidParamException('Failed initialize project with composer install');
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

250
                throw /** @scrutinizer ignore-deprecated */ new InvalidParamException('Failed initialize project with composer install');
Loading history...
251
            }
252
        }
253
        $vendors[] = $this->buildRootPath('vendor');
254
255
        foreach ($vendors as $vendor) {
256
            foreach (['console', 'hidev'] as $name) {
257
                $path = ConfigPlugin::path($name, $vendor);
258
                if (file_exists($path)) {
259
                    $this->appFiles[] = $path;
260
                }
261
            }
262
        }
263
    }
264
265
    /**
266
     * Update action.
267
     * @return int exit code
268
     */
269
    public function updateDotHidev()
270
    {
271
        if (file_exists('.hidev/composer.json')) {
272
            return $this->passthru('composer', ['update', '-d', '.hidev', '--prefer-source', '--ansi']);
273
        }
274
    }
275
276
    /**
277
     * Passthru command.
278
     * @param string $command
279
     * @param array $args
280
     * @return int exit code
281
     */
282
    private function passthru($command, $args)
283
    {
284
        $binary = new BinaryPhp([
285
            'name' => $command,
286
        ]);
287
288
        return $binary->passthru($args);
289
    }
290
291
    private function needsComposerInstall()
292
    {
293
        if (file_exists('vendor')) {
294
            return false;
295
        }
296
        if (!file_exists('composer.json')) {
297
            return false;
298
        }
299
300
        return true;
301
302
        /*
303
            $data = File::create('composer.json')->load();
304
            foreach (['require', 'require-dev'] as $key) {
305
                if (isset($data[$key])) {
306
                    foreach ($data[$key] as $package => $version) {
307
                        list(, $name) = explode('/', $package);
308
                        if (strncmp($name, 'hidev-', 6) === 0) {
309
                            return true;
310
                        }
311
                    }
312
                }
313
            }
314
315
            return false;
316
        */
317
    }
318
319
    /**
320
     * Include all configured includes.
321
     */
322
    private function includeAll()
323
    {
324
        $config = $this->readConfig();
325
        $files = array_merge(
326
            (array) $this->goals['include'],
327
            (array) $config['components']['include']
328
        );
329
        $this->includeGoals($files);
330
    }
331
332
    /**
333
     * Registers more application config to load.
334
     */
335
    private function moreConfig()
336
    {
337
        $paths = $this->goals['config'];
338
        foreach ((array) $paths as $path) {
339
            if ($path) {
340
                $this->appFiles[] = $path;
341
            }
342
        }
343
    }
344
345
    public function setRootDir($value)
346
    {
347
        $this->_rootDir = $value;
348
    }
349
350
    public function getRootDir()
351
    {
352
        if ($this->_rootDir === null) {
353
            $this->_rootDir = $this->findRootDir();
354
        }
355
356
        return $this->_rootDir;
357
    }
358
359
    /**
360
     * Chdirs to project's root by looking for config file in the current directory and up.
361
     * @throws InvalidParamException when failed to find
362
     * @return string path to the root directory of hidev project
363
     */
364
    private function findRootDir()
365
    {
366
        $configFile = 'hidev.yml';
367
        for ($i = 0; $i < 9; ++$i) {
368
            if (file_exists($configFile)) {
369
                return getcwd();
370
            }
371
            chdir('..');
372
        }
373
        throw new InvalidParamException("Not a hidev project (or any of the parent directories).\nUse `hidev init` to initialize hidev project.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

373
        throw /** @scrutinizer ignore-deprecated */ new InvalidParamException("Not a hidev project (or any of the parent directories).\nUse `hidev init` to initialize hidev project.");
Loading history...
374
    }
375
376
    public function buildRootPath($subpath)
377
    {
378
        return $this->getRootDir() . DIRECTORY_SEPARATOR . $subpath;
379
    }
380
}
381