📜 ⬆️ ⬇️

How to rewrite a big project or refactoring that is painless for business


The most frequently asked question for me is how to talk about refactoring with a manager?
In such cases, I give somewhat controversial advice: do not tell him anything!
Martin Fowler, Refactoring. Improving existing code

Obsolete code, support difficulties, unpredictable bugs - these terms appear one after another in the developer’s life as the product is developed. And if the first is more likely the interests of the developer, then the last is a direct business problem.

In this article, I want to share the experience of rewriting a large project and, as a bonus, bring a couple of pieces of code that helped us and, I hope, help you begin this interesting way.

Debriefing


Problems


They usually start in a known scenario:
  1. The boss comes running screaming, “Nothing works with us, the main client is at risk!”;
  2. or a manager with a request to fasten an unrealizable chip;
  3. less often, we, the developers, are so tired of digging into the “Legacy” shit code that we decide to rewrite everything.

And usually it ends with general indignation and frustration, because the chip is needed urgently, clients cannot wait either, and because of the sad heritage the team tends to run up. The situation is spoiled by the lack of “money for refactoring” (inaction of the team in terms of business)
')
As for the last point, I need to add that I don’t consider the situation with a new person in the team who is eager to rewrite everything, but he can easily argue the described approach for the project development.

Tasks


  1. Translate the project on modern architecture
  2. Ensure minimal refactoring costs

Implementation scheme


Our project was originally written in Kohana, we rewrote it in Symfony2, so all the examples are given in the context of these systems. However, this approach can be applied with any frameworks. The necessary requirement is a single entry point to the application.

Initially, the application handles user requests through the app_kohana.php entry point.

We will wrap the entry point in the new system, organizing a kind of “proxy”.

Refactoring


Controller - wrapper for the old system


The idea is quite simple and is as follows:
  1. We deploy two systems in parallel (kohana + symfony)
  2. Change the entry point to a new one (symfony)
  3. We organize a universal controller, which by default will forward all requests to the old system


And if there shouldn't be any problems with the first two points, the third one is of interest, because it can reveal pitfalls.

The first thing that comes to mind is to wrap the include in ob_start. So do:
class PassThroughController extends Symfony\Bundle\FrameworkBundle\Controller\Controller { public function kohanaAction() { ob_start(); $kohanaPath = $this->container->getParameter('kernel.root_dir') . '/../app_kohana.php'; include $kohanaPath; $response = ob_get_clean(); return new Response($response); } } 

Routing for universal controller
 application.passthrough_kohana: path: /{slug} defaults: _controller: ApplicationBundle:PassThrough:kohana requirements: slug: .* 



In this format, the system is already working, but after some time, the first bug arrives. For example, incorrect handling of ajax errors. Or on the site errors are given with the code 200 instead of 404.

Here we understand that the buffer swallows headers, so they need to be processed explicitly.
 class PassThroughController extends Symfony\Bundle\FrameworkBundle\Controller\Controller { public function kohanaAction() { ob_start(); $kohanaPath = $this->container->getParameter('kernel.root_dir') . '/../app_kohana.php'; include $kohanaPath; $headers = headers_list(); $code = http_response_code(); $response = ob_get_clean(); return new Response($response, $code, $headers); } } 

After this flight is normal.

The problems of the old system affecting the functioning of the new


exit ()


We had places in the system where, at the end of the controller, exit () was joyfully called. This is practiced, for example, in Yii (CApplication :: end ()). It does not deliver a particular headache until you start using the event model in the new system and handle events that occur after the execution of the controller. The most striking example is the Symfony Profiler, which stops working for requests with exit.
This case must be borne in mind and, if necessary, take appropriate measures.

ob_end _ * ()


Reckless use of ob_end functions can easily break the work of the new system by clearing the buffer of the new proxy controller. It should also be borne in mind.

Kohana_Controller_Template :: $ auto_render


The variable is responsible for the automatic drawing of the data received from the controller in the global template (it can strongly depend on the template used). During the adaptation of a new system, this can save debugging time in places where, for example, json is outputted by a simple echo $ json; exit (); . The controller will take approximately the following form:
 $this->auto_render = false; echo $json; return; 


What else is worth taking care


The entry points described above are an ideal situation. Our initial entry point was app.php and it was required that it should remain after refactoring (reconfiguration of numerous servers looked hopeless). The following algorithm was chosen:
  1. Rename app.php to app_kohana.php
  2. The entry point of the symphony is placed in app.php
  3. Profit

And everything seemed to be started, except for the console commands that were launched in Kohana through the same file. Therefore, at the beginning of the new app.php, the following crutch was born for backward compatibility:
 if (PHP_SAPI == 'cli') { include 'app_kohana.php'; return; } 


Life after refactoring


New controllers


We try to write all new controllers in symfony. The separation takes place at the routing level, the necessary one is added to the “universal” route, and Kohana does not load further. While we are writing in the new system only ajax-controllers, so the issue with reusing patterns (Twig) remains open.

DB and Configuration


To access the database, models from the current database were generated using standard Doctrine methods. New methods of working with the database are added to the repository as necessary. However, the configuration of the connection to the database is used existing from Kohana. To do this, a configuration file has been written that pulls data from the Kohang config and converts it into symphony configuration parameters. The logic of the search for the config, depending on the platform, alas, is duplicated in order not to connect the Kohana classes in the new system.
Application / Resources / config / kohana.php
 /** @var \Symfony\Component\DependencyInjection\ContainerBuilder $container */ $kohanaDatabaseConfig = []; $kohanaConfigPath = $container->getParameter('kernel.root_dir') . '/config'; if (!defined('SYSPATH')) { define('SYSPATH', realpath($container->getParameter('kernel.root_dir') . '/../vendor/kohana/core') . '/'); } $mainConfig = $kohanaConfigPath . '/database.php'; if (file_exists($mainConfig)) { $kohanaDatabaseConfig = include $mainConfig; } if (isset($_SERVER['PLATFORM'])) { $kohanaEnvConfig = $kohanaConfigPath . '/' . $_SERVER['PLATFORM'] . '/database.php'; if (file_exists($kohanaEnvConfig)) { $kohanaDatabaseConfig = array_merge($kohanaDatabaseConfig, include $kohanaEnvConfig); } } if (empty($kohanaDatabaseConfig['default'])) { throw new \Symfony\Component\Filesystem\Exception\FileNotFoundException('Could not load database config'); } $dbParams = $kohanaDatabaseConfig['default']; $container->getParameterBag()->add([ 'database_driver' => 'pdo_mysql', 'database_host' => $dbParams['connection']['hostname'], 'database_port' => null, 'database_name' => $dbParams['connection']['database'], 'database_user' => $dbParams['connection']['username'], 'database_password' => $dbParams['connection']['password'], ]); 

The config is connected in the standard way
Application / DependencyInjection / ApplicationExtension.php
 class ApplicationExtension extends Symfony\Component\HttpKernel\DependencyInjection\Extension { public function load(array $configs, ContainerBuilder $container) { $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('kohana.php'); } } 

How to continue: making functional in services


Further movement from Kohana to a symphony fits very well into rendering the functional to symphony services and using them in the old system through a DI-container. It so happened that we began to use the DI component before connecting the symphony to the project, so this process went quite smoothly, but nothing prevents you from doing it from scratch. The main task will be to throw the DI-container from the symphony into the kohana. We did this in Kohana-style through a static property; in another framework, you can find the appropriate approach.

Override the Kohans system class, add a property for the container there.
 class Kohana extends Kohana_Core { /** * @var Symfony\Component\DependencyInjection\ContainerBuilder */ public static $di; } 

And then you need to turn a couple more frauds to put in this property a DI-container between the initialization of the kohana and the execution of the controller code. To do this, we divide our app_kohana.php initialization file into two parts, highlighting directly the initialization of the system and the launch of the controller itself.

 /** app_kohana_init.php */ //   ,     bootstrap /** app_kohana_run.php */ echo Request::factory(TRUE, array(), FALSE) ->execute() ->send_headers(TRUE) ->body(); /** app_kohana.php */ include 'app_kohana_init.php'; include 'app_kohana_run.php'; 


We modify our controller by doing operations similar to app_kohana.php, but adding container forwarding between inclusions
 public function kohanaAction() { ob_start(); $kohanaPath = $this->container->getParameter('kernel.root_dir') . '/..'; include $kohanaPath . '/app_kohana_init.php'; \Kohana::$di = $this->container; include $kohanaPath . '/app_kohana_run.php'; $headers = headers_list(); $code = http_response_code(); $response = ob_get_clean(); return new Response($response, $code, $headers); } 


After that, we in the old system can use the DI-container and all the services declared in the new system, including the EntityManager and the new doctrine models.

At last


Pros of implementation




Implementation minuses




Thank you for reading to the end, I wish you success in refactoring, brush away the accumulated dust from the old code!
And sorry for the terrible fonts on the diagrams: (

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


All Articles