啥?從新學習 Laravel Eloquent:訪問器?爲何要從新學習這玩意?php
最近有反應說客戶列表頁面反應較慢,我測試了一下,使用體驗確實不好,特別慢。後來查日誌才知道,是在循環體中使用了一個定義好的一個訪問器,這個訪問器訪問了數據庫,可是相關數據庫是作了關聯預查詢的,因此這種狀況的發生是異常的。前端
那麼問題來了,爲何呢?laravel
帶着這樣的疑問,我決定忘記全部,從一個小白的態度,從新學習一下 Laravel Eloquent:訪問器。sql
開始實驗以前,對這一塊的使用方法還不瞭解的建議先看文檔——Eloquent: 訪問器瞭解大概數據庫
這裏先描述本次實驗的大體狀況json
- version: Laravel5.5
- Model:
customer
->customer_tags
一對多customer_tags
->tags
一對一- 需求:
- 將每一個客戶全部的
tag
轉成字符串用/
隔開返回
咱們須要一個在控制器中定義一個
function
處理請求返回數據。定義一個訪問器來實現需求返回給前端數組
脫敏,去除無關字段。緩存
responseSuccess()
返回給前端前對數據進行格式處理bash
iteratorGet()
從一個數組或者對象中獲取一個元素,若是沒有就返回 nullapp
// helpers.php
//對返回給前端前對數據進行格式處理
function responseSuccess($data = [], $message = '操做成功') {
$res = [
'msg' => $message,
'code' => 200,
'data' => $data
];
//分頁特殊處理
if ($data instanceof Paginator) {
$data = $data->toArray();
$page = [
'current_page' => $data['current_page'],
'last_page' => $data['last_page'],
'per_page' => $data['per_page'],
'total' => $data['total']
];
$res['data'] = $data['data'];
$res['pages'] = $page;
}
return response()->json($res)->setStatusCode(200);
}
// 從一個數組或者對象中獲取一個元素,若是沒有就返回null
function iteratorGet($iterator, $key, $default = null) {
//代碼省略,見諒
...
}
複製代碼
定義訪問器,將當前客戶全部的
tag
轉成字符串用/
隔開返回
// App/Models/Customer
public function getTestTagAttribute() {
$customerTags = iteratorGet($this, 'customerTags', []);
$tags = [];
foreach ($customerTags as $customerTag) {
$tags[] = iteratorGet($customerTag->tag, 'name');
}
return implode('/', $tags);
}
複製代碼
處理請求返回數據
// CustomerController
public function testCustomer() try {
$beginTime = microtime(true);
/** @var Collection $customers */
$customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
$endTime = microtime(true);
\Log::info($endTime - $beginTime);
return responseSuccess($customers);
} catch (\Exception $e) {
errorLog($e);
return responseFailed($e->getMessage());
}
}
複製代碼
返回的字段中並無咱們想要的數據
{
"msg": "操做成功",
"code": 200,
"data": [
{
"id": 92424,
"customer_tags": [
{
"id": 1586,
"customer_id": 92424,
"tag_id": 1,
"tag": {
"id": 1,
"name": "年齡過小",
}
},
{
"id": 1588,
"customer_id": 92424,
"tag_id": 2,
"tag": {
"id": 2,
"name": "零基礎",
}
},
{
"id": 1587,
"customer_id": 92424,
"tag_id": 10,
"tag": {
"id": 10,
"name": "年齡過大",
}
}
]
},
{
"id": 16,
"customer_tags": []
},
...
]
}
複製代碼
爲何會沒有呢?難道定義的訪問器並不能訪問數據?仍是說沒有被調用呢? 咱們在 tinker
中查詢一個 Customer
,看一下 Customer
打印的結果
>>> $c = Customer::find(92424);
=> App\Models\Customer {#3493
id: 92424,
category: 0,
name: "sadas",
}
>>> $c->test_tag
=> "年齡過小/零基礎/年齡過大"
>>> $c
=> App\Models\Customer {#3493
id: 92424,
category: 0,
name: "sadas",
customerTags: Illuminate\Database\Eloquent\Collection {#3487
all: [
App\Models\CustomerTag {#3498
id: 1586,
customer_id: 92424,
tag_id: 1,
tag: App\Models\Tag {#3504
id: 1,
name: "年齡過小",
},
},
App\Models\CustomerTag {#3499
id: 1588,
customer_id: 92424,
tag_id: 2,
tag: App\Models\Tag {#211
id: 2,
name: "零基礎",
},
},
App\Models\CustomerTag {#3500
id: 1587,
customer_id: 92424,
tag_id: 10,
tag: App\Models\Tag {#3475
id: 10,
name: "年齡過大",
},
},
],
},
}
複製代碼
Customer
中並無與 test_tags
屬性,也沒有相關信息。爲何咱們執行 $c->test_tag
是能夠執行咱們定義的訪問器呢?
不要着急,慢慢回顧一下 php
的 oop
,咱們都知道 php
有不少的魔術方法。
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(),__invoke(), __set_state(), __clone() 和 __debugInfo() 等方法在 PHP 中被稱爲魔術方法(Magic methods)。在命名本身的類方法時不能使用這些方法名,除非是想使用其魔術功能。
Caution PHP 將全部以 **(兩個下劃線)開頭的類方法保留爲魔術方法。因此在定義類方法時,除了上述魔術方法,建議不要以 ** 爲前綴。
讀取不可訪問屬性的值時,__get() 會被調用。
因此這裏咱們查看 laravel
的源碼,看一下 Cusomer
所繼承的 Model
對象中,對 __get()
的定義
namespace Illuminate\Database\Eloquent;
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable {
use Concerns\HasAttributes,
Concerns\HasEvents,
Concerns\HasGlobalScopes,
Concerns\HasRelationships,
Concerns\HasTimestamps,
Concerns\HidesAttributes,
Concerns\GuardsAttributes;
/** * Dynamically retrieve attributes on the model. * * @param string $key * @return mixed */
public function __get($key) {
return $this->getAttribute($key);
}
/** * Dynamically set attributes on the model. * * @param string $key * @param mixed $value * @return void */
public function __set($key, $value) {
$this->setAttribute($key, $value);
}
}
複製代碼
getAttribute()
不在 Model
對象中定義,在\Illuminate\Database\Eloquent\Concerns\HasAttributes
中定義,咱們看一下。
Str::studly()
是將字符串轉化成大寫字母開頭的駝峯風格字符串
namespace Illuminate\Database\Eloquent\Concerns;
trait HasAttributes
{
/** * The model's attributes. * 模型的屬性 * * @var array */
protected $attributes = [];
/** * Get an attribute from the model. * * @param string $key * @return mixed */
public function getAttribute($key) {
if (! $key) {
return;
}
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
// 檢測key是模型的屬性之一或者key有對應定義的訪問器,知足條件獲取key對應的值
if (array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key)) {
return $this->getAttributeValue($key);
}
// Here we will determine if the model base class itself contains this given key
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return;
}
//獲取[關聯關係relation]的值
return $this->getRelationValue($key);
}
/** * Determine if a get mutator exists for an attribute. * 檢查一個key是否存在對應定義的訪問器 * @param string $key * @return bool */
public function hasGetMutator($key) {
return method_exists($this, 'get'.Str::studly($key).'Attribute');
}
/** * Get a plain attribute (not a relationship). * 獲取key對應的值 * @param string $key * @return mixed */
public function getAttributeValue($key) {
//從已有元素中獲取一個key對應的值
$value = $this->getAttributeFromArray($key);
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
//檢查一個key是否存在對應定義的訪問器,知足條件就返回對應訪問器方法返回的值
// (注意這裏會傳一個參數給對應的方法,參數的值爲從已有元素中獲取一個key對應的值)
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// If the attribute exists within the cast array, we will convert it to
// an appropriate native PHP type dependant upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}
// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if (in_array($key, $this->getDates()) &&
! is_null($value)) {
return $this->asDateTime($value);
}
return $value;
}
/** * Get an attribute from the $attributes array. * 從已有元素中獲取一個key對應的值 * @param string $key * @return mixed */
protected function getAttributeFromArray($key) {
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}
}
/** * Get the value of an attribute using its mutator. * 返回對應訪問器方法返回的值(注意這裏會傳一個參數給對應的方法) * @param string $key * @param mixed $value * @return mixed */
protected function mutateAttribute($key, $value) {
return $this->{'get'.Str::studly($key).'Attribute'}($value);
}
/** * Set a given attribute on the model. * 給對象設置一個屬性 * @param string $key * @param mixed $value * @return $this */
public function setAttribute($key, $value) {
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
// 先檢查有沒有定義修改器
if ($this->hasSetMutator($key)) {
$method = 'set'.Str::studly($key).'Attribute';
return $this->{$method}($value);
}
// If an attribute is listed as a "date", we'll convert it from a DateTime
// instance into a form proper for storage on the database tables using
// the connection grammar's date format. We will auto set the values.
elseif ($value && $this->isDateAttribute($key)) {
$value = $this->fromDateTime($value);
}
if ($this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}
// If this attribute contains a JSON ->, we'll set the proper value in the
// attribute's underlying array. This takes care of properly nesting an
// attribute in the array's value in the case of deeply nested items.
if (Str::contains($key, '->')) {
return $this->fillJsonAttribute($key, $value);
}
$this->attributes[$key] = $value;
return $this;
}
}
複製代碼
看了源碼之後咱們就很清楚了
定義的訪問器是經過魔術方法來實現的,並非真的會註冊一個屬性。
明白了之後,咱們繼續
咱們將
controller function
稍做修改
// CustomerController
public function testCustomer() try {
$beginTime = microtime(true);
/** @var Collection $customers */
$customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
$customers->transform(function ($customer) {
/** @var Customer $customer */
$customer->test_tag = $customer->test_tag;
return $customer;
});
$endTime = microtime(true);
\Log::info($endTime - $beginTime);
return responseSuccess($customers);
} catch (\Exception $e) {
errorLog($e);
return responseFailed($e->getMessage());
}
}
複製代碼
日誌中記錄的時間爲
local.INFO: 0.01134991645813
{
"msg": "操做成功",
"code": 200,
"data": [
{
"id": 92424,
"test_tag": "年齡過小/零基礎/年齡過大",
"customer_tags": [
{
"id": 1586,
"customer_id": 92424,
"tag_id": 1,
"tag": {
"id": 1,
"name": "年齡過小",
}
},
{
"id": 1588,
"customer_id": 92424,
"tag_id": 2,
"tag": {
"id": 2,
"name": "零基礎",
}
},
{
"id": 1587,
"customer_id": 92424,
"tag_id": 10,
"tag": {
"id": 10,
"name": "年齡過大",
}
}
]
},
{
"id": 16,
"customer_tags": []
},
...
]
}
複製代碼
OK,很是好,到這裏咱們已經實現了咱們的需求。可是多餘的 customer_tags
是前端不須要的,因此咱們繼續略改代碼,將它移除掉。
咱們將
controller function
稍做修改,執行完訪問器之後,刪除掉customer_tags
// CustomerController
public function testCustomer() try {
$beginTime = microtime(true);
/** @var Collection $customers */
$customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
$customers->transform(function ($customer) {
/** @var Customer $customer */
$customer->test_tag = $customer->test_tag;
unset($customer->customerTags);
return $customer;
});
$endTime = microtime(true);
\Log::info($endTime - $beginTime);
return responseSuccess($customers);
} catch (\Exception $e) {
errorLog($e);
return responseFailed($e->getMessage());
}
}
複製代碼
很奇怪,這裏咱們明明
unset()
移除了$customer->customerTags
, 結果仍是返回了相關數據。
{
"msg": "操做成功",
"code": 200,
"data": [
{
"id": 92424,
"test_tag": "年齡過小/零基礎/年齡過大",
"customer_tags": [
{
"id": 1586,
"customer_id": 92424,
"tag_id": 1,
"tag": {
"id": 1,
"name": "年齡過小",
}
},
{
"id": 1588,
"customer_id": 92424,
"tag_id": 2,
"tag": {
"id": 2,
"name": "零基礎",
}
},
{
"id": 1587,
"customer_id": 92424,
"tag_id": 10,
"tag": {
"id": 10,
"name": "年齡過大",
}
}
]
},
{
"id": 16,
"customer_tags": []
},
...
]
}
複製代碼
很奇怪,這裏咱們明明 unset()
移除了 $customer->customerTags
, 結果仍是返回了相關數據。爲何呢?
這裏我開啓了 sql
日誌之後,再次執行,依然仍是以前的結果。不要緊,咱們不慌,來查看日誌。
能夠看出,在輸出執行時間以後,又多出來了許多 sql
,而這些 sql
正是用來查詢客戶的tags
相關信息的。執行時間輸出之後就執行了 responseSuccess()
,難道這個方法有問題?
讓咱們修改一下 responseSuccess()
,添加一條 log
//對返回給前端前對數據進行格式處理
function responseSuccess($data = [], $message = '操做成功') {
$res = [
'msg' => $message,
'code' => 200,
'data' => $data
];
//分頁特殊處理
if ($data instanceof Paginator) {
$data = $data->toArray();
$page = [
'current_page' => $data['current_page'],
'last_page' => $data['last_page'],
'per_page' => $data['per_page'],
'total' => $data['total']
];
$res['data'] = $data['data'];
$res['pages'] = $page;
}
\Log::info('------------華麗的分割線-------------');
return response()->json($res)->setStatusCode(200);
}
複製代碼
WTF ? 這是怎麼回事?「華麗的分割線」以後就是框架提供的返回 json
數據的方法,難道框架自己出了什麼問題?
json()
方法tinker
中執行 response()
查看返回的對象Psy Shell v0.9.9 (PHP 7.1.25 — cli) by Justin Hileman
>>> response()
=> Illuminate\Routing\ResponseFactory {#3470}
複製代碼
Illuminate\Routing\ResponseFactory
namespace Illuminate\Routing;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Routing\ResponseFactory as FactoryContract;
class ResponseFactory implements FactoryContract {
use Macroable;
/** * Return a new JSON response from the application. * * @param mixed $data * @param int $status * @param array $headers * @param int $options * @return \Illuminate\Http\JsonResponse */
public function json($data = [], $status = 200, array $headers = [], $options = 0) {
return new JsonResponse($data, $status, $headers, $options);
}
}
複製代碼
Illuminate\Http\JsonResponse
namespace Illuminate\Http;
use JsonSerializable;
use InvalidArgumentException;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
use Symfony\Component\HttpFoundation\JsonResponse as BaseJsonResponse;
class JsonResponse extends BaseJsonResponse {
use ResponseTrait, Macroable {
Macroable::__call as macroCall;
}
/** * Constructor. * * @param mixed $data * @param int $status * @param array $headers * @param int $options * @return void */
public function __construct($data = null, $status = 200, $headers = [], $options = 0) {
$this->encodingOptions = $options;
parent::__construct($data, $status, $headers);
}
/** * {@inheritdoc} */
public function setData($data = []) {
$this->original = $data;
if ($data instanceof Jsonable) {
$this->data = $data->toJson($this->encodingOptions);
} elseif ($data instanceof JsonSerializable) {
$this->data = json_encode($data->jsonSerialize(), $this->encodingOptions);
} elseif ($data instanceof Arrayable) {
$this->data = json_encode($data->toArray(), $this->encodingOptions);
} else {
$this->data = json_encode($data, $this->encodingOptions);
}
if (! $this->hasValidJson(json_last_error())) {
throw new InvalidArgumentException(json_last_error_msg());
}
return $this->update();
}
/** * Sets a raw string containing a JSON document to be sent. * * @param string $json * * @return $this * * @throws \InvalidArgumentException */
public function setJson($json) {
$this->data = $json;
return $this->update();
}
}
複製代碼
Symfony\Component\HttpFoundation\JsonResponse
中的構造方法在當前流程中,第四個參數必定是
false
(調用的時候壓根就沒傳第四個參數),因此就是調用了Illuminate\Http\JsonResponse::setData()
namespace Symfony\Component\HttpFoundation;
class JsonResponse extends Response {
/** * @param mixed $data The response data * @param int $status The response status code * @param array $headers An array of response headers * @param bool $json If the data is already a JSON string */
public function __construct($data = null, $status = 200, $headers = array(), $json = false) {
parent::__construct('', $status, $headers);
if (null === $data) {
$data = new \ArrayObject();
}
$json ? $this->setJson($data) : $this->setData($data);
}
}
複製代碼
Illuminate\Http\JsonResponse::setData()
的執行/** * {@inheritdoc} */
public function setData($data = []) {
$this->original = $data;
if ($data instanceof Jsonable) {
$this->data = $data->toJson($this->encodingOptions);
} elseif ($data instanceof JsonSerializable) {
$this->data = json_encode($data->jsonSerialize(), $this->encodingOptions);
} elseif ($data instanceof Arrayable) {
$this->data = json_encode($data->toArray(), $this->encodingOptions);
} else {
$this->data = json_encode($data, $this->encodingOptions);
}
if (! $this->hasValidJson(json_last_error())) {
throw new InvalidArgumentException(json_last_error_msg());
}
return $this->update();
}
複製代碼
經過看代碼, 這麼分支,那麼是執行了哪一個分支呢?因此咱們要先弄清楚$data
的類型,$data
是什麼呢?對 於$data
,一路傳遞過來,其實不難想明白,它就是咱們一開始在responseSuccess()
中拼接的 $res
,而後 $res['data']
是咱們一開始查詢得出的 $customers
,那咱們都知道ORM
模型的結果集是Illuminate\Database\Eloquent\Collection
。 因此這裏 $data
做爲一個數組,他會進入 setData()
中的第四個分支
data, $this->encodingOptions);
詳情請看這裏 json_encode()
如何轉化一個對象?
json_encode()
是一個向下遞歸的遍歷每個可遍歷的元素,若是遇到不可遍歷元素是一個對象,則會判斷對象是否實現了 JsonSerializable
,若是實現了 JsonSerializable
,則要看該對象的 jsonSerialize()
,不然只會編碼對象的公開非靜態屬性。
那咱們看一下 $customers
or Illuminate\Database\Eloquent\Collection
是否實現了 JsonSerializable
>>> $test = new \Illuminate\Database\Eloquent\Collection();
=> Illuminate\Database\Eloquent\Collection {#3518
all: [],
}
>>> $test instanceof JsonSerializable
=> true
複製代碼
Illuminate\Database\Eloquent\Collection
確實實現了 JsonSerializable
,因此這裏關於 $customers
被編碼的狀況,應該是要找到 Illuminate\Database\Eloquent\Collection
中的 jsonSerialize()
Illuminate\Database\Eloquent\Collection
namespace Illuminate\Database\Eloquent;
use LogicException;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Support\Collection as BaseCollection;
class Collection extends BaseCollection implements QueueableCollection {
/** * Get the collection of items as a plain array. * * @return array */
public function toArray() {
return array_map(function ($value) {
return $value instanceof Arrayable ? $value->toArray() : $value;
}, $this->items);
}
/** * Convert the object into something JSON serializable. * * @return array */
public function jsonSerialize() {
return array_map(function ($value) {
if ($value instanceof JsonSerializable) {
return $value->jsonSerialize();
} elseif ($value instanceof Jsonable) {
return json_decode($value->toJson(), true);
} elseif ($value instanceof Arrayable) {
return $value->toArray();
}
return $value;
}, $this->items);
}
/** * Get the collection of items as JSON. * * @param int $options * @return string */
public function toJson($options = 0) {
return json_encode($this->jsonSerialize(), $options);
}
}
複製代碼
Illuminate\Database\Eloquent\Collection
又繼承了Illuminate\Support\Collection
Illuminate\Support\Collection
namespace Illuminate\Support;
use stdClass;
use Countable;
use Exception;
use ArrayAccess;
use Traversable;
use ArrayIterator;
use CachingIterator;
use JsonSerializable;
use IteratorAggregate;
use Illuminate\Support\Debug\Dumper;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable {
/** * The items contained in the collection. * * @var array */
protected $items = [];
/** * Create a new collection. * * @param mixed $items * @return void */
public function __construct($items = []) {
$this->items = $this->getArrayableItems($items);
}
/** * Results array of items from Collection or Arrayable. * * @param mixed $items * @return array */
protected function getArrayableItems($items) {
if (is_array($items)) {
return $items;
} elseif ($items instanceof self) {
return $items->all();
} elseif ($items instanceof Arrayable) {
return $items->toArray();
} elseif ($items instanceof Jsonable) {
return json_decode($items->toJson(), true);
} elseif ($items instanceof JsonSerializable) {
return $items->jsonSerialize();
} elseif ($items instanceof Traversable) {
return iterator_to_array($items);
}
return (array) $items;
}
}
複製代碼
能夠看得出,Illuminate\Database\Eloquent\Collection
的父級 Illuminate\Support\Collection
實現了 JsonSerializable
看到這裏,咱們就已經很明白了。
$customers
會進入第一個分支,調用 $customers->jsonSerialize()
。
array_map()
中的回調函數會處理 $customers->items
。
array_map()
中的回調函數也有許多分支,依賴元素的類型來選擇進入的分支
那麼$customers->items
中的元素是什麼呢?是 App\Models\Customer
,它繼承了Illuminate\Database\Eloquent\Model
。
Illuminate\Database\Eloquent\Model
namespace Illuminate\Database\Eloquent;
use Exception;
use ArrayAccess;
use JsonSerializable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable {
use Concerns\HasAttributes,
Concerns\HasEvents,
Concerns\HasGlobalScopes,
Concerns\HasRelationships,
Concerns\HasTimestamps,
Concerns\HidesAttributes,
Concerns\GuardsAttributes;
/** * Convert the model instance to an array. * * @return array */
public function toArray() {
return array_merge($this->attributesToArray(), $this->relationsToArray());
}
/** * Convert the model instance to JSON. * * @param int $options * @return string * * @throws \Illuminate\Database\Eloquent\JsonEncodingException */
public function toJson($options = 0) {
$json = json_encode($this->jsonSerialize(), $options);
if (JSON_ERROR_NONE !== json_last_error()) {
throw JsonEncodingException::forModel($this, json_last_error_msg());
}
return $json;
}
/** * Convert the object into something JSON serializable. * * @return array */
public function jsonSerialize() {
return $this->toArray();
}
}
複製代碼
看到這裏,咱們就明白了。
$res
是一個數組,一路傳遞到 Illuminate\Http\JsonResponse::setData()
,而後數組會被 json_encode()
,而 json_endode()
的本質就是遍歷每個元素進行編碼。
$res['data']
是一個 Illuminate\Database\Eloquent\Collection
,它實現了 JsonSerializable
,因此當遍歷到它的時候,會調用 Illuminate\Database\Eloquent\Collection::jsonSerialize()
。
Illuminate\Database\Eloquent\Collection::jsonSerialize()
會遍歷集合的屬性 $items
,而 $items
中的每個元素又是一個 App\Models\Customer
。
App\Models\Customer
繼承了Illuminate\Database\Eloquent\Model
,Illuminate\Database\Eloquent\Model
也實現了 JsonSerializable
,因此會調用 Illuminate\Database\Eloquent\Model::jsonSerialize()
經過查看 Illuminate\Database\Eloquent\Model
源碼,咱們發現,Illuminate\Database\Eloquent\Model
中的 toJson()
和 jsonSerialize()
都是先調用了 toArray()
看來問題的關鍵就是 Illuminate\Database\Eloquent\Model::toArray()
public function toArray() {
return array_merge($this->attributesToArray(), $this->relationsToArray());
}
複製代碼
$this->attributesToArray()
$this->relationsToArray()
是處理關聯關係的,本質上仍是對Collection
和Model
中的toArray()
調用
namespace Illuminate\Database\Eloquent\Concerns;
use LogicException;
use DateTimeInterface;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Database\Eloquent\JsonEncodingException;
trait HasAttributes
{
/** * The model's attributes. * * @var array */
protected $attributes = [];
/** * The cache of the mutated attributes for each class. * * @var array */
protected static $mutatorCache = [];
/** * Convert the model's attributes to an array. * * @return array */
public function attributesToArray() {
// If an attribute is a date, we will cast it to a string after converting it
// to a DateTime / Carbon instance. This is so we will get some consistent
// formatting while accessing attributes vs. arraying / JSONing a model.
// 日期處理相關
$attributes = $this->addDateAttributesToArray(
$attributes = $this->getArrayableAttributes()
);
//處理突變的方法 就是定義的訪問器 $this->getMutatedAttributes() 就是正則獲取定義的訪問器名稱
$attributes = $this->addMutatedAttributesToArray(
$attributes, $mutatedAttributes = $this->getMutatedAttributes()
);
// Next we will handle any casts that have been setup for this model and cast
// the values to their appropriate type. If the attribute has a mutator we
// will not perform the cast on those attributes to avoid any confusion.
// 這個能夠忽略,咱們沒有定義 $this->casts
$attributes = $this->addCastAttributesToArray(
$attributes, $mutatedAttributes
);
// Here we will grab all of the appended, calculated attributes to this model
// as these attributes are not really in the attributes array, but are run
// when we need to array or JSON the model for convenience to the coder.
// 這個能夠忽略,咱們沒有定義 $this->appends
foreach ($this->getArrayableAppends() as $key) {
$attributes[$key] = $this->mutateAttributeForArray($key, null);
}
return $attributes;
}
/** * Add the mutated attributes to the attributes array. * // 將一個突變的key及對應的值 添加到$attributes 若是key已經存在於$attributes,調用其對應的訪問器 * * @param array $attributes * @param array $mutatedAttributes * @return array */
protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes) {
foreach ($mutatedAttributes as $key) {
// We want to spin through all the mutated attributes for this model and call
// the mutator for the attribute. We cache off every mutated attributes so
// we don't have to constantly check on attributes that actually change.
// 若是key不存在於$attributes,跳過
if (!array_key_exists($key, $attributes)) {
continue;
}
// Next, we will call the mutator for this attribute so that we can get these
// mutated attribute's actual values. After we finish mutating each of the
// attributes we will return this final array of the mutated attributes.
// 若是key存在於$attributes,就會調用這裏的方法,注意傳了一個值進去
$attributes[$key] = $this->mutateAttributeForArray(
$key, $attributes[$key]
);
}
return $attributes;
}
/** * Get the value of an attribute using its mutator. * 調用訪問器 * * @param string $key * @param mixed $value * @return mixed */
protected function mutateAttribute($key, $value) {
//調用訪問器
return $this->{'get'.Str::studly($key).'Attribute'}($value);
}
/** * Get the value of an attribute using its mutator for array conversion. * * @param string $key * @param mixed $value * @return mixed */
protected function mutateAttributeForArray($key, $value) {
//你沒看錯,$key已經存在於model的屬性了,仍是要繼續調用了相應的訪問器來執行一遍代碼
$value = $this->mutateAttribute($key, $value);
//若是訪問器返回的值實現了Arrayable,繼續toArray() (包含集合和模型)
return $value instanceof Arrayable ? $value->toArray() : $value;
}
/** * Get the mutated attributes for a given instance. * 緩存訪問器對應的key * * @return array */
public function getMutatedAttributes() {
$class = static::class;
if (! isset(static::$mutatorCache[$class])) {
static::cacheMutatedAttributes($class);
}
return static::$mutatorCache[$class];
}
/** * Extract and cache all the mutated attributes of a class. * 獲取緩存訪問器對應的key 轉化成了下劃線風格 * * @param string $class * @return void */
public static function cacheMutatedAttributes($class) {
static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match);
})->all();
}
/** * Get all of the attribute mutator methods. * 正則獲取定義的訪問器名稱 * * @param mixed $class * @return array */
protected static function getMutatorMethods($class) {
preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
return $matches[1];
}
}
複製代碼
看的有點迷?
跑個代碼看一下
>>> $c = Customer::query()->where('id',92424)->select(['id'])->first(92424);
=> App\Models\Customer {#3479
id: 92424,
}
>>> $c->test_tag = $c->test_tag;
=> "年齡過小/零基礎/年齡過大"
>>> $c
=> App\Models\Customer {#3479
id: 92424,
test_tag: "年齡過小/零基礎/年齡過大",
customerTags: Illuminate\Database\Eloquent\Collection {#3487
all: [
App\Models\CustomerTag {#3498
id: 1586,
customer_id: 92424,
tag_id: 1,
tag: App\Models\Tag {#3504
id: 1,
name: "年齡過小",
},
},
App\Models\CustomerTag {#3499
id: 1588,
customer_id: 92424,
tag_id: 2,
tag: App\Models\Tag {#211
id: 2,
name: "零基礎",
},
},
App\Models\CustomerTag {#3500
id: 1587,
customer_id: 92424,
tag_id: 10,
tag: App\Models\Tag {#3475
id: 10,
name: "年齡過大",
},
},
],
},
}
>>> $c->getMutatedAttributes()
=> [
"test_tag",
]
複製代碼
能夠看到,在執行 $c->test_tag = $c->test_tag;
之後 ,$c
中已經有了 test_tag
屬性,test_tag
又是咱們定義的訪問器對應的,因此在 $this->addMutatedAttributesToArray()
中,對於已經存在於 Model
中的訪問器屬性,仍是要繼續調用相應的訪問器來執行一遍代碼。
//App\Models\Customer
public function getTestTagAttribute() {
$customerTags = iteratorGet($this, 'customerTags', []);
$tags = [];
foreach ($customerTags as $customerTag) {
$tags[] = iteratorGet($customerTag->tag, 'name');
}
return implode('/', $tags);
}
複製代碼
// CustomerController
public function testCustomer() try {
$beginTime = microtime(true);
/** @var Collection $customers */
$customers = Customer::with('customerTags.tag')->select(['id'])->limit(15)->get();
$customers->transform(function ($customer) {
/** @var Customer $customer */
$customer->test_tag = $customer->test_tag;
unset($customer->customerTags);
return $customer;
});
$endTime = microtime(true);
\Log::info($endTime - $beginTime);
return responseSuccess($customers);
} catch (\Exception $e) {
errorLog($e);
return responseFailed($e->getMessage());
}
}
複製代碼
因爲咱們爲集合中的每個模型都設置了 test_tag
屬性,而後又刪除了不想返回給前端的 relation
數據,那麼根據上邊對 laravel
源碼的分析, 因爲 test_tag
是咱們定義的訪問器對應的 key
,而且 test_tag
被咱們設置成了模型的屬性,因此在將數據編碼成爲 json
的時候,訪問器是必定會被觸發的。而後關聯關係會被從新查詢出來,而且產生 sql
。
怎麼樣?驚喜不驚喜,意外不意外?
laravel
大法好,沒想到還有這樣的深坑等着咱們吧?
有人會說,我看你的 responseSuccess()
有判斷傳進去的數據是否實現了分頁器(Illuminate\Pagination\LengthAwarePaginator
)
分頁器方法返回的結果集對象是
Illuminate\Pagination\LengthAwarePaginator
//Illuminate\Pagination\LengthAwarePaginator
<?php
namespace Illuminate\Pagination;
use Countable;
use ArrayAccess;
use JsonSerializable;
use IteratorAggregate;
use Illuminate\Support\Collection;
use Illuminate\Support\HtmlString;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorContract;
class LengthAwarePaginator extends AbstractPaginator implements Arrayable, ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Jsonable, LengthAwarePaginatorContract {
/** * The total number of items before slicing. * * @var int */
protected $total;
/** * The last available page. * * @var int */
protected $lastPage;
/** * Create a new paginator instance. * * @param mixed $items * @param int $total * @param int $perPage * @param int|null $currentPage * @param array $options (path, query, fragment, pageName) * @return void */
public function __construct($items, $total, $perPage, $currentPage = null, array $options = []) {
foreach ($options as $key => $value) {
$this->{$key} = $value;
}
$this->total = $total;
$this->perPage = $perPage;
$this->lastPage = max((int) ceil($total / $perPage), 1);
$this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;
$this->currentPage = $this->setCurrentPage($currentPage, $this->pageName);
$this->items = $items instanceof Collection ? $items : Collection::make($items);
}
/** * Get the instance as an array. * * @return array */
public function toArray() {
return [
'current_page' => $this->currentPage(),
'data' => $this->items->toArray(),
'first_page_url' => $this->url(1),
'from' => $this->firstItem(),
'last_page' => $this->lastPage(),
'last_page_url' => $this->url($this->lastPage()),
'next_page_url' => $this->nextPageUrl(),
'path' => $this->path,
'per_page' => $this->perPage(),
'prev_page_url' => $this->previousPageUrl(),
'to' => $this->lastItem(),
'total' => $this->total(),
];
}
/** * Convert the object into something JSON serializable. * * @return array */
public function jsonSerialize() {
return $this->toArray();
}
/** * Convert the object to its JSON representation. * * @param int $options * @return string */
public function toJson($options = 0) {
return json_encode($this->jsonSerialize(), $options);
}
}
複製代碼
代碼很容易理解,不管是 toJson()
,仍是 jsonSerialize()
,都是調用 toArray()
而後看構造方法能夠明白 $this->items
就是集合(不是集合也轉成集合了)
而後你必定特別明白'data' => $this->items->toArray(),
這一句,沒錯,調用了集合的 toArray()
因此分頁器編碼數據的最終方案仍是會調用集合的 toArray()
來編碼數據
怎麼樣?驚喜不驚喜,意外不意外?
laravel
大法好,沒想到跳來跳去都會跳到同一個坑裏吧?
咱們已經瞭解了訪問器的坑是怎麼產生的,那麼針對性的解決方案其實並不難
換個絕不相干的屬性名,懶人專屬,不過不適合老項目,畢竟返回的字段名不是說改就能改的
public function testCustomer() {
try {
$beginTime = microtime(true);
/** @var Collection $customers */
$customers = Customer::query()->with('customerTags.tag')->orderByDesc('expired_at')->select(['id'])->paginate(5);
$customers->transform(function ($customer) {
/** @var Customer $customer */
$customer->test_tag_info = $customer->test_tag;
unset($customer->customerTags);
return $customer;
});
$endTime = microtime(true);
\Log::info($endTime - $beginTime);
return responseSuccess($customers);
} catch (\Exception $e) {
errorLog($e);
return responseFailed($e->getMessage());
}
}
複製代碼
將 test_tag
改成 test_tag_info
{
"msg": "操做成功",
"code": 200,
"data": [
{
"id": 92424,
"test_tag_info": "年齡過小/零基礎/年齡過大"
},
{
"id": 93863,
"test_tag_info": "年齡過小"
},
{
"id": 93855,
"test_tag_info": "零基礎"
},
{
"id": 93852,
"test_tag_info": "年齡過小"
},
{
"id": 93797,
"test_tag_info": ""
}
]
}
複製代碼
能夠看出並無多餘的數據返回
能夠看出並無多餘的 sql
產生
修改訪問器?訪問器還能怎麼修改?
回想一下前邊扒源碼的時候,我有說過,在
$model
執行訪問器的時候,有傳一個值給到訪問器,這個值就是訪問器對應的key
在$model->attributes
對應的值。在調用訪問器對應的
key
時,若是key
在$model->attributes
中不存在,那麼$value
是一個null
。在編碼轉化
$model
時,若是key
在$model->attributes
中不存在,那麼該訪問器不會被調用。咱們對傳進訪問器的值加以判斷
// App\Models\Customer
public function getTestTagAttribute($value) {
if ($value !== null) {
return $value;
}
$customerTags = iteratorGet($this, 'customerTags', []);
$tags = [];
foreach ($customerTags as $customerTag) {
$tags[] = iteratorGet($customerTag->tag, 'name');
}
return implode('/', $tags);
}
複製代碼
在這裏,我判斷 $value
不爲 null
,就返回 $value
{
"msg": "操做成功",
"code": 200,
"data": [
{
"id": 92424,
"test_tag": "年齡過小/零基礎/年齡過大"
},
{
"id": 93863,
"test_tag": "年齡過小"
},
{
"id": 93855,
"test_tag": "零基礎"
},
{
"id": 93852,
"test_tag": "年齡過小"
},
{
"id": 93797,
"test_tag": ""
}
]
}
複製代碼
能夠看出並無多餘的數據返回
能夠看出並無多餘的 sql
產生,別說我拿上邊的圖,看下時間戳。
laravel
確實是被你們承認的優秀的 php
框架(排除我,我喜歡接近原生的 CI)
功能和特性十分豐富,對開發效率帶來的提高確實不是一點半點,可是不少功能和特性,僅靠官方文檔並不能真正瞭解怎麼去用,怎麼避開可能的坑。做爲框架的使用者,咱們不可能要求框架爲咱們而改變,咱們能作的就是深刻了解它,真正的駕馭它(吹牛皮的感受真爽)
完