Laravel裡的那些坑 - Optional

李銘昕發表於2020-08-30

示例倉庫

Github倉庫

github.com/hyperf/hyperf 不要忘了給我一個 Star,謝謝

什麼是 Optional

先讓我們看一段 NodeJS 的程式碼

var obj = null;

console.log(obj?.id);

上述程式碼會輸出 undefined,如果去掉中間的 ?,則會丟擲一個 TypeError

console.log(obj.id);
                ^

TypeError: Cannot read property 'id' of null

Optional 就是 PHP 中的一個封裝

Laravel 中的實現

我們可以直接在 Laravel 框架中執行以下程式碼

<?php
dump(optional(null)->id);

會輸出 null

但當我們仔細看一下原始碼,其實實現是有坑的,按照正常的設計我們編寫以下程式碼進行測試

$obj = (object)['id' => 1];
dump(isset(optional($obj)->id)); // true
dump(optional($obj)->id); // 1
dump(isset(optional($obj)['id'])); // false
dump(optional($obj)['id']); // false

$obj = ['id' => 1];
dump(isset(optional($obj)->id)); // true
dump(optional($obj)->id); // null
dump(isset(optional($obj)['id'])); // true
dump(optional($obj)['id']); // 1

我們會發現當入參是陣列的時候,我判斷當前是否存在 id,結果是存在,但去拿值的時候,卻是 null

然後讓我們看一下原始碼,以下只展示相關的程式碼片段。

<?php

namespace Illuminate\Support;

use ArrayAccess;
use ArrayObject;

class Optional implements ArrayAccess
{
    /**
     * Dynamically access a property on the underlying object.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        if (is_object($this->value)) {
            return $this->value->{$key} ?? null;
        }
    }

    /**
     * Dynamically check a property exists on the underlying object.
     *
     * @param  mixed  $name
     * @return bool
     */
    public function __isset($name)
    {
        if (is_object($this->value)) {
            return isset($this->value->{$name});
        }

        if (is_array($this->value) || $this->value instanceof ArrayObject) {
            return isset($this->value[$name]);
        }

        return false;
    }
}

可見,在判斷是否存在成員變數和獲取成員變數的邏輯完全不一致。這才導致了這個問題。

這段程式碼,是後來其他人提交上去的 PR,所以我猜測,一開始的設計是,object 是 object ,array 是 array。二者是不能混用的,所以下面這段程式碼其實不應該被新增進來。

if (is_array($this->value) || $this->value instanceof ArrayObject) {
    return isset($this->value[$name]);
}

而在官方的單元測試中,__isset 的單測對 array 的情況已經覆蓋到了,而 __get 也是一樣,這就導致無論是刪除這段程式碼,還是新增這段程式碼到 __get 上,都會導致框架 BC

相關 PR

所以只能希望 Laravel 8.0 會修改這個問題了。

Hyperf 中的實現

Hyperf 框架對這麼好用的東西已經做了移植,並解決了這個問題。

我們在 Hyperf 框架中編寫以下測試

$obj = (object)['id' => 1];
dump(isset(optional($obj)->id));
dump(optional($obj)->id);
dump(isset(optional($obj)['id']));
dump(optional($obj)['id']);

$obj = ['id' => 1];
dump(isset(optional($obj)->id));
dump(optional($obj)->id);
dump(isset(optional($obj)['id']));
dump(optional($obj)['id']);

可以看到輸出是和我們的預想一致的

true
1
false
null
false
null
true
1
本作品採用《CC 協議》,轉載必須註明作者和本文連結
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

相關文章