๐พ Gato GraphQL kini sudah di-scope, berkat PHP-Scoper!
Plugin Gato GraphQL kini sudah di-scope. Artinya plugin ini akhirnya bisa diunggah ke direktori plugin WordPress.

Untuk melakukannya, saya menggunakan PHP-Scoper yang luar biasa. Menggunakan library ini dengan WordPress tidak lepas dari tantangannya, jadi saya akan menjelaskan di artikel blog ini bagaimana saya berhasil menyelesaikannya.
Bagian-bagian:
- Mengambil keputusan untuk melakukan scope
- Melihat opsi yang tersedia
- Mencoba Mozart, dan gagal
- Memeriksa PHP-Scoper, dan keluar dalam kepanikan
- Kembali ke PHP-Scoper, kali ini untuk selamanya
- PHP-Scoper, cara mudah ๐ ๐๐ฝ Di sinilah solusi saya dimulai
- Tunjukkan hal yang sesungguhnya
- Pengujian
- Lihat hasilnya
Mengambil keputusan untuk melakukan scope
Beberapa minggu lalu, Matt Mullenweg mengumumkan bahwa dia akan memperhatikan "plugin GraphQL", jelas merujuk pada WPGraphQL. Ekspresinya menunjukkan bahwa dia percaya hanya ada satu plugin GraphQL, padahal sebenarnya ada dua (yang ditinggalkan adalah, yah, milik saya). Hal itu membuat saya menyadari betapa sedikitnya visibilitas plugin saya, dan saya merasa kecewa karenanya.
Matt tidak tahu bahwa plugin saya ada. Begitu pula sebagian besar komunitas WordPress, kalau mau jujur. Jelas saya tidak cukup mempromosikannya. Saya tahu saya lemah dalam pemasaran dan media sosial; saya hanya lumayan dalam hal teknis (atau begitulah yang saya percaya). Jadi saya memutuskan untuk melakukan sesuatu, setidaknya dalam kemampuan saya.
Inilah yang sedang saya kerjakan:
- Saya baru saja selesai membuat kode website ini, gatographql.com, dan meluncurkannya 2 minggu lalu (hore! ๐ฅณ Ngomong-ngomong, bagaimana menurutmu? Silakan memberi masukan, via DM atau email)
- 3 hari lalu, saya akhirnya mulai melakukan scoping pada plugin ini, dan menyelesaikan tugas ini kemarin! (Jam 3 pagi, tapi sepadan ๐ )
- Dan akhirnya, saya sudah mengerjakan versi mendatang
0.8, yang akan menjadi versi pertama yang tersedia di repositori plugin
Melakukan scoping pada plugin adalah wajib untuk mengunggahnya ke repositori, karena jika tidak, plugin bisa berkonflik dengan plugin lain yang membutuhkan dependensi yang sama dengan plugin saya, tetapi dengan versi yang berbeda. Berhasil melakukannya adalah tonggak yang sangat besar; tidak ada pengembangan lain yang sepenting ini. Misalnya, saya masih harus melengkapi schema GraphQL agar sepenuhnya cocok dengan model data WordPress, tetapi itu akan dilakukan secara bertahap di setiap rilis baru.
Jadi dalam beberapa minggu ke depan, plugin ini akan muncul saat mencari "GraphQL", dan orang-orang yang benar-benar perlu mengimplementasikan GraphQL API akan mengetahui keberadaan plugin saya.
Memang, saya ingin plugin saya dipertimbangkan secara serius untuk masa depan WordPress. Saya sudah mengerjakannya selama beberapa tahun. Repo ini dimulai pada Agustus 2016; itu bahkan sebelum WPGraphQL ada, dan di awal GraphQL. Tapi saya tidak tahu bahwa proyek ini akan menjadi server GraphQL; ia mengambil arah itu baru sekitar 1,5 tahun lalu.
(Proyek ini sebenarnya adalah framework untuk membangun aplikasi menggunakan komponen sisi server, dan server GraphQL bisa dibangun dengan sempurna menggunakan arsitektur ini. Jadi saya membangunnya saja).
WPGraphQL adalah plugin yang sudah mapan, dan memang demikian: plugin ini dimulai beberapa tahun lalu, dan komunitas dibangun di sekitarnya. Pekerjaan Jason Bahl (yang dipekerjakan oleh Gatsby) dan para kontributor proyeknya sangat luar biasa: mengintegrasikan WordPress ke dalam Jamstack kini lebih mudah dari sebelumnya.
Tapi satu hal adalah Gatsby dan Jamstack, dan hal lain adalah WordPress. WordPress menguasai 40% web, bukan sekadar input untuk generator situs statis.
Jadi sekarang, kita bisa mempertimbangkan apakah WPGraphQL adalah pilihan yang tepat, tanpa keputusan ini diambilkan untuk kita karena kurangnya alternatif. Kita kini bisa menganalisis kedua plugin untuk melihat tujuan mana yang lebih selaras dengan apa yang penting bagi WordPress.
Gato GraphQL juga bisa bekerja dengan Jamstack. Tapi tujuan utamanya, saya percaya, lebih luar biasa: Untuk "mendemokratisasi penerbitan data", sehingga mengedit API menjadi semudah mengedit postingan (sesuatu yang semua orang bisa lakukan), dan menjadikan WordPress sebagai OS web.
Setelah plugin tersedia di repositori, saya berharap lebih banyak orang akan mencobanya dan berkata "Hei, ini benar-benar keren! Bagaimana mungkin saya tidak tahu tentang ini sebelumnya?".
Dan kemudian, pilihan "plugin GraphQL" tidak lagi ditentukan sebelumnya, dan komunitas WordPress bisa mempertimbangkan baik WPGraphQL maupun Gato GraphQL berdasarkan manfaat masing-masing.
Sekarang setelah motivasi saya sudah dijelaskan, mari bicara hal teknis ๐ค.
Melihat opsi yang tersedia
Melakukan scoping pada sebuah plugin melibatkan menjalankan beberapa tooling, yang mengambil kode plugin sebagai input, dan menghasilkan plugin yang sudah di-scope. Tidak rumit, kan? Seberapa susah sih?

Nah, tergantung pada codebase-nya, hanya menjalankan perintah scope saja belum tentu cukup. Setelah itu, kita perlu memeriksa error di konsol, memperbaikinya, menguji aplikasi secara menyeluruh, mengidentifikasi error dan penyebabnya, memperbaikinya, dan mengulangi lagi. Untuk benar-benar menyelesaikannya dengan benar, mungkin membutuhkan waktu.
Ada 2 library untuk scoping, yang memiliki tujuan berbeda:
- Mozart, untuk kode WordPress
- PHP-Scoper, untuk kode PHP apa pun, terutama saat menghasilkan PHAR
Karena saya memiliki plugin WordPress, saya mencoba Mozart terlebih dahulu. Mari kita lihat hasilnya.
Mencoba Mozart, dan gagal
Saya mencoba Mozart sekitar 1 tahun lalu. Berdasarkan dokumentasinya, "perintah mozart compose melakukan semua keajaiban". Jadi saya berharap semuanya akan sangat cepat dan mudah, dan bisa menikmati daiquiri untuk sisa hari itu.
Sayangnya, Mozart tidak pernah berfungsi untuk codebase saya. Terus mengalami masalah, sehingga scoping tidak pernah terwujud. Dan saya tidak bisa mendapatkan bantuan yang diperlukan: saya mengajukan PR, tetapi itu tidak dipertimbangkan untuk di-merge, dan saya bahkan tidak diberitahu tentangnya, jadi saya terus menunggu sampai saya secara alami kehilangan minat pada proyek ini.
Saya percaya Mozart tidak bisa menangani beberapa dependensi dalam plugin saya. Saya menggunakan beberapa komponen Symfony, termasuk DependencyInjection, Cache dan Dotenv, dengan semuanya dikelola melalui Composer.
Melakukan scoping pada PHP bukan hanya tentang PHP, jadi scoper akan memiliki banyak hambatan untuk dihindari dan tantangan untuk diselesaikan. Misalnya, Symfony DependencyInjection menggunakan file YAML untuk menyiapkan konfigurasi, dan ini juga harus di-scope. Dan file composer.json berisi konfigurasi untuk autoloading PSR-4, dan ini juga harus di-scope. Dan saya percaya Mozart tidak dapat menangani kompleksitas ini dengan benar.
Tapi saya yakin pengalaman saya bukan satu-satunya, dan ada banyak pengguna yang senang di luar sana. Juga, percobaan gagal saya terjadi 1 tahun lalu, jadi saya bertanya-tanya apakah toolnya sudah diperbaiki sejak saat itu. Dan jangan lupa pepatah: "Semua plugin yang di-scope serupa; setiap plugin yang tidak di-scope tidak di-scope dengan caranya sendiri", jadi mungkin itu hanya gagal untuk saya.
Jika plugin WordPress kamu sederhana, dengan logika yang terkontainkan, dan scoping harus dilakukan hanya dalam kode PHP, maka kemungkinan besar Mozart akan berfungsi. Kamu hanya perlu mencobanya.
Memeriksa PHP-Scoper, dan keluar dalam kepanikan
Jadi saya menuju PHP-Scoper. Namun, saya bahkan tidak pernah mencobanya, karena saya langsung ketakutan.
Untuk memulai, tool ini tidak mendukung WordPress secara alami. Dan untuk melanjutkan, mereka merekomendasikan untuk melihat Makefile milik mereka sendiri, yang terlihat seperti ini:
# See https://tech.davis-hansson.com/p/make/
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.DEFAULT_GOAL := help
PHPBIN=php
PHPNOGC=php -d zend.enable_gc=0
IS_PHP8=$(shell php -r "echo version_compare(PHP_VERSION, '8.0.0', '>=') ? 'true' : 'false';")
SRC_FILES=$(shell find bin/ src/ -type f)
.PHONY: help
help:
@echo "\033[33mUsage:\033[0m\n make TARGET\n\n\033[32m#\n# Commands\n#---------------------------------------------------------------------------\033[0m\n"
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | awk 'BEGIN {FS = ":"}; {printf "\033[33m%s:\033[0m%s\n", $$1, $$2}'
#
# Build
#---------------------------------------------------------------------------
.PHONY: clean
clean: ## Clean all created artifacts
clean:
git clean --exclude=.idea/ -ffdx
update-root-version: ## Check the lastest GitHub release and update COMPOSER_ROOT_VERSION accordingly
update-root-version:
rm .composer-root-version || true
$(MAKE) .composer-root-versionDan 600 baris lagi, semuanya seperti ini. Terlihat seperti teka-teki. Karena saya pikir saya perlu memahami kode itu hanya untuk melakukan scoping pada plugin saya, saya langsung kabur tanpa basa-basi.
(Yah, memahami kode itu adalah rekomendasi mereka untuk menguji aplikasi yang sudah di-scope, tetapi tidak wajib. Kita juga bisa langsung menjalankan perintah php-scoper add-prefix, biarkan ia melakukan semua keajaiban, dan pergi minum daiquiri kita.)
Kembali ke PHP-Scoper, kali ini untuk selamanya
Jadi, 3 hari lalu, saya mengambil keputusan untuk mengimplementasikan scoping, bagaimanapun caranya. Saya harus membuatnya terjadi.
Saya kembali ke PHP-Scoper, untuk mencobanya dengan sungguh-sungguh. Saya tahu WordPress bisa di-scope dengannya dari membaca PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (oleh orang-orang brilian dari Delicious Brains). Ini hanya soal sikap, dan ketekunan.
Saya mengeksplorasi beberapa solusi yang ada, termasuk:
- Yang ini oleh Lucas Bustamante
- Yang ini oleh Yoast
- Yang ini oleh Google Site Kit
- Yang ini oleh Google Web Stories
Tapi semuanya terlihat tidak sepenuhnya memuaskan bagi saya: kodenya terlihat hacky, atau rapuh dan menunggu untuk rusak di suatu saat nanti.
Misalnya, plugin Google Web Stories melakukan scoping pada kode, dan kemudian mengembalikan setiap konflik:
return [
'patchers' => [
function ( $file_path, $prefix, $contents ) {
/*
* There is currently no easy way to simply whitelist all global WordPress functions.
*
* This list here is a manual attempt after scanning through the AMP plugin, which means
* it needs to be maintained and kept in sync with any changes to the dependency.
*
* As long as there's no built-in solution in PHP-Scoper for this, an alternative could be
* to generate a list based on php-stubs/wordpress-stubs. devowlio/wp-react-starter/ seems
* to be doing just this successfully.
*
* @see https://github.com/humbug/php-scoper/issues/303
* @see https://github.com/php-stubs/wordpress-stubs
* @see https://github.com/devowlio/wp-react-starter/
*/
$contents = str_replace( "\\$prefix\\_doing_it_wrong", '\\_doing_it_wrong', $contents );
$contents = str_replace( "\\$prefix\\__", '\\__', $contents );
$contents = str_replace( "\\$prefix\\esc_html_e", '\\esc_html_e', $contents );
$contents = str_replace( "\\$prefix\\esc_html", '\\esc_html', $contents );
$contents = str_replace( "\\$prefix\\esc_attr", '\\esc_attr', $contents );
$contents = str_replace( "\\$prefix\\esc_url", '\\esc_url', $contents );
$contents = str_replace( "\\$prefix\\do_action", '\\do_action', $contents );
// ...
}
]
]Saya mengerti mengapa mereka melakukannya, tapi saya tidak suka. Setiap kali fungsi WordPress baru dirujuk, mereka perlu memastikannya juga masuk ke daftar ini. Terlalu manual, terlalu rapuh.
Jadi inilah tantangan saya: Bukankah ada cara yang lebih sederhana untuk melakukan scoping pada plugin, dan mengandalkan kode yang bisa kita tunjukkan kepada teman dan kolega tanpa malu?
PHP-Scoper, cara mudah ๐
Ternyata lebih mudah dari yang saya kira! Hanya dalam beberapa jam, semuanya sudah berfungsi.

Sekarang, ketika saya bilang "mudah" dan "jam", yang saya maksud adalah: Semuanya langsung berfungsi, tetapi hanya setelah menghabiskan 2 bulan membuat struktur yang tepat untuk codebase (saya akan menjelaskan lebih baik nanti).
Tapi hal yang penting adalah: Jika kamu memiliki setup yang tepat untuk proyek, melakukan scoping bisa diselesaikan dalam waktu singkat.
Masalah dengan melakukan scoping pada kode WordPress adalah, yah, kode WordPress. Masalahnya dijelaskan di sini, tetapi intinya semua fungsi dan kelas WordPress juga di-namespace. Jadi jika kita merujuk WP_Query atau memanggil get_posts dalam kode kita, ini akan diubah menjadi MyPrefixedNamespace\WP_Query dan MyPrefixedNamespace\get_posts, menghasilkan kegagalan epik saat runtime. Dan itu tidak bisa dihindari di PHP-Scoper tanpa hack.
Jadi, apa solusinya? Mudah sekali: jangan merujuk WP_Query, atau memanggil get_posts, atau menggunakan kode WordPress apa pun dalam codebase yang akan di-scope.

Tidak, saya tidak gila, dan saya yakin kamu juga tidak. Dan ya, saya tahu kita sedang membangun plugin WordPress... Biarkan saya menjelaskan.
Bagaimana kita tidak menyertakan kode WordPress? Dengan membagi codebase menjadi 2 set paket:
- Yang berisi kode WordPress, tanpa merujuk kode dari library eksternal apa pun
- Yang berisi logika bisnis, tanpa berisi kode WordPress apa pun, dan menyertakan semua dependensi yang diperlukan dan referensi ke kodenya
Dengan cara ini, alih-alih memiliki satu codebase, kita memiliki beberapa codebase (atau paket), di mana sebagian akan di-scope dan sebagian tidak, dan semuanya membentuk plugin, diikat bersama via Composer.
Kemudian, kita tidak melakukan scoping pada paket yang berisi kode WordPress, menghindari konflik. Ini berfungsi karena tidak merujuk kode apa pun yang termasuk dalam dependensi eksternal. Semua referensi bersifat internal, seperti MyNamespace\MyPlugin\MyClass. Tapi ini tidak perlu di-scope, karena kita dapat dengan aman mengasumsikan bahwa hanya akan ada 1 versi plugin yang diinstal di situs WordPress, dan kita bisa memasukkan namespace kita MyNamespace\* ke whitelist.
Selain itu, jika plugin kita dapat diperluas, maka memasukkan namespace kita sendiri ke whitelist adalah wajib. Misalnya, field resolver untuk Gato GraphQL diimplementasikan dengan memperluas kelas PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Jika saya melakukan scoping, pengembang akan dipaksa untuk merujuk PoP\ComponentModel\FieldResolvers\AbstractFieldResolver untuk development, dan PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver untuk production. Itu tidak bisa diterima.
Kemudian, kita hanya melakukan scoping pada paket-paket logika bisnis, yang berisi referensi ke semua library eksternal tetapi tidak ada kode WordPress.
Singkatnya, kita beralih dari strategi ini:
"Miliki satu codebase, lakukan scoping, dan kemudian dengan susah payah dan dengan banyak kesabaran batalkan kerusakannya, sambil berdoa agar tidak ada konflik yang luput dan ๐ฃ meledak di production"
Ke strategi ini:
"Pisahkan codebase menjadi 2 kelompok, lakukan scoping hanya pada yang berisi referensi ke dependensi eksternal dan tidak ada kode WordPress, dan pergi minum daiquiri yang sudah kamu layak dapatkan ๐น".
Tunjukkan hal yang sesungguhnya
Saatnya membuka sosis dan melihat apakah ada daging sungguhan di dalamnya ๐ญ.
4 hari lalu, saya memiliki kode berikut di plugin saya:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use Parsedown;
class MarkdownContentParser
{
protected function getHTMLContent(string $fileContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Kelas Parsedown berasal dari dependensi eksternal erusev/parsedown, sebagaimana didefinisikan dalam composer.json plugin:
{
"require": {
"erusev/parsedown": "^1.7"
}
}Oleh karena itu, plugin saya berisi referensi ke library eksternal, jadi saya perlu melakukan scoping, untuk mengubah Parsedown menjadi PrefixedByPoP\Parsedown. Tapi melakukannya juga akan melakukan scoping pada semua kode WordPress di plugin, menyebabkan konflik.
Jadi saya mengekstrak kode ke paket terpisah, bernama graphql-api/markdown-convertor, dan mengganti dependensi pihak ketiga di composer.json dengan dependensi saya sendiri:
{
"require": {
"graphql-api/markdown-convertor": "^0.8"
}
}Sekarang, plugin menghindari referensi langsung ke library eksternal; sebaliknya, ia merujuk layanan MarkdownConvertorInterface dari paket baru:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use GraphQLAPI\MarkdownConvertor\MarkdownConvertorInterface;
class MarkdownContentParser extends AbstractContentParser
{
protected MarkdownConvertorInterface $markdownConvertorInterface;
function __construct(MarkdownConvertorInterface $markdownConvertorInterface)
{
$this->markdownConvertorInterface = $markdownConvertorInterface;
}
protected function getHTMLContent(string $fileContent): string
{
return $this->markdownConvertorInterface->convertMarkdownToHTML($fileContent);
}
}Referensi ke dependensi pihak ketiga dilakukan di paket baru:
namespace GraphQLAPI\MarkdownConvertor;
use Parsedown;
class MarkdownConvertor implements MarkdownConvertorInterface
{
public function convertMarkdownToHTML(string $markdownContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Akhirnya, kita harus:
- Melakukan scoping pada dependensi
graphql-api/markdown-convertor - Melewati scoping pada kode plugin
- Memasukkan namespace
GraphQLAPI\*ke whitelist, agar kelas-kelas saya sendiri tidak di-scope
Inilah kurang lebih strateginya. Dari sini, ini akan menjadi pengulangan dari ide yang sama, untuk menghapus semua dependensi eksternal dari kode, hingga voilร , plugin dapat di-scope.
Dependensi yang perlu diekstrak hanyalah yang ada di bagian require pada file composer.json kamu; untuk require-dev kamu bisa menyimpan dependensi apa pun, eksternal atau tidak, karena kita tidak perlu melakukan scoping pada dependensi yang digunakan untuk development; hanya yang digunakan untuk membuat dan mengirimkan plugin, untuk production, yang perlu di-scope.
Pada akhirnya, composer.json dari plugin kamu tidak boleh berisi dependensi eksternal apa pun. Untuk plugin saya, tampilannya seperti ini:
{
"require": {
"php": "^7.4|^8.0",
"getpop/engine-wp": "^0.8",
"graphql-api/markdown-convertor": "^0.8",
"graphql-by-pop/graphql-clients-for-wp": "^0.8",
"graphql-by-pop/graphql-endpoint-for-wp": "^0.8",
"graphql-by-pop/graphql-server": "^0.8",
"pop-schema/basic-directives": "^0.8",
"pop-schema/comment-mutations-wp": "^0.8",
"pop-schema/commentmeta-wp": "^0.8",
"pop-schema/comments-wp": "^0.8",
"pop-schema/custompost-mutations-wp": "^0.8",
"pop-schema/custompostmedia-mutations-wp": "^0.8",
"pop-schema/custompostmedia-wp": "^0.8",
"pop-schema/custompostmeta-wp": "^0.8",
"pop-schema/generic-customposts": "^0.8",
"pop-schema/media-wp": "^0.8",
"pop-schema/pages-wp": "^0.8",
"pop-schema/post-mutations": "^0.8",
"pop-schema/post-tags-wp": "^0.8",
"pop-schema/posts-wp": "^0.8",
"pop-schema/taxonomymeta-wp": "^0.8",
"pop-schema/taxonomyquery-wp": "^0.8",
"pop-schema/user-roles-access-control": "^0.8",
"pop-schema/user-roles-wp": "^0.8",
"pop-schema/user-state-mutations-wp": "^0.8",
"pop-schema/user-state-wp": "^0.8",
"pop-schema/usermeta-wp": "^0.8",
"pop-schema/users-wp": "^0.8"
}
}Semua paket itu, dengan namespace getpop, graphql-api, graphql-by-pop, dan pop-schema, semuanya milik saya: dependensi yang berisi seluruh kode untuk plugin. Mereka didistribusikan ke dalam namespace yang berbeda untuk mengelola kode dengan lebih baik, tapi kamu tidak perlu melakukannya: menggunakan satu namespace saja sudah berfungsi dengan baik.
Sekarang, seiring bertambahnya jumlah paket dalam aplikasi kamu, kamu perlu menghosting semuanya di monorepo, atau kamu akan menjadi gila membuat pull request yang melibatkan lebih dari satu paket (percayalah, saya pernah mengalaminya). Dalam kasus saya, semua paket saya dihosting di monorepo GatoGraphQL/GatoGraphQL, dan saya menjaga sinkronisasinya melalui Monorepo Builder yang luar biasa (Saya perlu menulis artikel tentang tool ini, ini benar-benar penyelamat!).
Namespace untuk paket-paket ini adalah PoP, GraphQLAPI, GraphQLByPoP dan PoPSchema. Karena semuanya milik saya, saya tahu mereka hanya akan muncul sekali dalam aplikasi, sehingga saya bisa menghindari scoping pada mereka.
Untuk melakukan itu, saya memasukkannya ke whitelist di scoper.inc.php:
return [
'whitelist' => [
// Own namespaces
'PoPSchema\*',
'PoP\*',
'GraphQLByPoP\*',
'GraphQLAPI\*',
// Own container cache
'PoPContainer\*',
],
];Entri terakhir sesuai dengan container dependency injection, yang juga perlu di-scope. Secara default, container ini diberi nama ProjectServiceContainer, langsung di namespace global. Tapi PHP-Scoper tidak mendukung penambahan whitelist untuk kelas tertentu dari namespace global. Oleh karena itu, saya menambahkan namespace buatan PoPContainer ke whitelist, dan menetapkan namespace ini saat membuang container ke disk:
$dumper = new PhpDumper($containerBuilder);
file_put_contents(
self::$cacheFile,
$dumper->dump(
// Save under own namespace to avoid conflicts
array('namespace' => 'PoPContainer')
)
);Kamu mungkin memperhatikan bahwa, mengenai paket-paket, beberapa di antaranya berakhiran -wp (seperti pop-schema/users-wp) sementara yang lain tidak (seperti graphql-by-pop/graphql-server). Ya, kamu menebak dengan benar: yang pertama berisi kode WordPress dan tidak ada referensi ke library eksternal, dan yang terakhir bisa berisi referensi ke library eksternal, tetapi tidak ada kode WordPress sama sekali.
Kemudian, saya melewati scoping pada paket-paket WordPress:
return [
'finders' => [
// Scope packages under vendor/, excluding local WordPress packages
Finder::create()
->files()
->notPath([
// Exclude libraries ending in "-wp"
'#getpop/[a-zA-Z0-9_-]*-wp/#',
'#pop-schema/[a-zA-Z0-9_-]*-wp/#',
'#graphql-by-pop/[a-zA-Z0-9_-]*-wp/#',
])
->in('vendor')
]
];Apa yang terjadi jika beberapa paket WordPress perlu merujuk library eksternal, dan ini tidak bisa diekstrak ke paket lain? Misalnya, paket saya getpop/routing-wp bergantung pada brain/cortex, dan ini tidak bisa dihindari.
Saya tidak bisa melakukan scoping pada seluruh paket, karena getpop/routing-wp berisi kode WordPress. Sebaliknya, yang saya lakukan adalah mengidentifikasi file-file di mana referensi itu dibuat, dan memastikan file-file itu tidak berisi kode WordPress. Kemudian saya bisa melakukan scoping hanya pada file-file tersebut.
Dalam hal ini, referensi ke Cortex/Brain dilakukan dalam 2 file, termasuk layers/Engine/packages/routing-wp/src/Hooks/SetupCortexHookSet.php:
namespace PoP\RoutingWP\Hooks;
use PoP\Hooks\AbstractHookSet;
use Brain\Cortex\Route\RouteCollectionInterface;
use Brain\Cortex\Route\RouteInterface;
use Brain\Cortex\Route\QueryRoute;
use PoP\RoutingWP\WPQueries;
use PoP\Routing\Facades\RoutingManagerFacade;
class SetupCortexHookSet extends AbstractHookSet
{
protected function init()
{
$this->hooksAPI->addAction(
'cortex.routes',
[$this, 'setupCortex'],
1
);
}
/**
* @param RouteCollectionInterface<RouteInterface> $routes
*/
public function setupCortex(RouteCollectionInterface $routes): void
{
$routingManager = RoutingManagerFacade::getInstance();
foreach ($routingManager->getRoutes() as $route) {
$routes->addRoute(new QueryRoute(
$route,
function (array $matches) {
return WPQueries::STANDARD_NATURE;
}
));
}
}
}Apakah kamu memperhatikan keanehan di sini? Ini adalah implementasi hook, tapi tidak ada pemanggilan add_action, karena saya tidak bisa memiliki kode WordPress di sini. Sebaliknya, ia memanggil fungsi addAction dari layanan HooksAPIInterface, dan layanan ini diimplementasikan oleh kelas HooksAPI di paket getpop/hooks-wp, di mana kita bisa memiliki kode WordPress:
namespace PoP\HooksWP;
use PoP\Hooks\HooksAPIInterface;
class HooksAPI implements HooksAPIInterface
{
public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
{
add_action($tag, $function_to_add, $priority, $accepted_args);
}
}Sekarang kodenya sudah dipisahkan dengan bersih, kita bisa melakukan scoping pada 2 file yang merujuk dependensi eksternal tersebut:
return [
'finders' => [
Finder::create()->append([
'vendor/getpop/routing-wp/src/Component.php',
'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
])
]
];Sebelumnya saya menyebutkan bahwa menyiapkan scoping membutuhkan beberapa jam, tetapi hanya setelah 2 bulan kerja. Nah, contoh ini menunjukkan apa yang saya maksud: Pekerjaan nyata terletak pada pembagian codebase secara bersih menjadi 2 set.
Dalam kasus saya, pekerjaan membutuhkan 2 bulan karena tingkat detailnya sangat ekstrem: Plugin menjadi komposisi dari 125 paket! Tapi ini adalah kasus luar biasa, dengan tujuan agar server dasar plugin tidak bergantung pada CMS mana pun, untuk mendukung implementasi pada CMS/framework lain hanya dengan mengimplementasikan ulang paket-paket -wp yang sesuai.
(Saya menulis secara rinci tentang strategi ini, dalam artikel Abstracting WordPress Code To Reuse With Other CMSs: Concepts dan Implementation.)
Ini memang cukup banyak pekerjaan, tapi kebersihan kode yang lebih baik membuatnya sepadan. Dan bukan hanya untuk melakukan scoping pada plugin, yang datang sebagai kejutan total bagi saya, dan saya masih merasa geli dengan kebahagiaan tak terduga ini. Misalnya, saya menjalankan PHPStan dan PHPUnit secara terpisah pada kode WordPress dan non-WordPress, menghindarkan saya dari banyak sakit kepala.
Setelah codebase sudah dirapikan, dunia tiba-tiba menjadi tempat yang jauh lebih baik.
Pengujian
Jadi, bagaimana kita menguji makhluk ini?
Solusi yang saya temukan adalah mengandalkan Rector, tool yang sama yang saya gunakan untuk melakukan downgrade kode dari PHP 7.4, untuk development, ke 7.1, untuk production.
Idenya adalah sebagai berikut:
- Lakukan scoping pada plugin
- Analisis dengan Rector, terapkan aturan apa pun (tidak masalah yang mana)
Jika ada yang salah saat scoping, maka Rector tidak akan dapat memuat beberapa kelas, dan akan melempar error. Misalnya, jika kelas Brain\Cortex di-scope menjadi PrefixedByPoP\Brain\Cortex, tetapi beberapa referensi padanya dibiarkan sebagai Brain\Cortex, maka autoloading kelas ini akan gagal.
Ini adalah GitHub Action untuk pengujian saya (working-directory digunakan, karena saya beroperasi dari root monorepo, tetapi scoping terjadi di folder plugin):
name: Scope Gato GraphQL tests
on:
push:
branches:
- master
pull_request: null
env:
COMPOSER_ROOT_VERSION: "dev-master"
jobs:
main:
defaults:
run:
working-directory: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp
name: Scope the plugin code via PHP-Scoper, and execute tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set-up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install root dependencies
uses: "ramsey/composer-install@v1"
- name: Install plugin dependencies for PROD
run: composer install --no-dev --no-progress --no-interaction --ansi
- name: Install PHP-Scoper
run: |
composer global config minimum-stability dev
composer global config prefer-stable true
composer global require humbug/php-scoper
# The scoped results correspond to vendor/, so must generate them in such folder
- name: Scope plugin into separate folder
run: php-scoper add-prefix --output-dir ../../../../build-prefixed/vendor --ansi
- name: Copy scoped code back into plugin
run: rsync -av build-prefixed/ layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/ --quiet
working-directory: .
- name: Regenerate autoloader
run: composer dumpautoload --optimize --classmap-authoritative --ansi
- name: Run Rector on the scoped code
run: vendor/bin/rector process --config=layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/rector-test-scoping.php --ansi
working-directory: .
Dan ini adalah konfigurasi Rector saya:
use Rector\CodeQuality\Rector\LogicalAnd\AndAssignsToSeparateLinesRector;
use Rector\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(AndAssignsToSeparateLinesRector::class);
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
$parameters->set(Option::AUTOLOAD_PATHS, [
__DIR__ . '/vendor/scoper-autoload.php',
__DIR__ . '/vendor/erusev/parsedown/Parsedown.php',
__DIR__ . '/vendor/jrfnl/php-cast-to-type/cast-to-type.php',
__DIR__ . '/vendor/jrfnl/php-cast-to-type/class.cast-to-type.php',
]);
// files to rector
$parameters->set(Option::PATHS, [
__DIR__ . '/vendor',
]);
// files to skip
$parameters->set(Option::SKIP, [
// Exclude tests
'*/tests/*',
__DIR__ . '/vendor/nikic/fast-route/test/*',
__DIR__ . '/vendor/psr/log/Psr/Log/Test/*',
__DIR__ . '/vendor/symfony/service-contracts/Test/*',
]);
};Kamu mungkin memperhatikan bahwa beberapa file dependensi, seperti erusev/parsedown/Parsedown.php' perlu ditambahkan ke Option::AUTOLOAD_PATHS. Itu karena melakukan scoping pada composer.json paket tidak 100% andal, dan kemudian autoloading-nya mungkin gagal.
Setiap kali itu terjadi, Rector akan mengeluh bahwa beberapa kelas gagal di-autoload. Dari sana, kita mengidentifikasi file yang sesuai, dan menambahkannya secara manual ke jalur autoloading.
Lihat hasilnya
Ini adalah kode sumber plugin, dan ini adalah versinya yang sudah di-scope (dan di-downgrade ke PHP 7.1).
Temukan 7 perbedaannya ๐. (Saya beri petunjuk: cari PrefixedByPoP.)
Dan ini adalah file plugin final graphql-api.zip, siap diinstal di situs kamu.
Itu saja. Saya harap ini bermanfaat ๐๐ช๐