During building the REST API sometimes, we need to get related objects with the main object(s).
The main benefit of this approach is getting of all needed data at one query. Just agree it's cool! Let's try to implement this feature in Yii2.
By default, Yii2 has an implementation to get the related objects, but it fetches the whole record which can be too complex. And when you need to get for example only the user's name or some other attribute you will get all fields.
Let's see how it works in the default implementation. We skip the ActiveController and the models configuration. Please refer Yii2 docs to see how to do this by yourself.
Yii2 has 'expand' parameter which is responsible for the handling of related records in the response body. For example, we have some system which stores the users and their orders and we want to get all user records with their orders. In this case, the request will be like the following one:
GET domain.com/api/v1/users?expand=orderAnd the response is next:
[ { "userId": 1, "name": "Alex", "email": "alex@mail.com", "order": [ { "orderId": 1, "userId": 1, "product": "Macbook", "created": "2013-12-01" }, { "orderId": 2, "userId": 1, "product": "iPhone", "created": "2015-12-02" } ] }, { "userId": 2, "name": "John", "email": "john@mail.com", "order": [ { "orderId": 3, "userId": 2, "product": "Canon EOS999", "created": "2014-10-10" } ] }, { "userId": 3, "name": "Lucy", "email": "lucy@mail.com", "order": [] }, { "userId": 4, "name": "Mary", "email": "mary@mail.com", "order": [] } ]But what if we need only the product title instead of full order object, how to reach this goal?
Fortunately, there is a solution! You just need to override 'toArray' and 'resolveFields' methods of yii\base\ArrayableTrait which is used in a base Model class.
class User extends ActiveRecord { //... /** * @inheritdoc */ public function extraFields() { return [ 'order' ]; } /** * Additional method to check the related model has specified field */ private function ensureRelationHasField($relatedRecord, $field) { if (!is_object($relatedRecord)) { return false; } if (!$relatedRecord->hasAttribute($field)) { throw new yii\web\ServerErrorHttpException(sprintf( "Related record '%s' does not have attribute '%s'", get_class($relatedRecord), $field) ); } return true; } /** * @inheritdoc */ public function toArray(array $fields = [], array $expand = [], $recursive = true) { $data = []; $cachePrefix = __CLASS__; $cache = Yii::$app->cache; foreach ($this->resolveFields($fields, $expand) as $field => $definition) { if (strpos($field, '.')) { $fieldChunks = explode('.', $field); $uniqueRelationCacheKey = "{$cachePrefix}_{$this->primaryKey}_{$fieldChunks[0]}"; $relation = $cache->get($uniqueRelationCacheKey); if (!$relation) { $relation = $this->{$fieldChunks[0]}; $cache->set($uniqueRelationCacheKey, $relation); } if (is_array($relation)) { foreach ($relation as $relatedField => $relatedObject) { if ($this->enshureRelationHasField($relation, $fieldChunks[1])) { $data[$fieldChunks[0]][$relatedField][$fieldChunks[1]] = $relatedObject->{$fieldChunks[1]}; } } } else { if ($this->ensureRelationHasField($relation, $fieldChunks[1])) { $data[$fieldChunks[0]][$fieldChunks[1]] = $relation->{$fieldChunks[1]}; } } } else { $data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $field); } } if ($this instanceof Linkable) { $data['_links'] = Link::serialize($this->getLinks()); } return $recursive ? ArrayHelper::toArray($data) : $data; } /** * This method also will check relations which are declared in [[extraFields()]] * to determine which related fields can be returned. * @inheritdoc */ protected function resolveFields(array $fields, array $expand) { $result = []; foreach ($this->fields() as $field => $definition) { if (is_integer($field)) { $field = $definition; } if (empty($fields) || in_array($field, $fields, true)) { $result[$field] = $definition; } } if (empty($expand)) { return $result; } foreach ($this->extraFields() as $field => $definition) { if (is_integer($field)) { $field = $definition; } if (in_array($field, $expand, true)) { $result[$field] = $definition; } else { if (is_array($expand)) { foreach ($expand as $expandedAttribute) { $result[$expandedAttribute] = $expandedAttribute; } } } } return $result; } }And now the following request
GET domain.com/api/v1/users?expand=order.productwill return expected results:
[ { "userId": 1, "name": "Alex", "email": "alex@mail.com", "order": [ { "product": "Macbook" }, { "product": "iPhone" } ] }, { "userId": 2, "name": "John", "email": "john@mail.com", "order": [ { "product": "Canon EOS999" } ] }, { "userId": 3, "name": "Lucy", "email": "lucy@mail.com", "order": [] }, { "userId": 4, "name": "Mary", "email": "mary@mail.com", "order": [] } ]
It allowed us to reduce the response body and a bit of traffic, but the main benefit of this that we reduced fields count to that which we need and skipped those which we won't use.
No comments:
Post a Comment