Как стать автором
Обновить

Простые MVC-приложения

Время на прочтение6 мин
Количество просмотров46K
Я хотел бы затронуть тему правильной архитектуры приложений на PHP. Статья будет посвящена паттерну проектирования MVC. Написать про MVC меня сподвиг именно тот факт, что понимание этого паттерна является ключевым в становлении PHP-программиста. Так что если вы новичок и хотите начать писать программы правильно, читайте дальше.

Теория


Если коротко, то MVC (model view controller) это такой способ написания программы, когда код отвечающий за вывод данных, пишется в одном месте, а код который эти данные формирует, пишется в другом месте. В итоге получается так, что если вам надо подправить вывод вы сразу знаете в каком месте искать. Сейчас все популярные фреймворки используют такую архитектуру.

Также стоит упомянуть тот факт, что существует два лагеря: один пишет логику в контроллерах, второй в моделях. В тех фреймворках, которые знаю я (yii, laravel) логику пишут в контроллерах, а модели являются просто экземплярами ORM. У 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 App\Router();
        static::$kernel = new App\Kernel();
        static::$db = new App\Db();
       
    }
    
    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 \App\Exceptions\InvalidRouteException) {
            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 \App\Exceptions\InvalidRouteException();
        }
        require_once ROOTPATH.DIRECTORY_SEPARATOR.'Controllers'.DIRECTORY_SEPARATOR.$controllerName.'.php';
        if(!class_exists("\\Controllers\\".ucfirst($controllerName))){
            throw new \App\Exceptions\InvalidRouteException();
        }
        $controllerName = "\\Controllers\\".ucfirst($controllerName);
        $controller = new $controllerName;
        $actionName = empty($actionName) ? $this->defaultActionName : $actionName;
        if (!method_exists($controller, $actionName)){
            throw new \App\Exceptions\InvalidRouteException();
        }
        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 \App\Controller
{
    
    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>
    </body>
</html>

Заключение


Если решите пользоваться кодом приложения, которое описал я, не забудьте создать контроллер Error с методами error404 и error500. Класс для работы с бд, описанный мной, подходит для написания запросов руками, вместо него можно подключить ORM и вас будут настоящие модели.
Теги:
Хабы:
Всего голосов 32: ↑15 и ↓17-2
Комментарии62

Публикации

Истории

Работа

PHP программист
96 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань