Правильный паттерн MVC

в 12:37, , рубрики: mvc, php, request, route, Веб-разработка, Программирование, метки: , , , ,
Вступление

Добрый день, дорогие друзья! В сети, в частности на хабре можно найти достаточно много статей, по написанию паттерна обработки запросов MVC, но у всех есть свои недостатки. В этой статье, я попробую их все исправить.

Какие же недостатки в прошлых статьях?

Подключение контроллера прямо в обработчике, когда можно использовать удобную автозагрузку. Очень часто нет проверки на существование класса, даже если файл существует. И самый главный недостаток: отсутствие возможности задавать свои правила для обработки запросов, что мы реализуем в нашей статье.

Как мы видим будущее управление

Для начала, нужно обдумать логику работы, и как нам будет удобнее создавать правила, выводить страницу ошибок. Возможно, многие считают, что это довольно лишнее, так как можно создать внутри объекта метод, который будет генерировать ошибку 404. Это довольно глупо, так как нам придется исправлять класс, переписывать функцию. Эту задачу можно решить с помощью исключений. Давайте дальше, как же мы будем обрабатывать правила. Очень легко, проверять запрос по регулярному выражению, если он совпадает с одним из правил, то просто меняем его значение на значение из нашего массива с правилами. Так же, сделаем возможность добавлять правила из любой точки скрипта, просто будем сливать массивы.

Давайте посмотрим пример нашего будущего кода.

Route::addRules (array (
    '/feedback(?:/)?' => '/Welcome/feedback',
));

try {
    Route::factory ($_SERVER['REQUEST_URI']);
} catch (HttpException $e) {
    /*
     * Генерируем страницу ошибок
     * $e->getMessage (); // Текст нашей ошибки
     */
}
Часть 1. Автозагрузчик

Автозагрузку классов можно реализовать используя функцию __autoload, либо используя SPL (spl_autoload_register). Я придерживаюсь второго варианта, ибо это очень удобно. SPL позволяет регистрировать свои автозагрузчики на протяжении всего проекта.

spl_autoload_register ( array ('Core', 'Loader'));

Таким образом, загрузчик будет регистрироваться из класса Core, через метод Loader.

— Core

class Core {
    
    public static function Loader ($class) {
        $class = sprintf ('%s.php', ucfirst ($class));
        $class = str_replace ('_', DIRECTORY_SEPARATOR, $class);
        
        if (file_exists (Classes . $class)) {
            return require_once Classes . $class;
        }
        
        return false;
    }
    
}

Мы видим самый простой метод автозагрузки, вы можете легко его усовершенствовать, к примеру для поиска классов в различных директориях, для приложений и системных. Возможно вы заметили, что мы заменяем прочерк на слеш, это сделано для удобного подключения классов в разных подпапках.

Часть 2. Роутер

Как я уже писал ранее, в роутере у нас будет возможность указывать свои правила. Давайте напишем лёгкую функцию для добавления своих правил в наш массив.

    public static function addRules (array $rules = array ()) {
        self::$rules = array_merge (self::$rules, $rules);
        return;
    }

Тут у нас всё просто, наш добавленный массив сливается со статическим из нашего метода. Рассмотрим метод factory:

    public static function factory ($uri = null) {
        if (is_null ($uri)) {
            $uri = $_SERVER['REQUEST_URI'];
        }
        
        /*
         * Обработка правил 
         */
        
        foreach ($rules as $key => $value) {
            $regex = sprintf ('/^%s$/', $key);
            if (preg_match ($regex, $key)) {
                $uri = preg_replace ($regex, $value, $uri);
            }
        }
        
        $uri = explode ('/', $uri);
        unset ($uri[0]); // Удаляем лишний аргумент
        
        if (!empty ($uri[1])) {
            self::$controller = $uri[1];
        }
        
        if (!empty ($uri[2])) {
            self::$action = $uri[2];
        }
        
        self::__factory ($uri);
        
        return;
    }

    private static function __factory ($uri) {
        $controller = sprintf ('Controller_%s', ucfirst (self::$controller));
        $action = sprintf ('Action_%s', self::$action);
        
        if (!class_exists ($controller))
            throw new HttpException ('Невозможно найти контроллер');
        
        if (!method_exists($controller, $action))
            throw new HttpException ('Невозможно найти обработчик');
        
        // Вызываем наш метод из объекта, и заодно передаём аргументы
        call_user_func ( array ($controller, $action), self::$uri);
        
        return;
    }
Полный листинг класса Route

class Route {
    
    public static $controller = 'Welcome';
    public static $action = 'Main';
    public static $uri;
    public static $rules = array ();
    
    public static function factory ($uri = null) {
        if (is_null ($uri)) {
            $uri = $_SERVER['REQUEST_URI'];
        }
        
        /*
         * Обработка правил 
         */
        
        foreach ($rules as $key => $value) {
            $regex = sprintf ('/^%s$/', $key);
            if (preg_match ($regex, $key)) {
                $uri = preg_replace ($regex, $value, $uri);
            }
        }
        
        $uri = explode ('/', $uri);
        unset ($uri[0]); // Удаляем лишний аргумент
        
        self::$uri = $uri;
        
        if (!empty ($uri[1])) {
            self::$controller = $uri[1];
            unset (self::$uri[1]);
        }
        
        if (!empty ($uri[2])) {
            self::$action = $uri[2];
            unset (self::$uri[2]);
        }
        
        self::__factory ($uri);
        
        return;
    }

    public static function addRules (array $rules = array ()) {
        self::$rules = array_merge (self::$rules, $rules);
        return;
    }
    
    private static function __factory ($uri) {
        $controller = sprintf ('Controller_%s', ucfirst (self::$controller));
        $action = sprintf ('Action_%s', self::$action);
        
        if (!class_exists ($controller))
            throw new HttpException ('Невозможно найти контроллер');
        
        if (!method_exists($controller, $action))
            throw new HttpException ('Невозможно найти обработчик');
        
        // Вызываем наш метод из объекта, и заодно передаём аргументы
        call_user_func ( array ($controller, $action), self::$uri);
        
        return;
    }
}
Часть 3. Пример

Вот и основу нашего паттерна мы написали, осталось только попробовать запустить пример.
Пример нашего контроллера Welcome, который разместится в classes/Controller/Welcome.php

class Controller_Welcome {
    public function Action_main () {
        echo 'Главная страница';
    }
    
    public function Action_feedback () {
        echo 'Пример страницы обратной связи';
    }    
}

Файл index.php:

define ('System', __DIR__ . DIRECTORY_SEPARATOR);
define ('Classes', System . 'classes' . DIRECTORY_SEPARATOR);

require_once Classes . 'Core.php';

spl_autoload_register ( array ('Core', 'Loader'));

Route::addRules (array (
    '/feedback(?:/)?' => '/Welcome/feedback',
));

try {
    Route::factory ($_SERVER['REQUEST_URI']);
} catch (HttpException $e) {
    /*
     * Генерируем страницу ошибок
     * $e->getMessage (); // Текст нашей ошибки
     */
}

Естественно это только пример, чтобы подтолкнуть вас, на написание собственных, гораздо более удобных паттернов. Мы могли реализовать всякие удобные плюшки, использовать более грамотную реализацию, но всё это на ваших плечах, главную задачу думаю мы успешно выполнили =)

Огромное спасибо за внимание!

Автор: unrealphp

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js