Completed
Push — master ( f6cc4f...20183c )
by Andrii
03:46
created

src/base/Starter.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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;
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
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...
56
        $route = reset($request->resolve());
0 ignored issues
show
$request->resolve() cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
57
        $id = reset(explode('/', $route, 2));
0 ignored issues
show
explode('/', $route, 2) cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
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'])) {
95
                unset($config['components'][$id]);
96
                $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...
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'])) {
108
                    $def['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 = new Dotenv('.');
153
            $dotenv->load();
154
        }
155
    }
156
157
    private function loadGoals()
158
    {
159
        $this->includeGoals('hidev.yml');
160
        if (file_exists('hidev-local.yml')) {
161
            $this->includeGoals('hidev-local.yml');
162
        }
163
    }
164
165
    private function includeGoals($paths)
166
    {
167
        foreach ((array) $paths as $path) {
168
            $this->goals = ArrayHelper::merge(
169
                $this->goals,
170
                $this->readYaml($path)
171
            );
172
        }
173
    }
174
175
    private function readYaml($path)
176
    {
177
        return Yaml::parse(FileHelper::read($path));
178
    }
179
180
    /**
181
     * Adds aliases:
182
     * - @root alias to current project root dir
183
     * - @hidev own alias
184
     * - current package namespace for it could be used from hidev
185
     * - aliases listed in config.
186
     */
187
    private function addAliases()
188
    {
189
        Yii::setAlias('@root', $this->getRootDir());
190
        Yii::setAlias('@hidev', dirname(__DIR__));
191
192
        $package = $this->goals['package'];
193
        $alias  = isset($package['namespace']) ? strtr($package['namespace'], '\\', '/') : '';
194
        if ($alias && !Yii::getAlias('@' . $alias, false)) {
195
            $srcdir = Yii::getAlias('@root/' . ($package['src'] ?: 'src'));
196
            Yii::setAlias($alias, $srcdir);
0 ignored issues
show
It seems like $srcdir defined by \Yii::getAlias('@root/' ...ckage['src'] ?: 'src')) on line 195 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...
197
        }
198
        $aliases = $this->goals['aliases'];
199
        if (!empty($aliases) && is_array($aliases)) {
200
            foreach ($aliases as $alias => $path) {
201
                if (!$this->hasAlias($alias)) {
202
                    Yii::setAlias($alias, $path);
203
                }
204
            }
205
        }
206
    }
207
208
    private function hasAlias($alias, $exact = true)
0 ignored issues
show
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...
209
    {
210
        $pos = strpos($alias, '/');
211
212
        return $pos === false ? isset(Yii::$aliases[$alias]) : isset(Yii::$aliases[substr($alias, 0, $pos)][$alias]);
213
    }
214
215
    /**
216
     * - install configured plugins and register their app config
217
     * - install project dependencies and register
218
     * - register application config files.
219
     */
220
    private function requireAll()
221
    {
222
        $vendors = [];
223
        $plugins = $this->goals['plugins'];
224
        if ($plugins) {
225
            $file = File::create('.hidev/composer.json');
226
            $data = ArrayHelper::merge($file->load(), ['require' => $plugins]);
227
            if ($file->save($data) || !is_dir('.hidev/vendor')) {
228
                $this->updateDotHidev();
229
            }
230
            $vendors[] = $this->buildRootPath('.hidev/vendor');
231
        }
232
        if ($this->needsComposerInstall()) {
233
            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...
234
                throw new InvalidParamException('Failed initialize project with composer install');
235
            }
236
        }
237
        $vendors[] = $this->buildRootPath('vendor');
238
239
        foreach ($vendors as $vendor) {
240
            foreach (['common', 'console', 'hidev'] as $name) {
241
                $path = ConfigPlugin::path($name, $vendor);
242
                if (file_exists($path)) {
243
                    $this->appFiles[] = $path;
244
                }
245
            }
246
        }
247
    }
248
249
    /**
250
     * Update action.
251
     * @return int exit code
252
     */
253
    public function updateDotHidev()
254
    {
255
        if (file_exists('.hidev/composer.json')) {
256
            return $this->passthru('composer', ['update', '-d', '.hidev', '--prefer-source', '--ansi']);
257
        }
258
    }
259
260
    /**
261
     * Passthru command.
262
     * @param string $command
263
     * @param array $args
264
     * @return int exit code
265
     */
266
    private function passthru($command, $args)
267
    {
268
        $binary = new BinaryPhp([
269
            'name' => $command,
270
        ]);
271
272
        return $binary->passthru($args);
273
    }
274
275
    private function needsComposerInstall()
276
    {
277
        if (file_exists('vendor')) {
278
            return false;
279
        }
280
        if (!file_exists('composer.json')) {
281
            return false;
282
        }
283
284
        return true;
285
286
        /*
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...
287
            $data = File::create('composer.json')->load();
288
            foreach (['require', 'require-dev'] as $key) {
289
                if (isset($data[$key])) {
290
                    foreach ($data[$key] as $package => $version) {
291
                        list(, $name) = explode('/', $package);
292
                        if (strncmp($name, 'hidev-', 6) === 0) {
293
                            return true;
294
                        }
295
                    }
296
                }
297
            }
298
299
            return false;
300
        */
301
    }
302
303
    /**
304
     * Include all configured includes.
305
     */
306
    private function includeAll()
307
    {
308
        $config = $this->readConfig();
309
        $files = array_merge(
310
            (array) $this->goals['include'],
311
            (array) $config['components']['include']
312
        );
313
        $this->includeGoals($files);
314
    }
315
316
    /**
317
     * Registers more application config to load.
318
     */
319
    private function moreConfig()
320
    {
321
        $paths = $this->goals['config'];
322
        foreach ((array) $paths as $path) {
323
            if ($path) {
324
                $this->appFiles[] = $path;
325
            }
326
        }
327
    }
328
329
    public function setRootDir($value)
330
    {
331
        $this->_rootDir = $value;
332
    }
333
334
    public function getRootDir()
335
    {
336
        if ($this->_rootDir === null) {
337
            $this->_rootDir = $this->findRootDir();
338
        }
339
340
        return $this->_rootDir;
341
    }
342
343
    /**
344
     * Chdirs to project's root by looking for config file in the current directory and up.
345
     * @throws InvalidParamException when failed to find
346
     * @return string path to the root directory of hidev project
347
     */
348
    private function findRootDir()
349
    {
350
        $configFile = 'hidev.yml';
351
        for ($i = 0; $i < 9; ++$i) {
352
            if (file_exists($configFile)) {
353
                return getcwd();
354
            }
355
            chdir('..');
356
        }
357
        throw new InvalidParamException("Not a hidev project (or any of the parent directories).\nUse `hidev init` to initialize hidev project.");
358
    }
359
360
    public function buildRootPath($subpath)
361
    {
362
        return $this->getRootDir() . DIRECTORY_SEPARATOR . $subpath;
363
    }
364
}
365