* @since 2.0 */ class ModelSerializer extends Component { /** * @var string the model class that this API is serving. If not set, it will be initialized * as the class of the model(s) being exported by [[export()]] or [[exportAll()]]. */ public $modelClass; /** * @var mixed the context information. If not set, it will be initialized as the "user" application component. * You can use the context information to conditionally control which fields can be returned for a model. */ public $context; /** * @var array|string an array or a string of comma separated field names representing * which fields should be returned. Only fields declared in [[fields()]] will be respected. * If this property is empty, all fields declared in [[fields()]] will be returned. */ public $fields; /** * @var array|string an array or a string of comma separated field names representing * which fields should be returned in addition to those declared in [[fields()]]. * Only fields declared in [[expand()]] will be respected. */ public $expand; /** * @var integer the error code to be used in the result of [[exportErrors()]]. */ public $validationErrorCode = 1024; /** * @var string the error message to be used in the result of [[exportErrors()]]. */ public $validationErrorMessage = 'Validation Failed'; /** * @var array a list of serializer classes indexed by their corresponding model classes. * This property is used by [[createSerializer()]] to serialize embedded objects. * @see createSerializer() */ public $serializers = []; /** * @var array a list of paths or path aliases specifying how to look for a serializer class * given a model class. If the base name of a model class is `Xyz`, the corresponding * serializer class being looked for would be `XyzSerializer` under each of the paths listed here. */ public $serializerPaths = ['@app/serializers']; /** * @var array the loaded serializer objects indexed by the model class names */ private $_serializers = []; /** * @inheritdoc */ public function init() { parent::init(); if ($this->context === null && Yii::$app) { $this->context = Yii::$app->user; } } /** * Exports a model object by converting it into an array based on the specified fields. * @param Model $model the model being exported * @return array the exported data */ public function export($model) { if ($this->modelClass === null) { $this->modelClass = get_class($model); } $fields = $this->resolveFields($this->fields, $this->expand); return $this->exportObject($model, $fields); } /** * Exports an array of model objects by converting it into an array based on the specified fields. * @param Model[] $models the models being exported * @return array the exported data */ public function exportAll(array $models) { if (empty($models)) { return []; } if ($this->modelClass === null) { $this->modelClass = get_class(reset($models)); } $fields = $this->resolveFields($this->fields, $this->expand); $result = []; foreach ($models as $model) { $result[] = $this->exportObject($model, $fields); } return $result; } /** * Exports the model validation errors. * @param Model $model * @return array */ public function exportErrors($model) { $result = [ 'code' => $this->validationErrorCode, 'message' => $this->validationErrorMessage, 'errors' => [], ]; foreach ($model->getFirstErrors() as $name => $message) { $result['errors'][] = [ 'field' => $name, 'message' => $message, ]; } return $result; } /** * Returns a list of fields that can be returned to end users. * * These are the fields that should be returned by default when a user does not explicitly specify which * fields to return for a model. If the user explicitly which fields to return, only the fields declared * in this method can be returned. All other fields will be ignored. * * By default, this method returns [[Model::attributes()]], which are the attributes defined by a model. * * You may override this method to select which fields can be returned or define new fields based * on model attributes. * * The value returned by this method should be an array of field definitions. The array keys * are the field names, and the array values are the corresponding attribute names or callbacks * returning field values. If a field name is the same as the corresponding attribute name, * you can use the field name without a key. * * @return array field name => attribute name or definition */ protected function fields() { if (is_subclass_of($this->modelClass, Model::className())) { /** @var Model $model */ $model = new $this->modelClass; return $model->attributes(); } else { return array_keys(get_class_vars($this->modelClass)); } } /** * Returns a list of additional fields that can be returned to end users. * * The default implementation returns an empty array. You may override this method to return * a list of additional fields that can be returned to end users. Please refer to [[fields()]] * on the format of the return value. * * You usually override this method by returning a list of relation names. * * @return array field name => attribute name or definition */ protected function expand() { return []; } /** * Filters the data to be exported to end user. * The default implementation does nothing. You may override this method to remove * certain fields from the data being exported based on the [[context]] information. * You may also use this method to add some common fields, such as class name, to the data. * @param array $data the data being exported * @return array the filtered data */ protected function filter($data) { return $data; } /** * Returns the serializer for the specified model class. * @param string $modelClass fully qualified model class name * @return static the serializer */ protected function getSerializer($modelClass) { if (!isset($this->_serializers[$modelClass])) { $this->_serializers[$modelClass] = $this->createSerializer($modelClass); } return $this->_serializers[$modelClass]; } /** * Creates a serializer object for the specified model class. * * This method tries to create an appropriate serializer using the following algorithm: * * - Check if [[serializers]] specifies the serializer class for the model class and * create an instance of it if available; * - Search for a class named `XyzSerializer` under the paths specified by [[serializerPaths]], * where `Xyz` stands for the model class. * - If both of the above two strategies fail, simply return an instance of `ModelSerializer`. * * @param string $modelClass the model class * @return ModelSerializer the new model serializer */ protected function createSerializer($modelClass) { if (isset($this->serializers[$modelClass])) { $config = $this->serializers[$modelClass]; if (!is_array($config)) { $config = ['class' => $config]; } } else { $className = StringHelper::basename($modelClass) . 'Serializer'; foreach ($this->serializerPaths as $path) { $path = Yii::getAlias($path); if (is_file($path . "/$className.php")) { $config = ['class' => $className]; break; } } } if (!isset($config)) { $config = ['class' => __CLASS__]; } $config['modelClass'] = $modelClass; $config['context'] = $this->context; return Yii::createObject($config); } /** * Returns the fields of the model that need to be returned to end user * @param string|array $fields an array or a string of comma separated field names representing * which fields should be returned. * @param string|array $expand an array or a string of comma separated field names representing * which additional fields should be returned. * @return array field name => field definition (attribute name or callback) */ protected function resolveFields($fields, $expand) { if (!is_array($fields)) { $fields = preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY); } if (!is_array($expand)) { $expand = preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY); } $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->expand() as $field => $definition) { if (is_integer($field)) { $field = $definition; } if (in_array($field, $expand, true)) { $result[$field] = $definition; } } return $result; } /** * Exports an object by converting it into an array based on the given field definitions. * @param object $model the model being exported * @param array $fields field definitions (field name => field definition) * @return array the exported model data */ protected function exportObject($model, $fields) { $data = []; foreach ($fields as $field => $attribute) { if (is_string($attribute)) { $value = $model->$attribute; } else { $value = call_user_func($attribute, $model, $field); } if (is_object($value)) { $value = $this->getSerializer(get_class($value))->export($value); } elseif (is_array($value)) { foreach ($value as $i => $v) { if (is_object($v)) { $value[$i] = $this->getSerializer(get_class($v))->export($v); } // todo: array of array } } $data[$field] = $value; } return $this->filter($data); } }