บล็อก

🍾 Gato GraphQL ได้รับการ scope แล้ว ต้องขอบคุณ PHP-Scoper!

Leonardo Losoviz
โดย Leonardo Losoviz ·

ปลั๊กอิน Gato GraphQL ได้รับการ scope แล้ว ซึ่งหมายความว่าปลั๊กอินสามารถอัปโหลดไปยัง WordPress plugin directory ได้ในที่สุด

Talking business

เพื่อดำเนินการนี้ ผมใช้ PHP-Scoper ที่ยอดเยี่ยม การใช้ไลบรารีนี้กับ WordPress ไม่ใช่เรื่องง่าย ดังนั้นผมจะอธิบายในบล็อกโพสต์นี้ว่าผมจัดการกับมันได้อย่างไร

หัวข้อ:

การตัดสินใจทำ scope

เมื่อไม่กี่สัปดาห์ก่อน Matt Mullenweg ประกาศว่าเขาจะจับตาดู "GraphQL plugin" ซึ่งแน่นอนว่าหมายถึง WPGraphQL คำพูดของเขาแสดงให้เห็นว่าเขาเชื่อว่ามี GraphQL plugin เพียงตัวเดียว ในขณะที่ความจริงแล้วมีสองตัว (ตัวที่ถูกมองข้ามก็คือของผมนั่นเอง) ทำให้ผมตระหนักว่าปลั๊กอินของผมมีการมองเห็นน้อยมาก และรู้สึกไม่ดีกับเรื่องนี้

Matt ไม่รู้ว่าปลั๊กอินของผมมีอยู่ และชุมชน WordPress ส่วนใหญ่ก็เช่นกัน เห็นได้ชัดว่าผมประชาสัมพันธ์มันไม่ดีพอ ผมรู้ว่าตัวเองไม่เก่งเรื่องการตลาดและโซเชียลมีเดีย แต่เรื่องเทคนิคพอไหว (อย่างน้อยก็คิดเอาเอง) ดังนั้นผมจึงตัดสินใจทำบางอย่าง อย่างน้อยในขอบเขตที่ทำได้

นี่คือสิ่งที่ผมกำลังทำอยู่:

  • ผมเพิ่งเขียนโค้ดเว็บไซต์นี้ gatographql.com เสร็จ และเปิดตัวเมื่อ 2 สัปดาห์ก่อน (เยี่ยม! 🥳 อนึ่ง คุณคิดยังไงกับมัน? ยินดีรับฟีดแบ็คผ่าน DM หรือ อีเมล)
  • 3 วันก่อน ผมเริ่ม scope ปลั๊กอินในที่สุด และเสร็จงานนี้เมื่อวานนี้! (ตี 3 แต่คุ้มค่ามาก 😅)
  • และสุดท้าย ผมกำลังทำงานกับเวอร์ชัน 0.8 ที่กำลังจะมาถึงอยู่แล้ว ซึ่งจะเป็นเวอร์ชันแรกที่วางจำหน่ายใน plugin repository

การ scope ปลั๊กอินเป็นสิ่งจำเป็นสำหรับการอัปโหลดไปยัง repository เพราะหากไม่ทำอาจเกิดความขัดแย้งกับปลั๊กอินอื่นที่ต้องการ dependency เดียวกันแต่เวอร์ชันต่างกัน การทำสำเร็จนี้เป็น milestone สำคัญมาก ไม่มีการพัฒนาอื่นใดสำคัญเท่านี้ ตัวอย่างเช่น ผมยังต้องสร้าง GraphQL schema ให้ครบถ้วนตรงกับ WordPress data model แต่สิ่งนั้นจะดำเนินการอย่างต่อเนื่องในแต่ละ release ใหม่

ในอีกไม่กี่สัปดาห์ข้างหน้า ปลั๊กอินจะปรากฏเมื่อค้นหาคำว่า "GraphQL" และผู้ที่ต้องการ implement GraphQL API จริงๆ จะได้รู้จักปลั๊กอินของผม

แน่นอน ผมอยากให้ปลั๊กอินของผมได้รับการพิจารณาอย่างจริงจังสำหรับอนาคตของ WordPress ผมทำงานกับมันมาหลายปีแล้ว Repo เริ่มต้นในเดือนสิงหาคม 2016 ซึ่งเป็นเวลาก่อนที่ WPGraphQL จะมีอยู่ และอยู่ในช่วงเริ่มต้นของ GraphQL แต่ผมไม่รู้ว่าโปรเจกต์นี้จะกลายเป็น GraphQL server มันเริ่มไปในทิศทางนั้นเมื่อประมาณ 1.5 ปีก่อนเท่านั้น

(โปรเจกต์นี้จริงๆ แล้วเป็น framework สำหรับสร้างแอปพลิเคชันโดยใช้ server-side components และ GraphQL server สามารถสร้างขึ้นโดยใช้สถาปัตยกรรมนี้ได้อย่างสมบูรณ์แบบ ดังนั้นผมก็เลยสร้างมันขึ้นมาเลย)

WPGraphQL เป็นปลั๊กอินที่ถูกยอมรับแล้ว และก็สมควรเป็นเช่นนั้น: มันเริ่มต้นเมื่อไม่กี่ปีก่อน และมีชุมชนสร้างขึ้นรอบๆ มัน งานของ Jason Bahl (ซึ่งได้รับการว่าจ้างจาก Gatsby) และ ผู้มีส่วนร่วมในโปรเจกต์ของเขา นั้นโดดเด่นมาก: การรวม WordPress เข้ากับ Jamstack ง่ายกว่าที่เคยเป็นมา

แต่ Gatsby และ Jamstack เป็นสิ่งหนึ่ง และ WordPress เป็นอีกสิ่งหนึ่ง WordPress คือ 40% ของเว็บ ไม่ใช่แค่ input ของ static site generator

ดังนั้นตอนนี้ เราสามารถพิจารณาว่า WPGraphQL เป็นตัวเลือกที่เหมาะสมหรือไม่ โดยไม่ต้องให้การตัดสินใจนี้ถูกทำแทนเราเพราะขาดทางเลือก เราสามารถวิเคราะห์ทั้งสองปลั๊กอินเพื่อดูว่าเป้าหมายของใครสอดคล้องกับสิ่งสำคัญสำหรับ WordPress มากกว่า

Gato GraphQL ก็ทำงานกับ Jamstack ได้เช่นกัน แต่เป้าหมายหลักของมัน ผมเชื่อว่า ยิ่งใหญ่กว่า: เพื่อ "ทำให้การเผยแพร่ข้อมูลเป็นประชาธิปไตย" เพื่อให้การแก้ไข API ง่ายพอๆ กับการแก้ไขโพสต์ (สิ่งที่ทุกคนทำได้) และทำให้ WordPress กลายเป็น OS ของเว็บ

เมื่อปลั๊กอินพร้อมใช้งานใน repository ผมหวังว่าคนมากขึ้นจะลองใช้มันและพูดว่า "นี่มันเจ๋งมากเลย! ทำไมถึงไม่รู้จักมันมาก่อนนะ?"

แล้วการเลือก "GraphQL plugin" ก็ไม่ถูกกำหนดล่วงหน้า และชุมชน WordPress สามารถพิจารณาทั้ง WPGraphQL และ Gato GraphQL ตามคุณค่าของตัวเองได้

ตอนนี้ที่แรงจูงใจของผมชัดเจนแล้ว มาคุยเรื่องเทคนิคกันเถอะ 🤓

ตรวจสอบตัวเลือกต่างๆ

การ scope ปลั๊กอินเกี่ยวข้องกับการรัน tooling ที่รับโค้ดปลั๊กอินเป็น input และส่งออกปลั๊กอินที่ scope แล้ว ไม่ใช่เรื่องใหญ่โต ใช่ไหม? จะยากแค่ไหนกันล่ะ?

Talking technical

อ้าว ขึ้นอยู่กับ codebase บางครั้งแค่รัน scope command เพียงอย่างเดียวไม่เพียงพอ หลังจากนั้น เราต้องตรวจสอบข้อผิดพลาดใน console แก้ไข ทดสอบแอปพลิเคชันอย่างละเอียด ระบุข้อผิดพลาดและสาเหตุ แก้ไข และทำซ้ำ การทำให้ถูกต้องอย่างสมบูรณ์อาจต้องใช้เวลาพอสมควร

มีไลบรารี 2 ตัวสำหรับการ scope ซึ่งมีเป้าหมายที่แตกต่างกัน:

  • Mozart สำหรับโค้ด WordPress
  • PHP-Scoper สำหรับโค้ด PHP ทุกประเภท โดยเฉพาะเมื่อสร้าง PHAR

เนื่องจากผมมี WordPress plugin ผมจึงลอง Mozart ก่อน มาดูกันว่ามันเป็นยังไง

ลอง Mozart แล้วล้มเหลว

ผมลอง Mozart เมื่อประมาณ 1 ปีก่อน ตามที่เอกสารระบุว่า "คำสั่ง mozart compose ทำทุกอย่างด้วยเวทมนตร์" ดังนั้นผมคาดว่ามันจะรวดเร็วและง่ายดาย แล้วจะได้ไปจิบ daiquiri ที่เหลือของวัน

น่าเสียดาย Mozart ไม่เคยทำงานกับ codebase ของผม มันยังคงพบปัญหา ดังนั้นการ scope จึงไม่เกิดขึ้นจริง และผมไม่สามารถรับความช่วยเหลือที่ต้องการได้: ผมส่ง PR แต่ ไม่ได้รับการพิจารณาสำหรับการ merge และไม่ได้รับการแจ้งเตือนเกี่ยวกับเรื่องนี้ด้วย ดังนั้นผมจึงรอจนกระทั่งสูญเสียความสนใจในโปรเจกต์นี้ตามธรรมชาติ

ผมเชื่อว่า Mozart ไม่สามารถจัดการกับ dependency บางอย่างใน plugin ของผมได้ ผมใช้ component ของ Symfony หลายตัว รวมถึง DependencyInjection, Cache และ Dotenv โดยทุกอย่างจัดการผ่าน Composer

การ scope PHP ไม่ใช่แค่เรื่อง PHP เท่านั้น ดังนั้น scoper จะมีอุปสรรคมากมายที่ต้องหลีกเลี่ยงและความท้าทายที่ต้องแก้ไข ตัวอย่างเช่น Symfony DependencyInjection ใช้ไฟล์ YAML สำหรับการตั้งค่า และไฟล์เหล่านี้ต้องถูก scope ด้วย และไฟล์ composer.json มีการตั้งค่าสำหรับ PSR-4 autoloading และสิ่งนี้ต้องถูก scope ด้วย และผมเชื่อว่า Mozart ไม่สามารถจัดการกับความซับซ้อนเหล่านี้ได้อย่างถูกต้อง

แต่ผมแน่ใจว่าประสบการณ์ของผมไม่ใช่ประสบการณ์เดียว และมีผู้ใช้ที่มีความสุขมากมายอยู่ที่นั่น นอกจากนี้ ความพยายามที่ล้มเหลวของผมเกิดขึ้นเมื่อ 1 ปีก่อน ผมจึงสงสัยว่าเครื่องมือได้รับการปรับปรุงตั้งแต่นั้นมาหรือไม่ และอย่าลืมสุภาษิตที่ว่า: "ปลั๊กอินที่ถูก scope แล้วทุกตัวเหมือนกัน แต่ปลั๊กอินที่ยังไม่ถูก scope จะมีวิธีที่ไม่ถูก scope ของตัวเอง" ดังนั้นอาจจะล้มเหลวแค่กับผมคนเดียว

หาก WordPress plugin ของคุณเรียบง่าย มี logic ในตัวเอง และการ scope ต้องทำภายในโค้ด PHP เท่านั้น ก็มีโอกาสที่ Mozart จะทำงานได้ คุณต้องลองดูเอง

ตรวจสอบ PHP-Scoper แล้วตกใจ

ดังนั้นผมจึงหันไปหา PHP-Scoper อย่างไรก็ตาม ผมไม่เคยแม้แต่จะลองใช้มัน เพราะผมตกใจกับมันทันที

ในการเริ่มต้น เครื่องมือนี้ไม่รองรับ WordPress โดยธรรมชาติ และยิ่งกว่านั้น พวกเขาแนะนำให้ดู Makefile ของพวกเขาเอง ซึ่งมีลักษณะดังนี้:

# 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-version

และอีก 600 บรรทัด ทั้งหมดเป็นแบบนี้ มันดูเหมือนปริศนา การคิดว่าผมต้องเข้าใจโค้ดนั้นเพื่อ scope ปลั๊กอิน ทำให้ผมหนีออกไปอย่างไม่มีระเบียบ

(อ้าว การเข้าใจโค้ดนั้นเป็นคำแนะนำของพวกเขาสำหรับการทดสอบแอปพลิเคชันที่ถูก scope แต่ไม่จำเป็น เราสามารถรันคำสั่ง php-scoper add-prefix ปล่อยให้มันทำทุกอย่าง แล้วไปดื่ม daiquiri ของเราได้)

กลับมาใช้ PHP-Scoper อีกครั้ง คราวนี้จริงจัง

ดังนั้น 3 วันก่อน ผมตัดสินใจที่จะ implement การ scope ไม่ว่าจะอย่างไร ผมต้องทำให้สำเร็จ

ผมกลับมาหา PHP-Scoper เพื่อลองใช้อย่างจริงจัง ผมรู้ว่า WordPress สามารถ scope ได้ด้วยมันจากการอ่าน PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (โดยคนเก่งจาก Delicious Brains) มันเป็นเรื่องของทัศนคติและความมุ่งมั่นเท่านั้น

ผมสำรวจวิธีแก้ปัญหาที่มีอยู่บางส่วน รวมถึง:

แต่ทั้งหมดนั้นดูไม่น่าพอใจอย่างสมบูรณ์สำหรับผม: โค้ดดูเหมือนเป็นการแฮก หรือเปราะบางและรอที่จะพังในบางจุด

ตัวอย่างเช่น ปลั๊กอิน Google Web Stories ทำการ scope โค้ด แล้ว revert กลับแต่ละความขัดแย้ง:

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 );
      // ...
    }
  ]
]

ผมเข้าใจว่าทำไมพวกเขาถึงทำแบบนั้น แต่ผมไม่ชอบมัน เมื่อใดก็ตามที่ฟังก์ชัน WordPress ใหม่ถูกอ้างอิง พวกเขาต้องตรวจสอบให้แน่ใจว่ามันถูกเพิ่มเข้าไปในรายการนี้ด้วย มันเป็นการทำด้วยตนเองมากเกินไป เปราะบางเกินไป

ดังนั้นนี่คือความท้าทายของผม: ไม่มีวิธีที่ง่ายกว่านี้ในการ scope ปลั๊กอิน และพึ่งพาโค้ดที่เราสามารถแสดงให้เพื่อนและเพื่อนร่วมงานดูได้โดยไม่ต้องอาย?

PHP-Scoper วิธีง่ายๆ 😎

จริงๆ แล้วมันง่ายกว่าที่ผมคิด! ในแค่ไม่กี่ชั่วโมง ทุกอย่างก็ทำงานได้แล้ว

Scoping in a few hours

ตอนนี้เมื่อผมพูดว่า "ง่าย" และ "ชั่วโมง" ผมหมายความว่าจริงๆ ว่า: ทุกอย่างทำงานได้ทันที แต่หลังจากใช้เวลา 2 เดือนในการสร้างโครงสร้างที่เหมาะสมสำหรับ codebase (ผมจะอธิบายเพิ่มเติมในภายหลัง)

แต่สิ่งสำคัญคือ: หากคุณมีการตั้งค่าที่เหมาะสมสำหรับโปรเจกต์ การ scope มันสามารถทำสำเร็จได้ในเวลาอันสั้น

ปัญหาของการ scope โค้ด WordPress ก็คือโค้ด WordPress นั่นเอง ปัญหา อธิบายไว้ที่นี่ แต่สรุปได้ว่าฟังก์ชันและ class WordPress ทั้งหมดถูก namespace ด้วย ดังนั้นหากเราอ้างอิง WP_Query หรือเรียก get_posts ในโค้ดของเรา สิ่งเหล่านี้จะถูกแปลงเป็น MyPrefixedNamespace\WP_Query และ MyPrefixedNamespace\get_posts ทำให้เกิดความล้มเหลวในการทำงาน และสิ่งนั้นไม่สามารถหลีกเลี่ยงได้ใน PHP-Scoper โดยไม่ต้องใช้การแฮก

ดังนั้น วิธีแก้ปัญหาคืออะไร? ง่ายมาก: อย่าอ้างอิง WP_Query หรือเรียก get_posts หรือใช้โค้ด WordPress ใดๆ ใน codebase ที่จะถูก scope

Crazy me?

ไม่ ผมไม่บ้า และผมแน่ใจว่าคุณก็ไม่เช่นกัน และใช่ ผมรู้ว่าเราสร้าง WordPress plugin อยู่... ให้ผมอธิบาย

เราจะไม่รวมโค้ด WordPress ได้อย่างไร? โดยการแบ่ง codebase ออกเป็น 2 ชุดของ package:

  • ชุดที่มีโค้ด WordPress โดยไม่อ้างอิงโค้ดจากไลบรารีภายนอกใดๆ
  • ชุดที่มี business logic โดยไม่มีโค้ด WordPress ใดๆ และ รวม dependency ที่จำเป็นและการอ้างอิงโค้ดทั้งหมด

ด้วยวิธีนี้ แทนที่จะมี codebase เดียว เรามี codebase หลายตัว (หรือ package) ที่บางตัวจะถูก scope และบางตัวไม่ถูก scope และทั้งหมดก็รวมกันเป็นปลั๊กอินผ่าน Composer

จากนั้น เราไม่ scope package ที่มีโค้ด WordPress หลีกเลี่ยงความขัดแย้ง สิ่งนี้ได้ผลเพราะมันไม่อ้างอิงโค้ดใดๆ ที่เป็นของ dependency ภายนอก การอ้างอิงทั้งหมดเป็นภายใน เช่น MyNamespace\MyPlugin\MyClass แต่สิ่งเหล่านั้นไม่จำเป็นต้องถูก scope เพราะเราสามารถสมมติได้อย่างปลอดภัยว่าจะมีปลั๊กอินเพียง 1 เวอร์ชันที่ติดตั้งใน WordPress site และเราสามารถ whitelist namespace MyNamespace\* ของเรา

ยิ่งกว่านั้น หากปลั๊กอินของเราสามารถขยายได้ การ whitelist namespace ของเราเองเป็นสิ่งจำเป็น ตัวอย่างเช่น field resolver สำหรับ Gato GraphQL นั้น implement โดยการขยายจาก class PoP\ComponentModel\FieldResolvers\AbstractFieldResolver หากผม scope มัน นักพัฒนาจะถูกบังคับให้อ้างอิง PoP\ComponentModel\FieldResolvers\AbstractFieldResolver สำหรับการพัฒนา และ PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver สำหรับ production นั่นไม่ได้เรื่อง

จากนั้น เราจะ scope เฉพาะ package business logic ที่มีการอ้างอิงถึงไลบรารีภายนอกทั้งหมดแต่ไม่มีโค้ด WordPress

โดยสรุป เรากำลังเปลี่ยนกลยุทธ์นี้:

"มี codebase เดียว scope มัน แล้วค่อยๆ แก้ไขความเสียหายอย่างเจ็บปวดและด้วยความอดทนมาก ในขณะที่อธิษฐานว่าไม่มีความขัดแย้งใดที่ถูกมองข้ามและ 💣 ระเบิดใน production"

เป็นอันนี้แทน:

"แบ่ง codebase ออกเป็น 2 กลุ่ม scope เฉพาะกลุ่มที่มีการอ้างอิงถึง external dependency และไม่มีโค้ด WordPress แล้วไปดื่ม daiquiri ที่คุณได้มาอย่างสมควร 🍹"

ขอดูของจริงหน่อย

ถึงเวลาแล้วที่จะเปิดไส้กรอกดูว่ามีเนื้อจริงอยู่ข้างใน 🌭

4 วันก่อน ผมมี โค้ดต่อไปนี้ ใน plugin ของผม:

namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
 
use Parsedown;
 
class MarkdownContentParser
{
  protected function getHTMLContent(string $fileContent): string
  {
    return (new Parsedown())->text($markdownContent);
  }
}

Class Parsedown มาจาก external dependency erusev/parsedown ตามที่กำหนดใน plugin's composer.json:

{
  "require": {
    "erusev/parsedown": "^1.7"
  }
}

ดังนั้น plugin ของผมมีการอ้างอิงถึงไลบรารีภายนอก ดังนั้นผมต้อง scope มัน เพื่อแปลง Parsedown เป็น PrefixedByPoP\Parsedown แต่การทำเช่นนั้นจะทำให้โค้ด WordPress ทั้งหมดใน plugin ถูก scope ด้วย ทำให้เกิดความขัดแย้ง

ดังนั้นผมจึงดึงโค้ดออกมาเป็น package แยกต่างหาก ที่เรียกว่า graphql-api/markdown-convertor และแทนที่ 3rd-party dependency ใน composer.json ด้วย dependency ของผมเอง:

{
  "require": {
    "graphql-api/markdown-convertor": "^0.8"
  }
}

ตอนนี้ plugin หลีกเลี่ยงการอ้างอิงไลบรารีภายนอกโดยตรง แต่แทนที่ อ้างอิง service MarkdownConvertorInterface จาก package ใหม่:

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);
    }
}

การอ้างอิง 3rd-party dependency ทำใน package ใหม่:

namespace GraphQLAPI\MarkdownConvertor;
 
use Parsedown;
 
class MarkdownConvertor implements MarkdownConvertorInterface
{
  public function convertMarkdownToHTML(string $markdownContent): string
  {
    return (new Parsedown())->text($markdownContent);
  }
}

สุดท้าย เราต้อง:

  • Scope dependency graphql-api/markdown-convertor
  • ข้ามการ scope โค้ดปลั๊กอิน
  • Whitelist namespace GraphQLAPI\* เพื่อหลีกเลี่ยงไม่ให้ class ของผมเองถูก scope

นี่คือกลยุทธ์หลักโดยย่อ จากนี้ไปจะเป็นการทำซ้ำแนวคิดเดิมนี้ เพื่อลบ external dependency ทั้งหมดออกจากโค้ด จนกว่าปลั๊กอินจะสามารถถูก scope ได้

dependency ที่ต้องดึงออกมาคือเฉพาะจาก section require ในไฟล์ composer.json ของคุณเท่านั้น สำหรับ require-dev คุณสามารถเก็บ dependency ใดๆ ไว้ได้ ทั้งภายนอกหรือไม่ก็ตาม เนื่องจากเราไม่จำเป็นต้อง scope dependency ที่ใช้สำหรับการพัฒนา เฉพาะที่ใช้สร้างและส่งมอบปลั๊กอินสำหรับ production เท่านั้นที่ต้อง scope

ในท้ายที่สุด composer.json จาก plugin ของคุณไม่ควรมี external dependency ใดๆ สำหรับ plugin ของผม มีลักษณะดังนี้:

{
  "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"
  }
}

package เหล่านั้นทั้งหมด ที่มี namespace getpop, graphql-api, graphql-by-pop และ pop-schema ล้วนเป็นของผม: dependency ที่มีโค้ดทั้งหมดสำหรับปลั๊กอิน พวกมันกระจายอยู่ใน namespace ต่างๆ เพื่อจัดการโค้ดได้ดีขึ้น แต่คุณไม่จำเป็นต้องทำแบบนั้น การใช้ namespace เดียวก็ทำงานได้ดี

ตอนนี้ เมื่อจำนวน package ใน application ของคุณเพิ่มขึ้น คุณจะต้องมีทั้งหมดโฮสต์ใน monorepo มิฉะนั้นคุณจะเสียสติเมื่อสร้าง pull request ที่เกี่ยวข้องกับมากกว่าหนึ่ง package (เชื่อผมเถอะ ผมเคยผ่านมาแล้ว) ในกรณีของผม package ทั้งหมดโฮสต์อยู่ใน GatoGraphQL/GatoGraphQL monorepo และผมเก็บพวกมันให้ตรงกันผ่าน Monorepo Builder ที่ยอดเยี่ยม (ผมต้องเขียนบทความเกี่ยวกับเครื่องมือนี้ มันช่วยชีวิตจริงๆ!)

namespace สำหรับ package เหล่านี้คือ PoP, GraphQLAPI, GraphQLByPoP และ PoPSchema เนื่องจากพวกมันเป็นของผม ผมรู้ว่าพวกมันจะปรากฏเพียงครั้งเดียวใน application ดังนั้นผมจึงหลีกเลี่ยงการ scope พวกมันได้

เพื่อทำสิ่งนั้น ผม whitelist พวกมันใน scoper.inc.php:

return [
  'whitelist' => [
    // Own namespaces
    'PoPSchema\*',
    'PoP\*',
    'GraphQLByPoP\*',
    'GraphQLAPI\*',
    // Own container cache
    'PoPContainer\*',
  ],
];

entry สุดท้ายสอดคล้องกับ dependency injection container ซึ่งต้อง scope ด้วย โดยค่าเริ่มต้น container นี้ถูกกำหนดชื่อ ProjectServiceContainer โดยตรงใน global namespace แต่ PHP-Scoper ไม่รองรับการ whitelist class เฉพาะจาก global namespace ดังนั้นผมจึงเพิ่ม namespace เทียม PoPContainer เข้าไปใน whitelist และ กำหนด namespace นี้ เมื่อ dump container ไปยังดิสก์:

$dumper = new PhpDumper($containerBuilder);
file_put_contents(
  self::$cacheFile,
  $dumper->dump(
    // Save under own namespace to avoid conflicts
    array('namespace' => 'PoPContainer')
  )
);

คุณอาจสังเกตเห็นว่าเกี่ยวกับ package บางตัวลงท้ายด้วย -wp (เช่น pop-schema/users-wp) ในขณะที่บางตัวไม่ลงท้าย (เช่น graphql-by-pop/graphql-server) ใช่ คุณเดาถูก: ตัวแรกมีโค้ด WordPress และไม่มีการอ้างอิงถึงไลบรารีภายนอก และตัวหลังอาจมีการอ้างอิงถึงไลบรารีภายนอก แต่ไม่มีโค้ด WordPress เลย

จากนั้น ผม ข้ามการ scope WordPress packages:

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')
  ]
];

จะเกิดอะไรขึ้นหาก WordPress package จำเป็นต้องอ้างอิงไลบรารีภายนอก และไม่สามารถดึงออกมาเป็น package อื่นได้? ตัวอย่างเช่น package getpop/routing-wp ของผมขึ้นอยู่กับ brain/cortex และ นี่หลีกเลี่ยงไม่ได้

ผมไม่สามารถ scope ทั้ง package ได้ เนื่องจาก getpop/routing-wp มีโค้ด WordPress แทนที่ สิ่งที่ผมทำคือระบุไฟล์ที่มีการอ้างอิงเหล่านั้น และตรวจสอบให้แน่ใจว่าไฟล์เหล่านั้นไม่มีโค้ด WordPress จากนั้นผมสามารถ scope เฉพาะไฟล์เหล่านั้นได้

ในกรณีนี้ การอ้างอิงถึง Cortex/Brain ทำใน 2 ไฟล์ รวมถึง 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;
        }
      ));
    }
  }
}

คุณสังเกตเห็นความแปลกประหลาดที่นี่ไหม? นี่คือการ implement hook แต่ไม่มีการเรียก add_action เพราะผมไม่สามารถมีโค้ด WordPress ที่นี่ได้ แต่แทนที่มันเรียกฟังก์ชัน addAction จาก service HooksAPIInterface และ service นี้ถูก implement โดย class HooksAPI ใน package getpop/hooks-wp ซึ่งเราสามารถมีโค้ด 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);
  }
}

ตอนนี้โค้ดถูกแบ่งอย่างสะอาด เราสามารถ scope 2 ไฟล์ที่อ้างอิง external dependency:

return [
  'finders' => [
    Finder::create()->append([
      'vendor/getpop/routing-wp/src/Component.php',
      'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
    ])
  ]
];

ก่อนหน้านี้ผมกล่าวถึงว่าการตั้งค่าการ scope ใช้เวลาไม่กี่ชั่วโมง แต่หลังจากทำงาน 2 เดือน ตัวอย่างนี้แสดงให้เห็นว่าผมหมายความว่าอะไร: งานจริงอยู่ที่การแบ่ง codebase ออกเป็น 2 ชุดอย่างสะอาด

ในกรณีของผม งานใช้เวลา 2 เดือนเพราะระดับรายละเอียดสูงมาก: ปลั๊กอินกลายเป็นองค์ประกอบของ 125 package! แต่นี่เป็นกรณีพิเศษ ด้วยเป้าหมายให้ server พื้นฐานสำหรับปลั๊กอิน เป็น CMS-agnostic เพื่อรองรับการ implement สำหรับ CMS/framework อื่นๆ เพียงแค่ reimplementing package -wp ที่สอดคล้องกัน

(ผมเขียนรายละเอียดเกี่ยวกับกลยุทธ์นี้ในบทความ Abstracting WordPress Code To Reuse With Other CMSs: Concepts และ Implementation)

มันเป็นงานที่ค่อนข้างมากแน่นอน แต่ความสะอาดของโค้ดที่ดีขึ้นทำให้คุ้มค่า และไม่ใช่แค่สำหรับการ scope ปลั๊กอิน ซึ่งเป็นความประหลาดใจสมบูรณ์สำหรับผม และผมยังคงรู้สึกดีใจกับความสุขที่ไม่คาดคิดนี้ ตัวอย่างเช่น ผมรัน PHPStan และ PHPUnit แยกต่างหากบนโค้ด WordPress และ non-WordPress หลีกเลี่ยงปวดหัวมากมาย

เมื่อ codebase ถูกจัดระเบียบ โลกก็กลายเป็นสถานที่ที่ดีกว่าขึ้นทันที

การทดสอบ

แล้วเราจะทดสอบสัตว์ร้ายนี้ได้อย่างไร?

วิธีแก้ปัญหาที่ผมคิดขึ้นมาคือการพึ่งพา Rector ซึ่งเป็นเครื่องมือเดียวกับที่ผมใช้สำหรับ การ downgrade โค้ดจาก PHP 7.4 สำหรับการพัฒนา เป็น 7.1 สำหรับ production

แนวคิดคือดังต่อไปนี้:

  1. Scope ปลั๊กอิน
  2. วิเคราะห์ด้วย Rector โดยใช้ rule ใดๆ (ไม่สำคัญว่า rule ไหน)

หากมีบางอย่างผิดพลาดเมื่อ scope แล้ว Rector จะไม่สามารถโหลด class บางตัวได้ และจะ throw error ตัวอย่างเช่น หาก class Brain\Cortex ถูก scope เป็น PrefixedByPoP\Brain\Cortex แต่การอ้างอิงบางส่วนยังคงเป็น Brain\Cortex การ autoload class นี้จะล้มเหลว

นี่คือ GitHub Action สำหรับการทดสอบ ของผม (working-directory ถูกใช้เพราะผมดำเนินการจาก root ของ monorepo แต่การ scope เกิดขึ้นใน folder ของปลั๊กอิน):

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: .
 

และ นี่คือ Rector configuration ของผม:

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/*',
  ]);
};

คุณอาจสังเกตว่า dependency ไฟล์บางตัว เช่น erusev/parsedown/Parsedown.php' จำเป็นต้องเพิ่มเข้าไปใน Option::AUTOLOAD_PATHS เพราะการ scope composer.json ของ package นั้นไม่น่าเชื่อถือ 100% และ autoloading ของพวกมันอาจล้มเหลวได้

เมื่อเกิดเหตุการณ์นั้น Rector จะร้องเรียนว่า class บางตัว autoload ล้มเหลว จากนั้นเราระบุไฟล์ที่สอดคล้องกัน และเพิ่มมันเข้าไปใน autoloading paths ด้วยตนเอง

ตรวจสอบผลลัพธ์

นี่คือซอร์สโค้ดของปลั๊กอิน และ นี่คือเวอร์ชันที่ถูก scope (และ downgrade เป็น PHP 7.1)

หา 7 ความแตกต่าง 😁 (ผมให้힌트: ค้นหา PrefixedByPoP)

และ นี่คือไฟล์ปลั๊กอิน graphql-api.zip ขั้นสุดท้าย พร้อมติดตั้งบนไซต์ของคุณ

นั่นคือทั้งหมด หวังว่ามันจะเป็นประโยชน์ 😃💪🚀


สมัครรับจดหมายข่าวของเรา

ติดตามการอัปเดตทั้งหมดของ Gato GraphQL