Zend Framework: идеальное веб-приложение (ч. 2 из 2)

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

В прошлой части мы:

  1. Создали структуру БД
  2. С помощью ZF Tool создали файловую структуру приложения (основной костяк, добавили модуль с рядом действий, моделей и форм, экспортировали таблицы из БД в классы приложения)
  3. Прошлись по основным паттернам

Связываем шлюзы

В этой части мы начнем со связывания между собой созданных классов-«шлюзов». Другими словами, мы перенесем существующие логические связи между сущностями из БД в соответствующие классы в приложении.

Начнем со связи «многие ко многим» между таблицами авторов (authors) и их работ (papers). Для этого у нас уже были созданы следующие файлы с классами в папке /application/modules/papers/models/DbTable:

Соответственно классы в файлах необходимо привести к следующему виду

<?php
// /application/modules/papers/models/DbTable/Papers.php

class Papers_Model_DbTable_Papers extends Zend_Db_Table_Abstract
{
    protected $_name = 'papers';
    protected $_dependentTables = array('Papers_Model_DbTable_Papers2authors');
}
<?php
// /application/modules/papers/models/DbTable/Authors.php

class Papers_Model_DbTable_Authors extends Zend_Db_Table_Abstract
{
    protected $_name = 'authors';
    protected $_dependentTables = array('Papers_Model_DbTable_Papers2authors');
    protected $_referenceMap    = array(
	        'Organization' => array(
	            'columns'           => 'id',
	            'refTableClass'     => 'Papers_Model_DbTable_Organizations',
	            'refColumns'        => 'id'
	        ));
}
<?php
// /application/modules/papers/models/DbTable/Papers2authors.php

class Papers_Model_DbTable_Papers2authors extends Zend_Db_Table_Abstract
{

    protected $_name = 'papers2authors';

    protected $_referenceMap    = array(
        'Paper' => array(
            'columns'           => array('paper_id'),
            'refTableClass'     => 'Papers_Model_DbTable_Papers',
            'refColumns'        => array('id')
        ),
        'Author' => array(
            'columns'           => array('author_id'),
            'refTableClass'     => 'Papers_Model_DbTable_Authors',
            'refColumns'        => array('id')
        )
    );

}

Что мы натворили? Уверен, что все в курсе, что в случае связи «многие-ко-многим» (т.е. в нашем случае, когда у одного автора может быть много работ, и у одной работы может быть много авторов (сооавторство) используется дополнительная таблица, в которой находятся id записей из таблиц с авторами и работами соответственно. Для отражения этой связи мы:

  1. В классе Papers_Model_DbTable_Papersдобавили свойство
    protected $_dependentTables = array('Papers_Model_DbTable_Papers2authors');
  2. В классе Papers_Model_DbTable_Authors
    protected $_dependentTables = array('Papers_Model_DbTable_Papers2authors');
  3. В классе Papers_Model_DbTable_Papers2authors
    protected $_referenceMap    = array(
            'Paper' => array(
                'columns'           => array('paper_id'),
                'refTableClass'     => 'Papers_Model_DbTable_Papers',
                'refColumns'        => array('id')
            ),
            'Author' => array(
                'columns'           => array('author_id'),
                'refTableClass'     => 'Papers_Model_DbTable_Authors',
                'refColumns'        => array('id')
            )
        );

По тексту должно быть понятно, что-за-что отвечает в добавленных свойствах, но если есть желание разобраться до конца – подробнее см. в мануале. Единственное, что стоит отметить, что в $_referenceMap Paper и Author явлются т.н. «правилами» (rules), которые и используются в магических методах извлечения инфы из БД.

Теперь добавим связь «один-ко-многим» в непростые отношения авторов и организаций (т.е. один автор может состоять только в одной организации, но одна и та же организация может иметь множество авторов).

<?php

// /application/modules/papers/models/DbTable/Organizations.php

class Papers_Model_DbTable_Organizations extends Zend_Db_Table_Abstract
{
    protected $_name = 'organizations';
	protected $_dependentTables = array('Papers_Model_DbTable_Authors');

}

А класс Papers_Model_DbTable_Authors и так уже имеет законченный вид (см. выше). В нём за это связывание отвечает свойство

    protected $_referenceMap    = array(
	        'Organization' => array(
	            'columns'           => 'id',
	            'refTableClass'     => 'Papers_Model_DbTable_Organizations',
	            'refColumns'        => 'id'
	        ));

Всё, со шлюзами покончено. Переходим к моделям.

Совсем не топ-модели

Классы-модели отражают законченную сущностью объекта, поэтому там в основном функции типа getName, setName и т.д., и другие сопроводительные функции для действий получения и записи свойств объекта.

<?php
// /application/modules/papers/models/Author.php

class Papers_Model_Author
{
	protected $id, $f_name, $m_name, $l_name, $organization_id;

	/**
	 * Routine part. Suggest move it to external class and extend it
	 * =============================================================
	 */

	public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }

    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property ' . $name);
        }
        $this->$method($value);
    }

    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property ' . $name);
        }
        return $this->$method();
    }

    /**
     * Set all properties
     *
     * @param $options
     */
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    /**
     * Get all properties with values
     *
     * @return $dataArray  key-value array 'property' => 'value'
     */
    public function getOptions() {
    	$methods = get_class_methods($this);
    	$dataArray = array();
    	foreach ($methods as $m) {
    		list($property) = sscanf($m, 'get%s');
    		$property = strtolower($property);
    		if(!empty($property) && property_exists($this, $property))
    			$dataArray[$property] = $this->$m();
    	}

    	return $dataArray;
    }

    /**
     * Routine end.
     * =====================================================================
     */

	/**
	 *
	 * @param boolean $short
	 * @return if $short = true returns Last_Name F.M., else Last_Name First_Name Middle_Name
	 */
	public function getFIO($short = true) {
		if ($short) {
			return $this->l_name . ' ' .
	    				mb_substr($this->f_name, 0, 1, 'utf-8') . '.' .
	    				(empty($this->m_name) ?
	    			 	'' : (mb_substr($this->m_name, 0, 1, 'utf-8') . '.'));
		} else {
	    	return $this->l_name . ' ' .
	    		   $this->f_name . ' ' .
	    		   $this->m_name;
		}
	}

	public function getId() {
		return $this->id;
	}

	public function setId($id) {
		$this->id = $id;
		return $this;
	}

	public function getF_name() {
		return $this->f_name;
	}

	public function setF_name($f_name) {
		$this->f_name = $f_name;
		return $this;
	}

	public function getM_name() {
		return $this->m_name;
	}

	public function setM_name($m_name) {
		$this->m_name = $m_name;
		return $this;
	}

	public function getL_name() {
		return $this->l_name;
	}

	public function setL_name($l_name) {
		$this->l_name = $l_name;
		return $this;
	}

	public function getOrganization_id() {
		return $this->organization_id;
	}

	public function setOrganization_id($organization_id) {
		$this->organization_id = $organization_id;
		return $this;
	}
}
<?php
// /application/modules/papers/models/Paper.php

class Papers_Model_Paper
{
  	protected $id, $file, $title, $title_en, $authors, $source, $publisher, $date;

  	/**
	 * Routine part. Suggest move it to external class and extend it
	 * =============================================================
	 */

	public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }

    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property ' . $name);
        }
        $this->$method($value);
    }

    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid model property ' . $name);
        }
        return $this->$method();
    }

    /**
     * Set all properties
     *
     * @param $options
     */
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    /**
     * Get all properties with values
     *
     * @return $dataArray  key-value array 'property' => 'value'
     */
    public function getOptions() {
    	$methods = get_class_methods($this);
    	$dataArray = array();
    	foreach ($methods as $m) {
    		list($property) = sscanf($m, 'get%s');
    		$property = strtolower($property);
    		if(!empty($property) && property_exists($this, $property))
    			$dataArray[$property] = $this->$m();
    	}

    	return $dataArray;
    }

    /**
     * Routine end.
     * =====================================================================
     */

    public function getId()
    {
        return $this->id;
    }

    public function setId($id)
    {
        $this->id = (int) $id;
        return $this;
    }

    public function getFile()
    {
        return $this->file;
    }

    public function setFile($file)
    {
    	//Zend_Debug::dump($file);
    	if ($file instanceof Zend_Form_Element_File && $file->isReceived()) {
       		$extension = substr(strrchr($file->getFileName(null, false), '.'), 1);
       		//$newName = md5(uniqid(rand(), true)) . '.' . $extension;
       		$newName = $this->title_en . '.' . $extension;
       		$uploadPath = realpath(APPLICATION_PATH . '/../public/media/papers') . '/' . $newName;
       		$filterFileRename = new Zend_Filter_File_Rename(
	 					array('target' => $uploadPath, 'overwrite' => true));
			$filterFileRename->filter($file->getFileName());

			$this->file = '/media/papers/' . $newName;
			return $this;
       }
       $this->file = $file;
       return $this;
    }

	public function getTitle() {
		return $this->title;
	}

	public function setTitle($title) {
		$this->title = $title;
		return $this;
	}

	public function getTitle_en() {
		return $this->title_en;
	}

	public function setTitle_en($title_en) {
		if ($title_en instanceof Zend_Form_Element_Text) {
			$title_enMaxLength = 42;
	    	$title_en = Np_Text_FromRussian2Translit::convert($title_en->getValue());
	        if (strlen($title_en) < $title_enMaxLength)
	        	$title_enMaxLength = strlen($title_en);
	        //cut title with full last word within next 42 letters
	        $title_en = (strtolower(substr($title_en, 0, strpos($title_en, '_', $title_enMaxLength))));
		}
		$this->title_en = $title_en;
		return $this;
	}

	public function getAuthors($getArray = true) {
		if (!$getArray) {
			$array = $this->authors;
			$authors = '';
			foreach ($array as $author) {
				$authors .= $author->getFIO(false);
	    		if (true == next($array)) $authors .= ', ';
			}
			return $authors;
		}
		return $this->authors;
	}

	public function setAuthors($authors) {
		if ($authors instanceof Zend_Db_Table_Rowset_Abstract) {
			$this->authors  = array();
			foreach ($authors as $a) {
				$authorModel = new Papers_Model_Author();
				$authorModel->setOptions($a->toArray());
				$this->authors[] = $authorModel;
			}
			return $this;
		}

		// String with authors comes from form
		// All new authors must be added to DB and linked with this paper
		// Presented authors must be linked with this paper
		if ($authors instanceof Zend_Form_Element_Text && $authors->getValue() != null) {
			preg_match_all("/([^\s\.,]+)\s*([^\s\.]*)[\s\.]*([^\s\.,]*),*/", $authors->getValue(), $authorsArray, PREG_SET_ORDER);
			//Zend_Debug::dump($authorsArray);
			$authorMapper = new Papers_Model_AuthorMapper();
			$paperMapper = new Papers_Model_PaperMapper();
			//1. Delete all authors connections with this paper
			$p2aDbTable = new Papers_Model_DbTable_Papers2authors();
			$p2aDbTable->delete($p2aDbTable->getAdapter()->quoteInto('paper_id = ?', $this->getId()));
			foreach ($authorsArray as $key => $value) {
				$authorModel = new Papers_Model_Author(array(
					'l_name'	=>	$authorsArray[$key][1],
					'f_name'	=>	$authorsArray[$key][2],
					'm_name'	=>	$authorsArray[$key][3]
				));
				//2. Check if this author already in DB
				$authorMapper->findByFIO($authorModel);
				if(!$authorModel->getId()) {
					//if it's new author, add him to DB
					$authorMapper->save($authorModel);
				}
				//3. Connect author with paper
				$p2aDbTable->insert(array(
					'author_id'	=>	$authorModel->getId(),
					'paper_id'	=>	$this->getId()
				));
			}
			return $this;
		}
		$this->authors = $authors;
		return $this;
	}

	public function getSource() {
		return $this->source;
	}

	public function setSource($source) {
		$this->source = $source;
		return $this;
	}

	public function getPublisher() {
		return $this->publisher;
	}

	public function setPublisher($publisher) {
		$this->publisher = $publisher;
		return $this;
	}

	public function getDate() {
		return $this->date;
	}

	public function setDate($date) {
		$this->date = $date;
		return $this;
	}
}

Классы взяты из готового приложения, поэтому есть всякие мудреные функции типа загрузки файлов, чтения списка авторов через запятую и т.д. Решил оставить, мало ли кому пригодятся.)

Мапперы

Мапперы – это такие классы, которые взаимодействуя c моделями (представление сущностей в классах) и со шлюзами (доступ к операциям с БД с определенными таблицами), осуществляют операции сохранения, обновления и удаления данных.
Все мапперы являются потомками класса Np_Model_Mapper, который по сути просто содержит две рутинные функции для получения доступа к шлюзам:

<?php

class Np_Model_Mapper {

	protected $_dbTable;

    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }

    public function getDbTable()
    {
        return $this->_dbTable;
    }

}

Просто в уме добавляйте этот код ко описанным ниже мапперами.
Мы создадим два маппера: для работы с работами (papers) и авторами, соответственно.

<?php
// /application/modules/papers/models/PaperMapper.php

class Papers_Model_AuthorMapper extends Np_Model_Mapper {

	public function __construct() {
		$this->setDbTable('Papers_Model_DbTable_Authors');
	}

    public function save(Papers_Model_Author $author)
    {
    	$data = $author->getOptions();
        if (null === ($id = $author->getId())) {
            unset($data['id']);
            $author->setId($this->getDbTable()->insert($data));
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }

    public function find($id, Papers_Model_Author $author)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $author->setOptions($row->toArray());
    }

    /**
     * @return author's id, if he is found, else return null
     * @param $author Papers_Model_Author
     */
    public function findByFIO(Papers_Model_Author $author) {
    	$where = $this->getDbTable()->select()
    				  ->where('f_name = ?', $author->getF_name())
    				  ->where('m_name = ?', $author->getM_name())
    				  ->where('l_name = ?', $author->getL_name());
    	$result = $this->getDbTable()->fetchAll($where);
    	if (0 == count($result)) {
    		$author->setId(null);
    	} else {
    		$row = $result->current();
    		$author->setId($row->id);
    	}
    }

	public function delete(Papers_Model_Author $author){
		$id = $author->getId();
		if (empty($id)) {
			throw new Exception('Invalid model');
			return false;
		}
		$where = $this->getDbTable()
					  ->getAdapter()
					  ->quoteInto('id = ?', $id);
		$this->getDbTable()->delete($where);
	}

    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Papers_Model_Author();
            $entry->setOptions($entry->toArray());
            $entries[] = $entry;
        }
        return $entries;
    }

}

 

<?php
// /application/modules/papers/models/PaperMapper.php

class Papers_Model_PaperMapper extends Np_Model_Mapper {

	public function __construct() {
		$this->setDbTable('Papers_Model_DbTable_Papers');
	}

    public function save(Papers_Model_Paper $paper)
    {
    	$data = array(
            'file'		=> $paper->getFile(),
            'title'		=> $paper->getTitle(),
        	'title_en'	=> $paper->getTitle_en(),
            'source'	=> $paper->getSource(),
        	'publisher'	=> $paper->getPublisher(),
        	'date'		=> $paper->getDate(),
        );
        if (null === ($id = $paper->getId())) {
            unset($data['id']);
            $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }

    public function find($id, Papers_Model_Paper $paper)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $paper->setId($row->id)
              ->setFile($row->file)
              ->setTitle($row->title)
              ->setTitle_en($row->title_en)
              ->setSource($row->source)
              ->setPublisher($row->publisher)
              ->setDate($row->date)
              ->setAuthors($row->findManyToManyRowset('Papers_Model_DbTable_Authors',
	              									  'Papers_Model_DbTable_Papers2authors'));
    }

	public function delete(Papers_Model_Paper $paper){
		$id = $paper->getId();
		if (empty($id)) {
			throw new Exception('Invalid model');
			return false;
		}
		$where = $this->getDbTable()
					  ->getAdapter()
					  ->quoteInto('id = ?', $id);
		$this->getDbTable()->delete($where);
	}

    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Papers_Model_Paper();
            $entry->setId($row->id)
	              ->setFile($row->file)
	              ->setTitle($row->title)
	              ->setTitle_en($row->title_en)
	              ->setSource($row->source)
	              ->setPublisher($row->publisher)
	              ->setDate($row->date)
	              ->setAuthors($row->findManyToManyRowset('Papers_Model_DbTable_Authors',
	              										  'Papers_Model_DbTable_Papers2authors'));
            $entries[] = $entry;
        }
        return $entries;
    }

}

Самым интересным тут являются строки типа

->setAuthors($row->findManyToManyRowset('Papers_Model_DbTable_Authors',
	              										  'Papers_Model_DbTable_Papers2authors'));

Здесь $row – переменная класса Zend_Db_Table_Row_Abstract, полученная от итератора («проходителя» циклом по об элементам объекта) объекта класса Zend_Db_Table_Rowset_Abstract – набора строк, полученных от метода fetchAll() объекта класса Papers_Model_DbTable_Papers (того самого шлюза).
В силу того, что мы установили связи между таблицами в наших шлюзах, мы можем напрямую получить всех авторов, связанных с текущей работой, информация о которой хранится в переменной $row. Вообще, это ключевой момент в обоих статьях, поэтому повторим еще разок.)
Итак, нам необходимо получить массив с моделями сущности papers, за это отвечает функция Papers_Model_PaperMapper->fetchAll() мы:

  1. Мы получаем все записи в таблице papers, вызывая метод класса-шлюза этой таблицы Papers_Model_DbTable_Papers->fetchAll(). Метод возвращает объект класса Zend_Db_Table_Rowset_Abstract, который по сути является массивом объектов Zend_Db_Table_Row_Abstract, каждый из которых хранит информацию о каждой строке в таблице
  2. Поскольку класс Zend_Db_Table_Rowset_Abstract реализует интерфейс объекта, по которому можно пройтись циклом (т.н. итератор), то с помощью оператора foreach мы проходимся по всем объектам класса Zend_Db_Table_Row_Abstract, содержащихся в этом объекте
  3. Мы создаем новый объект класса-модели Papers_Model_Paper и наполняем его данными, полученными из БД. Т.к. Zend_Db_Table_Row_Abstract реализовал «магические» функции типа __get и __set, то мы можем напрямую обращаться к элементами полученной строки по ключам
  4. Самый интересный момент: Zend_Db_Table_Row_Abstract содержит инфу о текущей работе и связях между таблицами (засчет шлюзов). Поэтому мы можем получить всех авторов, связанных с этой работой, вызвав метод Zend_Db_Table_Row_Abstract->findManyToManyRowset

Ухх… это было непросто.) Ну теперь остается самое приятное – воспользоваться нашей превосходной частью работы с БД.

Контроллер

У нас будет один контроллер, который будет осуществлять действия вывод, добавления, обновления и удаления информации, касающейся работ.

<?php
// /application/modules/papers/controllers/IndexController.php

class Papers_AdminController extends Zend_Controller_Action
{

    public function init()
    {

    }

    public function indexAction()
    {
        $this->view->headTitle('Список статей');
        $paper = new Papers_Model_PaperMapper();
        $this->view->entries = $paper->fetchAll();
    }

    public function addPaperAction()
    {
    	$this->view->headTitle('Добавить статью');

    	$request = $this->getRequest();
        $form    = new Papers_Form_Paper_Add();

        if ($request->isPost()) {
        	if ($form->isValid($request->getPost())) {
            	$paper = new Papers_Model_Paper($form->getValues());
            	$paper->setTitle_en($form->title)
            		  ->setFile($form->file)
            		  ->setAuthors($form->authors);
            	$mapper	= new Papers_Model_PaperMapper();
                $mapper->save($paper);
                return $this->_helper->redirector('index', 'index');
            }
        }

        $this->view->form = $form;
    }

    public function editPaperAction()
    {
        $this->view->headTitle('Редактировать статью');

    	$request = $this->getRequest();
        $model	 = new Papers_Model_Paper();
        $mapper  = new Papers_Model_PaperMapper();
        $mapper->find($request->getParam('id'), $model);
        $form    = new Papers_Form_Paper_Edit();
        $form->populate($model->getOptions());
        $form->populate(array('authors' => $model->getAuthors(false)));

        if ($request->isPost()) {
        	if ($form->isValid($request->getPost())) {
            	$paper = new Papers_Model_Paper($form->getValues());
            	$paper->setTitle_en($form->title)
            		  ->setFile($form->file->isReceived() ?
            		  			$form->file:
            		  			$model->getFile())
            		  ->setAuthors($form->authors);
                $mapper->save($paper);
                //return $this->_helper->redirector('edit-paper');
            }
        }

        $this->view->form = $form;
    }

    public function deletePaperAction() {
	$this->_helper->viewRenderer->setNoRender(true);
    	$model	 = new Papers_Model_Paper();
        $mapper  = new Papers_Model_PaperMapper();
        $mapper->find($this->getRequest()->getParam('id'), $model);
    	$mapper->delete($model);

    	return $this->_helper->redirector('index','index');
    }

}

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

Представление начинается

Теперь дело осталось за малым – красиво показать полученную контроллером инфу в наших представлениях (views).

<!-- /application/modules/papers/views/scripts/index/index.phtml -->
<table>
	<thead>
		<tr>
			<td>Название статьи</td>
			<td>Источник</td>
			<td>Авторы</td>
			<td>Дата публикации</td>
		</tr>
	</thead>
	<tbody>
		<?php foreach ($this->entries as $entry): ?>
	    <tr>
	    	<td>
	    		<?php echo "<a href='{$this->url(array(
	    						'controller' => 'index',
	    						'action'	 => 'edit-paper',
	    						'id'		 => $entry->id))}'>
	    					{$this->escape($entry->title)}</a>"?>
	    	</td>
	    	<td><?php echo $this->escape($entry->source) ?></td>
	    	<td><?php
	    		if (null !== $entry->authors)
	    			$authors = $entry->authors;		//to eleminate Notice about overloading
	    			foreach ($authors as $author) {
	    				echo "<a href='
	    					{$this->url(array(
		    					'controller'	=>	'index',
		    					'action'		=>	'edit-author',
		    					'id'			=>	$author->getId()))}'
		    				>{$author->getFIO()}</a>";
	    				if (true == next($authors)) echo ', ';
	    			}
	    		?>
	    	</td>
	    	<td><?php echo $this->escape($entry->date) ?></td>
	    </tr>
	    <?php endforeach ?>
	</tbody>
</table>

Тут интересно то, как из моделей достается нужная инфа, просто и элегантно) Ну и вот это тоже довольно занятно:

if (true == next($authors)) echo ', ';

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

О боже, зачем столько всего?

Действительно, сначала может показаться, что такими толстыми классы работы с БД не должны быть. Куда проще писать простенькие sql-запросы и проходится по полученным результатами, нам же по сути ничего кроме трех запросов добавления, обновления и удаления информации из БД и не нужно. Зачем эти мапперы, шлюзы и модели?
Ответ всему – стандартизация и качественный код:

  1. Такой код изначально защищен от многих типов атак, типа sql-инъкций
  2. Архитектуру такого приложения легко понять другому программисту, знакомому с применяемыми паттернами и фреймворком, что есть огромный плюс для командной работы
  3. Такой код просто распараллеливать для написания. Стоит просто изначально договорится какие модели будут использовать и уже одновременно можно писать и вид, и контроллер, и маппер.
  4. Да и говорят еще, такой код проще покрывать тестами.)

Заключение

Вот и подошел к концу мой титанический труд.) В первый раз писал цикл статей и вообще посты такого объема.
Рабочий пример можно скачать здесь.
Там вы найдете установочный sql-скрипт, всё, что описано выше + формы, описание которых я оставляю на следующие статьи.

Надеюсь, всем было интересно, никто особо не зевал (ну или хотя бы прикрывался рукой), и получится интересная дискуссия по этому поводу.
Меня интересуют моменты правильности подхода как такового и соответствие корпоративным стандартам (если вообще такие уже есть на веб-приложения на php).