CModel , C is CController , V is views from the views folder), which leads away from understanding the principle . This causes a lot of errors, for example, when the controller selects all the necessary data for the view, or when pieces of business logic are carried to the controller.CContrller . CContrller has many responsibilities. In MVC, the concept of "controller" is primarily a CController' action. In the case of performing any operation on an object, the controller does not need to know exactly how to perform this operation — this is the “M” task. In the case of the display of an object, it does not need to know how to display an object — that is the “V” task. In fact, the controller should simply take the desired object (s) and tell it (them) what to do.CModel ). This includes, for example, special validators CValidator , or SERVICES (if they reflect business logic), REPOSITORIES, and much more. The model does not need to know anything about the controllers or displays that use it. It contains only business logic and nothing else.CWidget , etc.), which would contain the display logic. But it is easy to implement it yourself (rarely needed, but sometimes it is extremely useful). public function actionUserView($id) { $user = User::model()->findByPk($id); $posts = Post::model()->findAllByAttributes(['user_id' => $user->id]); $this->render('userWithPosts', [ 'user' => $user, 'posts' => $posts ]); } CAction ), we can use it wherever we need to display a view with user data. It would look something like this: public function actions() { return [ 'showWithPost' => [ 'class' => UserViewAction::class, 'view' => 'withPost' ], 'showWithoutPost' => [ 'class' => UserViewAction::class, 'view' => 'withoutPost' ], 'showAnythingUserView' => [ 'class' => UserViewAction::class, 'view' => 'anythingUserView' ] ]; } status field. We look action: public function actionArchiveNews($id) { $news = News::model()->findByPk($id); $news->status = News::STATUS_ARCHIVE; $news->save(); } true to the inArchive field? And this action will be performed in several places of the application? And this is not news, but a transaction for $ 10 million?Find Usage for the STATUS_ARCHIVE constant. But if you did it with the help of the query "status = 'archive'" - it is much more difficult to find, because even one extra space - and you would not find this line. $posts = Post::model()->findAllByAttributes(['user_id' => $user->id]); Post and User are connected is also the business logic of the application. Therefore, this string should not occur in the controller or in the view. Here, the correct solution would be to use a relay for a User , or a scop for a Post : // $posts = $user->posts; // $posts = Post::model()->forUser($user)->findAll(); UserViewAction from the examples above: class UserViewAction extends CAction { /** * @var string view for render */ public $view; /** * @param $id string user id * @throws CHttpException */ public function run($id) { $user = User::model()->findByPk($id); if(!$user) throw new CHttpException(HttpResponse::STATUS_NOT_FOUND, "User not found"); $this->controller->render($this->view, $user); } } User model, but with any successor of CActiveRecord : class ModelViewAction extends CAction { /** * @var string model class for action */ public $modelClass; /** * @var string view for render */ public $view; /** * @param $id string model id * @throws CHttpException */ public function run($id) { $model = CActiveRecord::model($this->modelClass)->findByPk($id); if(!$model) throw new CHttpException(HttpResponse::STATUS_NOT_FOUND, "{$this->modelClass} not found"); $this->controller->render($this->view, $model); } } User class with the configurable property $modelClass As a result, we have an action that can be used to output any model using any view.$view property we can pass not a string, but an anonymous function that returns the name of the view. In an action check: if a line is in the $view , then this is a $view , if callable , then call it and get a view.renderPartial property renderPartial and render using it if necessaryAccept : if html - render the view, if json - give json class ARUpdateAction extends CAction { /** * @var string update view */ public $view = 'update'; /** * @var string model class */ public $modelClass; /** * @var string model scenario */ public $modelScenario = 'update'; /** * @var string|array url for return after success update */ public $returnUrl; /** * @param $id string|int|array model id * @throws CHttpException */ public function run($id) { $model = CActiveRecord::model($this->modelClass)->findByPk($id); if($model === null) throw new CHttpException(HttpResponse::STATUS_NOT_FOUND, "{$this->modelClass} not found"); $model->setScenario($this->modelScenario); if($data = Yii::app()->request->getDataForModel($model)) { $model->setAttributes($data); if($model->save()) Yii::app()->request->redirect($this->returnUrl); else Yii::app()->response->setStatus(HttpResponse::STATUS_UNVALIDATE_DATA); } $this->controller->render($this->view, $model); } } $modelClass property for reusability, it is complemented by several other important points:scenario for the model. This is a crucial point that many people forget. The model should know what they are going to do with it! In more detail about it I will write in the following post devoted to models.$_POST , but using Yii::app()->request->getDataForModel($model) , because the data can come in json format, or in some other way. Knowledge of the format in which data arrives and how to parse them correctly is not a controller task, it is an infrastructure task, in this case HttpRequest .http status STATUS_UNVALIDATE_DATA . It is very important. In the standard version, the code would return status 200 - which means “everything is fine”. But this is not so! If, for example, the client determines the success of the operation on the http status, then this caused problems. And since we do not know exactly how the client will work, you need to follow all http protocol rules.$view and $retrunUrl are just strings (for flexibility, it is better to make string|callable )Accept header is not checked in order to understand how to display the data and whether to redirect or simply output json$model->{$this->updateMethod}()json , which partially facilitates the task. But the problem still remains, for example, if the client sends a timestamp , and in the model - MongoDate . Providing the model with the correct data is definitely the controller's task. But the information about which types of fields are the knowledge of the model class.Yii::app()->request->getDataForModel($model) method. You can get field types in several ways, for me the most attractive ones are:AR , then we can get this information from the table schema.getAttributesTypes in the model, which will return information about the types.CModel::getAttributeNames , then traversing them with reflection to parse a comment to the field and calculate the type, saving it to the cache. Unfortunately, there are no normal annotations in php, so this is a rather controversial way. But he gets rid of writing the routine.IAttributesTypes where we define the getAttributesTypes method, and declare the HttpRequest::getDataForModel as public getDataForModel(IAttributesTypes $model) . And let each class itself determine how to implement the interface. class MongoListAction extends CAction { /** * @var string view for action */ public $view = 'list'; /** * @var array|EMongoCriteria predefined criteria */ public $criteria = []; /** * @var string model class */ public $modelClass; /** * @var string scenario for models */ public $modelScenario = 'list'; /** * @var array dataProvider config */ public $dataProviderConfig = []; /** * @var string dataProvuder class */ public $dataProviderClass = 'EMongoDocumentDataProvider'; /** * @var string filter class */ public $filterClass; /** * @var string filter scenario */ public $filterScenario = 'search'; /** * */ public function run() { // /** @var $filter EMongoDocument */ $filterClass = $this->filterClass ? $this->filterClass : $this->modelClass; $filter = new $filterClass($this->filterScenario); $filter->unsetAttributes(); if($data = Yii::app()->request->getDataForModel($filter)) $filter->setAttributes($data); $filter->search(); // , // $filter->getDbCriteria()->mergeWith($this->criteria); // . yiimongodbsuite // ( - ) /** @var $dataProvider EMongoDocumentDataProvider */ $dataProviderClass = $this->dataProviderClass; $dataProvider = new $dataProviderClass($filter, $this->dataProviderConfig); // . , self::setScenario($dataProvider->getData(), $this->modelScenario); // $this->controller->render($this->view, [ 'dataProvider' => $dataProvider, 'filter' => $filter ]); } } public function actions() { return [ 'unactive' => [ 'class' => MongoListAction::class, 'modelClass' => User::class, 'criteria' => ['scope' => User::SCOPE_UNACTIVE], 'dataProviderClass' => UserDataProvider::class ], ]; } $filterClass ), it still implies that it will be a Model . This is evidenced by calling the methods $filter->unsetAttributes() and $filter->search() , which are inherent in models.EMongoCriteria . It just has to implement this interface: interface IMongoDataFilter { /** * @param array $data * @return mixed */ public function setFilterAttributes(array $data); /** * @return EMongoCriteria */ public function getFilterCriteria(); } Filter in the method names in order not to depend on the declaration of the setAttributes and getDbCriteria in the implementation class. To use the model as a filter, it is best to write a simple treit: trait MongoDataFilterTrait { /** * @param array $data * @return mixed */ public function setFilterAttributes(array $data) { $this->unsetAttributes(); $this->setAttrubites($data); } /** * @return EMongoCriteria */ public function getFilterCriteria() { if($this->validate()) $this->search(); return $this->getDbCriteria(); } } IMongoDataFilter action to use the interface, we could use any class that implements the IMongoDataFilter interface, IMongoDataFilter it is a model or something else.$dataProviderClass .yiimongodbsuite extension, in which there is no possibility to describe relays, we need to load them manually. (in fact, it is better to add this extension, but the example is good).select the criterion). The most important thing is that we learned these settings from the action code, making them reusable.CController ) works with one class (for example, with User ). In this case, there is no special need for each action to specify the class of the model - it is easier to specify it in the controller. But in action, this opportunity must be left. public function getModelClass() { if($this->_modelClass === null) { if($this->controller instanceof IModelController && ($modelClass = $this->controller->getModelClass())) $this->_modelClass = $modelClass; else throw new CException('Model class must be setted'); } return $this->_modelClass; } /** * Class BaseARController */ abstract class BaseARController extends Controller implements IModelController { /** * @return string model class */ public abstract function getModelClass(); /** * @return array default actions */ public function actions() { return [ 'list' => ARListAction::class, 'view' => ARViewAction::class, 'create' => ARCreateAction::class, 'update' => ARUpdateAction::class, 'delete' => ARDeleteAction::class, ]; } } class UserController extends BaseARController { /** * @return string model class */ public function getModelClass() { return User::class; } } CActiveRecordand EMongoDocumentdiffer only in the way the objects are selected), duplication can be practically avoided. Such code is much easier to refactor. And it’s harder to make a bug.CWidget, CGridColumn, CGridView, Menuand more. Do not be afraid to use it all, expand, rewrite.$this.Client), his address (model Address) and orders (model Order). One client can have one address and many orders.CController::renderand the controller instance itself (accessible by $this). The less data is transferred to it - the better, because the more it is independent. This approach will make the view reusable within the project. Especially considering that views in Yii are quietly invested in each other, and can even “communicate” with each other, for example, with the help CController::$clips.Here it is necessary to retreat and pay attention to the letter "M" ofMVC.
Each subject area has its own essence and connections between them. And it is very important that our code displays them as identically as possible.
In our store, the customer owns both the address and the order. This means that in the modelClientswe have to explicitly display these relationships using properties$client->adressor methods.$client->getOrders()
It is very important. I will tell you more about this in the next post.
$this->widget('zii.widgets.CDetailView', [ 'model' => $client, 'attributes' => [ 'fio', 'age', 'address.city', 'adress.street' ] ]); foreach($client->orders as $order) { $this->widget('zii.widgets.CDetailView', [ 'model' => $order, 'attributes' => [ 'date', 'sum', 'status', ] ]); } $this->renderPartial('_client', $client); $this->renderPartial('_address', $client->address); $this->renderPartial('_orders', $client->orders); Orderis a mongo document. Will wrap in EMongoDocumentDataProvider: $this->widget('zii.widgets.grid.CGridView', [ 'dataProvider' => new EMongoDocumentDataProvider((new Order())->forClient($client)), 'columns' => ['date', 'sum', 'status'] ]); Clientand Userlocated in the subject area (thanks to scopes forClient), and knowledge of how to display data are in the view.Client, Addressand Orderare three different collections in MongoDB. In the case of withdrawal of a single client, we can easily call $client->address. This will make a query to the database, but it is inevitable.$client->addresswe will receive 100 queries to the database - this is unacceptable. Download addresses for all customers at once.AR, we would describe relays, and use them in the action criteria. But with MongoDB(more precisely, with the extension yiimongodbsuite) it will not work. class ClientsDataProvider extends EMongoDocumentDataProvider { /** * @param bool $refresh * @return array */ public function getData($refresh=false) { if($this->_data === null || $refresh) { $this->_data=$this->fetchData(); // id $addressIds = []; foreach($this->_data as $client) $addressIds[] = $client->addressId; // $adresses = Address::model()->findAllByPk($addressIds); ... .... } return $this->_data; } } class ClientsDataProvider extends EMongoDocumentDataProvider { /** * @param bool $refresh * @return array */ public function getData($refresh=false) { if($this->_data === null || $refresh) { $this->_data=$this->fetchData(); Client::loadAddresses($this->_data); } return $this->_data; } } «»:Client,Address. , Client. . , , , — -. , .Client, . .
CControllerperforms many actions unrelated to its conceptual purpose. But nevertheless in his views, his immediate presence creates confusion. I have seen many times (yes, to confess, and I did it myself), as the presentation logic was carried to the controller (some special methods for formatting or something like that) only because the controller was present in all its views. It is not right. A view must be represented by its own separate class.Source: https://habr.com/ru/post/211739/
All Articles