Я хотел бы затронуть тему правильной архитектуры приложений на PHP. Статья будет посвящена паттерну проектирования MVC. Написать про MVC меня сподвиг именно тот факт, что понимание этого паттерна является ключевым в становлении программиста. Так что если вы новичок и хотите начать писать программы правильно, читайте дальше.
Теория
Если коротко, то MVC (model view controller) это такой способ написания программы, когда код отвечающий за вывод данных, пишется в одном месте, а код который эти данные формирует, пишется в другом месте. В итоге получается так, что если вам надо подправить вывод вы сразу знаете в каком месте искать. Сейчас все популярные фреймворки используют такую архитектуру.
Также стоит упомянуть тот факт, что существует два лагеря: один пишет логику в контроллерах, второй в моделях. В тех фреймворках, которые знаю я (yii, laravel) логику пишут в контроллерах, а модели являются просто экземплярами ORM (почитайте про паттерн Active Record). У yii кстати в мануале написано, что писать логику надо в моделях, а потом они сами в примерах пишут её в контроллерах, довольно забавно.
С бизнес-логикой определились, пишем в контроллерах. Также в методах контроллера происходит вызов моделей, которые у нас по сути экземпляры ORM, чтобы с их помощью получить данные из базы над которыми будут производить изменения. Конечный результат отправляется в виды. Виды cодержат HTML-разметку и небольшие вставки PHP-кода для обхода, форматирования и отображения данных.
Ещё можно упомянуть, что есть два вида MVC Page Controller и Front Controller. Page Controller'ом пользуются редко, его подход заключается в использовании нескольких точек входа (запросы к сайту осуществляются к нескольким файлам), и внутри каждой точки входа свой код отображения. Мы будем писать Front Controller с одной точкой входа.
Практика
Начать надо с настройки сервера для переадресации на нашу единую точку входа. Если у нас apache, то в файле .htaccess пишем следующее
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L]
Дальше в папке нашего проекта создаём папку, которую можно назвать App например. В ней будет следующее содержимое.
Наш Service Locator. Файл App.php
<?php
class App
{
public static $router;
public static $db;
public static $kernel;
public static function init()
{
spl_autoload_register(['static','loadClass']);
static::bootstrap();
set_exception_handler(['App','handleException']);
}
public static function bootstrap()
{
static::$router = new AppRouter();
static::$kernel = new AppKernel();
static::$db = new AppDb();
}
public static function loadClass ($className)
{
$className = str_replace('\', DIRECTORY_SEPARATOR, $className);
require_once ROOTPATH.DIRECTORY_SEPARATOR.$className.'.php';
}
public function handleException (Throwable $e)
{
if($e instanceof AppExceptionsInvalidRouteException) {
echo static::$kernel->launchAction('Error', 'error404', [$e]);
}else{
echo static::$kernel->launchAction('Error', 'error500', [$e]);
}
}
}
Сервис локатор нужен чтобы хранить в нём компоненты нашего приложения. Поскольку у нас простое mvc приложение, то мы не используем паттерн registry (как например в yii сделано). А просто сохраняем компоненты приложения в статические свойства, чтобы обращаться к ним было проще. Ещё App регистрирует автозагрузчик классов и обработчик исключений.
Роутер. Файл Router.php
<?php
namespace App;
class Router
{
public function resolve ()
{
if(($pos = strpos($_SERVER['REQUEST_URI'], '?')) !== false){
$route = substr($_SERVER['REQUEST_URI'], 0, $pos);
}
$route = is_null($route) ? $_SERVER['REQUEST_URI'] : $route;
$route = explode('/', $route);
array_shift($route);
$result[0] = array_shift($route);
$result[1] = array_shift($route);
$result[2] = $route;
return $result;
}
}
В простом mvc приложении роутер содержит всего один метод. Он парсит адрес из $_SERVER['REQUEST_URI']. Я ещё не сказал, что все наши ссылки на страницы сайта должны быть вида www.ourwebsite.com/%controller%/%action%, где %controller% — имя файла контроллера, а %action% — имя метода контроллера, который будет вызван.
Файл Db.php
<?php
namespace App;
use App;
class Db
{
public $pdo;
public function __construct()
{
$settings = $this->getPDOSettings();
$this->pdo = new PDO($settings['dsn'], $settings['user'], $settings['pass'], null);
}
protected function getPDOSettings()
{
$config = include ROOTPATH.DIRECTORY_SEPARATOR.'Config'.DIRECTORY_SEPARATOR.'Db.php';
$result['dsn'] = "{$config['type']}:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$result['user'] = $config['user'];
$result['pass'] = $config['pass'];
return $result;
}
public function execute($query, array $params=null)
{
if(is_null($params)){
$stmt = $this->pdo->query($query);
return $stmt->fetchAll();
}
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
Этот класс юзает файл конфига, который возврашает массив при подключении
Файл Config/Db.php
<?php
return [
'type' => 'mysql',
'host' => 'localhost',
'dbname' => 'gotlib',
'charset' => 'utf8',
'user' => 'root',
'pass' => ''
];
Наше ядро. Файл Kernel.php
<?php
namespace App;
use App;
class Kernel
{
public $defaultControllerName = 'Home';
public $defaultActionName = "index";
public function launch()
{
list($controllerName, $actionName, $params) = App::$router->resolve();
echo $this->launchAction($controllerName, $actionName, $params);
}
public function launchAction($controllerName, $actionName, $params)
{
$controllerName = empty($controllerName) ? $this->defaultControllerName : ucfirst($controllerName);
if(!file_exists(ROOTPATH.DIRECTORY_SEPARATOR.'Controllers'.DIRECTORY_SEPARATOR.$controllerName.'.php')){
throw new AppExceptionsInvalidRouteException();
}
require_once ROOTPATH.DIRECTORY_SEPARATOR.'Controllers'.DIRECTORY_SEPARATOR.$controllerName.'.php';
if(!class_exists("\Controllers\".ucfirst($controllerName))){
throw new AppExceptionsInvalidRouteException();
}
$controllerName = "\Controllers\".ucfirst($controllerName);
$controller = new $controllerName;
$actionName = empty($actionName) ? $this->defaultActionName : $actionName;
if (!method_exists($controller, $actionName)){
throw new AppExceptionsInvalidRouteException();
}
return $controller->$actionName($params);
}
}
Ядро обращается к роутеру, а потом запускает действия контроллера. Ещё ядро может кинуть исключение, если нет нужного контроллера или метода.
Файл Controller.php
Ещё нам нужно создать базовый класс для наших контроллеров, чтобы потом наследоваться от него. Наследовать методы нужно для того, чтобы вы могли рендерить (сформировать вывод) виды. Методы рендеринга поддерживают использование лэйаутов — шаблонов, которые содержат общие для всех видов компоненты, например футер и хэдер.
<?php
namespace App;
use App;
class Controller
{
public $layoutFile = 'Views/Layout.php';
public function renderLayout ($body)
{
ob_start();
require ROOTPATH.DIRECTORY_SEPARATOR.'Views'.DIRECTORY_SEPARATOR.'Layout'.DIRECTORY_SEPARATOR."Layout.php";
return ob_get_clean();
}
public function render ($viewName, array $params = [])
{
$viewFile = ROOTPATH.DIRECTORY_SEPARATOR.'Views'.DIRECTORY_SEPARATOR.$viewName.'.php';
extract($params);
ob_start();
require $viewFile;
$body = ob_get_clean();
ob_end_clean();
if (defined(NO_LAYOUT)){
return $body;
}
return $this->renderLayout($body);
}
}
Файл index.php
Не забываем создать индексный файл в корне:
<?php
define('ROOTPATH', __DIR__);
require __DIR__.'/App/App.php';
App::init();
App::$kernel->launch();
Создаём контроллеры и виды
Работа с нашим приложением (можно даже сказать минифреймворком) теперь сводится к созданию видов и контроллеров. Пример контроллера следующий (в папке Controllers):
<?php
namespace Controllers;
class Home extends AppController
{
public function index ()
{
return $this->render('Home');
}
}
Пример вида(в папке Views):
<img src="Img/my_photo.jpeg" alt="my_photo" id="my_photo">
<h1>Привет</h1>
<p>Меня зовут Глеб и я - веб-разработчик.</p>
Мои контакты:<br>
8-912-641-3462<br>
goootlib@gmail.com
В папке Views/Layout создаём Layout.php:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Обо мне</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="/Css/style_layout.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
</head>
<body>
<header>
<nav>
<a id="about_button" href="/home">Обо мне</a>
<a id="portfolio_button" href="/portfolio">Портфолио</a>
<a id="blog_button" href="/blog">Блог</a>
</nav>
</header>
<div class="main">
<?= $body ?>
</div>
<footer>
<div class="copyrights">
2017 Жуков Глеб(gotlib)<br>
При копировании материалов на сторонние ресурсы, ссылка на http://www.gotlib.info обязательна!
</div>
<div class="contacts">
8-912-641-3462<br>
goootlib@gmail.com
</div>
</footer>
</body>
</html>
Заключение
Если решите пользоваться кодом приложения, которое описал я, не забудьте создать контроллер Error с методами error404 и error500. Класс для работы с бд, описанный мной, подходит для написания запросов руками, вместо него можно подключить ORM и вас будут настоящие модели.
Автор: machetero