Применяем делегирование совместно с наследованием для организации контроллеров действий

в 14:59, , рубрики: php, ооп, метки: ,

Добрый день коллеги, сегодня я расскажу сказку о своём опыте организации контроллеров в проекте на ZF 1 (так исторически сложилось).
В хороших книжках по ООП часто пишут, что наследованием нельзя увлекаться, нужно предпочитать делегирование или делать так, чтобы они работали совместно. К сожалению, не всегда можно быстро догадаться, как применить сухую теорию на практике (а когда наконец-то доходит, удивляешься «что тут сложного?»), поэтому надеюсь мой опыт кому-нибудь пригодится.

И так сначала о проблемной области:
31 Controller Action, большинство из них имеет методы indexAction(), addAction(), editAction(), searchAction().
проблема №1: большинство, но не все. В остальных наличие этих методов варьируется,
проблема №2: методы editAction() и addAction() массивные сами по себе, и почти одинаковые для всех контроллеров, отличаются инициализация формы, и сохранение модели.

Как я это решил, покажу сразу в коде.

Фрагмент базового класса контроллеров

Class Common_Controller extends Zend_Controller_Action
{
    /**
    * Класс модели связанной с контроллером.
    * @var string;
    */
    protected $modelClass = '';

    /**
    * Класс формы редактирования модели связанной с контроллером.
    * @var string;
    */
    protected $editFormClass  = '';

    /**
    * JS файл, если он нужен, для редактирования модели связанной с контроллером. 
    * @var string;
    */
    protected $jsModelFile = '';

    /**
    * Создает модель связанную с контроллером,
    * если $id передан - ищет в базе, если нет - создает новую. 
    * 
    * @param mixed $id;
    * @return Model_Record $model;
    */
    protected function modelFactory( $id = null )
    {
        $modelClass = $this->modelClass;
        if  ( $id )  {
            $model = $modelClass::find( $id );    
        }
        else {
            $model = $modelClass::create();
        }
        return $model;
    }
    
    /**
    * Создает форму редактирования связанную с контроллером.
    * 
    * @param Model_Record $model;
    * @return Zend_form $form;
    */
    protected function formFactory( Model_Record $model )
    {
        $formClass = $this->editFormClass;
        $form = new $formClass();
        $form->setDefaults( $model->toArray(1) );
        return $form;
    }

    /**
    * Заполняем модель данными из формы и сохраняем. 
    * 
    * @param Model_record $model;
    * @param Zend_Form $form;
    */
    protected function save( Model_Record $model , Zend_Form $form )
    {
        $model->fromArray($form->getValues(), false);
        $model->save();
    }

    /**
    * Хелпер редактирования сущности, 
    * этому методу делегируются вызовы editAction() в производных контроллерах.
    */
    protected function _editActionHelper()
    {
        $id = $this->_request->getParam('id');
        if ( !$id ) {
            throw new Zend_Controller_Action_Exception('страница не найдена' , 404); 
        }
        // модель поднимаем 
        $model = $this->modelFactory($id); 
        
        if ( !$model ) {
            throw new Zend_Controller_Action_Exception('страница не найдена' , 404); 
        }
        
        $this->view->model = $model;

        // заголовок страницы
        $this->view->PageTitle = $model->getFullTitle();
        
        // форму редактирования создаем
        $form  = $this->formFactory( $model );
        $this->view->form = $form;
        
        // если страница загружена get-ом - прокидываем дальше (через форму) реферер, куда вернуться после сохранения 
        if ( $this->_request->isGet() )  {
            $form->redirect->setValue( $_SERVER['HTTP_REFERER']  );
        }
               
        // проверяем, есть ли права на редактирование записи
        // ...
       
        // блокируем запись
        $model->lock();
        
        // js файл подключаем
        if ( $this->jsModelFile ) {
            $this->view->headScript()->appendFile( '/js/models/' . $this->jsModelFile );
        }

        // сохраняем данные
        if ( isset($_POST['save']) || isset($_POST['saveExit']) ) {
            // валидация
            if (  $form->isValid( $this->_request->getPost() )  )  {    
                // пробуем сохранить 
                try {
                    Model::connection()->beginTransaction();

                    $this->save( $model, $form );

                    Model::connection()->commit(); 

                    $model->releaseLock();

                    $this->view->Flash()->addSuccess( 'Success !' ); 

                    // решаем, что делать после сохранения 
                    // вернуться на эту же страницу
                    $redirect = '/' . $this->_request->getControllerName() . '/edit/id/' . $model->ID
                              . '?redirect=' . $this->_request->getParam('redirect', '/');

                    // сохранить и выйти 
                    if ( isset($_POST['saveExit']) ) {  
                        $redirect = $this->_request->getParam('redirect', '/'); 
                    }

                    $this->_redirect( $redirect );
                }
                catch (Exception $e) {     
                    Model::connection()->rollback();
                    $this->view->Flash()->addError( $e->getMessage() );
                }
            }
            else {
                $this->view->Flash()->addError("Форма заполнена с ошибками");
            }
        }
    }
}

Protected метод modelFactory() создает экземпляр модели, связанной с конкретным контроллером. Класс модели указывается в переменной $this->modelClass и в большинстве случаев, кастомизация на этом заканчивается. Если модель должна быть инициализирована по особенному, то просто переопределяем этот метод в конкретном контроллере.

Protected метод formFactory() создает форму редактирования, кастомизация аналогично modelFactory().

Protected метод save() сохраняет в переданную модель данные из переданной формы, здесь также есть место для маневра, если в конкретном контроллере сохранение сущности получается развесистым. Наличие такого метода в контроллере может вызвать сомнение, поэтому поясню, в save() допускаются только вызовы дополнительных методов модели, никаких sql запросов нет, для этого в модели мы определяем методы вроде addTag(), setChannles() и т.п. вместо одного непрозрачного метода saveFromArray().
При такой композиции, модель и форма ничего не знают друг о друге, а контроллер играет роль интегратора.

Последний protected метод в этом фрагменте это _editActionHelper(), если в производном классе нам понадобится поддержка редактирования сущности, то мы просто добавляем в него метод:

    public function editAction()
    {
         $this->_editActionHelper();
    }

Аналогично для других общих методов. Фрагмент производного контроллера для примера:

Class Video extends Common_Controller
{
    protected $modelClass = 'Video';
    protected $editFormClass  = 'Form_Video';
    
    protected function save( Model_Record $model , Zend_Form $form )
    {
           parent::save(  $model ,   $form );
           $model->setChannels( $form->channels->getValue() );
    }

     
    public function editAction()
    {
           $this->_editActionHelper();
    }
    
    public function addAction()
    { 
           $this->_addActionHelper();
    }
    
    public function indexAction()
    {
           $this->_indexActionHelper();
    }
}

P.S. просьба, если что-то ни так, пишите в комментарии, это будет полезно всем читателям.

Автор: bardex

Источник

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


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