Yii PHP framework: создаём игровой сайт. Часть 6. Формируем страницы игр и жанров.

Владимир | | PHP, Web разработка, Yii.

yii_game_site_mvc

Приветствую всех! Сегодня мы продолжим разработку игрового сайта на основе фреймворка Yii.

Напомню, на чём мы остановились в прошлый раз. У нас создан контроллер для работы с играми GamesController, модель Games и стандартные представления (находятся в папке views/games). Кроме того, мы написали метод импорта игр actionImport.

Раз импортировать игры в базу мы можем, напишем методы для их отображения на сайте. Всего нам нужно создать три типа страниц:

1) с общим перечнем игр (главная);

2) с перечнем игр определенного жанра;

3) с выбранной игрой.

Первые два типа страниц мы реализуем с помощью одного метода — actionList. Дело в том, что страница с играми определенного жанра ничем не отличается от страницы с общим перечнем игр. Просто при поиске игр в базе мы используем один дополнительный параметр – код жанра.

Теперь взгляните на сам метод.

public function actionList()
{
	$type = null;
	//формируем запрос на поиск игр с сортировкой по дате
	$criteria=new CDbCriteria;
	$criteria->order = 'g_added DESC';
	//если указан параметр type_id, нужно показывать только игры выбранного жанра
	if (isset($_GET['type_id']) && is_numeric($_GET['type_id'])) {
		//ищем указанный жанр
		$type = Types::model()->findByPk($_GET['type_id']);
		//если указанный жанр найден...
		if (null !== $type) {
			//...добавляем в запрос дополнительный параметр
			$criteria->condition = 't_id=:t_id';
			$criteria->params = array(':t_id'=>$_GET['type_id']);
		}
	}

	//получаем данные для пагинации
	if (null !== $type) {
		$pages=new CPagination(Games::model()->published()->with('ygs_types')->count($criteria));
	} else {
		$pages=new CPagination(Games::model()->published()->count($criteria));
	}
	$pages->pageSize=self::PAGE_SIZE;
	$pages->applyLimit($criteria);

	//получаем список игр
	if (null !== $type) {
		//ВАЖНО! Вызов published должен идти до with
		$models=Games::model()->published()->with('ygs_types')->findAll($criteria);
	} else {
		$models=Games::model()->published()->findAll($criteria);
	}
	
	//если ни одной страницы не найдено, отправляем 404-ую ошибку
	if (empty($models)) {
		throw new CHttpException(404,'The requested page does not exist.');
	}

	//заполняем массив с жанрами игр
	foreach ($models as $key=>$game) {
		//расшифровываем жанры игр (по коду в поле g_type)
		$models[$key]->g_types = $this->_decodeTypes($game->g_type);
	}
	//показываем страницу
	$this->render('list',array(
		'models'=>$models,
		'pages'=>$pages,
		'type'=>$type
	));
}

Обратите внимание на то, как метод определяет какой тип страницы мы хотим сформировать. Если передан GET параметр type_id, то нужно сформировать страницу с играми определённого жанра. Иными словами, запрос вида
http://yiigame.l/index.php?r=games/list
сформирует страницу с общим перечнем игр, а запрос
http://yiigame.l/index.php?r=games/list&type_id=8
страницу с играми жанр которых имеет id == 8.

Как обычно для формирования условий поиска мы используем объект CDbCriteria. И в первую очередь устанавливаем его свойство
$criteria->order = 'g_added DESC';
в результате новые игры окажутся в начале списка.

Если получен параметр $_GET['type_id'] мы ищем указанный жанр (строка 10) и добавляем условие в запрос (строки 14 и 15). При этом используется тот же синтаксис объявления параметров, что и в PDO. Параметру присваивается имя, которое начинается с двоеточия, а затем, используя это имя, мы устанавливаем значение.

Затем выполняем настройку пагинации (с помощью класса CPagination). Тут появляется один интересный момент. Нам нужно просто посчитать общее количество игр (строка 21), но игры имеют два состояния: опубликована и черновик. Нас интересуют только опубликованные, т.е. те у которых в БД поле g_state == 0 (таблица ygs_games).

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

Мы можем в модели (Games) объявить метод scopes, который выглядит следующим образом.

public function scopes() {
	return array(
		'published'=>array(
			'condition'=>'g_state='.self::PUBLISHED,
		)
	);
}

Он возвращает массив в котором ключом является название метода, а значением – параметры запроса к БД. В данном случае, в нашем распоряжении появится метод published, который можно использовать при формировании запроса.

Например, при вызове
Games::model()->published()->count(…);
в запрос будет вставлено условие 'g_state='.self::PUBLISHED.

Называется эта возможность «Именованные группы условий».

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

Возвращаемся к методу actionList.

Нам нужно получить список игр (строки 29-34). Для страницы с общим перечнем игр, запрос формируется так:

$models=Games::model()->published()->findAll($criteria);

Тут всё просто, но когда нам нужно выбрать только игры конкретного жанра, мы должны использовать в запросе таблице ygs_types и ygs_games_types. Естественно, библиотека фреймворка позволяет сократить работу и не писать такой запрос вручную. Достаточно сделать такой вызов

$models=Games::model()->published()->with('ygs_types')->findAll($criteria);

Обратите внимание на метод with. В его параметре указан элемент массива, который возвращает метод relations модели.

public function relations()
{
	return array(
		//указываем, что в результам запроса нужно включить данные из связанной таблицы
		'ygs_types' => array(self::MANY_MANY, 'Types', 'ygs_games_types(gt_game_id, gt_type_id)','together'=>true,'joinType'=>'INNER JOIN'),
		'ygs_screenshots' => array(self::HAS_MANY, 'Screenshots', 's_game_id'),
	);
}

Консольная утилита Yii автоматически создаёт только два параметра self::MANY_MANY, 'Types', 'ygs_games_types(gt_game_id, gt_type_id)', которые указывают тип связи между таблицами. Но мы добавим ещё два 'together'=>true и 'joinType'=>'INNER JOIN'. С помощью первого мы указываем, что связанная таблица (в данном случае это таблица жанров) должна войти в результаты запроса, второй – изменяет тип объединения (по-умолчанию используется LEFT OUTER JOIN).

После этого, мы проверяем были ли найдены игры (строки 37-39) и если нет – возвращаем 404-ую ошибку.

На этом этапе у нас уже есть все необходимые данные. Нужно только подготовить их для передачи в представление. Эта операция несложная. Нас не устраивает только поле со списком жанров, т.к. он записан в закодированной форме.

В прошлый раз мы разбирали метод кодировки, поэтому повторяться я не буду. Приведу только код метода расшифровки

public function _decodeTypes($value) {
	if (count($this->_allTypes) == 0) {
		//получаем список жанров
		$this->_allTypes = Types::model()->findAll();
	}
	$types = array();
	//перебираем все жанры и проверяем указаны ли они в поле жанра игры
	//для этого используется логическая операция "И" 
	foreach ($this->_allTypes as $type) {
		if ((int)$value & (int)$type->t_id) {
			$types[] = $type;
		}
	}
	//возвращаем массив с жанрами
	return $types;
}

В качестве параметра он получает список жанров в закодированной форме, а возвращает массив с объектами типа CActiveRecord для каждого жанра к которому относится игра. (В начале метода мы получаем полный список жанров, а затем ищем те, которые относятся к данной игре и добавляем их в массив $types).

Заключительный этап – загрузка представления (list). Полностью приводить его код я не буду (вы можете посмотреть его в архиве), покажу только самую интересную часть.

$i = 0;
foreach($models as $n=>$model) {
	$this->renderPartial('_short_desc', array('game'=>$model));
	if ($i % 2 !== 0) {
		echo '<div class="clear"></div>';
	}
	$i++;
}

Это цикл, в котором формируем короткое описание мы, каждой найденной игры. Как видите, разметка для этого описания находится в отдельном представлении _short_desc, которое мы загружаем с помощью метода renderPartial. Отдельное преставление удобно использовать так как в дальнейшем мы создадим виджет «лучшие игры», который будет показывать точно такие же описания игр.

На этом мы остановимся. Осталась нерассмотренным создание страницы отдельной игры, но на этом примере я хочу рассказать о поддержке JS фреймворком (мы создадим слайдшоу для скриншотов), поэтому будет лучше выделить на эту тему отдельную часть 😉

Напоминаю, что вы можете скачать архив с этим примером

Source

И, конечно, вы можете задавать любые вопросы 🙂

До встречи!

Все разделы цикла.

  1. Yii PHP framework: создаём игровой сайт. Часть 1. Постановка задачи.
  2. Yii PHP framework: создаём игровой сайт. Часть 2. База данных и установка фреймворка.
  3. Yii PHP framework: создаём игровой сайт. Часть 3. Аутентификация.
  4. Yii PHP framework: создаём игровой сайт. Часть 4. Работа с жанрами игр.
  5. Yii PHP framework: создаём игровой сайт. Часть 5. Импорт игр.
  6. Yii PHP framework: создаём игровой сайт. Часть 6. Формируем страницы игр и жанров.
  7. Yii PHP framework: создаём игровой сайт. Часть 7. Работа с JavaScript и страницы игр.
  8. Yii PHP framework: создаём игровой сайт. Часть 8. Создаём виджеты.
  9. Yii PHP framework: создаём игровой сайт. Часть 9. Поиск ошибок.
  10. Yii PHP framework: создаём игровой сайт. Часть 10. Панель управления.
  11. Yii PHP framework: создаём игровой сайт. Часть 11. Человекопонятные URL.
  12. Архив с исходниками
  • Евгений

    Хорошо бы конечно еще метатеги разные для страниц сделать, не подскажете как это удобнее реализовать, чтобы теги динамически прописывались в лаяоут?!

    • Я бы делал с помощью виджета. В них вы можете получить имя контроллера и метода, например, так
      $controller = Yii::app()->controller->id;
      $action = Yii::app()->controller->action->id;
      и, используя эти данные, выполнить запрос к БД, который вернёт нужные данные для метатегов.
      После этого просто выводите метатег, а сам виджет вставляете в layout.

  • Евгений

    Хорошо бы конечно еще метатеги разные для страниц сделать, не подскажете как это удобнее реализовать, чтобы теги динамически прописывались в лаяоут?!

    • Я бы делал с помощью виджета. В них вы можете получить имя контроллера и метода, например, так
      $controller = Yii::app()->controller->id;
      $action = Yii::app()->controller->action->id;
      и, используя эти данные, выполнить запрос к БД, который вернёт нужные данные для метатегов.
      После этого просто выводите метатег, а сам виджет вставляете в layout.

  • Александр

    прежде всего хотелось бы выразить огромную благодарность за написание этого цикла статей! очень помогает в освоении нового фреймворка!

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

    Yii почему-то ругается на реализацию связей между таблицами (метод relations модели). я не знаю с чем это связано, но при попытке попасть на страницу с играми отфильтрованными по определенному жанру (?r=games/list&type_id=64) вылетает следующая ошибка:
    CDbCommand не удалось исполнить SQL-запрос: SQLSTATE[42S22]: Column not found: 1054 Unknown column 't_id' in 'where clause'
    если посмотреть запрос, на котором все заглохло:

    SELECT
    FROM `ygs_games` `t`
    WHERE ((g_state=0) AND (t_id=64))

    то можно понять суть ошибки — ну нет такой колонки в этой таблице 🙂

    собственно меня интересует по какой причине не произошло INNER JOIN'а (или я что-то не так понял)?

    и второй момент: в таблице ygs_games присутствует поле g_type
    если в вашем листинге в 14 строке:
    $criteria->condition = 't_id=:t_id';
    заменить на:
    $criteria->condition = 'g_type=:t_id';
    то все будет работать согласно указанному алгоритму.

    та же самая борода при попытке вывода виджета TopGames.

    • Проблема в том, что я немного поспешил с руководством, нужно было подождать версию 1.1 фреймоворка. Дело в том, что в нём внесли изменения в работу с БД. Изменения, связанные с 'жадной' загрузкой для отношений Active Record.

      Исправить несложно, но придётся менять и код, и описание, а этого делать очень не хочется 🙂

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

      В общем, пример должен правильно работать на версии 1.0.10.

      • Алекс

        А можете подсказать как изменить условие, чтобы запрос работал правильно в Yii 1.1 ? у меня что-то не получилось, не могу разобраться какие будут псевдонимы для связанных таблиц.
        Спасибо!

        • Честно говоря, мне было лень переписывать пример и, поэтому, готового решения у меня нет.
          Но идея такая. Нужно исправить строку 31, вместо Games::model()… поставить Types::model()…, т.е. получить нужный тип и связанные с ним игры.
          Но при этом возникнет другая проблема. Т.к. для вывода общего списка игр и списка игр определенного жанра используется один и тотже метод контроллера games/list, в представление нужно передавать данные одного типа.
          А если id жанра указан не будет, то выполнится строка 33 и мы получим массив с объектами Games.
          Лучшее, на мой взгляд, решение в этой ситуации — изменить структуру проекта. Т.е. добавить метод в контроллер Types и в нём выводить списки игр по жанрам, т.е. общий список будет доступен по адресу games/list, а список игр выбранного жанра — по адресу types/list.

      • Алекс

        Если я меняю на Types, то возникает такая же ошибка, но для поля 'g_added' в сортировке. Вообще, что-то не могу понять с этим отношением Many-to-many… Как я понял, в Yii версией < 1.1 таблицы просто соединялись и к любым полям можно было обратится. Как допустим получить данные с условиями по обоим связанным таблицам, сортировка по полю из games, а where по полю из types?

        • Там должно быть что-то вроде
          Types::model()->with('games')->findAll();

      • Алекс

        Я сделал отдельный метод в контроллере Games:
        public function actionType() {
        if (!(isset($_GET['type_id']) && is_numeric($_GET['type_id']))) {
        return;
        }

        $criteria=new CDbCriteria;
        $criteria->condition = 't_id=:t_id';
        $criteria->params = array(':t_id'=>$_GET['type_id']);

        $pages=new CPagination(Types::model()->with('gb_games')->count($criteria));
        $pages->pageSize=self::PAGE_SIZE;
        $pages->applyLimit($criteria);

        $models=Types::model()->with('gb_games')->findAll($criteria);

        if (empty($models)) {
        throw new CHttpException(404,'The requested page does not exist.');
        }

        foreach ($models as $key=>$type) {
        var_dump($type->gb_games);
        }
        }
        Пытаюсь просто вывести игры, связанные с жанром на экран — но gb_games оказывается пустым массивом. Что я упускаю?

        P.S. еще вопрос — как можно посмотреть какие sql запросы генерирует yii к базе данных?

  • Александр

    прежде всего хотелось бы выразить огромную благодарность за написание этого цикла статей! очень помогает в освоении нового фреймворка!

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

    Yii почему-то ругается на реализацию связей между таблицами (метод relations модели). я не знаю с чем это связано, но при попытке попасть на страницу с играми отфильтрованными по определенному жанру (?r=games/list&type_id=64) вылетает следующая ошибка:
    CDbCommand не удалось исполнить SQL-запрос: SQLSTATE[42S22]: Column not found: 1054 Unknown column 't_id' in 'where clause'
    если посмотреть запрос, на котором все заглохло:

    SELECT
    FROM `ygs_games` `t`
    WHERE ((g_state=0) AND (t_id=64))

    то можно понять суть ошибки — ну нет такой колонки в этой таблице 🙂

    собственно меня интересует по какой причине не произошло INNER JOIN'а (или я что-то не так понял)?

    и второй момент: в таблице ygs_games присутствует поле g_type
    если в вашем листинге в 14 строке:
    $criteria->condition = 't_id=:t_id';
    заменить на:
    $criteria->condition = 'g_type=:t_id';
    то все будет работать согласно указанному алгоритму.

    та же самая борода при попытке вывода виджета TopGames.

    • Проблема в том, что я немного поспешил с руководством, нужно было подождать версию 1.1 фреймоворка. Дело в том, что в нём внесли изменения в работу с БД. Изменения, связанные с 'жадной' загрузкой для отношений Active Record.

      Исправить несложно, но придётся менять и код, и описание, а этого делать очень не хочется 🙂

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

      В общем, пример должен правильно работать на версии 1.0.10.

      • Алекс

        А можете подсказать как изменить условие, чтобы запрос работал правильно в Yii 1.1 ? у меня что-то не получилось, не могу разобраться какие будут псевдонимы для связанных таблиц.
        Спасибо!

        • Честно говоря, мне было лень переписывать пример и, поэтому, готового решения у меня нет.
          Но идея такая. Нужно исправить строку 31, вместо Games::model()… поставить Types::model()…, т.е. получить нужный тип и связанные с ним игры.
          Но при этом возникнет другая проблема. Т.к. для вывода общего списка игр и списка игр определенного жанра используется один и тотже метод контроллера games/list, в представление нужно передавать данные одного типа.
          А если id жанра указан не будет, то выполнится строка 33 и мы получим массив с объектами Games.
          Лучшее, на мой взгляд, решение в этой ситуации — изменить структуру проекта. Т.е. добавить метод в контроллер Types и в нём выводить списки игр по жанрам, т.е. общий список будет доступен по адресу games/list, а список игр выбранного жанра — по адресу types/list.

      • Алекс

        Если я меняю на Types, то возникает такая же ошибка, но для поля 'g_added' в сортировке. Вообще, что-то не могу понять с этим отношением Many-to-many… Как я понял, в Yii версией < 1.1 таблицы просто соединялись и к любым полям можно было обратится. Как допустим получить данные с условиями по обоим связанным таблицам, сортировка по полю из games, а where по полю из types?

        • Там должно быть что-то вроде
          Types::model()->with('games')->findAll();

      • Алекс

        Я сделал отдельный метод в контроллере Games:
        public function actionType() {
        if (!(isset($_GET['type_id']) && is_numeric($_GET['type_id']))) {
        return;
        }

        $criteria=new CDbCriteria;
        $criteria->condition = 't_id=:t_id';
        $criteria->params = array(':t_id'=>$_GET['type_id']);

        $pages=new CPagination(Types::model()->with('gb_games')->count($criteria));
        $pages->pageSize=self::PAGE_SIZE;
        $pages->applyLimit($criteria);

        $models=Types::model()->with('gb_games')->findAll($criteria);

        if (empty($models)) {
        throw new CHttpException(404,'The requested page does not exist.');
        }

        foreach ($models as $key=>$type) {
        var_dump($type->gb_games);
        }
        }
        Пытаюсь просто вывести игры, связанные с жанром на экран — но gb_games оказывается пустым массивом. Что я упускаю?

        P.S. еще вопрос — как можно посмотреть какие sql запросы генерирует yii к базе данных?

  • Посмотреть запросы можно, добавив настройки лога в конфиг (main.php)
    'log'=>array(
    'class'=>'CLogRouter',
    'routes'=>array(
    array(
    //выводим лог внизу страницы
    'class'=>'CWebLogRoute',
    'levels'=>'trace, info, profile',
    ),
    ),
    ),

    Вроде бы вы ничего не упустили, но покажите, пожалуйста, метод relations (в массиве, который он возвращает должен быть элемент gb_games).

    • Алекс

      Метод вот такой:
      public function relations()
      {
      return array(
      'gb_games' => array(self::MANY_MANY, 'Games', 'gb_games_types(gt_game_id, gt_type_id)', 'together'=> true ),
      );
      }
      Я еще по-всякому перепробовал — пока ничего не получается..

      • Я прошу прощения, немного вас запутал.
        Дело в том, что если вы хотите использовать limit для связанной таблицы в новых верисиях yii, нужно использовать «ленивую загрузку».
        Пример я переделал и вы можете скачать архив.
        Изменения:
        1) метод actionShowGames в контроллере TypesController;
        2) добавлено представление types/showGames.php;
        3) два дополнительных правила в роутере (main.php).
        В ближайшее время постараюсь написать пост на эту тему. Вообще-то, я не уверен, что использовал лучшее решение, но оно работает 😉

        • Алекс

          Спасибо! Кое-как разобрался, но очень интересно будет почитать про такие запросы.

        • Alexdotcom

          Это моя вторая попытка перейти на Yii с процедурного способа написания сайтов на PHP. Очень хороший туториал — пока не деьали не разбирал, но везде одни только блоги и блоги в примерах — думаю, этот вариант немного разнообразней. Хотел только добавить: при попытке перейти на 1.1.6 скчивается в большинстве статей сайт под 1.0.11. И только тут я нашел ссылку на 1.1.0, хотя запустить смог только после создания (вручную) папок assets и runtime в вашей папке. Думаю, следует одновить все ссылки на перезалитый архив с исходниками (где уже и папку будут нужные). Спасибо!

        • Насчет папок assets и runtime.
          Понимаете, эта статья учебная, для тех, кто хочет разобраться в работе фреймворка. Я не ставил целью просто выложить готовое решение.

          А создание папок assets и runtime входит в процедуру установки Yii. И с этой проблемой вам придется столкнуться при создании первого же собственного приложения.

  • Посмотреть запросы можно, добавив настройки лога в конфиг (main.php)

    'log'=>array(
    	'class'=>'CLogRouter',
    	'routes'=>array(
    		array(
    			//выводим лог внизу страницы
    			'class'=>'CWebLogRoute',
    			'levels'=>'trace, info, profile',
    		),
    	),
    ),

    Вроде бы вы ничего не упустили, но покажите, пожалуйста, метод relations (в массиве, который он возвращает должен быть элемент gb_games).

    • Алекс

      Метод вот такой:
      public function relations()
      {
      return array(
      'gb_games' => array(self::MANY_MANY, 'Games', 'gb_games_types(gt_game_id, gt_type_id)', 'together'=> true ),
      );
      }
      Я еще по-всякому перепробовал — пока ничего не получается..

      • Я прошу прощения, немного вас запутал.
        Дело в том, что если вы хотите использовать limit для связанной таблицы в новых верисиях yii, нужно использовать «ленивую загрузку».
        Пример я переделал и вы можете скачать архив.
        Изменения:
        1) метод actionShowGames в контроллере TypesController;
        2) добавлено представление types/showGames.php;
        3) два дополнительных правила в роутере (main.php).
        В ближайшее время постараюсь написать пост на эту тему. Вообще-то, я не уверен, что использовал лучшее решение, но оно работает 😉

        • Алекс

          Спасибо! Кое-как разобрался, но очень интересно будет почитать про такие запросы.

  • Александр

    Владимир пожалуйста подскажите, что за переменная $update, где она инициализируется, по которой идёт проверка

    • Не могу найти переменную $update о которой вы спрашиваете. Напишите, пожалуйста, номера строк кода.

  • см

    фывапт

  • млин а можете создать отдельный пост «для самых маленьких» про дерево страниц и в зависимости от нажатия отображать ту или иную страницу с определенным занчением (передача ид по гет или пост). а еще бы «для самых самых маленьких» как создать отдельную страницу

    • Боюсь это будет не пост, а копия документации 🙂

      • документация ужасна. она больше запутывает, чем рассказывает. там слишком непонятно

        • Это вы зря. Никто не пишет документацию для того чтобы вас запутать. Вещи с которыми вы работаете в первый раз, практически всегда кажутся сложными.