Статьи

Создание веб-приложения с помощью Symfony 2: финализация

В части 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> , будет отображаться обработанное изображение.

Обработка изображений

В этом приложении перед отображением изображения мы делаем две вещи:

  1. В зависимости от того, существует изображение обложки книги, отобразите его или отобразите изображение обложки по умолчанию;
  2. Добавьте водяной знак и отрегулируйте размер (ширина 300 пикселей в режиме подробного просмотра книги и ширина 200 пикселей в виде списка чтения).

Полный код находится в src/tr/rsywxBundle/Controller/DefaultController.php в функции coverAction . Код прост и понятен, и я просто покажу вам вывод в подробном представлении как для обложки настоящей книги, так и для обложки по умолчанию:

Обратите внимание, что я создал папку «cover» в «web» для хранения обложки по умолчанию и отсканированных обложек книг. Я также использовал китайский TTF для отображения текстов водяных знаков. Пожалуйста, не стесняйтесь использовать свой собственный шрифт (и скопируйте этот шрифт в папку «cover»).

На сайте с большим трафиком правильный путь действий — кэшировать автоматически сгенерированные изображения, как это делал Лукас Уайт в своей статье , но я оставлю это на ваше усмотрение, чтобы поиграть с ним.

пагинация

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

Исходный код для класса находится в src/tr/rsywxBundle/Utility/Paginator.php .

Сам код легко читается и не содержит ничего особенно продвинутого, поэтому я просто расскажу о процессе.

В классе Pagination есть два фундаментальных значения:

  1. Сколько всего записей и сколько страниц в общей сложности?
  2. Что такое текущая страница и как создать легко доступный список страниц для дальнейшей обработки?

Многие классы нумерации страниц также часто имеют дело с поиском данных, но это не очень хорошая практика: нумерация страниц должна иметь дело только с вещами, относящимися к нумерации страниц, а не с самими данными. Данные являются доменом сущности / хранилища.

Если мы вернемся к реализации получения книг, соответствующих определенным критериям, вы заметите, как в контроллере разделены два шага (получение данных и разбиение на страницы):

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. Буду признателен за любую конструктивную критику и общие отзывы, которые вы можете мне представить.