Completed
Push — master ( f53f7d...572ce6 )
by Andrii
03:13
created

Starter::addAutoloader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
ccs 0
cts 9
cp 0
rs 9.6666
cc 2
eloc 6
nc 2
nop 0
crap 6
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-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hidev\base;
12
13
use Dotenv\Dotenv;
14
use hidev\components\Request;
15
use hidev\helpers\ConfigPlugin;
16
use hidev\helpers\FileHelper;
17
use Symfony\Component\Yaml\Yaml;
18
use Yii;
19
use yii\base\InvalidParamException;
20
use yii\helpers\ArrayHelper;
21
22
/**
23
 * Application starter.
24
 * Chdirs to the project's root directory and loads dependencies and configs.
25
 *
26
 * XXX it's important to distinguish:
27
 * - goals definitions (hidev config) - YAML files
28
 * - application config - PHP files
29
 * @author Andrii Vasyliev <[email protected]>
30
 */
31
class Starter
32
{
33
    /**
34
     * @var string absolute path to the project root directory
35
     */
36
    private $_rootDir;
37
38
    /**
39
     * @var array goals definitions
40
     */
41
    private $goals = [];
42
43
    /**
44
     * @var array application config files
45
     */
46
    private $appFiles = ['@hidev/config/basis.php'];
47
48
    /**
49
     * Make action.
50
     */
51
    public function __construct()
52
    {
53
        $request = new Request();
54
        $this->scriptFile = $request->getScriptFile();
0 ignored issues
show
Bug introduced by
The property scriptFile does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
55
        $route = reset($request->resolve());
0 ignored issues
show
Bug introduced by
$request->resolve() cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
56
        $id = reset(explode('/', $route, 2));
0 ignored issues
show
Bug introduced by
explode('/', $route, 2) cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
57
        if (in_array($id, ['init'], true)) {
58
            $this->noProject();
59
        } else {
60
            $this->startProject();
61
        }
62
    }
63
64
    public function noProject()
65
    {
66
        $this->setRootDir(getcwd());
67
        $this->addAliases();
68
    }
69
70
    public function startProject()
71
    {
72
        $this->getRootDir();
73
        $this->addAutoloader();
74
        $this->loadEnv();
75
        $this->loadGoals();
76
        $this->addAliases();
77
        $this->requireAll();
78
        $this->includeAll();
79
        $this->moreConfig();
80
    }
81
82
    public function getConfig()
83
    {
84
        $config = ArrayHelper::merge($this->readConfig(), [
85
            'components' => $this->goals,
86
        ]);
87
88
        $config['components']['request']['scriptFile'] = $this->scriptFile;
89
        unset($config['components']['include']);
90
        unset($config['components']['plugins']);
91
92
        foreach ($config['components'] as $id => $def) {
93
            if (empty($def['class'])) {
94
                unset($config['components'][$id]);
95
                $controllers[$id] = $def;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$controllers was never initialized. Although not strictly required by PHP, it is generally a good practice to add $controllers = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
96
            }
97
        }
98
        if (!empty($controllers)) {
99
            $config = ArrayHelper::merge($config, [
100
                'controllerMap' => $controllers,
101
            ]);
102
        }
103
104
        if (!empty($config['controllerMap'])) {
105
            foreach ($config['controllerMap'] as &$def) {
106
                if (is_array($def) && empty($def['class'])) {
107
                    $def['class'] = \hidev\controllers\CommonController::class;
108
                }
109
            }
110
        }
111
112
        $interpolator = new Interpolator();
113
        $interpolator->interpolate($config);
114
115
        return $config;
116
    }
117
118
    public function readConfig()
119
    {
120
        $config = [];
121
        foreach ($this->appFiles as $file) {
122
            $path = Yii::getAlias($file);
123
            $config = ArrayHelper::merge($config, require $path);
124
        }
125
126
        return $config;
127
    }
128
129
    public function getGoals()
130
    {
131
        return $this->goals;
132
    }
133
134
    public function addAutoloader()
135
    {
136
        $autoloader = './vendor/autoload.php';
137
        if (file_exists($autoloader)) {
138
            spl_autoload_unregister(['Yii', 'autoload']);
139
            require $autoloader;
140
            spl_autoload_register(['Yii', 'autoload'], true, true);
141
        }
142
    }
143
144
    private function loadEnv()
145
    {
146
        if (file_exists('.env') && class_exists(Dotenv::class)) {
147
            $dotenv = new Dotenv('.');
148
            $dotenv->load();
149
        }
150
    }
151
152
    private function loadGoals()
153
    {
154
        $this->includeGoals('hidev.yml');
155
        if (file_exists('hidev-local.yml')) {
156
            $this->includeGoals('hidev-local.yml');
157
        }
158
    }
159
160
    private function includeGoals($paths)
161
    {
162
        foreach ((array) $paths as $path) {
163
            $this->goals = ArrayHelper::merge(
164
                $this->goals,
165
                $this->readYaml($path)
166
            );
167
        }
168
    }
169
170
    private function readYaml($path)
171
    {
172
        return Yaml::parse(FileHelper::read($path));
173
    }
174
175
    /**
176
     * Adds aliases:
177
     * - @root alias to current project root dir
178
     * - @hidev own alias
179
     * - current package namespace for it could be used from hidev
180
     * - aliases listed in config.
181
     */
182
    private function addAliases()
183
    {
184
        Yii::setAlias('@root', $this->getRootDir());
185
        Yii::setAlias('@hidev', dirname(__DIR__));
186
187
        $package = $this->goals['package'];
188
        $alias  = isset($package['namespace']) ? strtr($package['namespace'], '\\', '/') : '';
189
        if ($alias && !Yii::getAlias('@' . $alias, false)) {
190
            $srcdir = Yii::getAlias('@root/' . ($package['src'] ?: 'src'));
191
            Yii::setAlias($alias, $srcdir);
0 ignored issues
show
Bug introduced by
It seems like $srcdir defined by \Yii::getAlias('@root/' ...ckage['src'] ?: 'src')) on line 190 can also be of type boolean; however, yii\BaseYii::setAlias() 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...
192
        }
193
        $aliases = $this->goals['aliases'];
194
        if (!empty($aliases) && is_array($aliases)) {
195
            foreach ($aliases as $alias => $path) {
196
                if (!$this->hasAlias($alias)) {
197
                    Yii::setAlias($alias, $path);
198
                }
199
            }
200
        }
201
    }
202
203
    private function hasAlias($alias, $exact = true)
0 ignored issues
show
Unused Code introduced by
The parameter $exact is not used and could be removed.

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

Loading history...
204
    {
205
        $pos = strpos($alias, '/');
206
207
        return $pos === false ? isset(Yii::$aliases[$alias]) : isset(Yii::$aliases[substr($alias, 0, $pos)][$alias]);
208
    }
209
210
    /**
211
     * - install configured plugins and register their app config
212
     * - install project dependencies and register
213
     * - register application config files.
214
     */
215
    private function requireAll()
216
    {
217
        $vendors = [];
218
        $plugins = $this->goals['plugins'];
219
        if ($plugins) {
220
            $file = File::create('.hidev/composer.json');
221
            $data = ArrayHelper::merge($file->load(), ['require' => $plugins]);
222
            if ($file->save($data) || !is_dir('.hidev/vendor')) {
223
                $this->updateDotHidev();
224
            }
225
            $vendors[] = $this->buildRootPath('.hidev/vendor');
226
        }
227
        if ($this->needsComposerInstall()) {
228
            if ($this->passthru('composer', ['install', '--ansi'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->passthru('compose...y('install', '--ansi')) of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
229
                throw new InvalidParamException('Failed initialize project with composer install');
230
            }
231
        }
232
        $vendors[] = $this->buildRootPath('vendor');
233
234
        foreach ($vendors as $vendor) {
235
            foreach (['console', 'hidev'] as $name) {
236
                $path = ConfigPlugin::path($name, $vendor);
237
                if (file_exists($path)) {
238
                    $this->appFiles[] = $path;
239
                }
240
            }
241
        }
242
    }
243
244
    /**
245
     * Update action.
246
     * @return int exit code
247
     */
248
    public function updateDotHidev()
249
    {
250
        if (file_exists('.hidev/composer.json')) {
251
            return $this->passthru('composer', ['update', '-d', '.hidev', '--prefer-source', '--ansi']);
252
        }
253
    }
254
255
    /**
256
     * Passthru command.
257
     * @param string $command
258
     * @param array $args
259
     * @return int exit code
260
     */
261
    private function passthru($command, $args)
262
    {
263
        $binary = new BinaryPhp([
264
            'name' => $command,
265
        ]);
266
267
        return $binary->passthru($args);
268
    }
269
270
    private function needsComposerInstall()
271
    {
272
        if (file_exists('vendor')) {
273
            return false;
274
        }
275
        if (!file_exists('composer.json')) {
276
            return false;
277
        }
278
279
        return true;
280
281
    /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
282
        $data = File::create('composer.json')->load();
283
        foreach (['require', 'require-dev'] as $key) {
284
            if (isset($data[$key])) {
285
                foreach ($data[$key] as $package => $version) {
286
                    list(, $name) = explode('/', $package);
287
                    if (strncmp($name, 'hidev-', 6) === 0) {
288
                        return true;
289
                    }
290
                }
291
            }
292
        }
293
294
        return false;
295
    */
296
    }
297
298
    /**
299
     * Include all configured includes.
300
     */
301
    private function includeAll()
302
    {
303
        $config = $this->readConfig();
304
        $files = array_merge(
305
            (array) $this->goals['include'],
306
            (array) $config['components']['include']
307
        );
308
        $this->includeGoals($files);
309
    }
310
311
    /**
312
     * Registers more application config to load.
313
     */
314
    private function moreConfig()
315
    {
316
        $paths = $this->goals['config'];
317
        foreach ((array) $paths as $path) {
318
            if ($path) {
319
                $this->appFiles[] = $path;
320
            }
321
        }
322
    }
323
324
    public function setRootDir($value)
325
    {
326
        $this->_rootDir = $value;
327
    }
328
329
    public function getRootDir()
330
    {
331
        if ($this->_rootDir === null) {
332
            $this->_rootDir = $this->findRootDir();
333
        }
334
335
        return $this->_rootDir;
336
    }
337
338
    /**
339
     * Chdirs to project's root by looking for config file in the current directory and up.
340
     * @throws InvalidParamException when failed to find
341
     * @return string path to the root directory of hidev project
342
     */
343
    private function findRootDir()
344
    {
345
        $configFile = 'hidev.yml';
346
        for ($i = 0; $i < 9; ++$i) {
347
            if (file_exists($configFile)) {
348
                return getcwd();
349
            }
350
            chdir('..');
351
        }
352
        throw new InvalidParamException("Not a hidev project (or any of the parent directories).\nUse `hidev init` to initialize hidev project.");
353
    }
354
355
    public function buildRootPath($subpath)
356
    {
357
        return $this->getRootDir() . DIRECTORY_SEPARATOR . $subpath;
358
    }
359
}
360