Введение
В любой 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));
}
$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
Реализация
Давно еще есть идея помимо FileConfigDetector'а сделать другие (например, HostNameConfigDetector и т.п.), но всё руки не доходят
Класс 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;
}
}
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 и т.п.), но всё руки не доходят
Заключение
Это далеко не полный список тех «велосипедов», что сделали мы, но в рамках одной статьи рассказать обо всём не могу, потому что боюсь, что читатель элементарно устанет. Разумеется, жду критики/предложений/замечаний. Также, в комментариях хотелось бы узнать мнение читателей на тему «Стоит ли писать дальше?»