├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
└── src
├── InitController.php
└── LocalFilePlaceholder.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii 2 Install extension Change Log
2 | ==================================
3 |
4 | 1.0.3, March 2, 2018
5 | --------------------
6 |
7 | - Bug #5: Fixed 'error' and 'warning' log level is not verbose at console output (klimov-paul)
8 | - Enh: `InitController` updated to use `yii\console\ExitCode` for exit code specification (klimov-paul)
9 |
10 |
11 | 1.0.2, January 12, 2017
12 | -----------------------
13 |
14 | - Bug #4: Fixed invalid project path in the confirm message at `InitController::actionAll()` (klimov-paul)
15 |
16 |
17 | 1.0.1, February 10, 2016
18 | ------------------------
19 |
20 | - Bug #2: `InitController::actionRequirements()` prevents installation on warning in case requirements checking is performed by output analyzes (klimov-paul)
21 | - Enh #3: `InitController::$commands` now allows usage of callable (klimov-paul)
22 |
23 |
24 | 1.0.0, December 29, 2015
25 | ------------------------
26 |
27 | - Initial release.
28 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The Yii framework is free software. It is released under the terms of
2 | the following BSD License.
3 |
4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech)
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions
9 | are met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in
15 | the documentation and/or other materials provided with the
16 | distribution.
17 | * Neither the name of Yii2tech nor the names of its
18 | contributors may be used to endorse or promote products derived
19 | from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 | POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Install Extension for Yii 2
6 |
7 |
8 |
9 | This extension provides ability for automated initialization of the project working copy, including local directories and
10 | files creation, running DB migrations and so on.
11 |
12 | For license information check the [LICENSE](LICENSE.md)-file.
13 |
14 | [](https://packagist.org/packages/yii2tech/install)
15 | [](https://packagist.org/packages/yii2tech/install)
16 | [](https://travis-ci.org/yii2tech/install)
17 |
18 |
19 | Requirements
20 | ------------
21 |
22 | This extension requires Linux OS.
23 |
24 |
25 | Installation
26 | ------------
27 |
28 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
29 |
30 | Either run
31 |
32 | ```
33 | php composer.phar require --prefer-dist yii2tech/install
34 | ```
35 |
36 | or add
37 |
38 | ```json
39 | "yii2tech/install": "*"
40 | ```
41 |
42 | to the require section of your composer.json.
43 |
44 | If you wish to setup crontab during project installation, you will also need to install [yii2tech/crontab](https://github.com/yii2tech/crontab),
45 | which is not required by default. In order to do so either run
46 |
47 | ```
48 | php composer.phar require --prefer-dist yii2tech/crontab
49 | ```
50 |
51 | or add
52 |
53 | ```json
54 | "yii2tech/crontab": "*"
55 | ```
56 |
57 | to the require section of your composer.json.
58 |
59 |
60 | Usage
61 | -----
62 |
63 | This extension provides special console controller [[yii2tech\install\InitController]], which allows initialization of the
64 | project working copy. Such initialization includes:
65 |
66 | - check if current environment matches project requirements.
67 | - create local directories (the ones, which may be not stored in version control system) and make them write-able.
68 | - create local files, such as configuration files, from templates.
69 | - run extra shell commands, like 'yii migrate' command.
70 | - setup cron jobs.
71 |
72 | In order to create an installer, you should create a separated console application entry script. This script should
73 | be absolutely stripped from the local configuration files, database and so on!
74 | See [examples/install.php](examples/install.php) for the example of such script.
75 |
76 | Once you have such script you can run installation process, using following command:
77 |
78 | ```
79 | php install.php init
80 | ```
81 |
82 |
83 | ## Working with local files
84 |
85 | The most interesting feature introduced by [[yii2tech\install\InitController]] is creating local project files, such as
86 | configuration files, from thier examples in interactive mode.
87 | For each file, which content may vary depending on actual project environment, you should create a template file named in
88 | format `{filename}.sample`. This file should be located under the same directory, where the actual local file should appear.
89 | Inside the template file you can use placeholders in format: `{{placeholderName}}`. For example:
90 |
91 | ```php
92 | defined('YII_DEBUG') or define('YII_DEBUG', {{yiiDebug}});
93 | defined('YII_ENV') or define('YII_ENV', '{{yiiEnv}}');
94 |
95 | return [
96 | 'components' => [
97 | 'db' => [
98 | 'dsn' => 'mysql:host={{dbHost}};dbname={{dbName}}',
99 | 'username' => '{{dbUser}}',
100 | 'password' => '{{dbPassword}}',
101 | ],
102 | ],
103 | ];
104 | ```
105 |
106 | While being processed, file templates are parsed, and for all found placeholders user will be asked to enter a value for them.
107 | You can make this process more user-friendly by setting [[yii2tech\install\InitController::localFilePlaceholders]], specifying
108 | hints, type and validation rules. See [[yii2tech\install\LocalFilePlaceholder]] for more details.
109 |
110 |
111 | ## Non interactive installation
112 |
113 | Asking user for particular placeholder value may be not efficient and sometimes not acceptable. You may need to run
114 | project intallation in fully automatic mode without user input, for example after updating source code from version
115 | control system inside automatic project update.
116 | In order to disable any user-interaction, you should use `interactive` option:
117 |
118 | ```
119 | php install.php init --interactive=0
120 | ```
121 |
122 | In this mode for all local file placeholders the default values will be taken, but only in case such values are explicitely
123 | defined via [[yii2tech\install\InitController::localFilePlaceholders]]. Because install entry script usually stored under
124 | version control system and local file placeholder values (as well as other installation parameters) may vary depending
125 | on particular environment, [[yii2tech\install\InitController]] instroduce 'config' option. Using this option you may
126 | specify extra configuration file, which should be merged with predefined parameters.
127 | In order to create such configuration file, you can use following:
128 |
129 | ```
130 | php install.php init/config @app/config/install.php
131 | ```
132 |
133 | Once you have adjusted created configuration file, you can run installation with it:
134 |
135 | ```
136 | php install.php init --config=@app/config/install.php --interactive=0
137 | ```
138 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yii2tech/install",
3 | "description": "Project installation support extension for the Yii2 framework",
4 | "keywords": ["yii2", "install", "init", "local file"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/yii2tech/install/issues",
9 | "forum": "http://www.yiiframework.com/forum/",
10 | "wiki": "https://github.com/yii2tech/install/wiki",
11 | "source": "https://github.com/yii2tech/install"
12 | },
13 | "authors": [
14 | {
15 | "name": "Paul Klimov",
16 | "email": "klimov.paul@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "yiisoft/yii2": "~2.0.13"
21 | },
22 | "suggest": {
23 | "yii2tech/crontab": "you need this package, if you wish to setup crontab during project installation"
24 | },
25 | "repositories": [
26 | {
27 | "type": "composer",
28 | "url": "https://asset-packagist.org"
29 | }
30 | ],
31 | "autoload": {
32 | "psr-4": {"yii2tech\\install\\": "src"}
33 | },
34 | "extra": {
35 | "branch-alias": {
36 | "dev-master": "1.0.x-dev"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/InitController.php:
--------------------------------------------------------------------------------
1 |
50 | * @since 1.0
51 | */
52 | class InitController extends Controller
53 | {
54 | /**
55 | * @var string the name of the default action.
56 | */
57 | public $defaultAction = 'all';
58 | /**
59 | * @var array list of local directories, which should be created and available to write by web server.
60 | * Path aliases can be used here. For example:
61 | *
62 | * ```php
63 | * [
64 | * '@app/web/assets',
65 | * '@runtime',
66 | * ]
67 | * ```
68 | */
69 | public $localDirectories = [];
70 | /**
71 | * @var array list of temporary directories, which should be cleared during application initialization/update.
72 | * Path aliases can be used here. For example:
73 | *
74 | * ```php
75 | * [
76 | * '@app/web/assets',
77 | * '@runtime',
78 | * ]
79 | * ```
80 | */
81 | public $tmpDirectories = [];
82 | /**
83 | * @var array list of local files, which should be created from the example files.
84 | * Path aliases can be used here. For example:
85 | *
86 | * ```php
87 | * [
88 | * '@app/web/.htaccess',
89 | * '@app/config/local.php',
90 | * ]
91 | * ```
92 | */
93 | public $localFiles = [];
94 | /**
95 | * @var string pattern, which is used to determine example file name for the local file.
96 | * This pattern should contain "{filename}" placeholder, which will be replaced by local file self name.
97 | */
98 | public $localFileExampleNamePattern = '{filename}.sample';
99 | /**
100 | * @var array list of local file placeholders in format: 'placeholderName' => [config].
101 | * Each placeholder value should be a valid configuration for [[LocalFilePlaceholder]].
102 | */
103 | public $localFilePlaceholders = [];
104 | /**
105 | * @var array list of files, which should be executable.
106 | * Path aliases can be used here. For example:
107 | *
108 | * ```php
109 | * [
110 | * '@app/yii',
111 | * '@app/install.php',
112 | * ]
113 | * ```
114 | */
115 | public $executeFiles = [];
116 | /**
117 | * @var string requirements list file name.
118 | * @see YiiRequirementsChecker
119 | */
120 | public $requirementsFileName = '@app/requirements.php';
121 | /**
122 | * @var array list of shell commands, which should be executed during project installation.
123 | * If command is a string it will be executed as shell command, otherwise as PHP callback.
124 | * For example:
125 | *
126 | * ```php
127 | * [
128 | * 'php /path/to/project/yii migrate/up --interactive=0'
129 | * ],
130 | * ```
131 | */
132 | public $commands = [];
133 | /**
134 | * @var bool whether to output log messages via "stdout". Defaults to true.
135 | * Set this to false to cease console output.
136 | */
137 | public $outputLog = true;
138 | /**
139 | * @var string configuration file name. Settings from this file will be merged with the default ones.
140 | * Such configuration file can be created, using action 'config'.
141 | * Path alias can be used here, for example: '@app/config/install.php'.
142 | */
143 | public $config = '';
144 | /**
145 | * @var string name of the file, which collect the process logs.
146 | */
147 | public $logFile = '';
148 | /**
149 | * @var string email address, which should receive the process error logs,
150 | * it can be comma-separated email addresses.
151 | * Inside the config file this parameter can be specified as array.
152 | */
153 | public $logEmail = '';
154 |
155 | /**
156 | * @var CronTab|array cron tab instance or its array configuration.
157 | * For example:
158 | *
159 | * ```php
160 | * [
161 | * 'jobs' => [
162 | * [
163 | * 'min' => '0',
164 | * 'hour' => '0',
165 | * 'command' => 'php /path/to/project/protected/yii some-cron',
166 | * ],
167 | * [
168 | * 'line' => '0 0 * * * php /path/to/project/protected/yii another-cron'
169 | * ]
170 | * ],
171 | * ];
172 | * ```
173 | *
174 | * Note: if you wish to use this option, make sure you have 'yii2tech/crontab' installed at your project.
175 | */
176 | private $_cronTab = [];
177 |
178 |
179 | /**
180 | * @param CronTab|array $cronTab cron tab instance or its array configuration.
181 | */
182 | public function setCronTab($cronTab)
183 | {
184 | $this->_cronTab = $cronTab;
185 | }
186 |
187 | /**
188 | * @throws InvalidConfigException on invalid configuration.
189 | * @return CronTab|null cron tab instance, or null if not set.
190 | */
191 | public function getCronTab()
192 | {
193 | if (empty($this->_cronTab)) {
194 | return null;
195 | }
196 |
197 | if ($this->_cronTab instanceof CronTab) {
198 | return $this->_cronTab;
199 | }
200 |
201 | if (!is_array($this->_cronTab)) {
202 | throw new InvalidConfigException('"' . get_class($this) . '::cronTab" should be instance of "' . CronTab::className() . '" or its array configuration.');
203 | }
204 |
205 | if (empty($this->_cronTab['class'])) {
206 | $this->_cronTab['class'] = CronTab::className();
207 | }
208 | $this->_cronTab = Yii::createObject($this->_cronTab);
209 |
210 | return $this->_cronTab;
211 | }
212 |
213 | /**
214 | * Initializes and adjusts the log process.
215 | */
216 | public function initLog()
217 | {
218 | $targets = [];
219 | if ($fileLogRoute = $this->createFileLogTarget()) {
220 | $targets['init-file'] = $fileLogRoute;
221 | }
222 | if ($emailLogRoute = $this->createEmailLogTarget()) {
223 | $targets['init-email'] = $emailLogRoute;
224 | }
225 |
226 | if (!empty($targets)) {
227 | Yii::getLogger()->flushInterval = 1;
228 | $log = Yii::$app->getLog();
229 | $log->targets = array_merge($log->targets, $targets);
230 | }
231 | }
232 |
233 | /**
234 | * Creates a file log target, if it is required.
235 | * @return FileTarget|null file log target or null, if it is not required.
236 | */
237 | protected function createFileLogTarget()
238 | {
239 | if (empty($this->logFile)) {
240 | return null;
241 | }
242 |
243 | return Yii::createObject([
244 | 'class' => FileTarget::className(),
245 | 'exportInterval' => 1,
246 | 'categories' => [get_class($this) . '*'],
247 | 'logFile' => $this->logFile,
248 | ]);
249 | }
250 |
251 | /**
252 | * Creates an email log target, if it is required.
253 | * @return EmailTarget|null email log target or null, if it is not required.
254 | */
255 | protected function createEmailLogTarget()
256 | {
257 | $logEmail = $this->logEmail;
258 | if (empty($logEmail)) {
259 | return null;
260 | }
261 |
262 | $userName = @exec('whoami');
263 | if (empty($userName)) {
264 | $userName = Inflector::slug(Yii::$app->name);
265 | }
266 | $hostName = @exec('hostname');
267 | if (empty($hostName)) {
268 | $hostName = Inflector::slug(Yii::$app->name) . '.com';
269 | }
270 | $sentFrom = $userName . '@' . $hostName;
271 |
272 | return Yii::createObject([
273 | 'class' => EmailTarget::className(),
274 | 'levels' => Logger::LEVEL_ERROR | Logger::LEVEL_WARNING,
275 | 'message' => [
276 | 'to' => $logEmail,
277 | 'subject' => 'Application "' . Yii::$app->name . '" initialization error at ' . $hostName,
278 | 'from' => $sentFrom,
279 | ],
280 | ]);
281 | }
282 |
283 | /**
284 | * {@inheritdoc}
285 | */
286 | public function beforeAction($action)
287 | {
288 | if (!empty($this->config)) {
289 | $this->populateFromConfigFile($this->config);
290 | }
291 | $this->initLog();
292 |
293 | return parent::beforeAction($action);
294 | }
295 |
296 | /**
297 | * {@inheritdoc}
298 | */
299 | public function options($actionID)
300 | {
301 | return array_merge(
302 | parent::options($actionID),
303 | [
304 | 'config',
305 | 'outputLog',
306 | 'logFile',
307 | 'logEmail',
308 | ]
309 | );
310 | }
311 |
312 | /**
313 | * Logs message.
314 | * @param string $message the text message
315 | * @param int $level log message level.
316 | * @return bool success.
317 | */
318 | protected function log($message, $level = null)
319 | {
320 | if ($level === null) {
321 | $level = Logger::LEVEL_INFO;
322 | }
323 | if ($this->outputLog) {
324 | if ($level != Logger::LEVEL_INFO) {
325 | $verboseLevel = $level;
326 | switch ($level) {
327 | case Logger::LEVEL_ERROR:
328 | $verboseLevel = 'error';
329 | break;
330 | case Logger::LEVEL_WARNING:
331 | $verboseLevel = 'warning';
332 | break;
333 | case Logger::LEVEL_TRACE:
334 | $verboseLevel = 'trace';
335 | break;
336 | }
337 | $this->stderr("\n[{$verboseLevel}] {$message}\n");
338 | } else {
339 | $this->stdout($message);
340 | }
341 | }
342 | $message = trim($message, "\n");
343 | if (!empty($message)) {
344 | Yii::getLogger()->log($message, $level, get_class($this));
345 | }
346 | return true;
347 | }
348 |
349 | /**
350 | * Returns configuration for the given local file placeholder name.
351 | * If placeholder has configured empty configuration will be returned.
352 | * @param string $placeholderName placeholder name
353 | * @return array placeholder configuration.
354 | */
355 | protected function getLocalFilePlaceholderConfig($placeholderName)
356 | {
357 | if (array_key_exists($placeholderName, $this->localFilePlaceholders)) {
358 | return $this->localFilePlaceholders[$placeholderName];
359 | } else {
360 | return [];
361 | }
362 | }
363 |
364 | /**
365 | * Performs all application initialize actions.
366 | * @param bool $overwrite indicates, if existing local file should be overwritten in the process.
367 | * @return int CLI exit code
368 | */
369 | public function actionAll($overwrite = false)
370 | {
371 | if ($this->confirm("Initialize project under '" . Yii::$app->basePath . "'?")) {
372 | $this->log("Project initialization in progress...\n");
373 | if ($this->actionRequirements(false) !== ExitCode::OK) {
374 | $this->log("Project initialization failed.", Logger::LEVEL_ERROR);
375 | return ExitCode::UNSPECIFIED_ERROR;
376 | }
377 | $this->actionLocalDir();
378 | $this->actionClearTmpDir();
379 | $this->actionExecuteFile();
380 | $this->actionLocalFile(null, $overwrite);
381 | $this->actionCommands();
382 | $this->actionCrontab();
383 | $this->log("\nProject initialization is complete.\n");
384 | }
385 |
386 | return ExitCode::OK;
387 | }
388 |
389 | /**
390 | * Check if current system matches application requirements.
391 | * @param bool $forceShowResult indicates if verbose check result should be displayed even,
392 | * if there is no errors or warnings.
393 | * @return int CLI exit code
394 | */
395 | public function actionRequirements($forceShowResult = true)
396 | {
397 | $this->log("Checking requirements...\n");
398 |
399 | $requirements = [];
400 |
401 | $requirementsFileName = Yii::getAlias($this->requirementsFileName);
402 | if (file_exists($requirementsFileName)) {
403 | ob_start();
404 | ob_implicit_flush(false);
405 | $requirements = require $requirementsFileName;
406 | $output = ob_get_clean();
407 |
408 | if (is_int($requirements) && !empty($output)) {
409 | if (preg_match('/^Errors: (?P[0-9]+)[ ]+Warnings: (?P[0-9]+)[ ]+Total checks: (?P[0-9]+)$/im', $output, $matches)) {
410 | $errors = (int)$matches['errors'];
411 | $warnings = (int)$matches['warnings'];
412 | if ($errors > 0) {
413 | $this->log("Requirements check fails with errors.", Logger::LEVEL_ERROR);
414 | $this->stdout($output);
415 | return ExitCode::UNSPECIFIED_ERROR;
416 | }
417 |
418 | if ($warnings > 0) {
419 | $this->log("Requirements check passed with warnings.", Logger::LEVEL_WARNING);
420 | $this->stdout($output);
421 | return ExitCode::OK;
422 | }
423 |
424 | $this->log("Requirements check successful.\n");
425 | if ($forceShowResult) {
426 | $this->stdout($output);
427 | }
428 |
429 | return ExitCode::OK;
430 | }
431 | }
432 | } else {
433 | $this->log("Requirements list file '{$requirementsFileName}' does not exist, only default requirements checking is available.", Logger::LEVEL_WARNING);
434 | }
435 |
436 | $requirementsChecker = $this->createRequirementsChecker();
437 | $requirementsChecker->checkYii()->check($requirements);
438 |
439 | $requirementsCheckResult = $requirementsChecker->getResult();
440 |
441 | if ($requirementsCheckResult['summary']['errors'] > 0) {
442 | $this->log("Requirements check fails with errors.", Logger::LEVEL_ERROR);
443 | $requirementsChecker->render();
444 | return ExitCode::UNSPECIFIED_ERROR;
445 | }
446 |
447 | if ($requirementsCheckResult['summary']['warnings'] > 0) {
448 | $this->log("Requirements check passed with warnings.", Logger::LEVEL_WARNING);
449 | $requirementsChecker->render();
450 | return ExitCode::OK;
451 | }
452 |
453 | $this->log("Requirements check successful.\n");
454 | if ($forceShowResult) {
455 | $requirementsChecker->render();
456 | }
457 |
458 | return ExitCode::OK;
459 | }
460 |
461 | /**
462 | * Creates all local directories and makes sure they are writeable for the web server.
463 | * @return int CLI exit code
464 | */
465 | public function actionLocalDir()
466 | {
467 | $this->log("\nEnsuring local directories:\n");
468 | $filePermissions = 0777;
469 | foreach ($this->localDirectories as $directory) {
470 | $directoryPath = Yii::getAlias($directory);
471 | if (!file_exists($directoryPath)) {
472 | $this->log("\nCreating directory '{$directoryPath}'...");
473 | if (FileHelper::createDirectory($directoryPath, $filePermissions)) {
474 | $this->log("complete.\n");
475 | } else {
476 | $this->log("Unable to create directory '{$directoryPath}'!", Logger::LEVEL_ERROR);
477 | }
478 | }
479 | $this->log("Setting permissions '" . decoct($filePermissions) . "' for '{$directoryPath}'...");
480 | if (chmod($directoryPath, $filePermissions)) {
481 | $this->log("complete.\n");
482 | } else {
483 | $this->log("Unable to set permissions '" . decoct($filePermissions) . "' for '{$directoryPath}'!", Logger::LEVEL_ERROR);
484 | }
485 | }
486 |
487 | return ExitCode::OK;
488 | }
489 |
490 | /**
491 | * Clears temporary directories, avoiding special files such as ".htaccess" and VCS files.
492 | * @param string $dir directory name.
493 | * @return int CLI exit code
494 | */
495 | public function actionClearTmpDir($dir = null)
496 | {
497 | if (!empty($dir) || $this->confirm('Clear all temporary directories?')) {
498 | $this->log("\nClearing temporary directories:\n");
499 | $temporaryDirectories = $this->tmpDirectories;
500 | $excludeNames = [
501 | '.htaccess',
502 | '.svn',
503 | '.gitignore',
504 | '.gitkeep',
505 | '.hgignore',
506 | '.hgkeep',
507 | ];
508 | foreach ($temporaryDirectories as $temporaryDirectory) {
509 | $tmpDirFullName = Yii::getAlias($temporaryDirectory);
510 | if ($dir !== null && (strpos($tmpDirFullName, $dir) === false)) {
511 | continue;
512 | }
513 | if (!is_dir($tmpDirFullName)) {
514 | $this->log("Directory '{$tmpDirFullName}' does not exists!", Logger::LEVEL_WARNING);
515 | continue;
516 | }
517 | $this->log("\nClearing directory '{$tmpDirFullName}'...");
518 | $tmpDirHandle = opendir($tmpDirFullName);
519 | while (($fileSystemObjectName = readdir($tmpDirHandle)) !== false) {
520 | if ($fileSystemObjectName === '.' || $fileSystemObjectName === '..') {
521 | continue;
522 | }
523 | if (in_array($fileSystemObjectName, $excludeNames)) {
524 | continue;
525 | }
526 | $fullName = $tmpDirFullName . DIRECTORY_SEPARATOR . $fileSystemObjectName;
527 | if (is_dir($fullName)) {
528 | FileHelper::removeDirectory($fullName);
529 | } else {
530 | unlink($fullName);
531 | }
532 | }
533 | closedir($tmpDirHandle);
534 | $this->log("complete.\n");
535 | }
536 | }
537 |
538 | return ExitCode::OK;
539 | }
540 |
541 | /**
542 | * Change permissions for the specific files, making them executable.
543 | * @return int CLI exit code
544 | */
545 | public function actionExecuteFile()
546 | {
547 | $this->log("\nEnsuring execute able files:\n");
548 | $filePermissions = 0755;
549 | foreach ($this->executeFiles as $fileName) {
550 | $this->log("Setting permissions '" . decoct($filePermissions) . "' for '{$fileName}'...");
551 | $fileRealName = Yii::getAlias($fileName);
552 | if (chmod($fileRealName, $filePermissions)) {
553 | $this->log("complete.\n");
554 | } else {
555 | $this->log("Unable to set permissions '" . decoct($filePermissions) . "' for '{$fileRealName}'!", Logger::LEVEL_ERROR);
556 | }
557 | }
558 |
559 | return ExitCode::OK;
560 | }
561 |
562 | /**
563 | * Runs the shell commands defined by [[commands]].
564 | * @return int CLI exit code
565 | */
566 | public function actionCommands()
567 | {
568 | if (empty($this->commands)) {
569 | $this->log("No extra shell commands are defined.\n");
570 | return ExitCode::OK;
571 | }
572 |
573 | $commandTitles = [];
574 | foreach ($this->commands as $key => $command) {
575 | if (is_string($command)) {
576 | $commandTitles[$key] = $command;
577 | } else {
578 | $commandTitles[$key] = 'Unknown (Closure)';
579 | }
580 | }
581 |
582 | if ($this->confirm("Following commands will be executed:\n" . implode("\n", $commandTitles) . "\nDo you wish to proceed?")) {
583 | foreach ($this->commands as $key => $command) {
584 | $this->log($commandTitles[$key] . "\n");
585 | if (is_string($command)) {
586 | exec($command, $outputRows);
587 | $this->log(implode("\n", $outputRows));
588 | } else {
589 | $this->log(call_user_func($command));
590 | }
591 | $this->log("\n");
592 | }
593 | }
594 |
595 | return ExitCode::OK;
596 | }
597 |
598 | /**
599 | * Sets up the project cron jobs.
600 | * @return int CLI exit code
601 | */
602 | public function actionCrontab()
603 | {
604 | $cronTab = $this->getCronTab();
605 | if (!is_object($cronTab)) {
606 | $this->log("There are no cron tab to setup.\n");
607 | return ExitCode::OK;
608 | }
609 |
610 | $cronJobs = $cronTab->getJobs();
611 | if (empty($cronJobs)) {
612 | $this->log("There are no cron jobs to setup.\n");
613 | } else {
614 | $userName = @exec('whoami');
615 | if (empty($userName)) {
616 | $userName = 'unknown';
617 | }
618 | if ($this->confirm("Setup the cron jobs for the user '{$userName}'?")) {
619 | $this->log("Setting up cron jobs:\n");
620 | $cronTab->apply();
621 | $this->log("crontab is set for the user '{$userName}'\n");
622 | }
623 | }
624 |
625 | return ExitCode::OK;
626 | }
627 |
628 | /**
629 | * Creates new local files from example files.
630 | * @param string $file name of the particular local file, if empty all local files will be processed.
631 | * @param bool $overwrite indicates, if existing local file should be overwritten in the process.
632 | * @return int CLI exit code
633 | */
634 | public function actionLocalFile($file = null, $overwrite = false)
635 | {
636 | $this->log("\nCreating local files:\n");
637 | foreach ($this->localFiles as $localFileRawName) {
638 | $localFileRealName = Yii::getAlias($localFileRawName);
639 | if ($file !== null && (strpos($localFileRealName, $file) === false)) {
640 | continue;
641 | }
642 | $this->log("\nProcessing local file '{$localFileRealName}':\n");
643 |
644 | $exampleFileName = $this->getExampleFileName($localFileRealName);
645 | if (!file_exists($exampleFileName)) {
646 | $this->log("Unable to find example for the local file '{$localFileRealName}': file '{$exampleFileName}' does not exists!", Logger::LEVEL_ERROR);
647 | }
648 | if (file_exists($localFileRealName)) {
649 | if (filemtime($exampleFileName) > filemtime($localFileRealName)) {
650 | $this->log("Local file '{$localFileRealName}' is out of date and should be regenerated.", Logger::LEVEL_WARNING);
651 | } else {
652 | if (!$overwrite) {
653 | $this->log("Local file '{$localFileRealName}' already exists. Use 'overwrite' option, if you wish to regenerate it.\n");
654 | continue;
655 | }
656 | }
657 | }
658 | $this->createLocalFileByExample($localFileRealName, $exampleFileName);
659 | }
660 |
661 | return ExitCode::OK;
662 | }
663 |
664 | /**
665 | * Generates new configuration file, which can be used to run
666 | * application initialization.
667 | * @param string $file output config file name.
668 | * @param bool $overwrite indicates, if existing configuration file should be overwritten in the process.
669 | * @return int CLI exit code
670 | */
671 | public function actionConfig($file = null, $overwrite = false)
672 | {
673 | if (empty($file)) {
674 | if (empty($this->config)) {
675 | $this->log('Either "config" or "file" option should be provided.');
676 | return ExitCode::UNSPECIFIED_ERROR;
677 | }
678 | $fileName = Yii::getAlias($this->config);
679 | } else {
680 | $fileName = Yii::getAlias($file);
681 | }
682 | if (file_exists($fileName)) {
683 | if (!$overwrite) {
684 | if (!$this->confirm("Configuration file '{$file}' already exists, do you wish to overwrite it?")) {
685 | return ExitCode::OK;
686 | }
687 | }
688 | }
689 |
690 | $configPropertyNames = [
691 | 'interactive',
692 | 'logFile',
693 | 'logEmail',
694 | 'tmpDirectories',
695 | 'localDirectories',
696 | 'executeFiles',
697 | 'localFileExampleNamePattern',
698 | 'localFiles',
699 | 'localFilePlaceholders',
700 | ];
701 | $config = [];
702 | foreach ($configPropertyNames as $configPropertyName) {
703 | $config[$configPropertyName] = $this->$configPropertyName;
704 | }
705 |
706 | $fileContent = "log("Old version of the configuration file '{$file}' has been removed.\n");
710 | } else {
711 | $this->log("Unable to remove old version of the configuration file '{$file}'!", Logger::LEVEL_ERROR);
712 | }
713 | }
714 | file_put_contents($fileName, $fileContent);
715 | if (file_exists($fileName)) {
716 | $this->log("Configuration file '{$file}' has been created.\n");
717 | return ExitCode::OK;
718 | }
719 |
720 | $this->log("Unable to create configuration file '{$file}'!", Logger::LEVEL_ERROR);
721 | return ExitCode::UNSPECIFIED_ERROR;
722 | }
723 |
724 | /**
725 | * Creates new local file from example file.
726 | * @param string $localFileName local file full name.
727 | * @param string $exampleFileName example file full name.
728 | * @return int CLI exit code
729 | */
730 | protected function createLocalFileByExample($localFileName, $exampleFileName)
731 | {
732 | $this->log("Creating local file '{$localFileName}':\n");
733 |
734 | $placeholderNames = $this->parseExampleFile($exampleFileName);
735 | if (!empty($placeholderNames) && $this->interactive) {
736 | $this->log("Specify local file placeholder values. Enter empty string to apply default value. Enter whitespace to specify empty value.\n");
737 | }
738 |
739 | $placeholders = [];
740 | foreach ($placeholderNames as $placeholderName) {
741 | $placeholderConfig = $this->getLocalFilePlaceholderConfig($placeholderName);
742 | $model = new LocalFilePlaceholder($placeholderName, $placeholderConfig);
743 | if ($this->interactive) {
744 | $placeholderLabel = $model->composeLabel();
745 | $isValid = false;
746 | while (!$isValid) {
747 | $model->value = $this->prompt("Enter {$placeholderLabel}:");
748 | if ($model->validate()) {
749 | $isValid = true;
750 | } else {
751 | $this->stdout("Error: invalid value entered:\n");
752 | $this->stdout($model->getErrorSummary() . "\n");
753 | }
754 | }
755 | }
756 | try {
757 | $placeholderActualValue = $model->getActualValue();
758 | $placeholders[$placeholderName] = $placeholderActualValue;
759 | } catch (\Exception $exception) {
760 | $this->log($exception->getMessage(), Logger::LEVEL_ERROR);
761 | }
762 | }
763 |
764 | $localFileContent = $this->composeLocalFileContent($exampleFileName, $placeholders);
765 | if (file_exists($localFileName)) {
766 | $this->log("Removing old version of file '{$localFileName}'...");
767 | if (!unlink($localFileName)) {
768 | $this->log("Unable to remove old version of file '{$localFileName}'!", Logger::LEVEL_ERROR);
769 | return ExitCode::UNSPECIFIED_ERROR;
770 | }
771 | $this->log("complete.\n");
772 | }
773 |
774 | file_put_contents($localFileName, $localFileContent);
775 | if (file_exists($localFileName)) {
776 | $this->log("Local file '{$localFileName}' has been created.\n");
777 | return ExitCode::OK;
778 | }
779 |
780 | $this->log("Unable to create local file '{$localFileName}'!", Logger::LEVEL_ERROR);
781 | return ExitCode::UNSPECIFIED_ERROR;
782 | }
783 |
784 | /**
785 | * Determines the full name of the example file for the given local file.
786 | * @param string $localFileName local file full name.
787 | * @return string example file full name.
788 | */
789 | protected function getExampleFileName($localFileName)
790 | {
791 | $localFileDir = dirname($localFileName);
792 | $localFileSelfName = basename($localFileName);
793 | $localFileExampleSelfName = str_replace('{filename}', $localFileSelfName, $this->localFileExampleNamePattern);
794 | return $localFileDir . DIRECTORY_SEPARATOR . $localFileExampleSelfName;
795 | }
796 |
797 | /**
798 | * Finds the placeholders in the example file.
799 | * @param string $exampleFileName example file name.
800 | * @return array placeholders list.
801 | */
802 | protected function parseExampleFile($exampleFileName)
803 | {
804 | $exampleFileContent = file_get_contents($exampleFileName);
805 | if (preg_match_all('/{{(\w+)}}/is', $exampleFileContent, $matches)) {
806 | $placeholders = array_unique($matches[1]);
807 | } else {
808 | $placeholders = [];
809 | }
810 | return $placeholders;
811 | }
812 |
813 | /**
814 | * Composes local file content from example file content, using given placeholders.
815 | * @param string $exampleFileName example file full name.
816 | * @param array $placeholders set of placeholders.
817 | * @return string local file content.
818 | */
819 | protected function composeLocalFileContent($exampleFileName, array $placeholders)
820 | {
821 | $exampleFileContent = file_get_contents($exampleFileName);
822 | $replacePairs = [];
823 | foreach ($placeholders as $name => $value) {
824 | $replacePairs['{{' . $name . '}}'] = $value;
825 | }
826 | return strtr($exampleFileContent, $replacePairs);
827 | }
828 |
829 | /**
830 | * Populates console command instance from configuration file.
831 | * @param string $configFileName configuration file name.
832 | * @return bool success.
833 | * @throws InvalidParamException on wrong configuration file.
834 | */
835 | public function populateFromConfigFile($configFileName)
836 | {
837 | $configFileName = realpath(Yii::getAlias($configFileName));
838 | if (!file_exists($configFileName)) {
839 | throw new InvalidParamException("Unable to read configuration file '{$configFileName}': file does not exist!");
840 | }
841 |
842 | $configFileExtension = pathinfo($configFileName, PATHINFO_EXTENSION);
843 | switch ($configFileExtension) {
844 | case 'php': {
845 | $configData = $this->extractConfigFromFilePhp($configFileName);
846 | break;
847 | }
848 | default: {
849 | throw new InvalidParamException("Configuration file has unknown type: '{$configFileExtension}'!");
850 | }
851 | }
852 |
853 | if (!is_array($configData)) {
854 | throw new InvalidParamException("Unable to read configuration from file '{$configFileName}': wrong file format!");
855 | }
856 | foreach ($configData as $name => $value) {
857 | $originValue = $this->$name;
858 | if (is_array($originValue) && is_array($value)) {
859 | $value = array_merge($originValue, $value);
860 | }
861 | $this->$name = $value;
862 | }
863 | return true;
864 | }
865 |
866 | /**
867 | * Extracts configuration array from PHP file.
868 | * @param string $configFileName configuration file name.
869 | * @return mixed configuration data.
870 | */
871 | protected function extractConfigFromFilePhp($configFileName)
872 | {
873 | $configData = require($configFileName);
874 | return $configData;
875 | }
876 |
877 | /**
878 | * Creates requirements checker instance.
879 | * @return YiiRequirementChecker requirements checker instance.
880 | */
881 | protected function createRequirementsChecker()
882 | {
883 | if (!class_exists('YiiRequirementChecker', false)) {
884 | require Yii::getAlias('@vendor/yiisoft/yii2/requirements/YiiRequirementChecker.php');
885 | }
886 | return new YiiRequirementChecker();
887 | }
888 | }
889 |
--------------------------------------------------------------------------------
/src/LocalFilePlaceholder.php:
--------------------------------------------------------------------------------
1 |
23 | * @since 1.0
24 | */
25 | class LocalFilePlaceholder extends Model
26 | {
27 | /**
28 | * @var string placeholder name.
29 | */
30 | public $name = 'value';
31 | /**
32 | * @var mixed placeholder value.
33 | */
34 | public $value;
35 | /**
36 | * @var mixed placeholder default value.
37 | */
38 | public $default;
39 | /**
40 | * @var string brief placeholder description.
41 | */
42 | public $hint = '';
43 | /**
44 | * @var string placeholder type.
45 | */
46 | public $type = 'string';
47 |
48 | /**
49 | * @var array validation rules.
50 | * Unlike the configuration for the common model, each rule should not contain attribute name
51 | * as it already determined as [[value]].
52 | */
53 | private $_rules = [];
54 |
55 |
56 | /**
57 | * Constructor
58 | * @param string $name placeholder name.
59 | * @param array $config placeholder configuration.
60 | */
61 | public function __construct($name, array $config = [])
62 | {
63 | $config['name'] = $name;
64 | parent::__construct($config);
65 | }
66 |
67 | /**
68 | * @param array $rules validation rules.
69 | */
70 | public function setRules(array $rules)
71 | {
72 | $this->_rules = $rules;
73 | }
74 |
75 | /**
76 | * @return array validation rules.
77 | */
78 | public function getRules()
79 | {
80 | return $this->_rules;
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | public function attributes()
87 | {
88 | return ['value'];
89 | }
90 |
91 | /**
92 | * {@inheritdoc}
93 | */
94 | public function attributeLabels()
95 | {
96 | return [
97 | 'value' => $this->name
98 | ];
99 | }
100 |
101 | /**
102 | * {@inheritdoc}
103 | */
104 | public function attributeHints()
105 | {
106 | return [
107 | 'value' => $this->hint
108 | ];
109 | }
110 |
111 | /**
112 | * {@inheritdoc}
113 | */
114 | public function createValidators()
115 | {
116 | $validators = parent::createValidators();
117 |
118 | $rules = $this->getRules();
119 | if ($this->default === null) {
120 | array_unshift($rules, ['required']);
121 | }
122 |
123 | foreach ($rules as $rule) {
124 | if ($rule instanceof Validator) {
125 | $validators->append($rule);
126 | } elseif (is_array($rule) && isset($rule[0])) { // attributes, validator type
127 | $validator = Validator::createValidator($rule[0], $this, ['value'], array_slice($rule, 1));
128 | $validators->append($validator);
129 | } else {
130 | throw new InvalidConfigException('Invalid validation rule: a rule must specify validator type.');
131 | }
132 | }
133 |
134 | return $validators;
135 | }
136 |
137 | /**
138 | * {@inheritdoc}
139 | */
140 | public function beforeValidate()
141 | {
142 | $value = $this->value;
143 | if ($value === null || $value === false || $value === '') {
144 | if ($this->default !== null) {
145 | $this->value = $this->default;
146 | }
147 | }
148 | return parent::beforeValidate();
149 | }
150 |
151 | /**
152 | * Returns verbose label for placeholder.
153 | * @return string placeholder verbose label.
154 | */
155 | public function composeLabel()
156 | {
157 | $labelContent = "'{$this->name}'";
158 | if (!empty($this->hint)) {
159 | $labelContent .= ' (' . $this->hint . ')';
160 | }
161 | if ($this->default !== null) {
162 | $labelContent .= ' [default: ' . $this->default . ']';
163 | }
164 |
165 | return $labelContent;
166 | }
167 |
168 | /**
169 | * Returns actual placeholder value according to placeholder type.
170 | * @throws \yii\base\InvalidCallException on invalid type.
171 | * @throws \yii\base\Exception on failure.
172 | * @return float|int|string actual value.
173 | */
174 | public function getActualValue()
175 | {
176 | $rawValue = $this->value;
177 | if ($rawValue === null || $rawValue === false || $rawValue === '') {
178 | if ($this->default === null) {
179 | throw new Exception("Unable to determine default value for the placeholder '{$this->name}'!");
180 | }
181 | $rawValue = $this->default;
182 | }
183 |
184 | switch ($this->type) {
185 | case 'bool':
186 | case 'boolean':
187 | if (strcasecmp($rawValue, 'true') === 0) {
188 | $rawValue = true;
189 | } elseif (strcasecmp($rawValue, 'false') === 0) {
190 | $rawValue = false;
191 | } else {
192 | $rawValue = (bool)$rawValue;
193 | }
194 | return $rawValue ? 'true' : 'false';
195 | case 'string':
196 | return $rawValue;
197 | case 'int':
198 | case 'integer':
199 | return (int)$rawValue;
200 | case 'decimal':
201 | case 'double':
202 | case 'float':
203 | return (float)$rawValue;
204 | default:
205 | throw new InvalidCallException("Unknown type '{$this->type}' for placeholder '{$this->name}'!");
206 | }
207 | }
208 |
209 | /**
210 | * Composes errors single string summary.
211 | * @param string $delimiter errors delimiter.
212 | * @return string error summary
213 | */
214 | public function getErrorSummary($delimiter = "\n")
215 | {
216 | $errorSummaryLines = [];
217 | foreach ($this->getErrors() as $attributeErrors) {
218 | $errorSummaryLines = array_merge($errorSummaryLines, $attributeErrors);
219 | }
220 | return implode($delimiter, $errorSummaryLines);
221 | }
222 | }
--------------------------------------------------------------------------------