Чоткій блог. Часть третя
Прєдупрєждєніє
Будь готов! В цій главі ми полностью переробим блог! Він не буде працювати (як раньше) аж до кінця домашнього заданія; код буде магічним; придеться пару раз удалять весь код з деяких файлів! Але в сухом остаткє, потом має стати ну дуже прикольно розробляти його дальше.
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
. В лінуксах всяких це робиться в два етапа:
- ставиться пакєт
apache2-utils
- включається це расширєніє командой
[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!
- Треба прєдусмотрєть ситуацію, коли ми не найшли нужний класс контроллєра (щитай, не найшли файл) - в таком случаї нада показать ошибку 404.
- Переведи весь блог на контроллєри, в’юхи, лейаути і моделі. Попробуй, чи прощє тепер чи сложніше стало робить це.