TIP: Use Markdown or, <pre> for multi line code blocks / <code> for inline code.
These forums are read-only and for archival purposes only!
Please join our new forums at discourse.kohanaframework.org
Универсальный контроллер с использованием ORM
  • При создании контроллеров постоянно приходится сталкиваться с однотипными повторяющимися действиями по созданию и загрузке моделей. Обычно каждый контроллер использует одну основную модель, например, контроллер статей создает модель статей, контроллер товаров для магазина создает модель товаров и т.д.
    Я решил упростить себе жизнь и создал контроллер, выполняющий всю черновую работу за меня.

    abstract class Controller_ORM extends Controller_Template
    {
       /**
        * Name of ORM model. 
        * If not specified, based on controller name.
        * 
        * @var mixed(string|ORM)
        */
       protected $model;
    
       /**
        * Request param with model id (parameter for find)
        * For switch off auto model loading set this property empty
        * 
        * @var string
        */
       protected $param_model_id = 'id';
    
       /**
        * A list of actions that don't need create a model
        *
        * @var array
        */
       protected $deny_create_model_actions = array();
    
       /**
        * A list of actions that don't need auto loading(find by id) a model
        * 
        * @var array
        */
       protected $deny_load_model_actions = array();
    
       /**
        * Automatically executed before the controller action. Can be used to set
        * class properties, do authorization checks, and execute other custom code.
        *
        * @return  void
        */
       public function before()
       {
          parent::before();
          
          if ( ! in_array($this->request->action(), $this->deny_create_model_actions))
          {
             if ( ! $this->model)
             {
                /*
                 * Auto detect model name
                 * Example: Controller_Blog_Category = Blog_Category(Model_Blog_Category)
                 */
                $this->model = strtolower(substr(get_class($this), 11));
             }
             
             // Set id parameter for find
             $id = $this->request->param($this->param_model_id);
             
             // Create model
             $this->model = ORM::factory($this->model, $id);
             
             // Checking loading model
             if ( ! in_array($this->request->action(), $this->deny_load_model_actions) AND ! $this->model->loaded())
             {
                throw HTTP_Exception::factory(404, 'Model :model not loaded', 
                   array(':model' => $this->model->object_name()));
             }
          }
       }
    
       /**
        * Automatically executed after the controller action. Can be used to apply
        * transformation to the response, add extra output, and execute
        * other custom code.
        * 
        * @return  void
        */
       public function after()
       {
          // Delete model object
          unset($this->model);
          
          parent::after();
       }
    
    } // End Controller_ORM
    

    Упрощенный пример использования:

    class Controller_Category extends Controller_ORM
    {
       // Не используем автозагрузку данных для action_list
       protected $deny_load_model_actions = array('list');
    
       // Выводит список категорий
       public function action_list()
       {
          $this->template->categories = $this->model->find_all();
       }
    
       // Выводит информацию конкретной категории
       public function action_item()
       {
          $this->template->category = $this->model;
       }
    }
    

    На самом деле большинство контроллеров имеет один и тот же функционал, например frontend контроллеры обычно похожи на показанный мной в примере - отображают список элементов или конкретный элемент, backend - производит CRUD(отображение\создание\изменение\удаление) действия на элементами, поэтому можно пойти дальше и создать универсальные контроллеры, для сложных задач расширять их, а совсем нестандартные уже создавать отдельно.

    Как будет время напишу такие вот универсальные контроллеры)

  • Кстати, идея интересная. Буду следить за темой.

  • Слегка улучшил код. Теперь можно производить поиск сразу по нескольким параметрам и не требуется параметр $deny_load_model_actions, для удобства код автозагрузки модели вынесен в отдельный метод.

    abstract class Controller_ORM extends Controller_Template {
    
        /**
         * Model name. If not specified, based on controller name.
         * 
         * @var string|ORM
         */
        public $model;
    
        /**
        * Request params, uses for model find rows.
        * Sets as array of param => value
        * [!!] Model will not be created if the list is empty or the first param is NULL.
        * 
        * @var array
        */
        public $model_params = array('id' => NULL);
    
        /**
        * A list of actions that don't need create a model
        *
        * @var array
        */
        public $actions_without_model = array();
    
        /**
        * Model autoloader
        *
        * @return void
        */
        protected function _autoload_model()
        {
            // Checking list actions without model 
            if (in_array($this->request->action(), $this->actions_without_model))
            {
                return NULL;
            }
            
            if (empty($this->model))
            {
                /*
                 * Auto detect model name
                 * Example: Controller_Blog_Category => Blog_Category => Model_Blog_Category
                 */
                $this->model = strtolower(substr(get_class($this), 11));
            }
            
            // Sets model parameters for search
            if ($this->request->param(current($this->model_params)))
            {
                foreach ($this->model_params as $key => $value)
                {
                    $this->model_params[$key] = $this->request->param($key, $value);
                }
            }
            else
            {
                $this->model_params = NULL;
            }
            
            // Create model
            $this->model = ORM::factory($this->model, $this->model_params);
            
            // Checking loading model
            if ($this->model_params !== NULL AND ! $this->model->loaded())
            {
                throw HTTP_Exception::factory(404, 'Model :name not loaded', 
                    array(':name' => $this->model->object_name()));
            }
        }
    
        /**
        * Called before executing action
        *
        * @return void
        */
        public function before()
        {
            parent::before();
            
            // Create model
            $this->_autoload_model();
        }
    
    } // End Controller_ORM
    

    Упрощенный пример использования:

    Route::set('default', '(<controller>(/<action>(/<slug>)))', array(
            'slug' => '[\w\-]+',
        ))
        ->defaults(array(
            'directory'  => 'Frontend',
            'controller' => 'Element',
            'action'     => 'index',
            'slug'        => NULL,
        ));
    
    class Controller_Frontend_Element extends Controller_ORM
    {
        // Параметры запроса, передаваемые в модель
        // slug - ЧПУ id, active - выбираем только активные элементы
        public $model_params = array('slug' => NULL, 'active' => 1);
        
        // Не используем автозагрузку модели для action_config
        public $actions_without_model = array('config');
        
        // Выводит конкретный элемент
        public function action_item()
        {
            $this->template->item = $this->model;
        }
        
        // Выводит список элементов
        public function action_index()
        {
            $this->template->items = $this->model->find_all();
        }
        
        // Конфигурирование страницы
        public function action_config()
        {
            $this->template->config = Kohana::$config->load('item');
        }
    }
    

    http://site.ru/element/ Выводит список элементов
    http://site.ru/element/item/zelenie_tapki Выводит конкретный элемент
    http://site.ru/element/config Конфигурирование страницы

  • Отложим пока CRUD контроллеры, с ними более-менее все ясно и обсудим вот что:

    HMVC Матрёшка

    Наконец полностью сформировалась в голове идея организации базового контроллера моей CMS, чем и спешу поделиться :)

    Каждая страница представляет собой слой(шаблон) на котором размещены виджеты и сниппеты:

    <div class="g">
        <div class="g-row">
            <header>
                <?=CMS::snippet('header/logo')?>
                <?=CMS::widget('menu/top')?>
                <?=CMS::widget('account')?>
            </header>
        </div>
        <div class="g-row">
            <aside class="g-4"><?=CMS::widget('menu/left')?></aside>
            <article class="g-8"><?=CMS::snippet('main_content')?></article>
        </div>
        <div class="g-row">
            <footer>
                <?=CMS::widget('menu/footer')?>
                <?=CMS::snippet('footer/copyright')?>
            </footer>
        </div>
    </div>
    


    CMS::snippet - сниппет. Представляет собой простой View, напрямую (через View::factory) они не вызываются потому, что данный метод будет реализовывать ряд дополнительных действий, например подгрузку css\js контента сниппета.

    CMS::snippet('main_content') - вывод основного контента также реализован сниппетом.

    CMS::widget - виджет. Реализует запрос(request) к контроллеру виджета и выводит полученный контент, опять же используем метод-обертку т.к. он кроме этого будет иметь дополнительный функционал.

    Такой поход позволит легко изменять структуру разделов-страниц, а также добавлять новые. В идеале планирую сделать визуальный редактор дизайна в админке (аля http://jqueryui.com/sortable/#portlets).

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

    • Создание слоёв. Например если дизайн главной страницы, отличается отличается от оформления внутренних, то для этого потребуется создать 2 шаблона.
    • Добавление страниц(разделов). Каждая страница имеет 4 основных параметра:
      • slug - ЧПУ идентификатор (например, `blog` или `about_us`)
      • layout - шаблон, используемый в качестве основы
      • content - html текст страницы
      • controller - контроллер страницы, формирующий основной контент. Имеет более широкий, по сравнению с виджетами, функционал. Например, методы для формирования meta данных страницы, поиска по разделу и создания sitemap(для frontend контроллеров). Данный параметр является необязательным.
      CMS::snippet('main_content') представляет собой текст content + результат запроса к controller'y.

    Страницы будут иметь URL примерно следующего вида: `(/<slug>(/<page_action>(/<page_slug>)))`

    Например, http://site.ru/myblog/post/html5-lesson_1/ или http://site.ru/about_us/

    Принцип работы:

    • Вызывается Controller_Page - action_index и выполняется поиск данных страницы по параметру `slug`, используя Model_Page.
    • Если у страницы задан параметр controller, то будет выполнен запрос(request).
      URL запроса формируется исходя из оставшихся параметров первичного запроса(slug удаляется) и значения controller. Полученный результат добляется к данных страницы.
    • Выполняется рендеринг слоя.
    • Формируется и возвращается контент страницы

    Например, при вызове `http://site.ru/myblog/post/html5-lesson_1/` происходит поиск страницы `myblog`, для которой задан controller blog(`content/page/blog`) - `content/<directory>/<controller>`, в результате получаем запрос `http://site.ru/content/page/blog/post/html5-lesson_1/`, вызывающий Controller_Page_Blog - action_post, в нем происходит поиск записи с идентификатором `html5-lesson_1`.

    Следующим шагом надо будет заменить отдельные контент controller'ы на контент-модули, которые будут группировать имеющийся функционал (например, модуль блога или форума).

    А также сделать редактирование content по аналогии с наполнением слоёв сниппетами и виджетами, но это вообще far, far away... :(

    Схематический код добавлю попозже, но вроде и так все довольно прозрачно вышло =)

  • По-моему единственный выигрыш со всей твоей затеи с автоматическим созданием моделей ORM - передача параметров, тоесть если брать само создание модели, то сокращается 4 строчки. Проигрыш - создание дополнительных условий, которые нужно помнить при создании очередного контролера. Ну а в результате еще вопрос "сокращается ли код".

  • Дело в первую очередь во времени, а не в количестве строк. Помнить ничего не нужно, IDE подскажет и покажет. Для 90% контроллеров эти параметры не потребуется изменять, если придерживаться единой структуры. Или потребуется изменить, но один раз, при переопределении Controller_ORM. Весь смысл в минимизации и автоматизации однотипных действий, если продолжить тему как я писал и создать типовые backend\frontend контроллеры, то в 50% случаев код будет выглядеть так:

    class Controller_Page extends Controller_Frontend {}
  • @WinterSilence Такие универсальные системы хороши до того момента пока не появился один единственный контроллер которому требуется другая логика. И начинаются костыли в своем собственном коде. То есть у все эти контроллеры должны оперировать одной сущностью выбранной по одним и тем же условиям.

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

  • @artad зачем костыли? если логика какого-то контроллера кардинально должна отличаться, то просто наследуем его от базового и пишем все вручную. kohana тоже строится на ряде соглашений\допущений (как и большинство других проектов), если их отбросить, то код потребует кардинальных доработок. Это дело опыта, если его не хватает, то конечно проще писать как получится.

  • HMVC Матрёшка - пример реализации

    Маршруты:

    Route::set('page', '(<slug>(/<subrequest>))', array(
            'slug'       => '[\w\-]+',
            'subrequest' => '\.+',
        ))
        ->defaults(array(
            'directory'  => NULL,
            'controller' => 'Page',
            'action'     => 'item',
            'slug'       => Kohana::$config->load('page.default.slug'),
            'subrequest' => NULL,
        ))
        ->filter(
            function(Route $route, $params, Request $request)
            {
                return $request->is_initial() ? $params : FALSE;
            }
        );
    
    Route::set('default', '(<directory>/<controller>(/<action>(/<id>)))')
        ->defaults(array(
            'directory'  => 'Page',
            'controller' => 'Home',
            'action'     => 'index',
        ));
    

    SQL дамп таблицы со страницами

    CREATE TABLE IF NOT EXISTS `pages` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `title` varchar(255) NOT NULL,
      `slug` varchar(255) NOT NULL,
      `content` text NOT NULL,
      `layout` varchar(255) NOT NULL,
      `controller` varchar(255) NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `slug` (`slug`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
    

    Контроллер страниц (основной)

    class Controller_Page extends Controller_Template
    {
        public $template = 'wrapper';
    
        public function action_item()
        {
            $slug = $this->request->param('slug');
            
            $page = ORM::factory('Page')->where('slug', '=', $slug)->find();
            if ( ! $page->loaded())
            {
                throw HTTP_Exception::factory(404, 'Page :name not found', array(':name' => $slug));
            }
            View::set_global('PAGE', $page);
            
            View::set_global('PROTOCOL', $this->request->protocol());
            
            $this->template->layout = View::factory('layout/'.$page->layout)->render();
        }
    
    } // End Controller_Page
    

    Вид wrapper (обёртка для контента)

    <!DOCTYPE html><?php defined('SYSPATH') OR die('No direct script access.') ?>
    <html lang="<?php echo I18n::$lang ?>">
        <head>
            <base href="<?php echo URL::base($PROTOCOL) ?>">
            <title><?php echo $PAGE->title ?></title>
            <!--[if lt IE 9]><script src="https://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
        </head>
        <body>
            <?php echo $layout ?>
        </body>
    </html>
    

    Вид layout/home (слой для главной)

    <header><?php echo CMS::snippet('home/header') ?></header>
    <main>
        <aside id="left_menu"><?php echo CMS::widget('menu/left') ?></aside>
        <article><?php echo CMS::snippet('content') ?></article>
    </main>
    <footer><?php echo CMS::snippet('footer') ?></footer>
    

    Вид layout/default (слой для всех остальных страниц)

    <header><?php echo CMS::snippet('default/header') ?></header>
    <main>
        <aside id="menu-left"><?php echo CMS::widget('menu/left') ?></aside>
        <article><?php echo CMS::snippet('content') ?></article>
        <aside id="menu-right"><?php echo CMS::widget('menu/right') ?></aside>
    </main>
    <footer><?php echo CMS::snippet('footer') ?></footer>
    

    Вид snippet/content (сниппет основного контента)

    <?php if ($PAGE->content): ?>
    <div class="header">
        <?php echo $PAGE->content ?>
    </div>
    <?php endif ?>
    <?php if ($PAGE->controller): ?>
    <div class="footer">
        <?php echo CMS::request('page/'.$PAGE->controller.'/'.Request::initial()->param('subrequest')) ?>
    </div>
    <?php endif ?>
    

    Вспомогательный класс CMS

    abstract class Kohana_CMS
    {
        /**
         * Create identification tag(key) uses the methods attributes.
         * Basically used for get or set cache.
         * 
         * @static
         * @return  string
         */
        public static function generate_tag()
        {
            return I18n::$lang.'_'.sha1(serialize(func_get_args()));
        }
        
        /**
         * Request bulder
         *
         */
        public static function request($name, array $data = array(), 
            $method = NULL, $allow_external = FALSE, Cache $cache = NULL)
        {
            $options = array();
            if ( ! is_null($cache) AND Kohana::$caching)
            {
                $options['cache'] = HTTP_Cache::factory($cache); 
            }
            
            $request = Request::factory($name, $options, $allow_external);
            
            if (is_null($method))
            {
                $method = Request::GET;
            }
            $request->method($method);
            
            switch ($method)
            {
                case Request::GET:
                    $request->query($data);
                    break;
                case Request::POST:
                    $request->post($data);
                    break;
            }
            
            return $request->execute();
        }
        
        public static function snippet($name, array $data = array(), $caching = TRUE)
        {
            $name = 'snippet'.DIRECTORY_SEPARATOR.str_replace(array(' ', '_', '\\'), DIRECTORY_SEPARATOR, $name);
            
            if ($caching AND Kohana::$caching)
            {
                $cache = Cache::instance('snippet');
                $tag = CMS::generate_tag($name, $data);
                if ($snippet = $cache->get($tag))
                {
                    return $snippet;
                }
            }
            
            $snippet = View::factory($name, $data)->render();
            
            if (isset($cache))
            {
                $cache->set($tag, $snippet);
            }
            
            return $snippet;
        }
    
        public static function widget($name, array $data = array(), $caching = TRUE)
        {
            if ($caching AND Kohana::$caching)
            {
                return CMS::request('widget/'.$name, $data, NULL, FALSE, Cache::instance('widget'));
            }
            else
            {
                return CMS::request('widget/'.$name, $data, NULL, FALSE);
            }
        }
    

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

In this Discussion