Чат PHP-разработчиков
Фронт-контроллер и роутинг в PHP

Фронт-контроллер и роутинг в PHP

В прошлом уроке мы добавили в контроллер 2 экшена и стали проверять в index.php GET-параметр. В зависимости от этого параметра мы решали, какой из экшенов вызвать и что передать в качестве аргументов. А что будет, когда нам на сайте понадобится более 100 страниц? Для каждого добавлять if? Согласитесь, неудобно. В этом уроке мы сделаем удобную систему для обработки адресов сайта – роутинг (от англ. routing - маршрутизация).

Если вы не работали ранее с регулярными выражениями – пройдите урок по регуляркам в PHP.

Apache RewriteEngine

Для начала немного магии. Создайте в директории www файл .htaccess и запишите в него следующее содержимое:

Код доступен только после покупки курса ООП в PHP.

Это – специальный файл с конфигурацией для веб-сервера Apache. Если забыли – то это именно он обрабатывает запросы от пользователя и передаёт их дальше интерпретатору PHP. Подробнее об этом читайте в статье "как работает PHP". Когда Apache находит в директории файл с именем .htaccess он понимает, что это его конфиг и применяет его для директории, в которой этот конфиг лежит (и для вложенных директорий тоже).

RewriteEngine – это такой механизм в сервере Apache, который позволяет перенаправлять запросы. А теперь давайте рассмотрим каждую строку файла отдельно.

  • RewriteEngine On – включаем режим перенаправления запросов
  • RewriteCond %{SCRIPT_FILENAME} !-d – если в директории есть папка, соответствующая адресу запроса, то отдать её в ответе
  • RewriteCond %{SCRIPT_FILENAME} !-f – если в директории есть файл, соответствующий адресу запроса, то вернуть его в ответе
  • RewriteRule ^(.)$ ./index.php?route=$1 [QSA,L] – если файл или папка не найдены, то для такого запроса выполнится этот пункт. В таком случае веб-сервер перенаправить этот запрос на скрипт index.php. При этом скрипту будет передан GET-параметр route со значением запрошенного адреса. $1 – это значение, выдернутое с помощью регулярки по маске ^(.)$. То есть вся адресная строка будет передана в этот GET-параметр.

Давайте теперь это проверим. Откроем в браузере адрес http://myproject.loc/abracadabra.

Опа! Видим текст «Главная страница». Значит мы попали на index.php. Давайте теперь попробуем в index.php вывести GET-параметр route. Уберём пока код, добавленный на предыдущих уроках и оставим только автозагрузку классов и вывод этого GET-параметра.

Код доступен только после покупки курса ООП в PHP.

Снова откроем тот же адрес http://myproject.loc/abracadabra и увидим следующее:

Код доступен только после покупки курса ООП в PHP.

Давайте попробуем другой адрес - http://myproject.loc/hello/username

Код доступен только после покупки курса ООП в PHP.

ЧПУ

Такие адреса через слэши называются ЧПУ – Человеко Понятные УРЛы. То есть адреса, которые нормально воспринимаются человеком.
Согласитесь
http://myproject.loc/hello/username
лучше чем
http://myproject.loc/?action=hello&name=username

На таких ЧПУ-адресах мы и будем разрабатывать нашу систему.

Роутинг

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

Для начала давайте сделаем по-простому – с помощью регулярки научимся понимать, что текущий адрес: http://myproject.loc/hello/ , где - вообще любая строка.

Код доступен только после покупки курса ООП в PHP.

Обратите внимание – в качестве ограничителя шаблона регулярного выражения мы использовали тильду - ~. Мы выбрали её вместо слэша, чтобы не экранировать слэш в адресной строке. Напомню, что в качестве ограничителя может выступать вообще любой символ.

Перейдём по адресу http://myproject.loc/hello/username и увидим наши совпадения по регулярке:

Код доступен только после покупки курса ООП в PHP.

Нулевой элемент – полное совпадение по паттерну. Первый элемент – значение, попавшее в маску (.*), то есть всё, что идёт после hello/.

Давайте теперь добавим проверку того, что если $matches не пустой, то будем создавать контроллер MainController и вызывать у него экшен hello. В качестве аргумента будем передавать ему значение из массива по ключу 1.

Код доступен только после покупки курса ООП в PHP.

Посмотрим, что получилось.

Код доступен только после покупки курса ООП в PHP.

Отлично! Давайте теперь добавим обработку случая, когда мы просто зашли на http://myproject.loc/. В таком случае переменная route будет пустой строкой. Регулярка для такого случая - ^$. Да, просто начало строки и конец строки. Проще не бывает!

Код доступен только после покупки курса ООП в PHP.

Перейдём теперь на страницу http://myproject.loc/ и увидим сообщение «Главная страница».
Остаётся только добавить обработку случая, когда ни одна из этих регулярок не подошла и просто вывести сообщение о том что страница не найдена.

Давайте просто добавим в конце index.php строку:

Код доступен только после покупки курса ООП в PHP.

И проверим, что всё работает, перейдя по любому другому адресу: http://myproject.loc/blabla.

А теперь давайте посмотрим на получившийся код и подумаем, нет ли здесь чего-то общего? Да конечно есть! В обоих случаях мы проверяем регулярки и в зависимости от совпадения создаём нужный контроллер, с нужным методом и нужными аргументами. Но механизм-то одинаковый! Значит, будем делать универсальную систему роутинга.

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

Давайте создадим отдельный файл с такой конфигурацией. Пусть это будет файл src/routes.php. Запишем в него следующее содержимое:
src/routes.php

Код доступен только после покупки курса ООП в PHP.

То есть это просто массив, у которого ключи – это регулярка для адреса, а значение – это массив с двумя значениями – именем контроллера и названием метода.

Теперь вернёмся в index.php и научимся обрабатывать этот файл. Для начала давайте просто положим этот массив в отдельную переменную.

Код доступен только после покупки курса ООП в PHP.

Результат:

Код доступен только после покупки курса ООП в PHP.

Что с этим делать? Да просто пробежаться по нему foreach-ом и найти соответствие по регулярке для текущего адреса. Как только совпадение найдено, нужно остановить перебор. Звучит несложно. Давайте сделаем это!

Код доступен только после покупки курса ООП в PHP.

Я завел также специальную переменную $isRouteFound – на случай, если совпадений не было найдено, она останется false, как и до перебора. В таком случае мы выведем сообщение о том, что страница не найдена и завершим работу скрипта. В противном случае – выведем значение переменных $controllerAndAction и $matches.

Давайте проверим случай, когда нужный роут не найден - http://myproject.loc/blabla

Код доступен только после покупки курса ООП в PHP.

Всё правильно. Давайте теперь вернёмся на http://myproject.loc/

Код доступен только после покупки курса ООП в PHP.

Видим, что у нас есть имя нужного контроллера и имя метода. Всё, этого достаточно. Вот так это делается в PHP:

Код доступен только после покупки курса ООП в PHP.

Да! Прямо вот так! Переменную можно использовать в качестве имени класса при создании объекта, и даже вместо имени метода!

Зайдите на http://myproject.loc/ и убедитесь, что всё прекрасно работает.

Но у нас осталась еще проблема с аргументами для методов.

Давайте вернёмся к предыдущему варианту кода, где мы просто вывели значения переменных:

Код доступен только после покупки курса ООП в PHP.

И перейдём по адресу http://myproject.loc/hello/username
Результат:

Код доступен только после покупки курса ООП в PHP.

Видим что у нас так же есть имя контроллера и имя метода. А также нужный нам аргумент в массиве $matches.

Но мы видим, что нужные нам аргументы всегда будут только после нулевого элемента, так как в нём лежит полное совпадение по паттерну. Не беда – просто удаляем этот ненужный элемент:

Код доступен только после покупки курса ООП в PHP.

Получаем следующую картину:

Код доступен только после покупки курса ООП в PHP.

Остаётся только один вопрос – как элементы массива передать в аргументы метода? Для этого в PHP есть специальный оператор троеточия:

Код доступен только после покупки курса ООП в PHP.

Он передаст элементы массива в качестве аргументов методу в том порядке, в котором они находятся в массиве.

Теперь доводим до ума наш скрипт:

Код доступен только после покупки курса ООП в PHP.

Переходим по адресу http://myproject.loc/hello/username и видим что всё работает!

Вот мы и сделали роутинг. Теперь если нам понадобится добавить новый адрес на сайте то мы просто пропишем его в routes.php, и укажем имя контроллера и метода. Остальное произойдёт автоматически!

Ах да, наш index.php - скрипт, в котором происходит обработка входящих запросов и создаются другие контроллеры, называется фронт-контроллером.

Домашнее задание

Создайте еще один экшн в контроллере – sayBye(string $name), который будет выводить «Пока, $name». Добавьте для него роут /bye/$name и убедитесь, что всё работает.

Читайте также
Комментарии (20)


sashatkachenko1993@gmail.com

Добрый день в первой части урока выдает вместо
'abracadabra'
'train.com/public_html/abracadabra'

ivashkevich

Привет, напишите мне в вк или телеграм.

SlipKnot

src\MyProject\Controllers\MainController.php

<?php

namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        echo 'Main page!';
    }
    public function sayHello(string $name)
    {
        echo 'Hello, ' . $name . '!';
    }
    public function sayBye(string $name)
    {
        echo 'Bye, bye ' . $name;
    }
}

src\routes.php

<?php

return [
    '~^hello/(.*)$~'=> [\MyProject\Controllers\MainController::class, 'sayHello'],
    '~^bye/(.*)$~'=> [\MyProject\Controllers\MainController::class, 'sayBye'],
    '~^$~'=>[\MyProject\Controllers\MainController::class, 'main'],
];

www\index.php

<?php

spl_autoload_register(function (string $className) {
    require_once __DIR__ . '/../src/' . $className . '.php';
});

$route = $_GET['route'] ?? '';
$routes = require __DIR__ . '/../src/routes.php';

$isRouteFound = false;
foreach ($routes as $pattern => $controllerAndAction) {
    preg_match($pattern, $route, $matches);
    if (!empty($matches)) {
        $isRouteFound = true;
        break;
    }
}
if (!$isRouteFound) {
    echo '!!!PAGE NOT FOUND!!!';
    return;
}
unset ($matches[0]);

$controllerName = $controllerAndAction[0]; // 0 => string 'MyProject\Controllers\MainController' (length=36)
$actionName = $controllerAndAction[1]; //1 => string 'sayHello' (length=8)
$controller = new $controllerName(); //object(MyProject\Controllers\MainController)[1]
$controller->$actionName(...$matches);
SBTesla

файл MainController.php

namespace MyProject\Controllers;

class MainController {
     public function main()
     {
          echo 'Главная страница';
     }
     public function sayHello (string $name)
     {
          echo 'Привет' . $name;
     }
     public function sayBye(string $name)
     {
          echo 'Проваливай' . $name;
     }
}

файл routes.php

return ['~ ^Hello/(.*)$~'=>[\MyProject\Controllers\MainController::class, 'sayHello'],
      '~^$~'=>[MyProject\Controllers\MainController::class, 'main'],
      '~^bye/(.*)$~'=>[MyProject\Controllers\MainController::class, 'sayBye']
      ];

файл Index.php

spl_autoload_register(function (string $className) {
     require_once __DIR__ . '/../src/' . $className . '.php';
});

$route = $_GET['route'] ?? '';
$routes = require __DIR__ . '/../src/routes.php';

$isRouteFound = false;
foreach ($routes as $pattern => $controllerAndAction) {
     preg_match($pattern, $route,$matches);
     if (!empty($matches)) {
          $isRouteFound = true;
          break;
     }
}
if (!$isRouteFound) {
     echo 'Страница не найдена';
     return ;
}
unset($matches[0]);
$controllerName = $controllerAndAction[0];
$actionName = $controllerAndAction[1];
$controller = new $controllerName();
$controller->$actionName(...$matches);
ArtemijeKA

src/routes.php +

'~^bye/(.*)$~' => [\MVCExample\Controllers\MainController::class, 'sayBye'],

src/MVCExample/Controllers/MainController.php +

public function sayBye(string $name)
    {
        echo 'Пока, ' . $name;
    }

url +

http://php_book/mvc/www/bye/artem

Выхлоп =

Пока, artem

Спасибо.

ivashkevich

А что у вас за url такой жуткий? Опять экспериментируете не по курсу?

AxLT

routes.php

<?php
return [
    '~^hello/(.*)$~' => [\MyProject\Controllers\MainController::class, 'sayHello'],
    '~^bye/(.*)$~' => [\MyProject\Controllers\MainController::class, 'sayBye'],
    '~^$~' => [\MyProject\Controllers\MainController::class, 'main'],
    ];

MainController.php

<?php
namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        echo 'Главная страница';
    }
    public function sayHello(string $name)
    {
        echo ' Hello ' . $name;
    }
    public function sayBye(string $name)
    {
        echo 'Bye ' . $name;
    }
}
AntonM99

Пока что трудно переваривается информация, но вроде справляюсь

routes.php

<?php
return [
    '~^hello/(.*)$~' => [\MyProject\Controllers\MainController::class, 'sayHello'],
    '~^bye/(.*)$~' => [\MyProject\Controllers\MainController::class, 'sayBye'],
    '~^$~' => [\MyProject\Controllers\MainController::class, 'main']

];

MainController.php

<?php
namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        echo 'Главная страница';
    }

    public function sayHello(string $name)
    {
        echo 'Привет, ' . $name;
    }

    public function sayBye(string $name)
    {
        echo 'Пока, ' . $name;
    }
}
Todd

MainController

namespace app\Controllers;

class MainController {

    public function main() {
        return 'main page';
    }

    public function sayHello(string $name) {
        return 'hi, ' . $name;
    }

    public function sayBye(string $name) {
        return 'Bye, ' . $name;
    }

}

routes

return [
    '~^hello/(.*)$~' => [\app\Controllers\MainController::class, 'sayHello'],
    '~^bye/(.*)$~'   => [\app\Controllers\MainController::class, 'sayBye'],
    '~^$~'           => [\app\Controllers\MainController::class, 'main'],
];

index

spl_autoload_register(function (string $className) {
    require __DIR__ . '/' . $className . '.php';
});

$route = $_GET['route'] ?? '';
$routes = require __DIR__ . '/routes.php';

$isRouteFound = false;
foreach ($routes as $pattern => $controllerAndAction) {
    preg_match($pattern, $route, $matches);
    if (!empty($matches)) {
        $isRouteFound = true;
        break;
    }
}

if (!$isRouteFound) {
    echo 'not found page';
}

unset($matches[0]);

$controllerName = $controllerAndAction[0];
$controllerAction = $controllerAndAction[1];

$controller = new $controllerName();
echo $controller->$controllerAction(...$matches);
demyanovpaul@yandex.ru

Создайте еще один экшн в контроллере – sayBye(string $name), который будет выводить «Пока, $name». Добавьте для него роут /bye/$name и убедитесь, что всё работает.

<?php
//routes.php
return [
    '#^hello/(.*)$#' => [MyProject\Controllers\MainController::class, 'sayHello'],
    '#^bye/(.*)$#' => [MyProject\Controllers\MainController::class, 'sayBye'],
    '#^$#' => [MyProject\Controllers\MainController::class, 'main']
];

//MainController.php
namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        echo 'Главная страница';
    }

    public function sayHello(string $name)
    {
        echo 'Привет, ' . $name;
    }

    public function sayBye(string $name)
    {
        echo 'Пока, ' . $name;
    }
}
bildep

src\MyProject\Controllers\MainController.php

<?php
namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        echo 'Главная страница';
    }

    public function sayHello(string $name)
    {
        echo 'Привет, ' . $name;
    }

    public function sayBye(string $name)
    {
        echo 'Пока, ' . $name;
    }
}

src\routes.php

<?php

return [
    '~^hello/(.*)$~' => [\MyProject\Controllers\MainController::class, 'sayHello'],
    '~^bye/(.*)$~' => [\MyProject\Controllers\MainController::class, 'sayBye'],
    '~^$~' => [\MyProject\Controllers\MainController::class, 'main'],
];
g--nokoder

Правильно ли делать отдельный класс route, чтобы делать маршрутизацию или же сделать как Вы указали? Есть много примеров отдельного класса, и актуально ли это?

ivashkevich

Да, можно делать отдельным классом, в котором будет логика парсинга и всё что только нужно. Здесь специально не стал выносить для простоты.

Популярное за сутки
Сейчас читают
Логические задачи с собеседований