123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- <?php
- /**
- * @link http://www.yiiframework.com/
- * @copyright Copyright (c) 2008 Yii Software LLC
- * @license http://www.yiiframework.com/license/
- */
- namespace yii\data;
- use Yii;
- use yii\base\Component;
- use yii\base\Model;
- use yii\helpers\StringHelper;
- /**
- * ModelSerializer converts a model or a list of models into an array representation with selected fields.
- *
- * Used together with [[\yii\web\ResponseFormatter]], ModelSerializer can be used to serve model data
- * in JSON or XML format for REST APIs.
- *
- * ModelSerializer provides two methods [[export()]] and [[exportAll()]] to convert model(s) into array(s).
- * The former works for a single model, while the latter for an array of models.
- * During conversion, it will check which fields are requested and only provide valid fields (as declared
- * in [[fields()]] and [[expand()]]) in the array result.
- *
- * @author Qiang Xue <[email protected]>
- * @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);
- }
- }
|