Buggy PHP Code: 10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา PHP Make
เผยแพร่แล้ว: 2022-03-11PHP ทำให้ง่ายต่อการสร้างระบบบนเว็บ ซึ่งเป็นสาเหตุส่วนใหญ่ที่ทำให้ความนิยมของระบบนี้ แต่ความง่ายในการใช้งานของมัน ถึงแม้ว่า PHP ได้พัฒนาเป็นภาษาที่ค่อนข้างซับซ้อนด้วยเฟรมเวิร์ก ความแตกต่าง และรายละเอียดปลีกย่อยมากมายที่สามารถกัดนักพัฒนาได้ บทความนี้เน้นย้ำข้อผิดพลาดทั่วไปสิบประการที่นักพัฒนา PHP ต้องระวัง
ข้อผิดพลาดทั่วไป #1: ออกจากการอ้างอิงอาร์เรย์ที่ห้อยต่องแต่งหลังจาก foreach
loops
ไม่แน่ใจว่าจะใช้ foreach loops ใน PHP ได้อย่างไร? การใช้การอ้างอิงใน foreach
loops อาจมีประโยชน์หากคุณต้องการดำเนินการกับแต่ละองค์ประกอบในอาร์เรย์ที่คุณกำลังวนซ้ำ ตัวอย่างเช่น:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)
ปัญหาคือถ้าคุณไม่ระวัง สิ่งนี้อาจมีผลข้างเคียงและผลที่ไม่พึงประสงค์บางอย่าง โดยเฉพาะในตัวอย่างข้างต้น หลังจากที่รันโค้ดแล้ว $value
จะยังคงอยู่ในขอบเขตและจะมีการอ้างอิงไปยังองค์ประกอบสุดท้ายในอาร์เรย์ การดำเนินการที่ตามมาที่เกี่ยวข้องกับ $value
อาจทำให้แก้ไของค์ประกอบสุดท้ายในอาร์เรย์โดยไม่ได้ตั้งใจ
สิ่งสำคัญที่ต้องจำไว้คือ foreach
ไม่ได้สร้างขอบเขต ดังนั้น $value
ในตัวอย่างข้างต้นจึงเป็น ข้อมูลอ้างอิง ภายในขอบเขตสูงสุดของสคริปต์ ในการวนซ้ำแต่ละครั้ง foreach
ตั้งค่าการอ้างอิงให้ชี้ไปที่องค์ประกอบถัดไปของ $array
หลังจากวนซ้ำเสร็จสิ้น ดังนั้น $value
ยังคงชี้ไปที่องค์ประกอบสุดท้ายของ $array
และยังคงอยู่ในขอบเขต
ต่อไปนี้คือตัวอย่างประเภทของข้อบกพร่องที่หลีกเลี่ยงและทำให้เกิดความสับสนที่อาจนำไปสู่:
$array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (ie, copy) echo implode(',', $array), "\n";
รหัสด้านบนจะแสดงผลต่อไปนี้:
1,2,3 1,2,3 1,2,2
ไม่ นั่นไม่ใช่การพิมพ์ผิด ค่าสุดท้ายในบรรทัดสุดท้ายคือ 2 ไม่ใช่ 3
ทำไม?
หลังจากผ่านลูป foreach
แรกแล้ว $array
ยังคงไม่เปลี่ยนแปลง แต่ตามที่อธิบายไว้ข้างต้น $value
จะถูกปล่อยให้เป็นการอ้างอิงแบบห้อยไปยังองค์ประกอบสุดท้ายใน $array
(เนื่องจาก foreach
loop เข้าถึง $value
โดย reference )
เป็นผลให้เมื่อเราผ่านลูป foreach
ที่สอง "สิ่งแปลก ๆ" จะปรากฏขึ้น โดยเฉพาะอย่างยิ่ง เนื่องจากขณะนี้ $value
ถูกเข้าถึงโดย value (เช่น โดย copy ) foreach
จะ คัดลอก องค์ประกอบ $array
ตามลำดับไปยัง $value
ในแต่ละขั้นตอนของลูป ด้วยเหตุนี้ นี่คือสิ่งที่จะเกิดขึ้นในแต่ละขั้นตอนของ foreach
loop ที่สอง:
- ส่ง 1: คัดลอก
$array[0]
(เช่น “1”) เป็น$value
(ซึ่งอ้างอิงถึง$array[2]
) ดังนั้น$array[2]
ตอนนี้เท่ากับ 1 ดังนั้น$array
จึงมี [1 2, 1]. - ส่ง 2: คัดลอก
$array[1]
(เช่น “2”) เป็น$value
(ซึ่งอ้างอิงถึง$array[2]
) ดังนั้น$array[2]
ตอนนี้เท่ากับ 2 ดังนั้น$array
จึงมี [1 2, 2]. - ส่ง 3: คัดลอก
$array[2]
(ซึ่งตอนนี้เท่ากับ “2”) เป็น$value
(ซึ่งอ้างอิงถึง$array[2]
) ดังนั้น$array[2]
ยังคงเท่ากับ 2 ดังนั้น$array
จึงมี [1 , 2, 2].
เพื่อให้ยังคงได้รับประโยชน์จากการใช้การอ้างอิงใน foreach
loop โดยไม่ต้องเสี่ยงกับปัญหาประเภทนี้ ให้เรียก unset()
บนตัวแปรทันทีหลังจาก foreach
loop เพื่อลบการอ้างอิง เช่น:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]
ข้อผิดพลาดทั่วไป #2: ความเข้าใจผิด isset()
พฤติกรรม
แม้ชื่อของมัน isset()
ไม่เพียงแต่คืนค่า false หากไม่มีรายการอยู่ แต่ ยังคืนค่า false
สำหรับ null
ลักษณะการทำงานนี้เป็นปัญหามากกว่าที่ปรากฏในตอนแรก และเป็นสาเหตุของปัญหาทั่วไป
พิจารณาสิ่งต่อไปนี้:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
ผู้เขียนรหัสนี้น่าจะต้องการตรวจสอบว่า keyShouldBeSet
ถูกตั้งค่าใน $data
หรือไม่ แต่ตามที่กล่าวไว้ isset($data['keyShouldBeSet'])
จะ คืนค่า false หาก $data['keyShouldBeSet']
ถูก ตั้งค่าไว้ แต่ถูกตั้งค่าเป็น null
ดังนั้นตรรกะข้างต้นจึงมีข้อบกพร่อง
นี่เป็นอีกตัวอย่างหนึ่ง:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
โค้ดด้านบนถือว่าถ้า $_POST['active']
คืนค่า true
ดังนั้น postData
จะต้องถูกตั้งค่า ดังนั้น isset($postData)
จะคืนค่า true
ในทางกลับกัน โค้ดด้านบนถือว่าวิธี เดียว ที่ isset($postData)
จะคืนค่า false
คือถ้า $_POST['active']
คืนค่า false
เช่นกัน
ไม่.
ตามที่อธิบายไว้ isset($postData)
จะคืนค่า false
หาก $postData
ถูกตั้งค่าเป็น null
ดังนั้นจึง เป็น ไปได้ที่ isset($postData)
จะคืนค่า false
แม้ว่า $_POST['active']
คืน true
ดังนั้นอีกครั้ง ตรรกะข้างต้นมีข้อบกพร่อง
และอีกประการหนึ่ง หากเจตนาในโค้ดด้านบนคือการตรวจสอบอีกครั้งว่า $_POST['active']
เป็นจริงหรือไม่ โดยอาศัย isset()
เนื่องจากเป็นการตัดสินใจในการเข้ารหัสที่ไม่ดีไม่ว่ากรณีใดๆ แต่จะดีกว่าถ้าตรวจสอบอีกครั้ง $_POST['active']
; เช่น:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
สำหรับกรณีที่สิ่งสำคัญ คือ ต้องตรวจสอบว่ามีการตั้งค่าตัวแปรจริงหรือไม่ (เช่น เพื่อแยกความแตกต่างระหว่างตัวแปรที่ไม่ได้ตั้งค่ากับตัวแปรที่ตั้งค่าเป็น null
) วิธี array_key_exists()
จะมีประสิทธิภาพมากกว่ามาก สารละลาย.
ตัวอย่างเช่น เราสามารถเขียนตัวอย่างแรกจากสองตัวอย่างข้างต้นใหม่ได้ดังนี้:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
นอกจากนี้ ด้วยการรวม array_key_exists( array_key_exists()
กับ get_defined_vars()
เราสามารถตรวจสอบได้อย่างน่าเชื่อถือว่ามีการตั้งค่าตัวแปรภายในขอบเขตปัจจุบันหรือไม่:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
ข้อผิดพลาดทั่วไป #3: ความสับสนเกี่ยวกับการส่งคืนโดยการอ้างอิงเทียบกับมูลค่า
พิจารณาข้อมูลโค้ดนี้:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
หากคุณเรียกใช้โค้ดข้างต้น คุณจะได้รับสิ่งต่อไปนี้:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
มีอะไรผิดปกติ?
ปัญหาคือโค้ดด้านบนสร้างความสับสนในการส่งคืนอาร์เรย์โดยการอ้างอิงกับการส่งคืนอาร์เรย์ตามค่า เว้นแต่คุณจะบอกให้ PHP คืนค่าอาร์เรย์โดยการอ้างอิง (เช่น โดยใช้ &
) โดยค่าเริ่มต้น PHP จะคืนค่าอาร์เรย์ "ตามค่า" ซึ่งหมายความว่า สำเนา ของอาร์เรย์จะถูกส่งคืน ดังนั้นฟังก์ชันที่เรียกและผู้เรียกจะไม่เข้าถึงอินสแตนซ์เดียวกันของอาร์เรย์
ดังนั้นการเรียก getValues()
ข้างต้นจะคืนค่า สำเนา ของอาร์เรย์ $values
แทนที่จะเป็นการอ้างอิง ด้วยเหตุนี้ เรามาทบทวนสองบรรทัดหลักจากตัวอย่างด้านบนนี้:
// getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the "undefined index" message). echo $config->getValues()['test'];
การแก้ไขที่เป็นไปได้อย่างหนึ่งคือการบันทึกสำเนาแรกของอาร์เรย์ $values
ที่ส่งคืนโดย getValues()
แล้วดำเนินการกับสำเนานั้นในภายหลัง เช่น:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
รหัสนั้นจะทำงานได้ดี (กล่าวคือ มันจะส่งออก test
โดยไม่สร้างข้อความ "ดัชนีที่ไม่ได้กำหนด") แต่ขึ้นอยู่กับสิ่งที่คุณพยายามทำให้สำเร็จ แนวทางนี้อาจหรืออาจไม่เพียงพอ โดยเฉพาะอย่างยิ่ง โค้ดด้านบนนี้จะไม่แก้ไขอาร์เรย์ $values
ดั้งเดิม ดังนั้น หาก คุณ ต้องการให้การปรับเปลี่ยนของคุณ (เช่น การเพิ่มองค์ประกอบ 'test') ให้ส่งผลต่ออาร์เรย์ดั้งเดิม คุณจะต้องแก้ไขฟังก์ชัน getValues()
เพื่อคืนค่าการ อ้างอิง ไปยังอาร์เรย์ $values
เอง ทำได้โดยการเพิ่ม &
ก่อนชื่อฟังก์ชัน ซึ่งบ่งชี้ว่าควรส่งคืนข้อมูลอ้างอิง เช่น:
class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
ผลลัพธ์ของสิ่งนี้จะเป็นการ test
ตามที่คาดไว้
แต่เพื่อให้เกิดความสับสนมากขึ้น ให้พิจารณาข้อมูลโค้ดต่อไปนี้แทน:
class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
หากคุณเดาว่าสิ่งนี้จะส่งผลให้เกิดข้อผิดพลาด "ดัชนีที่ไม่ได้กำหนด" เหมือนกับตัวอย่าง array
ก่อนหน้านี้ แสดงว่าคุณคิดผิด อันที่จริงรหัส นี้ จะทำงานได้ดี เหตุผลก็คือ PHP ส่งผ่านอ็อบเจ็กต์โดยการอ้างอิง ต่างจากอาร์เรย์เสมอ ( ArrayObject
เป็นวัตถุ SPL ซึ่งเลียนแบบการใช้งานอาร์เรย์อย่างสมบูรณ์ แต่ทำงานเป็นวัตถุ)
ดังที่ตัวอย่างเหล่านี้แสดงให้เห็น ไม่ชัดเจนเสมอไปใน PHP ว่าคุณกำลังจัดการกับสำเนาหรือข้อมูลอ้างอิง ดังนั้นจึงจำเป็นต้องเข้าใจพฤติกรรมเริ่มต้นเหล่านี้ (เช่น ตัวแปรและอาร์เรย์ถูกส่งผ่านตามค่า วัตถุถูกส่งผ่านโดยการอ้างอิง) และยังต้องตรวจสอบเอกสาร API อย่างละเอียดสำหรับฟังก์ชันที่คุณเรียกใช้เพื่อดูว่ามีการคืนค่าหรือไม่ สำเนาของอาร์เรย์ การอ้างอิงไปยังอาร์เรย์ หรือการอ้างอิงไปยังวัตถุ
ทั้งหมดที่กล่าวมา เป็นสิ่งสำคัญที่จะต้องทราบว่าการคืนค่าการอ้างอิงไปยังอาร์เรย์หรือ ArrayObject
โดยทั่วไปเป็นสิ่งที่ควรหลีกเลี่ยง เนื่องจากจะทำให้ผู้โทรมีความสามารถในการแก้ไขข้อมูลส่วนตัวของอินสแตนซ์ได้ สิ่งนี้ "บินต่อหน้า" ของการห่อหุ้ม ควรใช้ "getters" และ "setters" แบบเก่าแทน เช่น
class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'
วิธีการนี้ทำให้ผู้เรียกสามารถตั้งค่าหรือรับค่าใดๆ ในอาร์เรย์โดยไม่ต้องให้การเข้าถึงสาธารณะในอาร์เรย์ $values
ส่วนตัว
ข้อผิดพลาดทั่วไป #4: การสืบค้นแบบวนซ้ำ
ไม่ใช่เรื่องแปลกที่จะเจอสิ่งนี้หาก PHP ของคุณไม่ทำงาน:
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
แม้ว่าจะไม่มีอะไรผิดพลาดอย่างแน่นอนที่นี่ แต่ถ้าคุณทำตามตรรกะในโค้ด คุณอาจพบว่าการเรียกที่ดูไร้เดียงสาเหนือ $valueRepository->findByValue()
ในท้ายที่สุดส่งผลให้เกิดการสืบค้นบางประเภท เช่น:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
ด้วยเหตุนี้ การวนซ้ำแต่ละครั้งของลูปด้านบนจะส่งผลให้เกิดการสืบค้นแยกไปยังฐานข้อมูล ตัวอย่างเช่น หากคุณระบุอาร์เรย์ 1,000 ค่าให้กับลูป มันจะสร้างการสืบค้นแยก 1,000 รายการไปยังทรัพยากร! หากสคริปต์ดังกล่าวถูกเรียกในหลายเธรด อาจทำให้ระบบหยุดชะงักได้
ดังนั้นจึงเป็นเรื่องสำคัญที่จะต้องรู้ว่าเมื่อใดที่รหัสของคุณถูกสร้างขึ้นมา และรวบรวมค่าต่างๆ เมื่อใดก็ตามที่เป็นไปได้ จากนั้นเรียกใช้การสืบค้นหนึ่งรายการเพื่อดึงผลลัพธ์ทั้งหมด
ตัวอย่างหนึ่งของจุดที่พบได้บ่อยในการสอบถามที่ดำเนินการอย่างไม่มีประสิทธิภาพ (เช่น วนซ้ำ) คือเมื่อมีการโพสต์แบบฟอร์มพร้อมรายการค่าต่างๆ (เช่น ID) จากนั้น ในการดึงข้อมูลบันทึกทั้งหมดสำหรับแต่ละ ID โค้ดจะวนซ้ำผ่านอาร์เรย์และทำแบบสอบถาม SQL แยกกันสำหรับแต่ละ ID ซึ่งมักจะมีลักษณะดังนี้:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
แต่สิ่งเดียวกันสามารถทำได้อย่างมีประสิทธิภาพมากขึ้นในแบบสอบถาม SQL เดียว ดังนี้:
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
ดังนั้นจึงเป็นเรื่องสำคัญที่จะต้องตระหนักว่าเมื่อมีการสร้างคำค้นหาไม่ว่าทางตรงหรือทางอ้อมด้วยรหัสของคุณ เมื่อใดก็ตามที่เป็นไปได้ ให้รวบรวมค่าแล้วเรียกใช้หนึ่งแบบสอบถามเพื่อดึงผลลัพธ์ทั้งหมด ยังต้องใช้ความระมัดระวังด้วยเช่นกัน ซึ่งนำเราไปสู่ข้อผิดพลาด PHP ทั่วไปครั้งต่อไปของเรา...
ข้อผิดพลาดทั่วไป #5: การปลอมแปลงการใช้หน่วยความจำและความไร้ประสิทธิภาพ
แม้ว่าการดึงข้อมูลหลายระเบียนในคราวเดียวจะมีประสิทธิภาพมากกว่าการเรียกใช้แบบสอบถามเดียวสำหรับแต่ละแถวที่จะดึงข้อมูล วิธีการดังกล่าวอาจนำไปสู่สภาวะ "หน่วยความจำไม่เพียงพอ" ใน libmysqlclient
เมื่อใช้ส่วนขยาย mysql
ของ PHP
เพื่อสาธิต ให้ดูที่กล่องทดสอบที่มีทรัพยากรจำกัด (512MB RAM), MySQL และ php-cli
เราจะบูตตารางฐานข้อมูลดังนี้:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }
ตกลง ตอนนี้เรามาตรวจสอบการใช้ทรัพยากรกัน:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";
เอาท์พุท:

Before: 224704 Limit 1: 224704 Limit 10000: 224704
เย็น. ดูเหมือนว่าการสืบค้นข้อมูลจะได้รับการจัดการอย่างปลอดภัยภายในในแง่ของทรัพยากร
เพื่อความแน่ใจ เรามาเพิ่มขีดจำกัดอีกครั้งและตั้งค่าเป็น 100,000 เอ่อโอ้. เมื่อเราทำเช่นนั้น เราได้รับ:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
เกิดอะไรขึ้น?
ปัญหานี่คือวิธีการทำงานของโมดูล mysql
ของ PHP เป็นเพียงพร็อกซีสำหรับ libmysqlclient
ซึ่งทำงานสกปรก เมื่อเลือกข้อมูลบางส่วนแล้ว ข้อมูลนั้นจะเข้าสู่หน่วยความจำโดยตรง เนื่องจากหน่วยความจำนี้ไม่ได้รับการจัดการโดยตัวจัดการของ PHP memory_get_peak_usage()
จะไม่แสดงการใช้ทรัพยากรเพิ่มขึ้นเมื่อเราเพิ่มขีดจำกัดในการสืบค้นของเรา สิ่งนี้นำไปสู่ปัญหาดังที่แสดงไว้ข้างต้น ซึ่งเราถูกหลอกให้คิดว่าไม่พึงพอใจในการจัดการหน่วยความจำของเรา แต่ในความเป็นจริง การจัดการหน่วยความจำของเรามีข้อบกพร่องอย่างร้ายแรง และเราอาจประสบปัญหาเช่นเดียวกับที่แสดงด้านบน
อย่างน้อยคุณสามารถหลีกเลี่ยง headfake ข้างต้นได้ (แม้ว่าตัวมันเองจะไม่ปรับปรุงการใช้หน่วยความจำของคุณ) โดยใช้โมดูล mysqlnd
แทน mysqlnd
ถูกคอมไพล์เป็นส่วนขยาย PHP ดั้งเดิม และใช้ตัวจัดการหน่วยความ จำ ของ PHP
ดังนั้น หากเราเรียกใช้การทดสอบข้างต้นโดยใช้ mysqlnd
แทนที่จะเป็น mysql
เราจะได้ภาพที่สมจริงมากขึ้นของการใช้หน่วยความจำของเรา:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
และมันแย่ยิ่งกว่านั้นอีก ตามเอกสารประกอบของ PHP mysql
ใช้ทรัพยากรมากเป็นสองเท่า mysqlnd
เพื่อเก็บข้อมูล ดังนั้นสคริปต์ดั้งเดิมที่ใช้ mysql
จึงใช้หน่วยความจำมากกว่าที่แสดงที่นี่ (ประมาณสองเท่า)
เพื่อหลีกเลี่ยงปัญหาดังกล่าว ให้ลองจำกัดขนาดของข้อความค้นหาของคุณและใช้ลูปที่มีการวนซ้ำจำนวนเล็กน้อย เช่น:
$totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }
เมื่อเราพิจารณาทั้งข้อผิดพลาด PHP และข้อผิดพลาด #4 ข้างต้น เราตระหนักดีว่ามีความสมดุลที่ดีที่โค้ดของคุณต้องบรรลุในอุดมคติ ในแง่หนึ่ง การที่คำถามของคุณละเอียดและซ้ำซากเกินไป เทียบกับการมีแต่ละรายการของคุณ ข้อความค้นหาแต่ละรายการมีขนาดใหญ่เกินไป เช่นเดียวกับชีวิตส่วนใหญ่ ความสมดุลเป็นสิ่งจำเป็น สุดขั้วไม่ดีและอาจทำให้เกิดปัญหากับ PHP ทำงานไม่ถูกต้อง
ข้อผิดพลาดทั่วไป #6: ละเว้นปัญหา Unicode/UTF-8
ในแง่หนึ่ง นี่เป็นปัญหาใน PHP มากกว่าปัญหาที่คุณจะเจอขณะทำการดีบั๊ก PHP แต่ไม่เคยได้รับการแก้ไขอย่างเพียงพอ แกนหลักของ PHP 6 นั้นจะต้องทำให้รับรู้ Unicode แต่นั่นถูกระงับเมื่อการพัฒนา PHP 6 ถูกระงับในปี 2010
แต่นั่นไม่ได้หมายความว่านักพัฒนาไม่สามารถส่ง UTF-8 ได้อย่างถูกต้องและหลีกเลี่ยงข้อสันนิษฐานที่ผิดพลาดว่าสตริงทั้งหมดจะต้องเป็น "ASCII แบบเก่าธรรมดา" โค้ดที่ไม่สามารถจัดการสตริงที่ไม่ใช่ ASCII ได้อย่างเหมาะสมนั้นขึ้นชื่อเรื่องการแนะนำ heisenbugs ที่น่ารังเกียจในโค้ดของคุณ แม้แต่การเรียก strlen($_POST['name'])
อย่างง่ายก็อาจทำให้เกิดปัญหาได้หากมีผู้ที่มีนามสกุลเช่น “Schrodinger” พยายามลงทะเบียนเข้าสู่ระบบของคุณ
ต่อไปนี้คือรายการตรวจสอบเล็กๆ น้อยๆ เพื่อหลีกเลี่ยงปัญหาดังกล่าวในโค้ดของคุณ:
- หากคุณไม่ค่อยมีความรู้เกี่ยวกับ Unicode และ UTF-8 มากนัก อย่างน้อยคุณควรเรียนรู้พื้นฐาน มีไพรเมอร์ที่ดีที่นี่
- อย่าลืมใช้ฟังก์ชัน
mb_*
แทนฟังก์ชันสตริงแบบเก่า (ตรวจสอบให้แน่ใจว่าส่วนขยาย "มัลติไบต์" รวมอยู่ใน PHP build ของคุณแล้ว) - ตรวจสอบให้แน่ใจว่าฐานข้อมูลและตารางของคุณได้รับการตั้งค่าให้ใช้ Unicode (หลายรุ่นของ MySQL ยังคงใช้
latin1
โดยค่าเริ่มต้น) - จำไว้ว่า
json_encode()
จะแปลงสัญลักษณ์ที่ไม่ใช่ ASCII (เช่น “Schrodinger” กลายเป็น “Schr\u00f6dinger”) แต่serialize()
จะ ไม่ แปลง - ตรวจสอบให้แน่ใจว่าไฟล์โค้ด PHP ของคุณมีการเข้ารหัส UTF-8 ด้วยเพื่อหลีกเลี่ยงการชนกันเมื่อเชื่อมสตริงกับค่าคงที่สตริงที่กำหนดค่าฮาร์ดโค้ดหรือกำหนดค่าไว้
แหล่งข้อมูลที่มีค่าโดยเฉพาะในเรื่องนี้คือ UTF-8 Primer สำหรับ PHP และ MySQL โพสต์โดย Francisco Claria ในบล็อกนี้
ข้อผิดพลาดทั่วไป #7: สมมติว่า $_POST
จะมีข้อมูล POST ของคุณเสมอ
แม้ว่าชื่อจะเป็นเช่นนั้น แต่อาร์เรย์ $_POST
จะไม่มีข้อมูล POST ของคุณเสมอไป และสามารถหาข้อมูลว่างได้โดยง่าย เพื่อให้เข้าใจสิ่งนี้ ลองมาดูตัวอย่างกัน สมมติว่าเราทำการร้องขอเซิร์ฟเวอร์ด้วยการเรียก jQuery.ajax()
ดังนี้:
// js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });
(โดยบังเอิญ โปรดสังเกต contentType: 'application/json'
ที่นี่ เราส่งข้อมูลเป็น JSON ซึ่งค่อนข้างเป็นที่นิยมสำหรับ API ซึ่งเป็นค่าเริ่มต้น ตัวอย่างเช่น สำหรับการโพสต์ในบริการ AngularJS $http
)
ที่ฝั่งเซิร์ฟเวอร์ของตัวอย่างของเรา เราเพียงแค่ดัมพ์อาร์เรย์ $_POST
:
// php var_dump($_POST);
น่าแปลกที่ผลลัพธ์จะเป็น:
array(0) { }
ทำไม? เกิดอะไรขึ้นกับสตริง JSON ของเรา {a: 'a', b: 'b'}
คำตอบคือ PHP แยกวิเคราะห์เฉพาะเพย์โหลด POST โดยอัตโนมัติเมื่อมีประเภทเนื้อหาของ application/x-www-form-urlencoded
หรือ multipart/form-data
เหตุผลของเรื่องนี้เป็นเรื่องในอดีต — เนื้อหาทั้งสองประเภทนี้เป็นประเภทเดียวที่ใช้เมื่อหลายปีก่อนเมื่อใช้งาน $_POST
ของ PHP ดังนั้นสำหรับเนื้อหาประเภทอื่นๆ (แม้แต่เนื้อหาที่ค่อนข้างเป็นที่นิยมในปัจจุบัน เช่น application/json
) PHP จะไม่โหลดเพย์โหลด POST โดยอัตโนมัติ
เนื่องจาก $_POST
เป็น superglobal หากเราแทนที่มัน หนึ่งครั้ง (ควรเป็นช่วงต้นของสคริปต์ของเรา) ค่าที่แก้ไข (เช่น รวมถึงเพย์โหลด POST) จะสามารถอ้างอิงได้ตลอดทั้งโค้ดของเรา นี่เป็นสิ่งสำคัญเนื่องจากโดยทั่วไปแล้ว $_POST
จะใช้โดยกรอบงาน PHP และสคริปต์ที่กำหนดเองเกือบทั้งหมดเพื่อแยกและแปลงข้อมูลคำขอ
ตัวอย่างเช่น เมื่อประมวลผลเพย์โหลด POST ด้วยประเภทเนื้อหาของ application/json
เราจำเป็นต้องแยกวิเคราะห์เนื้อหาคำขอด้วยตนเอง (เช่น ถอดรหัสข้อมูล JSON) และแทนที่ตัวแปร $_POST
ดังนี้:
// php $_POST = json_decode(file_get_contents('php://input'), true);
จากนั้นเมื่อเราดัมพ์อาร์เรย์ $_POST
เราจะเห็นว่ามีเพย์โหลด POST อย่างถูกต้อง เช่น:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
ข้อผิดพลาดทั่วไป #8: คิดว่า PHP รองรับประเภทข้อมูลอักขระ
ดูโค้ดตัวอย่างนี้แล้วลองเดาว่าโค้ดนี้จะพิมพ์อะไร:
for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }
หากคุณตอบ 'a' ถึง 'z' คุณอาจแปลกใจที่รู้ว่าคุณตอบผิด
ใช่ มันจะพิมพ์ 'a' ถึง 'z' แต่จากนั้น ก็ จะพิมพ์ 'aa' ถึง 'yz' ด้วย มาดูกันว่าทำไม
ใน PHP ไม่มีประเภทข้อมูล char
มีเฉพาะ string
เท่านั้น ด้วยเหตุนี้การเพิ่ม string
z
ใน PHP จะให้ผล aa
:
php> $c = 'z'; echo ++$c . "\n"; aa
เพื่อให้เกิดความสับสนมากขึ้น aa
เป็นศัพท์เฉพาะ น้อย กว่า z
:
php> var_export((boolean)('aa' < 'z')) . "\n"; true
นั่นเป็นสาเหตุที่โค้ดตัวอย่างที่แสดงด้านบนพิมพ์ตัวอักษร a
ถึง z
แต่พิมพ์ aa
ถึง yz
ด้วย จะหยุดเมื่อถึง za
ซึ่งเป็นค่าแรกที่พบว่า "มากกว่า" z
:
php> var_export((boolean)('za' < 'z')) . "\n"; false
ในกรณีนี้ นี่เป็นวิธีหนึ่งใน การ วนซ้ำค่า 'a' ถึง 'z' ใน PHP อย่างถูกต้อง:
for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }
หรืออีกทางหนึ่ง:
$letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }
ข้อผิดพลาดทั่วไป #9: ละเว้นมาตรฐานการเข้ารหัส
แม้ว่าการเพิกเฉยต่อมาตรฐานการเข้ารหัสไม่ได้นำไปสู่ความจำเป็นในการแก้ไขข้อบกพร่องของโค้ด PHP โดยตรง แต่ก็อาจเป็นหนึ่งในสิ่งสำคัญที่สุดที่จะกล่าวถึงในที่นี้
การเพิกเฉยต่อมาตรฐานการเข้ารหัสอาจทำให้เกิดปัญหามากมายในโครงการ อย่างดีที่สุด โค้ดนี้จะส่งผลให้โค้ดไม่สอดคล้องกัน (เนื่องจากนักพัฒนาซอฟต์แวร์ทุกคน "ทำสิ่งต่างๆ ของตนเอง") แต่ที่แย่ที่สุด มันสร้างโค้ด PHP ที่ไม่ทำงานหรืออาจเป็นเรื่องยาก (บางครั้งแทบจะเป็นไปไม่ได้เลย) ในการนำทาง ทำให้ยากต่อการดีบัก ปรับปรุง บำรุงรักษา และนั่นหมายถึงประสิทธิภาพการทำงานที่ลดลงสำหรับทีมของคุณ ซึ่งรวมถึงความพยายามที่สูญเปล่า (หรืออย่างน้อยก็ไม่จำเป็น) จำนวนมาก
โชคดีสำหรับนักพัฒนา PHP มีคำแนะนำมาตรฐาน PHP (PSR) ซึ่งประกอบด้วยห้ามาตรฐานต่อไปนี้:
- PSR-0: มาตรฐานการโหลดอัตโนมัติ
- PSR-1: มาตรฐานการเข้ารหัสพื้นฐาน
- PSR-2: คู่มือสไตล์การเข้ารหัส
- PSR-3: อินเทอร์เฟซผู้บันทึก
- PSR-4: ตัวโหลดอัตโนมัติ
PSR ถูกสร้างขึ้นโดยอิงจากปัจจัยการผลิตจากผู้ดูแลแพลตฟอร์มที่เป็นที่รู้จักมากที่สุดในตลาด Zend, Drupal, Symfony, Joomla และอื่นๆ มีส่วนสนับสนุนมาตรฐานเหล่านี้ และขณะนี้กำลังปฏิบัติตามมาตรฐานเหล่านี้ แม้แต่ PEAR ซึ่งพยายามจะเป็นมาตรฐานมาหลายปีก่อนหน้านั้น ก็เข้าร่วม PSR แล้ว
ในแง่หนึ่ง เกือบจะไม่สำคัญว่ามาตรฐานการเข้ารหัสของคุณคืออะไร ตราบใดที่คุณเห็นด้วยกับมาตรฐานและยึดมั่นในมาตรฐานนั้น แต่โดยทั่วไปแล้ว การปฏิบัติตาม PSR จะเป็นความคิดที่ดี เว้นแต่คุณจะมีเหตุผลที่น่าสนใจบางอย่างในโครงการของคุณที่จะทำอย่างอื่น . ทีมงานและโครงการต่างๆ สอดคล้องกับ PSR มากขึ้นเรื่อยๆ ในตอนนี้ Tt ได้รับการยอมรับว่าเป็นมาตรฐาน "มาตรฐาน" โดยนักพัฒนา PHP ส่วนใหญ่ ดังนั้นการใช้สิ่งนี้จะช่วยให้มั่นใจว่านักพัฒนาใหม่จะคุ้นเคยและคุ้นเคยกับมาตรฐานการเข้ารหัสของคุณเมื่อพวกเขาเข้าร่วมทีมของคุณ
ข้อผิดพลาดทั่วไป #10: ใช้ empty()
ในทางที่ผิด
นักพัฒนา PHP บางคนชอบใช้ empty()
สำหรับการตรวจสอบบูลีนสำหรับทุกอย่าง แม้ว่าจะมีบางกรณีที่สิ่งนี้อาจนำไปสู่ความสับสน
อันดับแรก กลับมาที่อาร์เรย์และอินสแตนซ์ ArrayObject
(ซึ่งเลียนแบบอาร์เรย์) เนื่องจากมีความคล้ายคลึงกัน จึงง่ายที่จะถือว่าอาร์เรย์และอินสแตนซ์ ArrayObject
จะทำงานเหมือนกัน อย่างไรก็ตาม สิ่งนี้พิสูจน์ให้เห็นว่าเป็นข้อสันนิษฐานที่อันตราย ตัวอย่างเช่น ใน PHP 5.0:
// PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?
และที่แย่ไปกว่านั้น ผลลัพธ์ที่ได้จะแตกต่างออกไปก่อน PHP 5.0:
// Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)
วิธีนี้ค่อนข้างเป็นที่นิยม ตัวอย่างเช่น นี่เป็นวิธีที่ Zend\Db\TableGateway
ของ Zend Framework 2 ส่งคืนข้อมูลเมื่อเรียกใช้ผลลัพธ์ current()
บน TableGateway::select()
ตามที่เอกสารแนะนำ นักพัฒนาสามารถตกเป็นเหยื่อของความผิดพลาดนี้ได้อย่างง่ายดายด้วยข้อมูลดังกล่าว
เพื่อหลีกเลี่ยงปัญหาเหล่านี้ วิธีที่ดีกว่าในการตรวจสอบโครงสร้างอาร์เรย์ว่างคือการใช้ count()
:
// Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)
และโดยบังเอิญเนื่องจาก PHP แปลง 0
false
จึงสามารถใช้ count()
ภายใน if ()
เงื่อนไขเพื่อตรวจสอบอาร์เรย์ที่ว่างเปล่า นอกจากนี้ ยังควรสังเกตด้วยว่าใน PHP นั้น count()
มีความซับซ้อนคงที่ (การทำงาน O(1)
) บนอาร์เรย์ ซึ่งทำให้ชัดเจนยิ่งขึ้นว่านี่เป็นทางเลือกที่เหมาะสม
อีกตัวอย่างหนึ่งเมื่อ empty()
อาจเป็นอันตรายได้เมื่อรวมกับฟังก์ชันคลาสเวทย์มนตร์ __get()
ลองกำหนดสองคลาสและมีคุณสมบัติ test
ในทั้งสอง
ขั้นแรก ให้กำหนดคลาส Regular
ที่มี test
เป็นคุณสมบัติปกติ:
class Regular { public $test = 'value'; }
จากนั้นมากำหนดคลาส Magic
ที่ใช้ตัวดำเนินการ magic __get()
เพื่อเข้าถึงคุณสมบัติ test
:
class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
ตกลง ตอนนี้เรามาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราพยายามเข้าถึงคุณสมบัติ test
ของแต่ละคลาสเหล่านี้:
$regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"
สบายดีจนถึงตอนนี้
แต่ตอนนี้เรามาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราเรียก empty()
กับแต่ละรายการเหล่านี้:
var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)
ฮึ. ดังนั้นหากเราพึ่งพา empty()
เราอาจถูกหลอกให้เชื่อว่าคุณสมบัติ test
ของ $magic
ว่างเปล่า ในขณะที่ในความเป็นจริงมันถูกตั้งค่าเป็น 'value'
น่าเสียดาย หากคลาสใช้ฟังก์ชัน magic __get()
เพื่อดึงค่าของคุณสมบัติ ไม่มีทางที่จะตรวจสอบได้ว่าค่าคุณสมบัตินั้นว่างเปล่าหรือไม่ นอกขอบเขตของคลาส คุณสามารถตรวจสอบได้จริงๆ เท่านั้นว่าจะมีการคืน null
หรือไม่ และนั่นไม่ได้หมายความว่าจะไม่มีการตั้งค่าคีย์ที่เกี่ยวข้อง เนื่องจากจริง ๆ แล้ว สามารถ ตั้งค่า เป็น null
ได้
ในทางตรงกันข้าม หากเราพยายามอ้างอิงคุณสมบัติที่ไม่มีอยู่จริงของอินสแตนซ์คลาส Regular
เราจะได้รับการแจ้งเตือนในลักษณะดังต่อไปนี้:
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0
ดังนั้นประเด็นหลักในที่นี้คือ ควรใช้เมธอด empty()
อย่างระมัดระวัง เนื่องจากอาจทำให้สับสน หรือแม้กระทั่งอาจทำให้เข้าใจผิดได้ หากไม่ระมัดระวัง
สรุป
ความง่ายในการใช้งานของ PHP สามารถกล่อมนักพัฒนาให้รู้สึกสบายตัว โดยปล่อยให้ตัวเองเสี่ยงต่อการดีบัก PHP ที่ยาวนานเนื่องจากความแตกต่างและลักษณะเฉพาะของภาษา ซึ่งอาจส่งผลให้ PHP ไม่ทำงานและปัญหาดังที่อธิบายไว้ในที่นี้
ภาษา PHP มีการพัฒนาอย่างมากตลอดระยะเวลา 20 ปีของประวัติศาสตร์ การทำความคุ้นเคยกับรายละเอียดปลีกย่อยเป็นความพยายามที่คุ้มค่า เนื่องจากจะช่วยให้แน่ใจว่าซอฟต์แวร์ที่คุณผลิตนั้นสามารถปรับขนาดได้ แข็งแกร่ง และสามารถบำรุงรักษาได้มากขึ้น