Как реанимировать ваш PHP-проект с помощью Symfony2 компонентов

:

Данный пост является переводом не статьи, как принято, а доклада+презентации, поэтому текст поста достаточно вольный.

Думаю, всем хорошо известно и очевидно, что PHP — один из самых популярных языков программирования, на котором написано множество веб-проектов, начиная с персональных homepage-страниц и заканчивая мастодонтами типа Facebook, Vimeo, WordPress и даже YouPorn.

PHP появился в 1995 году, при этом полноценная поддержка ООП была реализована только в PHP5, который вышел в 2005 году. За это время было написано большое количество кода, как хорошего, так и плохого, а точнее сказать сильно устаревшего и тяжело сопровождаемого.

Многие проекты, как и экосистема PHP в целом, к настоящему моменту стали представлять подобие оживленного городского квартала.

Новые версии PHP


Новое десятилетие принесло нам PHP5.3 и PHP5.4, которые помимо увеличения производительности несут в себе множество плюшек! Вот только некоторые из них:

1. Пространства имен (php 5.3)

namespace JoliCode\Conferences;
use General\Talk\Session;

class SymfonyLive extends Session
{
    public function runPresentation()
    {
    } 
}

  • Спасают от совпадений в именовании классов;
  • Снижают неопределенность;
  • Уход от длинных именований классов.

2. Анонимные функции и замыкания (php 5.3), и даже внутри объектов (php 5.4)
$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

3. Phar-архивы (php 5.3)

Позволяют распространять библиотеки в виде одного подключаемого phar-файла.

4. «goto» — что за на..?? (php 5.3)

5. Трейты (php 5.4)

6. Короткий синтаксис объявления массивов! (php 5.4)

function fruits() {
  return ['apple', 'banana', 'orange'];
}

echo fruits()[0]; // Outputs: apple


и много чего другого.

Фреймворки


Новые версии PHP стали катализатором появления фреймворков. Среди них стоит упомянуть symfony, Zend Framework, Yii, CodeIgniter, CakePHP. Почти всех из них были значительно обновлены в вышедших вторых версиях.

Один из этих фреймворков — Symfony2, построен на наборе независимых компонентов, а также ряде сторонних решений, которые используют новые возможности языка PHP и помогут вам реанимировать проекты с устаревшим кодом.

Стратегия реанимации проекта


Представим, что ваш проект существует уже 5 лет. За это время:
  • Было реализовано большое количество функциональности;
  • Проект работает в production;
  • Его постоянно использует большая аудитория пользователей;
  • Скопилось больше количество данных;
  • Вокруг проекта сформировалась опытная команда.

При этом:
  • Медленно происходит внедрение новой функциональности;
  • Проблема с сопровождением проекта и его инфраструктурой;
  • Тяжело поддается улучшению существующая функциональность.

Путь 1: Большой взрыв

Наиболее естественное желание в такой ситуации — выкинуть весь код и переписать заново! Представим, что мы пошли по такому пути. Несомненно, мы получим удовлетворение от возможности работы с одним из новых современных фреймворком, забыв про старый код, а местами быдлокод, как страшный сон.

Но, если смотреть правде в глаза, процесс, скорее всего, будет достаточно масштабным, непростым и продолжительным и займет не менее 1 человеко-года на переписывание всей функциональности. При этом остановится развитие существующей версии проекта, что вызовет финансовые потери бизнеса вашего проекта и отток пользователей, которым уделяется недостаточно внимания. Также буду высоки риски того, что не удастся перенести полностью всю функциональность на новую платформу. Одной из причин этих рисков будет необходимость разработки средств миграции существующих данных в новую структуру систем хранения данных.

Путь 2: Будь последовательным

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

Переходим к действию


Шаг 1. Переводим проект на PHP 5.3 или PHP 5.4

Для начала обновляем PHP как минимум до версии PHP 5.3. Делаем это аккуратно, выполним обновление сначала на dev-сервере и проверив, что никакие части системы не отвалились. Затем можно проверить на prod-сервере. При этом желательно это делать не на всем проекте, а в режиме A/B-тестирования, так, чтобы на новую среду попадала только часть пользователей.

Как только вы убедились, что все работает нормально, можно двигаться дальше.

Шаг 2. Готовим фундамент под будущий рефакторинг системы

Подключаем ключевые компоненты: ClassLoader, DependencyInjection и HTTPFoundation. Они помогут нам с рефакторингом и будут фундаментом для подключения новых компонентов.

Эти и все подследующие компоненты можно легко подключить с помощью пакетного менеджера Composer. Он позволяет лаконично описывать зависимости проекта от сторонних библиотек в json-формате, автоматически устанавливать и обновлять их, а также генерировать autoload-файл для их подключения в проекте. На Хабре есть хорошая статья о том, как его использовать.

В нашем случае конфигурационный файл для Composer будет выглядеть так:

{
    "autoload": {
        "psr-0": {}
    },
    "name": "xavierlacot/blog",
    "description": "My old blog on steroids",
    "require": {
        "php": ">=5.3.3",
        "symfony/class-loader": "2.1.*",
        "symfony/dependency-injection": "2.1.*",
        "symfony/http-foundation": "2.1.*"
    } 
}

Устанавливаем компоненты:
$ php composer.phar install
Installing dependencies from lock file
- Updating symfony/class-loader (dev-master)

И подключаем их в наш проект:
<?php
// подключаем сгенерированный менеджером Composer файл
require 'vendor/autoload.php';

Теперь мы можем их использовать!
ClassLoader. Стандарты автозагрузки классов

Полтора года назад сообществом PHP был принято несколько соглашений, одно из них, соглашение PSR-0, описывает правила и требования к коду для совместимости с современными автозагрузчиками классов (об истории автозагрузчиков хорошо написано тут и тут)

Если говорить кратко, то стандарт разрешает два тип именования классов:

Через namespace (современный подход)

class: Symfony\Component\HttpFoundation\Request
path: vendor/src/Symfony/Component/HttpFoundation/Request.php

Через подчеркивания (PEAR-стандарт, устаревший подход)
class: Twig_Extension_Core
path: vendor/twig/lib/Twig/Extension/Core.php

При этом автозагрузчик при подключении файла класса автоматически заменяет знак слеша (\) и подчеркивания (_) на разделитель директорий (/) добавляя расширение .php в конце.

Компонент ClassLoader позволяет организовать автозагрузку классов в соответствии с соглашением PSR-0.

Шаг 3. Рефакторинг классов и использование ClassLoader

Например, у нас есть некий класс для работы c LDAP:
<?php
class prefixedLdapConnector
{
    public function __construct()
    {
        // какой-то код
    } 
}

Приводим его к PSR-0 стандарту:
<?php
namespace Prefix\Ldap\Client;

class Client
{
    public function __construct()
    {
        // какой-то код
    } 
}

Регистрируем пространство верхнего уровня Prefix в ClassLoader:
use Symfony\Component\ClassLoader\UniversalClassLoader;

$loader = new UniversalClassLoader();
$loader->registerNamespace('Prefix', __DIR__ . '/src/');
$loader->register();

Теперь при использовании класса Prefix\Ldap\Client в вашем коде ClassLoader позаботится об его автоматической загрузке:
//здесь свою работу сделает ClassLoader
use Prefix\Ldap\Client;

$client = new Client();
Шаг 4. Рефакторинг обработки запросов/подготовки ответов. Использование HttpFoundation

Компонент HTTPFoundation упрощает обработку входящих запросов и подготовку ответа сервера.

Добавив в проект 2 строчки:

//здесь свою работу сделает компонент ClassLoader, подключив нужный класс
use Symfony\Component\HttpFoundation\Request;
//а здесь уже работает класс Request компонента HTTPFoundation
$request = Request::createFromGlobals();

мы получаем объект $request, который знает все о входящем запросе:

  • $request->request заменяет $_POST
  • $request->query заменяет $_GET
  • $request->cookies заменяет $_COOKIE
  • и т.д.

И аналогично с Response:

use Symfony\Component\HttpFoundation\Response;

$response = new Response();

$response->setContent('This will be the response content');
$response->headers->set('Content-Type', 'text/html');
$response->setStatusCode(200);

// отправка ответа Response
$response->prepare($request);
$response->send();

Казалось бы, зачем заменять привычные переменные и методы PHP новыми классами. На самом деле, этот подход дает целый ряд преимуществ. Во-первых, он позволяет ввести более понятный уровень абстракции по работе с данными запроса и ответа, представленных в виде объектов. Вы можете полностью контролировать запрос и ответ из любой области вашего проекта. Во-вторых, данный компонент проверен многими независимыми разработчиками на соответствие требованиям безопасности. В-третьих, код, который использует HTTPFoundation, значительно легче тестировать.

Шаг 5. Оформляем функциональность проекта в сервисы. Компонент Dependency Injection

Почти во всех проектах существует целый ряд функций, за которые отвечают разные библиотеки, классы или банально программные функции. Компонент Dependency Injection позволяет организовать и централизовать процесс создания и конфигурирования объектов, которые реализуют ту или иную функциональность. К таким объектам могут быть отнесены почтовая служба, менеджер по работе с БД, шаблонизатор и многие другие.

Допустим, у нас есть класс для отправки почтовых писем:

<?php
namespace Prefix\Mailer;

class Mailer
{
    $prefix = '';

    public function send($to, $subject, $body)
    {
        //отправка почты
    } 
}

И в нашем коде почта отправляется примерно так:

function saveUser($name, $email)
{
    $mailer = new Mailer();
    $mailer->send($user->getEmail(), 'Welcome ' . $user->getName(), 'A test');
}

Сделаем небольшой рефакторинг, создадим service-контейнер и зарегистрируем в нем наш почтовый класс:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->register('mailer', 'Mailer');

Сделаем второй небольшой рефакторинг в строчках отправки письма:

function saveUser($name, $email)
{
    // stuff here
    $mailer = $this->container->get('mailer');
    $mailer->send($user->getEmail(), 'Welcome '.$user->getName(), 'A test');
}

Эти незначительные изменения дают сразу несколько бонусов:

  • Объект класса Mailer создается только один раз, даже если мы его получим из service-контейнера в другом месте проекта;
  • Сервис Mailer теперь можно централизованно конфигурировать;
  • Его легко заменить другим сервисом.

Аналогичным образом можно оформлять в виде сервисов одну библиотеку проекта за другой, организовав всю работу через вызовы сервисов.

Шаг 6. Что дальше?

Подключив первые три компонента Symfony2, вы можете по тому же алгоритму начинать использовать другие не менее полезные компоненты:
  • Console — разработка консольных скриптов (вызываемые из командной строки или по cron)
  • Finder — поиск файлов и папок
  • Routing — позволяет задавать соответствие между HTTP-запросами и набором конфигурируемых параметров
  • Templating — обеспечит всем необходимым для работы с шаблонами в проекте
  • и другие.

Шаг за шагом, компонент за компонентом ваш проект будет постепенно преображаться и обретать новую жизнь без каких-либо радикальных решений. Удачи и приятного рефакторинга!