В части 1 и части 2 этой серии я рассмотрел основы использования Symfony 2 для разработки функционирующего веб-сайта.
В этой части руководства я расскажу о более продвинутых методах и завершу проект с нумерацией страниц, водяными знаками изображений и NativeQuery.
Код, который мы будем использовать, идентичен коду из части 2 — функции уже есть, они просто не обсуждались.
Водяные знаки изображения
Поскольку уже есть множество манипуляций с изображениями и водяными знаками с помощью учебных пособий Imagick, доступных на SitePoint, и в связи с тем, что в этом конкретном проекте мы не выполняем никаких сложных манипуляций с изображениями, мы будем придерживаться стандартной библиотеки PHP для основной части изображений — GD.
В общем, когда есть «обработка изображения», мы говорим о действии или действиях, примененных к изображению, прежде чем мы в конечном итоге показываем его с помощью <img>
. В моем приложении это делается в два этапа:
- Создайте маршрут для захвата «запроса отображения изображения» и сопоставьте его с действием в контроллере.
- Реализуйте обработку в действии.
Конфигурация маршрута
Маршрут отображения изображения после обработки прост:
cover: pattern: /books/cover/{id}_{title}_{author}_{width}.png defaults: {_controller: trrsywxBundle:Default:cover, width: 300}
В шаблоне мы можем вызвать вызов для отображения обработанного изображения:
<img src="{{path("cover", {'id':book.id, 'author':author, 'title':book.title}) }}" alt="{{book.title}}'s cover" title="{{book.title}}'s cover"/>
Поэтому каждый раз, когда встречается этот <img>
, будет отображаться обработанное изображение.
Обработка изображений
В этом приложении перед отображением изображения мы делаем две вещи:
- В зависимости от того, существует изображение обложки книги, отобразите его или отобразите изображение обложки по умолчанию;
- Добавьте водяной знак и отрегулируйте размер (ширина 300 пикселей в режиме подробного просмотра книги и ширина 200 пикселей в виде списка чтения).
Полный код находится в src/tr/rsywxBundle/Controller/DefaultController.php
в функции coverAction
. Код прост и понятен, и я просто покажу вам вывод в подробном представлении как для обложки настоящей книги, так и для обложки по умолчанию:
Обратите внимание, что я создал папку «cover» в «web» для хранения обложки по умолчанию и отсканированных обложек книг. Я также использовал китайский TTF для отображения текстов водяных знаков. Пожалуйста, не стесняйтесь использовать свой собственный шрифт (и скопируйте этот шрифт в папку «cover»).
На сайте с большим трафиком правильный путь действий — кэшировать автоматически сгенерированные изображения, как это делал Лукас Уайт в своей статье , но я оставлю это на ваше усмотрение, чтобы поиграть с ним.
пагинация
Есть также много статей о разбиении на страницы большого набора данных. В этом уроке я покажу вам, как я это сделал в этом приложении, и мы проверим его.
Исходный код для класса находится в src/tr/rsywxBundle/Utility/Paginator.php
.
Сам код легко читается и не содержит ничего особенно продвинутого, поэтому я просто расскажу о процессе.
В классе Pagination есть два фундаментальных значения:
- Сколько всего записей и сколько страниц в общей сложности?
- Что такое текущая страница и как создать легко доступный список страниц для дальнейшей обработки?
Многие классы нумерации страниц также часто имеют дело с поиском данных, но это не очень хорошая практика: нумерация страниц должна иметь дело только с вещами, относящимися к нумерации страниц, а не с самими данными. Данные являются доменом сущности / хранилища.
Если мы вернемся к реализации получения книг, соответствующих определенным критериям, вы заметите, как в контроллере разделены два шага (получение данных и разбиение на страницы):
File location: src/tr/rsywxBundle/Controller/BookController.php
public function listAction($page, $key) { $em = $this->getDoctrine()->getManager(); $rpp = $this->container->getParameter('books_per_page'); $repo = $em->getRepository('trrsywxBundle:BookBook'); list($res, $totalcount) = $repo->getResultAndCount($page, $rpp, $key); //Above to retrieve data and counts //Below to instantiate the paginator $paginator = new \tr\rsywxBundle\Utility\Paginator($page, $totalcount, $rpp); $pagelist = $paginator->getPagesList(); return $this->render('trrsywxBundle:Books:List.html.twig', array('res' => $res, 'paginator' => $pagelist, 'cur' => $page, 'total' => $paginator->getTotalPages(), 'key'=>$key)); }
Конструктор моего пагинатора принимает 3 параметра:
- страница: чтобы сказать, что является текущей страницей. Это будет использоваться для возврата списка страниц, которые будут отображаться в виде кликабельных ссылок в шаблоне;
- totalcount: сообщить количество результатов. Это будет использоваться для вычисления общего количества страниц вместе с параметром rpp;
- rpp: сокращение от записей на страницу.
В моей текущей реализации я использовал простую версию разбиения на страницы, показывающую только ссылки на страницы «Первый», «Предыдущий», «Следующий» и «Последний», но вы можете попробовать разные типы, используя функцию getPagesList
.
Возьмите, например, список страниц, например:
1 2 3 4 5
Ключевым моментом здесь является функция getPagesList
которая гарантирует, что текущая страница всегда находится посередине, или, если страниц недостаточно, она проверяет, что она находится в правильном положении.
public function getPagesList() { $pageCount = 5; if ($this->totalPages <= $pageCount) //Less than total 5 pages return array(1, 2, 3, 4, 5); if($this->page <=3) return array(1,2,3,4,5); $i = $pageCount; $r=array(); $half = floor($pageCount / 2); if ($this->page + $half > $this->totalPages) // Close to end { while ($i >= 1) { $r[] = $this->totalPages - $i + 1; $i--; } return $r; } else { while ($i >= 1) { $r[] = $this->page - $i + $half + 1; $i--; } return $r; } }
Чтобы убедиться, что эта функция действительно работает, мы должны протестировать ее перед использованием. Мы будем использовать PHPUnit в качестве тестового стенда. Пожалуйста, обратитесь к официальному сайту для получения подробных инструкций по его установке . Я использовал способ phpunit.phar, чтобы загрузить пакет и поместить его в корневую папку моего проекта.
Чтобы протестировать класс Paginator, который мы только что создали, сначала нам нужно создать папку Utility
в src/tr/rsywxBundle/Tests
. Все тесты в Symfony должны идти под src/tr/rsywxBundle/Tests
. В папке Utility
создайте файл PHP с именем PaginatorTest.php
:
namespace tr\rsywxBundle\Tests\Utility; use tr\rsywxBundle\Utility\Paginator; class PaginatorTest extends \PHPUnit_Framework_TestCase { public function testgetPageList() { $paginator=new Paginator(2, 101, 10); $pages=$paginator->getTotalPages(); $this->assertEquals($pages, 11); $list=$paginator->getPagesList(); $this->assertEquals($list, array(1,2,3,4,5)); $paginator=new Paginator(7, 101, 10); $list=$paginator->getPagesList(); $this->assertEquals($list, array(5,6,7,8,9)); $paginator=new Paginator(10, 101, 10); $list=$paginator->getPagesList(); $this->assertEquals($list, array(7,8,9,10,11)); } }
Этот вид теста называется модульным тестом . Он тестирует конкретный модуль программы.
В функции testgetPageList
мы в основном создаем экземпляр объекта, который мы хотим протестировать (paginator), используя практически любую комбинацию параметров, о которой мы только можем подумать. Затем мы вызываем некоторые методы этого объекта и проверяем достоверность результата, используя утверждения. Здесь мы используем только метод assertEquals
.
В примере $this->assertEquals($list, array(7,8,9,10,11))
из $paginator
объекта $paginator
мы знаем, что в общей сложности должно быть 11 страниц (всего 101 запись и 10 записей на страницу), и страница 10 в качестве текущей страницы вернет список страниц 7,8,9,10,11
поскольку страница 10 очень близка к концу. Мы утверждаем это, и если это утверждение не выполняется, в логике функции должно быть что-то не так.
В нашей командной строке / терминале мы запускаем следующую команду:
php phpunit.phar -c app/
Это читает файл конфигурации для PHPUnit из папки app/
( phpunit.xml.dist
генерируется установкой Symfony. НЕ ИЗМЕНЯЙТЕ ЭТО!)
Примечание. Удалите все остальные тестовые файлы, автоматически сгенерированные Symfony (например, папку Controller
разделе « Tests
). В противном случае вы увидите хотя бы одну ошибку.
Приведенная выше команда проанализирует все тестовые файлы в разделе Tests
и убедится, что все утверждения пройдены. В приведенном выше примере вы увидите подсказку, в которой будет что-то вроде OK, 1 test, 4 assertions
. Это означает, что все тесты, которые мы создали, прошли и доказали, что функция работает правильно. Если нет, то должно быть что-то не так в коде (в реализации или в тесте).
Не стесняйтесь расширять тестовый файл для класса Paginator.
Рекомендуется всегда тестировать самодельный модуль перед его использованием в вашей программе.
Для более глубокого изучения PHPUnit и тестирования в PHP см. Любую из многочисленных статей SitePoint по PHPUnit .
NativeQuery
В нашей базе данных есть таблица book_visit
, мы используем метку времени в качестве типа данных, чтобы регистрировать время посещения страницы сведений о книге. Нам нужно сделать некоторую статистическую статистику посещений, и один из них — получить общее количество посещений за день (мой «день» находится в часовом поясе +8 часов).
В SQL это легко:
select count(v.bid) vc, date(from_unixtime(v.visitwhen+15*60*60)) vd from book_visit v group by vd order by vd
Выше 15*60*60
чтобы настроить время моего сервера в соответствии с моим часовым поясом.
Однако если вы попытаетесь использовать аналогичную грамматику в Symonfy (конечно, изменив имя таблицы на ее FQN), сообщение об ошибке скажет вам, что date function is not supported
. Чтобы решить эту проблему, можно использовать чистый SQL:
$q = $em->getConnection()->prepare('select count(v.bid) vc, date(from_unixtime(v.visitwhen+8*60*60)) vd from book_visit v group by vd order by vd'); $q->execute(); return $q->fetchAll();
Или в соответствии с рекомендациями Symfony и Doctrine , мы можем (и должны) использовать createNativeQuery
и ResultSetMapping
.
public function getVisitCountByDay() { $em = $this->getEntityManager(); $rsm=new \Doctrine\ORM\Query\ResultSetMapping; $rsm->addScalarResult('vc', 'vc'); $rsm->addScalarResult('vd', 'vd'); $q=$em->createNativeQuery('select count(v.bid) vc, date(from_unixtime(v.visitwhen+15*60*60)) vd from book_visit v group by vd order by vd', $rsm); $res=$q->getResult(); return $res; }
В приведенном выше примере наиболее важными утверждениями являются создание ResultSetMapping и добавление результатов в это отображение.
vc
(количество посещений) и vd
(дата посещения) дважды появлялись в вызове addScalarResult
. Первый — это имя столбца, которое будет возвращено из запроса, а второй — псевдоним этого столбца. Чтобы предотвратить усложнение создания большего количества имен, мы просто используем те же имена.
Скалярный результат описывает отображение одного столбца в наборе результатов SQL на скалярное значение в результате Doctrine. Скалярные результаты обычно используются для статистических значений, но любой столбец в наборе результатов SQL может быть отображен как скалярное значение.
Вышеуказанная функциональность не реализована в конечном коде. Возьми это как домашнюю работу.
Вывод
Это далеко не полный учебник для Symfony. Есть много не охваченных (формы, безопасность, функциональное тестирование, i18n и т. Д.), Которые могут легко занять еще 10-12 частей. Я настоятельно рекомендую вам прочитать полную официальную документацию, предоставленную Symfony, которую можно скачать здесь .
Я впервые пишу серию на PHP и для Sitepoint. Буду признателен за любую конструктивную критику и общие отзывы, которые вы можете мне представить.