ArrayableTrait.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\base;
  8. use Yii;
  9. use yii\helpers\ArrayHelper;
  10. use yii\web\Link;
  11. use yii\web\Linkable;
  12. /**
  13. * ArrayableTrait provides a common implementation of the [[Arrayable]] interface.
  14. *
  15. * ArrayableTrait implements [[toArray()]] by respecting the field definitions as declared
  16. * in [[fields()]] and [[extraFields()]].
  17. *
  18. * @author Qiang Xue <qiang.xue@gmail.com>
  19. * @since 2.0
  20. */
  21. trait ArrayableTrait
  22. {
  23. /**
  24. * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified.
  25. *
  26. * A field is a named element in the returned array by [[toArray()]].
  27. *
  28. * This method should return an array of field names or field definitions.
  29. * If the former, the field name will be treated as an object property name whose value will be used
  30. * as the field value. If the latter, the array key should be the field name while the array value should be
  31. * the corresponding field definition which can be either an object property name or a PHP callable
  32. * returning the corresponding field value. The signature of the callable should be:
  33. *
  34. * ```php
  35. * function ($model, $field) {
  36. * // return field value
  37. * }
  38. * ```
  39. *
  40. * For example, the following code declares four fields:
  41. *
  42. * - `email`: the field name is the same as the property name `email`;
  43. * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their
  44. * values are obtained from the `first_name` and `last_name` properties;
  45. * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name`
  46. * and `last_name`.
  47. *
  48. * ```php
  49. * return [
  50. * 'email',
  51. * 'firstName' => 'first_name',
  52. * 'lastName' => 'last_name',
  53. * 'fullName' => function () {
  54. * return $this->first_name . ' ' . $this->last_name;
  55. * },
  56. * ];
  57. * ```
  58. *
  59. * In this method, you may also want to return different lists of fields based on some context
  60. * information. For example, depending on the privilege of the current application user,
  61. * you may return different sets of visible fields or filter out some fields.
  62. *
  63. * The default implementation of this method returns the public object member variables indexed by themselves.
  64. *
  65. * @return array the list of field names or field definitions.
  66. * @see toArray()
  67. */
  68. public function fields()
  69. {
  70. $fields = array_keys(Yii::getObjectVars($this));
  71. return array_combine($fields, $fields);
  72. }
  73. /**
  74. * Returns the list of fields that can be expanded further and returned by [[toArray()]].
  75. *
  76. * This method is similar to [[fields()]] except that the list of fields returned
  77. * by this method are not returned by default by [[toArray()]]. Only when field names
  78. * to be expanded are explicitly specified when calling [[toArray()]], will their values
  79. * be exported.
  80. *
  81. * The default implementation returns an empty array.
  82. *
  83. * You may override this method to return a list of expandable fields based on some context information
  84. * (e.g. the current application user).
  85. *
  86. * @return array the list of expandable field names or field definitions. Please refer
  87. * to [[fields()]] on the format of the return value.
  88. * @see toArray()
  89. * @see fields()
  90. */
  91. public function extraFields()
  92. {
  93. return [];
  94. }
  95. /**
  96. * Converts the model into an array.
  97. *
  98. * This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]].
  99. * It will then turn the model into an array with these fields. If `$recursive` is true,
  100. * any embedded objects will also be converted into arrays.
  101. * When embeded objects are [[Arrayable]], their respective nested fields will be extracted and passed to [[toArray()]].
  102. *
  103. * If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element
  104. * which refers to a list of links as specified by the interface.
  105. *
  106. * @param array $fields the fields being requested.
  107. * If empty or if it contains '*', all fields as specified by [[fields()]] will be returned.
  108. * Fields can be nested, separated with dots (.). e.g.: item.field.sub-field
  109. * `$recursive` must be true for nested fields to be extracted. If `$recursive` is false, only the root fields will be extracted.
  110. * @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]]
  111. * will be considered.
  112. * Expand can also be nested, separated with dots (.). e.g.: item.expand1.expand2
  113. * `$recursive` must be true for nested expands to be extracted. If `$recursive` is false, only the root expands will be extracted.
  114. * @param bool $recursive whether to recursively return array representation of embedded objects.
  115. * @return array the array representation of the object
  116. */
  117. public function toArray(array $fields = [], array $expand = [], $recursive = true)
  118. {
  119. $data = [];
  120. foreach ($this->resolveFields($fields, $expand) as $field => $definition) {
  121. $attribute = is_string($definition) ? $this->$definition : $definition($this, $field);
  122. if ($recursive) {
  123. $nestedFields = $this->extractFieldsFor($fields, $field);
  124. $nestedExpand = $this->extractFieldsFor($expand, $field);
  125. if ($attribute instanceof Arrayable) {
  126. $attribute = $attribute->toArray($nestedFields, $nestedExpand);
  127. } elseif (is_array($attribute)) {
  128. $attribute = array_map(
  129. function ($item) use ($nestedFields, $nestedExpand) {
  130. if ($item instanceof Arrayable) {
  131. return $item->toArray($nestedFields, $nestedExpand);
  132. }
  133. return $item;
  134. },
  135. $attribute
  136. );
  137. }
  138. }
  139. $data[$field] = $attribute;
  140. }
  141. if ($this instanceof Linkable) {
  142. $data['_links'] = Link::serialize($this->getLinks());
  143. }
  144. return $recursive ? ArrayHelper::toArray($data) : $data;
  145. }
  146. /**
  147. * Extracts the root field names from nested fields.
  148. * Nested fields are separated with dots (.). e.g: "item.id"
  149. * The previous example would extract "item".
  150. *
  151. * @param array $fields The fields requested for extraction
  152. * @return array root fields extracted from the given nested fields
  153. * @since 2.0.14
  154. */
  155. protected function extractRootFields(array $fields)
  156. {
  157. $result = [];
  158. foreach ($fields as $field) {
  159. $result[] = current(explode('.', $field, 2));
  160. }
  161. if (in_array('*', $result, true)) {
  162. $result = [];
  163. }
  164. return array_unique($result);
  165. }
  166. /**
  167. * Extract nested fields from a fields collection for a given root field
  168. * Nested fields are separated with dots (.). e.g: "item.id"
  169. * The previous example would extract "id".
  170. *
  171. * @param array $fields The fields requested for extraction
  172. * @param string $rootField The root field for which we want to extract the nested fields
  173. * @return array nested fields extracted for the given field
  174. * @since 2.0.14
  175. */
  176. protected function extractFieldsFor(array $fields, $rootField)
  177. {
  178. $result = [];
  179. foreach ($fields as $field) {
  180. if (0 === strpos($field, "{$rootField}.")) {
  181. $result[] = preg_replace('/^' . preg_quote($rootField, '/') . '\./i', '', $field);
  182. }
  183. }
  184. return array_unique($result);
  185. }
  186. /**
  187. * Determines which fields can be returned by [[toArray()]].
  188. * This method will first extract the root fields from the given fields.
  189. * Then it will check the requested root fields against those declared in [[fields()]] and [[extraFields()]]
  190. * to determine which fields can be returned.
  191. * @param array $fields the fields being requested for exporting
  192. * @param array $expand the additional fields being requested for exporting
  193. * @return array the list of fields to be exported. The array keys are the field names, and the array values
  194. * are the corresponding object property names or PHP callables returning the field values.
  195. */
  196. protected function resolveFields(array $fields, array $expand)
  197. {
  198. $fields = $this->extractRootFields($fields);
  199. $expand = $this->extractRootFields($expand);
  200. $result = [];
  201. foreach ($this->fields() as $field => $definition) {
  202. if (is_int($field)) {
  203. $field = $definition;
  204. }
  205. if (empty($fields) || in_array($field, $fields, true)) {
  206. $result[$field] = $definition;
  207. }
  208. }
  209. if (empty($expand)) {
  210. return $result;
  211. }
  212. foreach ($this->extraFields() as $field => $definition) {
  213. if (is_int($field)) {
  214. $field = $definition;
  215. }
  216. if (in_array($field, $expand, true)) {
  217. $result[$field] = $definition;
  218. }
  219. }
  220. return $result;
  221. }
  222. }