🍾 Gato GraphQL đã được scoped, nhờ PHP-Scoper!
Plugin Gato GraphQL đã được scoped. Điều này có nghĩa là plugin cuối cùng cũng có thể được tải lên thư mục plugin WordPress.

Để làm được điều đó, tôi đang sử dụng thư viện tuyệt vời PHP-Scoper. Sử dụng thư viện này với WordPress không phải là không có thách thức, vì vậy tôi sẽ giải thích trong bài viết này cách tôi đã xoay sở để thực hiện được.
Các phần:
- Đưa ra quyết định scope
- Xem xét các lựa chọn
- Thử Mozart và thất bại
- Khám phá PHP-Scoper và hoảng loạn
- Quay lại PHP-Scoper, lần này là thật
- PHP-Scoper, cách dễ dàng 😎 👈🏽 Giải pháp của tôi bắt đầu từ đây
- Cho tôi xem thứ thực sự
- Kiểm thử
- Xem kết quả
Đưa ra quyết định scope
Vài tuần trước, Matt Mullenweg thông báo rằng ông sẽ để mắt đến "plugin GraphQL", rõ ràng là ám chỉ đến WPGraphQL. Cách diễn đạt của ông cho thấy ông tin rằng chỉ có một plugin GraphQL, trong khi thực tế có đến hai (cái bị bỏ sót là, thực ra, của tôi). Điều đó khiến tôi nhận ra plugin của mình có ít khả năng hiển thị đến mức nào, và tôi cảm thấy tệ về điều đó.
Matt không biết plugin của tôi tồn tại. Cộng đồng WordPress cũng vậy, thực ra. Rõ ràng là tôi chưa quảng bá nó đủ tốt. Tôi biết mình kém về marketing và mạng xã hội; tôi chỉ ổn với những thứ kỹ thuật (hoặc ít nhất là tôi tin vậy). Vì vậy tôi quyết định làm điều gì đó về vấn đề này, ít nhất là trong khả năng của mình.
Đây là những gì tôi đang làm:
- Tôi vừa hoàn thành việc lập trình trang web này, gatographql.com, và ra mắt nó 2 tuần trước (hoan hô! 🥳 Nhân tiện, bạn thấy thế nào? Hoan nghênh bạn góp ý, qua DM hoặc email)
- 3 ngày trước, tôi cuối cùng đã bắt đầu scope plugin và hoàn thành công việc này hôm qua! (Lúc 3 giờ sáng, nhưng đáng lắm 😅)
- Và cuối cùng, tôi đang làm việc trên phiên bản sắp tới
0.8, đây sẽ là phiên bản đầu tiên có trong kho plugin
Việc scope plugin là bắt buộc để tải lên kho, vì nếu không nó có thể xung đột với một plugin khác yêu cầu cùng một dependency với plugin của tôi nhưng với phiên bản khác. Hoàn thành việc này là một cột mốc thực sự lớn; không có sự phát triển nào quan trọng hơn. Ví dụ, tôi vẫn phải hoàn thành GraphQL schema để khớp hoàn toàn với mô hình dữ liệu WordPress, nhưng điều đó sẽ được thực hiện dần dần trong mỗi bản phát hành mới.
Vì vậy, trong vài tuần nữa, plugin sẽ xuất hiện khi tìm kiếm "GraphQL", và những người thực sự cần triển khai một GraphQL API sẽ biết đến sự tồn tại của plugin của tôi.
Thực sự, tôi muốn plugin của mình được xem xét nghiêm túc cho tương lai của WordPress. Tôi đã làm việc trên đó nhiều năm rồi. Repo được bắt đầu vào tháng 8 năm 2016; đó là thậm chí trước khi WPGraphQL tồn tại, và vào buổi đầu của GraphQL. Nhưng tôi không biết rằng dự án sẽ trở thành một GraphQL server; nó chỉ đi theo hướng đó khoảng 1,5 năm trước.
(Dự án thực sự là một framework để xây dựng ứng dụng sử dụng các server-side component, và một GraphQL server có thể được xây dựng hoàn toàn bằng kiến trúc này. Vì vậy tôi chỉ xây dựng nó thôi).
WPGraphQL là một plugin đã được khẳng định, và điều đó hoàn toàn xứng đáng: nó được bắt đầu vài năm trước, và một cộng đồng đã được xây dựng xung quanh nó. Công việc của Jason Bahl (người được Gatsby thuê) và những người đóng góp cho dự án của ông thật xuất sắc: tích hợp WordPress vào Jamstack giờ đây dễ dàng hơn bao giờ hết.
Nhưng Gatsby và Jamstack là một chuyện, còn WordPress lại là chuyện khác. WordPress chiếm 40% web, không chỉ là đầu vào cho một trình tạo trang web tĩnh.
Vì vậy, giờ đây chúng ta có thể xem xét liệu WPGraphQL có phải là lựa chọn đúng đắn hay không, mà không cần phải để quyết định đó được đưa ra thay chúng ta do thiếu lựa chọn. Chúng ta có thể phân tích cả hai plugin để xem mục tiêu của ai phù hợp hơn với những gì quan trọng cho WordPress.
Gato GraphQL cũng có thể hoạt động với Jamstack. Nhưng các mục tiêu chính của nó, tôi tin, còn hoành tráng hơn: "dân chủ hóa việc xuất bản dữ liệu", để chỉnh sửa một API trở nên dễ dàng như chỉnh sửa một bài đăng (điều mà bất kỳ ai cũng có thể làm), và biến WordPress trở thành hệ điều hành của web.
Khi plugin có sẵn trên kho, tôi hy vọng nhiều người sẽ thử nó và nói "Ê, thứ này thật tuyệt vời! Sao tôi lại không biết về chuyện này trước đây?".
Và sau đó, lựa chọn "plugin GraphQL" không còn được xác định trước nữa, và cộng đồng WordPress có thể xem xét cả WPGraphQL lẫn Gato GraphQL dựa trên giá trị riêng của chúng.
Bây giờ động lực của tôi đã nói xong, hãy nói về kỹ thuật 🤓.
Xem xét các lựa chọn
Việc scope một plugin liên quan đến việc chạy một số công cụ, lấy code của plugin làm đầu vào và xuất ra plugin đã được scope. Không có gì ghê gớm cả, phải không? Khó đến mức nào cơ chứ?

À, tùy thuộc vào codebase, chỉ thực thi lệnh scope thôi sẽ không đủ. Sau đó, chúng ta cần kiểm tra lỗi trong console, sửa chúng, kiểm thử ứng dụng kỹ lưỡng, xác định lỗi và lý do chúng xảy ra, sửa chúng, và lặp lại. Để làm đúng hoàn toàn, có thể cần một khoảng thời gian.
Có 2 thư viện để scope, với các mục tiêu khác nhau:
- Mozart, cho code WordPress
- PHP-Scoper, cho bất kỳ code PHP nào, đặc biệt khi tạo PHAR
Vì tôi có một plugin WordPress, tôi đã thử Mozart trước. Hãy xem nó diễn ra thế nào.
Thử Mozart và thất bại
Tôi đã thử Mozart khoảng 1 năm trước. Theo những gì tài liệu nói, "lệnh mozart compose làm tất cả phép màu". Vì vậy tôi kỳ vọng mọi thứ sẽ rất nhanh và đơn giản, và đi thưởng thức một ly daiquiri phần còn lại của ngày.
Tiếc thay, Mozart chưa bao giờ hoạt động với codebase của tôi. Nó tiếp tục gặp phải các vấn đề, vì vậy việc scope không bao giờ thành hiện thực. Và tôi không thể nhận được sự hỗ trợ cần thiết: tôi đã gửi một PR, nhưng nó không được xem xét để merge, và tôi thậm chí không được thông báo về điều đó, vì vậy tôi cứ chờ đợi cho đến khi tự nhiên mất hứng thú với dự án này.
Tôi tin rằng Mozart không thể xử lý một số dependency trong plugin của tôi. Tôi đang sử dụng nhiều component của Symfony, bao gồm DependencyInjection, Cache và Dotenv, với tất cả được quản lý thông qua Composer.
Việc scope PHP không chỉ là về PHP, vì vậy trình scope sẽ có nhiều rào cản phải tránh và thách thức phải giải quyết. Ví dụ, Symfony DependencyInjection sử dụng file YAML để thiết lập cấu hình, và những file này cũng phải được scope. Và file composer.json chứa cấu hình cho autoloading PSR-4, và đây cũng phải được scope. Và, tôi tin, Mozart không thể xử lý những phức tạp này đúng cách.
Nhưng tôi chắc chắn rằng kinh nghiệm của tôi không phải là duy nhất, và có nhiều người dùng hài lòng ngoài kia. Ngoài ra, lần thất bại của tôi xảy ra 1 năm trước, vì vậy tôi tự hỏi liệu công cụ có được cải thiện kể từ đó không. Và cũng đừng quên câu nói: "Tất cả các plugin đã được scope đều giống nhau; mỗi plugin chưa được scope lại chưa được scope theo cách riêng của nó", vì vậy có thể nó chỉ thất bại với tôi mà thôi.
Nếu plugin WordPress của bạn đơn giản, với logic tự chứa, và việc scope chỉ cần thực hiện trong code PHP, thì có khả năng Mozart sẽ hoạt động. Bạn chỉ cần thử và xem.
Khám phá PHP-Scoper và hoảng loạn
Vì vậy tôi chuyển sang PHP-Scoper. Tuy nhiên, tôi thậm chí chưa thử dùng nó, vì tôi bị sợ ngay lập tức.
Để bắt đầu, công cụ này không hỗ trợ WordPress một cách tự nhiên. Và để tiếp tục, họ khuyến nghị xem Makefile của chính họ, trông như thế này:
# 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-versionVà 600 dòng nữa, tất cả đều như thế này. Trông như một câu đố. Nghĩ rằng tôi cần hiểu code đó chỉ để scope plugin của mình đã khiến tôi bỏ chạy không kèn không trống.
(Thật ra, hiểu code đó là khuyến nghị của họ để kiểm thử ứng dụng đã scope, nhưng nó không bắt buộc. Chúng ta cũng có thể chỉ chạy lệnh php-scoper add-prefix, để nó làm tất cả phép màu, và đi uống daiquiri của mình.)
Quay lại PHP-Scoper, lần này là thật
Vì vậy, 3 ngày trước, tôi đã quyết định thực hiện việc scope, bằng cách nào đó. Tôi phải làm cho nó xảy ra.
Tôi quay lại PHP-Scoper, để thử nghiêm túc. Tôi biết rằng WordPress có thể được scope với nó khi đọc PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (bởi những người xuất sắc từ Delicious Brains). Chỉ là vấn đề thái độ và kiên trì.
Tôi đã khám phá một số giải pháp hiện có, bao gồm:
- Cái này bởi Lucas Bustamante
- Cái này bởi Yoast
- Cái này bởi Google Site Kit
- Cái này bởi Google Web Stories
Nhưng tất cả chúng đều không hoàn toàn thỏa mãn tôi: hoặc code trông hacky, hoặc dễ vỡ và đang chờ bị hỏng vào một lúc nào đó.
Ví dụ, plugin Google Web Stories scope code, và sau đó hoàn tác từng xung đột:
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 );
// ...
}
]
]Tôi hiểu tại sao họ làm vậy, nhưng tôi không thích. Bất cứ khi nào một hàm WordPress mới được tham chiếu, họ cần đảm bảo nó cũng được thêm vào danh sách này. Quá thủ công, quá dễ vỡ.
Vì vậy đây là thách thức của tôi: Chẳng phải có cách đơn giản hơn để scope một plugin, dựa trên code mà chúng ta có thể trình bày với bạn bè và đồng nghiệp mà không đỏ mặt?
PHP-Scoper, cách dễ dàng 😎
Hóa ra nó dễ hơn tôi nghĩ! Chỉ trong vài giờ, tôi đã làm cho tất cả hoạt động.

Bây giờ, khi tôi nói "dễ dàng" và "giờ", tôi thực sự có nghĩa là: Mọi thứ hoạt động ngay lập tức, nhưng chỉ sau khi dành 2 tháng tạo cấu trúc phù hợp cho codebase (Tôi sẽ giải thích rõ hơn sau).
Nhưng điều quan trọng là: Nếu bạn có thiết lập đúng đắn cho dự án, việc scope nó có thể được hoàn thành trong thời gian ngắn.
Vấn đề với việc scope code WordPress là, thực ra, code WordPress. Vấn đề được giải thích ở đây, nhưng tóm lại là tất cả các hàm và class WordPress cũng bị đặt vào namespace. Vì vậy nếu chúng ta tham chiếu WP_Query hoặc gọi get_posts trong code của mình, những thứ này sẽ được chuyển đổi thành MyPrefixedNamespace\WP_Query và MyPrefixedNamespace\get_posts, tạo ra lỗi nghiêm trọng lúc chạy. Và điều đó không thể tránh khỏi trong PHP-Scoper mà không có hack.
Vậy, giải pháp cho điều này là gì? Đơn giản lắm: đừng tham chiếu WP_Query, hay gọi get_posts, hay sử dụng bất kỳ code WordPress nào trong codebase sẽ được scope.

Không, tôi không điên, và tôi chắc bạn cũng vậy. Và vâng, tôi biết chúng ta đang xây dựng một plugin WordPress... Hãy để tôi giải thích.
Làm thế nào chúng ta có thể không bao gồm code WordPress? Bằng cách chia codebase thành 2 nhóm package:
- Những cái chứa code WordPress, không tham chiếu code từ bất kỳ thư viện bên ngoài nào
- Những cái chứa logic nghiệp vụ, không chứa bất kỳ code WordPress nào, và bao gồm tất cả các dependency cần thiết và tham chiếu đến code của chúng
Bằng cách này, thay vì có một codebase duy nhất, chúng ta có nhiều codebase (hoặc package), trong đó một số sẽ được scope và một số không, và tất cả chúng tạo thành plugin, được gắn kết với nhau thông qua Composer.
Sau đó, chúng ta không scope package chứa code WordPress, tránh xung đột. Điều này hoạt động vì nó không tham chiếu bất kỳ code nào thuộc về dependency bên ngoài nào. Tất cả các tham chiếu đều là nội bộ, chẳng hạn như MyNamespace\MyPlugin\MyClass. Nhưng những cái này không cần được scope, vì chúng ta có thể giả định an toàn rằng sẽ chỉ có 1 phiên bản plugin được cài đặt trong trang WordPress, và chúng ta có thể đưa namespace MyNamespace\* của mình vào whitelist.
Hơn nữa, nếu plugin của chúng ta có thể được mở rộng, thì việc đưa namespace của chính mình vào whitelist là bắt buộc. Ví dụ, một field resolver cho Gato GraphQL được triển khai bằng cách kế thừa từ class PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Nếu tôi scope nó, các nhà phát triển sẽ bị buộc phải tham chiếu PoP\ComponentModel\FieldResolvers\AbstractFieldResolver cho quá trình phát triển, và PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver cho production. Điều đó không thể chấp nhận được.
Sau đó, chúng ta chỉ scope các package logic nghiệp vụ, chứa tham chiếu đến tất cả các thư viện bên ngoài nhưng không có code WordPress.
Tóm lại, chúng ta đang chuyển từ chiến lược này:
"Có một codebase duy nhất, scope nó, và sau đó một cách đau đớn và với nhiều kiên nhẫn hoàn tác thiệt hại, trong khi cầu nguyện rằng không có xung đột nào bị bỏ sót và 💣 nổ trong production"
Sang chiến lược này:
"Chia codebase thành 2 nhóm, chỉ scope cái chứa tham chiếu đến các dependency bên ngoài và không có code WordPress, và đi thưởng thức daiquiri xứng đáng của bạn 🍹".
Cho tôi xem thứ thực sự
Đã đến lúc mổ xẻ và xem bên trong có gì thật sự không 🌭.
4 ngày trước, tôi đã có đoạn code sau trong plugin của mình:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use Parsedown;
class MarkdownContentParser
{
protected function getHTMLContent(string $fileContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Class Parsedown đến từ dependency bên ngoài erusev/parsedown, như được định nghĩa trong composer.json của plugin:
{
"require": {
"erusev/parsedown": "^1.7"
}
}Do đó, plugin của tôi chứa tham chiếu đến một thư viện bên ngoài, vì vậy tôi cần scope nó, để chuyển đổi Parsedown thành PrefixedByPoP\Parsedown. Nhưng làm vậy cũng sẽ scope tất cả code WordPress trong plugin, gây ra các xung đột.
Vì vậy tôi đã trích xuất code vào một package riêng biệt, được gọi là graphql-api/markdown-convertor, và thay thế dependency bên thứ ba trong composer.json bằng dependency của chính mình:
{
"require": {
"graphql-api/markdown-convertor": "^0.8"
}
}Bây giờ, plugin tránh tham chiếu thư viện bên ngoài; thay vào đó, nó tham chiếu service MarkdownConvertorInterface từ package mới:
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);
}
}Việc tham chiếu dependency bên thứ ba được thực hiện trong package mới:
namespace GraphQLAPI\MarkdownConvertor;
use Parsedown;
class MarkdownConvertor implements MarkdownConvertorInterface
{
public function convertMarkdownToHTML(string $markdownContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Cuối cùng, chúng ta phải:
- Scope dependency
graphql-api/markdown-convertor - Bỏ qua việc scope code plugin
- Đưa namespace
GraphQLAPI\*vào whitelist, để tránh các class của mình bị scope
Đây là chiến lược tóm gọn. Từ đây trở đi, sẽ là sự lặp lại của ý tưởng tương tự, để loại bỏ tất cả các dependency bên ngoài khỏi code, cho đến khi voilà, plugin có thể được scope.
Các dependency cần trích xuất chỉ là những cái trong phần require của file composer.json; đối với require-dev bạn có thể giữ bất kỳ dependency nào, bên ngoài hay không, vì chúng ta không cần scope các dependency được sử dụng cho quá trình phát triển; chỉ những cái dùng để tạo và vận chuyển plugin, cho production, mới cần được scope.
Cuối cùng, composer.json từ plugin của bạn không nên chứa bất kỳ dependency bên ngoài nào. Đối với plugin của tôi, nó trông như thế này:
{
"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"
}
}Tất cả các package đó, với namespace getpop, graphql-api, graphql-by-pop, và pop-schema, đều là của tôi: các dependency chứa toàn bộ code cho plugin. Chúng được phân phối vào các namespace khác nhau để quản lý code tốt hơn, nhưng bạn không cần phải làm vậy: sử dụng một namespace duy nhất cũng hoạt động tốt.
Bây giờ, khi số lượng package trong ứng dụng của bạn tăng lên, bạn sẽ cần lưu trữ tất cả chúng trong một monorepo, nếu không bạn sẽ phát điên khi tạo các pull request liên quan đến nhiều hơn một package (tin tôi đi, tôi đã trải qua điều đó). Trong trường hợp của tôi, tất cả các package của tôi được lưu trữ trong monorepo GatoGraphQL/GatoGraphQL, và tôi giữ chúng đồng bộ thông qua Monorepo Builder tuyệt vời (Tôi cần viết một bài về công cụ này, nó thực sự là cứu cánh!).
Các namespace cho những package này là PoP, GraphQLAPI, GraphQLByPoP và PoPSchema. Vì chúng là của tôi, tôi biết chúng sẽ chỉ xuất hiện một lần trong ứng dụng, và vì vậy tôi có thể tránh scope chúng.
Để làm điều đó, tôi đưa chúng vào whitelist trong scoper.inc.php:
return [
'whitelist' => [
// Own namespaces
'PoPSchema\*',
'PoP\*',
'GraphQLByPoP\*',
'GraphQLAPI\*',
// Own container cache
'PoPContainer\*',
],
];Mục cuối cùng tương ứng với dependency injection container, cũng cần được scope. Theo mặc định, container này được đặt tên là ProjectServiceContainer, trực tiếp trong namespace global. Nhưng PHP-Scoper không hỗ trợ đưa các class cụ thể từ namespace global vào whitelist. Do đó, tôi đã thêm namespace nhân tạo PoPContainer vào whitelist, và gán namespace này khi dump container ra đĩa:
$dumper = new PhpDumper($containerBuilder);
file_put_contents(
self::$cacheFile,
$dumper->dump(
// Save under own namespace to avoid conflicts
array('namespace' => 'PoPContainer')
)
);Bạn có thể nhận thấy rằng, về các package, một số kết thúc bằng -wp (như pop-schema/users-wp) trong khi một số không (như graphql-by-pop/graphql-server). Vâng, bạn đã đoán đúng: cái trước chứa code WordPress và không có tham chiếu đến thư viện bên ngoài, và cái sau có thể chứa tham chiếu đến thư viện bên ngoài, nhưng hoàn toàn không có code WordPress.
Sau đó, tôi bỏ qua việc scope các package 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')
]
];Điều gì xảy ra nếu một package WordPress cần tham chiếu một thư viện bên ngoài, và điều này không thể được trích xuất vào một package khác? Ví dụ, package getpop/routing-wp của tôi phụ thuộc vào brain/cortex, và điều này là không thể tránh khỏi.
Tôi không thể scope toàn bộ package, vì getpop/routing-wp chứa code WordPress. Thay vào đó, những gì tôi làm là xác định các file nơi những tham chiếu đó được thực hiện, và đảm bảo rằng chúng không chứa bất kỳ code WordPress nào. Sau đó tôi có thể chỉ scope những file đó mà thôi.
Trong trường hợp này, tham chiếu đến Cortex/Brain được thực hiện trong 2 file, bao gồm 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;
}
));
}
}
}Bạn có nhận ra điều kỳ lạ ở đây không? Đây là một triển khai của một hook, nhưng không có add_action nào được gọi, vì tôi không thể có bất kỳ code WordPress nào ở đây. Thay vào đó, nó gọi hàm addAction từ service HooksAPIInterface, và service này được triển khai bởi class HooksAPI trong package getpop/hooks-wp, nơi chúng ta có thể có code 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);
}
}Bây giờ code đã được chia tách gọn gàng, chúng ta có thể scope 2 file tham chiếu đến dependency bên ngoài:
return [
'finders' => [
Finder::create()->append([
'vendor/getpop/routing-wp/src/Component.php',
'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
])
]
];Trước đó tôi đã đề cập rằng việc thiết lập scope mất vài giờ, nhưng chỉ sau 2 tháng làm việc. Vâng, ví dụ này minh họa ý tôi muốn nói: Công việc thực sự nằm ở việc chia codebase gọn gàng thành 2 nhóm.
Trong trường hợp của tôi, công việc mất 2 tháng vì mức độ chi tiết cực kỳ cao: Plugin trở thành một tập hợp của 125 package! Nhưng đây là trường hợp ngoại lệ, với mục tiêu là server nền của plugin bất khả tri với CMS, để hỗ trợ triển khai cho các CMS/framework khác chỉ bằng cách triển khai lại các package -wp tương ứng.
(Tôi đã viết chi tiết về chiến lược này, trong bài viết Abstracting WordPress Code To Reuse With Other CMSs: Concepts và Implementation.)
Chắc chắn là khá nhiều công việc, nhưng sự sạch sẽ được cải thiện của code xứng đáng với nó. Và không chỉ vì việc scope plugin, điều đến như một bất ngờ hoàn toàn với tôi, và tôi vẫn còn vui mừng về niềm hạnh phúc bất ngờ này. Ví dụ, tôi chạy PHPStan và PHPUnit riêng biệt trên code WordPress và non-WordPress, tránh cho tôi nhiều đau đầu.
Khi codebase được dọn dẹp gọn gàng, thế giới đột nhiên trở thành một nơi tốt hơn nhiều.
Kiểm thử
Vậy, chúng ta kiểm thử con thú này như thế nào?
Giải pháp tôi đưa ra là dựa vào Rector, cùng công cụ tôi sử dụng để hạ cấp code từ PHP 7.4, để phát triển, xuống 7.1, cho production.
Ý tưởng như sau:
- Scope plugin
- Phân tích nó bằng Rector, áp dụng bất kỳ quy tắc nào (không quan trọng cái nào)
Nếu có gì đó sai trong quá trình scope, thì Rector sẽ không thể tải một số class, và nó sẽ ném ra lỗi. Ví dụ, nếu class Brain\Cortex đã được scope thành PrefixedByPoP\Brain\Cortex, nhưng một số tham chiếu đến nó bị để lại là Brain\Cortex, thì việc autoloading class này sẽ thất bại.
Đây là GitHub Action của tôi để kiểm thử (working-directory đang được sử dụng, vì tôi đang thao tác từ root của monorepo, nhưng việc scope xảy ra ở thư mục 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: .
Và đây là cấu hình Rector của tôi:
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/*',
]);
};Bạn có thể nhận thấy rằng một số file dependency, chẳng hạn như erusev/parsedown/Parsedown.php' cần được thêm vào Option::AUTOLOAD_PATHS. Đó là vì việc scope composer.json của package không đáng tin cậy 100%, và sau đó việc autoloading của chúng có thể thất bại.
Bất cứ khi nào điều đó xảy ra, Rector sẽ phàn nàn rằng một số class đã thất bại autoloading. Từ đó, chúng ta xác định file tương ứng, và thêm thủ công vào đường dẫn autoloading.
Xem kết quả
Đây là source code của plugin, và đây là phiên bản đã được scope (và hạ cấp xuống PHP 7.1).
Tìm 7 điểm khác nhau 😁. (Tôi cho bạn một gợi ý: tìm kiếm PrefixedByPoP.)
Và đây là file plugin graphql-api.zip cuối cùng, sẵn sàng để cài đặt trên trang web của bạn.
Vậy là xong. Tôi hy vọng điều này đã hữu ích 😃💪🚀