PHP源代码如何打包成一个单独的文件?

PHP打包

开发Node项目的时候,很喜欢Node的一个点就是可以通过rollup这些打包软件,把所有项目代码打包到一个js里,这样部署起来简单又方便。

本着一个问题肯定不止我一个人遇到的定理,自己也研究了一下PHP项目打包,最终实现了一个相对可行的方案。

1. 什么是Phar?

Phar 是一种 PHP 归档文件格式,类似于 Java 中的 JAR 文件,用于将多个文件打包到一个单一的文件中。

简而言之就是可以把指定目录内的一些文件打包到一个文件里,然后通过PHP执行。

2. 兼容Phar

  • 打包成phar后,如果需要访问 Phar 归档内部的文件,必须使用 Phar 的虚拟路径。
  • __FILE__ 会返回 Phar 文件的路径,而 __DIR__ 会返回 Phar 文件的目录路径

3. 优缺点

Phar 文件会压缩打包的文件,这在一定程度上可以减少文件的大小,但解压过程会增加一些 CPU 负载。

所以对于php-fpm模式的项目来说,使用phar是否会带来多余的开销,需要经过实际的测试评估。

但是对于swoole这种常驻内存的项目来说,phar只会在启动的时候加载,完全不会带来多余的开销,还可以优化启动速度。

如何打包

下面的打包脚本是参照vite等打包软件的流程来编写的,打包的同时会自动删除项目内的注释,可以修改对应的配置后通过 php 脚本名称.php 来进行打包:

<?php

/* 打包的配置 */
$config = [
    'name' => 'app.phar',  //打包后的文件名
    'entry' => "easyswoole", //应用入口
    'src' => './server', //打包的目录
    'tmp' => './tmp', //临时目录
    'dist' => './build', //打包输出的目录
    'exclude' => [
        /* 排除不打包的文件和目录 */
        'build' => [
            'public',
            'runtime',
            '*.sql',
            '*.phar',
            '*.md',
            '.git',
            '*.stackdump'
        ],
        /* 排除不移除文件注释的目录 */
        'comment' => [
            'vendor'
        ]
    ],

];


/**
 * @param $log
 * @return void
 * 输出日志
 */
function logEcho($log)
{
    echo date('Y-m-d H:i:s') . ",${log}\n";
}

/**
 * @param $dir
 * @return bool
 * 清理指定目录的文件
 */
function deleteDirectory($dir)
{
    if (!is_dir($dir)) {
        return true;
    }
    $files = array_diff(scandir($dir), ['.', '..']);
    foreach ($files as $file) {
        $path = $dir . '/' . $file;
        if (is_dir($path)) {
            deleteDirectory($path);
        } else {
            unlink($path);
        }
    }
    return rmdir($dir);
}

/* 注册关闭函数,确保在脚本中断时删除临时目录 */
register_shutdown_function(function () use ($config) {
    if (is_dir($config['tmp'])) {
        deleteDirectory($config['tmp']);
    }
});

/* 创建临时目录,用于保存源文件的拷贝 */
if (!is_dir($config['tmp'])) {
    mkdir($config['tmp'], 0777, true);
}

/* 删除目标目录 */
if (is_dir($config['dist'])) {
    deleteDirectory($config['dist']);
    mkdir($config['dist'], 0777, true);
} else {
    mkdir($config['dist'], 0777, true);
}

logEcho("开始处理文件...");

/* 将需要打包的文件复制到临时目录 */
$files = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($config['src'], \FilesystemIterator::SKIP_DOTS),
    RecursiveIteratorIterator::LEAVES_ONLY
);

/* 开始遍历生成文件 */
foreach ($files as $file) {

    $relativePath = str_replace($config['src'], '', $file->getPathname());
    $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath);
    $tempFilePath = $config['tmp'] . $relativePath;


    /* 检查是否排除当前文件 */
    $exclude = false;
    $parts = explode('/', $relativePath);

    foreach ($config['exclude']['build'] as $pattern) {
        foreach ($parts as $part) {
            if (fnmatch($pattern, $part)) {
                $exclude = true;
                break;
            }
        }
    }


    if (!$exclude) {

        /* 创建临时文件的目录 */
        $tempFileDir = dirname($tempFilePath);
        /* 创建 */
        if (!is_dir($tempFileDir)) {
            mkdir($tempFileDir, 0777, true);
        }

        /* 复制文件到临时目录 */
        copy($file->getPathname(), $tempFilePath);

        /* 移除注释 */
        $comment = true;
        $ext = pathinfo($tempFilePath)['extension'];

        /* 是否匹配当前路径 */
        foreach ($config['exclude']['comment'] as $pattern) {
            foreach ($parts as $part) {
                if (fnmatch($pattern, $part)) {
                    $comment = false;
                    break;
                }
            }
        }


        /* 移除注释 */
        if ($ext === 'php' && $comment) {
            /* 对文件进行二次处理(例如修改文件内容) */
            $content = file_get_contents($tempFilePath);
            /* 移除所有多行注释 */
            $preg_1 = '#/\*[\s\S]*?\*/#u'; // 正则表达式:匹配多行注释
            $content = preg_replace($preg_1, '', $content);
            /* 移除所有单行注释 */
            $preg_2 = '#//[^\'\"/]*?\n#u'; // 正则表达式:匹配单行注释
            $content = preg_replace($preg_2, "\n", $content);
            /* 写入文件 */
            file_put_contents($tempFilePath, $content);
        }

        /* 输出文件 */
        logEcho(str_replace(DIRECTORY_SEPARATOR, '/', $file->getPathname()));
    }
}

/* 输出日志 */
logEcho("开始打包...");

/* 创建 Phar 文件 */
$pharPath = $config['dist'] . '/' . $config['name'];
$phar = new Phar($pharPath, 0, $config['name']);
$phar->buildFromDirectory($config['tmp']);

/* 设置 Phar 的入口文件 */
$phar->setStub($phar->createDefaultStub($config['entry']));

/* 清理临时目录 */
deleteDirectory($config['tmp']);

/* 输出日志 */
logEcho("打包成功!");