├── 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 | [![Latest Stable Version](https://poser.pugx.org/yii2tech/install/v/stable.png)](https://packagist.org/packages/yii2tech/install) 15 | [![Total Downloads](https://poser.pugx.org/yii2tech/install/downloads.png)](https://packagist.org/packages/yii2tech/install) 16 | [![Build Status](https://travis-ci.org/yii2tech/install.svg?branch=master)](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 | } --------------------------------------------------------------------------------