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; } }
CActiveRecord
and EMongoDocument
differ 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
, Menu
and 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::render
and 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 modelClients
we have to explicitly display these relationships using properties$client->adress
or 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);
Order
is a mongo document. Will wrap in EMongoDocumentDataProvider
: $this->widget('zii.widgets.grid.CGridView', [ 'dataProvider' => new EMongoDocumentDataProvider((new Order())->forClient($client)), 'columns' => ['date', 'sum', 'status'] ]);
Client
and User
located in the subject area (thanks to scopes forClient
), and knowledge of how to display data are in the view.Client
, Address
and Order
are 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->address
we 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
, . .
CController
performs 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