Proper Use of Yii
Introduction
In fact, the title should have a question mark. For a long time I did not code both on yii and php in general. Now, having returned, I want to rethink my principles of development, to understand where to go next. And the best way is to present them and put them on the review for professionals, which I do in this post. Despite the fact that I pursue purely selfish goals, the post will be useful to many beginners, and not even beginners.
Design and concepts
In the text, the concepts of “controller” and “model” will appear in two contexts: MVC and Yii, pay attention to this. In non-obvious places, I will explain what context I use.
A view is a view in the context of MVC.
"View" is a file from the views folder.
I will highlight patterns in CAPITAL letters.
Go!
Yii is a very flexible framework. This makes it possible for some developers not to worry about structuring their code, which always leads to a bunch of bugs and complicated refactoring. However, Yii has nothing to do with it - quite often problems begin already with a banal misunderstanding of the MVC principle.
Therefore, in this post I will look at the basics of MVC, and its C and V in the context of Yii. The letter M is a separate complex topic that deserves its post. All code examples will be commonplace, but reflect the essence of the principles.
MVC
MVC is an excellent design principle that helps to avoid many problems. In my opinion, the necessary, sufficient knowledge about this design pattern can be gleaned from a Wikipedia article.
Unfortunately, I have seen more than once that the expression “Yii is an MVC framework” was taken too verbatim (that is, M is this
CModel
, C is this CController
, V is the folder view views
), which leads away from understanding the principle itself. This generates a lot of errors, for example, when all the necessary data for the view is selected in the controller, or when pieces of business logic are transferred to the controller. Controller (“C”) is the operational level of the application. Do not confuse it with the class
CContrller
. CContrller
endowed with many responsibilities. In MVC, the concept of “controller” is primarily actionCController'а
. In the case of performing any operation on the object, the controller does not need to know exactly how to perform this operation - this is task “M”. In the case of displaying an object, he should not know how to display the object - this is task “V”. In fact, the controller should simply take the desired object (s), and tell it (them) what to do. The model (“M”) is the level of the business logic of the application. It is dangerous to associate the concept of a model in Yii with the concept of a model in MVC. A model is not only entity classes (as a rule
CModel
). This, for example, includes special validators CValidator
, or SERVICES (if they display business logic), REPOSITORIES, and much more. The model does not need to know anything about the controllers or mappings using it. It contains only business logic and nothing more. Presentation ("V")- display level. You should not perceive it as just a php file for display (although, as a rule, it is). He has his own, sometimes very complex, logic. And if we need some specific data, for example, a list of languages or something else, to display an object, this level should request it. Unfortunately, in Yii, you cannot associate a view with any particular class (unless with the help
CWidget
, etc.), which would contain the display logic. But this is easy to implement yourself (rarely needed, but sometimes extremely useful). Yii himself provides us with a chic infrastructure for all these three levels.
Common MVC Errors
Here are a couple of typical mistakes. These examples are extremely exaggerated, but they reflect the essence. On a large application scale, these errors grow into catastrophic problems.
1. Suppose we need to display the user with his posts. A typical action looks something like this:
public function actionUserView($id)
{
$user = User::model()->findByPk($id);
$posts = Post::model()->findAllByAttributes(['user_id' => $user->id]);
$this->render('userWithPosts', [
'user' => $user,
'posts' => $posts
]);
}
This is a mistake. The controller does not need to know how the user will be displayed. He must find the user, and tell him "show up with this view." Here we take out part of the display logic in the controller (namely, the knowledge that she needs posts).
The problem is that if you do as in the example, you can forget about code reuse and catch widespread duplication.
Wherever we want to use this view, we will have to transfer to it a list of posts, which means that everywhere we will have to select them in advance - code duplication.
Also, we will not be able to reuse this action. If you remove a selection of posts from it, and make the name of the view a parameter (for example, implementing it in the form
CAction
) - we can use it wherever we need to display any 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'
]
];
}
If you interfere with the controller and display - this is not possible.
This error creates only duplication of code. The second mistake has far more catastrophic consequences.
2. Suppose we need to transfer the news to the archive. This is done by setting the field
status
. Watch the action: public function actionArchiveNews($id)
{
$news = News::model()->findByPk($id);
$news->status = News::STATUS_ARCHIVE;
$news->save();
}
The error of this example is that we transfer the business logic to the controller. This also leads to the inability to reuse the code (I will explain why below), but this is only a trifle compared to the second problem: what if we change the way the archive is transferred? For example, instead of changing the status, we will assign the
true
field inArchive
? And this action will be performed in several places of the application? And this is not news, but a $ 10 million transaction? In the example, these places are easy to find - just do it
Find Usage
for a constant STATUS_ARCHIVE
. But if you did this with the help of a query, it "status = 'archive'"
is much more difficult to find, because even one extra space is needed and you would not find this line.Business logic should always remain in the model. Here we should single out a separate method in essence, which translates the news into an archive (or something else, but in the layer of business logic). This example is extremely exaggerated, few make a similar mistake.
But in the example from the first error, there is also this problem, much less obvious:
$posts = Post::model()->findAllByAttributes(['user_id' => $user->id]);
Knowledge of how exactly connected
Post
and User
- this is also the business logic of the application. Therefore, this line should not occur in the controller or in the view. The correct solution here would be to use relays for User
, or scopes for Post
: // релейшн
$posts = $user->posts;
// скоуп
$posts = Post::model()->forUser($user)->findAll();
Magic CAction
Controllers (in MVC terminology, in Yii terminology are actions) are the most reusable part of applications. They carry almost no application logic. In most cases, they can be safely copied from project to project.
Let's see how it can be implemented
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);
}
}
Now we can set any view in the action config. This is a good example of code re-usability, but it is not perfect. We modify the code so that it works not only with the model
User
, but with any heir 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);
}
}
In fact, we just replaced the hard-coded class
User
with a configurable property. $modelClass
As a result, we got an action that can be used to output any model using any view. At first glance, it is not flexible, but this is just an example for understanding the general principle. PHP is a very flexible language, and it gives us room for creativity:
$view
we can pass not a string to a property , but an anonymous function that returns the name of the view. In the action, check: if in a$view
line - this is the view, ifcallable
- then call it and get the view.- make a
boolean
propertyrenderPartial
and render using it if necessary - check the title for
Accept
: ifhtml
- render the view, if json - givejson
- many many other things
Similar actions can be written for almost any action: CRUD, validation, business operations, working with related objects, etc.
In fact, it’s enough to write about 30-40 similar actions that will cover 90% of the controller code (naturally, if you separate the model, view and controller). The most pleasant plus, of course, is the reduction in the number of bugs, because much less code + easier to write tests + when the action is used in hundreds of places they pop up much faster.
Action example for Update
Let me give you a couple more examples. Here is the action on update
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);
}
}
I took his code from CRUD gii, and reworked it a bit. In addition to introducing the property
$modelClass
for reusability, it is supplemented by several other important points:- Installation
scenario
for the model. This is an extremely important point that many people forget. The model should know what they are going to do with her! I will write more about this in the next post devoted to models. - Receiving data is not from
$_POST
, but usingYii::app()->request->getDataForModel($model)
, because the data can come in ajson
format, or in some other way. Knowing what format the data comes in and how to parse it correctly is not the task of the controller, it is the task of the infrastructure, in this caseHttpRequest
. - If validation fails (which is in the save method), the
http
status is setSTATUS_UNVALIDATE_DATA
. It is very important. In the standard version, the code would return a status200
- which means "everything is fine." But this is not so! If, for example, the client determines the success of the operation byhttp
status, then this caused problems. And since we don’t know how the client will work, we must follow all thehttp
protocol rules .
Naturally, this controller is much simpler than the real one:
$view
and$retrunUrl
- just strings (for flexibility they are best donestring|callable
)- the header is not checked
Accept
to understand in what form to output data and whether to do a redirect or just outputjson
- The model method for saving is hard-coded. For example, it would be more flexible to do this:
$model->{$this->updateMethod}()
- much more
Another important point that is omitted here is to cast the input data to the necessary types. Now data is usually sent in
json
, which partially facilitates the task. But the problem still remains, for example, if the client sends timestamp
, and in the model - MongoDate
. Providing the model with the correct data is definitely the controller's job. But the information about what types of fields is the knowledge of the model class. In my opinion, the best place to perform a cast is a method
Yii::app()->request->getDataForModel($model)
. There are several ways to get field types, for me the most attractive ones are:- If we have
AR
- then we can get this information from the table schema. - Make a method in the model
getAttributesTypes
that returns type information. - Reflection, namely, obtaining using a
CModel::getAttributeNames
list of attributes, then bypassing them with reflection in order to parse the comment on the field and calculate the type, save it to the cache. Unfortunately, there are no normal annotations in php, so this is a pretty controversial way. But he saves from writing a routine.
In any case, we can make an interface
IAttributesTypes
where to define the method getAttributesTypes
, and declare the method HttpRequest::getDataForModel
as public getDataForModel(IAttributesTypes $model)
. And let each class itself determine how it implements the interface.Action example for List
Perhaps this is the most complex example, I will give it to show the division of responsibilities between classes:
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
]);
}
}
And an example of its use, displaying inactive users:
public function actions()
{
return [
'unactive' => [
'class' => MongoListAction::class,
'modelClass' => User::class,
'criteria' => ['scope' => User::SCOPE_UNACTIVE],
'dataProviderClass' => UserDataProvider::class
],
];
}
The logic of the work is simple: we get the filtering criterion, make a data provider and display.
Filter:
For simple filtering by attribute value, it is enough to use a model of the same class. But usually filtering is much more complicated - it can have its own very complex logic, which may well make a bunch of database queries or something else. Therefore, it is sometimes wise to inherit the filter class from the model, and implement this logic there.
But the only purpose of the filter is to obtain the criteria for sampling. The implementation of the filter in the example is not entirely successful. The fact is that despite the ability to set the filter class (using
$filterClass
), it still implies that it will be СModel
. This is evidenced by the call of methods $filter->unsetAttributes()
and $filter->search()
, which is inherent in models.The only thing the filter needs is to receive input and give
EMongoCriteria
. It just needs to implement this interface:interface IMongoDataFilter
{
/**
* @param array $data
* @return mixed
*/
public function setFilterAttributes(array $data);
/**
* @return EMongoCriteria
*/
public function getFilterCriteria();
}
Filter
in the names of the methods I inserted so as not to depend on the declaration of the methods setAttributes
and getDbCriteria
in the implementing class. To use the model as a filter, it is best to write a simple trait: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();
}
}
By rewriting the action for using the interface, we could use any class that implements the interface
IMongoDataFilter
, it doesn’t matter if it is a model or something else. Data provider:
Everything regarding the logic of selecting the necessary data - the data provider is responsible for this. Sometimes it also contains quite complex logic, so it makes sense to configure its class with
$dataProviderClass
. For example, in the case of an extension
yiimongodbsuite
in which there is no way to describe the relays, we need to load them manually. (in fact, it’s better to add this extension, but the example is good).The loading logic can also be placed in some REPOSITORY class, but if it is the responsibility of a particular data provider to return data along with relays, the data provider must call the REPOSITORY loader method. I will write about the reusability of data providers below.
Criteria for using the action:
I want to once again draw attention to the most “bug-generating” problem:
Knowing who you want to display (in this case, inactive users) is knowledge of the controller. But the knowledge about the criteria by which an inactive user is determined is the knowledge of the model.
In the example of using the action, everything is done correctly. With the help of the scope, we indicated who we want to output, but the scope itself is in the model.
In fact, scope is a “piece” of SPECIFICATIONS. You can easily rewrite action to work with specifications. Although, this is in demand only in complex applications. In most cases, scope is the perfect solution.
About the separation of controller and view:
Sometimes it is not practical to completely separate the view from the controller. For example, if we need only a few model attributes to display the list, it’s silly to select the entire document. But these are the features of specific actions, which are configured using configuration (in this case, by assigning
select
a criterion). The most important thing is that we removed these settings from the action code, making them reusable.Action Bundle with Model Class
In most cases, the controller (exactly
CController
) works with one class (for example, with User
). In this case, there is no special need for each action to specify the model class - it is easier to specify it in the controller. But in the action this opportunity must be left. To resolve this situation, in the action you need to register a getter and setter for $ modelClass. The getter will look like this:
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;
}
In principle, you can even make a controller blank for standard CRUD:
/**
* 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,
];
}
}
Now we can do a CRUD controller in several lines:
class UserController extends BaseARController
{
/**
* @return string model class
*/
public function getModelClass()
{
return User::class;
}
}
Controller Summary
A large set of customizable actions reduces code duplication. If ekshenov split classes to clear structure (e.g., editing action
CActiveRecord
, and EMongoDocument
differ only in the way the sample objects) - can practically avoid duplication. Such code is much easier to refactor. And it’s harder to make a bug in it. Of course, such actions can not cover absolutely all the needs. But a significant part of them is definitely yes.
Representation
Yii gives us the chic infrastructure to build it. This
CWidget
, CGridColumn
, CGridView
, СMenu
and more. Do not be afraid to use all this, expand, rewrite it. This is all easy to learn by reading the documentation, but I want to clarify something else.
I mentioned above that the controller does not need to know exactly how the entity will be displayed, therefore it should not contain code for fetching data for the view. I am well aware that this statement will cause a lot of protests - everyone always prepares data in controllers. Even Yii himself hints at us that the controller and view are connected, passing the instance of the controller to the view as a quality
$this
.But this is not so. On the controller side, the benefits of getting rid of high connectivity with views are obvious. But what to do with the views? I will answer this question here.
I will consider two general cases: a representation of an entity with related data, and a list of entities. The examples are trivial, but they will explain the point.
Let's say we have an online store. There is a client (model
Client
), his address (model Address
) and orders (model Order
). One customer can have one address and many orders.Representation of an entity with related data
Suppose we need to display information about the client, his address, and a list of his orders.
In fact, each view has its own “interface”. These are the data transferred to it from
CController::render
and the controller instance itself (available on $this
). The less data is transferred to it, the better, because the more independent it is. This approach will make the view reusable within the project. Especially considering that in Yii, views are quietly nested, and can even “communicate” with each other, for example, using CController::$clips
. Necessary-sufficient data to display our view is the client object. Having it, we will calmly receive all other data.
Here you should make a digression and pay attention to the letter "M" fromMVC
.
Each subject area has its own essences 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 must explicitly display these relationships using properties$client->adress
or methods.$client->getOrders()
This is very important. I will tell you more about this in the next post.
If the subject area is correctly designed, we will always have an easy way to get related data. And this absolutely solves the problem that the controller did not give us the list of orders.
In this case, the output code is as simple as possible:
$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',
]
]);
}
If we decide to split this view so that we can use its parts independently later, the code will be like this:
$this->renderPartial('_client', $client);
$this->renderPartial('_address', $client->address);
$this->renderPartial('_orders', $client->orders);
This code is simple, but it has a drawback - if the client has many orders, you need to display it with pagination.
No one is stopping us from stuffing all this into a date provider. Let's say a model
Order
is a mongo document. We will wrap in EMongoDocumentDataProvider
: $this->widget('zii.widgets.grid.CGridView', [
'dataProvider' => new EMongoDocumentDataProvider((new Order())->forClient($client)),
'columns' => ['date', 'sum', 'status']
]);
Creating a data provider in the view is somewhat unusual. But in fact, everything is in place: the Controller has already fulfilled its duties, the knowledge of how connected
Client
and User
located in the subject area (thanks to the scope forClient
), and the knowledge of how to display data is in the view. In fact, some of my colleagues, seeing this, were spinning at the temple - creating a data provider in the view - what kind of nonsense? At the same time, they themselves performed similar actions in widgets, not realizing that the widget is, first of all, the presentation infrastructure.
A widget is a great tool for creating a reusable and flexible presentation, as well as a logical demarcation. But its purpose is representation, therefore there is no conceptual difference where the above code is located - in the widget or in the view.
Entity List View
Representation of the list of entities differs from the representation of a specific entity only in data sampling.
Let's say that
Client
, Address
and Order
are three different collections in MongoDB
. In case of withdrawal of one client, we can safely call $client->address
. This will query the database, but it is inevitable. If we select 100 clients, and for each
$client->address
we call - we will receive 100 database queries - this is unacceptable. You need to download addresses for all clients at once. If we used
AR
, we would describe the releases, and use them in the action criteria. But with MongoDB
(more precisely, with the extension yiimongodbsuite
) this will not work.The best place to sample additional data is a data provider. He, as an object designed to select data, must know what data should return and how to select it.
It is done somehow like this:
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;
}
}
There are 2 problems here:
- it contains domain knowledge
- address loading code cannot be reused
The solution is to move the loading code to the REPOSITORY, which may be the model class itself.
If we move it there, then our data provider will look like this:
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;
}
}
Now everything is in place.
Отступление к «М»:
В качестве РЕПОЗИТОРИЯ мы могли использовать как классClient
, так иAddress
. Но существует четкая причина, почему я использовал именно Client. В нашей предметной области адрес абсолютно не важен вне контекста пользователя. Несмотря на то, что адрес имеет и свою коллекцию, и свой класс, логически он — всего лишь ОБЪЕКТ-ЗНАЧЕНИЕ. Поэтому он не должен знать ничего о том, кому принадлежит. Размещая код подгрузки адресов вClient
, мы избавляемся от двухсторонней связи классов. А это всегда хорошо.
Реюзабельность дата-провайдеров
Data providers are also reusable (within the application). Let's say we have 2 actions: displaying a list of orders, and the above user page, where a list of orders is also displayed.
In both cases, we can use the same data provider, which will load us the necessary data.
I also see no reason not to make them configurable.
Controller like $ this in views
In my opinion, this is a mistake. Of course, a class
CController
performs many actions that are not related to its conceptual purpose. But still in the views, his immediate presence creates confusion. I saw many times (but to be honest, I did it myself) how the presentation logic was transferred 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. View should be presented as its own separate class.Conclusion
All examples are greatly simplified. The real class of controllers, the structure of the models are much larger.
This is too complicated and confusing - many will think so. Many, having sat down to work for such a code, without understanding the structure, simply cut it out and write “in a simple way”.
This is understandable - I just described the interaction of several classes - and already wild confusion, the simplest code to implement is scattered across a bunch of files. But in reality - this is a clear and logical structure of classes in which each line is in its place.
Perhaps a small project will ruin this approach. It takes a pretty decent time to write one infrastructure. But for the big - this is the only chance to survive.
Afterword
Despite the fact that the post is called "how to do it right", it does not claim to be correct. I myself do not know how to. It is an attempt to convey that we need a more meaningful approach to the design of classes and their interaction.
PHP developers gave us the most powerful language. The Yii developers gave us a great framework. But look around - representatives of other languages and frameworks consider us to be shortcoders. PHP and Yii - we dishonor them.
With our negligent attitude to design, a banal ignorance of the basic principles of MVC, object-oriented design, the language we are writing in, and the framework we use, we summarize PHP with all this. We fail Yii. We bring the companies for which we work, and which provide us. But the most important thing is that we let ourselves down.
Think it over.
Good to all.