iT邦幫忙

2019 iT 邦幫忙鐵人賽

0
Software Development

Laravel 原始碼分析系列 第 36

array_get()、data_get() 與 object_get() 的差異

這三個都是 Laravel 所提供的 helpers 函式。剛好今天聽到有人提到這個問題,所以就來翻看看。

單就註解與介面來看這三個函式:

/**
 * Get an item from an array using "dot" notation.
 */
function array_get($array, $key, $default = null);

/**
 * Get an item from an array or object using "dot" notation.
 */
function data_get($target, $key, $default = null);

/**
 * Get an item from an object using "dot" notation.
 */
function object_get($object, $key, $default = null);

可以約略知道 array_get()object_get() 分別在處理 array 與 object,而 data_get() 則是混合的。

array_get()

接著來看 array_get() 原始碼:

function array_get($array, $key, $default = null)
{
    return Arr::get($array, $key, $default);
}

它是把任務交付給 Arr 類別處理的

會這樣做有兩種可能:一種是因為 4.1 版以前,並沒有 Arr 類別,這是為了符合過去的習慣所留下來的,但這個可能性較小。較有可能是因為 array 處理越來越複雜,為了讓檔案可以 SRP,所以就另外寫了一個靜態類別來處理。

public static function get($array, $key, $default = null)
{
    // 如果不是一個可用的 array,就回傳預設值
    if (! static::accessible($array)) {
        return value($default);
    }

    // key 是 null 的話,就回傳整個 array 回去
    if (is_null($key)) {
        return $array;
    }

    // 當存在就回傳
    if (static::exists($array, $key)) {
        return $array[$key];
    }
    
    // 如果找不到 `.` 的話,而直接存取也沒值,那就回傳 default 值
    if (strpos($key, '.') === false) {
        return $array[$key] ?? value($default);
    }
    
    // 如果有 `.` 的話,就一階一階找看看。
    foreach (explode('.', $key) as $segment) {
        if (static::accessible($array) && static::exists($array, $segment)) {
            $array = $array[$segment];
        } else {
            return value($default);
        }
    }
    
    // 把最後找到的結果回傳
    return $array;
}

public static function accessible($value)
{
    // 可用的 array,要嘛是原生 array,不然就是實作了 ArrayAccess
    return is_array($value) || $value instanceof ArrayAccess;
}

public static function exists($array, $key)
{
    // ArrayAccess 實例
    if ($array instanceof ArrayAccess) {
        return $array->offsetExists($key);
    }

    // 原生 array
    return array_key_exists($key, $array);
}

這裡面不意外的,都是使用 array 的方法在取內容。

object_get()

類似地,object_get() 則是對 object 型態的變數取資料:

function object_get($object, $key, $default = null)
{
    // 如果 key 是空的,就把整個 object 回傳
    if (is_null($key) || trim($key) == '') {
        return $object;
    }
    
    // 依續把每個階層的 key,用來取得 object 的屬性
    foreach (explode('.', $key) as $segment) {
        // 如果不是 object 或屬性不存在的時候,就回傳預設值
        if (! is_object($object) || ! isset($object->{$segment})) {
            return value($default);
        }
        
        // 重設 object 為下一個階層
        $object = $object->{$segment};
    }
    
    // 最後取得的 property 即結果
    return $object;
}

整個過程都是使用 object 的取屬性方法(->)。

data_get()

array_get() 必須要所有階層都是 array 或 ArrayAccess 實例,才能正常地使用。object_get() 則要每一階層都是 object。

如果是 array 包 object 或相反,就需要靠 data_get() 了。因為它除了同時支援兩種取資料方法,還另外實作了 * 的取資料方法,所以原始碼也比較複雜的:

function data_get($target, $key, $default = null)
{
    // key 是空的就直接回傳
    if (is_null($key)) {
        return $target;
    }

    // key 除了用 `.` 區隔外,也可以使用 array
    $key = is_array($key) ? $key : explode('.', $key);

    // 將 key 一個一個拿出來跑 
    while (! is_null($segment = array_shift($key))) {
        // 如果是 * 的話
        if ($segment === '*') {
            if ($target instanceof Collection) {
                // 是 Collection 的話,把裡面的 item 拿出來
                $target = $target->all();
            } elseif (! is_array($target)) {
                // 如果不是 Collection,也不是 array 的話,代表取資料的 key 有問題,直接回預設值
                return value($default);
            }

            // 依 key 取得裡面的 value,然後重組成 array
            $result = Arr::pluck($target, $key);

            // 如果剩下的 key 還有 * 的話,就使用 collapse 把結果打平成一維 array,如果沒有 * 的話就直接回傳
            return in_array('*', $key) ? Arr::collapse($result) : $result;
        }

        if (Arr::accessible($target) && Arr::exists($target, $segment)) {
            // 如果 $target 是個 array 且有資料的話,就使用 array 方法取資料
            $target = $target[$segment];
        } elseif (is_object($target) && isset($target->{$segment})) {
            // 如果 $target 是個 object 且有資料的話,就使用 object 方法取資料
            $target = $target->{$segment};
        } else {
            都不是的話,key 是有問題的,只好回傳預設值
            return value($default);
        }
    }
    
    // 回傳最後取到的 target
    return $target;
}

* 的用法,比方說在分析 Routing(5)曾提到 addToCollections() 會建立查詢表:

$domainAndUri = $route->getDomain().$route->uri();

foreach ($route->methods() as $method) {
    $this->routes[$method][$domainAndUri] = $route;
}

下面是應用方法:

// 取得所有 GET 方法的 route,並轉換成一維陣列
data_get($this->routes, 'get.*');

// 取得所有 URI 是 `/user` 的 route,並轉換成一維陣列
data_get($this->routes, '*./user');

三種方法都可以像 Javascript 使用 . 來取得多維陣列或 object 的取,像 config() 在取設定的時候,正是使用這些方法。

不過筆者平常如果要對一堆資料操作的話,還是都會選擇使用 Collection,未來有機會再來翻翻它的原始碼。


上一篇
自定義 bootstrapper
下一篇
分析 Collection(1)
系列文
Laravel 原始碼分析46

尚未有邦友留言

立即登入留言