Laravel5.5+ 使用API Resources快速輸出自定義JSON方法詳解
從Laravel 5.5+開始,加入了API Resources這個概念。
我們先來看一下官網如何定義這個概念的:
When building an API,you may need a transformation layer that sits between your Eloquent models and the JSON responses that are actually returned to your application's users. Laravel's resource classes allow you to expressively and easily transform your models and model collections into JSON.
可能看完這個概念之後,你仍然有點不明白,畢竟這個定義說的有點含糊。
如果你熟悉使用API進行輸出,構架前後端分離的網路應用,那麼你應該會發現,當我們使用Eloquent從資料庫中取出資料後,如果想以JSON格式進行輸出,那麼我們可以使用->toJson()這個方法,這個方法可以直接將我們的model序列化(這個方法從Laravel 5.1+開始就可以使用了):
$user = App\User::find(1); return $user->toJson();
使用多了,我們會發現,在model較為複雜,或者model中有很多我們API輸出可能用不到的欄位的情況下,toJson()仍然會忠實地幫我們把這些欄位序列化出來。
這個時候,我們會想,如何將model中的某些欄位隱藏起來,不輸出到JSON中。另外一種情況,比如欄位是password等一些敏感資訊的時候,我們不希望JSON資料裡包含這樣的敏感資訊。
要解決這個問題,我們可以在model裡定義$hidden或者$visible這兩個陣列來進行欄位的隱藏或者顯示:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 不希望在序列化中出現的欄位放入該陣列中 * * @var array */ protected $hidden = ['password','some','secret']; }
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 只有在以下陣列中出現的欄位會被序列化 * * @var array */ protected $visible = ['first_name','last_name']; }
那麼你可能會想,我們已經有了可以自動序列化的方法,以及可以隱藏或者顯示指定欄位的方法,這樣不就足夠了嗎?
現在我們來假設一個簡單的應用場景。假設我們在輸出一個客戶列表,裡面包含了客戶名字和送貨地址。我們使用Customer這個model定義客戶,使用ShippingAddress這個model進行定義送貨地址。為了簡化場景,我們的客戶只有一個送貨地址,所以只會出現一一對應的情況。
那麼在ShippingAddress對應的資料庫表shipping_addresses中,我們可能會有如下定義:
| id | country_id | province_id | city_id | address |
欄位型別我就不贅述了,其中country_id、province_id以及city_id這三個外來鍵分別對應了國家、省份以及城市表中的id。
而Customer對應的customers表中,會有shipping_address_id這個外來鍵指向shipping_addresses表中的id。
那麼我們要輸出顧客和送貨地址,我們需要先在model中定義好relationship:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Customer extends Model { public function shippingAddress() { return $this->belongsTo(ShippingAddress::class); } }
<?php namespace App; use Illuminate\Database\Eloquent\Model; class ShippingAddress extends Model { public function country() { return $this->belongsTo(Country::class); } public function province() { return $this->belongsTo(Province::class); } public function city() { return $this->belongsTo(City::class); } }
在我們的控制器中,我們拉取出所有客戶:
<?php namespace App\Http\Controllers; use App\Customer; use App\Http\Controllers\Controller; class CustomerController extends Controller { /** * Simple function to fetch all customers with their shipping addresses * * @return String */ public function index() { $customers = Customer::with(['shippingAddress','shippingAddress.country','shippingAddress.province','shippingAddress.city'])->get(); //這裡可以直接返回Eloquent Collections或Objects,toJson()將自動被呼叫 return $customers; } }
那麼輸出的JSON將會包含了多個層級的關係,那麼在我們前端呼叫的時候,將會非常麻煩,因為我們需要一層一層剝開Object關係。
但是如果你熟悉Laravel,你可能會說,慢著!這個情況我可以用accessor不就完事兒了嗎?
是的,我們確實可以使用accessor來簡化我們的資料層級:
/** * Get the customer's full shipping address * * @return string */ public function getFullShippingAddressAttribute() { return "{$this->shippingAddress->country->name} {$this->shippingAddress->province->name} {$this->shippingAddress->city->name} {$this->shippingAddress->address}"; }
但是我們還需要一步操作。由於customers這張表本身沒有full_shipping_address這個欄位,要使我們的JSON輸出包含full_shipping_address,我們需要新增$appends陣列:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Customer extends Model { /** * The accessors to append to the model's array form. * * @var array */ protected $appends = ['full_shipping_address']; }
對於每一個我們想自定義的JSON欄位,我們都需要進行上面兩部的操作。這樣一來其實非常麻煩,並且不利於程式碼的維護,因為這會讓原本簡潔的model顯得很複雜。
基於以上原因,我們需要一箇中間層,在我們輸出model成為JSON的時候,可以進行一次資訊的過濾及加工。
那麼還是使用我們上面的應用場景。要輸出自定義的欄位再簡單不過了。我們不需要在model裡定義各種accessor,也不需要使用黑白名單過濾欄位,只需要新建一個Resource類:
$ php artisan make:resource Customer
然後我們可以看到,在app/Http資料夾下,多出了一個名為Resources資料夾下,其中含有一個名為Customer.php的檔案:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class Customer extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return parent::toArray($request); } }
這裡我們看到其中有且僅有一個名為toArray的方法。這就是我們要自定欄位的地方:
public function toArray($request) { return [ 'fullName' => $this->first_name . $this->last_name,'fullShippingAddress' => $this->shippingAddress->country->name . $this->shippingAddress->province->name . $this->shippingAddress->city->name . $this->shippingAddress->address,]; }
注意到,無論是fullName還是fullShippingAddress,都是不存在於customers表中的欄位。
接著,我們只需要簡單修改一下我們的控制器:
<?php namespace App\Http\Controllers; use App\Customer; use App\Http\Resources\Customer as CustomerResource; use App\Http\Controllers\Controller; class CustomerController extends Controller { /** * Simple function to fetch all customers with their shipping addresses * * @return String */ public function index() { $customers = Customer::with(['shippingAddress','shippingAddress.city'])->get(); //這裡我們使用了新的Resource類 return CustomerResource::collection($customers); } }
這樣就OK了!我們輸出的JSON資料中,將會僅僅含有以上兩個欄位,即fullName和fullShippingAddress,非常乾淨,並且前端直接可用,不需要二次再加工。
唯一需要注意的是,這裡由於我們拉取了多個Customer,所以我們用了每個Resource類都自帶有的collection方法,將一個Collection中的所有物件都進行處理。而若要處理單個物件,我們需要使用以下程式碼:
public function show($id) { $customer = Customer::findOrFail($id); return new CustomerResource($customer); }
要了解更多關於API Resources的詳情,請戳官網文件:
https://laravel.com/docs/5.7/eloquent-resources
本文主要講解了Laravel5.5+ 使用API Resources快速輸出自定義JSON方法詳解,更多關於Laravel框架的使用技巧請檢視下面的相關連結