Pull to refresh
0
SmartProgress
Сервис постановки и достижения целей

Набор велосипедов Yii разработчика

Reading time6 min
Views11K
От автора

Когда автор пишет пост на хабре, он старается дать читателям максимально полную и полезную информацию по теме. А вот если правильного ответа или решения нет? Тогда этот пост просто пища для ума, а ценность заключается в коллективном разуме.

Зашел я из далека, не спорю, но надеюсь на ваше понимание и поддержку. Озвучивая решения повседневных проблем разработчика, в частности на фреймворке Yii, я предлагаю решение нашей команды. При этом, интересуют идеи сообщества. Ну, довольно пудрить вам мозги.
Вперёд.


Модно, молодёжно, trait'ы


Бывают такие ситуации, когда модель выполняет метод N(), и при этом возвращает true / false и это прекрасно. Но обычно пользователю не понятно почему произошла ошибка и как жить с этим дальше, нужны подробности. Хорошо, если логика простая и вы не перфекционист — вынес чуть-чуть бизнес логики в контроллер и вывел ошибки с подробностями — но мы то не такие!
А если метод может вывести до 20 разных ошибок, почему Вася не может купить пирожок или запостить комментарий.
В Yii есть прекрасный метод validate() у модели, но он точно завязан на валидации данных самой модели и не подходит, если вы создали абстрактный метод не связанный напрямую с моделью.

Как же быть?
А вот так
trait CustomError {
    private $_errorMessages = [];

    /**
     * Use in method :  return $this->setCustomErrorMessage(message);
     *
     * @param array $errorMessages
     * @return false
     */
    public function setCustomErrorMessage($errorMessages)
    {
        if(!is_array($errorMessages))
            $errorMessages = [$errorMessages];

        $this->_errorMessages = $errorMessages;

        return false;
    }

    /**
     * @param string $errorMessage
     */
    public function addCustomErrorMessage($errorMessage)
    {
        $this->_errorMessages[] = $errorMessage;
    }

    /**
     * @return array
     */
    public function getCustomErrorMessages()
    {
        return $this->_errorMessages;
    }

    /**
     * @return mixed
     */
    public function getCustomErrorMessageFirst()
    {
        return reset($this->_errorMessages);
    }

    /**
     * @return void
     */
    public function clearCustomErrorMessages()
    {
        $this->_errorMessages = [];
        return;
    }
}


Простой код как 5 копеек, но очень упрощает жизнь. Пример:
class Blog extends CActiveRecord {

    use CustomError; // Подключаем наш трайт

    public function checkPrivacyCreate() { // Проверяем, может ли пользователь написать коммент
           ...
            $parent_post = $this->getPost($this->parent_post_id);
            if (empty($parent_post))
                return $this->setCustomErrorMessage(Yii::t('blog', 'post_not_found'));
           ...
        return true;
    }
}

// использование в контроллере
public function actionAddPost() {
      ....
      if (!$model->addPost())
                Tools::jsonError($model->getCustomErrorMessages()); // выводим в JSON формате ошибку
      ...
}


Что же мы получаем на выходе? Мы получаем адекватные методы, которые возвращают bool значение, а не винегрет возможных ответов, от int до string. Никакого дублирование кода, чистый DRY. Хотя нет, уверен, что умные люди придумают вариант почище, ну что же, было бы здорово!

Долой модные штуки, только консоль, только хардкор!


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

О чём это, а да, о том, что у нас аж 6 баз данных. По две на каждый этап, рабочая и тестовая. Сказать, что мы молимся на миграции — ничего не сказать. Но вот незадача, Yii migrate команда не предлагает никакого адекватного решения для такого зоопарка баз. Да, через ключи вы можете указать нужное соединение, но делать это каждый раз долго, нудно, ЛЕНЬ (лень это то чувство, вызывающее симпатию у программистов даже больше, чем мужская солидарность. Что уж тут говорить, этот пост рожден в объятиях этой жрицы программисткого искусства)

Ох и потянуло меня, давайте как все любят, бац бац и…
решение
<?php
Yii::import('system.cli.commands.MigrateCommand');
class MigratecomboCommand extends MigrateCommand {
    public $connections = array('db', 'db_test'); // Название компонентов коннектов из вашего конфига

    public function actionUp($args)
    {
        if(($migrations=$this->getNewMigrations())===array())
        {
            echo "No new migration found. Your system is up-to-date.\n";
            return 0;
        }

        $total=count($migrations);
        $step=isset($args[0]) ? (int)$args[0] : 0;
        if($step>0)
            $migrations=array_slice($migrations,0,$step);

        $n=count($migrations);
        if($n===$total)
            echo "Total $n new ".($n===1 ? 'migration':'migrations')." to be applied:\n";
        else
            echo "Total $n out of $total new ".($total===1 ? 'migration':'migrations')." to be applied:\n";

        foreach($migrations as $migration)
            echo "    $migration\n";
        echo "\n";

        if($this->confirm('Apply the above '.($n===1 ? 'migration':'migrations')."?"))
        {
            foreach($migrations as $migration)
            {
                foreach($this->connections as $connectionId) { // !!! Вся магия здесь, мы прогоняем миграцию по всем конектам
                    $this->connectionID = $connectionId;
                    if($this->migrateUp($migration)===false)
                    {
                        echo "\nMigration failed. All later migrations are canceled.\n";
                        return 2;
                    }
                }
            }
            echo "\nMigrated up successfully.\n";
        }
    }

    public function actionDown($args)
    {
        $step=isset($args[0]) ? (int)$args[0] : 1;
        if($step<1)
        {
            echo "Error: The step parameter must be greater than 0.\n";
            return 1;
        }

        if(($migrations=$this->getMigrationHistory($step))===array())
        {
            echo "No migration has been done before.\n";
            return 0;
        }
        $migrations=array_keys($migrations);

        $n=count($migrations);
        echo "Total $n ".($n===1 ? 'migration':'migrations')." to be reverted:\n";
        foreach($migrations as $migration)
            echo "    $migration\n";
        echo "\n";

        if($this->confirm('Revert the above '.($n===1 ? 'migration':'migrations')."?"))
        {
            foreach($migrations as $migration)
            {
                foreach($this->connections as $connectionId) {
                    $this->connectionID = $connectionId;
                    if($this->migrateDown($migration)===false)
                    {
                        echo "\nMigration failed. All later migrations are canceled.\n";
                        return 2;
                    }
                }
            }
            echo "\nMigrated down successfully.\n";
        }
    }

    private $_db;
    protected function getDbConnection()
    {
        if(($this->_db=Yii::app()->getComponent($this->connectionID)) instanceof CDbConnection)
            return $this->_db;

        echo "Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n";
        exit(1);
    }
}


Немного поясню, в методе Up/Down мы проходимся в цикле по всем коннектам и по очереди применяем нашу миграцию к каждой базе.
Решение элементарное до нельзя. По моему даже где то подсмотренное, каюсь. Но теперь, достаточно одной команды, которую можно выполнить даже в пятницу вечером будучи в «абстрактном» состоянии.
yiic migratecombo up(/down/create/...)

И ваши миграции применяться ко всем существующим базам, указанным в переменной $connections.

Но есть нюансы. Если вы решите, как то по хитрому выполнить миграцию, не используя стандартные методы Yii, а напрямую через базу, то:
class m140317_060002_fill_search_column extends CDbMigration
{
	public function up()
	{
        $goals = $this->getDbConnection() // Обратите внимание, вместо Yii::app()->db->createCommand... мы используем $this->getDbConnection()
            ->createCommand("SELECT id, `name` FROM goals WHERE `moderated` != 'deleted'")
            ->queryAll();


Тестирование, юнит, функциональное, на кроликах


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

В функциональном тестирование первое, с чем я столкнулся, это то, что почти все функции сайта доступны только авторизированным пользователям, а как известно окружение для каждого теста девственно чисто.
Мы и это решили
class WebTestCase extends CWebTestCase
{
        public $loginRequired = false;

        protected function setUp()
	{
		parent::setUp();
                $this->setBrowser('*firefox');
		$this->setBrowserUrl(TEST_BASE_URL);
                $this->prepareTestSession();
        
                if($this->loginRequired) {
                    $this->login();
                }
	}
}


Код метода логина приводить не буду, там всё сугубо индивидуально. Теперь достаточно в классе теста указать loginRequired = true и ваш тест будет выполнять авторизированный по всем правилам пользователь.

Немогу не посоветовать молодым и неопытным тестировщикам как я, замечательный инструмент Faker для генерации фиктивных, но максимально реалистичных данных. Незаменимая вещь для DataProvider
Маленький пример
class MyTest extends CDbTestCase
{
     public function newUserProvider() { // генерим 3 случайных набора данных
        $faker = \Faker\Factory::create('ru_RU');
        $array = array();

        for($i=0; $i<3; $i++) {
            $array[$i]['user']['name'] = $faker->name;
            $array[$i]['user']['address'] = $faker->address;
            $array[$i]['user']['country'] = $faker->country;
        }
        return $array;
    }

   /**
     * @param $user
     * @dataProvider newUserProvider
     */
    public function testCreate($user) // Этот тест выполнится 3 раза и каждый раз с разными данными
    {
         $model = new User('signup');
         $model->name = $user['name'];
         ...
         $model->save()
    }

}



Конечно, это не все хитрости и плюшки, которые мы родили за долгий период разработки Smartprogress.
Есть еще много решение и улучшений, но я бы хотел попросить вас, дорогие читатели, поделиться своими мыслями и наработками по теме. Наверняка у каждого разработчика есть настоящий зоопарк хелперов и готовых решение для самых разных задач.
Надеюсь вы поделитесь ими со мной и всем сообществом habrahabr.
Tags:
Hubs:
0
Comments9

Articles

Change theme settings

Information

Website
smartprogress.do
Registered
Founded
Employees
2–10 employees
Location
Россия