
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 codeObsolete 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:
- The boss comes running screaming, “Nothing works with us, the main client is at risk!”;
- or a manager with a request to fasten an unrealizable chip;
- 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
- Translate the project on modern architecture
- 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:
- We deploy two systems in parallel (kohana + symfony)
- Change the entry point to a new one (symfony)
- 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:
- Rename app.php to app_kohana.php
- The entry point of the symphony is placed in app.php
- 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 $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 { 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.
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
- We have taken the first step for the further development of the system.
- The new system is independent of the old. All new code works without old
- Minimum time spent
Implementation minuses
- Additional overhead resources on the "wrapper" while working with the old part of the system. However, compared to the delays in the old system, the overhead projector (both in memory and processor) can be neglected.
- The new system is independent of the old. We cannot use the old code in the new one, but this is rather a plus, since we decided to rewrite.
- We have to maintain the model in two places.
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: (