Symfony против чистого РНР

Дата обновления перевода 2022-01-19

Symfony против чистого РНР

Почему использовать Symfony лучше, чем открыть файл, и писать на простом PHP?

Если вы раньше никогда не использовали PHP-фреймворки, не знакомы с философией Model-View-Controller (здесь и далее MVC) или просто интересуетесь шумихой вокруг Symfony, то эта глава создана для вас. Вместо того, чтобы рассказывать вам о том, что Symfony позволяет разрабатывать приложения быстрее и качественнее, чем при использовании чистого PHP, вы убедитесь в этом самим.

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

В конце вы увидите, как Symfony поможет вам избежать рутинных задач и взять контроль над вашим кодом в свои руки.

Простой блог на чистом PHP

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

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
<?php
// index.php
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');

$result = $connection->query('SELECT id, title FROM post');
?>

<!DOCTYPE html>
<html>
    <head>
        <title>List of Posts</title>
    </head>
    <body>
        <h1>List of Posts</h1>
        <ul>
            <?php while ($row = $result->fetch(PDO::FETCH_ASSOC)): ?>
            <li>
                <a href="/show.php?id=<?= $row['id'] ?>">
                    <?= $row['title'] ?>
                </a>
            </li>
            <?php endwhile ?>
        </ul>
    </body>
</html>

<?php
$connection = null;
?>

Такой код быстро пишется, так же быстро выполняется, и, по мере роста вашего приложения, становится совершенно неподдерживаемым. Имеется несколько проблем, которые необходимо решить:

  • Нет проверки ошибок: А что, если не получится подключиться к базе данных?
  • Плохая организация кода: По мере роста приложения, этот файл будет все сложнее поддерживать. Где вы должны будете разместить код, который обрабатывает отправку формы? Как вы сможете проверять данные? А где разместить код для отправки почты?
  • Сложность повторного использования кода: Так как весь код располагается в одном файле, нет никакой возможности повторного использования любой части приложения для других страниц блога.

Note

Другая проблема, не упомянутая выше, заключается в том, что фактически база данных привязана к MySQL. Несмотря на то, что этот вопрос не рассматривается в данной главе, Symfony полностью интегрирована с Doctrine, библиотекой, позволяющей с одним кодом работать с разными базами данных и преобразующей ваши объекты в записи в базе данных и обратно.

Изоляция представления

При разделении "логики" приложения и кода, который готовит HTML "представление" страницы - общая структура кода сразу же выигрывает:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.php
$connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');

$result = $connection->query('SELECT id, title FROM post');

$posts = [];
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
    $posts[] = $row;
}

$connection = null;

// включить презентацию HTML-кода
require 'templates/list.php';

HTML-код теперь расположен в отдельном файле templates/list.php, который в основном является HTML-файлом, использующим шаблонный PHP-синтаксис:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- templates/list.php -->
<!DOCTYPE html>
<html>
    <head>
        <title>List of Posts</title>
    </head>
    <body>
        <h1>List of Posts</h1>
        <ul>
            <?php foreach ($posts as $post): ?>
            <li>
                <a href="/show.php?id=<?= $post['id'] ?>">
                    <?= $post['title'] ?>
                </a>
            </li>
            <?php endforeach ?>
        </ul>
    </body>
</html>

По договоренности, файл, который содержит всю логику приложения - index.php - называется "контроллер". Термин «контроллер» - это слово, которое вы будете часто слышать вне зависимости от языка программирования или же используемого фреймворка. Он просто относится к части вашего кода, которая обрабатывает пользовательский ввод и готовит ответ.

В нашем случае, контроллер подготавливает данные из базы, а потом подключает шаблон для отображения этих данных. Изолируя контроллер, вы с легкостью сможете изменять только шаблонный файл, если вам понадобится отобразить записи блога в другом формате (например list.json.php для использования JSON-формата)

Изоляция логики приложения (бизнес-логики)

На данный момент, приложение содержит только одну страницу. Но что, если при создании второй страницы нужно использовать то же соединение с базой данных, или даже тот же массив постов из блога? Преобразуйте код таким образом, чтобы базовая логика и функции доступа к данным приложения были изолированы в новом файле под названием model.php:

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
// model.php
function open_database_connection()
{
    $connection = new PDO("mysql:host=localhost;dbname=blog_db", 'myuser', 'mypassword');

    return $connection;
}

function close_database_connection(&$connection)
{
    $connection = null;
}

function get_all_posts()
{
    $connection = open_database_connection();

    $result = $connection->query('SELECT id, title FROM post');

    $posts = [];
    while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
        $posts[] = $row;
    }
    close_database_connection($connection);

    return $posts;
}

Tip

Название файла model.php используется потому, что логика и доступ к данным приложения обычно известен как уровень «модели». В хорошо организованном приложении, большая часть кода, представляющая вашу «бизнес-логику», должна находиться в модели (а не в контроллере). И в отличие от этого примера, только часть модели отвечает за доступ к базе данных (либо не отвечает вовсе).

Контроллер (index.php) теперь выглядит как пара строк кода:

1
2
3
4
5
6
// index.php
require_once 'model.php';

$posts = get_all_posts();

require 'templates/list.php';

Теперь, основной задачей контроллера является получение данных из модели приложения и вызов шаблона для отображения этих данных. Это очень простой пример шаблона model-view-controller.

Изоляция HTML-макета

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

Единственная часть кода, которую нельзя использовать повторно – это разметка страницы. Исправьте это путем создания нового файла templates/layout.php:

1
2
3
4
5
6
7
8
9
10
<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
    <head>
        <title><?= $title ?></title>
    </head>
    <body>
        <?= $content ?>
    </body>
</html>

Шаблон templates/list.php теперь можно упростить для «расширения» файла templates/layout.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- templates/list.php -->
<?php $title = 'Список постов' ?>

<?php ob_start() ?>
    <h1>Список постов</h1>
    <ul>
        <?php foreach ($posts as $post): ?>
        <li>
            <a href="/show.php?id=<?= $post['id'] ?>">
                <?= $post['title'] ?>
            </a>
        </li>
        <?php endforeach ?>
    </ul>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

Теперь у вас есть система, которая позволит вам повторно использовать макет. К сожалению, для достижения этого эффекта, вам придется использовать несколько некрасивых функций РНР в шаблоне (ob_start(), ob_get_clean()). Symfony использует компонент Шаблонизация, который позволяет достичь этого легко и аккуратно. Скоро вы увидите – как именно.

Добавление страницы блога «один пост»

Страница блога "список постов" была оптимизирована для того, чтобы код был более организованным и был пригоден для повторного использования. Для того, чтобы доказать это, добавьте страницу блога "один пост", которая будет отображать отдельные посты блога по параметру id в запросе.

Для начала, создайте новую функцию в файле model.php, которая будет отображать отдельную запись в блоге по данному id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// model.php
function get_post_by_id($id)
{
    $connection = open_database_connection();

    $query = 'SELECT created_at, title, body FROM post WHERE id=:id';
    $statement = $connection->prepare($query);
    $statement->bindValue(':id', $id, PDO::PARAM_INT);
    $statement->execute();

    $row = $statement->fetch(PDO::FETCH_ASSOC);

    close_database_connection($connection);

    return $row;
}

Далее, создайте новый файл под названием show.php – контроллер для новой страницы:

1
2
3
4
5
6
// show.php
require_once 'model.php';

$post = get_post_by_id($_GET['id']);

require 'templates/show.php';

Наконец, создайте новый файл-шаблон templates/show.php для отображения отдельного поста из блога:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- templates/show.php -->
<?php $title = $post['title'] ?>

<?php ob_start() ?>
    <h1><?= $post['title'] ?></h1>

    <div class="date"><?= $post['created_at'] ?></div>
    <div class="body">
        <?= $post['body'] ?>
    </div>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

Создать вторую страницу теперь очень просто, и при этом код не дублируется. Тем не менее, эта страница добавляет еще больше проблем, которые вам поможет решить фреймворк. Например, отсутствующий или неверный параметр запроса id приведет к ошибке приложения. Было бы лучше, если бы это вызывало отображение страницы 404, но это пока не так просто сделать.

Еще одной большой проблемой является то, что каждый отдельный файл-контроллер должен включать в себя файл model.php. А что, если каждому файлу-контроллеру вдруг понадобится еще дополнительный файл, или выполнение какой-то другой глобальной задачи (например, аутентификация)? В нынешнем состоянии, этот код нужно добавлять к каждому контроллеру в файле. Если же вы забудете добавить что-то в один файл, то будем надеяться, что это не будет относиться к безопасности вашего приложения…

"Фронт-контроллер" спешит на помощь

Решением будет использовать фронт-контроллер – единый РНР-файл, через который будут обрабатываться все запросы. При использовании фронт-контроллера, URI приложения слегка изменяются, но становятся более гибкими:

1
2
3
4
5
6
7
Без фронт-контроллера
/index.php          => Список постов блога (выполняется index.php)
/show.php           => Отдельный пост блога (выполняется show.php)

С index.php в качестве фронт-контроллера
/index.php          => Список постов блога (выполняется index.php)
/index.php/show     => Отдельный пост блога (выполняется index.php)

Tip

Используя правила перенаправления (rewrite) в настройках веб-сервера, index.php в адресе не будет нужен и у вас будут красивые, чистые URLы (например, /show).

При использовании фронт-контроллера, единый РНР-файл (в этом случае index.php) обрабатывает каждый запрос. Для страницы с одним постом /index.php/show будет выполнять файл index.php, который теперь отвечает за маршрутизацию запросов, основываясь на полном URI. Как вы увидите, фронт-контроллер – это очень мощный инструмент.

Создание фронт-контроллера

Сейчас вы сделаете большой шаг в разработке вашего приложения. Имея единый файл, отвечающий за все запросы, вы можете централизованно управлять такими вещами как безопасность, загрузка конфигурации и маршрутизация. В этом приложении, index.php теперь должен быть достаточно умным для отображения страницы списка постов блога или страницы отдельного поста, основываясь на URI запроса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.php

// загружаем и инициализируем глобальные библиотеки
require_once 'model.php';
require_once 'controllers.php';

// внутренняя маршрутизация
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ('/index.php' === $uri) {
    list_action();
} elseif ('/index.php/show' === $uri && isset($_GET['id'])) {
    show_action($_GET['id']);
} else {
    header('HTTP/1.1 404 Not Found');
    echo '<html><body><h1>Page Not Found</h1></body></html>';
}

Для улучшения структуры приложения, оба контроллера (ранее /index.php и /index.php/show) теперь являются функциями РНР, и обе перенесены в отдельный файл под названием controllers.php:

1
2
3
4
5
6
7
8
9
10
11
12
// controllers.php
function list_action()
{
    $posts = get_all_posts();
    require 'templates/list.php';
}

function show_action($id)
{
    $post = get_post_by_id($id);
    require 'templates/show.php';
}

В качестве фронт-контроллера, index.php взял на себя абсолютно новую роль, которая включает в себя загрузку библиотек ядра и маршрутизацию приложения, и заключается в вызове одного из двух контроллеров (функции list_action() и show_action()). На самом деле, фронт-контроллер начинает выглядеть и вести себя очень схоже с Symfony.

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

Tip

Еще одним преимуществом фронт-контроллера являются гибкие URL. Заметьте, что URL отдельного поста блога может быть изменен с /show на /read, путем изменения кода только в одном месте. Раньше необходимо было переименовывать целый файл. В Symfony URL становятся еще более гибкими.

На данный момент, приложение разрослось из одного РНР-файла в организованную структуру, которая позволяет повторное использование кода. Скорее всего вы чувствуете себя немного счастливее, но далеко от полного удовлетворения. Например, система маршрутизации ненадежна и не распознает, что страница списка /index.php также должна быть доступна с помощью / (если используются правила вывода Apache). Также, вместо того, чтобы развивать блог, много времени уходит на «архитектуру» кода (например, маршрутизацию, вызов контроллеров, щаблоны, и т.д.). Еще больше времени тратится на отправку форм, проверку введенных данных, запись показаний и безопасность. Зачем вам заново изобретать решения для всех этих рутинных задач?

Добавьте немного Symfony

Symfony спешит на помощь. Перед тем, как использовать Symfony, вам необходимо будет ее скачать. Это может быть сделано с использованием Composer, который позаботится о скачивании правильной версии и всех ее зависимостей, предоставляет автозагрузчик. Автозагрузчик – это инструмент, который позволяет начать использовать РНР-классы, не подключая явно файлы, содержащие эти классы.

Создайте в корневом каталоге файл composer.json со следующим содержанием:

1
2
3
4
5
6
7
8
{
    "require": {
        "symfony/http-foundation": "^4.0"
    },
    "autoload": {
        "files": ["model.php","controllers.php"]
    }
}

Далее, скачайте Composer, и запустите следующую команду, которая скачает Symfony в папку vendor/:

1
$ composer install

Кроме скачивания необходимых библиотек Composer генерирует файл vendor/autoload.php, который занимается автозагрузкой всех файлов фреймворка Symfony, а также файлов, обозначенных в разделе автозагрузки в файле composer.json.

Основой философии Symfony является идея о том, что главная задача приложения – это интерпретировать каждый запрос и сформировать ответ. Для этого Symfony предоставляет два класса - Request и Response. Эти классы являются объектно-ориентированным представлением необработанного НТТР-запроса, который подлежит обработке, и НТТР-ответа, который будет представлен клиенту. Используйте их для улучшения блога:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.php
require_once 'vendor/autoload.php';

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

$request = Request::createFromGlobals();

$uri = $request->getPathInfo();
if ('/' === $uri) {
    $response = list_action();
} elseif ('/show' === $uri && $request->query->has('id')) {
    $response = show_action($request->query->get('id'));
} else {
    $html = '<html><body><h1>Page Not Found</h1></body></html>';
    $response = new Response($html, Response::HTTP_NOT_FOUND);
}

// вывод заголовков и отправка ответа
$response->send();

Теперь контроллеры отвечают за возврат объекта Response. Чтобы упростить этот процесс, вы можете добавить функцию render_template(), которая, кстати, действует практически как шаблонизатор 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
// controllers.php
use Symfony\Component\HttpFoundation\Response;

function list_action()
{
    $posts = get_all_posts();
    $html = render_template('templates/list.php', ['posts' => $posts]);

    return new Response($html);
}

function show_action($id)
{
    $post = get_post_by_id($id);
    $html = render_template('templates/show.php', ['post' => $post]);

    return new Response($html);
}

// функция-помощник для отображения шаблонов
function render_template($path, array $args)
{
    extract($args);
    ob_start();
    require $path;
    $html = ob_get_clean();

    return $html;
}

Используя небольшую часть Symfony, приложение стало более гибким и надежным. Request предоставляет надежный способ получить доступ к информации о НТТР-запросе. В частности, метод getPathInfo() возвращает «очищенный» URL (всегда возвращает /show, и никогда - /index.php/show). Так что даже если пользователь открывает /index.php/show, приложение достаточно умное, чтобы обработать запрос через show_action().

Объект Response предоставляет гибкость при создании НТТР ответа, и позволяет добавлять НТТР заголовки и контент посредством объектно-ориентированного интерфейса. И хотя ответы в этом приложении достаточно просты, его гибкость окупится по мере развития приложения.

Пробное приложение в Symfony

Блог уже прошел длинный путь, но он все еще содержит слишком много кода для такого простого приложения. По пути, вы создали простую систему маршрутизации и метод, использующий ob_start() и ob_get_clean() для отображения шаблонов. Если по какой-то причине вам необходимо продолжать строить этот «фреймворк» с нуля, то вы хотя бы можете использовать отдельные компоненты Symfony Маршрутизация и Twig, которые уже решают эти проблемы.

Вместо того, чтобы заново решать типичные проблемы, вы можете позволить 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
// src/Controller/BlogController.php
namespace App\Controller;

use App\Entity\Post;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class BlogController extends AbstractController
{
    public function list(ManagerRegistry $doctrine)
    {
        $posts = $doctrine->getRepository(Post::class)->findAll();

        return $this->render('blog/list.html.twig', ['posts' => $posts]);
    }

    public function show(ManagerRegistry $doctrine, $id)
    {
        $post = $doctrine->getRepository(Post::class)->find($id);

        if (!$post) {
            // вызвать отображение страницы 404 "не найдено"
            throw $this->createNotFoundException();
        }

        return $this->render('blog/show.html.twig', ['post' => $post]);
    }
}

Заметьте, что обе функции контроллера теперь находятся в «классе контроллера». Это хороший способ группировать связанные страницы. Функции контроллера также иногда называются действиями (actions).

Два контроллера (или действия) все еще легковесны. Каждый использует библиотеку Doctrine ORM для получения объектов из базы данных и компонент Templating для отображения шаблона и возврата объекта Response. Шаблон list.html.twig теперь стал значительно проще и использует Twig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{# templates/blog/list.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}List of Posts{% endblock %}

{% block body %}
<h1>List of Posts</h1>
<ul>
    {% for post in posts %}
    <li>
        <a href="{{ path('blog_show', { id: post.id }) }}">
            {{ post.title }}
        </a>
    </li>
    {% endfor %}
</ul>
{% endblock %}

Файл layout.php практически идентичен:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- templates/base.html.twig -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

Note

Шаблон show.html.twig оставим в качестве упражнения, его обновление должно быть очень схоже с обновлением шаблона list.html.twig.

Когда движок Symfony (который называется Kernel – ядро) загружается, он нуждается в «карте» для того, чтобы знать какой контроллер необходимо использовать, основываясь на информации о запросе. Конфигурация карты маршрутизатора config/routes.yaml предоставляет ему эту информацию в таком формате:

1
2
3
4
5
6
7
8
# config/routes.yaml
blog_list:
    path:     /blog
    controller: App\Controller\BlogController::list

blog_show:
    path:     /blog/show/{id}
    controller: App\Controller\BlogController::show

Теперь, когда Symfony занимается всеми повседневными задачами, фронт-контроллер public/index.php становится предельно простым. И поскольку он делает так мало, вам никогда не придется его трогать:

1
2
3
4
5
6
7
8
// public/index.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../src/Kernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new Kernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();

Единственная работа фронт-контроллера – инициализация движка Symfony (Kernel) и передача ему для обработки объекта Request. Ядро Symfony запрашивает у маршрутизатора обработку запроса. Маршрутизатор соотносит входящий URL c определенным путем и возвращает информацию о маршруте, включая необходимый для использования контроллер. Определенный маршрутизатором контроллер исполняется, и ваш код в контроллере создает и возвращает соответствующий объект Response. НТТР заголовки и контент объекта Response возвращаются клиенту.

Это прекрасно.

Преимущества Symfony

В следующих статьях документации вы узнаете больше о том, как работает каждая составляющая Symfony, и как вы можете организовать свой проект. Сейчас же, ещё раз порадуемся улучшению вашей жизни с переносом блога с чистого РНР на Symfony:

  • Ваше приложение теперь имеет простой, понятный и единообразный код (хотя Symfony не требует этого от вас). Это поощряет повторное использование и позволяет новым разработчикам быстрее становиться продуктивными в рамках вашего проекта;
  • 100% написанного вами кода относится к вашему приложению. Вам не нужно разрабатывать и поддерживать низкоуровневые инструменты вроде автозагрузки, маршрутизации или отображения контроллеров;
  • Symfony предоставляет вам доступ к инструментам с открытым кодом, таким как Doctrine и компонентам вроде Шаблонизация, Безопасность, Форма, Валидатор и Перевод (и это еще не все);
  • У приложения появились полностью настраиваемые URL благодаря компоненту Routing;
  • Архитектура Symfony, основанная на НТТР, предоставляет вам доступ к мощным инструментам вроде НТТР-кеширования, основанного на внутреннем НТТР-кеше Symfony, или еще более мощным инструментам, таким как Varnish (кеширующий прокси). Об этом рассказывается далее, в главе о кешировании.

И, возможно, лучшее из всего – используя Symfony, вы теперь имеете доступ к целому ряду высококачественных инструментов с открытым исходным кодом, разработанных участниками Symfony сообщества! Хороший выбор общественных инструментов Symfony можно найти на GitHub.