From b91249ff99053c8b166e635a74f4407f1637eeda Mon Sep 17 00:00:00 2001 From: Denis Brumann Date: Sun, 7 Apr 2019 10:06:14 +0200 Subject: [PATCH] Creates a migration guide. The general idea is to provide advice on how to migrate an existing application over to Symfony using some proven approaches. The page should provide around 3 alternatives and some discussion when they might be suitable as well as additional resources like videos on this topics from the official Symfony YouTube-channel. --- index.rst | 1 + migration.rst | 416 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 migration.rst diff --git a/index.rst b/index.rst index df3e3ac12e7..72e414e4c32 100644 --- a/index.rst +++ b/index.rst @@ -44,6 +44,7 @@ Topics logging mercure messenger + migration performance profiler routing diff --git a/migration.rst b/migration.rst new file mode 100644 index 00000000000..86b4feb0225 --- /dev/null +++ b/migration.rst @@ -0,0 +1,416 @@ +.. index:: + single: Migration + +Migrating an Existing Application to Symfony +============================================ + +When you have an existing application that was not built with Symfony, +you might want to move over parts of that application without rewriting +the existing logic completely. For those cases there is a pattern called +`Strangler Application`_. The basic idea of this pattern is to create a +new application that gradually takes over functionality from an existing +application. This migration approach can be implemented with Symfony in +various ways and has some benefits over a rewrite such as being able +to introduce new features in the existing application and reducing risk +by avoiding a "big bang"-release for the new application. + +Prerequisites +------------- + +The following steps do not require you to have the new Symfony +application in place and in fact it might be safer to introduce these +changes beforehand in your existing application. + +Before you start introducing Symfony to the existing application, you +have to ensure certain requirements are met by your existing application. +By extension this means that you will have to decide which version you +are aiming to migrate to, either a current stable release or the long +term support version (LTS). The main difference is, how frequently you +will need to upgrade in order to use a supported version. In the context +of a migration, other factors, such as the supported PHP-version or +support for libraries/bundles you use, may have a strong impact as well. +Using the most recent, stable release will likely give you more features, +but it will also require you to update more frequently to ensure you will +get support for bug fixes and security patches and you will have to work +faster on fixing deprecations to be able to upgrade. + +.. tip:: + + When upgrading to Symfony you might be tempted to also use + :doc:`Flex `. Please keep in mind that it primarily + focuses on bootstrapping a new Symfony application according to best + practices regarding the directory structure. When you work in the + constraints of an existing application you might not be able to + follow these constraints, making Flex less useful. + +First of all your environment needs to be able to support the minimum +requirements for both applications. In other words, when the Symfony +release you aim to use requires PHP 7.1 and your existing application +does not yet support this PHP version, you will probably have to upgrade +your legacy project. You can find out the +:doc:`requirements ` for running Symfony and +compare them with your current application's environment to make sure you +are able to run both applications on the same system. Having a test +system, that is as close to the production environment as possible, +where you can just install a new Symfony project next to the existing one +and check if it is working will give you an even more reliable result. + +.. tip:: + + If your current project is running on an older PHP version such as + PHP 5.x upgrading to a recent version will give you a performance + boost without having to change your code. + +In older PHP applications it was quite common to rely on global state and +even mutate it during runtime. This might have side effects on the newly +introduced Symfony application. In other words code relying on globals +in the existing application should be refactored to allow for both systems +to work simultaneously. Since relying on global state is considered an +anti-pattern nowadays you might want to start working on this even before +doing any integration. + +Another point you will have to look out for is conflicts between +dependencies in both applications. This is especially important if your +existing application already uses Symfony components or libraries commonly +used in Symfony applications such as Doctrine ORM, Swiftmailer or Twig. +A good way for ensuring compatibility is to use the same ``composer.json`` +for both project's dependencies. + +Once you have introduced composer for managing your project's dependencies +you can use its autoloader to ensure you do not run into any conflicts due +to custom autoloading from your existing framework. This usually entails +adding an `autoload`_-section to your composer.json and configuring it +based on your application and replacing your custom logic with something +like this:: + + require __DIR__ . '/vendor/autoload.php'; + +There might be additional steps you need to take depending on the libraries +you use, the original framework your project is based on and most importantly +the age of the project as PHP itself underwent many improvements throughout +the years that your code might not have caught on to, yet. As long as both +your existing code and a new Symfony project can run in parallel on the +same system you are on a good way. All these steps do not require you to +introduce Symfony just yet and will already open up some opportunities for +modernizing your existing code. + +Establishing a Safety Net for Regressions +----------------------------------------- + +Before we can safely make changes to the existing code we must ensure that we +don't break anything. One reason for choosing to migrate is making sure that +the application is in a state where it can run at all times. The best way for +ensuring a working state is to establish automated tests. + +It is quite common for an existing application to either not have a test suite +at all or have low code coverage. Introducing unit tests for this code is +likely not cost effective as the old code might be replaced with functionality +from Symfony components or might be adapted to the new application. +Additionally legacy code tends to be hard to write tests for making the process +slow and cumbersome. + +Instead of providing low level tests ensuring each class works as expected it +might make sense to write high level tests ensuring that at least anything user +facing works on at least a superficial level. These kinds of tests are commonly +called End-to-End tests, because they cover the whole application from what the +user sees in the browser down to the very code that is being run and connected +services like a database. In order to automate this you have to make sure that +you can get a test instance of your system running as easily as possible and +making sure that external systems do not change your production environment, +e.g. provide a separate test database with (anonymized) data from a production +system or being able to setup a new schema with a basic dataset for your test +environment. Since these tests do not rely as much on isolating testable code +and instead look at the interconnected system, writing them is usually easier +and more productive when doing a migration. You can then limit your effort on +writing lower level tests on parts of the code that you have to change or +replace in the new application making sure it is testable right from the start. + +There are tools aimed at End-to-End testing you can use such as +`Symfony Panther`_ or you can write :doc:`functional tests ` +in the new Symfony application as soon as the initial setup is completed. +For example you can add so called Smoke Tests, which only ensure a certain +path is accessible by checking the HTTP status code returned or looking for +a text snippet from the page. + +Introducing Symfony to the Existing Application +----------------------------------------------- + +The following instructions only provide an outline of common tasks for +setting up a Symfony application that falls back to a legacy application +whenever a route is not accessible. Your mileage may vary and likely you +will need to adjust some of this or even provide additional configuration +or retrofitting to make it work with your application. This guide is not +supposed to be comprehensive and instead aims to be a starting point. + +.. tip:: + + If you get stuck or need additional help you can reach out to the + :doc:`Symfony community ` whenever you need + concrete feedback on an issue you are facing. + +When looking at how a typical PHP application is bootstrapped there are +two major approaches. Nowadays most frameworks provide a so called +front controller which acts as an entrypoint. No matter which URL-path +in your application you are going to, every request is being sent to +this front controller, which then determines which parts of your +application to load, e.g. which controller and action to call. This is +also the approach that Symfony takes with ``public/index.php`` being +the front controller. Especially in older applications it was common +that different paths were handled by different PHP files. + +In any case you have to create a ``public/index.php`` that will start +your Symfony application by either copying the file from the +``FrameworkBundle``-recipe or by using Flex and requiring the +FrameworkBundle. You will also likely have to update you web server +(e.g. Apache or nginx) to always use this front controller. You can +look at :doc:`Web Server Configuration ` +for examples on how this might look. For example when using Apache you can +use Rewrite Rules to ensure PHP files are ignored and instead only index.php +is called: + +.. code-block:: apache + + RewriteEngine On + + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + RewriteCond %{ENV:REDIRECT_STATUS} ^$ + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + + RewriteRule ^index\.php - [L] + + RewriteCond %{REQUEST_FILENAME} -f + RewriteCond %{REQUEST_FILENAME} !^.+\.php$ + RewriteRule ^ - [L] + + RewriteRule ^ %{ENV:BASE}/index.php [L] + +This change will make sure, that from now on your Symfony application is +the first one handling all requests. The next step is to make sure that +your existing application is started and taking over whenever Symfony +can not yet handle a path previously managed by the existing application. + +Front Controller with Legacy Bridge +................................... + +Once we have a running Symfony application that takes over all requests, +falling back to your legacy application is as easy as extending the +original front controller script with some logic for going to your legacy +system. The file could look something like this:: + + use App\Kernel; + use App\LegacyBridge; + use Symfony\Component\Debug\Debug; + use Symfony\Component\HttpFoundation\Request; + + require dirname(__DIR__).'/config/bootstrap.php'; + + /* + * The kernel will always be available globally, allowing you to + * access it from your existing application and through it the + * service container. This allows for introducing new features in + * the existing application. + */ + global $kernel; + + if ($_SERVER['APP_DEBUG']) { + umask(0000); + + Debug::enable(); + } + + if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { + Request::setTrustedProxies( + explode(',', $trustedProxies), + Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST + ); + } + + if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { + Request::setTrustedHosts([$trustedHosts]); + } + + $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG'], dirname(__DIR__)); + $request = Request::createFromGlobals(); + $response = $kernel->handle($request); + + /* + * LegacyBridge will take care of figuring out whether to boot up the + * existing application or to send the Symfony response back to the client. + */ + $scriptFile = LegacyBridge::prepareLegacyScript($request, $response, __DIR__); + if ($scriptFile !== null) { + require $scriptFile; + } else { + $response->send(); + } + $kernel->terminate($request, $response); + +There are 2 major deviations from the original file. First of all we make +``$kernel`` globally available. This allows us to use Symfony features inside +our existing application and gives access to services configured in our +Symfony application. This helps us prepare our own code to work better +within the Symfony application when we transition it over or to replace +outdated and redundant libraries with for example Symfony components. + +The legacy bridge, being called after the Symfony request is being handled, +is responsible for figuring out which file should be loaded in order to +process the old application logic. This can either be a front controller +similar to Symfony's ``public/index.php`` or a specific script file based +on the current route. The basic outline of this LegacyBridge could look +somewhat like this:: + + namespace App; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class LegacyBridge + { + public static function prepareLegacyScript(Request $request, Response $response, string $publicDirectory): string + { + /* + * If Symfony successfully handled the route, + * we do not have to do anything. + */ + if (false === $response->isNotFound()) { + return; + } + + /* + * Figure out how to map to the needed script file + * from the existing application and possibly (re-)set + * some env vars. + */ + + return $legacyScriptFilename; + } + } + +This is the most generic approach you can take, that is likely to work +no matter what your previous system was. You might have to account for +certain "quirks", but since your original application is only started +after Symfony finished handling the request you reduced the chances +for side effects and any interference. Since the old script is called +in the global variable scope it will reduce side effects on the old +code which can sometimes require variables from the global scope. +At the same time, because your Symfony application will always be booted +first, you can access the container via the ``$kernel`` variable and then +fetch any service. This can be helpful if you want to introduce new +features to your legacy application, without switching over the whole +action to the new application. For example, you could now use the +Symfony Translator in your old application or instead of using your old +database logic, you could use Doctrine to refactor old queries. This will +also allow you to incrementally improve the legacy code making it easier +to transition it over to the new Symfony application. + +The major downside is, that both systems are not well integrated +into each other leading to some redundancies and possibly duplicated code. +For example since the Symfony application is already done handling the +request you can not take advantage of kernel events, utilize Symfony's +routing for determining which legacy script to call. + + +Legacy Route Loader +................... + +The major difference to the LegacyBridge-approach from before is, that we +move the logic inside the Symfony application. Removing some of the +redundancies and allowing us to also interact with parts of the legacy +application from inside Symfony, instead of just the other way around. + +.. tip:: + + The following route loader is just a generic example that you might + have to tweak for your legacy application. You can familiarize + yourself with the concepts by reading up on it in :doc:`Routing `. + +The legacy route loader has a similar functionality as the previous +LegacyBridge, but it is a service that is registered inside Symfony's +routing component:: + + public function load($resource, $type = null) + { + $collection = new RouteCollection(); + $finder = new Finder(); + $finder->files()->name('*.php'); + + /** @var SplFileInfo $legacyScriptFile */ + foreach ($finder->in($this->webDir) as $legacyScriptFile) { + // This assumes all legacy files use ".php" as extension + $filename = basename($legacyScriptFile->getRelativePathname(), '.php'); + $routeName = sprintf('app.legacy.%s', str_replace('/', '__', $filename)); + + $collection->add($routeName, new Route($legacyScriptFile->getRelativePathname(), [ + '_controller' => 'App\Controller\LegacyController::loadLegacyScript', + 'requestPath' => '/' . $legacyScriptFile->getRelativePathname(), + 'legacyScript' => $legacyScriptFile->getPathname(), + ])); + } + + return $collection; + } + +This only shows the ``load``-method of a custom Route Loader. You will also +have to register the loader in your application's ``routing.yaml`` as +described in the documentation for :doc:`Custom Route Loaders `. +Depending on your configuration you might also have to tag the service with +``routing.loader``. Afterwards you should be able to see all the legacy routes +in your route configuration, e.g. when you call the ``debug:router``-command: + +.. code-block:: terminal + + $ php bin/console debug:router + +In order to use these routes you will need to create a controller that handles +these routes. You might have noticed the ``_controller`` attribute we attached +to our routes, which tells Symfony which Controller to call whenever we try +to access one of our legacy routes. The controller itself can then use the +attributes we passed to determine which script to call and wrap the output in +a response class:: + + class LegacyController + { + public function loadLegacyScript($requestPath, $legacyScript) + { + return StreamedResponse::create( + function () use ($requestPath, $legacyScript) { + $_SERVER['PHP_SELF'] = $requestPath; + $_SERVER['SCRIPT_NAME'] = $requestPath; + $_SERVER['SCRIPT_FILENAME'] = $legacyScript; + + chdir(dirname($legacyScript)); + + require $legacyScript; + } + ); + } + } + +This controller will set some server variables that might be needed by +the legacy application. This will simulate the legacy script being called +directly, in case it relies on these variables, e.g. when determining +relative paths or file names. Finally the action requires the old script, +which essentially calls the original script as before, but it runs inside +our current application scope, instead of the global scope. + +There are some risks to this approach, but since the legacy code now runs +inside a controller action we gain access to many functionalities from the +new Symfony application, including the chance to use Symfony's event +lifecycle. For instance, this allows us to transition the authentication +and authorization over to the Symfony application using the Security +component and its firewalls. + +Additional Resources +-------------------- + +The topic of migrating from an existing application towards Symfony is +sometimes discussed during conferences. For example the talk +`Modernizing with Symfony`_ reiterates some of the points from this page. + + +.. _`Strangler Application`: https://www.martinfowler.com/bliki/StranglerApplication.html +.. _`autoload`: https://getcomposer.org/doc/04-schema.md#autoload +.. _`Modernizing with Symfony`: https://youtu.be/YzyiZNY9htQ +.. _`Symfony Panther`: https://github.com/symfony/panther pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy