forrest79 / deploy-php
PHP项目的简单资源构建和部署应用程序助手。
Requires
- php: ^8.0
- nette/utils: ^3.0 | ^4.0
- phpseclib/phpseclib: ^3.0
Requires (Dev)
- forrest79/phpcs: ^1.5
- forrest79/phpcs-ignores: ^0.5
- phpstan/phpstan: ^1.10
- phpstan/phpstan-strict-rules: ^1.5
This package is auto-updated.
Last update: 2024-09-08 21:22:55 UTC
README
PHP项目的简单资源构建和应用程序部署助手。
要求
Forrest79/DeployPhp 需要 PHP 8.0 或更高版本。
安装
推荐通过 Composer 安装 Forrest79/DeployPhp
composer require --dev forrest79/deploy-php
文档
资源
这是一个简单的资源构建器。目前,它支持复制文件、编译和压缩 less 文件、sass 文件和 JavaScript(简单的压缩器 UglifyJS 或复杂的 rollup.js + 推荐的 Babel)文件,并在调试环境中生成映射文件。
编译和压缩需要安装 node.js
以及已安装的 npm
包 less
、node-sass
、uglify-js
或 rollup
(《babel》)。在 Debian 或 Ubuntu 上,你可以这样做(-g
选项将在系统中全局安装包,而不是在你的仓库中)
curl -sL https://deb.nodesource.com/setup_15.x | sudo -E bash - sudo apt-get install -y nodejs # LESS compiler npm install less #sudo npm install -g less # SASS compiler npm install node-sass #sudo npm install -g node-sass # UglifyJS compiler npm install uglify-js #sudo npm install -g uglify-js # Babel and Rollup (prefer not to install this globally) npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser @rollup/plugin-babel @babel/core @babel/preset-env @babel/plugin-transform-runtime core-js
使用方法简单。以下示例展示了它是如何与 Nette 框架 一起工作的。只需创建新的 Forrest79\DeployPhp\Assets
类实例,并将临时目录、资源源目录和配置数组传递给构造函数。`key` 是要处理的目录(对于 DeployPhp\Assets::COPY
)或目标文件(对于 DeployPhp\Assets::UGLIFYJS
、DeployPhp\Assets::ROLLUP
或 DeployPhp\Assets::LESS
)或目录(对于 DeployPhp\Assets::SASS
)的源数据,而 `value` 可以是简单的 DeployPhp\Assets::COPY
,表示将此文件/目录从源复制到目标或另一个 `array`,包含项
- 必需的
type
- 值为DeployPhp\Assets::COPY
以复制文件/目录或DeployPhp\Assets::LESS
以编译和压缩 less 到 CSS 或DeployPhp\Assets::UGLIFYJS
以连接和压缩 JavaScript 或DeployPhp\Assets::ROLLUP
以使用现代 JavaScript 环境 - 可选的
env
- 如果缺失,此项目将用于调试和生产环境,或者您可以指定具体的环境DeployPhp\Assets::DEBUG
或DeployPhp\Assets::PRODUCTION
- 对于
type => DeployPhp\Assets::LESS
必需的file
- 要编译和压缩的源文件 - 对于
type => DeployPhp\Assets::SASS
必需的file
或files
- 要编译和压缩的源文件或文件 - 对于
type => DeployPhp\Assets::UGLIFYJS
必需的files
- 要连接和压缩的源文件 - 对于
type => DeployPhp\Assets::ROLLUP
必需的file
- 要处理的源文件(以下是一个示例配置)
下两个参数是可调用的函数,第一个用于从文件中读取哈希,第二个用于将哈希写入文件。在示例中展示了如何将其写入 neon 并与 Nette DI 一起使用。
最后一个(第四个)参数是可选的,定义一个包含可选设置的数组。更多关于此的内容请参阅示例。
要构建资源,首先需要调用 buildDebug($configNeon, $destinationDirectory)
或 buildProduction($configNeon, $destinationDirectory)
方法。
$configFile
文件,其中将存储实际的资源哈希,您可以在应用程序中使用它$destinationDirectory
目录,其中将构建资源
第一次仅当有更改的文件时才构建资源,并从所有文件的最后修改时间创建新的哈希(同时创建映射文件),第二次每次都构建资源并从每个文件的内容创建哈希。
带有 Babel 的 rollup.js 环境
这是现代JavaScript构建配置。您必须在您的资产目录中准备rollup
配置文件。
创建文件assets\rollup.config.js
import { babel } from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import { terser } from 'rollup-plugin-terser'; const config = { input: process.env.INPUT_FILE, // source file from PHP settings output: [ { // this compile file for old browsers file: process.env.OUTPUT_FILE.replace('{format}', 'iife'), // output file from PHP settings - string {format} is replaced with iife format: 'iife', name: 'app', // you can change this, it's some your identificator sourcemap: !!parseInt(process.env.SOURCE_MAP, 10), // this provide source map for DEVEL and not for production }, { // this complie modules JS for modern browsers file: process.env.OUTPUT_FILE.replace('{format}', 'esm'), format: 'esm', sourcemap: !!parseInt(process.env.SOURCE_MAP, 10), } ], plugins: [ nodeResolve(), // with this, you can import from node_modules commonjs(), // this resolve require() function babel({ // babel settings babelHelpers: 'runtime', presets: [ [ '@babel/preset-env', { 'bugfixes': true, 'corejs': '3.9', 'targets': '>0.25%', 'useBuiltIns': 'usage', } ] ], plugins: ['@babel/plugin-transform-runtime'], exclude: /\/node_modules\/core-js\//, // we must exclude core-js from being transpiled }), terser(), // minification ] }; export default config;
在您的HTML中,您可以像这样使用这两个文件
<script type="text/javascript" src="/js/scripts.iife.js" nomodule defer></script> <script type="module" src="/js/scripts.esm.js"></script>
示例
在deploy/assets.php
中
use Forrest79\DeployPhp; require __DIR__ . '/vendor/autoload.php'; return (new DeployPhp\Assets( __DIR__ . '/../temp', __DIR__ . '/assets', [ 'images' => DeployPhp\Assets::COPY, 'fonts' => DeployPhp\Assets::COPY, 'css/styles.css' => [ // target file 'type' => DeployPhp\Assets::LESS, 'file' => 'css/main.less', ], 'css/styles' => [ // target directory, main.css will be created here 'type' => DeployPhp\Assets::SASS, 'file' => 'css/main.sass', ], 'css/many-styles' => [ // target directory, main.css and print.css will be created here 'type' => DeployPhp\Assets::SASS, 'files' => [ 'css/main.sass', 'css/print.sass', ] ], 'js/scripts.js' => [ // target file 'type' => DeployPhp\Assets::JS, 'files' => [ 'js/bootstrap.js', 'js/modernizr-custom.js', 'js/web.js', ], ], 'js/jquery.min.js' => DeployPhp\Assets::COPY, 'js/jquery.min.map' => [ 'type' => DeployPhp\Assets::COPY, 'env' => DeployPhp\Assets::DEBUG, ], 'js/scripts.{format}.js' => [ // target file - will be compiled for more formats 'type' => DeployPhp\Assets::ROLLUP, 'file' => 'js/index.js', ], ], static function (string $configFile): ?string { if (!file_exists($configFile)) { return NULL; } $data = Neon\Neon::decode(file_get_contents($configFile)); if (!isset($data['assets']['hash'])) { return NULL; } return $data['assets']['hash']; }, static function (string $configFile, string $hash): void { file_put_contents($configFile, "assets:\n\t\thash: $hash\n"); }, ((($localConfig = @include __DIR__ . '/assets.local.php') === FALSE) ? [] : $localConfig) );
具有哈希的Neon文件具有以下结构
parameters: assets: hash: c11a678785091b7f1334c24a4123ee75 # md5 hash (32 characters)
在deploy/assets.local.php
中,您可以定义本地源资产目录,如果您使用的是虚拟服务器,其中路径与您的宿主路径不同。此目录将用于JS和CSS映射文件,以便在浏览器控制台中打开源文件
return [ 'localSourceDirectory' => 'P:/app/assets', ];
或者,您需要在此处指定您本地的服务器bin目录,如果它不同于/usr/bin:/bin
(包含node
二进制的目录)
return [ 'systemBinPath' => '/opt/usr/bin:/opt/bin', ];
在app/bootstrap.php
中
$configurator->addConfig(__DIR__ . '/config/config.neon'); if (PHP_SAPI !== 'cli') { $assetsConfigFile = __DIR__ . '/config/config.assets.neon'; $configurator->addConfig($assetsConfigFile); if ($configurator->isDebugMode()) { $assets = @include __DIR__ . '/../assets/assets.php'; // intentionally @ - file may not exists - good when production with production assets is running in debug mode (production preferable doesn't have assets source) if ($assets !== FALSE) { $assets->buildDebug($assetsConfigFile, __DIR__ . '/../../www/assets'); } } } $configurator->addConfig(__DIR__ . '/config/config.local.neon'); $container = $configurator->createContainer();
在调试模式下,哈希是从每个资产文件的最后修改时间戳计算得出的 - 创建哈希速度快(如果您更改文件或添加/删除某些文件,则哈希将更改,并且资产将在请求执行之前自动重建)。
在Nette中,您需要定义自己的资产扩展,该扩展将从assets.hash
读取哈希,并通过某种服务,您可以在您的应用程序中使用它。例如,如下所示
// Service to use in application namespace App\Assets; class Assets { /** @var string */ private $hash; public function __construct(string $hash) { $this->hash = $hash; } public function getHash(): string { return $this->hash; } } // Extension that uses neon structure with hash (just register this as extension in config.neon) namespace App\Assets\DI; use App\Assets; use Nette\DI\CompilerExtension; class Extension extends CompilerExtension { private $defaults = [ 'hash' => NULL, ]; public function loadConfiguration() { $builder = $this->getContainerBuilder(); $config = $this->validateConfig($this->defaults, $this->config); $builder->addDefinition($this->prefix('assets')) ->setFactory(Assets\Assets::class, [$config['hash']]); } }
在您的应用程序中,您可以使用哈希作为查询参数styles.css?hash
或在Web服务器中的虚拟路径,例如nginx,在路径/assets/hash/styles.css
上加载资产
location /assets/ {
expires 7d;
rewrite ^/assets/[a-z0-9]+/(.+)$ /assets/$1 break;
}
当构建应用程序时
/** @var DeployPhp\Assets $assets */ $assets = require __DIR__ . '/assets.php'; $assets->buildProduction($releaseBuildDirectory . '/app/config/config.assets.neon', $releaseBuildDirectory . '/www/assets')
哈希是从所有文件内容计算的,因此只有当某些文件内容更改或添加/删除相同文件时,哈希才会更改(创建哈希速度慢)。
构建和部署
仅包含一些辅助方法,用于从GIT检出、通过SFTP复制文件和通过SSH运行命令。有关文档,请参阅示例。
示例
use Forrest79\DeployPhp; require __DIR__ . '/../vendor/autoload.php'; //define('SSH_PRIVATE_KEY', 'define-this-in-deploy.local.php'); //define('SSH_AGENT_SOCK', 'define-this-in-deploy.local.php'); //define('DEPLOY_TEMP_DIRECTORY', 'define-this-in-deploy.local.php'); // if you want to change from default repository temp - on VirtualBox is recommended /tmp/... or some local (not shared) directory require __DIR__ . '/deploy.local.php'; class Deploy extends DeployPhp\Deploy { /** @var array<string, array<string, bool|float|int|string|array<mixed>|NULL>> */ protected array $config = [ 'vps' => [ 'gitBranch' => 'master', 'ssh' => [ 'server' => 'ssh.site.com', 'directory' => '/var/www/site.com', 'username' => 'forrest79', 'private_key' => 'C:\\Certificates\\certificate', 'passphrase' => NULL, // is completed dynamically - if needed (agent is tried at first), can be also callback call when password is needed 'ssh_agent' => SSH_AGENT_SOCK, // TRUE - try to read from env variable, string - socket file ], 'deployScript' => 'https://www.site.com/deploy.php', ] ]; private string $releasesDirectory; private string $releaseName; private string $releasePackage; private string $releaseBuildPackage; protected function setup() { $this->releasesDirectory = defined('DEPLOY_TEMP_DIRECTORY') ? DEPLOY_TEMP_DIRECTORY : __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . 'deploy'; $this->releaseName = 'release-' . date('Ymd-His') . '-' . uniqid(); $this->releasePackage = $this->releaseName . '.tar.gz'; $this->releaseBuildPackage = $this->releasesDirectory . DIRECTORY_SEPARATOR . $this->releasePackage; } public function run() { /** when password is get at the begin of the script (the old way) if (!$this->validatePrivateKey()) { $this->error('Bad passphrase for private key or bad private key.'); } */ $this->log('=> Creating build...'); $this->createBuild(); $this->log(' ...DONE'); $this->log('=> Deploying build...'); $this->deployBuild(); $this->log(' ...DONE'); $this->log('=> Cleaning up local files'); $this->delete($this->releaseDirectory); $this->log(' ...DONE'); } private function createBuild() { $releaseBuildDirectory = $this->releasesDirectory . DIRECTORY_SEPARATOR . $this->releaseName; $this->log(' -> checkout from GIT', FALSE); if (!$this->gitCheckout(__DIR__ . DIRECTORY_SEPARATOR . '..', $releaseBuildDirectory, $this->environment['gitBranch'])) { $this->error(' ...cant\'t checkout from GIT'); } $this->log(' ...OK'); $this->log(' -> building assets', FALSE); $assets = require __DIR__ . '/assets.php'; assert($assets instanceof DeployPhp\Assets); $assets ->setup($releaseBuildDirectory . '/app/config/config.assets.neon', $releaseBuildDirectory . '/app/assets', $releaseBuildDirectory . '/www/assets') ->buildProduction(); $this->log(' ...OK'); $this->log(' -> preparing package', FALSE); $this->delete($releaseBuildDirectory . '/app/assets'); $this->delete($releaseBuildDirectory . '/conf'); $this->delete($releaseBuildDirectory . '/data'); $this->delete($releaseBuildDirectory . '/db'); $this->delete($releaseBuildDirectory . '/deploy'); $this->delete($releaseBuildDirectory . '/download'); $this->delete($releaseBuildDirectory . '/logs'); $this->delete($releaseBuildDirectory . '/temp'); $this->delete($releaseBuildDirectory . '/.gitignore'); $this->delete($releaseBuildDirectory . '/composer.json'); $this->delete($releaseBuildDirectory . '/composer.lock'); $this->log(' ...OK'); $this->log(' -> compressing package', FALSE); $this->gzip($this->releasesDirectory, $this->releaseName, $this->releaseBuildPackage); $this->log(' ...OK'); } private function deployBuild() { $remoteReleaseDirectory = $this->environment['ssh']['directory'] . '/releases'; $remoteReleaseBudilDirectory = $remoteReleaseDirectory . '/' . $this->releaseName; $this->log(' -> uploading build package', FALSE); if (!$this->sftpPut($this->releaseBuildPackage, $remoteReleaseDirectory)) { $this->error(' ...an error occurred while uploading build package'); } $this->log(' ...OK'); $this->log(' -> extracting build package, creating temp, symlinks and removing build package', FALSE); if (!$this->ssh('cd ' . $remoteReleaseDirectory . ' && tar xfz ' . $this->releasePackage . ' && rm ' . $this->releasePackage . ' && mkdir ' . $remoteReleaseBudilDirectory . '/temp && ln -s ' . $this->environment['ssh']['directory'] . '/logs ' . $remoteReleaseBudilDirectory . '/logs && ln -s ' . $this->environment['ssh']['directory'] . '/data ' . $remoteReleaseBudilDirectory . '/www/data && ln -s ' . $this->environment['ssh']['directory'] . '/config/config.local.neon ' . $remoteReleaseBudilDirectory . '/app/config/config.local.neon')) { $this->error(' ...an error occurred while extracting build package, creating temp and symlinks'); } $this->log(' ...OK'); $this->log(' -> releasing build (replace link to current)', FALSE); if (!$this->ssh('ln -sfn ' . $remoteReleaseBudilDirectory . ' ' . $this->environment['ssh']['directory'] . '/current_new && mv -Tf ' . $this->environment['ssh']['directory'] . '/current_new ' . $this->environment['ssh']['directory'] . '/current')) { $this->error(' - an error occurred while releasing build'); } $this->log(' ...OK'); $this->log(' -> running after deploy script', FALSE); if (!$this->httpRequest($this->environment['deployScript'] . '?' . $this->releaseName , 'OK')) { $this->error(' ...an error occurred while running deploy script'); } $this->log(' ...OK'); $keepBuilds = 5; $this->log(' -> cleaning up old builds', FALSE); if (!$this->ssh('ls ' . $remoteReleaseDirectory . '/* -1td | tail -n +' . ($keepBuilds + 1) . ' | grep -v ' . $this->releaseName . ' | xargs rm -rf')) { $this->error(' ...an error occurred while cleaning old build'); } $this->log(' ...OK'); } } /** * RUN FROM COMMAND LINE ******************************************************* * ***************************************************************************** */ if ($argc == 1) { echo "Usage: php deploy.php <environment> [git-branch]"; exit(1); } /** when password is get at the begin of the script echo 'Enter SSH key password: '; try { $passphrase = Deploy::getHiddenResponse(); echo PHP_EOL; } catch (RuntimeException $e) { echo '[Can\'t get hidden response, password will be visible]: '; $passphrase = Deploy::getResponse(); } $additionalOptions = ['ssh' => ['passphrase' => $passphrase]]; */ $additionalOptions = [ 'ssh' => [ 'passphrase' => static function (Deploy $deploy, string $privateKeyFile): string { $passphrase = NULL; do { echo $passphrase === NULL ? PHP_EOL . ' > Enter SSH key password: ' : ' > Bad password, enter again: '; try { $passphrase = Deploy::getHiddenResponse(); echo PHP_EOL . ' '; } catch (RuntimeException) { echo '[Can\'t get hidden response, password will be visible]: '; $passphrase = Deploy::getResponse(); } } while (!$deploy->validatePrivateKey($privateKeyFile, $passphrase)); return $passphrase; }, ], ]; if ($argc > 2) { $additionalOptions['gitBranch'] = $argv[2]; } try { (new Deploy($argv[1], $additionalOptions))->run(); } catch (Exception $e) { echo $e->getMessage() . "\n"; exit(1); }
Composer monorepo
如果您为您的应用程序使用monorepo,您需要一个简单的工具来准备正确的composer.lock
。这是满足这些要求的一个简单工具
- 一个共享的全局供应商目录,包含所有库
- 更多应用程序具有本地供应商,在本地开发时使用共享的供应商,并在生产中安装
请注意,使用此工具始终会在全局composer上执行更新!下一步是将全局composer复制到本地一个,并且在这里也会执行更新。之后,本地供应商将被清理。
只是作为一个提示,全局和本地composer.json之间的差异将显示出来。这可能不是一个错误。
示例
/apps/appA/composer.json
/apps/appA/composer.lock
/apps/appA/vendor (autoload.php -> /vendor/autoload.php)
/apps/appB/composer.json
/apps/appB/composer.lock
/apps/appB/vendor (autoload.php -> /vendor/autoload.php)
/vendor/autoload.php
/vendor/[with all packages]
composer.json
composer.lock
prepare-monocomposer (source is below)
- 全局供应商已提交到存储库,为了准备生产构建,全局供应商被复制到本地一个,并在应用程序目录中执行
composer install
,因此这里只保留所需的包
#!/usr/bin/env php <?php declare(strict_types=1); (new Forrest79\DeployPhp\ComposerMonorepo(__DIR__ . '/composer.json', '--ignore-platform-reqs'))->updateSynchronize([ 'appA' => __DIR__ . '/apps/appA/composer.json', 'appB' => __DIR__ . '/apps/appB/composer.json', ]);
ComposeMonorepo
构造函数的第二个参数是composer update
命令的可选参数。