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

Дата оновлення перекладу 2022-11-03

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

Чому використовувати Symfony краще, ніж відкрити файл і писати простим PHP?

Якщо ви раніше ніколи не використовували PHP-фреймворки, не знайомі з філософією Model-View-Controller (тут і далі MVC) або просто цікавитеся галасом навколо Symfony, то ця глава створена для вас. Замість того, щоб розказувати вам про те, що Symfony дозволяє розробляти додатки швидше та якісніше, ніж при використанні чистого PHP, ви переконаєтеся в цьому самі.

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

Наприкінці ви побачите, як 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.

Ізоляція макету

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

Єдина частина коду, яку не можна використати повторно - це макет сторінки. Виправте це шляхом створення нового файлу 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, який потрубується про завантаження правильної версії та всіх її залежностей, надає автозавантажувач. Автозавантажувач - це інструмент, який дозволяє почати використовувати PНР-класи, не підключаючи файли, що містять ці класи, ясно.

Створіть у кореневому каталозі файл 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]);
    }
}

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

Два контролери (або дії) все ще легковажні. Кожний використовує бібліотеку Doctrine ORM для отримання обʼєктів з бази даних і компонент Шаблонізація для відображення шаблону та повернення обʼєкта 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 %}Список постів{% 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 (Ядра) та передача йому для обробки обʼєкта Request. Ядро Symfony запитує у маршрутизатора обробку запиту. Маршрутизатор співвідносить вхідний URL з визначеним шляхом і повертає інформацію про маршрут, включно з необхідним для використання контролером. Визначений маршрутизатором контролер виконується, і ваш код у контролері створює та повертає відповідний обʼєкт Response. НТТР-заголовки і зміст обʼєкта Response повертаються клієнту.

Це чудово.

Переваги Symfony

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

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

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