PHP - 10 lỗi phổ biến mà các lập trình viên PHP thường mắc phải (Phần 1)

Index

  1. PHP - 10 lỗi phổ biến mà các lập trình viên PHP thường mắc phải Part 1
  2. PHP - 10 lỗi phổ biến mà các lập trình viên PHP thường mắc phải Part 2
  3. PHP - 10 lỗi phổ biến mà các lập trình viên PHP thường mắc phải Part 3

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

$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:

$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.

$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.

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:

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.

$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

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

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.

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'

Tạm thời là vậy, hẹn các bạn ở bài sau.

Reference

https://www.toptal.com/php/10-most-common-mistakes-php-programmers-make