CakePHP: Articles/Tutorials/Practicum

К сожалению редактор этой Wiki не позволяет создавать сложно форматированный текст, из-за чего в некоторых местах статьи трудно читаются. Некоторые участки кода не подсвечены т.к. при их записи я не указывал теги “<?php” и ?>". Это связано с тем что такой код помещается внутри файла/класса/функции.

Для просмотра статьи в оригинальном виде рекомендую Вам скачать её PDF-вариант: practical_intro_cake.pdf (16 страниц, шрифт Times New Roman, 12pt).

Автор: Kuzya

Сайт: http://kuzya.name

Оригинал статьи: Начальный практикум в CakePHP

Начальный практикум в CakePHP.

Официальный сайт фреймворка: http://cakephp.org/

Русскоязычный сайт фреймворка: /

Версия фреймворка на момент написания статьи: 1.2

1. Введение.

Здравствуйте. В этой статье я хочу описать базовые практические методы работы с фреймворком CakePHP (далее CP). Почему я пишу именно «практические»? Потому что в этой статье не будет описания основ CP, теории его дизайна, MVC и всего подобного. Здесь будет лишь практика. Во многих науках есть теоретическая и практическая части. Программирование не исключение. Теорию Вы можете почерпнуть из документации, а практика нарабатывается либо самостоятельно, либо берётся из различных статей, в том числи и из этой. Второй вариант естественно легче, а в связке с документацией является отличным стартом для начинающих или познающих инструмент «с нуля». Остановимся на документации подробнее. Так как у данного фреймворка есть представительство

в нашей стране, то есть и русскоязычный перевод документации. Его Вы можете найти в Wiki сайта (путеводитель по ней есть на главной странице). Описание версии 1.2 ещё не до конца переведено и находится на сайте в неполном виде. Зато документация к версии 1.1 переведена полностью и может служить хорошим подспорьем разработчикам для которых языковой барьер является существенным препятствием на пути изучения чего-то нового. Конечно же в текущей версии языка имеются отличия от предыдущей, но базовые основы и большая часть инструментария осталась та же. Поэтому в каких-либо трудных ситуациях Вы можете обращаться к описанию версии 1.1. Официальная документация находится по адресу http://book.cakephp.org/. В ней дано полное и, на мой взгляд, понятное описание всего что есть в CP. Иногда Вам может понадобится посмотреть описание метода или свойства определённого класса. В этом случае стоит обращаться по адресу http://api.cakephp.org/. Этот сайт содержит данные по всему что есть внутри фреймворка. Очень удобно сделаны несколько вариантов просмотра — по таблице классов и по списку файлов. Вы в любом случае найдёте то что Вас интересует. Для хорошего понимания материала читателю достаточно ознакомится с документацией в общих чертах. Желательно создать тестовое приложение. Можно самостоятельно, можно взять описание тестового блога. Главное, иметь хоть какое-то представление о работе фреймворка и структуре его документации (как на русскоязычном сайте, так и на официальном). Внимание! Обязательно ознакомьтесь с соглашениями Cake PHP (/wiki/Manual/BasicPrinciples/Conventions) и постарайтесь всегда держать их рядом. По началу у меня было много проблем именно из-за соглашений, поэтому что бы не мучаться почаще к ним обращайтесь. Целью нашей практики будет написание слабой имитации интернет-магазина. В этом магазине можно будет просматривать категории товаров, списки их содержимого, сами товары. Отдельно с товарами можно будет производить следующие операции — заказывать, голосовать за них и оставлять о них отзывы. Приступим.

2. Приготовления

Начнём с предварительной подготовки. Первым шагом Вам нужно скопировать файлы CP на «чистый» хост. У меня он называется «cakephp».Если хост у Вас назван по другому то будьте внимательны к ссылкам из статьи во избежание каких-либо проблем.

2.1 База данных

После копирования откройте файл «app/config/core.php» и измените значение настройки « на любое другое что бы фреймворк перестал показывать предупреждения безопасности (информацию по работе с настройками Вы можете получить здесь – http://cakephp. ru/wiki/Manual/Developing/Configuration/Core). Затем в файл настроек БД (app/config/database.php), в массив «default», внесите данные для подключения к базе. Так же добавьте в него ячейку «encoding»,

содержащую текст «utf8». Этим мы определим кодировку работы с БД. Теперь нужно заняться непосредственно данными. В прикреплённых файлах имеется дамп базы магазина — «database.sql». Импортируйте его в Вашу БД. При импорте, во избежание проблем с русскоязычным текстом, следует учитывать что содержимое файла имеет кодировку utf8. Структура получившейся базы очень проста. В ней всего 3 таблицы:

1. categories – содержит категории продуктов. Их у нас 3 — книги, музыка, электроника. Поля в этой таблице следующие: id – порядковый номер категории, cat_name – имя категории, cat_t_name – имя категории в транслите (для ЧПУ), about – описание категории.

2. comments – таблица с комментариями. Поля: id – номер комментария, product_id – номер продукта к которому оставлен комментарий, author – автор комментария и answer – его содержимое.

3. products – таблица содержащая продукты нашего псевдо-магазина. Поля в ней следующие: id – порядковый номер товара, category_id – номер категории к которой принадлежит товар, name – имя товара, t_name – транслитеррационное имя товара, about – описание, photo – фотография (они будут храниться в папке «img/products» в веб-директории «app/webroot») товара, rating – его рейтинг, cost – цена (единицы цен будут обсуждаться ниже).

2.2 Дизайн

Возьмёмся за внешний вид. Всё что нужно для этого находится в прикреплённом архиве «templates.zip». Директории «img» и «css» скопируйте прямо в «app/webroot», предварительно удалив такие-же старые директории. Далее пройдите в папку «./cake/libs/view/layouts/» и поместите туда шаблон default_layout.ctp», попутно переименовав его в «default.ctp». Это наш основной дизайн. Обратитесь к корню сайта и Вы его увидите. Обратите внимание на отладочную информацию внизу экрана. Там отображаются запросы к базе данных, количество затронутых ими рядов и т.д. Нам эта информация не понадобится и мы её отключим. Для этого снова откройте файл «./app/config/core.php» и установите параметр «debug» в 0. Обновите страницу. Информация о запросах пропала. Теперь можно перейти непосредственно к программированию.

3.0 Программирование.

Сейчас мы займёмся непосредственно кодом. Если в отношении него у Вас возникнут какие-то вопросы то обращайтесь к документации. Всё что описано ниже делалось именно по ней, хотя многочисленные ссылки на неё должны не дать Вам запутаться. В случае если у Вас что-то не будет получаться то попробуйте вернуться к началу раздела и перечитать его заново.

3.1 Категории.

Самое первое что должен делать наш магазин — показывать список категорий товаров. Для этого мы создадим контроллер categories и соответствующую модель. В папке контроллеров(app/controllers) создайте файл «categorie_controler.php». В классе нового контроллера объявите функцию «index», которая и будет выводить нужный нам список.

<?php

class CategorieController extends AppController

{

    function index()

    {

    

    }

}

?>

И создадим пустую модель, сохранив её в директории app/models. Файл в котором она будет храниться назовите «categorie.php».

<?php

class Categorie extends AppModel

{

}

?>

С функционалом всё очень просто. В контроллере мы будем получать массив категорий и передавать их отображению. Там они, с помощью цикла foreach, будут отображаться. Для получения данных можно пойти двумя путями — использовать стандартные средства выборки (http://cakephp.ru/wiki/Manual/Developing/Models/Retrieving) или создать и использовать свои. Я решил выбрать второй вариант т.к. он «чище» в плане возвращаемых в контроллер данных. В модель «Categorie»

добавим функцию getCatsList(), которая будет возвращать массив категорий. Вот её код:

function getCatsList()

{

    $cats = Array();

    $result = $this->findAll();

    foreach($result as $line)

                $cats[] = $line['Categorie'];

    return $cats;

}

У такого решения есть 2 плюса – в контроллер поступят уже обработанные данные, тогда как на выходе той же findAll-функции они достаточно «сырые» и требуют дополнительной обработки(что мы и делаем в теле функции), и соответствие концепции MVC по которой все операции с данными должны происходить лишь в модели.

Теперь в контроллер добавим вызов этой функции и передачу результатов её работы в отображение.

function index()

{

    $categories = $this->Categorie->getCatsList();

    $this->set('categories',$categories);

}

Последний шаг — отображение. Создайте в папке видов (app/views) директорию нашего контролёра — categorie. Поместите туда файл index.ctp со следующим содержимым.

<?foreach($categories as $categorie):?>

    <h2><?=$categorie['cat_name']?></h2>

    <div class="entry">

        <?=$categorie['about']?>&nbsp;&nbsp;<a href='/categorie/view/<?=$categorie['cat_t_name']?  >/'>Подробнее &gt;&gt;</a>

    <p></p>

<?endforeach;?>

Здесь всё крайне просто – проходим по массиву категорий и каждую отображаем в небольшом html-коде. В итоге, обратившись по ссылке http://cakephp/categorie/, Вы должны увидеть желаемый список.

Сделаем небольшое отступление и установим контроллер «categorie» контроллером по умолчанию. Для этого откройте файл /app/config/routes.php и замените две хранящиеся там настройки следующей строкой:

Router::connect('/', array('controller' => 'categorie', 'action' => 'index'));

Мы указали что при обращении к корню сайта нужно подгружать контроллер «categorie» и вызывать его метод «index». Подробнее о работе с роутами Вы можете почитать тут – /wiki/Manual/Developing/Configuration/Routes.

Как видно из прошлого отображения, за просмотр отдельной категории отвечает метод «view». При этом в ссылке ему передаётся её транслитеррационное имя. Все продукты мы будем получать стандартно — по порядковому номеру выбранной категории. Следовательно, нам нужно написать в модели функцию которая по переданному имени определит этот номер. Назовём эту функцию «getCatIdByTName»
function getCatIdByTName($cat_t_name)

{

       $result = $this->find("cat_t_name='$cat_t_name'",'id');

       return $result['Categorie']['id'];

}

При вызове ей должно передаваться имя категории в транслите, и с помощью своего метода find она получает её номер из таблицы. Описанный только что метод мы вызовем в самом начале метода «view» и с помощью его результата получим список товаров из категории.

Список товаров можно получить той же функцией find. Но мы поступим немного по иному и воспользуемся ассоциациями. Мне кажется что ассоциации являются очень удачным инструментом в Cake PHP. Подробнее о них Вы можете почитать по адресу /wiki/Manual/Developing/Models/Associations. Из четырёх ассоциаций нам потребуется лишь одна – «один ко многим». Но что бы связать 2 модели нужно сначала их создать. А у нас есть всего одна модель. Так как мы получаем список продуктов находящихся в таблице «products» то, следуя соглашениям, нужно создать модель «Product». Создадим её совершенно пустой:
<?php

class Product extends AppModel

{

}

?>

И теперь можем связать их с помощью ассоциации “hasMany”. Так как мы в первую очередь работаем с категориями то и ассоциацию будем располагать там-же. Связывать модели будем по полю «categorie_id» из таблицы продуктов. Вот код нашей ассоциации которая должна располагаться в модели «Categorie».

var $hasMany = array('Product' =>

    array('className'     => 'Product',

                'conditions'    => '',

                        'order'         => '',

                        'limit'         => '',

                        'foreignKey'    => 'category_id',

                        'finderQuery'   => ''

                     )

);

Ничего сложного. Мы лишь указали вторую модель и поле для связки. Теперь, при вызове метода «find» мы будем получать данные не только из таблицы категорий, но и из таблицы продуктов. Полученная информация будет иметь следующий вид.

array(2) {

  ["Categorie"]=> // Имя модели

  array(4) {

    // Данные запрошенной категории

  }

  ["Product"]=>  // Имя модели

  array(5) {

    [0]=>

    array(8) {

    Данные продукта 1

    }

    [1]=>

    array(8) {

    Данные продукта 2

    }

    [2]=>

    array(8) {

    Данные продукта 3

    }

    [3]=>

    array(8) {

    Данные продукта 4

    }

  }

}

Осталось собрать всё воедино и описать метод «view».

function view($cat_t_name)

{

    $categorie_id = $this->Categorie->getCatIdByTName($cat_t_name);

    $categorie = $this->Categorie->find("id='$categorie_id'");

    $this->set('categorie',$categorie);

}

Поработаем над отображением. Оно будет храниться в файле ./app/view/categorie/view.ctp.

<h2>Категория &lt;<?=$categorie['Categorie']['cat_name'];?>&gt;</h2>

<p class="posted"><?=$categorie['Categorie']['about'];?></p>

<div class="entry">

<?foreach($categorie['Product'] as $product):?>

    <p><a href='/product/view/<?=$product['t_name'];?>/'><?=$product['name'];?></a></p>

        <blockquote>

            <p>

                <?=$product['about'];?>

            <br /><br />

            </p>

        </blockquote>

    <br /><br /><br /><br />

<?endforeach;?>

</div>

Как видите, в начале шаблона мы отображаем данные о категории, а дальше о продуктах. Именно в этом мы и выиграли используя ассоциацию — за одно обращение к модели получили 2 группы данных.

Вы должны помнить о том что в самом начале статьи я писал о изображениях продуктов находящихся в веб-директории фреймворка, в папке «img». Их нету в приведенном выше отображении потому что ими мы займёмся отдельно. Сейчас мы напишем хэлпер который будет заниматься изменением размера изображений. Если Вы не знакомы с хэлперами то можете почитать про них тут — /wiki/Manual11/helpers (описание с версии 1.1) или тут – http://book.cakephp.org/view/98/Helpers (официальное описание для версии 1.2). Это небольшие инструменты которые можно использовать на уровне отображений. Хранятся пользовательские хэлперы в директории «./app/views/helpers/» (создание собственных хэлперов описано здесь – http://book.cakephp.org/view/101/Creating-Helpers). Так как наш хэлпер будет отвечать за работу с изображениями то назовём его «image». Следовательно в директории пользовательских хэлперов создаём файл image.php. Вписываем туда следующий код.
class ImageHelper extends AppHelper

{

    function resize($image_path,$w,$h='auto')

    {

        $image_name = basename($image_path);

        $cached_image_name = "./img/cache/{$image_name}_{$w}_{$h}.jpg";

        if(!file_exists($cached_image_name))

        {

            $src = imagecreatefromjpeg($image_path);

            list($width,$height)=getimagesize($image_path);

            if ($width < $w)

            {

                imagejpeg($image,$cached_image_name);

            } else {

                $a = $width/$w;

                if($h == 'auto') $h = ceil($height/$a);

                $image = imagecreatetruecolor($w,$h);

                imagecopyresampled($image,$src,0,0,0,0,$w,$h,$width,$height);

                imagejpeg($image,$cached_image_name);

            }

        imagedestroy($image);

    }

    $image_name = basename($cached_image_name);

    return $this->output('/img/cache/'.$image_name);

    }

}

Это простой класс с одним методом — resize. Он изменяет размер изображения и создаёт его кэшированную копию в директории ./app/webroot/img/cache. При этом метод возвращает адрес уже изменённой картинки. Обратите внимание на то что хэлпер работает только с JPG-изображениями. Это сделано из-за того что все фотографии продуктов у нас хранятся в формате JPG. Для активизации поместим его имя в массив «helpers» контроллера.

var $helpers = Array('Image');

Пропишем вызов функции resize в шаблоне view.ctp. Прямо перед отображением описания товара ($product['about']) поместим следующий код.

<img src='<?=$image->resize('./img/products/'.$product['photo'],60);?>' hspace='5' vspace='5' align='left'/>

Если Вы всё сделали правильно то при просмотре категории отобразятся изображения шириной 60 пикселей.

3.2 Продукты

Займёмся продуктами. Из отображения списка товаров видно что за их просмотр отвечает метод «view» контроллера «products». Создадим его. В нужном нам методе мы просто будем через модель (которая уже создана) получать и передавать в отображение данные товара. Из-за того что в ссылке передаётся имя продукта в транслите нам нужно описать в модели функцию получения порядкового номера по этому имени.

function getProductIdByTName($t_name)

{

        $result = $this->find("t_name='$t_name'","id");

    return $result['Product']['id'];

}

Всё практически точно так же как и в категориях. Код метода «view» будет следующий.

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). Для начала добавим его в контроллер

var $components = Array('Session');

Теперь в конец метода view впишем запоминание текущего товара (ведь при просмотре следующего он будет последним просмотренным) и, если в сессии уже записан такой номер (пользователь смотрел что-то до этого), получение данных о прошлом продукте и передаче их в отображение. Для этого мы добавим в модель Product функцию получения данных о товаре по его порядковому номеру. Она очень проста.

function getProductData($product_id)

{

        $product_id = (int) $product_id;

        $result = $this->find("id={$product_id}");

        

    return $result['Product'];

}

В сам метод контроллера добавляем код работы с последним товаром.

// Читаем содержимое ячейки 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);

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

<?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'>&nbsp;2

<input type='radio' name='data[estimation]' value='3'>&nbsp;3

<input type='radio' name='data[estimation]' value='4'>&nbsp;4

<input type='radio' name='data[estimation]' value='5'>&nbsp;5&nbsp;&nbsp;&nbsp;

<input type='submit' value='Оценить'>

</form>

А в контроллер, работающий с продуктами, добавим метод «change_rating». Действия, происходящие в нём, будут делиться на 2 этапа. В начале мы вызовем метод поднятия рейтинга, которому передадим номер товара и его оценку, а затем перенесём пользователя обратно на просмотр этого товара. Функция поднятия рейтинга должна располагаться в модели и имеет следующий код:

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);

}

Теперь нужно сформировать метод контроллера.

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). Добавьте его имя в массив загружаемых компонентов.

var $components = Array('Email','Session');

Для отправки письма мы установим 3 свойства, название которых говорит само за себя — «to», «from» и «subject». И с помощью метода «send» отправим письмо менеджеру.

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. Подключите их в контроллере продукта
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)

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 ограничения — имя автора должно состоять только из букв и цифр, и минимальная его длинна должна быть не менее трёх символов.

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» становится следующим.

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).

App::import('Sanitize');

Далее к обрабатываемым данным может быть применено несколько функций. Но мы воспользуемся лишь одной — вырезанием спец-символов HTML. Для этого сразу после установки поля «product_id» добавим следующие строки.

// Чистим HTML-спецсимволы в тексте ответа

$this->data['answer'] = Sanitize::html($this->data['answer']);

Вот и всё. Управлять данным классом крайне просто. Стоит лишь заметить что функция «paranoid», этого класса, русские буквы удаляет начисто. Поэтому не стоит ею пользоваться при создании русскоязычных сайтов.

Дело за малым. Осталось организовать вывод сообщений на странице продуктов. Вернёмся в контроллер «Product». Так как мы собираемся использовать таблицу из другой модели, то есть использовать 2 модели сразу, нам нужно их указать в свойстве «uses» контроллера.

var $uses = Array('Product','Comment');

Теперь мы можем обращаться к модели комментариев так же как и к модели продуктов. Для получения списка отзывов мы можем воспользоваться уже знакомой нам функцией «find», а можем пойти по иному пути. Мы задействуем «магическую» функцию «Find All By». Её «волшебство» заключается в том что после её название Вы можете указать имя поля по которому требуется извлечь данные. То есть если мы берём комментарии из таблицы по номеру продукта (поле product_id), то можем получить их функцией «Find All By Product_id».

$comments = $this->Comment->findAllByproduct_id($product_id);

Настолько всё просто. Далее нам следует передать полученный массив отзывов в отображение

$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). Обратите внимание на то что последняя должна передаваться в виде массива в формате «поле_для_сортировки => тип_сортировки».

var $paginate = array(

        'limit' => 4,

        'order' => array('id' => 'desc')

 );

Теперь мы можем использовать метод «paginate» этого хэлпера для получения списка комментариев текущей страницы (её номер будет передаваться в ссылке, об этом чуть ниже). Вызовом этого метода нужно заменить вызов функции «Find All By*».

$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>

Обновите страницу и Вы увидите что в самом её низу появились переключатели между отзывами.

3.4 Кэширование.

К счастью разработчиков CP представляет несколько механизмов управления кэшированием. На мой взгляд они очень удобны. С их помощью мы сейчас настроем кэширование страниц с описаниями продуктов и отзывами о них. Стандартно, предлагается несколько вариантов хранения кэшированных данных, но мы будем использовать файловый, он стоит по умолчанию. За все нужные нам операции отвечает хэлпер «Cache» (http://book.cakephp.org/view/213/Cache). Подключив его, настроим кэширование только результата действия view (http://book.cakephp.org/view/346/Caching-in-the-Controller). Для этого обозначим массив «cacheAction» как свойство контроллера и поместим туда ячейку «view/» со значением «86400».

var $cacheAction = array(

        'view/' => 86400

);

Тем самым мы указываем CP что все результаты действия view нужно кэшировать на 86400 секунд, что эквивалентно 24 часам. Теперь откройте конфигурацию ядра («./app/config/core.php») и раскомментируйте установку опции «Cache.disable», заменив её значение на «false». И тоже самое сделайте с объявлением настройки «Cache.check». Сохраните изменения и посмотрите страницу любого товара. Затем откройте директорию «./app/tmp/cache/views», в которой хранится кэш отображений. Если Вы всё сделали правильно то там будет находится файл с именем типа «product_view_canon_hg20_black.php» в котором будет содержаться кэш просмотренной Вами страницы. Осталось научиться очищать кэш конкретных страниц, ведь их контент изменяется когда мы оставляем отзыв о товаре. Механизм кэширования CP уникален тем что кэш автоматически удаляется если через модель, работающую с кэшированным действием, внести какие-то изменения в базу (http://book.cakephp.org/ar/view/348/Clearing-the-Cache). Мы бы могли этим воспользоваться если бы комментарии вносились в базу через модель «Product», но как Вы помните отзывы в базу вносятся через «Comment». Следовательно, нужно обнулять кэш вручную. Для таких действий в CP используется глобальная функция «clearCache». Ей в качестве параметра нужно передать имя закэшированной странице в формате «контролллер_действие_параметр1_параметр2_параметрN». У нас это будет «product_view_имя_продукта_в_транслите». В контроллере «Comment», сразу после вызова метода «save» пишем следующее:

clearCache('product_view_'.$product_t_name);

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

3.5 Логирование

И последнее что будет рассмотренно в этой статье — логирование данных о работе приложения (http://book.cakephp.org/ar/view/157/Logging). Подобный функционал может очень пригодиться для отслеживания ошибок в крупных приложениях, когда сходу не сообразишь где и какой произошла сбой. Никаких компонентов или хэлперов подключать здесь не нужно. Единственная функция, нужная для логирования, доступна всегда. Это функция «log». Ей нужно передавать 2 параметра. Первый — сообщения. Им может быть информация об ошибке, сбое, действии пользователя и т.п. А второй — тип логируемой информации. В качестве этого параметра может быть передана одна из двух констант — «LOG_ERROR» и «LOG_DEBUG» – ошибочная или отладочная информация соответственно. Рассмотрим пример с ошибкой. В методе «view» контроллера «Product» может быть ошибочно указан товар в ссылке. В связи с этим метод «getProductIdByTName» вернёт ложное значение. Давайте добавим после вызова этого метода строку

if(!$product_id) $this->log('Request to non-existing product', 'products');

и пройдём по ссылке http://cakephp/product/view/11111111, вызвав тем самым несуществующий товар. Сообщение будет записано в файл «error.log». Он находится там же где и все логи — в директории «./app/tmp/logs». Открыв его Вы обнаружите что самая последняя надпись содержит информацию типа этой – «2009–03–16 20:07:33 Error: Request to non-existing product». А можно вообще выделить в отдельную группу например только лог-записи связанные с товарами. Для этого в качестве константы можно передать строку, скажем, «products». В таком случае автоматически создастся файл «products.log» и в него будет записана вся передаваемая информация.

Вот и всё. Код нашего магазина готов и нормально функционирует. Если у Вас что-то не получилось то можете взять исходный код всего приложения в прикреплённом архиве «ready.zip».

4.0 Заключение.

На мой взгляд фреймворк хороший, но очень сильно запутанный. Иногда использование некоторых функций не укладывается в голове — по логике они должны использоваться, например, в контроллере, а используются в модели. Что-то должно быть компонентом, а является хэлпером. Не понравились и инструменты для работы с БД. Я больше предпочитаю интерфейсы подобные паттерну ActiveRecord. Ну и худой функционал — мало хэлперов и компонентов. И всё-таки, как говориться, на вкус и цвет... Надеюсь, что он понравится читателям. Удачи Вам в Ваших проектах!