Sai lầm thứ nhất: không xoá reference sau vòng lặp foreach
Sử dụng reference là đặc biệt hữu ích khi bạn muốn thao tác với phần tử trong mảng
1 2 3 4 5 |
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr bây giờ là (2, 4, 6, 8) |
Nếu không cẩn thận bạn rất dễ gây ra side-effect không mong muốn ở đây. Trong ví dụ này, sau khi vòng lặp foreach chạy xong, value vẫn giữ nguyên scope và đang là reference tới phần tử cuối cùng của mảng arr. Những dòng lệnh sau sử dụng value sẽ rất dễ sinh bug ở đây. Cần phải nhớ là trong php vòng lặp foreach không tạo ra scope mới. Bởi vậy, biến value hiện đang giữ reference đang ở toàn cục ngay cả sau vòng foreach.
Hãy xem ví dụ dưới đây:
1 2 3 4 5 6 7 8 |
$array = [1, 2, 3]; echo implode(\',\', $array), "\n"; //1,2,3 foreach ($array as &$value) {} // reference echo implode(\',\', $array), "\n"; //1,2,3 foreach ($array as $value) {} // copy echo implode(\',\', $array), "\n"; //1,2,2 |
Giá trị sau cùng in ra là 1, 2, 2 chứ không như bạn mong đợi phải không? Chúng ta cùng phân tích điều gì đã xảy ra trong ví dụ này. Sau vòng foreach đầu tiên, mảng vẫn giữ nguyên nhưng lúc này biến toàn cục value đang là reference tới arr[2]. Vòng foreach thứ 2, không sử dụng reference, các giá trị value sẽ được copy từ array.
Vòng lặp đầu tiên, value nhận giá trị copy từ arr[0] = 1, vì value là reference tới arr[2] nên arr[2] = 1. Kết quả sau vòng đầu : arr = [1,2,1].
Vòng lặp thứ 2, value nhận giá trị copy từ arr[1] = 2, vì value là reference tới arr[2] nên arr[2] = 2. Kết quả sau vòng đầu : arr = [1,2,2].
Vòng lặp thứ 3, value nhận giá trị copy từ arr[2] = 2, vì value là reference tới arr[2] nên arr[2] = 2. Kết quả sau vòng đầu : arr = [1,2,2].
Để tránh mắc lỗi này, các bạn cần nhớ giải phóng biến reference sau vòng lặp foreach.
1 2 3 4 5 6 |
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value không còn là reference tới $arr[3] |
Sai lầm thứ 2: nhầm lẫn về hàm isset()
Hàm isset() trả về false không chỉ trong trường hợp biến không tồn tại mà còn cả trong trường hợp biến là null nữa. Điều tưởng như nhỏ bé này cũng thường dẫn đến bug khó lường.
1 2 3 4 5 6 7 8 9 10 |
if ($_POST[\'active\']) { //Lấy giá trị từ biến siêu toàn cục $_POST $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo \'Không tồn tại trạng thái active\'; } |
Hàm isset(postData) trả về false ngay cả trong trường hợp nó postData = null. Màn hình in ra chữ “Không tồn tại trạng thái active” ngay cả khi nó có trong super global. Để giải quyết chúng ta sẽ phải check lại sự tồn tại của $_POST[\’active\’]. Trong một vài trường hợp khác hãy nhớ tới hàm array_key_exists().
Sai lầm thứ 3: nhầm lẫn về reference và giá trị khi trả về kết quả của một hàm
Xem ví dụ sau đây:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()[\'test\'] = \'test\'; echo $config->getValues()[\'test\']; |
Thoạt nhìn thì không có vấn đề gì nhưng khi chạy thì trả ra lỗi sau:
Notice: Undefined index: test in /path/test.php on line 14
Đó là bởi vì PHP nhầm lẫn giữa trả về mảng bằng refernce và trả về mảng bằng giá trị. Trừ khi bạn sử dụng toán tử & – điều chẳng mấy ai nghĩ tới trong trường hợp này. PHP sẽ mặc định trả về mảng nhưng là dưới dạng value. Điều đó nghĩa là 1 bản copy của mảng sẽ được trả về và dĩ nhiên 2 function getValue() đang access tới 2 instance khác nhau => khá là củ chuối nhỉ.
Khắc phục bằng cách thao tác trên bản copy của mảng trả về thôi.
1 2 3 |
$vals = $config->getValues(); $vals[\'test\'] = \'test\'; echo $vals[\'test\']; |
Nhưng nếu như bạn muốn thao tác trực tiếp trên mảng gốc thì bắt buộc phải sử dụng đến toán tử reference
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Config { private $values = []; // Trả về reference tới property values public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()[\'test\'] = \'test\'; echo $config->getValues()[\'test\']; |
Vấn đề vẫn sẽ chưa dừng lại ở đó, hãy đánh giá đoạn code sau đây
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Config { private $values; // Sử dụng 1 đối tượng 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\']; |
Lần này code lại chạy ngon, đó là vì PHP luôn return Object dưới dạng Identifier Reference & Object in PHP. Và tất nhiên dù là instance trả về khác nhau nhưng nó vẫn mang 1 identifier trỏ tới đúng Object trong bộ nhớ.
Vậy nhưng không phải là cứ nên trả về 1 mảng dưới dạng ArrayObject để tránh phiền toái, làm thế này vô tình bạn để lộ các thuộc tính private của đối tượng và đi ngược lại với nguyên tắc bao đóng của OOP.
Practice tốt nhất trong trường hợp này là sử dụng getter và setter theo cách truyền thống
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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'); // In ra 'testValue' |
Sai lầm thứ 4: Thực hiện truy vấn trong vòng lặp
Không hiếm khi bạn sẽ gặp đoạn code giống như thế này:
1 2 3 4 5 |
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); } |
Mặc dù hoàn toàn không có gì sai ở đây, nhưng nếu bạn để ý logic trong code, bạn có thể thấy rằng giá trị $valueRepository->findByValue()
đang được gọi một cách vô tội vạ, cuối cùng kết quả của truy vấn của một số thứ, sẽ giống như thế này:
$result = $connection->query("SELECT
x,
yFROM
valuesWHERE
value=" . $inputValue);
Kết quả là, mỗi lần lặp của vòng lặp ở trên sẽ dẫn đến một truy vấn đến cơ sở dữ liệu. Ví dụ, nếu bạn lặp một mảng có 1000 giá trị, nó sẽ tạo ra 1000 truy vấn riêng biệt cho cơ sở dữ liệu. Nếu như việc này được gọi trong nhiều threads, nó có thể làm cho hệ thống bị ngừng hoạt động.
Do đó cần phải xác định khi nào các truy vấn được thực hiện trong code của bạn. Bất cứ khi nào có thể, hãy thu thập các giá trị và sau đó chạy một truy vấn để lấy ra tất cả các kết quả.
Một ví dụ về một chỗ khá phổ biến để gặp phải việc thực hiện truy vấn không hiệu quả (hay truy vấn trong vòng lặp) là với danh sách các giá trị (ví dụ ID). Sau đó, để lấy dữ liệu đầy đủ cho mỗi ID, code sẽ lặp qua array và thực hiện truy vấn SQL riêng cho mỗi ID. Giống thế này:
1 2 3 4 5 |
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); } |
Nhưng cùng một vấn đề như vậy có thể được giải quyết bằng cách hiệu quả hơn nhiều:
1 2 3 4 5 6 7 |
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } } |
Do đó, điều quan trọng để nhận ra các truy vấn đang được thực hiện, hoặc trực tiếp hoặc gián tiếp, đó là bằng code của bạn. Bất cứ khi nào có thể, hãy thu thập các giá trị sau đó chạy một truy vấn để lấy tất cả các kết quả. Tuy nhiên, phải thận trọng khi thực hiện điều đó, nó có thể dẫn chúng ta tới một sai lầm phổ biến tiếp theo.
Hy vọng, với bài viết này các bạn có thể chú ý hơn đến những vấn đề trong PHP, để tránh những sai lầm không đáng có.