Введение


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

Причин, как правило, несколько. Назову некоторые из них, которыми руководствовался я перед созданием этого поста. Во-первых, всегда приятно помочь сообществу, может кому пригодится то, что мы наработали. Во-вторых, не хочется «изобретать велосипед», то есть вдруг это уже кто-то сделал за нас, да ещё и лучше. Ну, и в-третьих, а вдруг мы что-то делаем не так, а нам никто не рассказал?

Работаем мы с Yii-фреймворком, поэтому все примеры будут именно касательно этого, без сомнения замечательного, PHP-фреймворка.

Об одной из «плюшек», которую мы сделали я уже рассказывал, хочется рассказать больше (и получить больше отзывов)

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

Суть проблемы: практически всегда CWebUser — это компонент Yii::app()->user, связаный с таблицей БД user и, соответственно, моделью ActiveRecord User. Заморачиваться каждый раз с получением модели пользователя для вывода данных таким образом:
$user = User::model()->findByPk(Yii::app()->user->id);
echo $user->email;

лично нам надоело. Далее, есть вариант при авторизации сохранить в state модель, а потом использовать что-то вроде Yii::app()->user->model->email, но писать лишнее model-> нам не понравилось.

Решение выглядит следующим образом:
Посмотреть
Класс WebUser
class VMWebUser extends CWebUser
{
    public $userClass;
    /**@var CActiveRecord $userModel */
    private $userModel = null;

	private function initializeUserModel() {
		if($this->userClass === null) {
			throw new CException(Yii::t('vmcore.errors', '{property} is not set up properly', array('{property}' => 'userClass')));
		}

		$userId = parent::getId();

		if ($userId && !$this->userModel) {
			$this->userModel = CActiveRecord::model($this->userClass)->findByPk($userId);
		}
	}

    public function __get($name)
    {
	    $this->initializeUserModel();

	    $userId = parent::getId();

        if ($userId) {
            if ($this->userModel && $this->modelHasOwnProperty($name)) {
                return $this->userModel->{$name};
            }
        }

        return parent::__get($name);
    }

	public function __call($name, $parameters)
	{
		$this->initializeUserModel();

		$userId = parent::getId();

		if ($userId) {
			if (method_exists($this->userModel, $name)) {
				return call_user_func_array(array($this->userModel, $name), $parameters);
			}
		}

		return parent::__call($name, $parameters);
	}

    private function modelHasOwnProperty($name)
    {
        return $this->userModel->hasAttribute($name) ||
        $this->userModel->hasProperty($name) ||
        array_key_exists($name, $this->userModel->relations());
    }
}


Конфигурация меняется следующим образом
...
'components' => array(
   'user' => array(
     'class' => 'WebUser',
     'userClass' => 'User',
  )
...
)
...


Суть в следующем: загружаем модель User из БД и ищем поле/метод у него. Если и там его нет — тогда уже возвращаемся к стандартному поведению CWebUser'а

Вложенные транзакции в MySQL

Вложенные транзакции в MySQL, увы, невозможны. В какой-то момент возникла проблема с тем, что есть некий метод, работающий с БД, всё обернуто в транзакцию. В другом методе, ситуация аналогичная. Далее, возникает необходимость сделать третий метод, который будет вызывать первый и второй, при том, что выполнится должны оба (или ни один). Проблема, не так ли? Мы это решили следующим образом:
...
public methodOne() {
   $transaction = new VMTransaction();
   //какие-то действия с БД
   $transaction->commit();
}

public methodTwo() {
   $transaction = new VMTransaction();
   //какие-то действия с БД
   $transaction->commit();
}

public methodThree() {
   $transaction = new VMTransaction();
   $this->methodOne();
   //какие-то действия с БД
   $this->methodTwo();
   $transaction->commit();
}
...


И, собственно, класс, который создает транзакцию, если нет активной, в противном случае — не делает ничего.
Класс Transaction
<?php

class VMTransaction extends CComponent
{
	private $transaction;

	public function __construct()
	{
		if(!Yii::app()->getDb()->currentTransaction) {
			$this->transaction = Yii::app()->getDb()->beginTransaction();
		}
	}

	public function execute($function)
	{
		try {
			$result = call_user_func($function, $this);
			$this->commit();
            return $result;

		} catch (Exception $exception) {
			$this->rollback();
			throw $exception;
		}
	}

	public function commit()
	{
		if ($this->transaction && $this->transaction->active) {
			$this->transaction->commit();
		}
	}

	public function rollback()
	{
		if ($this->transaction && $this->transaction->active) {
			$this->transaction->rollback();
		}
	}

	public function __destruct()
	{
		if ($this->transaction && $this->transaction->active) {
			$this->transaction->rollback();
		}
	}
}



Простая валидация форм

Лично меня постоянно напрягало написание вещей формата


Решение — класс FormValidator.
Класс FormValidator
<?php

/**
 * @class  VMFormValidator
 * Description of VMFormValidator class
 *
 * @property CFormModel $form
 */
class VMFormValidator extends CComponent
{
	private $formClass;
	private $form;

	public function __construct($formClass)
	{
		$this->formClass = $formClass;
	}

	public function validate($scenario = null, $ajaxValidate = false)
	{
		if (!$this->formClass) {
			throw new CException(Yii::t('vmcore.errors', '{property} is not set up properly', array('{property}' => 'formClass')));
		}

		$result = false;

		$this->form = new $this->formClass($scenario);
		$attributes = Yii::app()->request->getParam($this->formClass);

		if ($attributes) {
			$this->form->attributes = $attributes;

			if($ajaxValidate) {
				$errors = CActiveForm::validate($this->form);

				echo $errors;

				if($errors == CJavaScript::encode(array())) {
					$result = true;
				}
			} else {
				$result = $this->form->validate();
			}
		}

		return $result;
	}

	public function getForm()
	{
		return $this->form;
	}
}


Пример использования
public function actionSignUp() {
$validator = new VMFormValidator('SignUpForm');
if($validator->validate()) {
//Отсылаем на e-mail письмо об успешной регистрации
//Выводим сообщение
Yii::app()->user->setFlash('success', 'Вы успешно зарегистрировались. Дальнейшие инструкции высланы на e-mail');
$this->redirect('/');
}

$this->render('signUp', array('form' => $validator->form));
}


Разный конфиг для боевого/тестового сервера

Проблема общеизвестная, однако постоянно менять конфиг туда/обратно не нравится, мне кажется, никому.
Лично у нас конфиг теперь выглядит следующим образом: Все данные заполняются исключительно для тестового сервера. Перед паблишингом на боевой и в дальнейшем в секции components добавляется примерно следующая вещь:
...
'configManager' => array(
			'class'   => 'VMConfigManager',
			'configs' => array(
				'production' => array(
					'options' => array(
						'components' => array(
							'db' => array(
								'connectionString' => 'mysql:host=localhost;dbname=dbname',
								'emulatePrepare'   => true,
								'username'         => 'dbuser',
								'password'         => 'dbpassword',
								'charset'          => 'utf8',
							),
						),
						'params'     => array(
                                                   'adminEmail' => 'realadmin@domain.com',
						)
					),
				),
			),
		),

Далее, на боевом сервере в корне проекта кладется файл с названием 'production' и всё. Конфигов можно сделать много, главное файл с нужным названием кладите и всё, нужные данные будут переопределяться для конкретного случая.
И еще, не забываем configManager помещать в preload
Реализация
Класс ConfigManager
<?php

class VMConfigManager extends CApplicationComponent
{
	public $configs = array();

	public function init()
	{
		if (!$this->configs || !count($this->configs)) {
			throw new CException(Yii::t('vmcore.config', 'No configurations detected. Please specify one or more'));
		}

		foreach ($this->configs as $name => $configuration) {
			$instance = new VMConfig($name, $configuration);
			if ($instance->isActive()) {
				$instance->run();
			}
		}
	}
}


Класс Config
<?php

class VMConfig extends CComponent
{
	const DEFAULT_DETECTOR = 'VMFileConfigDetector';
	public $detector = null;
	public $options = array();
	public $modifiers = array();
	private $name = null;

	public function __construct($name, $params)
	{
		$this->name = $name;
		$params = VMObjectUtils::fromArray($params);

		$this->options = isset($params->options) ? $params->options : null;
		$this->modifiers = isset($params->modifiers) ? $params->modifiers : null;
		$detectorClass = null;

		if (isset($params->detector)) {
			$detectorOptions = $params->detector;

			if (isset($detectorOptions->class)) {
				$detectorClass = $detectorOptions->class;
			}
		}

		if (!$detectorClass) {
			$detectorClass = self::DEFAULT_DETECTOR;
		}

		$this->detector = new $detectorClass($name, $this->options);
	}

	public function isActive()
	{
		return $this->detector->detected();
	}

	public function run()
	{
		if($this->options) {
			foreach (VMObjectUtils::toArray($this->options) as $key => $value) {
				$this->setValue(Yii::app(), $key, $value);
			}
		}

		if($this->modifiers) {
			foreach (VMObjectUtils::toArray($this->modifiers) as $modifierOptions) {
				$modifierClass = $modifierOptions['class'];
				$modifier = new $modifierClass;
				foreach ($modifierOptions as $option => $value) {
					if ($option !== 'class') {
						$modifier->{$option} = $value;
					}
				}
				$modifier->run();
			}
		}
	}

	public function setValue(CComponent $component, $key, $value)
	{
		if (is_array($value) && is_a($component->{$key}, 'CComponent')) {
			foreach ($value as $subKey => $subValue) {
				$this->setValue($component->{$key}, $subKey, $subValue);
			}
		} else {
			$component->{$key} = $value;
		}
	}
}



Класс FileConfigDetector
<?php

class VMFileConfigDetector extends VMConfigDetector
{
public function detected()
{
$filename = isset($this->params->filename)? $this->params->filename: $this->name;

if (!$filename) {
throw new CException(Yii::t('vmcore.errors', '{property} is not set up properly', array('{property}' => 'filename')));
}
$exists = file_exists(Yii::app()->basePath. DIRECTORY_SEPARATOR. '..'. DIRECTORY_SEPARATOR. $filename);
return $exists;
}
}


Давно еще есть идея помимо FileConfigDetector'а сделать другие (например, HostNameConfigDetector и т.п.), но всё руки не доходят


Заключение


Это далеко не полный список тех «велосипедов», что сделали мы, но в рамках одной статьи рассказать обо всём не могу, потому что боюсь, что читатель элементарно устанет. Разумеется, жду критики/предложений/замечаний. Также, в комментариях хотелось бы узнать мнение читателей на тему «Стоит ли писать дальше?»