ModelSerializer.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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\data;
  8. use Yii;
  9. use yii\base\Component;
  10. use yii\base\Model;
  11. use yii\helpers\StringHelper;
  12. /**
  13. * ModelSerializer converts a model or a list of models into an array representation with selected fields.
  14. *
  15. * Used together with [[\yii\web\ResponseFormatter]], ModelSerializer can be used to serve model data
  16. * in JSON or XML format for REST APIs.
  17. *
  18. * ModelSerializer provides two methods [[export()]] and [[exportAll()]] to convert model(s) into array(s).
  19. * The former works for a single model, while the latter for an array of models.
  20. * During conversion, it will check which fields are requested and only provide valid fields (as declared
  21. * in [[fields()]] and [[expand()]]) in the array result.
  22. *
  23. * @author Qiang Xue <[email protected]>
  24. * @since 2.0
  25. */
  26. class ModelSerializer extends Component
  27. {
  28. /**
  29. * @var string the model class that this API is serving. If not set, it will be initialized
  30. * as the class of the model(s) being exported by [[export()]] or [[exportAll()]].
  31. */
  32. public $modelClass;
  33. /**
  34. * @var mixed the context information. If not set, it will be initialized as the "user" application component.
  35. * You can use the context information to conditionally control which fields can be returned for a model.
  36. */
  37. public $context;
  38. /**
  39. * @var array|string an array or a string of comma separated field names representing
  40. * which fields should be returned. Only fields declared in [[fields()]] will be respected.
  41. * If this property is empty, all fields declared in [[fields()]] will be returned.
  42. */
  43. public $fields;
  44. /**
  45. * @var array|string an array or a string of comma separated field names representing
  46. * which fields should be returned in addition to those declared in [[fields()]].
  47. * Only fields declared in [[expand()]] will be respected.
  48. */
  49. public $expand;
  50. /**
  51. * @var integer the error code to be used in the result of [[exportErrors()]].
  52. */
  53. public $validationErrorCode = 1024;
  54. /**
  55. * @var string the error message to be used in the result of [[exportErrors()]].
  56. */
  57. public $validationErrorMessage = 'Validation Failed';
  58. /**
  59. * @var array a list of serializer classes indexed by their corresponding model classes.
  60. * This property is used by [[createSerializer()]] to serialize embedded objects.
  61. * @see createSerializer()
  62. */
  63. public $serializers = [];
  64. /**
  65. * @var array a list of paths or path aliases specifying how to look for a serializer class
  66. * given a model class. If the base name of a model class is `Xyz`, the corresponding
  67. * serializer class being looked for would be `XyzSerializer` under each of the paths listed here.
  68. */
  69. public $serializerPaths = ['@app/serializers'];
  70. /**
  71. * @var array the loaded serializer objects indexed by the model class names
  72. */
  73. private $_serializers = [];
  74. /**
  75. * @inheritdoc
  76. */
  77. public function init()
  78. {
  79. parent::init();
  80. if ($this->context === null && Yii::$app) {
  81. $this->context = Yii::$app->user;
  82. }
  83. }
  84. /**
  85. * Exports a model object by converting it into an array based on the specified fields.
  86. * @param Model $model the model being exported
  87. * @return array the exported data
  88. */
  89. public function export($model)
  90. {
  91. if ($this->modelClass === null) {
  92. $this->modelClass = get_class($model);
  93. }
  94. $fields = $this->resolveFields($this->fields, $this->expand);
  95. return $this->exportObject($model, $fields);
  96. }
  97. /**
  98. * Exports an array of model objects by converting it into an array based on the specified fields.
  99. * @param Model[] $models the models being exported
  100. * @return array the exported data
  101. */
  102. public function exportAll(array $models)
  103. {
  104. if (empty($models)) {
  105. return [];
  106. }
  107. if ($this->modelClass === null) {
  108. $this->modelClass = get_class(reset($models));
  109. }
  110. $fields = $this->resolveFields($this->fields, $this->expand);
  111. $result = [];
  112. foreach ($models as $model) {
  113. $result[] = $this->exportObject($model, $fields);
  114. }
  115. return $result;
  116. }
  117. /**
  118. * Exports the model validation errors.
  119. * @param Model $model
  120. * @return array
  121. */
  122. public function exportErrors($model)
  123. {
  124. $result = [
  125. 'code' => $this->validationErrorCode,
  126. 'message' => $this->validationErrorMessage,
  127. 'errors' => [],
  128. ];
  129. foreach ($model->getFirstErrors() as $name => $message) {
  130. $result['errors'][] = [
  131. 'field' => $name,
  132. 'message' => $message,
  133. ];
  134. }
  135. return $result;
  136. }
  137. /**
  138. * Returns a list of fields that can be returned to end users.
  139. *
  140. * These are the fields that should be returned by default when a user does not explicitly specify which
  141. * fields to return for a model. If the user explicitly which fields to return, only the fields declared
  142. * in this method can be returned. All other fields will be ignored.
  143. *
  144. * By default, this method returns [[Model::attributes()]], which are the attributes defined by a model.
  145. *
  146. * You may override this method to select which fields can be returned or define new fields based
  147. * on model attributes.
  148. *
  149. * The value returned by this method should be an array of field definitions. The array keys
  150. * are the field names, and the array values are the corresponding attribute names or callbacks
  151. * returning field values. If a field name is the same as the corresponding attribute name,
  152. * you can use the field name without a key.
  153. *
  154. * @return array field name => attribute name or definition
  155. */
  156. protected function fields()
  157. {
  158. if (is_subclass_of($this->modelClass, Model::className())) {
  159. /** @var Model $model */
  160. $model = new $this->modelClass;
  161. return $model->attributes();
  162. } else {
  163. return array_keys(get_class_vars($this->modelClass));
  164. }
  165. }
  166. /**
  167. * Returns a list of additional fields that can be returned to end users.
  168. *
  169. * The default implementation returns an empty array. You may override this method to return
  170. * a list of additional fields that can be returned to end users. Please refer to [[fields()]]
  171. * on the format of the return value.
  172. *
  173. * You usually override this method by returning a list of relation names.
  174. *
  175. * @return array field name => attribute name or definition
  176. */
  177. protected function expand()
  178. {
  179. return [];
  180. }
  181. /**
  182. * Filters the data to be exported to end user.
  183. * The default implementation does nothing. You may override this method to remove
  184. * certain fields from the data being exported based on the [[context]] information.
  185. * You may also use this method to add some common fields, such as class name, to the data.
  186. * @param array $data the data being exported
  187. * @return array the filtered data
  188. */
  189. protected function filter($data)
  190. {
  191. return $data;
  192. }
  193. /**
  194. * Returns the serializer for the specified model class.
  195. * @param string $modelClass fully qualified model class name
  196. * @return static the serializer
  197. */
  198. protected function getSerializer($modelClass)
  199. {
  200. if (!isset($this->_serializers[$modelClass])) {
  201. $this->_serializers[$modelClass] = $this->createSerializer($modelClass);
  202. }
  203. return $this->_serializers[$modelClass];
  204. }
  205. /**
  206. * Creates a serializer object for the specified model class.
  207. *
  208. * This method tries to create an appropriate serializer using the following algorithm:
  209. *
  210. * - Check if [[serializers]] specifies the serializer class for the model class and
  211. * create an instance of it if available;
  212. * - Search for a class named `XyzSerializer` under the paths specified by [[serializerPaths]],
  213. * where `Xyz` stands for the model class.
  214. * - If both of the above two strategies fail, simply return an instance of `ModelSerializer`.
  215. *
  216. * @param string $modelClass the model class
  217. * @return ModelSerializer the new model serializer
  218. */
  219. protected function createSerializer($modelClass)
  220. {
  221. if (isset($this->serializers[$modelClass])) {
  222. $config = $this->serializers[$modelClass];
  223. if (!is_array($config)) {
  224. $config = ['class' => $config];
  225. }
  226. } else {
  227. $className = StringHelper::basename($modelClass) . 'Serializer';
  228. foreach ($this->serializerPaths as $path) {
  229. $path = Yii::getAlias($path);
  230. if (is_file($path . "/$className.php")) {
  231. $config = ['class' => $className];
  232. break;
  233. }
  234. }
  235. }
  236. if (!isset($config)) {
  237. $config = ['class' => __CLASS__];
  238. }
  239. $config['modelClass'] = $modelClass;
  240. $config['context'] = $this->context;
  241. return Yii::createObject($config);
  242. }
  243. /**
  244. * Returns the fields of the model that need to be returned to end user
  245. * @param string|array $fields an array or a string of comma separated field names representing
  246. * which fields should be returned.
  247. * @param string|array $expand an array or a string of comma separated field names representing
  248. * which additional fields should be returned.
  249. * @return array field name => field definition (attribute name or callback)
  250. */
  251. protected function resolveFields($fields, $expand)
  252. {
  253. if (!is_array($fields)) {
  254. $fields = preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY);
  255. }
  256. if (!is_array($expand)) {
  257. $expand = preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY);
  258. }
  259. $result = [];
  260. foreach ($this->fields() as $field => $definition) {
  261. if (is_integer($field)) {
  262. $field = $definition;
  263. }
  264. if (empty($fields) || in_array($field, $fields, true)) {
  265. $result[$field] = $definition;
  266. }
  267. }
  268. if (empty($expand)) {
  269. return $result;
  270. }
  271. foreach ($this->expand() as $field => $definition) {
  272. if (is_integer($field)) {
  273. $field = $definition;
  274. }
  275. if (in_array($field, $expand, true)) {
  276. $result[$field] = $definition;
  277. }
  278. }
  279. return $result;
  280. }
  281. /**
  282. * Exports an object by converting it into an array based on the given field definitions.
  283. * @param object $model the model being exported
  284. * @param array $fields field definitions (field name => field definition)
  285. * @return array the exported model data
  286. */
  287. protected function exportObject($model, $fields)
  288. {
  289. $data = [];
  290. foreach ($fields as $field => $attribute) {
  291. if (is_string($attribute)) {
  292. $value = $model->$attribute;
  293. } else {
  294. $value = call_user_func($attribute, $model, $field);
  295. }
  296. if (is_object($value)) {
  297. $value = $this->getSerializer(get_class($value))->export($value);
  298. } elseif (is_array($value)) {
  299. foreach ($value as $i => $v) {
  300. if (is_object($v)) {
  301. $value[$i] = $this->getSerializer(get_class($v))->export($v);
  302. }
  303. // todo: array of array
  304. }
  305. }
  306. $data[$field] = $value;
  307. }
  308. return $this->filter($data);
  309. }
  310. }