💁🏻♀️ Gato GraphQL Neden Bir Monorepo'ya İhtiyaç Duyar ve Nasıl Optimize Edilir
Birkaç gün önce Tüm PHP paketlerinizi bir monorepo'da barındırma adlı makaleyi yayımladım; PHP kod tabanımızı yönetmek için neden bir monorepo kullanmak isteyebileceğimizi ve bunu Monorepo Builder aracılığıyla nasıl yapacağımızı açıkladım.
Burada o makaleyi tamamlamak, GatoGraphQL/GatoGraphQL kod tabanının (Gato GraphQL'i, temel alınan GraphQL motorunu ve üzerine inşa edildiği bileşen modeli mimarisini barındıran) neden bir monorepo'da barındırılması gerektiğini ve yaptığım optimizasyonları biraz daha ayrıntılı açıklamak istiyorum.
Gato GraphQL Neden Bir Monorepo'ya İhtiyaç Duyar
CMS-agnostisizmi desteklemek için Gato GraphQL ve ilişkili projelerin kod tabanı, Composer aracılığıyla yönetilen çok sayıda pakete bölündü. Toplamda 100'den fazla paket oluşturuldu! (Şu anda bu sayı 200'ün üzerinde.)
Çok sayıda paket, hepsini Composer aracılığıyla bir araya getirmeye herhangi bir ekstra karmaşıklık katmaz: sadece composer install çalıştırırız ve her şey çalışır. Ancak, her paket kendi deposunda yaşadığında, sürümleme nedeniyle geliştirme sürecinde sorunlar ortaya çıkar.
Her paketin sürümlenmesi gerekir ve bir paketin her sürümü, başka bir paketin belirli bir sürümüne bağlı olacaktır. Bu kadar çok paket varken, PR oluştururken tüm sürümlerin birbirine nasıl bağlı olduğunu yapılandırmak bir kâbusa dönüşür; bir ucunu görebileceğiniz ama nerede bittiğini bilemeyeceğiniz bir spagetti kod tabağına benzer.

Gerçek şu ki, tüm dahil olan depolardaki birden fazla dalın tüm sürümlerini birbirine bağlamak o kadar zorlaştı ki, bu süreci tamamen atlayıp kodu doğrudan her depodaki master dalına push ettim ve ardından her birinde dev-master sürümüne bağlı kaldım.
Bu doğru değildi. Tüm kodu GatoGraphQL/GatoGraphQL içinde barındıran monorepo modeline geçmek, sorunu fiilen çözdü.
Hoş Geldin Yan Etkisi: Katkılarda Daha Düşük Engel
Makalede de belirttiğim gibi, projenin paket başına bir depo kullandığı dönemde, bir katılımcı çalışma ortamını kuramadığı için projeye katılmadan bile vazgeçti.
Monorepo'ya geçmeden önce, geliştirme ortamını kurmak çok zordu. Ben yazarı olduğum için tüm depoları klonlayıp hepsini tek bir VSCode çalışma alanına ekleyebildim; bu benim için bir şekilde işe yarıyordu.
Potansiyel katılımcıların aynı ortamı kurmasını kolaylaştırmaya çalıştım; bu bash betiği aracılığıyla. Ama ciddiye alınırsa, bu hiçbir zaman işe yarayamazdı; en başından yenik bir savaştı ve kimse projeye katkıda bulunmaya başlayamıyordu.
Monorepo ile artık gece rahat uyuyabiliyorum; eğer katkıda bulunmak isterlerse, katılımcıları gereksiz bürokratik engellerle geri çevirmeyeceğimi biliyorum.
Monorepo'yu Optimize Etme
Makalede de belirttiğim gibi, Monorepo Builder kütüphanesini alternatiflere tercih etmenin avantajı, PHP ile inşa edilmiş olması ve genişletilebilir olmasıdır.
Örneğin, master'a push yapılırken ve monorepo bölünürken, GitHub Action'daki matris normalde her paket için bir runner örneği başlatır; bu örnekler, paketin kodunu dağıtım için (Packagist aracılığıyla) kendi deposuyla senkronize eder.
GatoGraphQL/GatoGraphQL 200'den fazla paket içerdiğinden, 200'den fazla runner örneği başlatılıyordu.

Sorun şu ki GitHub paralel olarak çalışan 20 iş limitini uyguluyor. Tüm işlemler bir kuyruğa alındığından, diğer işlemleri çalıştırmaya devam edebilmek için bunların bitmesini beklemem gerekiyordu.
Ayrıca, GitHub zaman zaman bir runner'ı hemen sağlamıyordu ve sizi daha sonrasına beklemeye alıyordu:

Tüm bunlar bekleme süresine dönüşüyor. 200'den fazla paketle, tek bir PR'ı birleştirmek 1 saate kadar sürebiliyordu! Bu çözülmesi gereken bir sorundu.
Monorepo'yu özel komutlarla genişletmek sorunu çözebilir.
Monorepo Builder'ı Genişletme
Normalde şu komutu çalıştırdığımızda, depodaki tüm paketlerin listesini elde ederiz:
vendor/bin/monorepo-builder packages-json
Ama sonra şunu düşündüm: tüm paketleri senkronize etmek gerekmez; yalnızca PR'da değiştirilen kodu içerenler senkronize edilmelidir.
Değiştirilen dosyaların listesini bulabilirsek, onları içeren değiştirilmiş paketleri hesaplayabiliriz. Başka bir deyişle: git diff çalıştırın ve sonuçları packages-json komutuna bir filter girdisi aracılığıyla şu şekilde verin:
vendor/bin/monorepo-builder packages-json --filter=modified_file_1 --filter=modified_file_2 --filter=...Şimdi, Monorepo Builder ile birlikte gelen packages-json komutu filter girdisini kabul etmiyor. İşte burada özel komutlarımızla genişletmemiz gerekiyor.
Monorepo Builder, Symfony'nin DependencyInjection'ını kullanır; bu nedenle konteynerine yeni servisler enjekte ederek genişletilebilir. Gerçekten de monorepo-builder.php yapılandırma dosyası zaten bir servis yapılandırıcısıdır.
Bu yüzden Monorepo Builder'ı filter girdisini destekleyen package-entries-json adında yeni bir komutla genişlettim:
final class PackageEntriesJsonCommand extends AbstractSymplifyCommand
{
private PackageEntriesJsonProvider $packageEntriesJsonProvider;
public function __construct(PackageEntriesJsonProvider $packageEntriesJsonProvider)
{
$this->packageEntriesJsonProvider = $packageEntriesJsonProvider;
parent::__construct();
}
protected function configure(): void
{
$this->setDescription('Provides package entries in json format. Useful for GitHub Actions Workflow');
$this->addOption(
Option::FILTER,
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Filter the packages to those from the list of files. Useful to split monorepo on modified packages only',
[]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string[] $fileFilter */
$fileFilter = $input->getOption(Option::FILTER);
$packageEntries = $this->packageEntriesJsonProvider->providePackageEntries($fileFilter);
// must be without spaces, otherwise it breaks GitHub Actions json
$json = Json::encode($packageEntries);
$this->symfonyStyle->writeln($json);
return ShellCode::SUCCESS;
}
}Servis konteynerine şu şekilde enjekte edilir:
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->defaults()->autowire()->autoconfigure();
$services->set(PackageEntriesJsonCommand::class);
}Artık package-entries-json adlı yeni komut, GitHub Action workflow'unda kullanılabilir olacak.
GitHub Action'da Değiştirilen Dosyaların Listesini Alma
Şimdi workflow'u nasıl güncelleyeceğimize bakalım.
PR'daki tüm değiştirilen dosyaların git diff'ini sağlayan technote-space/get-diff-action action'ını kullanıyorum:
# git diff to generate matrix with modified packages only
- uses: technote-space/get-diff-action@v4
with:
PATTERNS: layers/*/*/*/**Bu sonuçlardan (${{ env.GIT_DIFF }} altında saklanır) özel package-entries-json komutuna yapılan çağrıyı oluştururum ve bunu çıktı olarak ayarlarım:
- id: output_data
name: Calculate matrix for packages
run: |
quote=\'
clean_diff="$(echo "${{ env.GIT_DIFF }}" | sed -e s/$quote//g)"
packages_in_diff="$(echo $clean_diff | grep -E -o 'layers/[A-Za-z0-9_\-]*/[A-Za-z0-9_\-]*/[A-Za-z0-9_\-]*/' | sort -u)"
echo "[Packages in diff] $(echo $packages_in_diff | tr '\n' ' ')"
filter_arg="--filter=$(echo $packages_in_diff | sed -e 's/ / --filter=/g')"
echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json $(echo $filter_arg))"Elde edilen paketler daha sonra matrisi oluşturmak için kullanılır:
outputs:
matrix: ${{ steps.output_data.outputs.matrix }}Mükemmel çalışıyor! Bu örnekte yalnızca iki paket değiştirildi, dolayısıyla matriste yalnızca 2 örnek başlatıldı:

Artık bir PR'ı birleştirmek sadece birkaç dakika sürebilir (1 saatten inerek), bu yüzden yeniden mutlu bir geliştirici oldum.
Daha Fazla Optimizasyon/Zorluklar
GitHub Action'dan zaman kazanabileceğim bir başka durum daha var: PHPUnit testleri çalıştırılırken.
Şu anda, yeni bir kod parçası yüklendiğinde, tüm paketler için tüm test dizisi çalıştırılıyor. Ama yine, bu optimize edilebilir.
Diyelim ki monorepo 3 paket içeriyor: A, B ve C; B, A'ya bağlıdır ve C, B'ye bağlıdır.
O zaman tek bir paketten kod değiştirirsek, çalıştırılması gereken testler değişecektir:
- A'dan kod değiştirme: A, B ve C test edilmelidir
- B'den kod değiştirme: B ve C test edilmelidir
- C'den kod değiştirme: C test edilmelidir
Optimizasyon, değiştirilen paketlerin listesini (önceki optimizasyonda olduğu gibi) elde etmeye ve bunlar ile onlara bağlı tüm paketler için testleri çalıştırmaya bağlı olacaktır.
Ancak, şu anda monorepo'daki her paketin birbirine nasıl bağlı olduğu bilgisine sahip değilim.
Kök composer.json tüm yerel paketleri içerse de, composer info ${ package_name } çalıştırarak Composer aracılığıyla bağımlılıklarını alamıyorum; çünkü bunlar require yerine replace bölümünde tanımlanmıştır.
Alternatif olarak, her paketin alt klasörüne girebilir, composer install çalıştırıp ardından composer info yapabilirdim. Ama composer install'ı 200'den fazla kez çalıştırmak tamamen delilik olurdu.
Bu nedenle, bu senaryoyu henüz optimize etmedim. Şimdiye kadar sorunu oluşturdum ve eninde sonunda bir çözüm bulmayı umuyorum.
Sonuç
Monorepo Builder'ı keşfetmekten son derece mutlu olduğumu söylemeliyim. Gato GraphQL'in kod tabanını başka türlü yönetemeyeceğimi düşünüyorum.
Her projenin bunu kullanması gerektiğini söylemiyorum. Ama benim durumumda olduğu gibi 200'den fazla paketiniz varsa, ya da belki 20'den fazla, o zaman hayatınızı kesinlikle kolaylaştırıyor.
Monorepo'yu yönetmek kurulum ve bakım için biraz zaman ve çaba gerektiriyor; ancak o zaman ve çabayı her gün yalnızca devam eden geliştirme sayesinde defalarca geri kazanıyorum.