إنشاء Multi-Monorepo عام / خاص لمشاريع PHP

نشرت: 2022-03-10
ملخص سريع ↬ دعنا نرى كيفية استخدام نهج "monorepo متعدد" لجعل تجربة التطوير أسرع ، مع الحفاظ على خصوصية حزم PHP الخاصة بك. يمكن أن يكون هذا الحل مفيدًا بشكل خاص لمنشئي المكونات الإضافية للمحترفين.

لجعل تجربة التطوير الخاصة بي أسرع ، قمت بنقل جميع حزم PHP المطلوبة لمشاريعي إلى monorepo. عندما يتم استضافة كل حزمة في مستودع خاص بها (نهج multirepo) ، سأحتاج إلى تطويرها واختبارها بمفردها ثم نشرها على Packagist قبل أن أتمكن من تثبيتها في حزم أخرى عبر Composer. باستخدام monorepo ، نظرًا لاستضافة جميع الحزم معًا ، يمكن تطويرها واختبارها وإصدارها وإصدارها في نفس الوقت.

يعد monorepo الذي يستضيف حزم PHP الخاصة بي عامًا ، ويمكن لأي شخص الوصول إليه على GitHub. لا يمكن لمستودعات Git منح وصول مختلف إلى أصول مختلفة ؛ كلها إما عامة أو خاصة. نظرًا لأنني أخطط لإصدار مكون إضافي لبرنامج WordPress ، فأنا أريد أن تظل حزمه خاصة ، مما يعني أنه لا يمكن إضافتها إلى monorepo العام.

الحل الذي وجدته هو استخدام نهج "monorepo متعدد" ، يتألف من وحدتين monorepos: أحدهما عام والآخر خاص ، مع تضمين monorepo الخاص العام كنموذج Git الفرعي ، مما يسمح له بالوصول إلى ملفاته. يمكن اعتبار monorepo العام واحد المنبع ، و monorepo الخاص واحد المصب.

هندسة معمارية متعددة monorepo
هندسة معمارية متعددة monorepo. (معاينة كبيرة)

نظرًا لأنني واصلت التكرار على الكود الخاص بي ، كان إعداد المستودع الذي أحتاج إلى استخدامه في كل مرحلة من مراحل مشروعي بحاجة أيضًا إلى الترقية. ومن ثم ، لم أصل إلى نهج متعدد المونوريبو في اليوم الأول ؛ لقد كانت عملية امتدت لعدة سنوات واستغرقت قدرًا لا بأس به من الجهد ، من مستودع واحد ، إلى مستودعات متعددة ، إلى monorepo ، وأخيراً ، monorepo متعدد.

في هذه المقالة ، سوف أصف كيف أقوم بإعداد monorepo متعدد باستخدام Monorepo Builder ، والذي يعمل مع مشاريع PHP ويعتمد على Composer.

المزيد بعد القفز! أكمل القراءة أدناه ↓

إعادة استخدام الكود في Multi-Monorepo

monorepo العام في leoloso/PoP هو المكان الذي أحتفظ فيه بجميع مشاريع PHP الخاصة بي.

يحتوي هذا المونوريبو على ملف سير العمل generate_plugins.yml ، والذي يقوم بإنشاء العديد من مكونات WordPress الإضافية للتوزيع عندما أقوم بإنشاء إصدار على GitHub:

توليد الإضافات عند إنشاء إصدار
توليد الإضافات عند إنشاء إصدار. (معاينة كبيرة)

لم يتم ترميز تكوين سير العمل في ملف YAML ، بل يتم حقنه عبر كود PHP:

 - id: output_data run: | echo "::set-output name=plugin_config_entries::$(vendor/bin/monorepo-builder plugin-config-entries-json)"

ويتم توفير التكوين عبر فئة PHP مخصصة:

 class PluginDataSource { public function getPluginConfigEntries(): array { return [ // GraphQL API for WordPress [ 'path' => 'layers/GraphQLAPIForWP/plugins/graphql-api-for-wp', 'zip_file' => 'graphql-api.zip', 'main_file' => 'graphql-api.php', 'dist_repo_organization' => 'GraphQLAPI', 'dist_repo_name' => 'graphql-api-for-wp-dist', ], // GraphQL API - Extension Demo [ 'path' => 'layers/GraphQLAPIForWP/plugins/extension-demo', 'zip_file' => 'graphql-api-extension-demo.zip', 'main_file' => 'graphql-api-extension-demo.php', 'dist_repo_organization' => 'GraphQLAPI', 'dist_repo_name' => 'extension-demo-dist', ], ]; } }

أدى إنشاء العديد من مكونات WordPress الإضافية معًا ، وتكوين سير العمل عبر PHP ، إلى تقليل مقدار الوقت الذي أحتاجه لإدارة المشروع. يتعامل سير العمل حاليًا مع مكونين إضافيين (واجهة برمجة تطبيقات GraphQL وامتدادها التوضيحي) ، ولكن يمكنه التعامل مع 200 بدون مجهود إضافي مني.

هذا هو الإعداد الذي أريد إعادة استخدامه لمونوريبو الخاص في leoloso/GraphQLAPI-PRO ، بحيث يمكن أيضًا إنشاء المكونات الإضافية الاحترافية دون جهد.

سيتضمن الكود الذي سيتم إعادة استخدامه ما يلي:

  • سير عمل إجراءات GitHub لإنشاء مكونات WordPress الإضافية (بما في ذلك تحديد النطاق ، والرجوع إلى إصدار سابق من PHP 8.0 إلى 7.1 ، والتحميل إلى صفحة الإصدارات).
  • خدمات PHP المخصصة لتكوين سير العمل.

يمكن لـ monorepo الخاص بعد ذلك إنشاء إضافات WordPress الاحترافية ببساطة عن طريق تشغيل سير العمل من monorepo العام وتجاوز تكوينها في PHP.

ربط Monorepos عبر وحدات Git الفرعية

لتضمين المستودع العام في المستودع الخاص ، نستخدم وحدات Git الفرعية:

 git submodule add <public repo URL>

لقد قمت بتضمين المستودع العام في الوحدات الفرعية للمجلد الفرعي من submodules الخاص ، مما سمح لي بإضافة المزيد من monorepos المنبع في المستقبل إذا لزم الأمر. في GitHub ، يعرض المجلد الالتزام المحدد للوحدة الفرعية ، والنقر فوقه سينقلك إلى هذا الالتزام في leoloso/PoP :

تضمين monorepo العام في monorepo الخاص
تضمين monorepo العام في monorepo الخاص. (معاينة كبيرة)

نظرًا لأن المستودع الخاص يحتوي على وحدات فرعية ، لاستنساخه ، يجب علينا توفير الخيار --recursive :

 git clone --recursive <private repo URL>

إعادة استخدام مهام سير عمل إجراءات GitHub

يقوم GitHub Actions بتحميل مهام سير العمل فقط من ضمن .github/workflows . نظرًا لأن تدفقات العمل العامة في monorepo المتلقين للمعلومات تكون ضمن submodules/PoP/.github/workflows ، يجب تكرارها في الموقع المتوقع.

من أجل الحفاظ على سير العمل الأساسي كمصدر وحيد للحقيقة ، يمكننا أن نقصر أنفسنا على نسخ الملفات أسفل المسار تحت .github/workflows ، ولكن لا نقوم بتحريرها هناك. إذا كان يتعين إجراء أي تغيير ، فيجب إجراؤه في monorepo المنبع ثم نسخه.

كملاحظة جانبية ، لاحظ كيف أن هذا يعني أن monorepo متعدد التسريبات: إن monorepo المنبع ليس مستقلًا تمامًا ، وسيحتاج إلى تكييفه ليناسب monorepo المصب.

في أول تكرار لي لنسخ سير العمل ، قمت بإنشاء برنامج نصي بسيط من Composer:

 { "scripts": { "copy-workflows": [ "php -r \"copy('submodules/PoP/.github/workflows/generate_plugins.yml', '.github/workflows/generate_plugins.yml');\"", "php -r \"copy('submodules/PoP/.github/workflows/split_monorepo.yaml', '.github/workflows/split_monorepo.yaml');\"" ] } }

بعد ذلك ، بعد تحرير عمليات سير العمل في monorepo المنبع ، أود نسخها في اتجاه مجرى النهر عن طريق تنفيذ ما يلي:

 composer copy-workflows

ولكن بعد ذلك أدركت أن مجرد نسخ مهام سير العمل لا يكفي: يجب أيضًا تعديلها في هذه العملية. هذا لأن فحص monorepo المصب يتطلب الخيار --recurse-submodules أجل فحص الوحدات الفرعية أيضًا.

في إجراءات GitHub ، يتم إجراء الخروج في اتجاه مجرى النهر على النحو التالي:

 - uses: actions/checkout@v2 with: submodules: recursive

لذا ، فإن التحقق من المستودع المتلقين للمعلومات يحتاج إلى وحدات إدخال submodules: recursive ، لكن المنبع لا يحتاج ، وكلاهما يستخدم نفس الملف المصدر.

الحل الذي وجدته هو توفير القيمة submodules للإدخال عبر متغير البيئة CHECKOUT_SUBMODULES ، والذي يكون فارغًا افتراضيًا للمستودع الأولي:

 env: CHECKOUT_SUBMODULES: "" jobs: provide_data: steps: - uses: actions/checkout@v2 with: submodules: ${{ env.CHECKOUT_SUBMODULES }}

بعد ذلك ، عند نسخ مهام سير العمل من المنبع إلى المصب ، يتم استبدال قيمة CHECKOUT_SUBMODULES بالقيمة recursive :

 env: CHECKOUT_SUBMODULES: "recursive"

عند تعديل سير العمل ، من الأفضل استخدام تعبير عادي (regex) ، بحيث يعمل مع تنسيقات مختلفة في الملف المصدر (مثل CHECKOUT_SUBMODULES: "" أو CHECKOUT_SUBMODULES:'' أو CHECKOUT_SUBMODULES: . هذا سيمنع الخلل من التكون لهذه الأنواع من التغييرات غير المؤذية ظاهريًا.

وبالتالي ، فإن النص البرمجي لـ Copy copy-workflows Composer الموضح أعلاه ليس جيدًا بما يكفي للتعامل مع هذا التعقيد.

في التكرار التالي ، قمت بإنشاء أمر PHP ، CopyUpstreamMonorepoFilesCommand ، ليتم تنفيذه عبر Monorepo Builder:

 vendor/bin/monorepo-builder copy-upstream-monorepo-files

يستخدم هذا الأمر خدمة مخصصة ، FileCopierSystem ، لنسخ جميع الملفات من مجلد مصدر إلى الوجهة المحددة ، مع استبدال محتوياتها اختياريًا:

 namespace PoP\GraphQLAPIPRO\Extensions\Symplify\MonorepoBuilder\SmartFile; use Nette\Utils\Strings; use Symplify\SmartFileSystem\Finder\SmartFinder; use Symplify\SmartFileSystem\SmartFileSystem; final class FileCopierSystem { public function __construct( private SmartFileSystem $smartFileSystem, private SmartFinder $smartFinder, ) { } /** * @param array $patternReplacements a regex pattern to search, and its replacement */ public function copyFilesFromFolder( string $fromFolder, string $toFolder, array $patternReplacements = [] ): void { $smartFileInfos = $this->smartFinder->find([$fromFolder], '*'); foreach ($smartFileInfos as $smartFileInfo) { $fromFile = $smartFileInfo->getRealPath(); $fileContent = $this->smartFileSystem->readFile($fromFile); foreach ($patternReplacements as $pattern => $replacement) { $fileContent = Strings::replace($fileContent, $pattern, $replacement); } $toFile = $toFolder . substr($fromFile, strlen($fromFolder)); $this->smartFileSystem->dumpFile($toFile, $fileContent); } } } namespace PoP\GraphQLAPIPRO\Extensions\Symplify\MonorepoBuilder\SmartFile; use Nette\Utils\Strings; use Symplify\SmartFileSystem\Finder\SmartFinder; use Symplify\SmartFileSystem\SmartFileSystem; final class FileCopierSystem { public function __construct( private SmartFileSystem $smartFileSystem, private SmartFinder $smartFinder, ) { } /** * @param array $patternReplacements a regex pattern to search, and its replacement */ public function copyFilesFromFolder( string $fromFolder, string $toFolder, array $patternReplacements = [] ): void { $smartFileInfos = $this->smartFinder->find([$fromFolder], '*'); foreach ($smartFileInfos as $smartFileInfo) { $fromFile = $smartFileInfo->getRealPath(); $fileContent = $this->smartFileSystem->readFile($fromFile); foreach ($patternReplacements as $pattern => $replacement) { $fileContent = Strings::replace($fileContent, $pattern, $replacement); } $toFile = $toFolder . substr($fromFile, strlen($fromFolder)); $this->smartFileSystem->dumpFile($toFile, $fileContent); } } }

عند استدعاء هذه الطريقة لنسخ جميع مهام سير العمل في اتجاه المصب ، أستبدل أيضًا قيمة CHECKOUT_SUBMODULES :

 /** * Copy all workflows to `.github/`, and convert: * `CHECKOUT_SUBMODULES: ""` * into: * `CHECKOUT_SUBMODULES: "recursive"` */ $regexReplacements = [ '#CHECKOUT_SUBMODULES:(\s+".*")?#' => 'CHECKOUT_SUBMODULES: "recursive"', ]; (new FileCopierSystem())->copyFilesFromFolder( 'submodules/PoP/.github/workflows', '.github/workflows', $regexReplacements );

سير العمل في generate_plugins.yml يحتاج إلى بديل إضافي. عندما يتم إنشاء المكون الإضافي WordPress ، يتم تخفيض رمزه من PHP 8.0 إلى 7.1 من خلال استدعاء البرنامج النصي ci/downgrade/downgrade_code.sh :

 - name: Downgrade code for production (to PHP 7.1) run: ci/downgrade/downgrade_code.sh "${{ matrix.pluginConfig.rector_downgrade_config }}" "" "${{ matrix.pluginConfig.path }}" "${{ matrix.pluginConfig.additional_rector_configs }}"

في monorepo المصب ، سيكون هذا الملف موجودًا ضمن submodules/PoP/ci/downgrade/downgrade_code.sh . بعد ذلك ، نوجه سير العمل في المراحل النهائية إلى المسار الصحيح باستخدام هذا الاستبدال:

 $regexReplacements = [ // ... '#(ci/downgrade/downgrade_code\.sh)#' => 'submodules/PoP/$1', ];

تكوين الحزم في Monorepo Builder

الملف monorepo-builder.php - الموجود في جذر monorepo - يحمل تكوين Monorepo Builder. في ذلك ، يجب أن نشير إلى مكان وجود الحزم (والمكونات الإضافية والعملاء وأي شيء آخر):

 use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\MonorepoBuilder\ValueObject\Option; return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PACKAGE_DIRECTORIES, [ __DIR__ . '/packages', __DIR__ . '/plugins', ]); };

يجب أن يكون لدى monorepo الخاص إمكانية الوصول إلى جميع الكود: حزمه الخاصة ، بالإضافة إلى تلك من monorepo العام. بعد ذلك ، يجب أن تحدد جميع الحزم من كل من monorepos في ملف التكوين. تقع الوحدات من monorepo العامة ضمن /submodules/PoP :

 return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PACKAGE_DIRECTORIES, [ // public code __DIR__ . '/submodules/PoP/packages', __DIR__ . '/submodules/PoP/plugins', // private code __DIR__ . '/packages', __DIR__ . '/plugins', __DIR__ . '/clients', ]); };

كما هي ، فإن تكوينات المنبع والمصب هي نفسها إلى حد كبير ، والاختلافات هي أن التكوينات النهائية ستكون:

  • تغيير المسار إلى الحزم العامة ،
  • أضف الحزم الخاصة.

لذلك ، من المنطقي إعادة كتابة التكوين باستخدام البرمجة الشيئية (OOP). دعنا نتبع مبدأ DRY ("لا تكرر نفسك") من خلال توسيع فئة PHP في المستودع العام في المستودع الخاص.

إعادة التكوين عبر OOP

دعونا نعيد تشكيل التكوين. في المستودع العام ، يشير الملف monorepo-builder.php ببساطة إلى فئة جديدة ، ContainerConfigurationService ، حيث ستحدث كل الإجراءات:

 use PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurationService = new ContainerConfigurationService( $containerConfigurator, __DIR__ ); $containerConfigurationService->configureContainer(); };

تشير المعلمة __DIR__ إلى جذر monorepo. ستكون هناك حاجة للحصول على المسار الكامل لأدلة الحزمة.

أصبحت فئة ContainerConfigurationService مسؤولة الآن عن إنتاج التكوين:

 namespace PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\MonorepoBuilder\ValueObject\Option; class ContainerConfigurationService { public function __construct( protected ContainerConfigurator $containerConfigurator, protected string $rootDirectory, ) { } public function configureContainer(): void { $parameters = $this->containerConfigurator->parameters(); if ($packageOrganizationConfig = $this->getPackageOrganizationDataSource($this->rootDirectory)) { $parameters->set( Option::PACKAGE_DIRECTORIES, $packageOrganizationConfig->getPackageDirectories() ); } } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource($this->rootDirectory); } }

يمكن تقسيم التكوين عبر عدة فئات. في هذه الحالة ، تسترد ContainerConfigurationService تكوين الحزمة من خلال الفئة PackageOrganizationDataSource ، والتي يمكنك رؤية تنفيذها:

 namespace PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources; class PackageOrganizationDataSource { public function __construct(protected string $rootDir) { } public function getPackageDirectories(): array { return array_map( fn (string $packagePath) => $this->rootDir . '/' . $packagePath, $this->getRelativePackagePaths() ); } public function getRelativePackagePaths(): array { return [ 'packages', 'plugins', ]; } }

تجاوز التكوين في Monorepo المصب

الآن بعد أن تم إعداد التكوين في monorepo العام باستخدام OOP ، يمكننا توسيعه ليناسب احتياجات monorepo الخاص.

من أجل السماح لـ monorepo الخاص بالتحميل التلقائي لشفرة PHP من monorepo العام ، يجب علينا أولاً تكوين ملف composer.json المتلقين للمعلومات للإشارة إلى كود المصدر من المنبع ، والذي يقع تحت المسار submodules/PoP/src :

 { "autoload": { "psr-4": { "PoP\\GraphQLAPIPRO\\": "src", "PoP\\PoP\\": "submodules/PoP/src" } } }

يوجد أدناه ملف monorepo-builder.php الخاص. لاحظ أن فئة ContainerConfigurationService المشار إليها في المستودع الرئيسي تنتمي إلى مساحة الاسم PoP\PoP ولكن تم تحويلها الآن إلى مساحة الاسم PoP\GraphQLAPIPRO . يجب أن يتلقى هذا الفصل الإدخال الإضافي $upstreamRelativeRootPath (بقيمة submodules/PoP ) لإعادة إنشاء المسار الكامل للحزم العامة:

 use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurationService = new ContainerConfigurationService( $containerConfigurator, __DIR__, 'submodules/PoP' ); $containerConfigurationService->configureContainer(); };

تلغي فئة " ContainerConfigurationService " المتلقية للمعلومات أي فئة PackageOrganizationDataSource مستخدمة في التكوين:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators; use PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService as UpstreamContainerConfigurationService; use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; class ContainerConfigurationService extends UpstreamContainerConfigurationService { public function __construct( ContainerConfigurator $containerConfigurator, string $rootDirectory, protected string $upstreamRelativeRootPath ) { parent::__construct( $containerConfigurator, $rootDirectory ); } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource( $this->rootDirectory, $this->upstreamRelativeRootPath ); } }

أخيرًا ، تحتوي فئة المصب PackageOrganizationDataSource على المسار الكامل لكل من الحزم العامة والخاصة:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource as UpstreamPackageOrganizationDataSource; class PackageOrganizationDataSource extends UpstreamPackageOrganizationDataSource { public function __construct( string $rootDir, protected string $upstreamRelativeRootPath ) { parent::__construct($rootDir); } public function getRelativePackagePaths(): array { return array_merge( // Public packages - Prepend them with "submodules/PoP/" array_map( fn ($upstreamPackagePath) => $this->upstreamRelativeRootPath . '/' . $upstreamPackagePath, parent::getRelativePackagePaths() ), // Private packages [ 'packages', 'plugins', 'clients', ] ); } }

حقن التكوين من PHP في إجراءات GitHub

يقدم Monorepo Builder packages-json ، والتي يمكننا استخدامها لحقن مسارات الحزمة في سير عمل GitHub Actions:

 jobs: provide_data: steps: - id: output_data name: Calculate matrix for packages run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder packages-json)" outputs: matrix: ${{ steps.output_data.outputs.matrix }}

ينتج عن هذا الأمر JSON سلسلة. في سير العمل ، يجب تحويله إلى كائن JSON عبر fromJson :

 jobs: split_monorepo: needs: provide_data strategy: matrix: package: ${{ fromJson(needs.provide_data.outputs.matrix) }}

لسوء الحظ ، packages-json أسماء الحزم وليس مساراتها. سيعمل هذا إذا كانت جميع الحزم ضمن نفس المجلد (مثل packages/ ) ، لكنه لا يعمل في حالتنا لأن الحزم العامة والخاصة موجودة في مجلدات مختلفة.

لحسن الحظ ، يمكن توسيع Monorepo Builder بخدمات PHP المخصصة. لذلك ، قمت بإنشاء أمر مخصص ، package-entries-json (عبر الفئة PackageEntriesJsonCommand ) ، والذي يُخرج المسار إلى الحزمة.

ثم تم تحديث سير العمل بالأمر الجديد:

 run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json)"

يتم تنفيذه على monorepo العام ، ينتج هذا الحزم التالية (من بين العديد من الحزم الأخرى):

 [ { "name": "graphql-api-for-wp", "path": "layers/GraphQLAPIForWP/plugins/graphql-api-for-wp" }, { "name": "extension-demo", "path": "layers/GraphQLAPIForWP/plugins/extension-demo" }, { "name": "access-control", "path": "layers/Engine/packages/access-control" }, { "name": "api", "path": "layers/API/packages/api" }, { "name": "api-clients", "path": "layers/API/packages/api-clients" } ]

يتم تنفيذه على monorepo الخاص ، فإنه ينتج الإدخالات التالية (من بين أشياء أخرى كثيرة):

 [ { "name": "graphql-api-for-wp", "path": "submodules/PoP/layers/GraphQLAPIForWP/plugins/graphql-api-for-wp" }, { "name": "extension-demo", "path": "submodules/PoP/layers/GraphQLAPIForWP/plugins/extension-demo" }, { "name": "access-control", "path": "submodules/PoP/layers/Engine/packages/access-control" }, { "name": "api", "path": "submodules/PoP/layers/API/packages/api" }, { "name": "api-clients", "path": "submodules/PoP/layers/API/packages/api-clients" }, { "name": "graphql-api-pro", "path": "layers/GraphQLAPIForWP/plugins/graphql-api-pro" }, { "name": "convert-case-directives", "path": "layers/Schema/packages/convert-case-directives" }, { "name": "export-directive", "path": "layers/GraphQLByPoP/packages/export-directive" } ]

إنه يعمل بشكل جيد. يحتوي تكوين monorepo المتجه نحو المصب على كل من الحزم العامة والخاصة ، ويتم تقديم المسارات إلى الحزم العامة مسبقًا مع submodules/PoP .

تخطي الحزم العامة في المصب Monorepo

حتى الآن ، يتضمن monorepo المصب كلاً من الحزم العامة والخاصة في تكوينه. ومع ذلك ، لا يلزم تنفيذ كل أمر على الحزم العامة.

خذ التحليل الساكن ، على سبيل المثال. يقوم monorepo العام بالفعل بتنفيذ PHPStan على جميع الحزم العامة عبر ملف مسار العمل phpstan.yml ، كما هو موضح في هذا التشغيل. إذا قامت monorepo المصب بتشغيل PHPStan مرة أخرى على الحزم العامة ، فسيكون ذلك مضيعة لوقت الحوسبة. يحتاج سير عمل phpstan.yml إلى التشغيل على الحزم الخاصة فقط.

هذا يعني أنه بناءً على الأمر الذي سيتم تنفيذه في مستودع المصب ، قد نرغب في تضمين الحزم العامة والخاصة أو الحزم الخاصة فقط.

لتحديد ما إذا كان يجب إضافة حزم عامة في التكوين المتلقين للمعلومات ، نقوم بتكييف فئة المصب PackageOrganizationDataSource للتحقق من هذا الشرط عبر الإدخال $includeUpstreamPackages :

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource as UpstreamPackageOrganizationDataSource; class PackageOrganizationDataSource extends UpstreamPackageOrganizationDataSource { public function __construct( string $rootDir, protected string $upstreamRelativeRootPath, protected bool $includeUpstreamPackages ) { parent::__construct($rootDir); } public function getRelativePackagePaths(): array { return array_merge( // Add the public packages? $this->includeUpstreamPackages ? // Public packages - Prepend them with "submodules/PoP/" array_map( fn ($upstreamPackagePath) => $this->upstreamRelativeRootPath . '/' . $upstreamPackagePath, parent::getRelativePackagePaths() ) : [], // Private packages [ 'packages', 'plugins', 'clients', ] ); } }

بعد ذلك ، نحتاج إلى تقديم القيمة $includeUpstreamPackages إما true أو false ، اعتمادًا على الأمر المراد تنفيذه.

يمكننا القيام بذلك عن طريق استبدال ملف التكوين monorepo-builder.php تكوين آخرين: monorepo-builder-with-upstream-packages.php -pack.php (الذي يمر $includeUpstreamPackages => true ) و monorepo-builder-without-upstream-packages.php (الذي $includeUpstreamPackages => false ):

 // File monorepo-builder-without-upstream-packages.php use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurationService = new ContainerConfigurationService( $containerConfigurator, __DIR__, 'submodules/PoP', false, // This is $includeUpstreamPackages ); $containerConfigurationService->configureContainer(); };

نقوم بعد ذلك بتحديث ContainerConfigurationService لتلقي المعلمة $includeUpstreamPackages إلى PackageOrganizationDataSource :

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators; use PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService as UpstreamContainerConfigurationService; use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; class ContainerConfigurationService extends UpstreamContainerConfigurationService { public function __construct( ContainerConfigurator $containerConfigurator, string $rootDirectory, protected string $upstreamRelativeRootPath, protected bool $includeUpstreamPackages, ) { parent::__construct( $containerConfigurator, $rootDirectory, ); } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource( $this->rootDirectory, $this->upstreamRelativeRootPath, $this->includeUpstreamPackages, ); } }

بعد ذلك ، يتعين علينا استدعاء monorepo-builder مع أي من ملفي التكوين ، من خلال توفير الخيار --config :

 jobs: provide_data: steps: - id: output_data name: Calculate matrix for packages run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json --config=monorepo-builder-without-upstream-packages.php)"

ومع ذلك ، كما رأينا سابقًا ، نريد الاحتفاظ بسير عمل GitHub Actions في monorepo المنبع كمصدر وحيد للحقيقة ، ومن الواضح أنهم لا يحتاجون إلى هذه التغييرات.

الحل الذي وجدته لهذه المشكلة هو توفير خيار --config دائمًا في المستودع الرئيسي ، مع حصول كل أمر على ملف التكوين الخاص به ، مثل أمر validate الذي يستقبل ملف تكوين validate.php :

 - name: Run validation run: vendor/bin/monorepo-builder validate --config=config/monorepo-builder/validate.php

الآن ، لا توجد ملفات تكوين في monorepo المنبع ، لأنها لا تحتاج إليها. لكنه لن ينكسر ، لأن Monorepo Builder يتحقق مما إذا كان ملف التكوين موجودًا ، وإذا لم يكن كذلك ، فإنه يقوم بتحميل ملف التكوين الافتراضي بدلاً من ذلك. لذلك ، إما أننا سنتجاوز التكوين أو لن يحدث شيء.

يوفر مستودع المصب ملفات التكوين لكل أمر ، مع تحديد ما إذا كان سيتم إضافة الحزم الأولية:

كملاحظة جانبية ، هذا مثال آخر على كيفية تسرب المونوريبو المتعدد.

 // File config/monorepo-builder/validate.php return require_once __DIR__ . '/monorepo-builder-with-upstream-packages.php';

تجاوز التكوين

نحن على وشك الإنتهاء. الآن ، يمكن monorepo المصب تجاوز التكوين من monorepo المنبع. لذا ، كل ما تبقى هو توفير التكوين الجديد.

في فئة PluginDataSource ، تجاوزت التكوين الذي يجب إنشاء مكونات WordPress الإضافية الخاصة به ، مع توفير الإضافات الاحترافية بدلاً من ذلك:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PluginDataSource as UpstreamPluginDataSource; class PluginDataSource extends UpstreamPluginDataSource { public function getPluginConfigEntries(): array { return [ // GraphQL API PRO [ 'path' => 'layers/GraphQLAPIForWP/plugins/graphql-api-pro', 'zip_file' => 'graphql-api-pro.zip', 'main_file' => 'graphql-api-pro.php', 'dist_repo_organization' => 'GraphQLAPI-PRO', 'dist_repo_name' => 'graphql-api-pro-dist', ], // GraphQL API Extensions // Google Translate [ 'path' => 'layers/GraphQLAPIForWP/plugins/google-translate', 'zip_file' => 'graphql-api-google-translate.zip', 'main_file' => 'graphql-api-google-translate.php', 'dist_repo_organization' => 'GraphQLAPI-PRO', 'dist_repo_name' => 'graphql-api-google-translate-dist', ], // Events Manager [ 'path' => 'layers/GraphQLAPIForWP/plugins/events-manager', 'zip_file' => 'graphql-api-events-manager.zip', 'main_file' => 'graphql-api-events-manager.php', 'dist_repo_organization' => 'GraphQLAPI-PRO', 'dist_repo_name' => 'graphql-api-events-manager-dist', ], ]; } }

سيؤدي generate_plugins.yml إصدار جديد على GitHub إلى تشغيل سير عمل create_plugins.yml وسيُنشئ المكونات الإضافية الاحترافية في monorepo الخاص:

توليد الإضافات الاحترافية
توليد الإضافات الاحترافية. (معاينة كبيرة)

تا دا!

خاتمة

كالعادة ، لا يوجد حل "أفضل" ، فقط الحلول التي قد تعمل بشكل أفضل حسب السياق. نهج متعدد المونوريبو ليس مناسبًا لكل نوع من المشاريع أو الفريق. أعتقد أن أكبر المستفيدين سيكونون منشئو المكونات الإضافية الذين يطلقون المكونات الإضافية العامة التي ستتم ترقيتها إلى إصداراتهم الاحترافية ، بالإضافة إلى الوكالات التي تخصص المكونات الإضافية لعملائها.

في حالتي ، أنا سعيد جدًا بهذا النهج. يستغرق القيام بذلك بشكل صحيح بعض الوقت والجهد ، لكنه استثمار لمرة واحدة. بمجرد انتهاء الإعداد ، يمكنني التركيز على بناء المكونات الإضافية الاحترافية ، وقد يكون الوقت الموفر في إدارة المشروع ضخمًا.