การใช้ PSR-4 autoload ใน Composer

ข้อดีอย่างหนึ่งในการเขียนการฟีเจอร์ต่างๆ แยกมาเป็นคลาสคลาสหนึ่ง คือเราสามารถเก็บค่าทุกค่า ฟังก์ชันทุกฟังก์ชัน เอาไว้แค่ในตัวมันได้โดยไม่ต้องกลัวว่ามันจะไปตีกับค่าเดียวกันหรือฟังก์ชันเดียวกัน (จริงๆ มันเรียกว่า property กับ method) ที่อยู่ในคลาสอื่น หรือที่เขาเรียกกันว่า encapsulation

ตัวอย่างการทำ encapsulation ที่เราเคยเขียนไว้ คือการใช้ ACF ทำบล็อก Gutenberg

แต่ข้อเสียอย่างหนึ่งของการทำ encapsulation นั่นคือเราต้องมานั่ง include ไฟล์คลาสจำนวนมาก (ยิ่งเขียนเยอะยิ่ง include เยอะ) ซึ่งถึงจุดหนึ่งแล้วมันจะกลายเป็นเรื่องน่ารำคาญขึ้นมาแทน แต่เราก็มีทางออก คือการทำ PSR-4 Autoloading

การทำ PSR-4 Autoloading จำเป็นต้องใช้ Composer เข้ามาช่วย ใครที่ใช้ Sage 9 อยู่แล้วก็ไปต่อได้เลย (เพราะมันใช้ composer อยู่แล้ว) ส่วนใครใช้ธีมอื่นที่ไม่ได้ใช้ composer ก็จัดการสั่ง composer init ให้ธีมตัวเองให้เรียบร้อย เมื่อเสร็จแล้วจะได้ไฟล์ composer.json ขึ้นมาหนึ่งไฟล์

เพิ่ม namespace ให้คลาส สำหรับทำ Autoloading

คลาสที่จะนำมาทำ Autoloading ได้นั้นจำเป็นจำต้องอยู่ในเนมสเปซโดยเราสามารถประกาศ namespace ได้ด้วยการวางคีย์เวิร์ด namespace เอาไว้ที่บรรทัดบนสุดในไฟล์

<?php
namespace App\Widget;

class News {
  ...
}

การกำหนดเนมสเปซนั้น ปกติจะเริ่มด้วยชื่อ vendor (มักจะเป็นชื่อคนทำ หรือชื่อโปรเจ็กท์ ซึ่งจริงๆ มันใช้เป็นชื่อของอะไรก็ได้) และอาจจะตามด้วย subnamespace อีกชั้น (กี่ชั้นก็ได้) สำหรับใช้แยกส่วนคลาสต่างๆ เหมือนการเก็บไฟล์ไว้ในโฟลเดอร์ย่อย

ข้อดีของการกำหนดเนมสเปซคือเราสามารถใช้ชื่อคลาสซ้ำกันในแต่ละส่วนได้ เช่นเราอาจจะมีคลาส News ที่อยู่ในเนมสเปซ App\Widget และมีคลาส News อีกคลาสหนึ่งที่อยู่ในเนมสเปซ App\Endpoint ก็ได้ด้วยเช่นกัน

แต่ในทางกลับกัน เมื่อเราเพิ่มเนมสเปซให้คลาสแล้ว เมื่อใดก็ตามที่เราเรียกใช้คลาสใดๆ ภายในไฟล์นี้ PHP จะโหลดคลาสนั้นโดยหาจากเนมสเปซเดียวกัน เช่นในตัวอย่างข้างบน ถ้าเราเรียกใช้คลาส WP_Query PHP ก็จะพยายามโหลดคลาส App\Widget\WP_Query ขึ้นมาแทน และจะจบด้วยข้อผิดพลาด Class not found หน้าตาประมาณนี้

วิธีแก้คือให้เราเรียกคลาสนั้นๆ ขึ้นมาจาก root namespace โดยตรงโดยการเติมเครื่องหมาย \ เข้าไปข้างหน้า เช่น $query = new \WP_Query();

อีกวิธีหนึ่งที่นิยมกันคือการใช้คีย์เวิร์ด use ในการประกาศใช้คลาสต่างๆ ในไฟล์นั้น โดยเราจะประกาศด้วย Fully Qualified Class Name (FQCN) ซึ่งประกอบด้วยชื่อเนมสเปซและชื่อคลาส เช่น App\Widget\News หรือถ้าคลาสนั้นๆ ไม่ได้อยู่ในเนมสเปซใดๆ เราก็สามารถใช้ root namespace ด้วยการเติมเครื่องหมาย \ ลงไปได้เล่น เช่น \WP_Query

<?php
namespace App\Widget;

use \WP_Query;

class News {
  public function __construct() {
    $data = new WP_Query();
  }
}

และคีย์เวิร์ด use นี่ยังมีประโยชน์อีกอย่างหนึ่งคือมันสามารถใช้เปลี่ยนชื่อคลาสที่ซ้ำกันได้ด้วย เช่นถ้าจะเรียกใช้คลาส App\Widget\News และคลาส App\Endpoint\News เข้ามาพร้อมกัน ตามปกติมันจะจบลงด้วยข้อผิดพลาดชื่อคลาสซ้ำกัน แต่เราสามารถใช้ use ... as ... ในการเปลี่ยนชื่อคลาสได้ เช่น

<?php
namespace App;

use App\Widget\News as NewsWidget;
use App\Endpoint\News as NewsEndpoint;

class Main {
  public function __construct() {
    $widget = new NewsWidget();
    $endpoint = new NewsEndpoint();
  }
}

การตั้งชื่อไฟล์และไดเรคทอรี่

ปกติแล้วเมื่อเราเขียนโปรแกรมแบบ OOP เราจะแยกคลาสเอาไว้ 1 คลาสต่อ 1 ไฟล์เสมอ และตามมาตรฐาน PSR-4 เราจะต้องตั้งชื่อไฟล์และชื่อคลาสให้ตรงกันด้วย เช่นเรามีคลาสที่ชื่อว่า Main เราก็ต้องตั้งชื่อไฟล์ว่า Main.php (พิมพ์เล็กพิมพ์ใหญ่ต้องเหมือนกัน)

ในส่วนของโครงสร้างไดเรคทอรี่นั้น ตามหลักจริงๆ เราควรจะสร้าง subdirectory ให้ตรงกับ subnamespace ด้วย เช่นคลาส News นั้นอยู่ในเนมสเปซ App\Widget เราก็ควรจะเก็บไฟล์นี้เอาไว้ที่ /Widget/News.php

แต่จริงๆ แล้วเกณฑ์การตั้งชื่อไดเรคทอรี่นั้นค่อนข้างจะยืดหยุ่น เราสามารถตั้งชื่อไดเรคทอรี่ว่าอะไรก็ได้ แล้วเราค่อยไปแมพเอาในไฟล์ composer.json ในภายหลัง แต่มีข้อแม้คือคลาสที่อยู่ในเนมสเปซเดียวกัน ต้องอยู่ในไดเรคทอรี่เดียวกันด้วยเช่นกัน

เพิ่ม autoload ลงใน composer.json

หลังจากเราเตรียมไฟล์ต่างๆ เรียบร้อยแล้ว ขั้นตอนต่อไปคือการเพิ่ม autoload ลงในไฟล์ composer.json โดยให้เราเปิดไฟล์ composer.json ขึ้นมา กรณีที่เราแค่ init โปรเจ็กท์เปล่าๆ ไว้ ไฟล์เราจะมีหน้าตาประมาณนี้

{
  "name": "wp63/test",
  "type": "project",
  "authors": [
    {
      "name": "WP63",
      "email": "[email protected]"
    }
  ],
  "require": {}
}

ให้เราเพิ่มคีย์ autoload.psr-4 ลงไปในไฟล์ตามนี้

{
  "name": "wp63/test",
  "type": "project",
  "authors": [
    {
      "name": "WP63",
      "email": "[email protected]"
    }
  ],
  "require": {},
  "autoload": {
    "psr-4": {
      
    }
  }
}

ภายในคีย์ psr-4 ให้เราทำการแมพเนมสเปซกับไดเรคทอรี่ได้เลย โดยฝั่งเนมสเปซจะแทนที่ \ ด้วย \\ และต้องมี \\ ปิดท้ายเนมสเปซด้วยเสมอ ส่วนฝั่งไดเรคทอรี่ก็จะอ้างพาทจากไฟล์ composer.json นี้ และต้องมี / ปิดท้ายด้วย

เช่นเราเก็บไฟล์เนมสเปซ App\Widgets เอาไว้ที่ app/Widgets และเก็บเนมสเปซ App\Endpoints เอาไว้ที่ app/API เราจะสามารถแมพค่าได้ดังนี้

{
  "name": "wp63/test",
  "type": "project",
  "authors": [
    {
      "name": "WP63",
      "email": "[email protected]"
    }
  ],
  "require": {},
  "autoload": {
    "psr-4": {
      "App\\Widgets\\": "app/Widgets",
      "App\\Endpoints\\": "app/API"
    }
  }
}

จริงๆ นอกจากการแมพคลาสตาม PSR-4 แล้ว เรายังใช้ composer ในการ include ไฟล์เข้ามาได้ด้วย โดยเพิ่มไฟล์ที่ต้องการ include เข้าไปที่เข้าไปในอาเรย์คีย์ autoload.files

เช่นเราจะ include ไฟล์ app/helpers.php เราสามารถทำได้ดังนี้

{
  "name": "wp63/test",
  "type": "project",
  "authors": [
    {
      "name": "WP63",
      "email": "[email protected]"
    }
  ],
  "require": {},
  "autoload": {
    "files": ["app/helpers.php"],
    "psr-4": {
      "App\\Widgets\\": "app/Widgets",
      "App\\Endpoints\\": "app/API"
    }
  }
}

เมื่อแก้ไขไฟล์เสร็จแล้ว ให้เราสั่งรัน composer install -o หรือ composer dumpautoload -o เพื่อให้ composer สร้างไฟล์ autoload ขึ้นมา

การสั่งด้วยพารามิเตอร์ -o นั้นจะเป็นการสั่งให้ composer สร้างไฟล์ autoload ขึ้นมา โดยแปลง autoload แบบ PSR-0 หรือ PSR-4 ให้เป็น class map ที่จะช่วยให้มีประสิทธิภาพดีขึ้น

เมื่อเราต้องการจะเอามาใช้ใน WordPres ก็ให้เปิดไฟล์ functions.php ของธีมเราขึ้นมา แล้วเพิ่มคำสั่ง include ไฟล์ autoload เข้าไปดังนี้

<?php
require __DIR__ . '/vendor/autoload.php';

ฟีเจอร์อื่นๆ เกี่ยวกับ composer ลองเข้าไปดูกันได้ที่ getcomposer.org


Posted

in

by

Comments

One response to “การใช้ PSR-4 autoload ใน Composer”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.