3.2 Продукты
Займёмся продуктами. Из отображения списка товаров видно что за их просмотр отвечает метод «view»
контроллера «products». Создадим его. В нужном нам методе мы просто будем через модель (которая
уже создана) получать и передавать в отображение данные товара. Из-за того что в ссылке передаётся
имя продукта в транслите нам нужно описать в модели функцию получения порядкового номера по
этому имени.
PHP код:
function getProductIdByTName($t_name)
{
$result = $this->find("t_name='$t_name'","id");
return $result['Product']['id'];
}
Всё практически точно так же как и в категориях. Код метода «view» будет следующий.
PHP код:
function view()
{
$product_id = $this->Product->getProductIdByTName($t_name);
$product = $this->Product->find("id='$product_id'");
$this->set('product',$product['Product']);
}
Ничего сложного. Осталось поработать над отображением. Оно достаточно простое.
Код:
<h2><?=$product['name'];?></h2>
<div class="entry">
<p>
<img src='/img/products/<?=$product['photo'];?>' width='70px' hspace='5' vspace='5' align='left' />
<?=$product['about'];?>
<br />Цена: <?=$product['cost'];?>
<br /><br />
<br />Рейтинг:<?=$product['rating'];?>
</p>
<br />
</div>
Проверьте правильно ли отображается информация о выбранном товаре. Сейчас немного приукрасим
выводимую информацию с помощью хэлпера Number (http://book.cakephp.org/view/215/Number). У него
есть функция currency с помощью которой можно отображать числа в денежном формате — в долларах,
евро и марках (если я не ошибаюсь). В качестве аргументов ей нужно передать число и формат
выводимых денежных единиц. Мы выведем цену продукта в евро. Для этого в контроллер, в список
используемых хэлперов, нужно добавить хэлпер Number. А в отображении строку
Код:
<br />Цена: <?=$product['cost'];?>
заменить на вызов метода «currency»
Код:
<br />Цена: <?=$number->currency($product['cost'],'EUR');?>
Обновите страницу и Вы увидите что цена отображается в новом формате
Сейчас мы дополним просмотр продукта и чуть пониже информации о нём будем отображать товар
который был просмотрен до этого. Здесь нам поможет компонент для работы с сессиями Session
(http://book.cakephp.org/view/173/Sessions). Для начала добавим его в контроллер
PHP код:
var $components = Array('Session');
Теперь в конец метода view впишем запоминание текущего товара (ведь при просмотре следующего он
будет последним просмотренным) и, если в сессии уже записан такой номер (пользователь смотрел что-
то до этого), получение данных о прошлом продукте и передаче их в отображение. Для этого мы
добавим в модель Product функцию получения данных о товаре по его порядковому номеру. Она очень
проста.
PHP код:
function getProductData($product_id)
{
$product_id = (int) $product_id;
$result = $this->find("id={$product_id}");
return $result['Product'];
}
В сам метод контроллера добавляем код работы с последним товаром.
PHP код:
// Читаем содержимое ячейки Product.last. В ней у нас будет храниться этот номер.
$last_product_id = $this->Session->read('Product.last');
// Если в ней есть номер продукта и он не совпадает с текущим
// даём отображению эти данные
if($last_product_id && $last_product_id != $product_id)
{
$last_product_data = $this->Product->getProductData($last_product_id);
$this->set('last_product',$last_product_data);
} // Записываем номер текущего продукта как последний просмотренный
$this->Session->write('Product.last',$product_id);
Теперь при просмотре двух или более товаров в отображение будет передаваться номер последнего
просмотренного продукта. В него, в самый конец, мы добавим следующий код:
PHP код:
<?if(isset($last_product)):?>
До этого Вы смотрели: <a href='/product/view/<?=$last_product['t_name'];?>/'><?=$last_product['name'];?
></a>
<?endif;?>
Данные товара отобразятся если будут переданы в массиве «last_product». Если нет, то клиенту не будет
показано никакого упоминания о прошлом товаре. Попробуйте проверить это самостоятельно.
Следующий на очереди рейтинг продуктов. Сразу после записи о последнем товаре мы
отобразим форму его оценки по пятибальной шкале.
Код:
<form action='/product/change_rating/<?=$product['id'];?>/' method='POST'>
Ваша оценка товару: <input type='radio' value='1'> 1
<input type='radio' name='data[estimation]' value='2'> 2
<input type='radio' name='data[estimation]' value='3'> 3
<input type='radio' name='data[estimation]' value='4'> 4
<input type='radio' name='data[estimation]' value='5'> 5
<input type='submit' value='Оценить'>
</form>
А в контроллер, работающий с продуктами, добавим метод «change_rating». Действия, происходящие в
нём, будут делиться на 2 этапа. В начале мы вызовем метод поднятия рейтинга, которому передадим
номер товара и его оценку, а затем перенесём пользователя обратно на просмотр этого товара. Функция
поднятия рейтинга должна располагаться в модели и имеет следующий код:
PHP код:
function rating($product_id,$estimation)
{
// Указыаем id продукта для формирования условия WHERE при обновлении
$this->id = $product_id;
// Получаем текущий рейтинг продукта и к нему прибавляем оценку
$rating = $this->find("id='$product_id'","rating");
$rating = $rating['Product']['rating'];
$rating += $estimation;
// Формируем массив данных для обновления с одним полем - "rating"
$data = Array('rating'=>$rating);
// Сохраняем изменённый рейтинг у товара, номер которого указан в $this->id
$this->save($data);
}
Теперь нужно сформировать метод контроллера.
PHP код:
function change_rating($product_id)
{
// Изменяем рейтинг
$this->Product->rating((int)$product_id,(int)$this->data['estimation']);
// Получаем данные текущего продукта (нам нужно его имя в транслите для редиректа)
$product = $this->Product->find("id={$product_id}","t_name");
// Показываем окно редиректа с сообщением о том что оценка сохранена
$this->flash('Estimation saved','/product/view/'.$product['Product']['t_name']);
}
Думаю этот код не вызовет вопросов. Перед проверкой его работоспособности нам нужно заменить
стандартный шаблон flash-страниц (http://book.cakephp.org/view/426/flash). Возьмите его а архиве
шаблонов, приложенном к статье, (он называется «flash_layout.ctp») и скопируйте в папку
«./cake/libs/view/layouts/», переименовав во «flash.ctp». Теперь можно поставить любому товару оценку и
посмотреть как всё сработает.
Следующей частью функционала, которой мы займёмся, будет формирование заказа. На
страничке каждого товара будет выводиться специальная форма с именем и адресом заказчика. После её
заполнения данные будут уходить на e-mail менеджеру и заказ будет считаться оформленным. В
отображении код этой формы будет располагаться самым последним.
Код:
<br />
<br />
<h3>Хотите заказать?</h3>
<form action="/product/order/<?=$product['id'];?>/" method="post" id="commentform">
<p><label for="author">Ваше имя:</label>
<input type="text" name="data[customer]" id="author" value="" size="22" tabindex="1" /></p>
<label for="answer">Ваш адрес:</label>
<p valign='top'><textarea name="data[address]" id="answer" cols="40" rows="3"
tabindex="4"></textarea></p>
<p><input name="submit" type="submit" class="submit" tabindex="5" value="Заказать" />
</p>
</form>
Далее мы опишем метод «order». Так как оформленный заказ уходит на E-mail то нам понадобится
одноимённый компонент для работы с электронными письмами
(http://book.cakephp.org/view/176/Email) . Добавьте его имя в массив загружаемых компонентов.
PHP код:
var $components = Array('Email','Session');
Для отправки письма мы установим 3 свойства, название которых говорит само за себя — «to», «from» и
«subject». И с помощью метода «send» отправим письмо менеджеру.
PHP код:
function order($product_id)
{
// Получаем данные о заказываемом продукте
$product_data = $this->Product->getProductData($product_id);
// От кого идёт письмо
$this->Email->from = 'Site <noreply@our.shop>';
// Кому идёт письмо
$this->Email->to = 'Manager <manager@our.shop>';
// Тема письма
$this->Email->subject = 'New order';
// Формируем текст письма
$text = "Hello manager. We have new order. Recipient is {$this->data['customer']}. His address - {$this-
>data['address']}. Customer need product `{$product_data['name']}`";
// и отправляем
$this->Email->send($text);
}
Для проверки работоспособности данного кода я использовал заглушку для sendmail, которая имеется в
комплекте Denwer 3. Она не отправляет письма, а складывает их в папку «tmp». Попробуйте отправить
заказ и с сайта должно уйти письмо типа «Hello manager. We have new order. Recipient is Kuzya. His
address - Russia, my town. Customer need product `Независимая Украина. Крах проекта`». Т.к. у нас нет
шаблона для действия «order», CP может показать Вам ошибку отсутствия страницы - «Not Found. Error:
The requested address '/product/order/11' was not found on this server.» Это не означает того что данной
страницы, контроллера или действия нет на самом деле. Если Вы в настройках вернёте параметр
«debug» в значение 2 то увидите что за место этой ошибки покажется сообщение об отсутствии
отображения для действия «order». Мы бы могли создать отображение с надписью типа «Ваш заказ
принят», или с помощью flash-метода возвращать пользователя обратно, но мы поступим немного по
другому.
Форму для отправки заказа мы сейчас переведём на AJAX. Из-за этого страница продукта, при
отправке, не будет перезагружаться. Как Вы наверное уже догадались — для наших целей мы будем
использовать хэлперы. Их будет 2 — Ajax и Javascript. Подключите их в контроллере продукта
PHP код:
var $helpers = Array('Number','Ajax','Javascript');
Информацию о них Вы можете получить по ссылкам http://book.cakephp.org/view/208/AJAX и
http://book.cakephp.org/view/207/Javascript. Прежде чем пойти дальше сделаю ещё одно небольшое
отступление. Для того что бы работали оба эти хэлпера нужно загрузить JS-скрипты с сайтов
http://www.prototypejs.org/ и http://script.aculo.us/ . Но можно загрузить архив лишь с последнего. После
его загрузки пройдите в директорию архива «src» и файл «scriptaculous.js» скопируйте в папку
«./app/webroot/js/». Туда же поместите файл «prototype.js» из директории «lib». Можно возвращаться к
коду. Для активизации обоих библиотек в самое начало отображения впишите следующий код:
Код:
<?=$javascript->link('prototype');?>
<?=$javascript->link('scriptaculous');?>
Он автоматически создаст ссылки на требуемые скрипты и они будут подключаться при загрузке
страницы. Осталась фоновая отправка формы. В этом нам поможет метод «submit»
(http://book.cakephp.org/view/629/submit) «Ajax»-хэлпера. Он возвращает код кнопки отправки формы,
при нажатии на которую данные отправляются на сервер в фоновом режиме. Ему нужно передать 2
параметра — надпись, которая отобразиться на кнопке, и массив настроек. Настроек мы укажем только
две. Это будут адрес отправки и вызов функции «alert» после отправления данных (сообщение о
принятии заказа). В итоге код старой кнопки должен замениться вот таким:
Код:
<?=$ajax->submit('Отправить',Array('url'=>"/product/order/{$product['id']}/",'after'=>'alert("Your order is
sended!")'));?>
Попробуйте теперь обновить страницу и оформить заказ. Браузер должен показать сообщение «Your
order is sended», а с сайта должно отправиться письмо с заказом.
3.3 Комментарии к продуктам
Сейчас мы добавим ко всему сделанному ещё и возможность оставлять отзывы о товаре. Для этого мы
будем использовать контроллер comment и модель с таким же именем. Пока что создайте их совершенно
пустыми классами. Вернёмся к шаблону просмотра товара и в самый низ добавим форму отправки
сообщений.
Код:
<h3>Оставьте отзыв о товаре</h3>
<form action="/comment/send/<?=$product['id'];?>/<?=$product['t_name'];?>/" method="post"
id="commentform">
<p><input type="text" name="data[author]" id="author" value="" size="22" tabindex="1" />
<label for="author">Ваше имя</label></p>
<p><textarea name="data[answer]" id="answer" cols="40" rows="3" tabindex="4"></textarea></p>
<p><input name="submit" type="submit" class="submit" tabindex="5" value="Опубликовать" /></p>
</form>
Из кода видно что для сохранения данных в контроллере «comment» мы будем использовать метод
«send». Этому действию будут переданы всего 2 поля — автор и его отзыв. Для вставки в таблицу их
недостаточно. В массив «data» мы поместим номер продукта (передаваемый как параметр нашему
методу) и нулевое значение поля «id» (для auto increment`a)
PHP код:
function send($product_id,$product_t_name)
{
// Устанавливаем id будущей записи в 0 т.к. в таблице комментариев у нас
// автоматическое увеличение счётчика (auto increment)
$this->data['id'] = 0;
// Устанавливаем номер продукта
$this->data['product_id'] = $product_id;
// Сохраняем данные
$this->Comment->save($this->data);
// Показываем сообщение о успешном сохранении комментария
// и отправляем пользователя обратно
$this->flash('Comments saved!','/product/view/' . $product_t_name);
}
Как любой добросовестный разработчик мы должны провести и проверку введённых данных. Для этого
в CP имеются 2 инструмента Validation(http://book.cakephp.org/view/125/Data-Validation) и Sanitize(http://
book.cakephp.org/view/153/Data-Sanitization/). Воспользуемся обоими. Имя автора мы проверим с
помощью первого, а поле отзыва — с помощью второго. Для валидации нужно создать соответствующее
правило в модели. Установим по нему 2 ограничения — имя автора должно состоять только из букв и
цифр, и минимальная его длинна должна быть не менее трёх символов.
PHP код:
var $validate = Array(
'author'=>Array(
'rule'=>'alphaNumeric',
'minLength'=>3,
)
);
Обратите внимание на то что под правило «alphaNumeric» русские буквы не попадают. Саму проверку
мы будем осуществлять в контроллере с помощью метода «validates» (http://api.cakephp.org/class/model
— описание этого метода, http://book.cakephp.org/fr/view/410/Validating-Data-from-the-Controller —
описание проведения валидации через контроллер) который в случае удачной проверки вернёт «true». В
связи с этим логика сохранения комментария немного изменится. Перед сохранением мы передадим
данные в модель, проверим их, и только после этого произведём добавление в базу. В итоге код метода
«send» становится следующим.
PHP код:
function send($product_id,$product_t_name)
{
// Устанавливаем id будущей записи в 0 т.к. в таблице комментариев у нас
// автоматическое увеличение счётчика (auto increment)
$this->data['id'] = 0;
// Устанавливаем номер продукта
$this->data['product_id'] = $product_id;
// Передаём данные модели
$this->Comment->set($this->data);
// Проверяем их
if($this->Comment->validates())
{
// Если данные прошли проверку то проводим сохранение
$this->Comment->save($this->data);
// И показываем сообщение об успешном сохранении комментария
// отправляя пользователя обратно
$this->flash('Comments saved!','/product/view/' . $product_t_name);
} else {
// Если проверка не пройдена то сообщаем об этом пользователю
// и отправляем его обратно
$this->flash('Input error!','/product/view/' . $product_t_name);
}
}
Примемся за «Sanitize». Подключается он к приложению методом «import», класса «App»
(http://book.cakephp.org/view/499/The-App-Class).
Далее к обрабатываемым данным может быть применено несколько функций. Но мы воспользуемся
лишь одной — вырезанием спец-символов HTML. Для этого сразу после установки поля «product_id»
добавим следующие строки.
PHP код:
// Чистим HTML-спецсимволы в тексте ответа
$this->data['answer'] = Sanitize::html($this->data['answer']);
Вот и всё. Управлять данным классом крайне просто. Стоит лишь заметить что функция «paranoid»,
этого класса, русские буквы удаляет начисто. Поэтому не стоит ею пользоваться при создании
русскоязычных сайтов.
Дело за малым. Осталось организовать вывод сообщений на странице продуктов. Вернёмся в
контроллер «Product». Так как мы собираемся использовать таблицу из другой модели, то есть
использовать 2 модели сразу, нам нужно их указать в свойстве «uses» контроллера.
PHP код:
var $uses = Array('Product','Comment');
Теперь мы можем обращаться к модели комментариев так же как и к модели продуктов. Для получения
списка отзывов мы можем воспользоваться уже знакомой нам функцией «find», а можем пойти по иному
пути. Мы задействуем «магическую» функцию «FindAllBy». Её «волшебство» заключается в том что
после её название Вы можете указать имя поля по которому требуется извлечь данные. То есть если мы
берём комментарии из таблицы по номеру продукта (поле product_id), то можем получить их функцией
«FindAllByProduct_id».
PHP код:
$comments = $this->Comment->findAllByproduct_id($product_id);
Настолько всё просто. Далее нам следует передать полученный массив отзывов в отображение
PHP код:
$this->set('comments',$comments);
и в сам шаблон добавить код показа сообщений.
Код:
<?foreach($comments as $comment):?>
<!-- ### Post Entry Begin ### -->
<div class="post">
<h2>Answer from <?=$comment['Comment']['author'];?></h2>
<div class="entry">
<p><?=$comment['Comment']['answer'];?></p>
</div>
</div>
<!-- ### Post Entry End ### -->
<?endforeach;?>
Поместите его в самый конец. Вы можете проверить работу комментариев посмотрев отзывы о продукте
«Canon HG20». О нём заранее написано 5 отзывов.
Последняя часть связанная с пользовательским функционалом — это разбиение отзывов на
страницы. За подобные операции отвечает класс «Paginator»
(http://book.cakephp.org/view/164/Pagination ) . Как пример возьмём всё тот же «HG20» с его
комментариями. Разбиение будет производиться по 4 комментария на страницу. Всё очень просто. Для
начала нужно добавить требуемый класс в массив подключаемых хэлперов. Далее требуется определить
свойство контроллера «paginate», в котором указываются параметры разбиения на страницы. Нам будет
достаточно лишь двоих опций — числа комментариев (limit) и поля сортировки (order). Обратите
внимание на то что последняя должна передаваться в виде массива в формате «поле_для_сортировки =>
тип_сортировки».
PHP код:
var $paginate = array(
'limit' => 4,
'order' => array('id' => 'desc')
);
Теперь мы можем использовать метод «paginate» этого хэлпера для получения списка комментариев
текущей страницы (её номер будет передаваться в ссылке, об этом чуть ниже). Вызовом этого метода
нужно заменить вызов функции «FindAllBy*».
PHP код:
$comments = $this->paginate('Comment');
Просмотрев теперь страницу с отзывами Вы обнаружите что на ней сообщений осталось ровно столько
сколько нам было нужно. Осталось немного — отобразить ссылки для навигации по страницам. Это
можно сделать с помощью методов «prev», «next» и «counter» (их описание Вы можете найти по
следующей ссылке- http://api.cakephp.org/class/paginator-helper). Последний просто отображает какая
страница из скольких просматривается (1 из 3, 4-ая из 5, и т.д.). А вот первым двум нужно передать как
минимум 2 параметра — название ссылки на следующую/предыдущую страницу и массив настроек.
Как названия мы передадим «<< Previous» и «Next>>» для переключения на предыдущую и следующую
страницы соответственно. А из настроек передадим лишь один параметр - «url». Он будет содержать
транслитеррационное имя продукта для того что бы при переходе между страниц формировался
правильный адрес. Засовываем эти 3 метода в простенькую таблицу и получаем следующий код.
Код:
<table>
<tr>
<td>
<?=$paginator->prev('<< Previous ', Array('url'=>Array($product['t_name'])));?>
</td>
<td>
<?=$paginator->counter();?>
</td>
<td>
<?=$paginator->next(' Next >>', Array('url'=>Array($product['t_name'])));?>
</td>
</tr>
</table>
Обновите страницу и Вы увидите что в самом её низу появились переключатели между отзывами.