Работа со связями моделей в Yii2 на примере каталога

Сначала хотел провести вебинар по связям ActiveRecord-моделей в Yii2 на основе урока о терии баз данных, осветив несколько тем из документации. Но получилась бы куча «воды» без практики. Так что решил показать на примере разработки каталога для интернет-магазина из реальной жизни:

Открыть на YouTube | Исходники примера

Приглашаю на следующие видеоуроки. Анонс и ссылку на эфир, как обычно, пришлю в отдельной рассылке по вебинарам:

И задавайте вопросы в комментариях. Заранее спасибо и до встречи в эфире!

Комментарии

 

Макс

Димон, готовь курс по Yii2, тебе удается всё грамотно и понятно объяснять, а те курсы которые существуют явно нет то, миру необходим курс от профи =)

Ответить

 

Алекс

Да, обсуждалось это на вебинаре. Дмитрий высказал мысль, что прологом к такому курсу будет ликбез по ООП. Считаю это неправильным. Такой подход превратит (ну может не превратит, но приблизит) к уже существующим бездарным. Не нужно тратить на это время. Тем, кому нужен Yii уже следовало бы изучить ООП. Если не изучили - материала как блох на собаке.

Ответить

 

Дмитрий Елисеев

> Тем, кому нужен Yii уже следовало бы изучить ООП.

А тем, кто изучил ООП, уже не пригодится курс ни по какому фреймворку.

Я под ООП имею в виду всю экосистему принципов, паттернов, антипаттернов, метрик технического и визуального качества кода. Применения принципов и паттернов по назначению именно как средств уменьшения сложности. То есть всех составляющих осознанного объектно-ориентированного мышления.

Это не просто ликбез про синтаксис, о котором говорите. Если напишете слово class и скинете в него свои процедуры (по требованию фреймворка или моды), то это никак не сделает вас ОО-программистом. А псевдоООПшного кода кругом ещё больше, чем упомянутых материалов.

Ответить

 

Алекс

И все же позволю себе не согласиться.

Что значит "изучить"? Год назад я начинал с c# (в отличии от PHP, си шарп чисто ООП язык). Кроме того, прочел пару книг по паттернам. В частности "Паттерны проектирования" Фрименов, и "PHP объекты, шаблоны и методики". И я как раз в той стадии, когда "уже Не, но еще и НЕ". Так вот, теория это одно, а практический разбор фреймворка, да еще и от такого хорошего специалиста и учителя - это совсем другое. Так что с фразой "кто изучил ООП, уже не пригодится курс ни по какому фреймворку" не согласен категорически.

Ответить

 

Сергей

Спасибо.
Как раз то что искал по поводу many-to-many связей. Делаю мультикатегории и не мог найти как это реорганизовывается на фреймворке, так что пример с тегами помог.

Добавлю что в миграциях, если выполняется более одного действия (создание таблицы и внешнего ключа или создание двух таблиц) в методе up() документация рекомендует использовать safeUp() для того чтобы если какое то действие зафейлится, то остальные действия откатятся. Может кому будет полезно.

И еще, Дмитрий, я не совсем понял за курсы. Вы проводите какие-то курсы по ООП, фреймворку или берете кого-то в ученики? Как можно попасть на обучение?

Ответить

 

Дмитрий Елисеев

MySQL поддерживает откат транзакций только для данных, так как автоматически коммитит транзакции при изменении таблиц или полей. Так что для неё safeUp бесполезен.

Ответить

 

Дмитрий Елисеев

Групповых курсов пока не веду. Только репетиторствую, когда просят. Напишите на почту или в Контакты.

Ответить

 

Денис

Было сказано: "А тем, кто изучил ООП, уже не пригодится курс ни по какому фреймворку"
Дмитрий, не слушаейте никого, делайте курсы для новичков с ликбезом по ООП !

Ответить

 

Яромир

Сразу видно психологию школоты - никого не слушайте, а только меня слушайте. Книги читай, школяр!

Ответить

 

Сергей

Привет. Подскажи плз какой дистрибутив linux лучше всего подходит для веб разработки по твоим предпочтениям?

Ответить

 

Дмитрий Елисеев

Стандартный стек Apache/Nginx, PHP и MySQL везде есть (а где нет - собирается из исходников), так что без разницы.

Ответить

 

Сергей

Я имел ввиду операционную систему на ядре линукс какую выбрать лучше (убунту, минт ....), может я не так выразился, вроде согласно википедии дистрибутив линукса и ОС это одни и те же вещи.

Спасибо за твои старания, изучении yii2 с твоими уроками стало намного проще!

Ответить

 

Дмитрий Елисеев

Ну я и говорю, что на любом Линуксе PHP есть, так что без разницы.

Ответить

 

Денис

Ubuntu desktop operating system powers millions of PCs and laptops around the world :)

Ответить

 

Сергей

Хотелось бы иметь последнии версии php7 и mysql 5.7, но я так понял в линуксе есть сборки lamp и xampp и они еще на старых версиях

Ответить

 

Денис

Для обучения тебе php>5.6 и mariadb 5.5 хватит надолго. А потом сам разберешься. Недорогого хостинга с поддержкой php 7 нужно ещё поискать). Сборки типа xamp проще ставятся, но для обучения я бы посоветовал ставить все отдельно, чтобы отличать хотябы что такое php от sql ))))

Ответить

 

Сергей

Не понял с чего ты взял, что я не отличаю php от sql?

Ответить

 

Aleksandr

Сергей, я так же как и Денис решил, что вы только собираетесь заняться обучением. Про линукс. На сервера чаще ставят серверный вариант Debian / Ubuntu / CentOS. В принципе, на начальном этапе линукс Вам не понадобится. Для разработки чаще используют виртуалку внутри виндоуса. Серверная и десктопная версия линукса отличается только набором программ для визуальной части, для серверов его вырезают. Самый популярный дистр на базе Убунту (та на базе Дэбиан) - Минт. В него установлено всё, что нужно пользователю без технических знаний. Мне приглянулся Elementary OS. Это дело вкуса. Если очень грубо - то чаще разные варианты линукса различаются "обоями да вариантами менюшек". Я про клонов на базе убунты. Научитесь работать с Bash, это то, что линукс делает линуксом для простого обывателя и пользователя.

Ответить

 

Денис

Дмитрий, подскажите, пожалуйста для особо понятливых. Честно, я посмотрел все 4 часа видео)
У меня есть форма для заполнения данных из двух моделей. Вопрос по экшену create.
Как сохранить данные одной модели, взять id первой сохраненной модели, и сохранить этот id в соответствующем поле второй модели? Приведенный код ниже у меня не сохраняет данные ни одной модели(

public function actionCreate()
{
    $model = new Dailyinfo();
    $info = new Information();

    if ($model->load(Yii::$app->request->post()) && $info->load(Yii::$app->request->post()) && $info->save()&& $model->info_id=$info->id && $model->save()) {
        return $this->redirect(['view', 'id' => $model->id]);
    } else {
        return $this->render('create', [
            'model' => $model,
            'info' => $info,
        ]);
    }
}
Ответить

 

Дмитрий Елисеев

Сначала $model->save(), потом $info->save().

Ответить

 

Денис

Поменял весь порядок, добавил ещё несколько переменных для заполнения required полей, теперь $info сохраняется, а $model нет.
Мне кажется при сохранении $model не получает $info->id.

Ответить

 

Дмитрий Елисеев

Сделайте print_r($info) и print_r($model), чтобы не казалось.

Ответить

 

Денис

С вашей помощью, Дмитрий, заработало) Есть ли у вас замечания по моему коду, все таки стремлюсь постепенно к феншую, хотя понимаю, что ещё далеко) И думаю начинающим будет полезно.

public function actionCreate()
{
    $model = new Dailyinfo();
    $model->dept_id=1;
    $model->insert_date=date('Y-m-d');

    $info = new Information();
    $info->period_id=7;
    $info->forma_type=12;

    if (  
        $info->load(Yii::$app->request->post())
        && $info->save()
        && $model->load(Yii::$app->request->post())
    ) {
        $model->info_id=$info->id;
        $model->save();
        return $this->redirect(['view', 'id' => $model->id]);
    } else {
        return $this->render('create', [
            'model' => $model,
            'info' => $info,
        ]);
    }
}
Ответить

 

Дмитрий Елисеев

Я бы либо установку первоначальных значений перенёс в методы loadDefaultValues моделей. И вообще бы отдельную модель формы использовал.

Ответить

 

Евгений

Подскажите пожалуйста как изучить ООП? Что читать и что смотреть? На что делать упор?

Ответить

 

Дмитрий Елисеев

Про кое-что расказывал здесь. А так, думаю, в YouTube что-нибудь уже есть.

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

Ответить

 

Дмитрий Елисеев

Уговорили :) Запускаю интенсив.

Ответить

 

Кабаченко Виктор

Отличный вебинар.
Маленькое уточнение. Чтобы поместить в гриде количество записей в нескольких связанных таблицах, надо немного изменить ваше решение, указав DISTINCT в COUNT. Примерно так:

$query = Category::find()
  ->select(['{{%category}}.*', 
         'products_count' => new Expression('COUNT(DISTINCT {{%product}}.id)')]),
         'materials_count' => new Expression('COUNT(DISTINCT {{%material}}.id)')]),
  ->joinWith(['products', 'materials'], false)
  ->groupBy('{{%category}}.id')
Ответить

 

Andrewkha

В коммите added product tabular attributes editor ошибка.
При таком коде в контроллере

        foreach (array_diff_key($attributes, $values) as $attribute) {
            $values[] = new Value(['attribute_id' => $attribute->id]);
        }

и таком выводе в представлении

<?= $form->field($value, '[' . $value->productAttribute->id . ']value')


все работает, если только id-шники атрибутов в соотв таблице идут подряд. Если же что-то вроде 1, 2, 5 то все поломается, loadMultiple не подберет значения там, где не совпадут индексы.
В контроллере надо

$values[$attribute->id] = new Value(['attribute_id' => $attribute->id]);
Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Сергей

Дмитрий, а можешь объяснить как в Yii2 сделать древовидные комментарии, без использования nested sets и т д? И чтобы можно было самому регулировать уровень максимальной вложенности.

Ответить

 

Дмитрий Елисеев

У меня обычным parent_id. С выборкой всех одним запросом и с рекурсивным выводом. Максимальный сдвиг для вложенности делается простым CSS:

.comment {
    margin-left: 40px;
}
.comment > .comment > .comment .comment {
    margin-left: 0;
}
Ответить

 

Andrewkha

Дмитрий, добрый день!

Слегка запутался в join, with и joinWith.
Скажи, пожалуйста, я правильно понимаю, что joinWith с параметром false - это аналог простого Join? или есть какие-то особенности?

Ответить

 

Дмитрий Елисеев

Да, аналог. Но join'у нужно вручную имя таблицы и колонок указать, а joinWith работает автоматически по имени связи.

Ответить

 

Иван Васильев

Спасибо за вебинар. Смотрел в записи. Очень много полезного для себя почерпнул из этого видео.

Ближе к концу видео, в CatalogController.php вы поменяли имя параметра действия с $tag на $name.
Тогда нужно поменять параметр при формировании массива ссылок на тэги в представлении _item.php.

$tagLinks[] = Html::a(Html::encode($tag->name), ['tag', 'name' => $tag->name]);

Еще раз спасибо. Теперь собираюсь пересмотреть ваши более ранние вебминары.

Ответить

 

Andrewkha

Дмитрий, приветствую. Нужна Ваша помощь :) Вопрос по работе с моделью и ActiveDataProvider.

Итак, имеется Junction table простого вида
id_user
it_tournament
points

первые два - внешние ключи для соотв таблиц.
Задача - для массива, содержащего id_tournaments получить все записи с максимальными points.

Что я сделал.

        $query = [];
        foreach ($array as $one)
            $query[] = UsersTournaments::find()
                ->where(['id_tournament' => $one])
                ->orderBy(['points' => SORT_DESC])
                ->with('idTournament.country0')
                ->with('idUser')
                ->limit(1);

        $count = count($query);

        $toExecute = $query[0];
        for($i = 0; $i < $count - 1; $i++)
            $toExecute = $toExecute->union($query[$i + 1]);

        return $toExecute;

Если затем вызываю

$toExecute->all()

все работает хорошо, т.е. выбирает именно записи с максимальным значением points для каждого id_tournament.
Но если же я этот $toExecute скармливаю в качестве свойства query для ActiveDataProvider начинаются чудеса.
А именно, для первого id_tournament (и только для него) выдает все имеющиеся записи, не только с максимальным значением points. Т.е. limit(1) в запросе не срабатывает. Для всех последующих id_tournament все хорошо.
Через дебаггер посмотрел, какой выполняется запрос и очень удивился.
Для первого id_tournament выполняется limit(20), такое ощущение, что ActiveDataProvider подменяет мой limit(1) своим дефолтовым для пагинации. Что интересно, для всех последющих, которые цепляются через union все хорошо, там limit(1).

Есть идеи, как это можно пофиксить?

Ответить

 

Дмитрий Елисеев

Можно попробовать все UNION обернуть в подзапрос в скобках. А так точно не знаю.

Ответить

 

Денис

Подскажите, пожалуйста, как можно сделать так, чтобы при помещении в парку site\pages файла формата md (Markdown), он открывался как статическая страница?

Ответить

 

Дмитрий Елисеев

Открыть через file_get_contents и скормить парсеру:

$this->render('page', [
    'content' => yii\helpers\Markdown::process($md),
]);
Ответить

 

Sergey Aver

Дмитрий подскажите почему после связи данных, я не могу обратиться к определенному свойству?

public function getIngredients()
{
    return $this->hasMany(Ingredients::className(), ['id' => 'ingredient_id']);
}

public function getReceptIngredient($id)
{
    return ReceptyIngredients::find()
        ->joinWith('ingredients')
        ->where(['recept_id'=>$id])
        ->all();	
    }
}

$items = ReceptyIngredients::getReceptIngredient(1);
foreach($items as $item) {
    print_r($item->ingredients);
}

выводит вот такой результат

Array
(
    [0] => frontend\models\Ingredients Object
        (
            ...
        )
)

А если обращаюсь к $item->ingredients->name

пишет что Trying to get property of non-object

Ответить

 

Дмитрий Елисеев

Потому что $item->ingredients - это тоже массив:

foreach ($item->ingredients as $ingredient) {
    echo $ingredient->name;
}
Ответить

 

Sergey Aver

А если я делаю связь многие ко многим, вообще выходит что выдает два массива, ингредиенты и мера веса, а мне надо чтобы был массив один где связаны две таблицы через промежуточную. Тогда надо другой запрос делать? В ручную через select??? Потому что результат моего запроса такой

SELECT `np_ingredients`.* FROM `np_ingredients` LEFT JOIN `np_recepty_ingredients` ON `np_ingredients`.`id` = `np_recepty_ingredients`.`ingredient_id` LEFT JOIN `np_measures` ON `np_recepty_ingredients`.`id_measure` = `np_measures`.`id` WHERE `recept_id`=485 

А хотелось бы такой

SELECT * FROM `np_ingredients` LEFT JOIN `np_recepty_ingredients` ON `np_ingredients`.`id` = `np_recepty_ingredients`.`ingredient_id` LEFT JOIN `np_measures` ON `np_recepty_ingredients`.`id_measure` = `np_measures`.`id` WHERE `recept_id`=485 
Ответить

 

Дмитрий Елисеев

C "SELECT * FROM ..." только вручную, так как ActiveRecord результаты JOIN-ов не парсит.

Ответить

 

Яромир

Дмитрий, во-первых, большое человеческое спасибо за Ваш труд и Вашу отзывчивость. Во-вторых, не поделитесь Вашими планами на будущие вебинары? Помнится на интенсиве по ООП много людей высказывало желание увидеть вебинары по устройству symfony. Собственно, это было бы прямым развитием темы ООП про правильное структуирование клиентского кода в фреймворке через введение сервисного слоя, иньекции и т.д.Прекрасно было бы и в виде платного интенсива, хотя я отдаю себе отчет сколько времени уходит на подготовку к подобному событию.
А может быть вы планируете еще какие то вебинары по Yii? Поделитесь плиз, чего можно ждать, или наоборот не ждать. Спасибо.

Ответить

 

Дмитрий Елисеев

Всё планирую, но не всё успеваю. Так что как повезёт :)

Ответить

 

Роман

Спасибо за ваши труды, лучшего сайта по YII не находил))
Было бы интересно узнать как правильно делать URL каталога, н-р
domain/catalog/categoryParent/categoryChild/categoryChild/name_product

Ответить

 

Вася

Дмитрий подскажите как в виджете категорий в каталоге, вывести рядом количество постов?

Ответить

 

Дмитрий Елисеев

Если без иерархии, то echo $category->getPosts()->count().

Ответить

 

Вася

Записал вот так:

'template' => '<a href="{url}" class="url-class">{label}</a>'.'<small class="badge">'.$category->getPosts()->count().'</small>,

И количество запросов к базе выросло в два раза, было 8 стало 14, я делал другим образом, как в модели поиска categorySearch.php в файл categoriesWidget добавил public $posts_count;
Потом

$categories = Category::find()
    ->select(['{{%category}}.*', 'courses_count' => new Expression('COUNT(DISTINCT {{%post}}.id)')])
    ->joinWith(['posts'], false)
    ->groupBy('{{%category}}.id')
    ->orderBy('name')->all();

и вот так:

$items[] = [
    'label' => $category->name,
    'template' => '<a href="{url}" class="url-class">{label}</a>'.'<small class="badge">'.$category->posts_count.'</small>',
    'url' => ['main/category', 'id' => $category->id],
    'active' => $this->category && $category->id == $this->category->id ? true : null,
    'items' => $this->getItemsRecursive($categories, $category->id, $current),
];

Выводиться нормально и запросов 8, так нормально или можно как то по другому?

Ответить

 

Дмитрий Елисеев

Нормально. Мы так и делали в админке в гриде категорий в этом вебинаре. Ещё учтите, что с древовидными категориями в виджете нужно считать суммарное число постов в подкатегориях.

Ответить

 

Вася

Подскажите пожалуйста как это сделать?

Ответить

 

Дмитрий Елисеев

Либо завести поле total_count и при каждом изменении постов его пересчитывать, либо каждый раз рекурсивно суммировать по parent_id, либо перевести базу на Nested Sets и считать COUNT по массивам id вложенных категорий.

Ответить

 

Александр

Дмитрий, спасибо за очередной качественный вебинар.
Для себя хочу уточнить один момент.
В миграциях вы создаете индексы так же для полей, на которых так же создаете FK на другое поле (или другое поле другой таблицы). Для postgres согласен. Но, ведь в mysql при создании FK автоматически создается и индекс.

Ответить

 

Дмитрий Елисеев

Просто стараюсь делать миграции, универсально работающие и на MySQL, и на PostgreSQL.

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>