Symfony проти чистого PHP

21/04/2015 0 symfony, шаблон, контролер, шаблон Twig, Response, Request, фронт-контролер

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

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

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

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

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

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

<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);

$result = mysql_query('SELECT id, title FROM post', $link);
?>

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

<?php
mysql_close($link);
?>

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

1) Нема інструмента для обробки помилок: а що, коли “відпаде” підключення до бази даних?

2) Практично неорганізований код: з часом, коли додаток росте, файл буде збільшуватись, а підтримувати його ставатиме дедалі складніше. Де розмістити код для обробки відправки форми? Як перевіряти вхідні дані? Куди вставити код для розсилки email повідомлень?

3) Складність (скоріше — неможливість) повторного використання коду: зважаючи на те, що весь код розміщений в одному-єдиному файлі, ви не зможете використати його повністю або частково для іншої сторінки вашого блогу.

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

Ізоляція представлення

При відділенні “логіки” додатку від коду, який формує HTML “представлення” сторінки, структура додатку тільки виграє:

 <?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);

$result = mysql_query('SELECT id, title FROM post', $link);

$posts = array();
while ($row = mysql_fetch_assoc($result)) {
    $posts[] = $row;
}

mysql_close($link);

// include the HTML presentation code
require 'templates/list.php';

HTML код тепер знаходиться в окремому файлі (templates/list.php). Він являє собою HTML-файл, який використовує шаблонний синтаксис РНР:

 <!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="/read?id=<?php echo $post['id'] ?>">
                    <?php echo $post['title'] ?>
                </a>
            </li>
            <?php endforeach ?>
        </ul>
    </body>
</html>

За домовленістю файл, у якому міститься вся логіка додатку — index.php — називається “контролер” (англ. "controller"). Термін "контролер" ви будете часто чути незалежно від мови програмування чи фреймворка, який використовуєте (більше читайте тут). Насправді мова йде про частину вашого коду, який обробляє запит користувача і генерує відповідь.

У нашому випадку, контролер отримує дані з бази і підключає шаблон для того, щоб відобразити їх. Відокремивши контролер, ви отримуєте можливість змінювати тільки шаблон, якщо вам потрібно буде вивести список записів блогу в іншому форматі (наприклад, list.json.php дли використання формату JSON).

Ізоляція логіки додатку (домена)

До цього наш додаток містив тільки одну сторінку. А що робити, якщо потрібна ще одна, така, що використовує те ж підключення до бази даних або навіть той же масив постів із блогу? Давайте внесемо зміни у код, відокремивши логіку від функцій доступу до БД. Ці зміни збережемо в новому файлі під назвою model.php:

 <?php
// model.php
function open_database_connection()
{
    $link = mysql_connect('localhost', 'myuser', 'mypassword');
    mysql_select_db('blog_db', $link);

    return $link;
}

function close_database_connection($link)
{
    mysql_close($link);
}

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

    $result = mysql_query('SELECT id, title FROM post', $link);
    $posts = array();
    while ($row = mysql_fetch_assoc($result)) {
        $posts[] = $row;
    }
    close_database_connection($link);

    return $posts;
}

Така назва файлу, model.php, була обрана не випадково: логіка і доступ до даних додатку відома як “модель”. В правильно організованому додатку основна частина коду, що стосується “робочої логіки”, повинна бути розміщена у моделі (а не в контролері). І, на відміну від нашого прикладу, тільки частина моделі відповідає за доступ до БД (інколи взагалі не відповідає).

Тепер контролер (index.php) виглядає дуже просто:

<?php
require_once 'model.php';

$posts = get_all_posts();

require 'templates/list.php';

Тепер контролер відповідає за отримання даних з моделі додатку і звернення до шаблона для відображення даних. Це простий приклад шаблону model-view-controller.

Ізоляція розмітки (Layout)

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

Щоправда, зараз ми не можемо повторно використовувати розмітку сторінки (layout). Виправимо це, створивши файл layout.php:

<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
    <head>
        <title><?php echo $title ?></title>
    </head>
    <body>
        <?php echo $content ?>
    </body>
</html>

Шаблон (templates/list.php) також можна спростити, бо він буде “розширювати” базову розмітку:

<?php $title = 'List of Posts' ?>

<?php ob_start() ?>
    <h1>List of Posts</h1>
    <ul>
        <?php foreach ($posts as $post): ?>
        <li>
            <a href="/read?id=<?php echo $post['id'] ?>">
                <?php echo $post['title'] ?>
            </a>
        </li>
        <?php endforeach ?>
    </ul>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

Тепер ви знаєте, як можна зробити розмітку-layout доступною для повторного використання. Правда, щоб досягти цього, вам доведеться використати кілька "негарних" функцій РНР (ob_start(), ob_get_clean()) у шаблоні. Symfony використовує компонент Templating, який дозволяє виконати це завдання набагато простіше. Скоро дізнаєтесь, як саме.

Додаємо сторінку блогу "Show"

Ми оптимізували сторінку блогу “list” так, що код віднині став більш організованим і доступним для повторного використання. Аби переконатися, що це правда, додамо сторінку “show”, яка виводить один пост у блозі (який саме визначається за допомогою параметра запиту id).

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

// model.php
function get_post_by_id($id)
{
    $link = open_database_connection();

    $id = intval($id);
    $query = 'SELECT created_at, title, body FROM post WHERE id = '.$id;
    $result = mysql_query($query);
    $row = mysql_fetch_assoc($result);

    close_database_connection($link);

    return $row;
}
 Потім створюємо новий файл під назвою show.php (контролер для нашої нової сторінки):
  <?php
require_once 'model.php';

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

require 'templates/show.php';

І, нарешті, створюємо новий шаблон — templates/show.php, для виведення одного поста з блогу:

 <?php $title = $post['title'] ?>

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

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

<?php include 'layout.php' ?>

Ми легко і просто створили ще одну сторінку, уникнувши дублювання коду. Але ця сторінка додає інших проблем, які можна вирішити за допомогою фреймворка. Наприклад, якщо параметр id відсутній або неправильний, це призведе до фатальної помилки в додатку. Було б краще, якби в такому випадку виводилась сторінка 404, але, на жаль, досягнути цього наразі не дуже просто. І ще одна проблема: ви ж забули “очистити” параметр id за допомогою функції intval(). Тому вся ваша база даних ризикує постраждати від SQL-ін’єкції.

Є ще одна серйозна проблема, і полягає вона в тому, що кожен файл-контролер повинен підключати файл model.php. А що, якщо до кожного контролера потрібно буде підключити додатковий файл або виконати якусь іншу глобальну операцію (наприклад, пов’язану з безпекою)? Якщо додаток буде організовано так, як зараз, цей код потрібно буде додати в кожен контролер. Якщо ви забудете включити щось в один з файлів, ви можете тільки сподіватися на те, що це не вплине на безпеку всього додатку...

Фронт-контролер приходить на допомогу

Всі ці проблеми можна вирішити за допомогою фронт-контролера (front controller) — єдиного РНР файла, який буде обробляти абсолютно всі запити. При використанні фронт-контролера URI у вашому додатку трохи зміняться, зате стануть більш гнучкими:

  Without a front controller
/index.php          => Blog post list page (index.php executed)
/show.php           => Blog post show page (show.php executed)

With index.php as the front controller
/index.php          => Blog post list page (index.php executed)
/index.php/show     => Blog post show page (index.php executed)

Ту частину URI, до якої входить index.php, можна опустити, скориставшись правилами переписування веб-сервера Apache (чи його еквівалента). У такому разі URI для сторінки з блог постом буде мати вигляд /show.

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

Створення фронт-контролера

Увага! Це дуже важливий крок у розробці вашого додатку. За допомогою одного файлу, який приймає всі запити, ви можете їх централізовано обробляти (наприклад, запити, пов’язані з безпекою, завантаженням конфігурації, маршрутизацією тощо). У нашому додатку index.php повинен мати змогу відрізняти, відображати йому сторінку зі списком постів, або ж сторінку конкретного поста. Зробити це можна, беручи до уваги URI запиту:

 <?php
// index.php

// load and initialize any global libraries
require_once 'model.php';
require_once 'controllers.php';

// route the request internally
$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('Status: 404 Not Found');
    echo '<html><body><h1>Page Not Found</h1></body></html>';
}

Для оптимізації структури додатку обидва контролера (а ними були index.php i show.php) тепер перетворюються у PHP функції та віднині переміщуються до окремого файлу 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() i show_action()). Насправді цей фронт-контролер уже починає поводити себе як контролер в Symfony в питаннях обробки запитів та маршрутизації.

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

Зараз наш додаток розрісся з одного РНР-файла до цілої структури. Вона добре організована і дає можливість повторно використовувати код. Мабуть, ви вже щасливі, але до ідеалу все ще далеко! Наприклад, система “маршрутизації” ненадійна і не може визначити, що сторінка list (/index.php) повинна бути доступна через / (якщо використовуються Apache rewrite rules). Також, замість того, щоб займатись розробкою блогу, ви приділили купу часу на “архітектуру” коду (йдеться про маршрутизацію, виклик контролерів, шаблони і т.д.). Ще більше часу потрібно буде затратити, щоб обробити відправку форм, валідацію введених даних, логування і безпеку. Навіщо заново винаходити рішення для всіх цих рутинних проблем?

Наближення до Symfony

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

У кореневому каталозі створіть composer.json файл з наступним змістом:

{
    "require": {
        "symfony/symfony": "2.6.*"
    },
    "autoload": {
        "files": ["model.php","controllers.php"]
    }
}

Далі завантажте Composer, а потім виконайте наступну команду, яка завантажує Symfony в каталог vendor/:

$ composer install

Окрім завантаження залежностей, Composer створює файл vendor/autoload.php, який відповідає за автозавантаження всіх файлів у Symfony Framework, а також файлів, згаданих у розділі автозавантажень вашого composer.json.

В основі філософії Symfony лежить така ідея: основне завдання додатку — інтерпретація кожного запиту і повернення відповіді. Для цих цілей Symfony має два класи: Request і Response. Ці класи — об’єктно-орієнтоване представлення необробеного НТТР-запиту, який потрібно опрацювати, і адекватної до нього НТТР-відповіді, яка буде відправлена клієнту. Скористайтесь ними для вдосконалення вашого блогу:

<?php
// 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);
}

// echo the headers and send the response
$response->send();

Контролери тепер відповідають за повернення об’єкта Response. Для того, щоб спростити процес створення відповіді, можна додати нову функцію render_template(), яка поводить себе практично так, як і шаблонізатор Symfony:

// controllers.php
use Symfony\Component\HttpFoundation\Response;

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

    return new Response($html);
}

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

    return new Response($html);
}

// helper function to render templates
function render_template($path, array $args)
{
    extract($args);
    ob_start();
    require $path;
    $html = ob_get_clean();

    return $html;
}

Додаток став більш гнучким і надійним, почавши використовувати елементи Symfony. Request надає надійний спосіб отримати інформацію про запит. Наприклад, метод getPathInfo() повертає “очищений” URI (/show замість /index.php/show). Таким чином, навіть якщо користувач відкриє у браузері /index.php/show, додаток звернеться до show_action().

Об’єкт Response забезпечує гнучкість в побудові НТТР-відповіді, дозволяючи включати в них заголовки НТТР і контент сторінки через об’єктно-орієнтований інтерфейс. Хоча зараз в додатку відповіді і прості, ця гнучкість зіграє важливу роль з часом, коли додаток розростеться.

Додаток-приклад на Symfony

Наш блог почав свій розвиток. Але він все ще містить надто багато коду як для такого невеличкого додатку. Так, ми ввели просту систему маршрутизації і метод, який використовує ob_start() i ob_get_clean() для відображення шаблонів. Якщо з якихось причин ви хочете продовжити створювати цей “фреймворк” з нуля, можете хоча б скористатись спеціальними компонентами Symfony, які вирішують зазначені проблеми — Routing i Templating.

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

// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function listAction()
    {
        $posts = $this->get('doctrine')
            ->getManager()
            ->createQuery('SELECT p FROM AcmeBlogBundle:Post p')
            ->execute();

        return $this->render('Blog/list.html.php', array('posts' => $posts));
    }

    public function showAction($id)
    {
        $post = $this->get('doctrine')
            ->getManager()
            ->getRepository('AppBundle:Post')
            ->find($id);

        if (!$post) {
            // cause the 404 page not found to be displayed
            throw $this->createNotFoundException();
        }

        return $this->render('Blog/show.html.php', array('post' => $post));
    }
}

Ці два контролери залишились досить простими. Кожен використовує бібліотеку Doctrine ORM (про неї йтиметься детальніше в наступних розділах) для отримання об’єктів з бази і компонент Templating для відображення шаблонів та генерації об’єкту Response. Шаблон list тепер спростився ще більше:

 <!-- app/Resources/views/Blog/list.html.php -->
<?php $view->extend('layout.html.php') ?>

<?php $view['slots']->set('title', 'List of Posts') ?>

<h1>List of Posts</h1>
<ul>
    <?php foreach ($posts as $post): ?>
    <li>
        <a href="<?php echo $view['router']->generate(
            'blog_show',
            array('id' => $post->getId())
        ) ?>">
            <?php echo $post->getTitle() ?>
        </a>
    </li>
    <?php endforeach ?>
</ul>

Шаблон практично не змінився:

<!-- app/Resources/views/layout.html.php -->
<!DOCTYPE html>
<html>
    <head>
        <title><?php echo $view['slots']->output(
            'title',
            'Default title'
        ) ?></title>
    </head>
    <body>
        <?php echo $view['slots']->output('_content') ?>
    </body>
</html>

Шаблон show можете переробити самостійно за поданим прикладом роботи над шаблоном list. Це буде нескладно.

Коли завантажується "двигун" Symfony (що називається Kernel — ядро), він потребує “карти”, з якої можна дізнатися, який контролер потрібно застосувати, базуючись на інформації з запиту. Конфігурація маршрутизатора надає йому цю інформацію в такому вигляді:

# app/config/routing.yml
blog_list:
    path:     /blog
    defaults: { _controller: AppBundle:Blog:list }

blog_show:
    path:     /blog/show/{id}
    defaults: { _controller: AppBundle:Blog:show }

Тепер, коли Symfony перебрав на себе виконання всіх рутинних задач, фронт-контролер став надзвичайно простим. Він тепер має небагато функцій, тому вам не доведеться чіпати його після створення (якщо ви використовуєте дистрибутив Symfony, вам і створювати його не доведеться!):
// web/app.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

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

Єдина задача фронт-контролера тепер — ініціалізація движка Symfony (Kernel) і передача йому об’єкта Request для подальшої обробки. Ядро Symfony використовує карту маршрутизації для того, щоб визначити, який контролер потрібно запустити. Як і раніше, метод контролера відповідає за повернення кінцевого об’єкта Response.

Візуальне уявлення про процес обробки запиту на Symfony дає ця діаграма.

Чим корисний Symfony?

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

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

2) 100% написаного вами коду стосується вашого додатку. Не потрібно розробляти і підтримувати низькорівневі інструменти типу інсталятора, маршрутизатора чи інструмента для відображення контролерів;

3) Symfony дає вам доступ до інших інструментів з відкритим кодом, наприклад Doctrine, і компонентів Templating, Security, Form, Validation i Translation;

4) тепер додаток використовує надзвичайно гнучкі URL завдяки компоненту Routing.

5) Архітектура Symfony, централізована на НТТР, дає вам доступ до потужних інструментів, наприклад, НТТР кешування (базується на внутрішньому НТТР-кеші Symfony), або Varnish. Детальніше про це читайте у розділі про кешування.

І головне — використовуючи Symfony, ви отримаєте доступ до цілого набору якісних open source інструментів, розроблених учасниками спільноти! На KnpBundles.com ви знайдете чудову підбірку інструментів Symfony Community.

Кращі шаблони

Якщо ви зробили вибір на користь Symfony, приготуйтесь до зустрічі з шаблонізатором Twig, який дозволяє швидко розробляти прості та зрозумілі шаблони. Так, ваш додаток буде містити ще менше коду! Для прикладу розглянемо шаблон списку, написаний на Twig:

{# app/Resources/views/blog/list.html.twig #}
{% extends "layout.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.html.twig виглядає ще простіше:

 {# app/Resources/views/layout.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <title>{% block title %}Default title{% endblock %}</title>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

Twig прекрасно інтегрований із Symfony. РНР шаблони також підтримуються фреймворком, але ми детальніше зупинимось на перевагах Twig у розділі "Створення та використання шаблонів".

Дізнайтесь більше у Cookbook

1) Як використовувати PHP замість Twig для шаблонів

2) Як використовувати контролери як сервіси

 

Поділитися