PHP Closure: เริ่มต้นชีวิตใหม่ปิดกั้นตัวเองยอมลืมทุกสิ่งอย่าง
ภาษาทั่วไปเช่น Python เราสามารถทำ closure เช่นนี้ได้
read_only = 42
def f(x):
return read_only + x
print(f(10))
# 52
หรือกระทั่ง
ls = [4, 8, 15, 16, 23, 42]
def g(x):
ls.append(x)
g(99)
print(ls)
# [4, 8, 15, 16, 23, 42, 99]
ความประหลาดและน่ารำคาญใน PHP คือเราไม่สามารถเขียนแค่นี้เพื่อทำ closure ง่ายๆ ตามข้างบนได้ เพราะเมื่อเราสร้างฟังก์ชันขึ้นมาแล้ว ฟังก์ชันนั้นจะไม่รู้จักตัวแปรใดๆ เลย (ยกเว้นพวก superglobals อย่าง $_GET
) เราต้องเพิ่ม keyword global
เข้าไปอีก เช่น
<?php
$ls = array(4, 8, 15, 16, 23, 42);
function g($x) {
global $ls;
$ls[] = $x;
}
ซึ่งท่านี้ก็ยังมีปัญหาอีกว่าถ้าตัวแปรที่ต้องการไม่อยู่ใน global scope (เช่นไปอยู่ใน local scope ของฟังก์ชันที่สร้างฟังก์ชันนี้อีกที) PHP ก็จะหาตัวแปรนั้นไม่เจอ
ทางแก้ที่ดูแล้วเป็น functional มากที่สุด คือใช้ keyword use
เช่นนี้
<?php
$ls = array(4, 8, 15, 16, 23, 42);
$g = function($x) use($ls) {
$ls[] = $x;
return $ls;
};
แน่นอนว่าเมื่อทำแบบ functional แล้ว ตัวแปร $ls
เก่าจะไม่เปลี่ยนค่า เพราะเมื่อมันถูกเรียกผ่าน use
นั่นหมายถึงการคัดลอกค่าตัวแปรมาทั้งหมด แล้วตั้งชื่อตัวแปรให้เหมือนกันใน scope ต่างกัน แต่ถ้าอยากให้ตัวแปรเดิมเปลี่ยนค่าก็ยังสามารถใช้เทคนิคเดิมได้คือ
<?php
function($x) use(&$ls) { ... }
อย่างไรก็ตาม ท่านี้ยังมีปัญหาตรงที่การประกาศฟังก์ชันต้องทำแบบ anonymous (แล้วค่อยเอาตัวแปรไปรับ) แถมถ้าเราจะอ้างค่าใน scope อื่นเป็นจำนวนมาก ที่หัวฟังก์ชันจะเขียนได้รุงรังอย่าบอกใคร
ข้อดีเดียวที่นึกออกจากการบังคับใช้ global
หรือ use
สำหรับเรียกตัวแปรนอก scope คือ PHP อนุญาตให้ไม่ต้อง init ตัวแปรก็ได้ (ถ้าไม่มีการ init มาก่อน มันจะถือว่าเป็นค่าว่างตามการใช้งานนั้นๆ) ทำให้เราสามารถเขียนอะไรเช่นนี้ได้
<?php
function query_to_array() {
$res = mysql_query('SELECT * FROM blah_blah_blah');
foreach ($res as $row) {
$ls[] = $row['foo_blah'];
}
return $ls;
}
เราอาจมองว่าท่านี้สวยตรงที่ไม่ต้อง init ตัวแปร $ls
ที่รู้ๆ กันอยู่แล้วว่าต้องเป็น array ว่างแน่ๆ แต่ถ้าเกิดว่า query ข้างบนให้ผลลัพท์เป็นเซตว่าง ตอน return เอาไปใช้ต่อจะเกิด bug เพราะ PHP ไม่สามารถบอก type ของ $ls
ได้ สุดท้ายก็ต้องกลับไปประกาศตัวแปรไว้ที่จุดเริ่มต้นฟังก์ชันอยู่ดี หรือไม่งั้นก็เปลี่ยน return เป็น
<?php
return $ls ?: array();
ส่วนข้อเสียของการไม่ยอมให้อ้างตัวแปรนอก scope ได้นั้น นอกจากความหงุดหงิดแล้ว ก็มาจากวิวัฒนาการของภาษาสมัยนี้ที่พยายามทำให้เป็น OOP มากขึ้น ทุกวันนี้มันคงไม่แปลกที่จะเขียน
<?php
$db = new PDO('mysql: ...');
$res = $db->prepare('SELECT * FROM blah_blah_blah WHERE answer = ?');
$res->execute(array(42));
ในความจริงแล้ว ขั้นตอน prepare/execute มักถูกเขียนในส่วนอื่นๆ ไม่เอาไว้ติดกันเช่นนี้ ยิ่งไปกว่านั้นมันมักโดน refactor ไว้ในฟังก์ชันเพื่อจัดระเบียบให้อ่านง่ายด้วย ในเมื่อตัวแปร $db
ที่ควรเป็น global ดันไม่สามารถเรียกใช้ได้ง่ายๆ ใครมันจะอยากเขียนแบบ OOP กันหละ?
ทางออกโดยทั่วไปก็คือสร้าง model ที่เป็น interface สำหรับ query ทั้งหมดให้ทำผ่านตัวมัน แล้วตอนสร้าง model ก็ bind ตัวแปร $db
เข้าไป ก็คือเราสามารถเข้าถึง database ได้โดย $this->db
ซึ่งท่านี้ก็ยังรุงรังเหมือนเดิม แถมตอนเรียกใช้ก็ยังต้องพิมพ์ยาวขึ้นอีกด้วย
ท่าที่ผมชอบมากกว่าเป็นของ Laravel ที่สร้าง class เชื่อมต่อ database นั้นๆ ไว้ให้เลย ทำให้เวลาจะทำ query ก็เพียงแค่
<?php
$res = DB::select('SELECT * FROM blah_blah_blah WHERE answer = ?', array(42));
เพราะว่า class และฟังก์ชันใน PHP สามารถเรียกใช้จาก scope ไหนๆ ก็ได้ครับ
ข้อดีนิดเดียว (แถมยังไม่แน่ว่ามันเป็นข้อดีจริงๆ หรือเปล่า) แต่ข้อเสียบานเลย รู้งี้แล้วยังเขียน PHP กันอยู่อีกรึ :P
author