И снова MVC

Привет Хаброобществу.

Однажды мне на глаза попалась статья с хабра ссылка. Попробовав ту систему, пришел к выводу, что у нее много недостатков, да и перевод оригинальной статьи присутствовал не полностью. Да, статья старючая, но в то же время хотелось написать свой небольшой фреймворк для создания простых и средних по сложности сайтов. Так как тащить с собой 2-3 мегабайта какого-либо серьезного фреймворка для создания сайта-визитки считаю кощунством (хоть и не всегда).

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

Итак, что же было усовершенствованно?

1. Избавился от использования класса Registry. Зачем он нам, если в PHP5 присутствуют такие замечательные методы как __set и __get?

2. Вместо PDO написал некий класс, названный мной как Active Records, позволяющий общаться с базой данных через прослойку. Приводить исходный код не буду, так как качество кода там не ахти и нуждается в рефакторинге. Но если будет нужно — добавлю.

3. Переписан немного класс Template, добавлена возможность делать вложенные шаблоны. Теперь он имеет такой вид:

<?php
 
class Template {
 
    private $vars = array();
 
    function __set($varname, $value) {
        $this->vars[$varname] = $value;
        return true;
    }
 
    function show($name, $data = false, $display = true) {
        $path = site_path . 'views' . DIRSEP . $name . '.php';
 
        if (!file_exists($path)) {
            trigger_error('Template `' . $name . '` does not exist.', E_USER_NOTICE);
            return false;
        }
 
        if ($data) {
            // fill with data
            if (is_array($data)) {
                foreach ($data as $key => $value) {
                    $this->__set($key, $value);
                }
            }
        }
 
        extract($this->vars);
 
        if ($display) {
            include ($path);
        } else {
            if (is_file($path)) {
                ob_start();
                include $path;
                $contents = ob_get_contents();
                ob_end_clean();
                return $contents;
            }
            return false;
        }
    }
}
 
?>
 


4. Был изменен роутер, появилась поддержка нескольких параметров в URL. Хотя подозреваю, что роутер все еще с недостатками, которые буду со временем устранять. Но пока работает :)

<?php
 
class Router {
 
    public $path;
    private $args = array();
 
    function setPath($path) {
        $path = trim($path, '/\\');
        $path .= DIRSEP;
 
        if (!is_dir($path)) {
            throw new Exception('Invalid controller path: `' . $path . '`');
        }
        $this->path = $path;
    }
 
    function delegate() {
        // analize path
        $this->getController($file, $controller, $action, $args);
        // is file available?
        if (!is_readable($file)) {
            trigger_error('File `' . $file . '` not found', E_USER_ERROR);
        }
        include ($file);
 
        // create controller
        $class = 'Controller_' . $controller;
        $controller = new $class();
 
        // action is available in object?
        if (!is_callable(array($controller, $action))) {
            trigger_error('No method `' . $action . '` was found in class', E_USER_ERROR);
        }
        // call action
        call_user_func_array(array($controller, $action), $args);
    }
 
    private function getController(&$file, &$controller, &$action, &$args) {
        $route = (empty($_GET['route'])) ? 'index' : $_GET['route'];
        // split called url
        $route = trim($route, '/\\');
        $parts = explode('/', $route);
 
        // find right controller
        $cmd_path = $this->path;
 
        // set default controller name (will be replaced if found another)
        $controller = 'index';
 
        foreach ($parts as $part) {
            $fullpath = $cmd_path . $part;
            // is dir?
            if (is_dir($fullpath)) {
                $cmd_path .= $part . DIRSEP;
                array_shift($parts);
                continue;
            }
 
            // is file?
            if (is_file($fullpath . '.php')) {
                $controller = $part;
                array_shift($parts);
                break;
            }
        }
 
        // get action method in controller
        $action = (isset($parts[0]) && !is_numeric($parts[0])) ? array_shift($parts) : 'index';
        $file = $cmd_path . $controller . '.php';
        $args = $parts;
    }
}
?>
 


5. Добавлен класс Loader, в котором будет происходить подзагрузки моделей (реализовано), библиотек (реализовано). В дальнейшем, думаю, прикрутить туда и загрузку хелперов.

<?php
 
class loader {
 
    public $instance;
    public $vars = array();
 
    function __construct($parent) {
        $this->instance = $parent;
    }
 
    function model($model_name) {
        $filename = strtolower($model_name) . '.php';
        $file = site_path . 'models' . DIRSEP . $filename;
        if (!file_exists($file)) {
            return false;
        }
        include ($file);
        $this->instance->$model_name = new $model_name(new ActiveRecords());
    }
 
    function library($name) {
        $filename = strtolower($name) . '.php';
        $file = site_path . 'libraries' . DIRSEP . $filename;
        if (!file_exists($file)) {
            return false;
        }
        include ($file);
        $this->instance->$name = new $name(new ActiveRecords());
    }
}
 
?>
 


Здесь, как мы видим, что в любую модель или библиотеку передается вышеупомянутый класс ActiveRecords (который в данный момент является singletone). Благодаря этому, в моделях и библиотеках становится доступной работа с базой данных (чего и следует ожидать от концепта MVC). Но присутствуют ограничения на названия моделей и библиотек — нельзя, например, использовать модель с именем load.

Сам же базовый класс модели, от которого наследуются все последующие в проекте, выглядит таким образом:

<?php
 
class Model {
 
    var $db;
 
    function __construct($db) {
        $this->db = $db;
    }
}
 
?>
 


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

<?php
 
class Controller_Base {
 
    public $template;
    public $load;
 
    function __construct() {
        $this->template = new Template();
        $this->load = new loader($this);
    }
 
    function __set($varname, $value) {
        $this->$varname = $value;
        return true;
    }
}
 
?>
 


Таким образом в каждом контроллере доступен класс Loader и Template. И мы можем писать в коде подобные вещи:

$this->load->model(‘links’);
$this->links->add (new);


Тоесть все методы модели становятся доступными по такому вот типу:

$this->[имя модели]->[имя метода в модели]


7. Также в стартап скрипт добавлены небольшие изменения — инициализация сессии (в последующем, планирую добавить сессии с БД), добавлена наряду с функцией __autoload также часто используемая функция redirect:

function redirect ($url)
{
   Header("Location: ".$url);
}


а также соединение с базой данных (вот тут не уверен, может имеет смысл вынести подключение к базе данных в класс Active Records? )

8. И что самое важное — наш главный файл index.php, который обрабатывает все запросы. Теперь он имеет такой вот вид:

<?php
 
require 'includes/start.php';
$router = new Router();
$router->setPath (site_path . 'controllers');
$router->delegate();
 
?>


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

Буду рад вашим комментариям, замечаниям и наставлениям.

P.S. И да — самое главное. Рабочий пример небольшого ресурса, созданного с применением ресурса — ссылка. Сервис позволяет хранить ссылки по категориям онлайн. Написано под себя, так как существующие хранилища по ряду причин не устраивают. К тому же нужно было на чем-то тестировать ядро.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 19
  • +9
    изобретаете очередной codeigniter?
    • –2
      нет. просто зачастую весь функционал монструозных фреймворков остается невостребованным. вот для таких задач планирую использовать.
      • +5
        Думаю Silex вас заинтересует.
        • –2
          Да, Silex неплох, но он довольно требователен к версии PHP. К сожалению, не каждый хостинг сейчас может предложить php 5.3
    • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Да я вроде пока не планирую расширять его очень сильно. Ставка сделана на каркас и размер. В данный момент прикрутил хелперы, ибо они как функции в проекте. А их количество и возможности уже не привязываются напрямую к фреймворку.
      • +15
        На самом деле ужасно. Не забывайте, что та статья — 2007 года. Для 2007 года там высказываются вполне современные идеи. Но сейчас на дворе уже 2011 год и ТАК писать — право, стыдно. По пунктам:

        >> Вместо PDO написал некий класс, названный мной как Active Records

        Это напоминает мне «принципиально новую ОС с нескучными обоями». Мало того, что PDO написан и оттестирован десятками разработчиков, так он еще и в разы быстрее, так как это native расширение. Итог: сэкономили на спичках.

        >> Переписан немного класс Template, добавлена возможность делать вложенные шаблоны

        Убейте меня, не нашел там вложенных шаблонов.

        >> extract($this->vars);

        Это сразу премия Дарвина в ИТ. Попробуйте ради интереса выполнить что-то типа $this->template->show('main_template', array('_get' => 'OLOLO'));

        Template::__set определена, __get — нет. Зачем передавать еще один массив в функцию show? Почему вместо этого не сделать метод assign? Зато вызывается __set при передаче массива — лишний код и лишние вызовы.

        Существование переменных и индексов автора вообще не интересует. Пробуем: $this->template->show('main_template', null);

        Полная чехарда с ошибками и исключениями. Не найден каталог? Это исключение. Не найден файл? А это уже просто ошибка. Где логика? А в Loader::library, если нет файла — просто false возвращаем…

        Везде обычный include. Таким образом $router->delegate()->delegate(); гарантированно вызывает падение.

        Передача параметров по ссылке в метод класса!!! И это в 5-м PHP!!!

        >> $class = 'Controller_'. $controller;
        $controller = new $class();

        Отлично! А если файл есть, а класса в нем нет? Где проверки на существование класса?

        Зачем в контроллере loader и template сделаны публичными?

        function __set($varname, $value) {
        $this->$varname = $value;
        return true;
        }


        Что еще за бред?
        • 0
          Спасибо за развернутый комментарий. Никто не говорит, что в будущем Active Records будет тут по-прежнему использоваться. Это временное решение, не претендующее на лавры повсеместного использования. Отчасти, например, если я захочу применить отличную от MySQL БД, то текущее решение окажется неработоспособным.

          По поводу вложенности шаблонов… В оригинале не было возможность вкладывать шаблон в шаблон. Здесь же в методе show последний параметр и отвечает вернуть ли вам содержимое обработанного темплейта или отправить его непосредственно в броузер. С такой возможность вполне можно делать вложенные view.

          С ошибками да — пока косяк. Ведь многое осталось и с оригинала. В дальнейшем поправлю.

          >>Зачем передавать еще один массив в функцию show? Почему вместо этого не сделать метод assign? Зато вызывается __set при передаче массива — лишний код и лишние вызовы.
          Тут немного не понял основной мысли. Имеется ввиду передача данных в шаблон и их последующая выгрузка в переменные?
          • +1
            >> Тут немного не понял основной мысли. Имеется ввиду передача данных в шаблон и их последующая выгрузка в переменные?

            Да. Есть __set — этот метод модифицирует внутреннее состояние класса, а метод show по логике не должен модифицировать внутреннее состояние класса, так как его назначение на основании предопределенного внутреннего состояния выдать какой-то результат. Вы же создали путаницу, добавив в метод show изменение внутреннего состояния класса. Это не смертельно, но это очень нелогично и вносит путаницу. Лучше создать метод assign, который будет заносить данные из массива.
          • 0
            Забыл про extract написать… Да, такой способ импорта переменных очень опасен и неправильный. Но я пока не определился с методом передачи переменных во view.
            • +1
              Так как include выполняется в контексте класса, то внутри шаблона будет доступен $this. Следовательно реализуйте __get и используйте $this->{имя переменной} в шаблонах. Использование просто переменных очень плохо, так как в случае неопределения какой-либо переменной, она будет взята из глобальной области видимости. Я даже не говорю про notice, если эта переменная вообще нигде неопределена
            • +2
              > function __set($varname, $value) {
              > $this->$varname = $value;
              > return true;
              > }

              Это что-то. Вы явно зашли не в ту сторону.

              Я, кстати. сам не люблю эту особенность PHP — возможность добавлять недекларированные поля, и всегда пишу в __set() и __get() что-нибудь вроде function { throw new Exception(); }. Чем строже подходить к отлову ошибок, тем меньше их окажется в итоге.

              • +1
                бросьте эту затею и лучше разберите какой-то фреймворк/cms/платформу по кирпичикам, тогда поймете, что как должно работать.

                + на фрилансе обычно заказчик себя страхует тем, что хочет делать продукт на широко известном OpenSource движке, который в будущем сможет поддержать любой программист с достаточным к-вом знаний
                • 0
                  А у нас что, сейчас на хостингах ограничение в 10 МБ на сайт? Почему нельзя взять необходимые классы из любого, удобного вам фреймворка, и использовать только их. Пусть будет тяжелее чем самописное, но зато надежнее и проверено не одним программистом.
                  • 0
                    По мелочам:
                    $path = site_path . 'views' . DIRSEP . $name . '.php';

                    site_path — это что? Где определено? Или варнинг выдаёт?
                                    $contents = ob_get_contents();
                                    ob_end_clean();

                    есть такая функция ещё с PHP4.3 ob_get_clean()
                    essentially executes both ob_get_contents() and ob_end_clean().
                    • 0
                      site_path определяется в start.php
                      • 0
                        Константа? Почему не капсом? :)
                        • 0
                          А вот даже не знаю… :)

                          define ('site_path', $site_path);

                          Это со тарой статьи кусочек остался. Наверное, не тронул его…

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.