Buggy PHP Code: 10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา PHP Make

เผยแพร่แล้ว: 2022-03-11

PHP ทำให้ง่ายต่อการสร้างระบบบนเว็บ ซึ่งเป็นสาเหตุส่วนใหญ่ที่ทำให้ความนิยมของระบบนี้ แต่ความง่ายในการใช้งานของมัน ถึงแม้ว่า 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 ปีของประวัติศาสตร์ การทำความคุ้นเคยกับรายละเอียดปลีกย่อยเป็นความพยายามที่คุ้มค่า เนื่องจากจะช่วยให้แน่ใจว่าซอฟต์แวร์ที่คุณผลิตนั้นสามารถปรับขนาดได้ แข็งแกร่ง และสามารถบำรุงรักษาได้มากขึ้น