PHP `Monorepo` 架构:多包管理与构建优化 (Composer / Lerna)

各位观众老爷,晚上好!今天咱们聊点儿“骚”的,啊不,是“潮”的——PHP Monorepo架构,看看怎么用Composer和Lerna把咱们的代码玩出新花样!

开场白:单身久了,看什么都像对象?

话说,程序员的世界里,代码就像自己的孩子,辛辛苦苦写出来,总想好好呵护。但项目多了,代码散落在各处,就像单身久了,看什么都像对象,想找个靠谱的都难。

传统的代码管理方式,每个项目一个仓库(Repo),叫做Multi-Repo。优点是独立性强,改动互不影响。缺点嘛,就像各自为战的游击队,资源分散,依赖管理混乱,简直是噩梦。

这时候,Monorepo就像一个温柔的港湾,把所有代码都放在一起,集中管理,统一构建,仿佛一个大家庭,其乐融融。

第一幕:Monorepo是个啥?

Monorepo,字面意思就是“单一代码仓库”。它是一种代码管理策略,将多个项目、组件或库的代码放在同一个版本控制仓库中。这和Multi-Repo(多仓库)模式形成鲜明对比,Multi-Repo每个项目都有自己的独立仓库。

Monorepo的优点:

  • 代码复用性高: 就像一家人,互相借东西方便得很,代码复用起来也更容易,避免重复造轮子。
  • 依赖管理简单: 集中管理依赖关系,升级、修复bug更方便,再也不用担心依赖冲突了。
  • 原子性变更: 一个变更可以影响多个项目,保证一致性,避免出现“按下葫芦浮起瓢”的情况。
  • 协作效率高: 团队成员可以更容易地了解整个项目的结构和代码,协作起来更流畅。

Monorepo的缺点:

  • 仓库体积大: 所有代码都在一个仓库里,体积肯定不小,clone、checkout可能会慢一些。
  • 构建复杂: 需要更复杂的构建流程来管理各个项目之间的依赖关系。
  • 权限管理: 需要更精细的权限管理,避免误操作影响其他项目。

第二幕:Composer登场!PHP世界的依赖管家

Composer,PHP的依赖管理工具,相当于Java的Maven,JavaScript的npm。它可以帮助我们管理项目中的各种依赖包,自动下载、安装、更新。

Composer的基本使用:

  1. 安装Composer:

    curl -sS https://getcomposer.org/installer | php
    mv composer.phar /usr/local/bin/composer
    composer --version
  2. 创建composer.json文件:

    {
        "name": "my-project",
        "description": "My awesome project",
        "require": {
            "monolog/monolog": "^2.0"
        },
        "autoload": {
            "psr-4": {
                "MyProject\": "src/"
            }
        }
    }
    • name: 项目名称
    • description: 项目描述
    • require: 依赖包列表
    • autoload: 自动加载配置
  3. 安装依赖:

    composer install
  4. 更新依赖:

    composer update

Composer在Monorepo中的应用:

在Monorepo中,每个项目/package都有自己的composer.json文件,用于声明自己的依赖。可以通过Composer Workspaces功能,实现更高效的依赖管理。

Composer Workspaces:

Composer Workspaces允许在一个项目中管理多个包,并共享依赖关系。这对于Monorepo来说非常有用。

  1. 根目录的composer.json:

    {
        "name": "my-monorepo",
        "description": "My awesome monorepo",
        "require": {
            "php": "^7.4 || ^8.0"
        },
        "autoload": {
            "psr-4": {
                "MyMonorepo\": "src/"
            }
        },
        "repositories": [
            {
                "type": "path",
                "url": "packages/*"
            }
        ],
        "extra": {
            "composer-exit-on-patch": true,
            "patchLevel": "minor"
        }
    }
    • repositories: 声明包的查找路径,这里指向packages目录下的所有子目录。
  2. packages/package-a/composer.json:

    {
        "name": "my-monorepo/package-a",
        "description": "Package A",
        "autoload": {
            "psr-4": {
                "MyMonorepo\PackageA\": "src/"
            }
        },
        "require": {
            "my-monorepo/package-b": "*"
        }
    }
  3. packages/package-b/composer.json:

    {
        "name": "my-monorepo/package-b",
        "description": "Package B",
        "autoload": {
            "psr-4": {
                "MyMonorepo\PackageB\": "src/"
            }
        }
    }

使用Composer Workspaces后,可以在根目录下执行composer install,Composer会自动解析所有子项目的依赖关系,并将它们安装到vendor目录中。

第三幕:Lerna闪亮登场!JavaScript世界的Monorepo神器

Lerna最初是为JavaScript Monorepo设计的工具,但它的一些思想和机制可以借鉴到PHP Monorepo中。Lerna的主要功能是管理多个包的版本发布和依赖关系。

Lerna的主要功能:

  • 版本管理: 自动检测哪些包需要发布新版本,并自动更新package.json文件。
  • 依赖管理: 支持使用npmyarn来管理依赖关系。
  • 发布: 自动发布包到npm仓库。

Lerna在PHP Monorepo中的借鉴:

虽然Lerna是为JavaScript设计的,但我们可以借鉴它的版本管理和依赖管理思路。

  1. 版本管理: 可以使用Git tag来管理PHP包的版本。当需要发布新版本时,可以创建一个新的Git tag,并更新composer.json文件中的版本号。
  2. 依赖管理: 可以使用Composer Workspaces来管理依赖关系。
  3. 构建脚本: 可以编写Shell脚本或PHP脚本来自动化构建、测试、发布流程。

一个简单的PHP Monorepo构建脚本:

#!/bin/bash

# 遍历packages目录下的所有子目录
for package_dir in packages/*; do
  # 检查是否是目录
  if [ -d "$package_dir" ]; then
    # 进入子目录
    cd "$package_dir"

    # 执行构建命令
    echo "Building $package_dir..."
    composer install
    # 执行单元测试
    echo "Running tests for $package_dir..."
    ./vendor/bin/phpunit

    # 返回上一级目录
    cd ..
  fi
done

echo "All packages built and tested successfully!"

第四幕:实战演练!搭建一个简单的PHP Monorepo

咱们来搭建一个简单的PHP Monorepo,包含两个包:package-apackage-b

  1. 创建目录结构:

    my-monorepo/
    ├── packages/
    │   ├── package-a/
    │   │   ├── src/
    │   │   │   └── MyClassA.php
    │   │   ├── composer.json
    │   │   └── phpunit.xml
    │   ├── package-b/
    │   │   ├── src/
    │   │   │   └── MyClassB.php
    │   │   ├── composer.json
    │   │   └── phpunit.xml
    ├── composer.json
    └── phpunit.xml
  2. 根目录的composer.json

    {
        "name": "my-monorepo",
        "description": "My awesome monorepo",
        "require": {
            "php": "^7.4 || ^8.0",
            "phpunit/phpunit": "^9.0"
        },
        "autoload": {
            "psr-4": {
                "MyMonorepo\": "src/"
            }
        },
        "repositories": [
            {
                "type": "path",
                "url": "packages/*"
            }
        ],
         "config": {
            "sort-packages": true
        },
        "extra": {
            "composer-exit-on-patch": true,
            "patchLevel": "minor"
        }
    }
  3. packages/package-a/composer.json

    {
        "name": "my-monorepo/package-a",
        "description": "Package A",
        "autoload": {
            "psr-4": {
                "MyMonorepo\PackageA\": "src/"
            }
        },
        "require": {
            "my-monorepo/package-b": "*"
        }
    }
  4. packages/package-b/composer.json

    {
        "name": "my-monorepo/package-b",
        "description": "Package B",
        "autoload": {
            "psr-4": {
                "MyMonorepo\PackageB\": "src/"
            }
        }
    }
  5. packages/package-a/src/MyClassA.php

    <?php
    
    namespace MyMonorepoPackageA;
    
    use MyMonorepoPackageBMyClassB;
    
    class MyClassA
    {
        public function doSomething()
        {
            $b = new MyClassB();
            return "A says: " . $b->getMessage();
        }
    }
  6. packages/package-b/src/MyClassB.php

    <?php
    
    namespace MyMonorepoPackageB;
    
    class MyClassB
    {
        public function getMessage()
        {
            return "Hello from B!";
        }
    }
  7. packages/package-a/phpunit.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
             bootstrap="vendor/autoload.php"
             cacheResultFile=".phpunit.cache/test-results"
             executionOrder="depends,defects"
             forceCoversAnnotation="true"
             beStrictAboutCoversAnnotation="true"
             beStrictAboutOutputDuringTests="true"
             beStrictAboutTodoAnnotated="true"
             verbose="true">
        <testsuites>
            <testsuite name="Package A Test Suite">
                <directory suffix="Test.php">./tests</directory>
            </testsuite>
        </testsuites>
        <coverage processUncoveredFiles="true">
            <include>
                <directory suffix=".php">./src</directory>
            </include>
        </coverage>
        <php>
            <ini name="error_reporting" value="-1"/>
        </php>
    </phpunit>
  8. packages/package-b/phpunit.xml

    内容类似packages/package-a/phpunit.xml,只需要修改testsuite name即可。

  9. 在根目录下执行composer install

  10. 编写单元测试,验证代码的正确性。

第五幕:构建优化!让Monorepo飞起来

Monorepo的构建过程可能会比较慢,尤其是在大型项目中。我们需要采取一些优化措施来提高构建速度。

  1. 并行构建: 可以使用xargsparallel等工具来并行执行构建任务。

    find packages -name composer.json -print0 | xargs -0 -n 1 -P 4 composer install
    • -P 4: 指定并行执行的任务数量。
  2. 缓存: 可以使用Composer的缓存机制来避免重复下载依赖包。

    composer config --global cache-dir /path/to/global/cache
  3. 增量构建: 只构建修改过的包及其依赖。可以使用Git来检测哪些包发生了变化。

    # 获取上次提交到当前提交之间修改过的目录
    changed_dirs=$(git diff --name-only HEAD^ HEAD | xargs -n 1 dirname | sort -u)
    
    # 遍历修改过的目录
    for dir in $changed_dirs; do
      # 检查是否是packages目录下的子目录
      if [[ $dir == packages/* ]]; then
        # 进入子目录
        cd "$dir"
    
        # 执行构建命令
        echo "Building $dir..."
        composer install
        ./vendor/bin/phpunit
    
        # 返回上一级目录
        cd ../..
      fi
    done
  4. 使用Docker: 将构建过程放在Docker容器中,可以保证构建环境的一致性,并利用Docker的缓存机制来加速构建。

第六幕:权衡利弊!Monorepo适合你吗?

Monorepo并非银弹,它也有自己的适用场景。在选择Monorepo架构之前,需要仔细权衡利弊。

特性 Monorepo Multi-Repo
代码复用性
依赖管理 简单 复杂
原子性变更 支持 不支持
协作效率
仓库体积
构建复杂性
权限管理 复杂 简单
适用场景 大型项目,多个组件/库之间存在依赖 小型项目,项目之间独立性强

总结:

Monorepo是一种先进的代码管理策略,可以提高代码复用性、简化依赖管理、提高协作效率。但它也需要更复杂的构建流程和权限管理。在选择Monorepo架构之前,需要仔细评估项目的需求和团队的实力。

希望今天的讲座能给大家带来一些启发,让大家在代码管理的道路上越走越远,越走越宽广!下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注