HOWTO: Одна из возможных реализация Модели (MVC) в Zend Framework

  • Tutorial
Написание статьи навеяно habrahabr.ru/qa/34735 и habrahabr.ru/qa/32135 вопросами, в качестве ответов на которые не смог найти полной и подробной информации, чего очень не хватало. Я надеюсь, что она будет полезна и другим.

Проект, на чью долю пал выбор в виде ZF в качестве основного фреймворка, представлял из себя мобильную версию сервиса (адаптивный дизайн с некоторыми ньюансами) + АПИ для мобильных приложений.
Коллегиально было принято политико-техническое решение делать единое АПИ, посредством которого будет общаться и сайт, и приложения.

На этом, думаю, прелюдию можно закончить и перейти к самому интересному.

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

Немного теории


Начало

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

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

Сразу хочу обратить внимание, тех, кто говорит о монструозности сего чуда — да, зенд достаточно большая, масштабная пушка, из которых на первый взгляд… по воробьям то и не к чему… Но присмотревшись и изучив его особенности даже поверхностно, я могу добавить, что калибр этой пушки очень даже настраивается. Есть достаточно неплохо работающий autoloader, который позволяет подключать минимальный набор классов.

Собрав каркас тестового приложения (quick start) начал процесс проектирования архитектуры при активном изучении возможностей, рекомендаций и best practice разработки на ZF (очень понравилась презентация, почерпнул из нее много мыслей, по тексту на нее еще будут ссылки).

Модель в MVC

Многие воспринимают и описывают модель как способ доступа к данным на уровне базы данных, но это не совсем верно. Модель — это не реляционная база данных, и даже не таблица. Данные могут приходить из разных источников.
Я рассмотрел модель как многоуровневую прослойку и выделил для себя 3 слоя:
  • Domain Model
  • Data Mapper
  • Data Access Layer (DAL)

Domain Model — описание метаданных объекта, включая геттеры, сеттеры, валидацию данных и описание поведения объекта (behavior). Бытует мнение, что описание поведение также можно вынести в прослойку DomainEvents, и это есть нечто иное как Table Data Gateway pattern.
Этот уровень в моей реализации ничего не знает о способах (и местах) хранения данных.

Data Mapper представляет собой некоторую прослойку, предназначенную для непосредственного трансфера данных от уровня абстрактного описания к низкому уровню.

DAL содержит прямые запросы к источнику хранения данных. Там можно встретить SQL код и другие прелести жизни. В ZF роль этого уровня выполняет Zend_Db_Table и его производные.

Если использовать внешние ORM, например Doctrine, то она вполне заменяет последние уровни и облегчает жизнь разработчика. Так как я себе поставил цель скорее «изучать с погружением», я не стал использовать сторонние ORM и решил сделать свой велосипед свою реализацию.

HowTo


Структура проекта

Реально получившаяся картина соответствует следующей организации файловой структуры:

application/
	controllers/
		IndexController.php
		FooController.php
	models/
		Abstract/
			AbstractMapper.php
			AbstractModel.php
		DBTable/
			FooTable.php
			DeviceTable.php
		Mapper/
			FooMapper.php
			DeviceMapper.php
		Foo.php
		Device.php
	services/
		DeviceService.php
		FooService.php
	views/

Немного кода в примерах

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

Пример контроллера
class DeviceapiController extends Zend_Controller_Action
{
	public function init()
	{
		$this->_helper->viewRenderer->setNoRender(true);
	}

	/**
	 * Login user from API Request
	 * @method POST
	 * @param json rawBody {"data":{"login": "admin", "password": "password"}}
	 * @param string login in JSON
	 * @param string password in JSON
	 *
	 * @return string SecretKey
	 * @return HTTP STATUS 200 if ok 
	 * @return HTTP STATUS 400 if fields doesn't valid  
	 * @return HTTP STATUS 409 if user already exist
	 */
	public function loginAction()
	{
		$request    = $this->getRequest();
		$data	    = $request->getRawBody();
		
		if ($data) {
			// decode from json params
			$params = Zend_Json::decode($data);
			
			$result = Application_Service_DeviceService::login($params);
			if (!is_null($result['secretKey'])) {
				$this->getResponse()
					->setHttpResponseCode(200)
					->setHeader('Content-type', 'application/json', true)
					->setBody(Zend_Json::encode($result));
				
				$this->_setSecretKeyToCookies($result['secretKey']);
				return;
			}
			$this->getResponse()
				->setHttpResponseCode(401);
			return;
		}
		
		$this->getResponse()
			->setHttpResponseCode(405);
		return;
	}

	/**
	 * Profile from API Request
	 *
	 * @method GET
	 * @param Request Header Cookie secretKey
	 *
	 * @return json string {"id":"","email":"","realName":""}
	 * @return HTTP STATUS 200 OK
	 */
	public function profileAction()
	{
		$cookies = $this->getRequest()->getCookie();
		if (!isset($cookies['secretKey']) || (!Application_Service_DeviceService::isAuthenticated($cookies['secretKey']))) {
			$this->getResponse()
				->setHttpResponseCode(401)
				->setHeader('Content-Type', 'application/json')
				->setBody(Zend_Json::encode(array("message" => "Unauthorized")));
			return;
		}
		
		$result = Application_Service_DeviceService::getProfile($cookies['secretKey'])->toArray();
		
		unset($result['password']);
		unset($result['passwordSalt']);
		
		$this->getResponse()
			->setHttpResponseCode(200)
			->setHeader('Content-type', 'application/json', true)
			->setBody(Zend_Json::encode($result));
		return;
	}
	
	/**
	 * Logout from API Request
	 * @method POST
	 * @param Request Header Cookie secretKey
	 * 
	 * @return HTTP STATUS 200 OK
	 */
	public function logoutAction()
	{
		$cookies = $this->getRequest()->getCookie();

		if ($cookies['secretKey']) {
			$device = new Application_Model_Device();
			$device->deleteByKey($cookies['secretKey']);
			$this->_setSecretKeyToCookies($cookies['secretKey'], -1);

			if(Zend_Auth::getInstance()->hasIdentity()) {
				Zend_Auth::getInstance()->clearIdentity();
			}
		}
	        $this->getResponse()
			->setHttpResponseCode(200);
		return;
	}
	
	/**
	 * Signup user from API Request
	 * @method POST
	 * @param  json string {"email": "", "password": "", “realName”: “”}
	 *
	 * @return string SecretKey
	 * @return HTTP STATUS 201 Created
	 * @return HTTP STATUS 400 Bad request
	 * @return HTTP STATUS 409 Conflict - user already exist
	 */
	public function signupAction()
	{
		$request    = $this->getRequest();
		$data	    = $request->getRawBody();

		// decode from json params
		$params = Zend_Json::decode($data);

		$email = $params['email'];
		$name = $params['realName'];
		$password = $params['password'];

		$err = array();
		if (!isset($email) || !isset($name) || !isset($password) || (filter_var($email, FILTER_VALIDATE_EMAIL)==FALSE))
		{
			if (!isset($email)) {
				$err['email'] = "Email is missing";
			}
			if (!isset($name)) {
				$err['name'] = "Name is missing";
			}
			if (!isset($password)) {
				$err['password'] = "Password are missing";
			}
			if (filter_var($email, FILTER_VALIDATE_EMAIL)==FALSE) {
				$err['valid_email'] = "Email is not valid";
			}
		}
		
		if (!empty($err)) {
			$this->getResponse()
				->setHttpResponseCode(400)
				->setBody(Zend_Json::encode(array ("invalid" => $err)));
			return;
		}
		
		$service = new Application_Service_DeviceService();
		$params = array("email" => $email, "username" => $name, "password" => $password);
		
		try {
			$result = $service->signup($params);
		} catch (Zend_Exception_UserAlreadyExist $e) {
			$this->getResponse()
				->setHttpResponseCode(409)
				->setBody(Zend_Json::encode(array("message" => "User already exist")));
			return;
		}
		
		$this->getResponse()
			->setHttpResponseCode(201)
			->setHeader('Content-type', 'application/json', true)
			->setBody(Zend_Json::encode($result));

		$this->_setSecretKeyToCookies($result['secretKey']);
		return;
	}

	/**
	 * Protected local method to set Secretkey to Cookies
	 * @param string $secretKey
	 * @param int | null $timeFlg
	 */
	protected function _setSecretKeyToCookies($secretKey,$timeFlg = 1) {
			$cookie = new Zend_Http_Header_SetCookie();
			$cookie->setName('secretKey')
				->setValue($secretKey)
				->setPath('/')
				->setExpires(time() + (1* 365 * 24 * 60 * 60)*$timeFlg);
			$this->getResponse()->setRawHeader($cookie);
			return;
	}
}


Таким образом, контроллер в данном примере выполняет роль предварительного валидатора входных данных, роутера на бизнес (вызов определенных сервисов) и формирование ответов. В моем примере мне было необходимо возвращать данные только посредством АПИ. В более сложных случаях, когда нужно отработать одну и ту же логику, только в зависимости от типа запроса или других параметров, выдать ответ в разном формате, удобно использовать content switcher. Например, это может быть полезно, когда один и тот же запрос используется для простого взаимодействия с сайтом, для отработки аякс вызовов, или когда нужно одни и те же данные отдать в различных форматах (либо в JSON, либо в XML, например) в зависимости от Content-Type запроса.
На мой взгляд, это наиболее эффективное использование контроллеров, которое позволяет достаточно легко расширять функционал.
Такие контроллеры достаточно легко тестировать. При этом, тесты действительно помогают понять, как работает функционал, как он должен работать. В процессе разработки я не применял такие методики, как TDD, поэтому тесты писал уже по готовым контроллерам. Это помогло выявить пару узких мест и потенциальных багов.
В подтверждение моих слов о легкой тестируемости таких контроллеров ниже приведу пример тестов.

Тесты для такого контроллера выглядят так
class LoginControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    /*
     * Fixtures:
     * User with `email@example.com` and `password`
    */
    public function setUp()
    {
        $this->bootstrap = new Zend_Application(APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini');
        parent::setUp();
    }

    public function testSuccessfulLoginAction()
    {
    	$request = $this->getRequest();

    	$email = 'email@example.com';
    	
    	$request->
    		setMethod('POST')->
    		setHeader('Content-Type', 'application/json')->
    		setRawBody(Zend_Json::encode(array(
    			'email' => $email,
    			'password' => 'password',
    		)));
    	$this->dispatch('/login');
    	$this->assertResponseCode(200); 
    	$this->assertNotRedirect();
    	$this->assertHeaderContains('Content-Type', 'application/json');
    	$data = $this->getResponse()->getBody();
    	$data = Zend_Json::decode($data, true);
    	
    	$this->assertArrayHasKey('secretKey', $data);
    	$this->resetRequest()
    		->resetResponse();  

    	// Test logout
    	$request->
	    	setMethod('POST')->
	    	setHeader('Content-Type', 'application/json')->
	    	setCookie('secretKey', $data['secretKey']);
    	$this->dispatch('/logout');
    	$this->assertResponseCode(200);
    	
    	$this->resetRequest()
    		->resetResponse();
    }
    
    public function testLoginWithEmptyParamsAction()
    {
    	$request = $this->getRequest();
    
    	$request->
    		setMethod('POST')->
    		setHeader('Content-Type', 'application/json')->
    		setRawBody(Zend_Json::encode(array(
    			'email' => '',
    			'password' => '',
    	)));
    	$this->dispatch('/login');
    	$this->assertResponseCode(401); 
    	
    	$this->resetRequest()
    		->resetResponse();
    }
    
    public function testLoginWithoutParamsAction()
    {
    	$request = $this->getRequest();
    
    	$request->
    		setMethod('POST')->
    		setHeader('Content-Type', 'application/json');
    	
    	$this->dispatch('/login');
    	$this->assertResponseCode(405);
    	
    	$this->resetRequest()
    		->resetResponse();
    }
    
    public function testSignupAction()
    {
        $request = $this->getRequest();
        
        $email = "newemail_".substr(MD5(uniqid(rand(), true)), 0, 12)."@".substr(MD5(uniqid(rand(), true)), 0, 5).".com";
        
        $request->
            setMethod('POST')->
            setHeader('Content-Type', 'application/json')->
            setRawBody(Zend_Json::encode(array(
                'email' => $email,
                'password' => 'password',
                'realName' => 'John Dow',
            )));
        $this->dispatch('/signup');
        $this->assertResponseCode(201);
        $this->assertHeaderContains('Content-Type', 'application/json');
        $data = json_decode($this->getResponse()->outputBody(), true);
        $this->assertArrayHasKey('secretKey', $data);
        $secretKey = $data['secretKey'];
        $this->assertArrayHasKey('user', $data);

        $this->resetRequest()
             ->resetResponse();

        $request->
            setMethod('POST')->
            setHeader('Content-Type', 'application/json')->
            setRawBody(json_encode(array(
                'email' => '2',
                'password' => '11',
                'realName' => '23s',
            )));
        $this->dispatch('/signup');
        $this->assertResponseCode(400);
        $data = json_decode($this->getResponse()->outputBody(), true);
        $this->assertArrayHasKey('invalid', $data);
        $invalid = $data['invalid'];
        $this->assertArrayHasKey('email', $invalid);
        $this->assertArrayHasKey('password', $invalid);
        
        $this->resetRequest()
             ->resetResponse();
    }
    
    public function testAlreadyExistUserSignup() 
    {
    	$request = $this->getRequest();
    	
    	$request->
	    	setMethod('POST')->
	    	setHeader('Content-Type', 'application/json')->
	    	setRawBody(Zend_Json::encode(array(
	    			'email' => 'email@example.com',
	    			'password' => 'password',
	    			'realName' => 'John Dow',
	    	)));
    	$this->dispatch('/signup');
    	$this->assertResponseCode(409);
    	
    	$this->resetRequest()
    		->resetResponse();
    }
}


В сервисах реализован сам бизнес. Методы сервисов я старался делать статическими. Такой подход позволял мне не создавать лишний раз объект сервиса и минимизировать зависимости сервисов от контекста и друг от друга, что также облегчает их тестирование, рефактор, внесение изменений, расширение функциональности.
Также стоит обратить внимание, что сервисы возвращают данные в контекстно-независимом формате (например, массивы), а их упаковкой в конкретный формат, уже занимается контроллер. Поэтому, если завтра нам потребуется изменить формат передачи данных, мы это сможем изменить легким движением руки «без лишней крови».

Пример сервиса
class Application_Service_DeviceService
{
	public static function login (array $params) 
	{
		if (!empty($params) && !empty($params['email']) && !empty($params['password']))
		{	
			$user = new Application_Model_User();
			$device = new Application_Model_Device();
			$adapter = new Zend_Auth_Adapter_DbTable(
					Zend_Controller_Front::getInstance()->getParam('bootstrap')->getPluginResource("db")->getDbAdapter(),
					'user',
					'email',
					'password',
					'MD5(CONCAT(?, passwordSalt,"' //MD5(пароль + уникальный хэш + общий хэш)
					. Zend_Controller_Front::getInstance()->getParam('bootstrap')->getOption('salt') . '"))'
			);
			//идентификатор пользователя
			$adapter->setIdentity($params["email"]);
			//параметр для проверки через функцию из Zend_Registry::get('authQuery')
			$adapter->setCredential($params["password"]);

			$auth = Zend_Auth::getInstance();
			if ($auth->authenticate($adapter)->isValid()) //успешная авторизация
			{
				$id = $user->getCurrentUserId();
				$secretKey = $user->generateSecretKey();
				
				try {
					$device->userId = $id;
					$device->secretKey = $secretKey;
					$device->lastUsage = time();
					$device->save();
				} catch (Exception $e) {
					throw new Exception("Couldn't save with error ".$e);
				}
				
				$user->loadById($id);
				
				return array("secretKey" => $secretKey, "user" => array("email" => $user->{Application_Model_User::ATTRIBUTE_EMAIL}, "realName" => $user->{Application_Model_User::ATTRIBUTE_REALNAME}, "id" => $user->{Application_Model_User::ATTRIBUTE_ID}));
			}
		}
		return NULL;
	}
	
	public function signup (array $params) {
		//добавляем пользователя в базу данных
		$user = new Application_Model_User();
		
		if ($user->findExistUserByEmail($params['email'])) 
		{
			throw new Zend_Exception_UserAlreadyExist();
		} 
		
		$user->email = $params['email'];
		$user->realName = $params['username'];
		$user->passwordSalt = $user->generatePwdSalt();
		$user->password = $user->generatePwd($params['password']);
		$user->save();
		return $this->login($params);
	}


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

Пример класса модели
class Application_Model_Device extends Application_Model_Abstract_AbstractModel
{
	const ATTRIBUTE_ID 			= "id";
	const ATTRIBUTE_USER_ID 	        = "userId";
	const ATTRIBUTE_SECRET_KEY 	= "secretKey";
	const ATTRIBUTE_LAST_USAGE 	= "lastUsage";

	protected 	$_id,
				$_userId,
				$_secretKey,
				$_lastUsage;

	public function __construct(array $options = null, $mapper = null)
	{
		// for future decorate
		if (is_null($mapper)) $this->_mapper = new Application_Model_DeviceMapper();
		else $this->_mapper = $mapper;

		if (is_array($options)) {
			$this->setOptions($options);
		}
	}

	/**
	 * Wrapper block
	 */
	public function fromProps() {
		return $data = array(
				self::ATTRIBUTE_USER_ID => $this->userId,
				self::ATTRIBUTE_SECRET_KEY => $this->secretKey,
				self::ATTRIBUTE_LAST_USAGE => $this->lastUsage,
		);
	}

	/*
	 * Start describe behaivors of object
	 */
	public function getDeviceByKey ($key) {
		return $this->_mapper->findByKey($key);
	}
	
	public function deleteByKey($key) {
		return $this->_mapper->deleteByCriteria('secretKey', $key);
	}
}


Более сложный пример метода модели
        /**
	 * Delete File in DB and unlink physical file
	 *
	 */
	public function deleteFile()
	{
		$id = $this->id;
		if (empty($id)) {
			throw new Exception('Invalid id');
			return false;
		}
		$imageFile = UPLOAD_PATH.'/'.$this->{self::ATTRIBUTE_REAL_NAME};
		$thImageFile = THUMB_PATH.'/'.$this->{self::ATTRIBUTE_TH_NAME};
		// Удаляем эту запись из БД
		
		$this->_mapper->deleteById($id);
		// Удаляем физический файл
		unlink($imageFile);
		unlink($thImageFile);
	}


Таким образом, наша непосредственная модель включает в себя определение метаданных (свойств объекта) и описывает их поведение. При этом, поведение объекта описано на достаточно абстрактном уровне и заканчивается вызовом определенного метода из маппера, который уже отвечает за взаимодействие с хранилищем. При необходимости подключить дополнительный источник данных, например, завтра мы решим использовать дополнительно NoSQL базу, или начать использовать кэш, то нам будет достаточно декорировать. Еще раз хочу сослаться на презентацию, где очень наглядно продемонстрированы все преимущества такого подхода.
Погружаемся глубже.
Следующим уровнем в моей реализации является маппер. Его основное назначение — пробросить данные или запрос от модели до уровня DAL. Другими словами, на этом уровне мы реализуем Table Data Gateway pattern.

Пример маппера
class Application_Model_DeviceMapper extends Application_Model_Abstract_AbstractMapper
{
	const MODEL_TABLE = "Application_Model_DBTable_DeviceTable";
	const MODEL_CLASS = "Application_Model_Device";

	/**
	 * Get DBTable
	 *
	 * @return string $dbTable return current dbTable object
	 */
	public function getDbTable()
	{
		if (null === $this->_dbTable) {
			$this->setDbTable(self::MODEL_TABLE);
		}
		return $this->_dbTable;
	}

	public function _getModel() {
		return new Application_Model_Device();
	}

	public function update(array $data, $where)
	{
		// add a timestamp
		if (empty($data['updated'])) {
			$data['updated'] = time();
		}
		return parent::update($data, $where);
	}

	/**
	 * @param string $key
	 * @throws Zend_Exception_Unauthtorize
	 */
	public function findByKey($key)
	{
		$result = $this->getDbTable()->fetchRow($this->getDbTable()->select()->where("secretKey = ?", $key));
		if (0 == count($result)) {
			throw  new Zend_Exception_Unauthtorize();
		}
		return $result;
	}
}


В рамках своей задачи я реализовал только один маппер — работа с MySql базой, но уже есть задача и подключение работы с кэшем, и потенциальная мысль перевести ряд объектов на NoSQL. Для меня это будет означать лишь необходимость декорирования и написания минимального количества кода. Тесты при этом переписывать не придется (за исключением написания новых :) )
Как видно из кода, данный маппер обращается к классу таблицы — DAL.
Для этой прослойки я не придумывал ничего нового и использовал стандартные классы, которые предоставляет Zend.
Сам класс выглядит весьма не замысловато:

Класс доступа к данным (уровень DAL)
class Application_Model_DBTable_DeviceTable extends Zend_Db_Table_Abstract
{
	protected $_name = 'deviceKey';
	protected $_primary = 'id';
	
	protected $_referenceMap    = array(
			'Token' => array(
					'columns'           => 'userId',
					'refTableClass'     => 'Application_Model_DBTable_UserTable',
					'refColumns'        => 'id',
					'onDelete'      	=> self::CASCADE,
					'onUpdate'      	=> self::CASCADE,
			));
	
	public function __construct($config = array()) {
		$this->_setAdapter(Zend_Db_Table::getDefaultAdapter());
		parent::__construct();
	}
}


Если заглянуть в мануалы Zend Framework, то легко заметить, что именно этот (и только этот уровень) предлагается в качестве реализации модели (см. мануалы + Quick Start).
Дополнительно я использую абстрактные методы маппера и модели, но их назначение, надеюсь, очевидно.
В дополнение хочу сказать, что Zend_Db_Table возвращает значения либо в массивах, либо в виде объекта соответствующего типа, который не соответствует типу нужного нам объекта, из контекста которого мы вызываем эти методы.
Для приведения данных, получаемых из хранилищ данных, а также для их валидации, мы можем использовать методы, заданные на уровне модели(ORM).

Резюме


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

На первый взгляд, решение, описанное мною выше, более сложное относительно описанного в мануалах. Также неизбежно создается больше объектов и идет более глубокая проброска данных внутрь.
Это, бесспорно, минусы.
Теперь о плюсах.
  • Мы получаем более атомарный код, который удобнее тестировать, а значит он будет проще для чтения, качественнее и вероятность ошибок в нем будет значительно меньше.
  • Гибкость, расширяемость. Для расширения функционала необходимо лишь декорировать существующий код.
  • Разделение «зон ответственности» между уровнями.

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

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

PS

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

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 46

    +1
    Хорошая статья, спасибо!
      +1
      Спасибо за отзыв. Очень боялся негативных отзывов. После вопроса habrahabr.ru/qa/34735/ были неоднозначные ощущения.
      +1
      Отличная статья! Спасибо!
        +2
        Вместо ручного формирования ответа в экшене контроллера

        $this->getResponse()
                    ->setHttpResponseCode(200)
                    ->setHeader('Content-type', 'application/json', true)
                    ->setBody(Zend_Json::encode($result));
        

        можно использовать JSON Action Helper и тогда код сократится до:

        $this->_helper->json($result);
        
          +2
          Промахнулся ответом :(
          Не совсем согласен с Вашей реализацией сервисов и на мой взгляд она содержит следующие недостатки:
          1) public static function login (array $params) — передавать все параметры в виде массива это плохая привычка, если использовать такой подход со временем окажется что в ваш сервис надо передать массив из 20-100 параметров
          в Вашем случае достаточно обьявить функцию:
          public static function login ($email, $password)
          Согласитесь что это намного понятнее чем массив и позволяет использовать сервис без необходимости читать его код или документацию (которую кстати надо сначала написать, а потом поддерживать)
          Кроме того передача параметров явно очень полезна т.к. если вдруг вам понадобиться сервис в котором более 3-х параметров вы уже задумаетесь о разделении ответственности.
          2) Статические сервисы хороши только для логина в вакууме — во всех остальных случаях лучше все же создавать обьект и пользоваться нестатическими методами. Это даст гораздо большую гибкость если Вам понадобятся более сложные сервисы. Например можно будет легко декорировать сервисы дополнительным функционалом (кеширование, изменение формата данных для разных версий АПИ например). Для того чтобы облегчить создание сервис обьектов советую посмотреть на паттерн DependancyInjection Container и как он реализован в Zend2 и Symfony2 хотя я и не считаю их реализацию очень удачной (в основном изза ограничений самого пхп)
            0
            Спасибо за критику по существу.

            Насчет параметров — ну скажу так, это достаточно холиварные вопросы, и каждый из вариантов содержит свои плюсы и минусы. Да, можно и так, как вы предложили, но модно и так, как реализовал я, а также есть еще 100500 способов.

            Насчет статических сервисов. Для начала скажу, что они у меня не все статические. Все таки я привел «нейтральный код». Для рассматриваемой логики в сервисах, я считаю, что лучше сервисы сделать статическими.

              0
              А насчет dependency — в ближайших планах столкнуться с zf2 )
              0
              Еще Context Switch не забудьте.
                +1
                <придирчивость>
                При необходимости подключить дополнительный источник данных, например, завтра мы решим использовать дополнительно NoSQL базу, или начать использовать кэш, то нам будет достаточно декорировать.
                            $adapter = new Zend_Auth_Adapter_DbTable(
                                    Zend_Controller_Front::getInstance()->getParam('bootstrap')->getPluginResource("db")->getDbAdapter(),
                                    'user',
                                    'email',
                                    'password',
                                    'MD5(CONCAT(?, passwordSalt,"' //MD5(пароль + уникальный хэш + общий хэш)
                                    . Zend_Controller_Front::getInstance()->getParam('bootstrap')->getOption('salt') . '"))'
                            );
                

                Кроме декорирования, вам придется переписывать сам сервис. MD5 и CONCAT не валидны не только для NoSQL, но даже для других СУБД. Солить пароль, я думаю, стоит на уровне модели.
                </придирчивость>
                  0
                  Соглашусь с вами. Там разные контроллеры есть. Если смотреть на конкретную схему реализации контроллера авторизацию — его можно полностью переписать, так как мы ушли на другую схему авторизации, где можно сделать проще.
                  Как говорится, руки сюда еще не дошли. Спасибо за замечание.
                    0
                    Если посмотреть на метод logout, то там видно, что zend_auth скорее артефакт, нежели рабочая схема.
                    0
                    Context switcher в статье упоминается.
                    Я его лично пока что не использую, так как нет нужды.
                  0
                  Спасибо за наводку, но есть ряд со мнений.
                  1) ссылка на zf2. Не уверен что этот хелпер есть и в первой ветке.
                  2) не всегда нужно возвращать 200 статус, опять таки, надо посмотреть на практике, как будет себя вести
                  3) не будет ли перекрываться setNoRender, как в случае с json_encode?
                  –1
                  Мне не совсем понятна логика такого пространства имён:
                  DBTable/
                  FooTable.php
                  DeviceTable.php

                  должно быть так:
                  DbTable/
                  Foo.php
                  Device.php

                  При это доменная модель должна быть доступна вообще без префиксов, куда приятнее набирать просто new Foo(); это ведь корень системы.

                  В модели у вас конструктор можно перенести в её абстрактную часть. Там же в данном контексте не совсем понятна роль констант.

                  Вообще странная логика из Application_Model_Device вы получаете обьекты Zend_Db_Table_Row, гораздо прозрачнее и логичнее инстанцировать маппер, раз он есть и из него получать доменную модель. К тому же после внесения поведений, это уже не доменная модель.

                  Что ещё больше бросается в глаза, так это вообще необходимость таких моделей, раз вы пользуетесь всё равно пользуетесь Zend_Db_Table_Row, куда удобнее использовать расширение модели через Zend_Db_Table_Abstract и protected $_rowClass.

                  Вы написали про мапперы, но то что я вижу это не мапперы, что вы маппите? Вы получаете уже готовые обьекты и никакого маппинга в примере нет.

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

                  Статья мне не понравилась, хорошо что она практически не актуальна и новички скорее всего отсюда ничего не возьмут.
                    0
                    Вас архитектура не понравилась или названия классов?
                    Тут уж как говорится «вам шашечки или ехать»?

                    Насчет мапперов — я не приводил примеры абстрактного классов и более сложных моделей и мапперов, но написал, что данные можно и собирать в нужный нам объект, и в конструкторов можно инстанцировать сразу загрузку. Но только ведь тут два но:
                    1) статья о другом
                    2) в статье указаны все эти возможности и рекомендации. Вас смущает, что я привел не весь код проекта? Извините, он коммерческий.

                    Насчет мапперов, аозаращаемых данных в zend_db_row и тд — по моему вы очень не внимательно ознакомились со статьей.
                      0
                      Прошу прошения за ряд опечаток. Печатал с телефона.
                        0
                        Да, мне не понравились названия классов и я мягко дал совет.
                        Да, мне не понравилась архитектура.
                        Нет, меня не смущает «не весь код проекта», меня смущает именно тот что есть.
                      +1
                      Ну, вас же предупреждали, что не нужно трогать мёртвую кошку, а то распахнется. Буду критиковать, но с попытками конструктивно послать в правильном направлении.
                      По порядку, файловая система, в зенде давно была впиленая модульная система, которая позволяет собрать файлы в наборы модулей. которые затем можно спокойно переносить из проекта в проект framework.zend.com/manual/1.12/ru/zend.controller.modular.html
                      Дальше для яджакса существует понятие смениы контекста stackoverflow.com/questions/7065378/clean-ajax-controller-implementation-in-zend-framework Удивительно что ваш код не ругается на отсутствие лэйаута вызова не нашёл его отключения$this->_helper->layout()->disableLayout();
                      Дальше if (!is_null($result['secretKey'])) и здесь я с ужасом понял что вы пишете код с отключенными нотисами.
                      Дальше вы писали про тестирование, а затем лепите статик вызовы $result = Application_Service_DeviceService::login($params), жёстко хардкодите зависимость да ещё с отсутствие её мочить прии тестировании.
                      Дальше у вас нет своего прокси класса для Zend_Controller_Action и сервисов, из-за чего приходится писать длинные цепочки вызовов для посылки заголовков или таскания из бутстрапа ресурса db.
                      У молелей типа как в методе signup вы устанавливаете волшебные аттрибуты, хотя нужно было выделить отдельный метод, который был бы единой точкой расположенной в самой модели где легче регулировать эти параметры. А вот для deleteFile вы создали лишний метод не связанный с самой моделью и программист должен помнить что нужно удалять файл, а не просто саму запись, нужно было конечно оверврайтить стандартный метод delete, чтобы сохранять целостность.
                      И т.д. и т.п. Зендом пользовался лишь пару раз, да и то в основном с доктриной, но уж у вас проблемы более чем очевидные и вопиющие, главным признаком которых является куча кода.
                        0
                        Проблема не в «мертвой кошке». Я очень много PS написал на этот счет.
                        Также по порядку отвечу.
                        Про файловую систему — знаю про модульность, но так как тема называется не «как сделать свое предложение на ZF», и не «Организация модульной архитектуры», решил специально опустить эту тему. С точки зрения архитектуры модели, не важно, сделано именно в таком виде и модель находится наверху, или же собрано в модуль, и модель находится внутри модели.
                        Насчет смены контекста — в рамках одного контроллера у меня нет нужды (исходя из задач) менять контекст. Насчет layout — использую setNoRender. Пока нотисов не видел.
                        Нотисы не отключены. Приведенный код упрощен. Про зависимость — не понял. Не могли пояснить. Также не совсем понял контекст слова «мочить ее».

                        «Дальше у вас нет своего прокси класса для Zend_Controller_Action и сервисов, из-за чего приходится писать длинные цепочки вызовов для посылки заголовков или таскания из бутстрапа ресурса db.»
                        А зачем? Не могли бы на примере показать, что это даст, для чего и тд. Буду благодарен.

                        «У молелей типа как в методе signup вы устанавливаете волшебные аттрибуты, хотя нужно было выделить отдельный метод, который был бы единой точкой расположенной в самой модели где легче регулировать эти параметры»
                        Я некоторые вещи опустил, некоторые используются в абстрактных классах. Опять таки, мы сейчас говорим не о теме поста.

                        «А вот для deleteFile вы создали лишний метод не связанный с самой моделью и программист должен помнить что нужно удалять файл, а не просто саму запись, нужно было конечно оверврайтить стандартный метод delete, чтобы сохранять целостность.»
                        В каком плане? На мой взгляд, именно это и есть действие над объектом, а значит именно это и должно быть в модели. Овверрайдить метод, который в маппере? А если завтра мы начнем хранить в MongoDB? Нам надо будет все переписать?
                        Насчет целостности — если мы не смогли удалить файл — ну ок. На мой взгляд, из этого не надо делать транзакцию. Ну по крайней мере в контексте моего проекта.

                        «И т.д. и т.п. Зендом пользовался лишь пару раз, да и то в основном с доктриной, но уж у вас проблемы более чем очевидные и вопиющие, главным признаком которых является куча кода.»
                        А где там куча кода? Я в статье написал, что доктрина может забрать на себя пару уровней, но речь то не об этом. Вы пытаетесь выдрать контекст и найти косяки в совершенно отчужденных местах. Вы бы еще сказали, что названия плохие для классов, что строчки выравнены плохо, там нет 4 пробелов зендовских, поэтому статья — полное УГ. Ей богу.
                          0
                          Не смешите, у вас про модель ничего почти нету, только калька на манульный пример, ссылку на которой минусонули в Q&A habrahabr.ru/qa/34735/#answer_135223, где действительно про модель. Лучше бы мануал по фреймворку осилили, чем спорили и выгораживались.
                            0
                            Мануал осилил. Спасибо.

                            А что такое модель в вашем понимании?

                            В моем — несколько слоев, которые отвечают за сами данные и тд (см статью). Каждый из этих уровней я старался максимально расписать, в том числе с примерами кода.

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

                            Собственно, судя по этому комментарию, есть ощущение, что вы сами статью то не до конца осилили.

                            Опять таки, я написал, что не претендую на «идеальное решение». Я не нашел в сети такой структурированной информации и потратил некоторое количество времени на разбор и написание. Если вы не оценили — очень жаль. С радостью почитаю вашу статью.

                            PS Не я минусовал вышу ссылку. могу скинуть скрин :)
                        +1
                        1. Почему сервисы статичные?
                        2. Зачем тестировать контроллеры, логичнее ведь тестировать сервисы?
                          0
                          1. Вы знаете, у меня нет однозначного ответа и 100% уверенности, что это правильно. Но на данных примерах, я не увидел «для чего нужно создавать объект сервиса». Есть методы в сервисах, связанные с контекстом. В пример они не попали, было бы лучше, если бы были видны различные подходы. Если вы считаете, что эти методы не должны быть статическими, поясните почему, и я вполне смогу с вами согласится.

                          2. Логичнее тестировать все — и контроллеры, и сервисы, и мапперы и модели.
                          Опять таки, я для примера показал, что в таком виде все достаточно легко тестируется… А цель этой статьи — все таки не «как правильно тестировать в ZF».
                            0
                            1. А мы помним что происходит с статичными переменными и функциями, как они работают и как они хранятся?
                            Если мне не изменяет память, то при компилировании проекта, под статические медоты и функции сразу выделяется пямять, в не зависимости, будите ли вы создавать объект класса или нет.

                            А с логической точки зрения, выделение медота login в область видимости всему проекту, особенно, когда login используется только один раз в одном месте, собственно, при залогинивании, не есть хорошо. ИМХО.
                              0
                              хм… Мне казалось наоборот, потребление меньшее. Ведь так получается, что в каждом месте, где мы используем, нам необходимо создать объект, и так проводится полная инициализация. А так -все же меньше. Могу ошибаться.
                              Также, если у меня тут нет явной зависимости между методами и переменными, но так проще изначально думать о дальнейшем рефакторе. А связывать это все вместе — мне кажется не правильная мысль. Тем более, связывание будет искусственным.

                              Беглое гугление дало такую ссылку www.sql.ru/forum/actualthread.aspx?tid=716873, но поищу еще.

                              Насчет области видимости… А что нам будет мешать создать объект класса сервиса в любом месте проекта и вызвать этот метод?
                                0
                                Не красиво просто это. Ну в примере с залогиниванием — точно не красиво.

                                Вот хороший пример (на мой взгляд):

                                Есть класс Humanity, который имеет 2 функции declension (Склонение существительных по числовому признаку) и human_date (Выводит дату в приблизительном удобочитаемом виде (например, «2 часа и 13 минут назад»)).

                                Эти функции мы используем во многих местах во view файлах. Вот такие функции стоит выделить в статику для дальнейшего упрощенного обращения к ним.

                                А вот метод залогинивания, который используется в одном месте и всего лишь один раз — не стоит, не красивое решение :)
                                  0
                                  Ну во-первых, он у меня используется не один раз ;)

                                  Просто инстанцировать объект вместо статики и оперируя лишь аргументом — мы будем это использовать только один раз. Ну ок. А зачем, если мы будем инстанцировать только в одном месте, то создавать под это лишний объект?
                          +2
                                  // decode from json params
                                  $params = Zend_Json::decode($data);
                          
                                  $email = $params['email'];
                                  $name = $params['realName'];
                                  $password = $params['password'];
                          
                                  $err = array();
                                  if (!isset($email) || !isset($name) || !isset($password) || (filter_var($email, FILTER_VALIDATE_EMAIL)==FALSE)) ...........
                          

                          Для чего надо делать проверку !isset($email) || !isset($name) || !isset($password), если переменные явным образом определены несколькими строчками ранее?

                          Дело в том, что соответствующих элементов массива после Zend_Json::decode() может не быть, а это уже: Notice: Undefined index: email.
                          Мы не можем быть уверены точно, что JSON придёт именно тот, что нам нужен.
                          В рамках данного контекста кода, на мой взгляд проверку как раз таки надо было делать так:

                          $email    = isset($params['email']) ? $params['email'] : null;
                          $name     = isset($params['realName']) ? $params['realName'] : null;
                          $password = isset($params['password']) ? $params['password'] : null;
                          

                          После того, как переменные определены и имеют какие-то значения, только тогда уже проверять и не isset(), а уже null !== $email.
                          И вообще, код в DeviceapiController::signupAction() мягко говоря шокировал меня:

                          if (!isset($email) || !isset($name) || !isset($password) || (filter_var($email, FILTER_VALIDATE_EMAIL)==FALSE))
                                  {
                                      if (!isset($email)) {
                                          $err['email'] = "Email is missing";
                                      }
                          

                          Зачем так делать?!
                          Я понимаю, что это всё примера ради, но всё же надо позаботиться, что бы это было как подобается, тем более, как я понял — это копипаст с рабочего проекта.
                            0
                            Для чего надо делать проверку !isset($email) || !isset($name) || !isset($password), если переменные явным образом определеный несколькими строчками ранее?

                            Там может быть NULL. Соглашусь с тем, что, возможно, стоило бы проверять сразу params['...'], а лишь потом присваивать переменным, либо вообще не использовать доп переменные.

                            После того, как переменные определены и имеют какие-то значения, только тогда уже проверять и не isset(), а уже null !== $email.

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

                            Насчет последнего — если учесть предыдущее замечание, с которым я согласился, и лишь прокомментировать этот момент — для того, чтобы вернуть error в определенном виде со всеми не валидными полями и описанием ошибок.

                            А как сделали бы вы? Если вы поможете мне понять мои ошибки, это позволит мне уже сегодня начать писать еще более лучший код. А значит это было не зря и для меня. Мы все учимся. Постоянно.

                            Насчет копипаста — с ньюансами, но в целом ~70-80% кода не изменял.
                              +1
                              Ну, к примеру, вот набросал в рамках вашего кода:

                                      $params = Zend_Json::decode($data);
                                      $errors = array();
                              
                                      $validate = function(array $what) use ($params, $errors) {
                                          if (!is_array($params) || empty($params)) {
                                              throw new InvalidArgumentException('Invalid params has provided');
                                          }
                                          foreach ($what as $name) {
                                              if (!isset($params[$name])) {
                                                  $errors[$name] = ucfirst($name) . ' can\'t be empty!';
                                              } elseif ('email' == $name) {
                                                  $validator = new Zend_Validate_EmailAddress();
                                                  if (!$validator->isValid($params[$name])) {
                                                      $errors[$name] = current($validator->getMessages());
                                                  }
                                              }
                                              /*
                                               * Your additional logic here
                                               * .................................
                                               */
                                          }
                                      };
                              
                                      try {
                                          $validate(array('email', 'name', 'password'));
                                      } catch (InvalidArgumentException $e) {
                                          // Handle exception
                                      }
                                      
                                      if (empty($errors)) {
                                          // Success
                                      } else {
                                          // Failure
                                      }
                              

                              А лучше сделать всё сервисом, используя factory pattern, для того что бы иметь возможность авторизовать пользователя к примеру через Facebook или Twitter, а не только учётной записью вашей локальной базы.
                              Имея такой подход можно с лёгкостью добавлять новые методы авторизации, написав лишь для каждого новый адаптор имплементрирующий конвенцию интерфейса, которую вы уже сами реализуете.

                              Тут, конечно, на вкус и цвет… Но, надо приучать себя писать scalable код, потому как никогда точно не знаешь, как может в последствии развиться проект и какие новые потребности могут появиться. Экономиться куча времени на рефакторинге и нервов, когда в конечном итоге приходиться всё переписывать. А тут, оп-оп и в дамках! :-)

                              Статья не плохая, развивайтесь!
                              Удачи вам!
                                0
                                Стоит отметить, что этот код будет работать, начиная с версии 5.3.
                                И еще тут уместнее не factory, а strategy.
                                  0
                                  На дворе 2013 год, вы ещё пишете на <=5.2?
                                  Почему лучше strategy? Обоснуйте.
                            0
                            Этот уровень в моей реализации ничего не знает о способах (и местах) хранения данных.

                            Так зачем тогда, засунули следующий кодв модель:
                            /* Start describe behaivors of object
                                 */
                                public function getDeviceByKey ($key) {
                                    return $this->_mapper->findByKey($key);
                                }
                                
                                public function deleteByKey($key) {
                                    return $this->_mapper->deleteByCriteria('secretKey', $key);
                                }
                            


                            Вынесети, такого рода методы в маппер, он ведь знает и от DataAccessLevel и от Domain Model.
                              0
                              Так сама имплементация же и есть в маппере. А маппер уже знает о местах хранения данных.
                              Если у нас изменится место хранения, то метод в модели останется тем же, а имплементация в маппере изменится. Ну и определим другой маппер. Или я не верно вас понял?
                                0
                                Подходов к реализации патернов много, но вот подход Доктрины, намного ближе к сути. Когда Модели ни чего не знаю об окружающем мире, кроме отношений между собой.
                                К примеру:
                                 $user = new Article();
                                 $user->setName();
                                 $user->setFirstname();
                                 $article = new Article();
                                 $article->setTitle('Doctrine model');
                                 $article->setUser($user);
                                 $em->persist($user);
                                 $em->persist($article);
                                 $em->flush();
                                
                                $em->remove($article);
                                $em->flush();
                                

                                Собственно Zend 2 предлагает аналогичный подход к реализации моделей, за некоторыми исключениями.
                                  0
                                  Полностью согласен с вами в этом моменте.
                                  Для себя на следующий проект хочу взять связку ZF2 + Doctrine попробовать.

                                  Но у меня были изначально другие входные данные и ограничения, поэтому я старался найти эффективное решение внутри ZF1, так как стандартная реализация мне показалась неэффективной.
                              +1
                              Очень интересное разделение модели на несколько уровней.
                              А не боитесь при этом получить более сложное обнаружение ошибок?
                              В случаи с тестированием проекта намного проще протестировать пару маленьких классов, но при появлении бага с залогиниванием, скажем пользватель не логинится, вам придется лезть в проект и копать кучу классов и искать место бага в них.
                              Вместо того, чтоб открыть одну модель UserTable и глянуть в ней функцию login.

                              И небольшая идея, как уменьшить ваш контроллер.
                              Используйте формы.
                              Понимаю что вы подумали, у меня тут API приложение, зачем мне формы?
                              Но формы нужны не только для их вывода, они представляют из себя хороший компонент для фильтрации и валидации данных!

                              К примеру, ваш код

                              $email = $params['email'];
                              $name = $params['realName'];
                              $password = $params['password'];

                              $err = array();
                              if (!isset($email) || !isset($name) || !isset($password) || (filter_var($email, FILTER_VALIDATE_EMAIL)==FALSE))
                              {
                              if (!isset($email)) {
                              $err['email'] = «Email is missing»;
                              }
                              if (!isset($name)) {
                              $err['name'] = «Name is missing»;
                              }
                              if (!isset($password)) {
                              $err['password'] = «Password are missing»;
                              }
                              if (filter_var($email, FILTER_VALIDATE_EMAIL)==FALSE) {
                              $err['valid_email'] = «Email is not valid»;
                              }
                              }

                              превратится в красивый и правильный

                              $loginForm = new LoginForm();
                              if ($loginForm->isValid($params)) {
                              // all cool!
                              } else {
                              $this->getResponse()
                              ->setHttpResponseCode(400)
                              ->setBody(Zend_Json::encode(array («invalid» => $loginForm->getErrors())));
                              return;
                              }

                              И вся логика по проверке и подготовке данных из контроллера и моделей перейдут в формы.
                                0
                                Насчет более сложного уровня обнаружения — точно нет. Очень быстро обнаруживается уровень, на котором происходит что-то не так, а дальше сами классы получается меньшего размера, поэтому дебажить значительно проще.

                                За мысль с формами — спасибо. Когда перешли на Апи, побоялся их использовать. Сказался недостаток опыта работы с ZF. Возьму на заметку.
                                  +1
                                  Если НЕ хотите Form(как я, например), Можно использовать Zend_Filter_Input. То же самое, но без Рендерера.
                                0
                                Поддержу про тестирование — такая изначально глубокая иерархия ради иерархии только создаёт сложности в поддержке и тестировании.

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

                                Да, это будет относительно трудоёмко при переделке, но ведь большой шанс что не понадобится вообще. А сейчас сложности уже есть.
                                  0
                                  А в чем сейчас сложности?

                                  Можно вообще все в одном файле делать. Вы будете точно знать, что вдруг чего — у вас все в одном файле. Удобно же? Мне вот нет.

                                  Также замечу, что иерархия далеко не ради иерархии.
                                +1
                                Прошу прощение за оформление кода. Тег source почему-то не хочет работать :(
                                  0
                                  вставлю свои пять копеек.
                                  у вашего кода есть «запах». просто отвлеченно посмотрите на это
                                  Application_Model_Abstract_AbstractModel
                                  

                                  array $options = null
                                  

                                  $this->_setAdapter(Zend_Db_Table::getDefaultAdapter());
                                  


                                  ->setHttpResponseCode(400)

                                  Вы уверены, что правильно используете эти коды? Для клиента они имеют какой-нибудь смысл?

                                  Короче, получилось примерно как вы писали в habrahabr.ru/qa/32135/ «Тут вопрос скорее не в том, где писать запросы в базу, а как это красивее разнести :) Именно, как вы и сказали — архитектура :)». Вот эта самая «архитектура» вам пока и мешает. Можно даже сказать, что на начальных этапах архитектура — это синоним для «ненужное усложнение».
                                    0
                                    // Я сторонник подхода, когда реализуется тонкий контроллер, а весь бизнес выносится в сервисы и модели.

                                    Полагаю, что это единственный и верный подход.

                                    Я поступал следующим образом: бизнес логика выносится в действия (Actions) — это по сути атомарная единица в функциональном разрезе приложения. Валадиция данных производиться там же. По сути это черный ящик на вход которого подаются данные а на выходе мы получаем результат. Задача контроллера контролировать выполнение этого действия, обрабатывать ошибки и отправлять результать выполнения действия на дальнейшую обработку. В самом действии могут быть использованы различные сущности предметной области (модели). Если для хранения данных используется реляционная БД, то контроль транзакции также осуществляется контролером а не моделью.

                                    Никакой логики представления в контроллере не может быть. Пример кода:

                                    class Order extends CController
                                    {
                                    function makeOrder()
                                    {
                                    // If passed data is not valid an exception is being thrown
                                    $action = new Action\MakeOrder($this->getPostData());
                                    // Perform our action.
                                    $action->perfom();
                                    // Get context of a template rendering the order form.
                                    $tplCtx = $this->getLayout()->getTemplateById('someTplId')->getContext();
                                    // Check errors
                                    if ($action->hasErrors())
                                    {
                                    switch ($action->getErrorType())
                                    {
                                    case Action\MakeOrder::ERR_LOW_BALANCE:
                                    $tplCtx->setLowBalanaceErr(true);
                                    //…
                                    }
                                    }
                                    }
                                    // Magic method. Being called if an exception in the method ::MakeOrder happens.
                                    function __exceptionMakeOrder()
                                    {

                                    }
                                    }

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

                                    Самое читаемое