Browse Source

Fixes #6844: `yii\base\ArrayableTrait::toArray()` now allows recursive `$fields` and `$expand`

tags/2.0.14
Benoît Bouré 7 years ago committed by Alexander Makarov
parent
commit
f1277d6d21
  1. 2
      framework/CHANGELOG.md
  2. 82
      framework/base/ArrayableTrait.php
  3. 162
      tests/framework/rest/SerializerTest.php

2
framework/CHANGELOG.md

@ -36,6 +36,7 @@ Yii Framework 2 Change Log
- Enh #3087: Added `yii\helpers\BaseHtml::error()` "errorSource" option to be able to customize errors display (yanggs07, developeruz, silverfire)
- Enh #3250: Added support for events partial wildcard matching (klimov-paul)
- Enh #5515: Added default value for `yii\behaviors\BlameableBehavior` for cases when the user is guest (dmirogin)
- Enh #6844: `yii\base\ArrayableTrait::toArray()` now allows recursive `$fields` and `$expand` (bboure)
- Enh #7988: Added `\yii\helpers\Console::errorSummary()` and `\yii\helpers\Json::errorSummary()` (developeruz)
- Enh #7996: Short syntax for verb in GroupUrlRule (schojniak, developeruz)
- Enh #8752: Allow specify `$attributeNames` as a string for `yii\base\Model` `validate()` method (developeruz)
@ -64,7 +65,6 @@ Yii Framework 2 Change Log
- Enh: Added check to `yii\base\Model::formName()` to prevent source path disclosure when form is represented by an anonymous class (silverfire)
- Chg #15420: Handle OPTIONS request in `yii\filter\Cors` so the preflight check isn't passed trough authentication filters (michaelarnauts, leandrogehlen)
2.0.13.1 November 14, 2017
--------------------------

82
framework/base/ArrayableTrait.php

@ -103,13 +103,19 @@ trait ArrayableTrait
* This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]].
* It will then turn the model into an array with these fields. If `$recursive` is true,
* any embedded objects will also be converted into arrays.
* When embeded objects are [[Arrayable]], their respective nested fields will be extracted and passed to [[toArray()]].
*
* If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element
* which refers to a list of links as specified by the interface.
*
* @param array $fields the fields being requested. If empty, all fields as specified by [[fields()]] will be returned.
* @param array $fields the fields being requested.
* If empty or if it contains '*', all fields as specified by [[fields()]] will be returned.
* Fields can be nested, separated with dots (.). e.g.: item.field.sub-field
* `$recursive` must be true for nested fields to be extracted. If `$recursive` is false, only the root fields will be extracted.
* @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]]
* will be considered.
* Expand can also be nested, separated with dots (.). e.g.: item.expand1.expand2
* `$recursive` must be true for nested expands to be extracted. If `$recursive` is false, only the root expands will be extracted.
* @param bool $recursive whether to recursively return array representation of embedded objects.
* @return array the array representation of the object
*/
@ -117,7 +123,27 @@ trait ArrayableTrait
{
$data = [];
foreach ($this->resolveFields($fields, $expand) as $field => $definition) {
$data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $field);
$attribute = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $field);
if ($recursive) {
$nestedFields = $this->extractFieldsFor($fields, $field);
$nestedExpand = $this->extractFieldsFor($expand, $field);
if ($attribute instanceof Arrayable) {
$attribute = $attribute->toArray($nestedFields, $nestedExpand);
} elseif (is_array($attribute)) {
$attribute = array_map(
function ($item) use ($nestedFields, $nestedExpand) {
if ($item instanceof Arrayable) {
return $item->toArray($nestedFields, $nestedExpand);
} else {
return $item;
}
},
$attribute
);
}
}
$data[$field] = $attribute;
}
if ($this instanceof Linkable) {
@ -128,8 +154,56 @@ trait ArrayableTrait
}
/**
* Extracts the root field names from nested fields.
* Nested fields are separated with dots (.). e.g: "item.id"
* The previous example would extract "item".
*
* @param array $fields The fields requested for extraction
* @return array root fields extracted from the given nested fields
* @since 2.0.14
*/
protected function extractRootFields(array $fields)
{
$result = [];
foreach ($fields as $field) {
$result[] = current(explode(".", $field, 2));
}
if (in_array('*', $result, true)) {
$result = [];
}
return array_unique($result);
}
/**
* Extract nested fields from a fields collection for a given root field
* Nested fields are separated with dots (.). e.g: "item.id"
* The previous example would extract "id".
*
* @param array $fields The fields requested for extraction
* @param string $rootField The root field for which we want to extract the nested fields
* @return array nested fields extracted for the given field
* @since 2.0.14
*/
protected function extractFieldsFor(array $fields, $rootField)
{
$result = [];
foreach ($fields as $field) {
if (0 === strpos($field, "{$rootField}.")) {
$result[] = preg_replace("/^{$rootField}\./i", '', $field);
}
}
return array_unique($result);
}
/**
* Determines which fields can be returned by [[toArray()]].
* This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]]
* This method will first extract the root fields from the given fields.
* Then it will check the requested root fields against those declared in [[fields()]] and [[extraFields()]]
* to determine which fields can be returned.
* @param array $fields the fields being requested for exporting
* @param array $expand the additional fields being requested for exporting
@ -138,6 +212,8 @@ trait ArrayableTrait
*/
protected function resolveFields(array $fields, array $expand)
{
$fields = $this->extractRootFields($fields);
$expand = $this->extractRootFields($expand);
$result = [];
foreach ($this->fields() as $field => $definition) {

162
tests/framework/rest/SerializerTest.php

@ -112,6 +112,147 @@ class SerializerTest extends TestCase
], $serializer->serialize($model));
}
public function testNestedExpand()
{
$serializer = new Serializer();
$model = new TestModel();
$model->extraField3 = new TestModel2();
TestModel::$extraFields = ['extraField3'];
TestModel2::$extraFields = ['extraField4'];
\Yii::$app->request->setQueryParams(['expand' => 'extraField3.extraField4']);
$this->assertSame([
'field1' => 'test',
'field2' => 2,
'extraField3' => [
'field3' => 'test2',
'field4' => 8,
'extraField4' => 'testExtra2',
],
], $serializer->serialize($model));
}
public function testFields()
{
$serializer = new Serializer();
$model = new TestModel();
$model->extraField3 = new TestModel2();
TestModel::$extraFields = ['extraField3'];
\Yii::$app->request->setQueryParams([]);
$this->assertSame([
'field1' => 'test',
'field2' => 2,
], $serializer->serialize($model));
\Yii::$app->request->setQueryParams(['fields' => '*']);
$this->assertSame([
'field1' => 'test',
'field2' => 2,
], $serializer->serialize($model));
\Yii::$app->request->setQueryParams(
[
'fields' => 'field1,extraField3.field3',
'expand' => 'extraField3.extraField4'
]
);
$this->assertSame([
'field1' => 'test',
'extraField3' => [
'field3' => 'test2',
'extraField4' => 'testExtra2',
],
], $serializer->serialize($model));
\Yii::$app->request->setQueryParams(
[
'fields' => 'extraField3.*',
'expand' => 'extraField3',
]
);
$this->assertSame([
'extraField3' => [
'field3' => 'test2',
'field4' => 8,
],
], $serializer->serialize($model));
\Yii::$app->request->setQueryParams(
[
'fields' => 'extraField3.*',
'expand' => 'extraField3.extraField4'
]
);
$this->assertSame([
'extraField3' => [
'field3' => 'test2',
'field4' => 8,
'extraField4' => 'testExtra2',
],
], $serializer->serialize($model));
$model->extraField3 = [
new TestModel2(),
new TestModel2(),
];
\Yii::$app->request->setQueryParams(
[
'fields' => 'extraField3.*',
'expand' => 'extraField3',
]
);
$this->assertSame([
'extraField3' => [
[
'field3' => 'test2',
'field4' => 8,
],
[
'field3' => 'test2',
'field4' => 8,
],
],
], $serializer->serialize($model));
\Yii::$app->request->setQueryParams(
[
'fields' => '*,extraField3.*',
'expand' => 'extraField3',
]
);
$this->assertSame([
'field1' => 'test',
'field2' => 2,
'extraField3' => [
[
'field3' => 'test2',
'field4' => 8,
],
[
'field3' => 'test2',
'field4' => 8,
],
],
], $serializer->serialize($model));
\Yii::$app->request->setQueryParams(
[
'fields' => 'extraField3.field3',
'expand' => 'extraField3',
]
);
$this->assertSame([
'extraField3' => [
['field3' => 'test2'],
['field3' => 'test2'],
],
], $serializer->serialize($model));
}
/**
* @see https://github.com/yiisoft/yii2/issues/12107
*/
@ -284,6 +425,27 @@ class TestModel extends Model
public $field2 = 2;
public $extraField1 = 'testExtra';
public $extraField2 = 42;
public $extraField3;
public function fields()
{
return static::$fields;
}
public function extraFields()
{
return static::$extraFields;
}
}
class TestModel2 extends Model
{
public static $fields = ['field3', 'field4'];
public static $extraFields = [];
public $field3 = 'test2';
public $field4 = 8;
public $extraField4 = 'testExtra2';
public function fields()
{

Loading…
Cancel
Save