Міграція існуючого додатку в Symfony

Дата оновлення перекладу 2024-06-10

Міграція існуючого додатку в Symfony

Якщо у вас є існуючий додаток, який було створено не в Symfony, вам може захотітися перемістити частини додатку, не переписуючи існуючу логіку з нуля. Для таких випадків існує патерн під назвою Strangler Fig Application. Основна ідея цього патерну - створення нового додатку, який поступово бере на себе функціональність існуючого додатку. Цей підхід до міграції можна реалізувати з Symfony різноманітними способами, і він має декілька переваг над переписуванням коду, на кшталт можливості представити нові функції в ічнуючому додатку та зменшення ризиків, шляхом уникнення "вибухового" релізу нового додатку.

Screencast

Тема міграції існуючого додатку в Symfony іноді обговорюється під час конференцій. Наприклад, промова Модернізація з Symfony цитує деякі тези з цієї сторінки.

Попередні умови

До того як ви почнете впроваджувати Symfony в існуючий додаток, вам потрібно забезпечити виконання деяких вимог вашим додатком та середовищем. Прийняття рішень та підготовка середовища до початку процесу міграції є дуже важливими для успіху процесу.

Note

Наступні кроки не вимагають, щоб у вас був новий додаток Symfony, і насправді може бути безпечніше впроваджувати ці зміни у вашому існуючому додатку заздалегідь.

Вибір цільової версії Symfony

Найважливіше що вам потрібно зробити - вирішити, на яку версію ви хочете мігрувати - на поточний стабільний реліз або на довгостроково підтримувану версію (LTS). Основна різниця в тому, як часто вам потрібно буде проводити оновлення для використання підтримуваної версії. В контексті міграції, інші фактори, на кшталт підтримуваної PHP-версії або підтримки бібліотек/пакетів, які ви використовуєте, можуть також мати сильний вплив. Використання найновішого стабільного релізу скоріш за вссе надасть вам більше функцій, але також вимагатиме частіших оновлень, щоб гарантувати вам підтримку випрравлень багів та патчей безпеки, і вам потрібно буде швидше працювати над виправленням застарілих функцій, щоб мати можливість оновлюватися.

Tip

При оновленні на Symfony у вас може виникнути спокуса використати Flex . Будь ласка, памʼятайте, що він в першу чергу орієнтований на запуск нового додатку Symfony у відповідності з кращими практиками по відношенню до структури каталогів. Коли ви працюєте з обмеженнями вже існуючого додатку, не факт, що ви зможете дотримуватись цих обмежень, що робить Flex менш корисним.

Спочатку вашому середовищу необхідна можливість підтримувати мінімальні вимоги для обох додатків. Іншими словами, якщо релізу Symfony, який ви хочете використати, необхідно PHP 7.1, а ваш існуючий додаток ще не підтримує дану версію PHP, вам скоріш за все доведеться оновити ваш успадкований проект. Використайте команду check:requirements, щоб перевірити, чи відповідає ваш сервер технічним вимогам для запуску додатків Symfony , та порівняйте їх з вашим поточним середовищем додатку, щоб переконатися, що ви можете запускати обидва додатки в одній системі. Наявність тестової системи, максимально наближеної до середовища виробництва, де ви можете просто встановити новий проект Symfony поряд з існуючим і перевірити, чи працює він, надасть вам ще надійніший результат.

Tip

Якщо ваш поточний проект працює на старішій версії PHP на кшталт PHP 5.x, оновленна на новішу веррсію дасть вам прискорення продуктивності без зміни коду.

Налаштування Composer

Вам також потрібно буде слідкувати за конфліктами між залежностями в обох додатках. Це особливо важливо, якщо ваш існуючий додаток вже використовує компоненти Symfony або бібліотеки, часто використовувані у додатках Symfony, на кшталт Doctrine ORM, Swiftmailer або Twig. Гарним способом гарантувати сумісність буде використовувати однаковий composer.json для залежностей обох проектів.

Як тільки ви представите компонувальник для управління залежностями вашого проекта, ви зможете використовувати його автозавантажувач, щоб гарантувати, що ви не зіштовхнетесь з жодними конфліктами у звʼязку з користувацьким автоматичним завантаженням з вашого існуючого фреймворку. Це зазвичай тягне за собою додавання розділу autoload у ваш composer.json, його конфігурації, засновуючись на вашому додатку, та заміну вашої користувацької логіки на щось типу цього:

1
require __DIR__.'/vendor/autoload.php';

Видалення глобального стану з успадкованого додатку

У старішх PHP-додатках було розповсюджено покладатися на глобальний стан і навіть модивікувати його під час прогону. Це може мати побічні ефекти у новопредставленому додатку Symfony. Іншими словами, код, що покладається на глобальності в існуючому додатку, повинен бути перебудований, щоб дозволити обом системам працювати водночас. Так як глобальні стани на сьогоднішній день вважаються анти-патерном, вам варто почати працювати з цим ще до початку процесів інтеграції.

Налаштування середовища

В залежності від використовуваних вами бібліотек, початкового фреймворку, на якому засновано вашш проект, і, найважливіше, віку вашого проекту (так як сам PHP пройшов багато покращень протягом останніх років і ваш код можливо їх ще не наздогнав), вам можуть знадобитися додаткові кроки. Якщо ваш існуючий код і новий проект Symfony можуть працювати паралельно в одній системи, у вас все добре. Всі ці кроки поки не вимагають від вас представлення Symfony, але вже відкриють деякі можливості для модернізації вашого існуючого коду.

Створення страхування для регресій

До того як ви зможете безпечно вносити зміни в існуючий код, ви повинні бути впевнені, що нічого не зламається. Одна з причин міграції - гарантія того, що ваш додаток буде здатний працювати в будь-який час. Найкращий спосіб гарантувати робочий стан - створити автоматизовані тести.

Для існуючого додатку часто не існує набору тестів або вони охоплюють лише невелику частину коду. Введення модульних тестів для цього коду скоріш за все не буде виправдано, так як старирй код може бути замінено функціоналом компонентів Symfony або може бути адаптований до нового додатку. Крім того, для успадкованого коду зазвичай складно писати тести, і процес стає повільним та виснажливим.

Замість надання тестів низького ррівня, які гарантують, що кожний клас працює так, як очікується, має сенс напссати тести високого рівня, які гарантують, що як мінімум все, з чим зіштовхується користувач, парцюватиме хоча б на зовнішньому рівні. Такі типи тестів часто називаються тестами End-to-End, так як вони охоплюють весь додаток від того, що бачить ваш користувач у браузері, до самого коду, який виконується, і створює звʼязки з сервісами, на кшталт бази даних. Щоб автоматизувати це, ви маєте переконатися, що ви можете зробити так, щоб екземпляр тесту длля вашої системи виконувався максимально легко, і що зовнішні системи не змінюють ваше середовище виробництва, наприклад, щоб надати окремий тест бази даних з (анонімними) даними з середовиша виробництва. Так як ці тести не сильно покладаються на ізоляцію тестованого коду, а замість цього дивляться на взаємоповʼязану систему, їх написання зазвичай легше та продуктивніше під час міграції. Ви потім можете зосередити свої зусилля на написанні тестів нижнього рівня для тих частин коду, які ви повинні змінити або замінити у новому додатку, щоб гарантувати, що він буде тестованим з самого початку.

Існують інструменти, спрямовані на тестування End-to-End, які ви можете використовувати, на кшталт Symfony Panther, або ви можете написати функціональні тести у новому додатку Symfony, як тільки буде завершена первісна установка. Наприклад, ви
можете додати так звані вибіркові тести, які гарантують лише доступність кожного шляху, перевіряючи повернений HTTP статус-код, або шукають уривок тексту на сторінці.

Введення Symfony в існуючий додаток

Наступні інструкції надають лише обриси розповсюджених завдань для налаштування додатку Symfony, яке відкочується до успадкованого додатку кожний раз, коли маршрут недоступний. Ваш кілометраж може відрізнятися і, скоріш за все, вам знадобиться пристосувати деякі з них, або навіть надати додаткову конфігурацію або модифікацію, щоб це працювалло з вашим додатком. Цей посібник не має бути повним, а просто є відправною точкою.

Tip

Якщо ви застррягнете або вам знадобиться додаткова допомога, ви можете звʼязатися зі спільнотою Symfony, коли вам потрібно буде отримати чіткий фідбек з приводу проблеми, з якою ви зіштовхнулися.

Запуск Symfony у фронт-контролері

При розгляді первісного запуску типового PHP-додатку, існує два основних підходи. Сьогодні більшість фреймворрки надають так званий фронт-контролер, який діє як точка входу. Незалежно від того, за яким шляхом URL вашого додатку ви хочете перейти, кожний запит відправляється цьому фронт-контролеру, який потім визначає, які частини вашого додатку завантажувати, наприклад, який контроллер та дію викликати. Цей підхід також використовує Symfony, де public/index.php - це фронт-контролер. Особливо у старіших додатках було розповсюджено, щоб різні шляхи оброблялися різними PHP-файлами.

В будь-якому випадку, вам потрібно створити public/index.php, який запустить ваш додаток Symfony або скопіювавши файл з рецепту FrameworkBundle, або використовуючи Flex и вимагаючи FrameworkBundle. Вам також скоріш за все буде необхідно оновити ваш веб-сервер (наприклад, Apache або nginx), щоб він завжди використовував цей фронт-контролер. Ви можете переглянути конфігурацію веб-сервера, щоб побачити приклади того, як це може виглядати. Наприклад, при використанні Apache, ви можете використовувати Правила перепису, щоб гарантувати, що PHP-файли ігноруються, а викликається лише index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]

Ця зміна гарантує, що з цих пір ваш додаток Symfony буде першим в обробці всіх запитів. Наступний крок - переконатися, що ваш існуючий додаток запускається та перехоплює контроль кожний раз, коли Symfony поки ще не може обробити шлях, раніше керований існуючим додатком.

З цього моменту доступно безліч тактик і кожний проект вимагає унікального підходу для міграції. Цей посібник демонструє два приклади часто використовуваних підходів, які ви можете використовувати в якості бази для вашого власного підходу:

Фронт-контролер з мостом наслідування

Коли у вас буде робочий додаток Symfony, який перехоплює контроль над всіма запитами, відкат до успадкованого додатку відбуваєть шляхом розширення початкового скрипту фронт- контролера з деякою логікою для входу у вашу систему наслідування. Файл може виглядати якось так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// public/index.php
use App\Kernel;
use App\LegacyBridge;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;

require dirname(__DIR__).'/vendor/autoload.php';

(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');

/*
 * Ядро буде завжди доступним глобально, що дозволяє вам мати
 * доступ до нього з вашого існуючого додатку, і через нього до
 * сервіс-контейнеру. Це дозволяє представити нові функції в
 * існуючому додатку.
 */
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_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO
    );
}

if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
    Request::setTrustedHosts([$trustedHosts]);
}

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);

/*
 * LegacyBridge потурбується про те, щоб вирішити, чи потрібно завантажувати
 * існуючий додаток, або ж відправити відповідь Symfony назад до клієнта.
 */
$scriptFile = LegacyBridge::prepareLegacyScript($request, $response, __DIR__);
if ($scriptFile !== null) {
    require $scriptFile;
} else {
    $response->send();
}
$kernel->terminate($request, $response);

Існує 2 основних відхилення від початкового файлу:

Рядок 18
По-перше, $kernel доступний глобально. Це дозволяє вам використовувати функції Symfony всередині вашого існуючого додатку, та надає доступ до сервісів, сконфігуррованих у вашому додатку Symfony. Це допомагає вам підготувати власний код до кращої роботи в рамках додатку Symfony до здійснення переходу. Наприклад, замінивши застарілі або надмірні бібліотеки компонентами Symfony.
Рядки 41 - 50
Замість відправки відповіді Symfony напряму, викликається LegacyBridge, щоб вирішити, чи має бути запущено та використано для створення відповіді успадкований додаток.

Цей міст наслідування відповідає за розуміння того, який файл має бути завантажено для того, щоб обробити стару логіку додатку. Це може бути або фронт-контролер, схожий на public/index.php Symfony, або особливий скриптовий файл, заснований на поточному маршруті. Базовий вид цього LegacyBridge може виглядати якось так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// src/LegacyBridge.php
namespace App;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class LegacyBridge
{

    /**
     * Мапуйте вхідний запит до правильного файлу. Це ключова функція
     * LegacyBridge.
     *
     * Тільки приклад коду. Ваша реалізація відрізнятиметься, в залежності від
     * архітектури успадкованого коду та його виконання.
     *
     * Якщо ваше мапування складне, ви можете захотіти написати модульні тести, щоб
     * перевірити свою логіку, отже, це публічна статика..
     */
    public static function getLegacyScript(Request $request): string
    {
        $requestPathInfo = $request->getPathInfo();
        $legacyRoot = __DIR__ . '/../';

        // Мапуйте маршрут до скрипту наслідування:
        if ($requestPathInfo == '/customer/') {
            return "{$legacyRoot}src/customers/list.php";
        }

        // Maпуйте прямий виклик файлу, наприклад, виклик ajax:
        if ($requestPathInfo == 'inc/ajax_cust_details.php') {
            return "{$legacyRoot}inc/ajax_cust_details.php";
        }

        // ... тощо.

        throw new \Exception("Unhandled legacy mapping for $requestPathInfo");
    }


    public static function handleRequest(Request $request, Response $response, string $publicDirectory): void
    {
        $legacyScriptFilename = LegacyBridge::getLegacyScript($request);

        // Можливо (пере)встановіть якісь змінні середовища (наприклад, щоб обробити
        // відправку форм у PHP_SELF):
        $p = $request->getPathInfo();
        $_SERVER['PHP_SELF'] = $p;
        $_SERVER['SCRIPT_NAME'] = $p;
        $_SERVER['SCRIPT_FILENAME'] = $legacyScriptFilename;

        require $legacyScriptFilename;
    }
}

Цей найрозповсюдженіший шллях, яким ви можете піти, і який скоріщ за все буде працювати, незалежно від того, якою була ваша попередня система. Можливо, вам доведеться зіштовхнутися з деякими "фокусами", але так як ваш початковий додаток запускається лише після того, як Symfony закінчить обробку запиту, ви зменшуєте шанс побічних ефектів та будь-яких втручань.

Так як старий скрипт викликається в рамках глобальної змінної, він зменшить побічні ефекти старого коду, який іноді може вимагати змінні з глобальної області. В той же час, як ваш додаток Symfony буде завжди запускатися першим, ви можете отримати доступ до контейнера через змінну $kernel, а потім вилучити будь-який сервіс (використовуючи getContainer()). Це може бути корисним, якщо ви хочете представити нові функці у ваш успадкований додаток, не переносячи всю дію у новий додаток. Наприклад, ви тепер можете використовувати Перекладач Symfony у вашому старому додатку, або замість використання вашої старої логіки бази даних, ви можете використати Doctrine для рефакторингу старих запитів. Це також дозволить вам значно покращити код наслідування, що полегшить процес його переведення у новий додаток Symfony.

Основний недолік полягає в тому, що обидві системи не дуже добре інтегровані одна з одною, що призводить до деякого перенасичення та потенційно дубльованого коду. Наприклад, так як додаток Symfony вже закінчив обробку запиту, ви не можете використовувати переваги подій ядра або маршрутизацію Symfony для визначення того, який успадкований скрипт викликати.

Завантажувач маршруту наслідування

Основна відмінність від підходу LegacyBridge, описаного вище, полягає в тому, що логіка переміщується всередині додатку Symfony. Він видаляє деякі надмірності та дозволляє нам також взаємодіяти з частинами успадкованого додатку зсередини Symfony, замість прямо протилежного способу.

Tip

Наступний завантажувач маршруту - це просто загальний приклад, який вам може знадобитися налаштувати длля вашого успадкованого додатку. Ви можете ознайомитися з концепціями, прочитавши про них у розділі Машрутизація.

Завантажувач маршруту наслідування - це користувацький заванатажувач маршруту. Завантажувач маршруту наслідування має функції, схожі на попередній LegacyBridge, але також є сервісом, зареєстрованим всередині компонента Routing Symfony:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Legacy/LegacyRouteLoader.php
namespace App\Legacy;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class LegacyRouteLoader extends Loader
{
    // ...

    public function load($resource, $type = null): RouteCollection
    {
        $collection = new RouteCollection();
        $finder = new Finder();
        $finder->files()->name('*.php');

        /** @var SplFileInfo $legacyScriptFile */
        foreach ($finder->in($this->webDir) as $legacyScriptFile) {
            // Припускає, що всі файли наслідування використовують ".php" в якості розширення
            $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;
    }
}

Вам також знадобиться зареєструвати завантажувач у routing.yaml вашого додатку, як описано в документації Користувацьких завантажувачів маршруту. В залежності від вашої конфігурації, вам також може знадобитися тегувати сервіс за допомогою routing.loader. Після цього ви маєте побачити всі успадковані маршрути у вашій конфігурації маршрутів, наприклад, коли ви викликаєте команду debug:router:

1
$ php bin/console debug:router

Для того, щоб використовувати ці маршрути, вам знадобиться створити контролер, який обробляє ці маршрути. Ви могли помітити атрибут _controller у попередньому прикладі коду, який повідомляє Symfony, який контролер викликати, коли вона намагається отримати доступ до одного з маршрутів наслідування. Сам контролер потім може використовувати інші атрибути маршруту (тобто, requestPath і legacyScript), щоб визначити, який скрипт викликати, і огорнути виведення у клас відповіді:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/LegacyController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\StreamedResponse;

class LegacyController
{
    public function loadLegacyScript(string $requestPath, string $legacyScript): StreamedResponse
    {
        return new StreamedResponse(
            function () use ($requestPath, $legacyScript): void {
                $_SERVER['PHP_SELF'] = $requestPath;
                $_SERVER['SCRIPT_NAME'] = $requestPath;
                $_SERVER['SCRIPT_FILENAME'] = $legacyScript;

                chdir(dirname($legacyScript));

                require $legacyScript;
            }
        );
    }
}

Цей контролер встановить деякі змінні сервера, які можуть знадобитися успадкованому додатку. Це зімітує прямий виклик успадкованого скрипту у випадку, якщо він покладається на ці змінні (наприклад, при визначенні відносних шляхів або імен файлів). Нарешті, дії необхідний старий скрипт, який по суті викликає початковий скрипт, як і раніше, але він запускається в рамках нашого поточного додатку, а не у глобальній області.

У цьому підході існують деякі ризики, так як він більше не виконується у глобальній області. Однак, так як успадкований код тепер запускається всередині дії контролера, ви отримуєте доступ до багатьох функцій з нового додатку Symfony, включно з можливістю використання життєвого циклу події Symfony. Наприклад, це дозволяє вам провести аутентифікацію та авториазцію успадкованого додатку у додаток Symfony, використовуючи компонент Secutiry та його файрволи.