📜 ⬆️ ⬇️

Badoo PHP framework

The code of our site has seen more than one version of PHP. He was repeatedly supplemented, corresponded, modified, refactored - in general, he lived and developed his life. At this time, new best practices, approaches, frameworks and other similar phenomena appeared and disappeared in the world, facilitating the life of a developer and ready to solve all the main problems arising in the process of creating websites.
In this article we will talk about our path: how the code was originally organized, what problems arose and how the current framework appeared.

What happened


The project began to do in 2005. Then there were no hard and fast rules for writing code and a clearly structured framework. The code was written by several developers, they were easily guided and supported by it, each adding something different. At that time, the frameworks that are currently known were only being created, so there were few examples to follow. So we can say that our framework was formed spontaneously.

From an architectural point of view, it looked like this: there were page objects inherited from a whole hierarchy of base classes responsible for initializing the environment, session, user, etc. Each page itself decided when, how and what to display, redirect, etc. In the hierarchy of base classes, many auxiliary functions were collected for initializing and generating standard page blocks, checking users, displaying intermediate promotional pages, etc. Over time, most of them were redefined by heirs beyond recognition, which at times complicated both the understanding of how the site works and the code support itself.

There were packages — sets of classes that pages accessed to retrieve data or process in some way. There were presentations that were responsible for templating and outputting. In the usual case, each page received some data, passed them to the View class, which placed them into the structure of the blitz-template and output them. It so happened that each page had its own template (there was no base one), and it differed in the set of plug-in scripts, styles and the central part.
')
In principle, it looked like a regular MVC-like scheme. But without a clear organization of the code and with an increase in the number of developers, such code has become increasingly difficult to maintain.

What, in fact, did not suit us and needed to be improved?

1. Use global context and static variables.
On the one hand, it is convenient when you can get a global object from anywhere in the code. On the other hand, it becomes dependent, connectivity increases. With the beginning of unit testing, we realized that such code is terribly hard to test: the first test easily breaks the next one, it is necessary to follow this very tightly. In addition, code that uses global objects, and does not have only input and output, requires many mock objects for testing.

2. Most connected controllers with views.
Data preparation often took place for a specific View, i.e., for a specific template. Entire hierarchies of controllers inherited from each other, collect data for a blitz pattern in parts. Maintaining this code is extremely difficult. Creating a version with a completely different template (for example, a mobile version) sometimes became almost an impossible task, so it was easier to write everything from scratch.

3. The use of public properties as the norm.
Initially, PHP did not support private properties of objects. Since our code has a fairly large history, there are many places left where properties are declared through var, and a lot of code that uses it. It is quite normal to meet an object that is transferred to another object, and that one establishes something or produces some manipulations with the properties of the first one. Such code is very difficult to understand and debug. Ideally, you should always do getters and setters for class properties - this will save a lot of time and nerves for you and your colleagues!

4. Associative arrays as a container for passing parameters.
The big problem for us was that the data obtained from one source is transferred to some kind of handler or controller, and on the way there anything can be written in unlimited quantities. As a result, all this is constantly overgrown with new parameters and in this form is sent to the View class. Although it would be better to use some type or interface to avoid chaos.

5. The lack of a single entry point.
Each page is a separate php file containing a class inherited from the base one. If the input data in such a scheme can be controlled in one place, then it will be extremely difficult to do something massively at the output. Establishing a route other than the name of a folder, file, or containing variables requires editing nginx configs. This complicates testing in a standard workflow, requires additional access and is more difficult to maintain with a large number of developers.

New tasks for the framework


Naturally, we wanted to solve most of the above problems. We wanted to be able to "out of the box" display the same data in a different representation (JSON, mobile or web version). Prior to this, the problem was solved only by a set of IFs in each specific case.

Of course, the new framework should have been fairly easily compatible with the old code, and not cause the developers difficulties during the transition. That is why we did not switch to popular frameworks, but only used some of the ideas from them. It would be much more difficult to retrain everyone to write for a particular popular framework. In addition, we have a lot of code, standard components sharpened for us, which are in any framework, and we would not have to carry out a very simple integration.

When designing, we tried to make the framework as convenient as possible for the developer to make it easy to use auto-substitutions, code generation and other useful functions that speed up and simplify development and, importantly, refactoring.

What is the architecture of the framework?


Based on global environment variables, a Request object is created, which is passed to the application to receive a response.

$Request = new Request($_GET, $_POST, $_COOKIE, $_SERVER); $App = new Application(); $Response = $App->handle($Request); $Response->send(); 

In the course of operation, the application generates the "Received request", "Found controller", "Received data", "Caught an exception", "Render data", "Received response" events.

 $Dispatcher = new EventDispatcher(); $Dispatcher->setListeners($Request->getProject()->loadListeners()); $RequestEvent = new Event_ApplicationRequest($Request); $Dispatcher->dispatch($RequestEvent); 

For each event there is a set of subscribers who react in a special way. For example, Listener_Router on the client (mainly on HTTP_USER_AGENT) and the value REQUEST_URI finds the controller (for example, Controller_Mobile_Index) and sets it to the event object. After dispatching this event, the application either calls the controller it finds or throws the Exception_HttpNotFound exception, which will be output as a response from the 404 server. Example list of subscribers:

 $listeners = [ \Framework\Event_ApplicationRequest::class => [ [\Framework\Listener_Platform::class, 'onApplicationRequest'], [\Framework\Listener_Client::class, 'onApplicationRequest'], [\Framework\Listener_Router::class, 'onApplicationRequest'], ], ]; 

Each controller is a separate class with a set of methods - action-s. The framework finds a corresponding class and method on the route map, creates an Action object (for convenience, instead of a callable array). Example route map:

 $routes = [ Routes::PAGE_INDEX => [ 'path' => '/', 'action' => [Controller_Index::class, 'actionIndex'], ], Routes::PAGE_PROFILE => [ 'path' => '/profile/{user_login}', 'action' => [Controller_Profile::class, 'actionProfile'], ], ]; 

In the route array are the base classes. If the class has an inheritor for the current client, then it will be used. Route names in constants allow you to conveniently generate URLs anywhere in the project.

Then the event “controller found” is dispatched. The behavior of the subscribers of this event can be controlled from the controller.

 $ActionEvent = new Event_ApplicationAction($Request, $RequestEvent->getAction()); $Dispatcher->dispatch($ActionEvent); 

For example, for all controllers accessed by JavaScript, we automatically check the request-token to protect against CSRF vulnerabilities. A separate class Listener_WebService is responsible for this. But there are services for which we do not need it. In this case, the controller inherits the Listener_WebServiceTokenCheckInterface interface and implements the checkToken method:

 public function checkToken($method_name) { return true; } 

Here $ method_name is the name of the controller method to be called.

For working with data (for example, loading from a database) we have packages that are sets of classes combined in one application area. The controller receives data from the packets and, instead of using an array to transfer data to the View, sets it into a ViewModel object — in fact, into a container with a set of setters and getters. Due to this, inside the View it is always known what data is transferred. If the data set changes, all uses of the ViewModel class methods can be easily found and corrected as needed. If it were an array, we would have to search around the repository, and then among all the entries, especially if the array key is called a simple and common word, for example, “name”.

Although such an abundance of classes may seem superfluous, but in a large project with a large number of developers, this greatly helps with the support of the code. At first, we made it possible to set the data immediately in View, but quickly abandoned it, because we may have more than one project that requires the same controller, but the data is displayed differently. In any case, you will have to create a ViewModel and make additional View, so it’s better to write a little more code right away, which will save you from refactoring and additional testing in the future. Now we are considering different options for optimization, since many developers believe that there is a lot of code.

Based on the received data, View prepares the final result - this is a string or a special ParseResult object. This is done to implement deferred rendering: all the data is first prepared, and only then the final rendering is done all at once. The most frequent case is the creation of a page based on a blitz-template and some data inserted into it. In this case, the ParseResult object will contain the name of the template and an array with data ready for templating, which you need at the right time to send to Blitz and get the resulting HTML. Data for templating can contain nested ParseResult objects, so the final rendering is recursive. Here you want to warn you against using the array_walk_recursive function: it goes not only in arrays, but also in public properties of objects. In this regard, some pages have fallen from memory until we have made our own simple recursive function:

 function arrayWalkRecursive($data, $function) { if (!is_array($data)) { return call_user_func($function, $data); } foreach ($data as $k => $item) { $data[$k] = arrayWalkRecursive($item, $function); } return $data; } 

Since the communication between PHP and JavaScript is very close, we have implemented appropriate support for it. Each View object is a specific block on the site: header, sidebar, footer, central part, etc. For each block or component there can be its own JavaScript handler, which is configured using a specific data set - js_vars. For example, settings for a comet connection are sent via js_vars, through which various updates come - counters, pop-up notifications, etc. All such data is transmitted through a single point defined in the blitz-template:

 <script type="text/javascript"> $vars = {{JS_VARS}}; </script> 

In addition, we have controllers that are only accessed by JS and receive as a result JSON. We call them web services. Relatively recently, we began to describe the protocol of communication PHP and JS using Google Protocol Buffers (protobuf). Based on proto-files, PHP-classes with a set of setters are generated that automatically validate the data set in them, which allows one to formalize the agreement between the front-end and back-end developers in an optimal way. Below is an example of a proto file for describing the overlay and using the PHP class generated from it:

 package base; message Ovl { optional string html = 1; optional string url = 2; optional string type = 3; } 

 $Ovl = \GPBJS\base\Ovl::createInstance(); $Ovl->setHtml($CloudView); $Ovl->setType('cloud'); 

At the output we get JSON:

 {"$gpb":"base.Ovl","html":"here goes html","type":"cloud"} 

Among other things, the data for JS may be HTML, derived from blitz templates. Each block sets js_vars from the root, and they recursively merge into one structure using the array_replace_recursive function. An example of a structure ready for rendering:

 ParseResult Object ( [js_vars:protected] => Array ( [Sidebar] => Array ( [show_menu] => 1 ) [Popup] => Array ( [html] => ParseResult Object ( [js_vars:protected] => Array () [template:protected] => popup.tpl [tpl_data:protected] => Array ( [name] => Alex ) ) ) ) [template:protected] => index.tpl [tpl_data:protected] => Array ( [title] => Main page ) ) 

Usually the controller prepares one block on the site - its central part, and the remaining blocks either hide or show, or change their behavior in a certain way depending on the current controller. To control all the “frame” of the page, a Layout object is used (roughly speaking, this is the base View object), which sets the building blocks and the central part to the ParseResult object for the base template. To declare which Layout will be used, the controller inherits the special HasLayoutInterface interface and implements the getLayout method.

In addition to assembling the entire page, Layout has an additional function: it forms the result in the form of JSON for “seamless” transitions between pages. Our website has been working for quite a long time as a web application: transitions between pages are performed without reloading the entire page, only certain elements change (URL, title, central part, certain blocks are shown or hidden).

Integration


The first and one of the most difficult tasks in the transition to a new framework was the need to create a unified initialization of the site. Initially, we checked the work on one of the simple pages and started from the old framework where all initialization was performed.

 class TestPage extends CommonPage { public function run() { \Framework\Application::run(); //    } } $TestPage = new TestPage(); $TestPage->init(); //    $TestPage->run(); 

To make an independent launch of the new framework and leave a single initialization, it was necessary to take everything that happens in init () into separate classes and use both there and there. This was done in stages, and in the end we got about 40 classes.

After that, for the sample, we transferred several small projects. One of the first was the translation for our Chrome browser extension ( Badoo Chrome Extension ). And the first big project was the site Hot Or Not , entirely written in the new framework. We are currently moving to our main Badoo website.

Projects fully implemented on the new framework work through the front controller, that is, the single entry point - index.phtml. For badoo.com we have a lot of rules in nginx, the last of which sends to the profile. Those. badoo.com/something will either open something’s user profile, or return 404. That’s why, until the profile is completely transferred to the new framework, we still have a lot of * .phtml files that contain only the launch of the framework.

 <?php /*... includes ...*/ \Framework\Application::run(); 

Earlier in these files was the code on the old framework. After refactoring, the code was transferred to the controllers of the new framework, but the files themselves cannot be deleted. They must exist in order for nginx to run them and not send a request for a profile.

Conclusion


As a result, we decided to have a fairly large amount of problems: we created a single entry point, removed many levels of inheritance, minimized the use of the global context as much as possible, the code became more object and typed. Basically, the changes affected the routing and the whole process of web scripts, from the request to the server response. Also, a radically new data presentation system has appeared. We tried to create a unified scheme for data access, but have not yet found a solution that would completely suit us. But the result can be safely considered as a confident step towards convenient development and support of the code.

Alexander Treg Treger, developer.

Source: https://habr.com/ru/post/238987/


All Articles