Blog

🍾 Gato GraphQL artık kapsamlandırıldı, PHP-Scoper'a teşekkürler!

Leonardo Losoviz
Yazan: Leonardo Losoviz ·

Gato GraphQL eklentisi artık kapsamlandırıldı. Bu, eklentinin sonunda WordPress eklenti dizinine yüklenebileceği anlamına geliyor.

İş görüşmesi

Bunu yapmak için harika PHP-Scoper kütüphanesini kullanıyorum. Bu kütüphaneyi WordPress ile kullanmak birtakım zorluklardan bağımsız değil; bu yüzden bu blog yazısında nasıl başardığımı açıklayacağım.

Bölümler:

Kapsamlandırma kararını vermek

Birkaç hafta önce Matt Mullenweg, açıkça WPGraphQL'i kastederek "GraphQL eklentisini" takip edeceğini duyurdu. Bu ifade, yalnızca bir GraphQL eklentisinin var olduğuna inandığını gösteriyor; oysa aslında iki tane var (dışarıda kalan, benim eklentim). Bu durum bana eklentimin ne kadar az görünürlüğe sahip olduğunu fark ettirdi ve kendimi kötü hissettirdi.

Matt, eklentimin varlığından habersizdi. WordPress topluluğunun büyük çoğunluğu da öyle. Açıkçası onu yeterince tanıtamıyorum. Pazarlama ve sosyal medyada berbat olduğumu biliyorum; teknik konularda biraz idare ederim (ya da öyle sanıyorum). Bu yüzden en azından kendi yeteneklerim dahilinde bir şeyler yapmaya karar verdim.

İşte üzerinde çalıştığım konular:

  • Bu web sitesini, gatographql.com, yeni bitirdim ve 2 hafta önce yayına aldım (yaşasın! 🥳 Bu arada, ne düşünüyorsun? DM veya e-posta ile geri bildirim vermekten çekinme)
  • 3 gün önce nihayet eklentiyi kapsamlandırmaya başladım ve bu görevi dün tamamladım! (Sabah 3'te, ama değdi 😅)
  • Ve son olarak, eklenti deposunda mevcut olacak ilk sürüm olan 0.8 üzerinde çalışıyorum

Eklentiyi kapsamlandırmak, depoya yüklemek için zorunludur; aksi takdirde, benim eklentimle aynı bağımlılığı gerektiren ancak farklı bir sürüm kullanan başka bir eklentiyle çakışabilir. Bunu yapmış olmak gerçekten büyük bir dönüm noktasıdır; hiçbir geliştirme bu kadar önemli değildir. Örneğin, GraphQL şemasını WordPress veri modeliyle tam olarak eşleşecek şekilde tamamlamam gerekiyor; ancak bu her yeni sürümde istikrarlı bir şekilde yapılacak.

Yani birkaç hafta içinde eklenti "GraphQL" aramasında görünecek ve gerçekten bir GraphQL API uygulaması gerektiren kişiler eklentimin varlığından haberdar olacak.

Gerçekten eklentimin WordPress'in geleceği için ciddi biçimde değerlendirileceğini istiyorum. Bunun üzerinde birkaç yıldır çalışıyorum. Repo Ağustos 2016'da başlatıldı; bu, WPGraphQL'in var olmasından ve GraphQL'in başlangıcından bile önce. Ama projenin bir GraphQL sunucusuna dönüşeceğini bilmiyordum; bu yön yaklaşık 1,5 yıl önce oluştu.

(Proje aslında sunucu taraflı bileşenler kullanarak uygulama oluşturmak için bir çerçeve; ve bu mimariyle mükemmel biçimde bir GraphQL sunucusu inşa edilebilir. Dolayısıyla ben de onu inşa ettim.)

WPGraphQL köklü bir eklentidir ve haklı olarak: birkaç yıl önce başlatıldı ve etrafında bir topluluk oluştu. Jason Bahl'ın (Gatsby tarafından istihdam edilmektedir) ve projeye katkıda bulunanların yaptığı çalışma olağanüstüdür: WordPress'i Jamstack'e entegre etmek artık her zamankinden daha kolay.

Ama Gatsby ve Jamstack bir şey, WordPress başka bir şey. WordPress, yalnızca statik site oluşturucusuna bir girdi değil, web'in %40'ıdır.

Artık alternatiflerin yokluğu nedeniyle bu karar bizim adımıza alınmadan WPGraphQL'in doğru seçenek olup olmadığını değerlendirebiliriz. Şimdi her iki eklentiyi de inceleyerek hangisinin hedeflerinin WordPress için önemli olanlarla daha uyumlu olduğunu görebiliriz.

Gato GraphQL de Jamstack ile çalışabilir. Ancak bence asıl hedefleri daha görkemdir: "Veri yayınını demokratikleştirmek", böylece bir API'yi düzenlemek, bir gönderiyi düzenlemek kadar kolay hale gelsin (herkesin yapabileceği bir şey) ve WordPress'i web'in işletim sistemi haline getirsin.

Eklenti depoda kullanılabilir hale geldiğinde, daha fazla insanın onu deneyip "Hey, bu inanılmaz derecede harika! Bunu daha önce neden bilmiyordum ki?" diyeceğini umuyorum.

Ve o zaman "GraphQL eklentisi" seçimi önceden belirlenmiş olmayacak; WordPress topluluğu hem WPGraphQL'i hem de Gato GraphQL'i kendi değerlerine göre değerlendirebilecek.

Motivasyonlarımı bir kenara bırakarak teknik konulara geçelim 🤓.

Seçenekleri incelemek

Bir eklentiyi kapsamlandırmak, eklenti kodunu girdi olarak alıp kapsamlandırılmış eklentiyi çıkaran bazı araçları çalıştırmayı gerektirir. Büyük mesele değil, değil mi? Ne kadar zor olabilir ki?

Teknik konuşma

Kod tabanına bağlı olarak yalnızca kapsam komutunu çalıştırmak yeterli olmayabilir. Bunun ardından konsolda hataları kontrol etmek, düzeltmek, uygulamayı kapsamlı biçimde test etmek, hataları ve nedenlerini belirlemek, düzeltmek ve tekrarlamak gerekir. Her şeyi doğru yapmak biraz zaman alabilir.

Farklı hedeflere sahip 2 kapsamlandırma kütüphanesi var:

  • Mozart, WordPress kodu için
  • PHP-Scoper, özellikle PHAR üretirken herhangi bir PHP kodu için

Bir WordPress eklentisine sahip olduğum için önce Mozart'ı denedim. Nasıl gittiğine bakalım.

Mozart'ı denemek ve başarısız olmak

Mozart'ı yaklaşık 1 yıl önce denedim. Dokümantasyonda yazdığına göre, "mozart compose komutu tüm sihri yapar". Bu yüzden her şeyin çok hızlı ve basit olacağını, günün geri kalanında daiquiri içeceğimi umuyordum.

Ne yazık ki, Mozart kod tabanım için hiç çalışmadı. Sürekli sorunlarla karşılaştı, bu yüzden kapsamlandırma hiç gerçekleşmedi. Gerekli yardımı da alamadım: bir PR gönderdim, ancak birleştirme için değerlendirilmedi ve bu konuda bilgilendirilmedim bile; bu projeye doğal olarak ilgimi kaybedene kadar beklemeye devam ettim.

Mozart'ın eklentimde bazı bağımlılıkları yönetemediğine inanıyorum. Symfony bileşenlerinden birkaçını kullanıyorum: DependencyInjection, Cache ve Dotenv; hepsi Composer aracılığıyla yönetiliyor.

PHP'yi kapsamlandırmak yalnızca PHP ile ilgili değil; bu nedenle kapsamlandırıcının kaçınması gereken birçok engel ve çözmesi gereken zorluklar olacak. Örneğin, Symfony DependencyInjection yapılandırmayı kurmak için YAML dosyaları kullanır ve bunların da kapsamlandırılması gerekir. composer.json dosyası PSR-4 otomatik yükleme yapılandırmasını içerir ve bunun da kapsamlandırılması gerekir. Ve bence Mozart bu karmaşıklıkları düzgün biçimde yönetemedi.

Ama benim deneyimimin tek olmadığından ve orada mutlu kullanıcıların bulunduğundan eminim. Ayrıca başarısız girişimim 1 yıl önce gerçekleşti; araç o zamandan beri geliştirilmiş olabilir. Ve şunu unutma: "Tüm kapsamlandırılmış eklentiler birbirine benzer; her kapsamlandırılmamış eklenti kendi yolunda kapsamlandırılmamıştır" diye bir söz var; belki yalnızca benim için başarısız oluyor.

WordPress eklentin kendi mantığıyla basitse ve kapsamlandırmanın yalnızca PHP kodu içinde yapılması gerekiyorsa, Mozart'ın çalışma ihtimali var. Denemek zorundasın.

PHP-Scoper'ı keşfetmek ve paniğe kapılmak

PHP-Scoper'a yöneldim. Ancak, hemen korktuğum için hiç denemek için bile girişimde bulunmadım.

Her şeyden önce, bu araç WordPress'i doğal olarak desteklemiyor. Dahası, kendi Makefile dosyalarına bakılmasını öneriyorlar; bu şöyle görünüyor:

# 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

Ve hepsi bunun gibi olan 600 satır daha. Bir bilmece gibi görünüyor. Eklentimi kapsamlandırmak için bu kodu anlamam gerektiğini düşünerek seri biçimde kaçtım.

(Şey, bu kodu anlamak kapsamlandırılmış uygulamayı test etmek için önerdikleri şey; ancak zorunlu değil. php-scoper add-prefix komutunu çalıştırabilir, tüm sihri yapmasına izin verebilir ve daiquirimizi içmeye gidebiliriz.)

PHP-Scoper'a bu sefer gerçekten dönmek

3 gün önce, bir şekilde kapsamlandırmayı uygulamaya karar verdim. Bunu başarmam gerekiyordu.

PHP-Scoper'a geri döndüm ve ciddiyetle denemeye karar verdim. WordPress'in onunla kapsamlandırılabileceğini PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies makalesini (Delicious Brains'deki parlak insanlar tarafından) okuyarak biliyordum. Sadece doğru tutum ve azim meselesiydi.

Mevcut bazı çözümleri inceledim:

Ama hepsi bana tam olarak tatmin edici gelmiyor: ya kod hacky görünüyor, ya da kırılgan ve eninde sonunda bozulacakmış gibi.

Örneğin, Google Web Stories eklentisi kodu kapsamlandırıyor ve ardından çakışmaların her birini geri alıyor:

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

Neden yaptıklarını anlıyorum, ama beğenmiyorum. Yeni bir WordPress fonksiyonuna referans her verildiğinde, bu listeye de eklendiğinden emin olmaları gerekiyor. Çok manuel, çok kırılgan.

İşte benim zorluğum buydu: Bir eklentiyi kapsamlandırmanın daha basit bir yolu yok mu? Arkadaşlarımıza ve iş arkadaşlarımıza kızarmadan gösterebileceğimiz kodlara dayanan bir yol?

PHP-Scoper, kolay yol 😎

Aslında düşündüğümden çok daha kolay çıktı! Sadece birkaç saat içinde her şeyi çalıştırdım.

Birkaç saatte kapsamlandırma

"Kolay" ve "saatler" derken şunu kastediyorum: Her şey hemen çalıştı, ama yalnızca kod tabanı için uygun yapıyı oluşturmak için 2 ay harcadıktan sonra (daha sonra daha iyi açıklayacağım).

Ama önemli olan şu: Proje için doğru kuruluma sahipsen, onu kapsamlandırmak kısa sürede gerçekleştirilebilir.

WordPress kodunu kapsamlandırmanın sorunu, WordPress kodunun kendisi. Sorun burada açıklanıyor, ancak özünde tüm WordPress fonksiyon ve sınıflarının da namespace'e alınmasına indirgenebilir. Dolayısıyla kodumuzda WP_Query'ye referans verirsek veya get_posts'u çağırırsak, bunlar MyPrefixedNamespace\WP_Query ve MyPrefixedNamespace\get_posts'a dönüştürülür ve çalışma zamanında epik bir başarısızlık üretir. Bu, PHP-Scoper'da hack'ler olmadan engellenemez.

Peki çözüm ne? Çok basit: Kapsamlandırılacak kod tabanında WP_Query'ye referans verme, get_posts'u çağırma veya herhangi bir WordPress kodu kullanma.

Deli miyim?

Hayır, deli değilim ve senin de olmadığından eminim. Ve evet, bir WordPress eklentisi oluşturduğumuzu biliyorum... Açıklamama izin ver.

WordPress kodunu nasıl dahil etmeyebiliriz? Kod tabanını 2 paket kümesine bölerek:

  • WordPress kodu içerenler, herhangi bir harici kütüphaneden kod referansı vermeden
  • İş mantığı içerenler, hiçbir WordPress kodu içermeden ve tüm gerekli bağımlılıkları ve kodlarına referansları dahil ederek

Bu şekilde, tek bir kod tabanına sahip olmak yerine, bazıları kapsamlandırılacak bazıları kapsamlandırılmayacak birden fazla kod tabanımız (veya paketimiz) olur; hepsi Composer aracılığıyla birbirine bağlanarak eklentiyi oluşturur.

Ardından WordPress kodu içeren paketi kapsamlandırmayız; böylece çakışmayı önleriz. Bu işe yarar çünkü herhangi bir harici bağımlılığa ait koda referans vermez. Tüm referanslar MyNamespace\MyPlugin\MyClass gibi dahilidir. Bunların kapsamlandırılması gerekmez çünkü WordPress sitesinde eklentinin yalnızca 1 sürümünün yüklü olacağını güvenle varsayabilir ve MyNamespace\* namespace'imizi beyaz listeye ekleyebiliriz.

Üstelik, eklentimiz genişletilebilirse kendi namespace'imizi beyaz listeye eklemek zorunludur. Örneğin, Gato GraphQL için bir field resolver, PoP\ComponentModel\FieldResolvers\AbstractFieldResolver sınıfından türeyerek uygulanır. Bunu kapsamlandırsaydım, geliştiriciler geliştirme için PoP\ComponentModel\FieldResolvers\AbstractFieldResolver'a ve üretim için PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver'a referans vermek zorunda kalacaktı. Bu kabul edilemez.

Ardından yalnızca iş mantığı paketlerini kapsamlandırırız; bunlar tüm harici kütüphanelere referanslar içerir ancak hiç WordPress kodu içermez.

Özetle, şu stratejiden geçiş yapıyoruz:

"Tek bir kod tabanına sahip ol, onu kapsamlandır, ardından acı çekerek ve büyük sabırla hasarı geri al; hiçbir çakışmanın fark edilmeden geçmemesini ve üretimde 💣 patlamamasını dile"

Şu stratejiye:

"Kod tabanını 2 gruba böl, yalnızca harici bağımlılıklara referans içereni ve WordPress kodu içermeyeni kapsamlandır, ve hak ettiğin daiquirini iç 🍹".

Gerçek malzemeyi göster

Sosisi açıp içinde gerçek et olup olmadığını görme zamanı 🌭.

4 gün önce, eklentimde şu koda sahiptim:

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

Parsedown sınıfı, eklentinin composer.json'ında tanımlandığı gibi harici bağımlılık erusev/parsedown'dan geliyor:

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

Dolayısıyla eklentim bir harici kütüphaneye referans içeriyordu; ParsedownPrefixedByPoP\Parsedown'a dönüştürmek için onu kapsamlandırmam gerekiyordu. Ama bunu yapmak, eklentideki tüm WordPress kodunu da kapsamlandırır ve çakışmalara neden olurdu.

Bu nedenle kodu graphql-api/markdown-convertor adlı ayrı bir pakete çıkardım ve composer.json'daki üçüncü taraf bağımlılığını kendi bağımlılığımla değiştirdim:

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

Artık eklenti harici kütüphaneye referans vermekten kaçınıyor; bunun yerine yeni paketten MarkdownConvertorInterface servisine referans veriyor:

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

Üçüncü taraf bağımlılığına referans yeni pakette yapılıyor:

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

Son olarak şunu yapmamız gerekiyor:

  • graphql-api/markdown-convertor bağımlılığını kapsamlandır
  • Eklenti kodunu kapsamlandırmayı atla
  • Kendi sınıflarımın kapsamlandırılmaması için GraphQLAPI\* namespace'ini beyaz listeye ekle

Bu stratejinin özü bu. Bundan itibaren, voilà, eklenti kapsamlandırılabilir hale gelene kadar tüm harici bağımlılıkları kodu kaldırmak için aynı fikrin tekrarı olacak.

Çıkarılacak bağımlılıklar yalnızca composer.json dosyanızdaki require bölümündekilerdir; require-dev için herhangi bir bağımlılığı, harici veya dahili olsun, tutabilirsiniz; geliştirme için kullanılan bağımlılıkları kapsamlandırmamız gerekmez; yalnızca üretim için eklentiyi oluşturmak ve göndermek için olanların kapsamlandırılması gerekir.

Sonunda, eklentinizin composer.json'ı herhangi bir harici bağımlılık içermemelidir. Benim eklentim için şöyle görünüyor:

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

getpop, graphql-api, graphql-by-pop ve pop-schema namespace'lerine sahip tüm bu paketler hepsi benim: eklentinin tüm kodunu içeren bağımlılıklar. Kodu daha iyi yönetmek için farklı namespace'lere dağıtılmışlar, ama buna gerek yok: tek bir namespace kullanmak da iyi çalışır.

Uygulamandaki paket sayısı arttıkça, hepsini bir monorepo'da barındırman gerekecek; yoksa birden fazla paketi kapsayan pull request'ler oluştururken çıldırırsın (inan bana, bunu yaşadım). Benim durumumda, tüm paketlerim GatoGraphQL/GatoGraphQL monorepo'sunda barındırılıyor ve harika Monorepo Builder aracılığıyla senkronize tutuyorum (bu araç hakkında bir makale yazmam gerekiyor, gerçek bir kurtarıcı!).

Bu paketlerin namespace'leri PoP, GraphQLAPI, GraphQLByPoP ve PoPSchema. Bunlar benim olduğu için uygulamada yalnızca bir kez görüneceklerini biliyorum; bu nedenle onları kapsamlandırmaktan kaçınabilirim.

Bunu yapmak için onları scoper.inc.php'de beyaz listeye ekliyorum:

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

Son giriş, kapsamlandırılması gereken dependency injection container'ına karşılık geliyor. Varsayılan olarak, bu container'a doğrudan global namespace'de ProjectServiceContainer adı atanır. Ancak PHP-Scoper, global namespace'den belirli sınıfları beyaz listeye almayı desteklemiyor. Bu nedenle, beyaz listeye yapay PoPContainer namespace'i ekledim ve container'ı diske dökerken bu namespace'i atadım:

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

Paketlere gelince, bazılarının -wp ile bittiğini fark edebilirsin (örneğin pop-schema/users-wp) bazıları bitmez (örneğin graphql-by-pop/graphql-server). Evet, tahmin ettin: öncekiler WordPress kodu içeriyor ve harici kütüphanelere referans yok; sonrakiler harici kütüphanelere referans içerebilir, ancak hiç WordPress kodu yok.

Ardından, WordPress paketlerini kapsamlandırmayı atlıyorum:

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

Bir WordPress paketinin harici bir kütüphaneye referans vermesi gerekiyorsa ve bu başka bir pakete çıkarılamıyorsa ne olur? Örneğin, getpop/routing-wp paketim brain/cortex'e bağımlı ve bu kaçınılmaz.

Tüm paketi kapsamlandıramam çünkü getpop/routing-wp WordPress kodu içeriyor. Bunun yerine, bu referansların yapıldığı dosyaları belirleyip hiç WordPress kodu içermediğinden emin oluyorum. Ardından yalnızca bu dosyaları kapsamlandırabilirim.

Bu durumda, Cortex/Brain'e yapılan referans, layers/Engine/packages/routing-wp/src/Hooks/SetupCortexHookSet.php dahil 2 dosyada yapılıyor:

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

Buradaki tuhaflığı fark ettin mi? Bu bir hook uygulaması, ancak add_action çağrılmıyor; çünkü burada WordPress kodu bulunduramam. Bunun yerine, HooksAPIInterface servisinden addAction fonksiyonunu çağırıyor; bu servis, WordPress kodu bulundurabileceğimiz getpop/hooks-wp paketindeki HooksAPI sınıfı tarafından uygulanıyor:

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

Kod temiz biçimde bölündükten sonra, harici bağımlılıklara referans veren bu 2 dosyayı kapsamlandırabiliriz:

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

Daha önce kapsamlandırma kurulumunun birkaç saat sürdüğünü, ancak yalnızca 2 aylık çalışmanın ardından olduğunu söyledim. Bu örnek ne kastettiğimi gösteriyor: Asıl çalışma, kod tabanını 2 kümeye temiz biçimde bölmektir.

Benim durumumda, ayrıntı düzeyi aşırı olduğu için iş 2 ay sürdü: Eklenti 125 paketin bileşimine dönüştü! Ama bu, temel hedefi eklentinin altındaki sunucunun CMS-agnostic olmasını sağlamak olan istisnai bir durum; böylece yalnızca ilgili -wp paketlerini yeniden uygulayarak diğer CMS'ler/çerçeveler için bir uygulama desteklenebilir.

(Bu strateji hakkında ayrıntılı yazdım: Abstracting WordPress Code To Reuse With Other CMSs: Concepts ve Implementation makalelerinde.)

Elbette bu oldukça fazla iş, ancak kodun artan temizliği buna değiyor. Ve yalnızca eklentiyi kapsamlandırmak için değil; bu benim için tamamen bir sürpriz oldu ve bu beklenmedik mutluluğumda hâlâ çıldırıyorum. Örneğin, PHPStan ve PHPUnit'i WordPress ve WordPress olmayan kod üzerinde ayrı ayrı çalıştırıyorum; bu bana pek çok baş ağrısından kurtarıyor.

Kod tabanı bir kez düzeltildiğinde, dünya aniden çok daha güzel bir yer oluyor.

Test etmek

Peki bu canavarı nasıl test ederiz?

Bulduğum çözüm, PHP 7.4'ten geliştirme için 7.1'e, üretim için, kod aktarmak amacıyla kullandığım araç olan Rector'a güvenmek.

Fikir şu:

  1. Eklentiyi kapsamlandır
  2. Herhangi bir kural uygulayarak Rector ile analiz et (hangisi olduğu önemli değil)

Kapsamlandırma sırasında bir şeyler yanlış gittiyse, Rector bazı sınıfları yükleyemeyecek ve hata fırlatacak. Örneğin, Brain\Cortex sınıfı PrefixedByPoP\Brain\Cortex olarak kapsamlandırıldıysa ancak ona yapılan bazı referanslar Brain\Cortex olarak kaldıysa, bu sınıfın otomatik yüklenmesi başarısız olacak.

Bu benim test için GitHub Action'ım (working-directory kullanılıyor çünkü monorepo kökünden işlem yapıyorum, ancak kapsamlandırma eklenti klasöründe gerçekleşiyor):

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

Ve bu benim Rector yapılandırmam:

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

erusev/parsedown/Parsedown.php gibi bazı bağımlılık dosyalarının Option::AUTOLOAD_PATHS'e eklenmesi gerektiğini fark edebilirsin. Bunun nedeni, paketin composer.json'ını kapsamlandırmanın %100 güvenilir olmaması ve otomatik yüklemelerinin başarısız olabilmesidir.

Bu her gerçekleştiğinde, Rector bazı sınıfların otomatik yüklemesinin başarısız olduğundan şikayet edecektir. Oradan, ilgili dosyayı belirleriz ve onu manuel olarak otomatik yükleme yollarına ekleriz.

Sonuçlara bak

Bu eklentinin kaynak kodu ve bu da kapsamlandırılmış (ve PHP 7.1'e indirgeme yapılmış) sürümü.

7 farkı bul 😁. (Bir ipucu vereyim: PrefixedByPoP araması yap.)

Ve bu da graphql-api.zip eklenti dosyasının son hali, sitenize yüklenmeye hazır.

Hepsi bu. Umarım faydalı olmuştur 😃💪🚀


Bültenimize abone olun

Gato GraphQL'deki tüm güncellemelerden haberdar olun.