Прєдупрєждєніє

Будь готов! В цій главі ми полностью переробим блог! Він не буде працювати (як раньше) аж до кінця домашнього заданія; код буде магічним; придеться пару раз удалять весь код з деяких файлів! Але в сухом остаткє, потом має стати ну дуже прикольно розробляти його дальше.

RIP, блог!

От коли-небудь ти точно міг замітить, шо на сайтах мало ссилок з адрєсами віда some_file.php?.... А більше ссилок віда /moo/foo. Так от, не зря ми чуть раньше вспоминали про MVC. У нас ще осталось цілих дві букви для расшифровки - Controller і View.

Раньше у нас один контроллєр отвічав чотко за одну странічку, за один php-файл. І ти помниш, як ми круто зробили з моделями, шоб можна було щитай пустий клас (ну і, канєшна, соотвєтствующу табличку в базє даних) создать - і модель у нас работала. От давай тепер зробим так, шоб і з кантроллєрами у нас було шось похоже - один клас отвічає за кучу разних странічок!

В всяких там Ruby On Rails, ASP.NET MVC, Django і прочіх модних мейнстрімових фреймворках штука “контроллєр” оброблює багато всяких странічок, об’єдіньонних похожой функциональностью. Каждий публічний метод контроллєра називається action і обрабатує виключно одну странічку. Так, напрімєр, якшо у нас є контроллєр “Пости”, він обрабатує всьо шо зв’язано тільки з постами:

  • редактірованіє
  • удалєніє
  • созданіє
  • просмотр
  • просмотр списка всіх постів

В цій часті ми будем убивать на корню весь наш блог! Чисто раді експерименту!

По суті, весь наш блог - це одна програма. В народі - application. Так давай зробим клас, який буде “запускать” наш сайт! Назвем цей клас Application і положим ми його рядом з index.php. В цьому класі нада зробить якийсь мєтод, який буде задавать настройки нашому сайту (конкрєтно нашому сайту) і мєтод, який буде собсно запускать наш сайт з цими настройками.

Якшо ти вніматєльно ізучав ООП, то поймеш, шо по феншую треба всякі пєрємєнні, які стосуються конкретно такого-то об’єкта задавать в конструкторі. А всі мєтоди шо визиватимуться у цього об’єкта будуть іспользовать ці пєрмєнні.

От так і предлагаю зробить:

<?php
    class Application {
        var $database;

        public function __construct() {
            $this->database = array(
                'user' => 'root',
                'password' => 'abc123',
                'host' => 'localhost',
                'database' => 'dbank'
            );
        }

        public function run() {
            // шось тут коїться!
        }
    }

а в файлі index.php тепер у нас буде тільки от така штука:

<?php
    require_once('application.php');

    $app = new Application();
    $app->run();

І якшо ти зараз запустиш свій блог - ти побачиш ровним щотом нічого. Бо в методі run() нічого не виводиться. Давай научимось розбирать адрєс странічки, яку хоче пользоватєль

Закопуєм блог

Тепер давай розберемся от з чим: у нас пользоватєль не хоче бачити таку бяку в адрєсной строкє браузера, як тіпа moo.php?foo=bar&baz=dao. Йому удобніше, пріятніше читать шось тіпа /moo/bar?baz=dao. Тіпа вони будуть запоминать або тіпа вони зможуть записать на бумажці десь це. Не важно особо, нашо це пользоватєлю - главне зробить його щасливим.

Щас ми будем обільно рефлєксірувать. Єсть в PHP така одна чудєсна пєрємєнна, як $_SERVER. Це масів. В ньому лежить багато інтересних всяких настройок, але нам поки треба буде тільки одна - $_SERVER['REQUEST_URI']. Як не странно, воно слабо зв’язано з сервером. Це - адрєс, який ввів пользоватєль, тільки без домєна. Так шо, якшо у нас сайт називається http://google.com/search/, а пользоватєль введе в адрєсну строку браузєра http://google.com/search/moo/foo/bar, то в соотвєтствующому php-файлі, пєрємєнна оця $_SERVER['REQUEST_URI'] буде мати значення /moo/foo/bar.

Але по умолчанію наш сервер перенаправляє пользоватєля на файл index.php, при цьому рісує цей index.php в адрєс странічки. Нада убрать його відти. Для цього ми візьмем настройки сєрвєра Apache (якшо у тебе ВНЕЗАПНО другий сервер - будем шось думать - пиши мені в лічку) і трошки їх поміняєм.

Першим ділом тобі треба включить расширєніє сервера під назвою rewrite. В лінуксах всяких це робиться в два етапа:

  1. ставиться пакєт apache2-utils
  2. включається це расширєніє командой [sudo] a2enmod rewrite

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

Потом тобі треба в корні сайта, рядом з index.php положить файлік .htaccess (начинається ім’я його з крапки!) з таким вмістом:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]

Ця штука заставить всі адрєса, шо пользоватєль вводить для цього сайта перенаправлять в index.php. І у тебе в пєрємєнной $_SERVER['REQUEST_URI'] всігда буде адрєс без /index.php.

Тепер давай розіб’єм все, шо нам буде присилать пользоватєль в строчці адрєса условно на три тіпа - контроллєр, екшн цього контроллєра і все остальне (в народі - парамєтри). Розбивать будем просто - по символу /. Все остальне - це по суті масив $_GET.

ЯКШО НЕ СТРЕЛЬНУЛО то треба в настройках хоста (в лінуксах - /var/www/apache2/sites-available/000-default.conf) дописати

<Directory /var/www/>
        AllowOverride all
        Options FollowSymlinks
        Order allow,deny
        Allow from all
</Directory>

Шо по суті для папки /var/www (де лежить сайт і, в часності, index.php) дає можливість через .htaccess мінять настройки сервера.

Давай тепер в методі Application::run() впишем код шоб воно виводило нам наш адрєс:

public function run() {
    echo $_SERVER['REQUEST_URI'];
}

Тепер розіб’єм цей адрєс на ім’я контроллєра і екшна. Розбиваєм по символу / і тупо берем перші два елємєнта получєного массіва. Якшо не хватає другого елємєнта - по умолчанію поставим index. Якшо не хватає ще й першого - по умолчанію поставим application.

$pieces = array_filter(explode('/', $_SERVER['REQUEST_URI']));
$controllerName = isset($pieces[1]) ? $pieces[1] : 'application';
$actionName = isset($pieces[2]) ? $pieces[2] : 'index';

Така хороша функція array_filter() убирає із массіва пусті елементи. А тройним оператором (condition) ? if_true : if_false ми робим круте присвоєніє нужного значєнія в пєрємєнні. І як не странно, але в цьому випадку ключі у масива - 1 і 2.

Тепер якшо ти виведеш ці пєрємєнні в методі Application::run() і зайдеш на сайт напрімєр по ссилці http://localhost/moo/foo , то ти побачиш як ця штука працює. Якшо передасиш один параметр через слешку: http://localhost/moo - побачиш шо $controllerName = 'moo' а $actionName = 'index'. Якшо два параметра - http://localhost/moo/foo - побачиш шо буде ще й $actionName = 'foo'. А якшо більше - то ніякої різниці вже не побачиш.

Цей момент важно запомнить: якшо наш сайт не знатиме якого контроллєра взять - буде намагатись найти контроллєр всього сайта. Якшо він не буде знати який йому екшн визвати - буде пробувать визвати екшн index.

Почучуть оживляєм

Тепер давай зробим якийсь конкрєтний контроллєр! Положим ми його в папочку controllers. Хай він поки особо нічого крутого не вміє - просто виводить <h1>I am alive!</h1>. І це у нас буде екшн index:

<?php
    class PostingsController {
        public function index() {
            echo '<h1>I am alive!</h1>';
        }
    }

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

Ми скористаємось такою шикарною штукою PHP як авто-пошук нужних класів. В народі - autoloading. Якшо пишем ми на стареньком PHP 5.0, то у нас ще довго буде возможность пользуватись глобальною магічною функцією __autoload($className).

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

Ну от напрімєр, ми точно знаєм, шо наші кантроллєри лежать в папці controllers. Так давай научим наш Application находить нужний контроллєр і создавать його об’єкт в методі run()!

<?php
    function __autoload($className) {
        require("controllers/$className.php");
    }

    class Application {
        // ...

        public function run() {
            $pieces = array_filter(explode('/', $_SERVER['REQUEST_URI']));

            $controllerName = isset($pieces[1]) ? $pieces[1] : 'application';
            $actionName = isset($pieces[2]) ? $pieces[2] : 'index';

            $controllerName = ucfirst($controllerName) . 'Controller';

            $controller = new $controllerName();
            $controller->$actionName();
        }
    }

Трошки заумний код. Начнем з нового: функція ucfirst($text) бере першу букву з $text і перероблює її у верхній регістр. Тоість, якшо було moo стане Moo, якшо було Moo - нічо не поміняється. Функція __autoload просто бере файл з папки controllers/ з імєнєм контроллєра і подключає його.

Тут важний момет нащот самой функції require(): коли ти її визиваєш вона по суті бере код із того файла і вставляє в місце, де ти визвав цю функцію. Точніше, не сам код файла, а виполняє цей файл і всьо шо вийшло в результаті - вставляє в це місце, де ти визвав require. Якщо там опрєдєлєні пєрємєнні чи класи - вони стають опрєдєлєні там де ти визвав require. Але якшо ти його визвав внутрі __autoload - то ці пєрємєнні чи класи стають доступні в усьому файлі, де сработав __autoload. Бо ще __autoload можна на каждий файл свій написать.

Виводим HTML

Якшо ти ще помниш, там де ми визиваєм require - виполняється код файла, який ми подключаєм і все що виполнилось вставляється в те місце.

Давай зробим таку інтересну штуку: зробим файлік який-небудь views/index.html з яким-небудь HTML і в екшні PostingsController::index() просто підключим його:

<?php
    class PostingsController {
        public function index() {
            require('index.html');
        }
    }

І якшо зараз зайти на http://localhost/postings - можна буде побачити содєржиме цього файла! І це прекрасно, ящітаю!

У каждого екшна є свій view - оддєльний файлік, який виводить HTML тільки основної частини страніци. Щитай це вміст тега <body>. А контроллєр збирає виведений цей HTML і виводить в свій оддєльний view, в якому і описане все вокруг тега <body> - щитай це весь HTML, кромє <body>. Для контроллєра цей view називається по-особєнному, layout. І от цей лейаут, він може бути один на всі-всі контроллєри, а може бути разний для нєкоторих або і вообщє - свій для каждого кантроллєра.

Так от, давай зробим слєдующім образом: у нас буде папочка views/layouts, де будуть лежать разні шаблончики. І ми зробим один шаблончик, який буде аутоматично подтягуватись для каждого контроллєра і назвем його application.phtml, як контроллєр “по умолчанію”. Замєть, файл має расширєніє .phtml. Це нормально. Це показує шо у нас в файлі є і PHP-код і HTML размєтка. В папочку views/layouts давай положим файлік postings.phtml куди наб’єм допустім такий HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <?php echo $body ?>
</body>
</html>

Тут ми берем якусь пєрємєнну $body і виводим її. Простенький файлік, нічого сложного. Але нам нада звідкись достать цей самий $body. Для цього ми іспользуєм ще одну інтересну плюшку PHP, output buffer. Воно позволяє виполнить якийсь код якби в фоні, а все шо цей код вивів - сохранить в пєрємєнну. Виглядить це так:

<?php
    ob_start();

        echo 'I am alive!';

    ob_end_clean();

Якшо ти запустиш цей код - нічого не виведеться. Но зато оцей тєкст, I am alive! можна достать функцієй ob_get_contents():

<?php
    ob_start();

        echo 'I am alive!';

    $content = ob_get_contents();

    ob_end_clean();

    echo "<h1>$content</h1>";

А от цей код вже виведе <h1>I am alive!</h1>. Замєть: ob_end_clean() очищає цей буфєр, куди ложиться вивод всього-всього між ob_start() і ob_end_clean(). І забрати звідки вивод треба до того, як визвать ob_end_clean().

А якшо ми тепер поміняємо просте echo між ob_start і ob_end на, скажімо, require()?

<?php
    ob_start();

    require('views/index.html');

    $content = ob_get_contents();

    ob_end_clean();

    require('views/layouts/postings.phtml');

Виходить, що в $content у нас лежить вміст файла views/index.html, а якшо ми подключаєм лейаут - він виведе HTML з цього лейаута і всередині нього - пєрємєнну $content.

А тепер, якшо ти оцей код написав внутрі PostingsController::index(), ми його переробим. Шоб не писать в каждом екшні каждого кантроллєра багато всяких ob_start, require і прочіх. Опять вертаємось до класа Application, в якому ми описуєм запуск нашого сайта. В нашому чудєсном методі run() ми вже написали шо робить коли пользоватєль заходить на сайт. Давай тепер трошки перепишем цей код шоб обернуть його вивод в нужний нам HTML:

<?php
    class Application {
        // ...
        public function run() {
            $pieces = array_filter(explode('/', $_SERVER['REQUEST_URI']));

            $controllerName = isset($pieces[1]) ? $pieces[1] : 'application';
            $actionName = isset($pieces[2]) ? $pieces[2] : 'index';

            $controllerClass = ucfirst($controllerName) . 'Controller';

            // вичісляєм ім’я view
            $viewFile = "views/$controllerName/$actionName.phtml";

            // вичісляєм ім’я layout
            $layoutFile = "views/layouts/$controllerName.phtml";

            $controller = new $controllerClass();

            ob_start();

            // тут внутрі контроллєра задаються якісь пєрємєнні
            $controller->$actionName();

            // рісуєм відповідний view, в якому виводяться пєрємєнні, задані в екшні
            require($viewFile);

            // получаєм вивод екшна - наш body
            $body = ob_get_contents();

            ob_end_clean();

            // виводим лейаут, в якій виводиться body
            require($layoutFile);
        }
    }

При такому розкладі у нас цей мєтод робить сильно забагато всього. А всі пєрємєнні, які ми задаєм внутрі екшна недоступні всередині view - вони задаються внутрі контроллєра, а рісує в’юху у нас - application. Значить, вертаємось до ООП. Будем трошки усложнять зараз роботу шоб потом писать було проще.

Зробим основний клас для всіх контроллєрів, який буде рісувать свій лейаут і нужні в’юхи. А із application будем визивать якийсь один метод, який просто виведе все шо треба. Цей клас ми, по старой доброй традиції, положим в папку core/:

<?php
    class BaseController {
        public function __invoke($controllerName, $actionName) {
            // вичісляєм ім’я view
            $viewFile = "views/$controllerName/$actionName.phtml";

            // вичісляєм ім’я layout
            $layoutFile = "views/layouts/$controllerName.phtml";

            ob_start();

            $this->$actionName();

            // рісуєм відповідний view, в якому виводяться пєрємєнні, задані в екшні
            require($viewFile);

            $body = ob_get_contents();

            ob_end_clean();

            // виводим лейаут, в якій виводиться body
            require($layoutFile);
        }
    }

Ми просто щитай перенесли код з одного класа в другий. Замєть, ім’я мєтода називається з двох підкреслень. Ми тоже можем создавать магічні методи, муа-ха-ха! Тепер трошки переробим наш контроллєр PostingsController:

<?php
    require_once('core/BaseController.php');
    require_once('models/Posting.php');

    class PostingsController extends BaseController {
        function index() {
            $this->postings = Posting::all();
        }
    }

Але тепер дивись: у нас в каждій моделі подключається файл BaseModel.php. Тепер ще нам прийдеться в каждом контроллєрі подключать BaseController.php. Не комільфо. Щас будем рефакторить - всі ці строчки з моделей і з контроллерів можна перенести в Application.php - цей файл у нас по-любому подключається і всігда запускається самий перший.

<?php
    require_once('core/BaseModel.php');
    require_once('core/BaseController.php');

    // ...

    class Application {
        // ...
    }

Тепер нам треба перемістить views/index.html в views/postings/index.phtml шоб наш Application::run() знайшов його. І з PostingsController::index() убрать весь код - у нас тепер усьо аутоматично!

Ітог

Шо нам дає така сложна сістєма? По-перше, і це дуже важно, нам тепер не треба в усі файли пихать один і той же HTML. По-друге, у нас сайт може мати кучу совєршенно разних по дизайну і вигляду странічок тепер. І шоб їх помінять чи шоб в них встроїть, допустім, виведення всіх постінгів чи якоїсь формочки, нам треба просто вивести туди в’юху. І перероблювать, в случаї чого, треба дуже і дуже мало кода.

Наконєц, чисто для наглядності давай зробим простеньке упражнєніє: виведем в екшні PostingsController::index() всі постінги, іспользуя всі ці лейаути-в’юхи і моделі.

У нас уже є лейаут для контроллєра PostingsController. У нас уже є сам контроллєр. У нас є модель поста. У нас даже є в’юха (і байдуже шо дурна) для цього!

Значить, пробуєм? В екшні ми задамо перемєнну з усіма постінгами. Кстаті, конструктор контроллєра нам уже не треба - вже не актуально, не мейнстрім. А у в’юхє ми просто виведем всі постінги.

<?php
    require_once('models/Posting.php');

    class PostingsController {
        public function index() {
            $this->postings = Posting::all();
        }
    }

І в’юха:

<?php foreach ($this->postings as $posting): ?>
    <h1><?php echo $posting->title ?></h1>

    <div class="description">
        <?php echo $posting->description ?>
    </div>
<?php endforeach; ?>

Домашнє заданіє

Воно буде велике. Prepare yourself!

  1. Треба прєдусмотрєть ситуацію, коли ми не найшли нужний класс контроллєра (щитай, не найшли файл) - в таком случаї нада показать ошибку 404.
  2. Переведи весь блог на контроллєри, в’юхи, лейаути і моделі. Попробуй, чи прощє тепер чи сложніше стало робить це.