Страницы

Поиск по вопросам

Показаны сообщения с ярлыком yii2. Показать все сообщения
Показаны сообщения с ярлыком yii2. Показать все сообщения

четверг, 9 апреля 2020 г.

Yii2, файл вне web, скачать его

#php #yii2 #apache2

                    
В проекте на Yii2, я загружаю через форму файлы в папку files, находящуюся в корне
проекта, а не в папке web. 

Как вывести ссылки на эти файлы в view, чтобы их можно было скачивать, как-то динамически?
    


Ответы

Ответ 1



У меня нет под рукой ни php, ни Yii2, поэтому ограничусь подсказками, а не готовым решением из разряда "скопируй и выполни". Вам нужен некий контролер, назовем его FilesController: class FilesController extends Controller { // ... } Внутри него будут два action'а - actionIndex и actionDownload. Первый будет служить для показа списка файлов, а второй для непосредственной выдачи содержимого файла пользователю. Так же нам понадобится некое свойство, которое будет содержать путь к папке с файлами. В Yii2 путь к корневой папке находится в Yii::$app->basePath. Соответственно, пишем в методе инициализации контролера: /** * @var string */ private $path; public function init() { $this->path = realpath(Yii::$app->basePath . '/files/'); } В actionIndex получаем список файлов и передаём его в view: public function actionIndex() { $files = array(); foreach (new DirectoryIterator($this->path) as $item) { array_push($files, $item->getFilename()); } return $this->render('files', array( 'files' => $files )); } В view вам нужно будет вывести список ссылок указывающих на actionDownload. Url для каждого файла можно получить примерно так: Url::to(['download','name'=> $fileName]); Ну и собственно в actionDownload отдаём файл(для $name желательно добавить валидацию, что бы пользователи не присылали всякую ересь): public function actionDownload($name) { return Yii::$app->response->sendFile($this->path . $name); }

Ответ 2



Как я понял вопрос, вы загружаете файлы - ресурсы для пользователей сайты(документы, картинки ит.д.). То вам следует их загружать в папку web - чтобы они были доступны для просмотра и скачивания. Папка assets - нужна для ресурсов относящихся к frontend части сайта(скрипты, стили, иконки и т.д.)

Ответ 3



https://habrahabr.ru/post/151795/ Можно просто сообщить nginx что бы он отдал файл из закрытой директории. function file_force_download($file) { if (file_exists($file)) { header('X-Accel-Redirect: ' . $file); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); exit; } }

Ответ 4



В Yii из коробки есть только два варианта - файлы видны (папка web) и файлы не видны (любая другая папка). Соответственно, без внесения своих изменений непосредственно во фреймворк, единственное, что можно сделать - это переносить нужные файлы из невидимой папки в web путем подключения их как ресурса (Asset). Что в итоге все равно соберет (продублирует) все файлы в папку web. Так что, это бессмысленно.

пятница, 13 марта 2020 г.

Yii2 CacheSession создает сессию в $_SESSION и в кеше одновременно

#php #yii2


Случайно заметил такой нюанс при использования хранилища сессий в кеше ( в моем случае
memcached ) Yii2 создает одновременно ее же и в массиве $_SESSION, хотя по логике не
должен его использовать если в настройках указывается другое хранилище

Конфиг: ( пример - http://www.yiiframework.com/doc-2.0/yii-web-cachesession.html )

'cache' => [
        'class' => 'yii\caching\MemCache',
        'useMemcached' => TRUE,
        'servers' => [
            [   
               'host' => '******/.system/memcache/socket',
               'port' => 11211,
               'weight' => 64,
           ],

       ]
    ],
    'session' => [
        'class' => 'yii\web\CacheSession',
       // 'cache' => 'cache',
    ],


Создание сессии:

    $session = Yii::$app->session;
    $userDataStorage = [
        'id'        => $this->id,
        'name'      => $this->name,
        'settings'  => json_decode($this->settings),          
    ];
    $session->set('user_data' , $userDataStorage);


Проверяем:

        echo 'Yii session -> ';
            var_dump(Yii::$app->session->get('user_data')  );
            echo '------------- 
'; echo 'Php session -> '; var_dump($_SESSION); echo '-------------
'; var_dump(ini_get('session.save_path')); Видим: Yii session -> array(4) { ["id"]=> int(1) ["name"]=> string(5) "admin" ["settings"]=> NULL } ------------- Php session -> array(3) { ["__flash"]=> array(0) { } ["__id"]=> int(1) ["user_data"]=> array(4) { ["id"]=> int(1) ["name"]=> string(5) "admin" ["settings"]=> NULL } } ------------- string(24) "****/.system/tmp" Собственно это баг или... ? В чем тогда смысль настройки хранения сессий в кеше если он дублируется со стандартным ? Или я что-то делаю не так? СОРРИ не доглядел. https://github.com/yiisoft/yii2/blob/master/framework/web/Session.php методом registerSessionHandler() устанавливает session_set_save_handler а класс "CacheSession" реализует методы которые будут хранить сессию в кеше определенном в настройках, т.о. $_SESSION хранится уже не в файле папки tmp (как по умолчанию) а в memcached. Вопрос закрыт.


Ответы

Ответ 1



Возможно будет кому-то полезно, тогда начну сначала. Как настроить кеширование Yii2 для работы с memcache или memcached ( в чем отличия я думаю тут описывать нет смысла, в нете масса инфы по этому поводу ), постараюсь кратко изложить саму суть. Итак, для начала заходим в файл конфигурации проэкта и добавляем настройки кеширования и подключения к серверу 'components' => [ ... 'cache' => [ 'class' => 'yii\caching\MemCache', 'useMemcached' => TRUE, 'servers' => [ [ 'host' => 'localhost', 'port' => 11211, 'weight' => 64, ], ] ], ... ] Параметр 'useMemcached' ( класс MemCache расположен в директории yii2/caching/MemCache.php ( наследуется от компонента Cache /yii2/caching/Cache.php ) и по умолчанию FALSE, т.е. юзает memcache ) указывает на то что будет использоватся memcache или memcached, для использования memcached ставим в конфиге 'useMemcached' => TRUE Далее если все верно настроено прописано можем юзать кеш \Yii::$app->cache->set('key', 'value'); \Yii::$app->cache->get('key'); //или можем кешировать запросы в БД $db = Yii::$app->db; $result = $db->cache(function ($db) { return $db->createCommand('SELECT * FROM tablename LIMIT 0,1000')->queryAll(); }); // и т.д. **** Подробнее в доке http://www.yiiframework.com/doc-2.0/guide-caching-data.html Тут сразу нужно оговорится если вы захотите проверить работу Yii2 memcached напрямую, т.е. в Yii2 \Yii::$app->cache->set('mykey', 'value'); в произвольном файле $m = new Memcached(); $m->addServer('localhost', 11211); var_dump($m->get('mykey')); У вас ничего не получится т.к. компонент Cache Yii ( /yii2/caching/Cache.php ) методом buildKey так сказать "нормализует" ключи ( описание метода есть в классе ), но можно для теста сделать так в Yii2 $cache = Yii::$app->cache; $keyStr = 'cache_test'; $key = $cache->buildKey($keyStr); $cache->set($keyStr, 'This is mem test!'); $get = $cache->get($keyStr); var_dump($get , $key); // дампим результат из кеша и сгенерированный ключ ( копируем его ) в произвольном файле $m = new Memcached(); $m->addServer('localhost', 11211); $result = $m->get('242c9254b0a4b5ea5c7f0e000f668408'); // Вставляем полученый ключ var_dump(unserialize($result)); Далее собственно ближе к сути, мы хотим хранить сесии в нашем мемкеше, для этого идем обратно в конфиг и добавляем после настроек кеша следующее 'session' => [ 'class' => 'yii\web\CacheSession', //'cache' => 'cache', ], Теперь для работы с сессией будет заюзан класс CacheSession ( /yii2/web/CacheSession.php наследуется от Session /yii2/web/Session.php ) и можно создать нашу сессию \Yii::$app->session->set('Mysession', 'Mem session'); И собственно тут у меня возник некий конфуз, ибо \Yii::$app->session->get('Mysession'); // Вернет нам значение сессии $_SESSION // К моему удивлению так же содержал в себе эту сессию Но на самом деле ничего страшного здесь нет, $_SESSION уже не хранится на сервере в папке tmp как по умолчанию, теперь $_SESSION хранится в мемкешед. Все дело в том что класс CacheSession переопределяет некоторые методы класса Session ( в подробности работы вдаватся не буду, можно впринципе пройтись по этим файлам и посмотреть ) в т.ч. getUseCustomStorage(), так же класс Session имеет метод registerSessionHandler() который вызывается при откритии сессии, метод open(), в Session и вызывает в этом случаи getUseCustomStorage() который был переопределен, собственно после этого и происходит магия переноса $_SESSION в мемкешед, функция session_set_save_handler() ( http://php.net/manual/ru/function.session-set-save-handler.php ) переопределит методы работы с сессией на пользовательские. После этого если мы сделаем так $_SESSION['mysession'] = 'value' ( так же как и в методе Session set() ), после открытия сесиии медодом Session open(), сессия будет хранится в нашем кеше. Надеюсь мои изысканя будут кому-то полезными

воскресенье, 8 марта 2020 г.

Запрет повторного голосования

#mysql #база_данных #yii2


Здравствуйте. Сделал голосовалку и появилась необходимость запретить повторное голосование.
Решил через id пользователя. Для этого создал таблицу и в ней два поля user_id и poll_id.
То есть при заходе в голосование ищем в таблице все id текущего голосования, берем
id текущего пользователя и если есть совпадения - в данном голосовании уже был этот
пользователь, то запрещаем ему голосовать здесь, если id пользователя не совпадает
ни с одним id голосования, то даем ему проголосовать и записываем в таблицу id голосования
и id пользователя.
Скажите пожалуйста я правильно мыслю ? Или нагородил фигни ?
И эта таблица ведь должна быть связана с таблицами user и poll ?
    


Ответы

Ответ 1



Ваше решение правильное и адекватное. Добавьте уникальный составной ключ на таблицу (user_id, poll_id), тогда база данных и сама не допустит одинаковых записей в БД и следовательно нескольких голосов одного пользователя по одному голосованию. Внешние ключи также желательно добавить на таблицы user и poll. Перед голосованием делайте запрос на данную таблицу по уникальному составному ключу и при наличии записи делайте запрет голосования, также данную выбранную запись можно записывать в кеш, чтобы лишний раз не обращаться к таблице. Возможно, но не обязательно, для дополнительной информации добавить туда дату и время, когда пользователь проголосовал, здесь будет видна статистика голосований, а в будущем Вы сможете добавить различный функционал, связанный с ограничением голосования по времени или количеству голосов за определенный интервал.

Ответ 2



Вроде получилось, вот код контроллера: public function actionPoll($id, $user_id, $poll_id) { $single = Poll::getOne($id); $getdata = PollUser::getData($user_id, $poll_id); $u = Yii::$app->user->id; if ($u != $getdata->user_id && $id != $getdata->poll_id) { if ($single->load(Yii::$app->request->post())) { if ($single->count1) { $single->updateCounters(['count1' => 1]); } if ($single->count2) { $single->updateCounters(['count2' => 1]); } if ($single->count3) { $single->updateCounters(['count3' => 1]); } Yii::$app->db->createCommand("INSERT INTO poll_user (user_id, poll_id) VALUES ($u, $id)")->execute(); return $this->redirect("/site/poll/?id=$single->id&user_id=$u&poll_id=$single->id"); } } else { return $this->render('pollonlyresult', ['single' => $single, 'getdata' => $getdata]); } return $this->render('poll', ['single' => $single, 'getdata' => $getdata]); } Берем голосование. Берем данные из проверочной таблицы. Получаем id залогиненого юзера - проверяем на совпадения в проверочной таблице - если нет совпадений, то голосуем и записываем текущего юзера и голосование в таблицу. Редиректим на страницу с результатами. Если же есть совпадения, то сразу редиректим на страницу с результатами. Вроде бы так, большое спасибо всем кто ответил.

суббота, 7 марта 2020 г.

Где хранить нетипичные файлы

#yii2 #yii


Вот, допустим, есть у меня php-файлик c самонаписанными функциями, и я хочу его везде
подключать.
В какой каталог его класть по правилам этого фреймворка?
    


Ответы

Ответ 1



Хранить кастомный файл можно где угодно. Нужно всего лишь работать с пространствами имен и подключать где нужно. Для примера создадим файл Person.php в папке helpers в корневой директории проекта. namespace app\helpers; //Указываем пространство имен для подключения в будущем class Person { public static function showName($name) { return "Привет $name"; } } Для того чтобы подключить выше созданный класс нужно сделать следующее: use app\helpers\Person; //используем пространства имен echo Person::showName("Urmuz"); //Используем статичный метод класса Person

Ответ 2



Смотря что там за функции. Можно оформить как behavior или как trait или это helper. Можно вынести на github и подключать через компосер, соответственно попадет в vendor или же закинуть в common в зависимости common/behavior common/trait common/helpers

понедельник, 24 февраля 2020 г.

Как вывести вместо числа название категории Yii2? GridView

#php #yii2 #gridview


Имеется GridView, но категории выведены в виде цифр. Как вывести названия из другой
таблицы - категорий вместо этих цифр по связи Category_id-Title? Очень трудно дается Yii2.



Что мне нужно прописать в коде? Я пробовал по-разному, и тут брал, и тут. Но всё
оканчивалось ошибками. Спасибо за помощь!

    


Ответы

Ответ 1



[ 'attribute' => 'category_id', 'filter' => Html::activeDropDownList( $searchModel, 'category_id', ArrayHelper::map(Category::find()->all(), 'id', 'name'), ['class' => 'form-control'] ), 'value' => 'category.name', ] Вот такой метод ещё работает, на фильтр не обращайте внимания, я про 'value' => 'category.name'. Не знаю красивее ли это, чем в предыдущем варианте, но я сам новенький. Работает так, подскажет может кто из бывалых в чем разница?

Ответ 2



Требуется для начала определить связи в модели, например, у Вас есть модели для таблиц Category и Data, тогда в модели category будет такой метод: public function getData() { return $this->hasOne(Data::className(), ['id' => 'data_id']); } Подробнее тут расжёвано про Active Record. Далее, мы можем непосредственно в GridView настроить собственный столбец: $dataProvider, 'filterModel' => $searchModel, 'columns' => [ [ 'attribute' => 'id', 'value' => function ($model, $key, $index, $widget) { return $model->data->title; }, 'format' => 'raw', ], ], ]); Возможно, я что-то и упустил из виду или не совсем верно понял, что вы хотите добиться, но советую почитать документацию на гитхабе, где всё довольно хорошо расжёвано. Успехов в изучении.

Установка yii2 + base template на локальный компьютер Linux/ubuntu

#php #yii2


Никогда не имел дело с фреймворком yii2. Скачал весь сайт по фтп + база к нему.  

Файлы скинул в /var/www/html - однако ничеого не заработало.
При переходе на localhost/index.php - выхлоп ошибки.
Подключение к бд сделал вроде правильно.
Есть ли какие либо нюансы при портировании сайта на yii2 на локальную машину?
Выхлоп  

//exit();

//Main config file with all settings and autoloads
require_once "config/autoload.php";

//Set 1 to see the work time and sql queries
$debug = new Debug(0); 

//Main object of site also contains all modules objects
$mv = new Builder();

$products_delete = isset($_SESSION['shop']['cart']) && $_SESSION['shop']['cart']
? $_SESSION['shop']['cart'] : "";

if ($products_delete && is_array($products_delete))
{
    $products_delete = $mv -> products -> select(array("id->in" => implode(",",array_keys($products_delete))));
    $mv -> cart -> deleteProducts($products_delete);
}

//Router refers to include needed view to display the page
include_once $mv -> router -> defineRoute();

//If 1 was passed above, displays the data
$debug -> displayInfo($mv -> router);

    


Ответы

Ответ 1



Для начала, вам следует почитать документацию о том, как настроить виртуальный хост в Линуксе. Любой виртуальный хост. А только после этого пытаться примерять фреймворки и прочее)) Попробую описать под Ubuntu 14.04 как это сделать. Бежим вот сюда:http://www.yiiframework.com/download/#yii2 качаем Yii 2 with basic application template - это базовый шаблон с папкой vendor(сам фремворк) Идем в папку /var/www создаем директорию, к примеру yii2-app и в нее кидаем все, что находится в скачанной папке yii-basic-app-2.0.7/basic. В итоге в /var/www/yii2-app должны находится папки assets,config,vendor, web и прочее Идем в /etc/apache2/apache2.conf и добавляем (если нет таких строк): ServerName localhost ## Для того, чтобы Apache интерпретировал php и не предлагал сохранить php-файл AddType application/x-httpd-php .php .phtml ## Установка кодировки UTF-8 по умолчанию AddDefaultCharset UTF-8 # Идем в /etc/apache2/sites-available/ создаем конфигурационный файл yii2-app.conf, прописываем в него то, что под спойлером: ServerName yii2-app ServerAdmin webmaster@localhost DocumentRoot /var/www/yii2-app Options Indexes FollowSymlinks AllowOverride All Require all granted ErrorLog /var/www/yii2-app/logs/error.log CustomLog /var/www/yii2-app/logs/access.log combined Создаем паку logs в папке /var/www/yii2-app и файлы error.log и access.log Теперь надо включить сайт sites-enabled.... для этого просто создаем сим линк конфига из sites-available.... Для этого в терминале можно написать: ln -s откуда_копируем куда_копируем. В нашем случае это: sudo ln -s /etc/apache2/sites-available/yii2-app.conf /etc/apache2/sites-enabled/yii2-app.conf Открываем файл /etc/hosts, прописываем 127.0.0.1 localhost 127.0.0.1 yii2-app Открываем терминал, включаем mod rewrite апачу и перезапускаем его: sudo a2enmod rewrite sudo service apache2 restart Как минимум уже по адресу http://yii2-app должны появится папки. Если нет - значит надо настроить права у папок. Очень опасно, но можно выполнить в консоли chmod 777 /var/www/yii2-app -R что позволит читать и писать и выполнять все файлы всем кому не лень. Так делать нельзя! Хоть и увидите свои папки. Просто это для демонстрации того, что все появилось и надо лишь настроить права для папок. В /var/www/yii2-app/config/web.php необходимо задать значение для ключа cookieValidationKey И по крайней мере по адресу http://yii2-app/web уже будет что-то видно: В целом нужно еще файлы .htaccess Один .htaccess в корне /var/www/yii2-app с настройками: Options +FollowSymlinks RewriteEngine On RewriteCond %{REQUEST_URI} ^/.* RewriteRule ^(.*)$ web/$1 [L] RewriteCond %{REQUEST_URI} !^/web/ RewriteCond %{REQUEST_FILENAME} !-f [OR] RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^.*$ web/index.php И один .htaccess в папке /var/www/yii2-app/web с содержимым: RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.php И уже остальное настраивать в конфигурационных файлах... Должно работать

Ответ 2



В Yii2 корневая папка - web. А у вас корневая сайта /var/www/html. Изменить на /var/www/html/web Пример привел для Basic приложения. Но суть не меняется.

среда, 12 февраля 2020 г.

Кросс доменная авторизация на yii2

#php #yii2 #framework


Всем привет!
Возникла такая проблема, нужно реализовать кросс доменную авторизацию.
Схема такая:
1. На первом домене(user.mysite.com) находится личный кабинет пользователя который
работает на yii2 и через который будет проходить авторизация.
2. На втором домене(mysite.com) находится интернет магазин на "самописном" движке
который должен работать с авторизированным пользователем. 
 
3. БД у них одна.
4. Лежат на одном сервере, в соседних каталогах.
5. должна быть реализована опция "запомнить меня"

Подскажите, можно ли это заставить работать? И как это лучше сделать?
    


Ответы

Ответ 1



В конфиге 'user' => [ 'identityClass' => 'common\models\User', 'enableAutoLogin' => true, 'identityCookie' => [ 'name' => '_identity', 'httpOnly' => true, 'domain' => '.' . DOMAIN, ], ], 'session' => [ 'cookieParams' => [ 'domain' => '.' . DOMAIN, 'httpOnly' => true, ], ], В index.php defined('DOMAIN') or define('DOMAIN', 'mysite.com'); Куки будут общими для домена и сабдоменов

Ответ 2



Надо чтобы на обоих доменах совпадали класс аутентификации, вероятно так и есть, если используются родные механизмы Yii; домен, где сохраняется аутентификационная кука. в настройках сайта на под-домене укажите родительский домен; база с пользовательскими аккаунтами. вы сказали, что БД общая - ок. На всякий случай скажу: Не тестируйте это на домене localhost. Браузеры чудят с куками на доменах первого уровня. Если надо испытать локально, заведите в hosts синоним для локалхоста, типа localhost.com. Обратите внимание в настройках на точку перед именем домена, это каноническая форма! См. http://yiiframework.ru/doc/cookbook/ru/install.cookie.subdomains https://stackoverflow.com/questions/29378697/automagically-log-into-multiple-domains-in-yii2

четверг, 23 января 2020 г.

yii2 события в контроллерах

#php #yii2 #события


Есть два модуля - Comments и Pages со своей структурой, т.е. реализация не стандартная
как по умолчанию в Yii2. Соответственно два контроллера 

class CommentsController extends BaseController
{            
    public function actionIndex()
    {
        return $this->render('index');
    }
}

Event::on(PagesController::className(), PagesController::EVENT_GET_COMMENTS, 
function ($event) {
       echo 'comments';
    });


ну и аналогично для Pages

class PagesController extends BaseController
{   
    const EVENT_GET_COMMENTS = '';

    public function actionIndex()
    {
        $this->trigger(self::EVENT_GET_COMMENTS);

        return $this->render('index');
    }
}


В теории при вызове контроллера Pages действия index должно срабатывать событие EVENT_GET_COMMENTS
и выводиться comments, но этого не происходит :(
    


Ответы

Ответ 1



У вас есть 3 способа: №1 (глобально): Создать компонент с необходимой логикой Добавить в конфигурационный файл 'components' => [ 'myComponent' => [ 'class'=>'app\components\MyComponent' ], //other components ] и не забыть про 'bootstrap' => ['log','myComponent'] №2: Унаследовать метод в контроллере public function beforeAction($event) { // ваш код return parent::beforeAction($event); } №3: Создать поведение: http://www.yiiframework.com/doc-2.0/guide-concept-behaviors.html, см. как Добавить написанное поведение в контроллер, например как AccessControl

суббота, 11 января 2020 г.

Убрать “web” из адреса в Yii2

#php #yii2 #apache


На хостинге установлен yii2-basic, установлен не в корень, а в htdocs/basic.  

Хочу http://домен/basic/web/about заменить на http://домен/basic/about.

На данный момент последний адрес перенаправляет на главную страницу.  

В basic лежит следующий .htaccess:


    Options +FollowSymlinks
    RewriteEngine On



    RewriteCond %{REQUEST_URI} ^/.*
    RewriteRule ^(.*)$ web/$1 [L]

    RewriteCond %{REQUEST_URI} !^/web/
    RewriteCond %{REQUEST_FILENAME} !-f [OR]
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^.*$ web/index.php



В basic/web тоже лежит .htaccess:

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php


В config/web.php:

'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'rules' => [
        '' => 'site/index',
        ''=>'site/',
    ],
],


В 'request' добавление 'baseUrl' => '' даёт 404.
    


Ответы

Ответ 1



Сам не раз сталкивался с клиентами, у которых был обычный хостинг, где не было возможности прописать корневую директорию. Вот как я решал данную проблему: 1) В корне проекта создаем .htaccess с содержимым: Options +FollowSymlinks RewriteEngine On RewriteCond %{REQUEST_URI} ^/.* RewriteRule ^(.*)$ basic/web/$1 [L] RewriteCond %{REQUEST_URI} !^/basic/web/ RewriteCond %{REQUEST_FILENAME} !-f [OR] RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^.*$ basic/web/index.php 2) В папке web создаем .htaccess с содержимым: RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.php 3) Далее открываем файл config/web.php и делаем следующее: Находим строчку 'request' => [ 'cookieValidationKey' => ......., ], И добавляем в массив строчку 'baseUrl' => '' 'request' => [ 'cookieValidationKey' => ......., 'baseUrl' => '', ], 4) Заранее забегаю вперед и пишу, как убрать ещё и index.php Находим строчку в файле config/web.php 'urlManager' => [ ........ //остальная часть кода ], И добавляем в массив строчку 'showScriptName' => false 'urlManager' => [ 'showScriptName' => false, ........ //остальная часть кода ], Данная инструкция уместна для структуры папок, указанной Вами. Обычно проект заливают в корень сайта. В таком случае убираем в первом пункте basic и наслаждаемся жизнью :)

пятница, 10 января 2020 г.

Естественная сортировка (natural order) и ActiveRecord в Yii2

#php #сортировка #yii2


Использую Active Record в Yii2, и получаю список в такой последовательности (сортировка
по ASCII, ASCIIbetical order):


Pacific 12°-28° 1200w 80v
Pacific 14°-35° 1200w 80v
Pacific 23°-50° 1200w 80v
Pacific 5.5°-13° 1200w 80v


Однако нормальный пользователь (не программист!) ожидает, что последний из элементов
будет в начале. Естественная сортировка (natural order, сортировка по алфавиту):


Pacific 5.5°-13° 1200w 80v
Pacific 12°-28° 1200w 80v
Pacific 14°-35° 1200w 80v
Pacific 23°-50° 1200w 80v


В PHP возможность использовать естественную сортировку есть по умолчанию (что, кстати,
преимущество языка по сравнению с другими):


функция natsort()
функция sort() с флагом SORT_NATURAL
функция strnatcmp()


Но вот использование фреймворка Yii2, похоже, сводит это преимущество на нет. Я получаю
тот самый "ASCIIbetical order", который нужно как-то пересортировывать. Дополнительная
засада зарыта в виджете GridView, который если и получит отсортированный список, то
при сортировке по клику на ярлык колонки перестроит список опять же как в первом примере.

Есть ли возможность "минимальной кровью" подружить Yii2 и естественную сортировку?

P.S. В указанной выше статье есть чудный фрагмент:


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

    


Ответы

Ответ 1



Неделю я воевал с PHP и MySQL, выясняя нюансы работы с сортировкой. Грабли лежат на каждом шагу. Например, natcasesort() - учитывает регистр, но спотыкается о цифры: Рабочая станция визуализации/файл-сервер системы рабочая станция художника по свету Тубус Pacific 12°-28° Тубус Pacific 14°-35° Тубус Pacific 23°-50° Тубус Pacific 5.5°-13° (элемент списка не на месте) sort($lines, SORT_NATURAL | SORT_FLAG_CASE) - упорядочивает смесь цифр и букв, но спотыкается о регистр, если есть кириллица: Рабочая станция визуализации/файл-сервер системы Тубус Pacific 5.5°-13° Тубус Pacific 12°-28° Тубус Pacific 14°-35° Тубус Pacific 23°-50° рабочая станция художника по свету (элемент не на месте) Важный теоретический момент: MySQL не умеет делать естественную сортировку, а Yii2 получает данные в том порядке, в каком из отдает база данных. Поэтому вместо сортировки по буквенно-цифровому полю стоит использовать сортировку по полю вспомогательному. О реализации этой идеи - ниже. Не буду рассказывать о всех деталях, расскажу о ключевых. Создаем вспомогательное поле (ниже - sequence). В нем будем хранить целые числа (int). Элементы списка пронумерованы сотнями начиная со ста. Это позволит нам добавлять новые элементы не пересчитывая всю последовательность. Со ста начинаем, чтобы можно было добавлять элементы в начало списка и не заморачиваться с отрицательными числами. Аналогичный рецепт есть у Александра Макарова в блоге, только он предлагает хранить последовательность в виде decimal. Этот момент совершенно непринципиален, выбирайте то, что больше нравится. Теперь нам нужно просчитать последовательность для имеющегося списка. Я создал класс CyrrilicHandler, в котором есть несколько методов, связанных с этой задачей: public function upperletters() { return [ 'А','Б','В','Г','Д','Е','Ё','Ж','З','И','Й','К','Л','М','Н','О','П', 'Р','С','Т','У','Ф','Х','Ц','Ч','Ш','Щ','Ъ','Ы','Ь','Э','Ю','Я', ]; } public function lowerletters() { return [ 'а','б','в','г','д','е','ё','ж','з','и','й','к','л','м','н','о','п', 'р','с','т','у','ф','х','ц','ч','ш','щ','ъ','ы','ь','э','ю','я', ]; } // Приводим строку к нижнему регистру public function strtolower($string) { return str_replace(self::upperletters(), self::lowerletters(), $string); } // Приводим строку к верхнему регистру public function strtoupper($string) { return str_replace(self::lowerletters(), self::upperletters(), $string); } // Естественная регистронезависимая сортировка (базовая латиница + кириллица) public function sort($array = []) { $complexArray = []; foreach ($array as $value) { $complexArray[] = [ 'value' => $value, 'lowercase' => self::strtolower($value), ]; } ArrayHelper::multisort($complexArray, ['lowercase'], [SORT_ASC], SORT_NATURAL | SORT_FLAG_CASE); return ArrayHelper::getColumn($complexArray, 'value'); } // Естественная регистронезависимая сортировка с возможностью задать первый // ключ возвращаемого массива и интервал между элементами // Первый элемент - 100, далее - с интервалом 100 public function sortAndSetInitialAndInterval($array = []) { $raw = self::sort($array); $res = []; $key = 100; // начинаем последовательность со ста foreach ($raw as $value) { $res[$key] = $value; $key += 100; // интервал - 100 } return $res; } Обновляем имеющиеся данные: $lines = Line::find() ->select('line') ->orderBy('line') ->column(); // Естественная регистронезависимая сортировка $lines = CyrrilicHandler::sortAndSetInitialAndInterval($lines); $table = Line::tableName(); foreach ($lines as $sequence => $line): $query = "UPDATE {$table} SET sequence = :sequence WHERE line = :line"; Yii::$app->db->createCommand($query) ->bindValue(':sequence', $sequence) ->bindValue(':line', $line) ->execute(); endforeach; // $lines as $sequence => $line Проделывать такую операцию каждый раз не хочется, поэтому я создал метод newPosition(), который возвращает номер в существующей последовательности для заданной строки. Вот этот метод и методы, связанные с ним (CyrrilicHandler): // Поиск позиции нового элемента в последовательности (чтобы не обновлять все записи // последовательности) public function newPosition($string) { // В зависимости от первого символа строки (цифра или нет) получаем разные блоки данных $block = self::isInitialSymbolNumber($string) ? self::findEntriesStartedWithNumber() : self::findEntriesStartedWithSameSymbol($string); if (sizeof($block)) { // Если переданная строка уже есть в БД, возвращаем ее позицию if (array_search($string, $block) !== false) return array_search($string, $block); list($before, $after) = self::analyseBlockWithString($block, $string); } else { list($before, $after) = self::analyseBordersWithString($string); } return $before + floor(($after - $before)/2); } // Проверяем первый символ (цифра или нет) private function isInitialSymbolNumber($string) { return is_numeric(mb_substr($string, 0, 1, 'UTF-8')); } private function findEntriesStartedWithNumber() { return Line::find() ->select('line') ->where("line REGEXP '^[0-9]'") ->orderBy('sequence') ->indexBy('sequence') ->column(); } private function findEntriesStartedWithSameSymbol($string) { return Line::find() ->select('line') ->where(['like', 'line', mb_substr($string, 0, 1, 'UTF-8') . '%', false]) ->orderBy('sequence') ->indexBy('sequence') ->column(); } private function analyseBlockWithString($block, $string) { // Добавляем в блок строку и сортируем, исходный блок остается неизменным $sorted = self::sort(array_merge($block, ['x' => $string])); // Находим номер нового элемента в отсортированной последовательности $eid = array_search($string, $sorted); $before = isset($sorted[$eid-1]) ? array_search($sorted[$eid-1], $block) : false; $after = isset($sorted[$eid+1]) ? array_search($sorted[$eid+1], $block) : false; if ($before && $after) return [$before, $after]; // Если строка на границе - нужны доп. данные из БД if (!$before) $before = self::findBeforeByAfter($after) ?: 0; if (!$after) $after = self::findAfterByBefore($before) ?: array_pop(array_keys($block)) + 100; return [$before, $after]; } private function analyseBordersWithString($string) { $before = self::findPreviousEntryNumber($string) ?: 0; $after = self::findAfterByBefore($before) ?: false; // Если не нашлось ничего ни до ни после (первая запись в БД) if ($before === 0 && !$after) $after = 200; if ($before !== 0 && $after) return [$before, $after]; // Добавляемая строка попадает в конец списка, т.к. интервал 100, то суммируем двойной интервал // (позже разделится на 2) return [$before, $before + 200]; } private function findPreviousEntryNumber($string) { return self::isInitialSymbolNumber($string) ? self::findPreviousStartedWithNumber() : self::findPreviousStartedWithNotNumber($string); } private function findPreviousStartedWithNumber() { return Line::find() ->select('sequence') ->where(['<', 'line', 0]) ->orderBy('sequence DESC') ->limit(1) ->scalar(); } private function findPreviousStartedWithNotNumber($string) { return Line::find() ->select('sequence') ->where(['<', 'line', $string]) ->orderBy('sequence DESC') ->limit(1) ->scalar(); } // Находим следующий номер за указанным private function findAfterByBefore($number) { return Line::find() ->select('sequence') ->where(['>', 'sequence', $number]) ->orderBy('sequence ASC') ->limit(1) ->scalar(); } // Находим номер, предыдущий указанному private function findBeforeByAfter($number) { return Line::find() ->select('sequence') ->where(['<', 'sequence', $number]) ->orderBy('sequence DESC') ->limit(1) ->scalar(); } Чтобы результат стал виден пользователям, указываем поле, по которому будем сортировать данные в параметрах сортировки провайдера данных: [ 'defaultOrder' => [ 'sequence' => SORT_ASC, ], 'attributes' => [ 'line' => [ 'asc' => ['sequence' => SORT_ASC], 'desc' => ['sequence' => SORT_DESC], ], ], ]; Буду рад замечаниям и уточнениям.

Как правильно использовать функцию validateAuthKey() в yii2?

#php #yii #yii2


Я провёл исследования стандартного примера, и выяснил, что функция validateAuthKey()
вообще не вызывается. Для чего она тогда нужна, как её правильно вызвать, как правильно
с ней работать? 

public function validateAuthKey($authKey)
    {
        Yii::info('test ================');
        return $this->authKey === $authKey;
    }

    


Ответы

Ответ 1



validateAuthKey() проверяет $authKey - хэш, который хранится в куках. Вызывается для авторизации с помощью cookie в yii\web\User::loginByCookie при $enableAutoLogin = true protected function loginByCookie() { ... if ($identity->validateAuthKey($authKey)) { if ($this->beforeLogin($identity, true, $duration)) { $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); $ip = Yii::$app->getRequest()->getUserIP(); Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); $this->afterLogin($identity, true, $duration); } } ... }

Ответ 2



Если сессий нет (уничтожены) и при логинировании пользователя мы указывали «remenberMe» (то есть второй параметр метода Yii::$app->user->login(), чтобы кука установилась), то Yii2 попытаеся авторизовать пользователя с помощью куки, вот здесь-то и вызывается этот загадочный метод validateAuthKey() ... Естественно, попытка авторизации будет происходить только в том случае, если мы обращаемся к сущности пользователя, например так: if (!Yii::$app->user->isGuest) { echo Html::a('Выйти', ['logout']); } Это можно проверить, удалив сессии из временной папки сервера (то есть оттуда, где они храняться) и как-нибудь обозначить вызов validateAuthKey(), например так: public function validateAuthKey($authKey) { echo ''; return $this->getAuthKey() === $authKey; } Ну или помидитировать над исходными кодами Yii2. Источники: метод проб и ошибок документация; How to login user from a database in Yii Framework 2

понедельник, 6 января 2020 г.

Вопрос про модель в MVC

#php #yii2 #mvc


Здравствуйте, я смотрел, как делают два разных человека сайт и так и не понял, откуда
брать данные из БД в модели или контроллере? ну, т.е. в модели мы пишем табл из который
будет выборка, это понятно.

Один человек делал так: в модели название табл и все. А выборку он делал уже в контроллере,
например: 

$feeds = Feedback::find()->orderBy('RAND()')->limit(4)->all();


А другой выборку делал в модели, а в контроллере уже обращался к методу того класса,
пример: 

$categories = Category::getCategoriesList();


Как правильно или как лучше и что лучше всего делать в модели?)))
    


Ответы

Ответ 1



Для примера возьмем ваш пример. Выводим $feeds = Feedback::find()->orderBy('RAND()')->limit(4)->all(); в контроллере и все работает. Завтра нужно тоже самое, но в другом контроллере (или методе)? Что тогда? Вот если мы создадим метод в модели, то сможем использовать все это и в других контроллерах. public function getFeeds() { return self::find()->orderBy('RAND()')->limit(4)->all(); } Тут даже дело уже не в самом MVC, а в подходе к гибкости системы. Старайтесь всегда думать о том, что вдруг данный код понадобиться где-то ещё. Я использую помимо моделей (Models) ещё и модели представления (ViewModels). Это очень удобно. Например есть модель Users: class Users extends ActiveRecord { public static function tableName() { return 'users'; } //Behaviors... //Roles... //Relations } В данной модели я храню все методы, касаемо самого Yii (правила,связи, поведения и т.д.). Не засоряю данный класс. Для остальных же методов использую UsersViewModel: class UsersViewModel extends Users { public static function getAllUsers() { return Users::find()->all(); } } Я привел для примера примитивный пример, но важна сама суть. Старайтесь всегда думать на шаг вперед, чтобы потом не возвращаться на 2 шага назад. Лучшие практики MVC. На примере Yii

Ответ 2



Все очень просто. Согласно правилам MVC - контроллер является посредником между view и моделью и по сему модель вызывает контроллер и уже из подгруженной модели передает данные. Сие есть правильно. А в модели нужно описывать связи с другими моделями или поведение этой модели.

пятница, 27 декабря 2019 г.

Yii2 имя текущего контроллера в модуле

#php #yii2 #controller #modules


Есть базовый класс для всех модулей

namespace app\components;

use Yii;

class Module extends \yii\base\Module
{
    public function init()
    {
        parent::init();

        var_dump(Yii::$app->controller->id); // null :(
    }
}


Все модули расширяются от него, т.е.

namespace app\modules\test;

class Module extends \app\components\Module
{
    //
}


Как мне получить название текущего контроллера в базовом классе модуля ? 

Yii::$app->controller->id


возвращает null
    


Ответы

Ответ 1



Ну для начала не Yii::$app->controller->id а Yii::app()->controller->id Тогда он выдаст тебе название контроллера. //UPD Ты вызываешь его из init() модуля. Что может дать тебе этот вызов, если он не значет какой контроллер у тебя сейчас. Он и будет тебе всегда NULL показывать! и если вопрос звучит именно так Как мне получить название текущего контроллера в базовом классе модуля ? то ответ никак. т.к. модуль не знает о контроллере ничего чтобы выдать какую-то информацию.

Ответ 2



Я думаю самым простым решением будет создание еще одного класса от которого будут наследоваться все остальные, в котором можно делать операции с ид контроллеров. class MainController extends yii\base\Controller { // some... public function beforeAction($event) { $ct = $event->controller->id; // current ctrl return parent::beforeAction($event); } } А от него наследовать уже все остальные контроллеры class PostController extends MainController { // do something... }

вторник, 24 декабря 2019 г.

Yii2 и Яндекс Касса

#php #yii2 #яндекс


Три часа борюсь с яндекс кассой. Саппорт говорит  что у меня сайт не принимает POST
запросы и возвращает bad request #400. Сайт на Yii2. Есть предположение что это из
за ckrf токена. Может быть кто то сталкивался и сможет мне помочь?
    


Ответы

Ответ 1



В контроллере реализуйте метод beforeAction таким образом. public function beforeAction($action) { $this->enableCsrfValidation = false; return parent::beforeAction($action); } Если не хотите отключать CSRF для всех действий, то можете отключить для определенных действий: public function beforeAction($action) { if ($action->id === 'test') $this->enableCsrfValidation = false; return parent::beforeAction($action); } В данном случае мы отключили CSRF для действия test

Ответ 2



Для нескольких экшенов: public function beforeAction($action) { $this->enableCsrfValidation = !in_array($action->id, ['check', 'aviso']); return parent::beforeAction($action); }

четверг, 19 декабря 2019 г.

Yii2 подключение сторонней библиотеки

#php #yii2


Я недавно начал изучать Yii2 и столкнулся с такой задачей:

Есть библиотека PhpWord, мне нужно её подключить, я делаю так в коде приложения

use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Autoloader;


Соответственно библиотека лежит в корне приложения в PhpOffice\PhpWord

Пространства имен класса вроде как совпадают, но почему-то класс не загружается PhpWord.

В чем может быть проблема?
    


Ответы

Ответ 1



Установите класс через composer, тем более есть инструкция как это сделать: https://github.com/PHPOffice/PHPWord

Ответ 2



Как вам и посоветовали проще всего подключить библиотеку через composer (думаю что он у вас еже установлен, но на всякий случай - https://getcomposer.org/doc/00-intro.md), в консоле, перейдите в корень и введите строку composer require "phpoffice/phpword":"dev-master" Но бывают случаи когда не можете или не хотите использовать composer, вы можите подключать свои расширения через конфигурацию - http://www.yiiframework.com/doc-2.0/yii-base-application.html#$extensions-detail То есть вам нужно указать данные вашего расширения в конфигурации (к примеру \config\web.php) в ключе extensions. ... 'components'=>[...], 'params' => $params, 'extensions' => [ 'name' => 'extension name', 'version' => 'version number', 'bootstrap' => 'BootstrapClassName', 'alias' => [ '@alias1' => 'to/path1', '@alias2' => 'to/path2', ], ], ... Тут нужно понимать что данная настройка заменяет основной файл фреймворка для расширений - @vendor/yiisoft/extensions.php, так что вы не сможете использовать composer и свои расширений одновременно. Но это можно обойти, для этого, в папке конфигураций, создайте новый файл (назовем его extensions.php) с кодом: use yii\helpers\ArrayHelper; $extensionsDir = dirname(__DIR__). DIRECTORY_SEPARATOR . 'extensions'; $vendoExtension = require_once dirname(__DIR__).'/vendor/yiisoft/extensions.php'; Yii::setAlias('@extensions', $extensionsDir); $extensions = [ 'extension name'=>[ 'name' => 'extension name', 'version' => 'version number', 'bootstrap' => 'BootstrapClassName', // optional, may also be a configuration array 'alias' => [ '@alias1' => 'to/path1', '@alias2' => 'to/path2', ], ] ]; foreach ($extensions as $extension) { if (isset($extension['alias'])) { foreach ($extension['alias'] as $alias => $path) { Yii::setAlias($alias, $path); } } } $merge = ArrayHelper::merge( $extensions, $vendoExtension ); return $merge; Обратите внимание что в массиве $extensions вы будите указывать свои расширения, по аналогии с файлом @vendor/yiisoft/extensions.php, посмотрите в нем как указываются расширения. Для текущего расширения настройка будет что то вроде этого: 'phpoffice/phpword'=>[ 'name'=>'phpoffice/phpword', 'alias'=>[ '@phpoffice/phpword'=>$extensionsDir . '/phpoffice/phpword', ], ] (код не проверял) Так же я использую отдельную папку для расширений которая установлены без composer (папка extensions в корне), путь к ней указана в переменной $extensionsDir, вы можете указать свою. Остается изменить файл конфигурации (для примера web.php) ... 'extensions' => require_once 'extensions.php', ... Далее заливайте свои расширения в папку указанную в $extensionsDir (в моем случае - extensions в корне), и подключайте их в массиве $extensions . Все это использую в базовом шаблоне, в advanced шаблоне может что либо отличатся. Так же нужно понимать что многие расширения тянут за собой зависимые расширения, которые нужны им для работы, так что вам придется загрузить и подключить их все. Но удобно когда разрабатываешь свое расширение или расширяешь функционал для разового использования.

Ответ 3



Для того чтобы использовать классы вы должны подключить их через require. Для того чтобы не делать это каждый раз был разработан механизм автозагрузки. В случае с yii2 вы используете autoload от composer. Вы можете прописать алиас. Yii::setAlias('@PhpOffice', 'ProjectRoot/PhpOffice'); Лучшим вариантом будет выделить PhpOffice как библиотеку и подключать её через composer как ответил @ShapeShifter08.

понедельник, 16 декабря 2019 г.

Как отучить Yii2 удалять cookie?

#php #cookie #yii2


Суть:
Yii2 все куки и сессии удаляет через некоторое время, хотя время жизни 3600*24*30*12
- что подтверждается при просмотре кук в браузере.
Удаляет и PHPSESSID и любые другие на домене

Мой конфиг:

'session' => [
            'class' => 'yii\web\Session',
            'cookieParams' => ['httponly' => true, 'lifetime' => 3600*24*30*12],
            'timeout' => 3600*24*30*12,
            'useCookies' => true,
        ],
'request' => [
            'enableCookieValidation' => false,
            'enableCsrfValidation' => true,
            'cookieValidationKey' => 'секретка',
        ],


Валидацию отключил т.к. пробовал задавать через setcookie напрямую.
Момент удаления отследить не смог, но он наступает через какое-то время после неактивности
клиента, примерно 20 минут.

Так же выяснил что удаляются все куки, если удалить вручную PHPSESSID.
    


Ответы

Ответ 1



Возможно дело не в yii. Такое поведение встречается из-за неверного или отсутствующего значения параметра session.cookie_domain php.ini, который можно установить: session.cookie_domain = ".site.com" Или: ini_set('session.cookie_domain', '.site.com' );

суббота, 14 декабря 2019 г.

Тесты с ActiveRecord: Как разрабатывать быстрые тесты с большим количеством ActiveRecord в проекте

#yii2 #activerecord #юнит_тесты #codeception


Дано: Проект с большим количеством Active Record и большим количеством Foreign Keys
между ними. Сейчас для тестирования в целях независимости тестов друг от друга каждый
тест загружает в базу данных фикстуры.

В случае если тесту требуется пользователь он загрузит в базу данных фикстуру UserFixture
и все зависимые фикстуры.

Проблемы:


Количество зависимостей очень большое, в базу данных грузится не только фикстура
пользователя, но ещё и ещё больше 10 зависимых фикстур.
Такую загрузку в базу требует каждый тест.
Обычно тестам нужен не пользователь, но даже в этом случае для корректной подготовки
БД его необходимо загрузить, так как фикстура пользователя скорее всего будет являеться
зависимостью.


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

Как лучше всего использовать фикстуры для тестов?

Я вижу два варианта:


Загрузить фикстуры в базу данных один раз и в тестах вручную возвращать измененные
значения назад к исходным. (Крайне тяжело, вариант сразу отпадает).
В тестах готовить необходимые объекты Active Record, а базу данных использовать только
для тестов которые вызывают insert или update в базу данных. (Приоритетный вариант,
но не слишком накладно будет перед каждым тестом готовить необходимые модели?)


Как правильно писать тесты в контексте веб-разработки, где большая часть операций
идет с использованием Active Record? Как добиться ускорения работы тестов?

Приведите пример как в вашем проекте организованы тестирование методов, которые используют
Active Record.
    


Ответы

Ответ 1



Это довольно классическая проблема тестирования, и ее решение заключается в выкидывании зависимостей. Вы не должны тестировать базу данных, поэтому никакие фикстуры вам на самом деле не нужны вплоть до того момента, как вы подойдете к системному/интеграционному тестированию сервиса целиком. На уровне модульного тестирования вы тестируете одну конкретную feature с очень узкой областью видимости, где все внешние зависимости можно (и нужно) подменить на моки, которые не только освоодят вас от проблем с БД, но и ускорят тесты. У вас скорее всего уже появился вопрос - что делать с теми местами, в которых напихано штук десять моделей, которые приходят через статические методы. По-хорошему этих мест вообще не должно быть: приложение должно делиться на слои, и слой базы данных должен без проблем подменяться в вышестоящих менеджерах бизнес-логики (хотя саму бизнес-логику в модульных тестах я бы не стал тестировать, отдав это все на откуп более высокоуровневым тестам). Впрочем, с базой данных есть еще одно решение, которое я не устану пиарить - это Docker. Docker позволит вам сохранить базу данных в определенном состоянии и возвращать ее к нему же путем переподнятия контейнера; пул из десятка перезапускаемых контейнеров может предоставлять новую БД практически мгновенно. Впрочем, примерно то же самое можно сделать на основе SQLite.

Ответ 2



Ответ состоит в том, что вы должны использовать моки вместо реального обращения к базе данных в каждом тесте. По сути это второй вариант решения, который вы предложили. На вашем примере: пусть есть метод saveCalculatedPrice, который поручает одному классу посчитать сумму, а другому классу - сохранить ее в бд. Этот метод надо тестировать на моках. Расчет суммы выполняет один класс и это поведение протестировано в тестах этого класса. Сохранение выполняет другой класс и это поведение протестировано в его тестах. Задача функции saveCalculatedPrice поочередено обратиться к этим двум классам, вот эту задачу и надо тестировать. Сделать это можно только с помощью моков, иначе вы неизбежно будете повторно тестировать поведение уже протестированное в других местах и иметь дело с теми проблемами которые вы описываете в вопросе. По поводу накладности написания моков для тестов, вопрос в том как вы делаете моки. Если вы пишете все моки сами, то это действительно очень трудоемко, чтобы требовалось меньше рутинной работы посмотрите в сторону mock-фреймворков, например phake. Вообще, как советуют в соседнем ответе, я бы не тестировал работу с бд в юнит-тестах и оставил бы это на откуп интеграционным тестам. Вместо этого в юнит тестах везде где надо обратиться к реальной базе данных я бы подставлял мок. Когда вы дойдете до написания интеграцинных тестов, вы будете тестировать ваше приложение на более высоком уровне, и вам скорее всего не потребуется готовить бд отдельно для каждого теста. Вместо этого лучше организовать тестирование следующим образом: создать тестовую бд поочередно выполнить на ней все интеграционные тесты То есть интеграционные тесты будут выполняться в среде приближенной к реальной среде, в которой выполняется приложение.

воскресенье, 8 декабря 2019 г.

Как использовать Sphinx в Yii2 + PostgreSQL?

#php #yii2 #postgresql #yii #sphinx


Не до конца понимаю, возможно ли использование Sphinxsearch в Yii2 в качестве поискового
движка в связке с PostgreSQL? В экстеншене: Yii2-sphinx говорится про MySQL, но про
PgSQL ни слова. Сам экстеншен устанавливается, и конфиг вида:

'components' => [
    'sphinx' => [
        'class' => 'yii\sphinx\Connection',
        'dsn' => 'pgsql:host=localhost;port=5432;dbname=testdb',
        'username' => 'yii',
        'password' => '********',
    ],
]


ошибок не вызывает, даже при отсутствии Sphinx на сервере.
В связи с вышеизложенным возникает ряд вопросов:


Подходит ли данный экстеншен для использования его с PgSQL. Если да, где про это
можно почитать, или посмотреть реальные примеры (Yii2 + PgSQL + Sphinx)?
Стоит ли использовать его, или лучше что-то другое, вроде: pg-sphinx?
Стоит ли вообще его использовать, быть может есть решения быстрее и проще?


Стэк: Debian, Nginx, FPM, PHP7, Yii2, PgSQL

UPD Вопрос всё ещё актуален! :)
    


Ответы

Ответ 1



Первая фраза по ссылке yii2-sphinx в блоке Configuration, говорит о том, что используется только протокол MySQL: This extension interacts with Sphinx search daemon using MySQL protocol and SphinxQL query language. По идее, Вы можете попробовать добавить 'pgsql' => 'yii\sphinx\Schema' в массив $schemaMap, в файле Connection.php (в основной директории модуля), и, скорее всего простые запросы даже будут отрабатывать, но, как только будет более-менее сложный запрос, гарантирую, что Вы получите ошибку. Лучшим вариантом будет использование расширения pg-sphinx или похожее, если Вам действительно нужен Sphinx. В противном случае, попробуйте обычный полнотекстовый поиск, возможно, для ваших целей хватит и его.

websocket выключается из-за простоя PHP

#php #javascript #yii2 #websocket


Возникли проблемы с веб-сокетом. Работает отлично, все супер, но если сокетом никто
не пользуется больше 1 минуты, он выключается. Спустя минуту простоя я получаю "CloseEvent"
c кодом "1000" на любой запрос. Когда подключен хотя бы один пользователь проблема
решается пинг-понгом, но когда нет ни одного пользователя сокет висит без дела. Вроде
уже указал все конфигурации связанные с timeout-ом, но не помогло.

Сокет реализован с помощью библиотеки: https://github.com/pmill/php-chat

Проверял на Open Server. Используется Apache в связке с Nginx.
    


Ответы

Ответ 1



Есть 2 способа решения этой проблемы: 1) Чтобы сервер пинговал каждые 30сек (любой интервал, меньше 1 минуты) 2) Реализовать функцию reconnect на клиенте, в случае обрыва соединения или reconnect по интервалу. Вот примерная реализация reconnect. Ты так же можешь использовать другие библиотеки, которые делают reconnect под капотом. const WebSocket = require('ws'); function WebSocketClient(){ this.number = 0; // Message number this.autoReconnectInterval = 5*1000; // ms } WebSocketClient.prototype.open = function(url){ this.url = url; this.instance = new WebSocket(this.url); this.instance.on('open',()=>{ this.onopen(); }); this.instance.on('message',(data,flags)=>{ this.number ++; this.onmessage(data,flags,this.number); }); this.instance.on('close',(e)=>{ switch (e.code){ case 1000: // CLOSE_NORMAL console.log("WebSocket: closed"); break; default: // Abnormal closure this.reconnect(e); break; } this.onclose(e); }); this.instance.on('error',(e)=>{ switch (e.code){ case 'ECONNREFUSED': this.reconnect(e); break; default: this.onerror(e); break; } }); } WebSocketClient.prototype.send = function(data,option){ try{ this.instance.send(data,option); }catch (e){ this.instance.emit('error',e); } } WebSocketClient.prototype.reconnect = function(e){ console.log(`WebSocketClient: retry in ${this.autoReconnectInterval}ms`,e); this.instance.removeAllListeners(); var that = this; setTimeout(function(){ console.log("WebSocketClient: reconnecting..."); that.open(that.url); },this.autoReconnectInterval); } WebSocketClient.prototype.onopen = function(e){ console.log("WebSocketClient: open",arguments); } WebSocketClient.prototype.onmessage = function(data,flags,number){ console.log("WebSocketClient: message",arguments); } WebSocketClient.prototype.onerror = function(e){ console.log("WebSocketClient: error",arguments); } WebSocketClient.prototype.onclose = function(e){ console.log("WebSocketClient: closed",arguments); } var wsc = new WebSocketClient(); wsc.open('wss://localhost:8080/'); wsc.onopen = function(e){ console.log("WebSocketClient connected:",e); this.send("Hello World !"); } wsc.onmessage = function(data,flags,number){ console.log(`WebSocketClient message #${number}: `,data); }

Ответ 2



Кто бы мог подумать что проблема будет в конфигах MySQL. Спустя минуту бездействия база автоматически прерывает соединение с ней, а у меня при любом запросе происходила проверка прав пользователя. Для решения проблемы в mysql.ini нужно установить свойство wait_timeout в секундах сколько вам необходимо, у меня стояло по умолчанию 60. Так же прошу заметить что есть вариант "Б", если его таковым можно назвать. Для обхода проблемы можно воспользоваться "Supervisor", он в таких случаях решает следствие, а не причину. Он автоматически запускает или перезапускает сервер когда тот упал или недоступен. Для примера можно воспользоваться этим туториалом для установки и настройки Supervisor.

суббота, 30 ноября 2019 г.

Как работать со связями Active Record в Yii2 (populateRelation, link и т.п.)?

#yii2 #activerecord


В июне 2014 года на Хабрахабре была статья с описанием работы с связями Active Record
в Yii2. Через месяц статья исчезла, но осталась в многочисленных копиях, например здесь. 

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

Нормального описания подхода я так и не нашел (разве что наметки здесь). Зачитал
официальное руководство до дыр, но приемы записи связанны моделей в БД описаны очень
скупо, чтение документации по Active Record тоже не особо помогло.

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

Код из статьи

Модель

class Post extends ActiveRecord
{
    // Будем использовать транзакции при указанных сценариях
    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_INSERT | self::OP_UPDATE,
        ];
    }

    public function getTags()
    {
        return $this->hasMany(Tag::className(), ['id' => 'tag_id'])
            ->viaTable('post_tag', ['post_id' => 'id']);
    }

    public function setTags($tags)
    {
        $this->populateRelation('tags', $tags);
        $this->tags_count = count($tags);
    }

    // Сеттер для получения тегов из строки, разделенных запятой
    public function setTagsString($value)
    {
        $tags = [];

        foreach (explode(',' $value) as $name) {
             $tag = new Tag();
             $tag->name = $name;
             $tags[] = $tag;
        }

        $this->setTags($tags);
    }

    public function getCover()
    {
        return $this->hasOne(Image::className(), ['id' => 'cover_id']);
    }

    public function setCover($cover)
    {
        $this->populateRelation('cover', $cover);
    }

    public function getImages()
    {
        return $this->hasMany(Image::className(), ['post_id' => 'id']);
    }

    public function setImages($images)
    {
        $this->populateRelation('images', $images);

        if (!$this->isRelationPopulated('cover') && !$this->getCover()->one()) {
            $this->setCover(reset($images));
        }
    }

    public function loadUploadedImages()
    {
           $images = [];

           foreach (UploadedFile::getInstances(new Image(), 'image') as $file) {
                $image = new Image();
                $image->name = $file->name;
                $images[] = $image;
           }

           $this->setImages($images);
    }

    public function beforeSave($insert)
    {
        if (!parent::beforeSave($insert)) {
            return false;
        }

       // В beforeSave мы сохраняем связанные модели
       // которые нужно сохранить до основной, т.е. нужны их ИД
       // Не волнуйтесь о транзакции т.к. мы настроили,
       // она будет начата при вызове метода `insert()` и `update()`

       // Получаем все связанные модели, те что загружены или установлены
       $relatedRecords = $this->getRelatedRecords();

       if (isset($relatedRecords['cover'])) {
           $this->link('cover', $relatedRecords['cover']);
       }

       return true;
    }

    public function afterSave($insert)
    {

       // В afterSave мы сохраняем связанные модели
       // которые нужно сохранять после основной модели, т.к. нужен ее ИД

       // Получаем все связанные модели, те что загружены или установлены
       $relatedRecords = $this->getRelatedRecords();

       if (isset($relatedRecords['tags'])) {
           foreach ($relatedRecords['tags'] as $tag) {
               $this->link('tags', $tag);
           }
       }

       if (isset($relatedRecords['images'])) {
           foreach ($relatedRecords['images'] as $image) {
               $this->link('images', $image);
           }
       }
    }
}


Контроллер

class PostController extends Controller
{
    public function actionCreate()
    {
        $post = new Post();

        if ($post->load(Yii::$app->request->post())) {
            // Сохраняем загруженные файлы
            $post->loadUploadedImages();

            if ($post->save()) {
                return $this->redirect(['view', 'id' => $post->id]);
            }
        }

        return $this->render('create', [
            'post' => $post,
        ]);
     }
}


Вопросы по коду: 


Как работают сеттеры в этом примере? Откуда берутся значения, передаваемые сеттерам
($tags, $cover, $images...)? 
В какой момент пишутся данные из связанных моделей (теги, изображения, главное изображение)
в базу данных?
Чего не хватает в этом коде, чтобы он заработал? 


Отдельно хотелось бы попросить ссылок на репозитории серьезных проектов, использующих
Yii2. Очень хочется посмотреть на лучшие практики в реальных сложных проектах.
    


Ответы

Ответ 1



Overview В контроллере: $post->load(Yii::$app->request->post() выполняет загрузку модели. Yii::$app->request->post() возвращает массив, вида ['MyFormName[key]' => 'value]. В load для каждого свойства модели с именем key устанавливается значение, если для них существуют правила валидации rules и данное поле прописано в сценарии. В модели: getTags это релейшен. Служит для связей ActiveRecord моделей. setTags хоть и выглядит как сеттер (сеттер, это функция вызываемая при обращении к несуществующему свойству), но в данном случае это просто функция. Она сохраняет связанную модель с помощью populateRelation и увеличивает текущий счетчик $this->tags_count. К слову о проблеме публичных свойств, счетчик можно увеличить напрямую, без вызова метод setTags. Как работают сеттеры в этом примере? Откуда берутся значения, передаваемые сеттерам ($tags, $cover, $images...)? setCover, setTags, setImages вызываются внутри модели c обычной передачей параметров. Полагаю, они должны быть приватными и не вызываться из клиентского кода (контроллера). В какой момент пишутся данные из связанных моделей (теги, изображения, главное изображение) в базу данных? В момент вызова populateRelation в модели происходит заполнение связанного релейшена. Сохранение происходит в контроллере в момент save модели. Чего не хватает в этом коде, чтобы он заработал? Написать всё заново, используя этот код только в качестве примера. Вам ведь нужен опыт, и это избавит вас от необходимости думать о функциях вроде setTagsString, которые нигде не используются и вносят только путаницу. Также будет понятно что для работы метода getRelatedRecords у вас должны быть заполнены таблицы images и tags. Я рекомендую взять этот раздел документации и по очереди пройтись по каждым функциям. Это долго, по всем я сам ещё не прошелся, но это позволит получить максимально полное представление о возможностях ActiveRecord фреймворка, не копаясь в коде сомнительного качества. Update: Пример рабочего кода данного примера. Несмотря на то что это просто быстрый рефакторинг готов в кодревью и вопросам в комментариях. Миграция: db->driverName === 'mysql') { // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%post}}', [ 'id' => $this->primaryKey(), 'message' => $this->text(), 'tags_count' => $this->integer(2)->notNull()->defaultValue(0) ], $tableOptions); $this->createTable('{{%tag}}', [ 'id' => $this->primaryKey(), 'name' => $this->string(32)->notNull(), 'UNIQUE INDEX `UNQ_tag__name` (`name`)', ], $tableOptions); $this->batchInsert('{{%tag}}', ['name'], [ ['tag1'], ['tag2'], ]); $this->createTable('{{%post_tag}}', [ 'id' => $this->primaryKey(), 'post_id' => $this->integer(11)->notNull(), 'tag_id' => $this->integer(11)->notNull(), 'FOREIGN KEY `FK_post_tag__post_id` (post_id) REFERENCES post(id) ON UPDATE RESTRICT ON DELETE RESTRICT', 'FOREIGN KEY `FK_post_tag__tag_id` (tag_id) REFERENCES tag(id) ON UPDATE RESTRICT ON DELETE RESTRICT', ], $tableOptions); $this->createTable('{{%post_image}}', [ 'id' => $this->primaryKey(), 'post_id' => $this->integer(11)->notNull(), 'image' => $this->string(128)->notNull(), 'is_cover' => $this->boolean()->defaultValue(0), 'FOREIGN KEY `FK_post_image__post_id` (post_id) REFERENCES post(id) ON UPDATE RESTRICT ON DELETE RESTRICT', ], $tableOptions); } public function down() { $this->dropTable('{{%post_image}}'); $this->dropTable('{{%post_tag}}'); $this->dropTable('{{%tag}}'); $this->dropTable('{{%post}}'); } } Контроллер: search(Yii::$app->request->queryParams); return $this->render('index', [ 'searchModel' => $searchModel, 'dataProvider' => $dataProvider, ]); } /** * Displays a single Post model. * * @param integer $id * * @return mixed */ public function actionView($id) { return $this->render('view', [ 'model' => $this->findModel($id), ]); } /** * Creates a new Post model. * If creation is successful, the browser will be redirected to the 'view' page. * * @return mixed */ public function actionCreate() { $model = new CreatePostForm(); if ($model->load(Yii::$app->request->post()) && $model->validate()) { if (!$model->createNewPost()) { throw new Exception('Failed to save CreatePostForm'); } return $this->redirect(['view', 'id' => $model->id]); } return $this->render('create', [ 'model' => $model, ]); } /** * Finds the Post model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. * * @param integer $id * * @return Post the loaded model * @throws NotFoundHttpException if the model cannot be found */ protected function findModel($id) { if (($model = Post::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('The requested page does not exist.'); } } } Представление index.php title = 'Posts'; $this->params['breadcrumbs'][] = $this->title; ?>

title) ?>

'btn btn-success']) ?>

$dataProvider, 'filterModel' => $searchModel, 'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'id', 'message:ntext', ['class' => 'yii\grid\ActionColumn'], ], ]); ?>
Представление create.php title = 'Create Post'; $this->params['breadcrumbs'][] = ['label' => 'Posts', 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title; ?>

title) ?>

['enctype' => 'multipart/form-data']]); ?> field($model, 'message')->textarea(['rows' => 6]) ?> field($model, 'tagString')->input('text') ?> field((new \frontend\models\PostImage()), 'image[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?>
isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
Представление view.php title = $model->id; $this->params['breadcrumbs'][] = ['label' => 'Posts', 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title; ?>

title) ?>

$model->id], ['class' => 'btn btn-primary']) ?> $model->id], [ 'class' => 'btn btn-danger', 'data' => [ 'confirm' => 'Are you sure you want to delete this item?', 'method' => 'post', ], ]) ?>

$model, 'attributes' => [ 'id', 'message:ntext', ], ]) ?>
Модель CreatePostForm loadUploadedImages(); return $this->save(); } /** * Просто получаем теги, которые ввел пользователь на форме * * @return string */ public function getTagString() { return $this->_tagString; } /** * Сохраняем картинки. Тут только запись названий в бд, запись файлов не производится. * * @see http://www.yiiframework.com/doc-2.0/guide-input-file-upload.html */ private function loadUploadedImages() { $images = []; foreach (UploadedFile::getInstances(new PostImage(), 'image') as $file) { $image = new PostImage(); $image->image = $file->name; $images[] = $image; } $this->setImages($images); } /** * Сохряняем связи картинок и для первой картинки устанавливаем флаг is_cover. Тут в примере только создание, но если бы было обновление вызов setCover бы * не произошел. * * @param PostImage[] $images */ private function setImages($images) { $this->populateRelation('images', $images); if (!$this->isRelationPopulated('cover') && !$this->getCover()->one()) { $this->setCover(reset($images)); } } /** * Сохраняем главную картинку поста. * * @param PostImage $cover */ private function setCover($cover) { $cover->is_cover = true; $this->populateRelation('cover', $cover); } /** * Записываем строчку тегов, полученную от пользователя в связанную таблицу. * * @param string $tagString */ public function setTagString($tagString) { $this->_tagString = $tagString; $this->saveTagsToRelation(); } /** * Сохраняем теги в связанной таблице и увеличиваем счетчик */ private function saveTagsToRelation() { $tags = []; /** * Пример с viaTable в релейшене Post::getTags подразумевал что теги только выбираются, но не создаются. */ foreach (explode(',', $this->_tagString) as $name) { $tag = Tag::find()->where(['name' => trim($name)])->one(); if (!$tag) { continue; } $tags[] = $tag; } $this->populateRelation('tags', $tags); $this->tags_count = count($tags); } } Модель Post 'ID', 'message' => 'Message', ]; } /** * @inheritdoc */ public function transactions() { return [ self::SCENARIO_DEFAULT => self::OP_INSERT | self::OP_UPDATE, ]; } /** * @return \yii\db\ActiveQuery */ public function getCover() { return $this->hasOne(PostImage::className(), ['post_id' => 'id']) ->andWhere(['is_cover' => true]); } /** * @return \yii\db\ActiveQuery */ public function getImages() { return $this->hasMany(PostImage::className(), ['post_id' => 'id']); } /** * @return \yii\db\ActiveQuery */ public function getTags() { return $this->hasMany(Tag::className(), ['id' => 'tag_id']) ->viaTable('post_tag', ['post_id' => 'id']); } /** * В afterSave мы сохраняем связанные модели, которые нужно сохранять после основной модели, т.к. нужен ее ИД. * * @param bool $true * @param array $changedAttributes */ public function afterSave($true, $changedAttributes) { $relatedRecords = $this->getRelatedRecords(); if (isset($relatedRecords['cover'])) { $this->link('cover', $relatedRecords['cover']); } if (isset($relatedRecords['tags'])) { foreach ($relatedRecords['tags'] as $tag) { $this->link('tags', $tag); } } if (isset($relatedRecords['images'])) { foreach ($relatedRecords['images'] as $image) { $this->link('images', $image); } } } } Модель PostImage 128], [['post_id'], 'unique'], [['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'post_id' => 'Post ID', 'image' => 'Image', 'is_cover' => 'Is Cover', ]; } /** * @return \yii\db\ActiveQuery */ public function getPost() { return $this->hasOne(Post::className(), ['id' => 'post_id']); } } Модель PostTag true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']], [['tag_id'], 'exist', 'skipOnError' => true, 'targetClass' => Tag::className(), 'targetAttribute' => ['tag_id' => 'id']], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'post_id' => 'Post ID', 'tag_id' => 'Tag ID', ]; } /** * @return \yii\db\ActiveQuery */ public function getPost() { return $this->hasOne(Post::className(), ['id' => 'post_id']); } /** * @return \yii\db\ActiveQuery */ public function getPost0() { return $this->hasOne(Tag::className(), ['id' => 'post_id']); } } Модель Tag 32], [['name'], 'unique'], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'name' => 'Name', ]; } /** * @return \yii\db\ActiveQuery */ public function getPostTags() { return $this->hasMany(PostTag::className(), ['post_id' => 'id']); } } Модель PostSearch $query, ]); $this->load($params); if (!$this->validate()) { // uncomment the following line if you do not want to return any records when validation fails // $query->where('0=1'); return $dataProvider; } // grid filtering conditions $query->andFilterWhere([ 'id' => $this->id, ]); $query->andFilterWhere(['like', 'message', $this->message]); return $dataProvider; } }

Ответ 2



Как работают сеттеры в этом примере? Откуда берутся значения, передаваемые сеттерам ($tags, $cover, $images...)? Это PHP-магия. В базовом классе переопределены методы __get и __set, которые преобразуют чтение и запись атрибутов $foo->bar = $baz->qwe в вызовы методов $foo->setBar($baz->getQwe()). Если магия не нравится, то можно вызывать методы и напрямую. По сути, это банальные геттеры и сеттеры. В некоторых языках есть свойства (геттеры и сеттеры, объединённые в единую сущность), но PHP их не поддерживает. В какой момент пишутся данные из связанных моделей (теги, изображения, главное изображение) в базу данных? Когда вызывается метод save у ActiveRecord. В этот момент данные валидируются и, если валидация проходит, то записываются данные этой ActiveRecord и всех связанных. Чего не хватает в этом коде, чтобы он заработал? На этот вопрос невозможно ответить, пока вы не скажете, что именно не работает. Можно посмотреть комментарии, там указан какой-то баг: https://sohabr.net/habr/post/226103/ Судя по всему, популярностью эта статья не пользовалась, поэтому не понимаю желания полагаться на неё при обучении. Отдельно хотелось бы попросить ссылок на репозитории серьезных проектов, использующих Yii2. Очень хочется посмотреть на лучшие практики в реальных сложных проектах. Есть пример yii2-shop от одного из авторов фреймворка.