Askeet. День девятый. Локальные улучшения

Приветствуем!

Хотите что-то написать?

Нужно назвать себя.

Если вы пришли в первый раз,
то нужно зарегистрироваться.

Читайте нас в:

Блог на ya.ru
Блог на Деловом квартале
Блог на Twitter.om
Блог на Livejournal.com
15 декабря

ПереводыAskeet. День девятый. Локальные улучшения

Предисловие

В течение восьмого дня, мы добавили AJAX в "Спроси-ка" без головной боли. Приложение теперь готово к использованию, но его можно сделать удобнее. Для вопросов должен быть разрешён текстовый редактор, а первичные ключи не стоит использовать в URI. Всё это не трудно сделать с помощью symfony: сегодня будет хорошая возможность использовать то, чему научились, и проверить знаете ли Вы как управлять всеми уровнями архитектуры MVC.

Поддержка текстового редактора в вопросах и ответах

Markdown

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

Если вы смотрели документацию по symfony в текстовом формате, возможно вы заметили, что мы большие фанаты Markdown. Markdown - это утилита для преобразования текста в HTML, имеющая свой синтаксис для форматирования текста. Большое преимущество Markdown над, например, синтаксисом Wiki или форумов, это то, что обычный тестовый файл markdown очень легко читается:

Test Markdown text
------------------

This is a **very simple** example of [Markdown][1].
The best thing about markdown is its _auto-escape_ feature for code chunks:

<a href="http://www.symfony-project.com">link to symfony</a>

>The `<` and `>` are properly escaped as `&lt;` and `&gt;`,
>and are not interpreted by any browser

[1]: http://daringfireball.net/projects/markdown/ "Markdown"

Markdown выведет:

Test Markdown text

This is a very simple example of Markdown. The best thing about markdown is its auto-escape feature for code chunks:

 <a href="http://www.symfony-project.com">link to symfony</a>

The < and > are properly escaped as &lt; and &gt;, and are not interpreted by any browser

Библиотека Markdown

Хотя Markdown изначально написан на Perl, он доступен как библиотека PHP Markdown. Её то мы и будем использовать. Скачайте файл markdown.php и положите в папку lib проекта "Спроси-ка" (askeet/lib/). Вот и всё: Теперь она доступна из любого класса проекта "Спроси-ка", при условии что вы её сначала подключите:

require_once('markdown.php');

Мы могли бы вызывать конвертер Markdown каждый раз, когда отображаем сообщение, но это будет слишком нагружать наши серверы. Лучше мы конвертируем текст в HTML на этапе создания вопроса, и будем хранить HTML версию в таблице Question. Возможно Вы используете это, так что расширение модели не будет для Вас сюрпризом.

Расширение модели

Сначала добавим колонку в таблицу Question в schema.xml:

<column name="html_body" type="longvarchar"  />

Затем пересоздадим модель и обновим базу данных:

$ symfony propel-build-model
$ symfony propel-build-sql
$ symfony propel-insert-sql

Перегрузка метода setBody

Когда вызывается метод ->setBody() класса Question, колонка html_body должна также обновиться, путём преобразования Markdown'ом текстового поля. Откройте файл модели askeet/lib/model/Question.php и создайте:

public function setBody($v){  parent::setBody($v)require_once('markdown.php')// strip all HTML tags  $v = htmlentities($v, ENT_QUOTES, 'UTF-8')$this->setHtmlBody(markdown($v));} 

Применение функции htmlentities() перед установкой HTML-поля защищает "Спроси-ка" от атак cross-site-scripting (XSS) - все тэги <script> экранируются.

Обновление тестовых данных

Мы добавим форматирование Markdown в некоторые вопросы в тестовых данных (в askeet/data/fixtures/test_data.yml), чтобы проверить корректное пробразование:

Question:
q1:
title: What shall I do tonight with my girlfriend?
user_id: fabien
body: |
We shall meet in front of the __Dunkin'Donuts__ before dinner,
and I haven't the slightest idea of what I can do with her.
She's not interested in _programming_, _space opera movies_ nor _insects_.
She's kinda cute, so I __really__ need to find something
that will keep her to my side for another evening.

q2:
title: What can I offer to my step mother?
user_id: anonymous
body: |
My stepmother has everything a stepmother is usually offered
(watch, vacuum cleaner, earrings, [del.icio.us](http://del.icio.us) account).
Her birthday comes next week, I am broke, and I know that
if I don't offer her something *sweet*, my girlfriend
won't look at me in the eyes for another month.

Теперь вы можете заново заполнить базу данных:

$ php batch/load_data.php

Изменение шаблонов

Шаблон showSuccess.php модуля question может быть красиво изменён:

...<div class="question_body">  <?php echo $question->getHtmlBody() ?></div>... 

Фрагмент шаблона списка (_list.php) также отображает тело, но в обрезаном формате:

<div class="question_body">  <?php echo truncate_text(strip_tags($question->getHtmlBody()), 200) ?></div> 

Теперь всё готово для финального теста: отобразить три изменённые страницы и посмотреть отформатированный текст из тестовых данных:

http://askeet/question/list
http://askeet/recent
http://askeet/question/show/stripped_title/what-shall-i-do-tonight-with-my-girlfriend

Отформатированный текст

Тоже самое сделаем для Answer body: создадим дополнительную колонку html_body в моделе, перегрузим метод ->setBody() и использовать для отображения ответов в question/show метод ->getHtmlBody() вместо ->getBody(). Т.к. код абсолютно идентичен вышенаписаному, мы не будет здесь его описывать, но Вы сможете его найти в сегодняшнем SVN коде.

Скрываем id-шники

Другой хороший приём в symfony - это как по возможности избавляться от первичных ключей в параметрах запроса. Наши первичные ключи автоматически увеличиваются и это даёт взломщику некоторую информацию о записях в базе данных. Плюс, отображаемые URI ничего не значат, и это плохо для поисковой оптимизации.

Возьмём, например, страницу пользователя. Сейчас она использует id пользователя как параметр. Но если мы уверены, что nickname уникален, мы можем использовать его в качестве параметра для запроса. Так сделаем же это!

Изменим экшн

Отредактируем экшн user/show:

public function executeShow(){  $this->subscriber = UserPeer::retrieveByNickname($this->getRequestParameter('nickname'));  $this->forward404Unless($this->subscriber)$this->interests = $this->subscriber->getInterestsJoinQuestion();  $this->answers   = $this->subscriber->getAnswersJoinQuestion();  $this->questions = $this->subscriber->getQuestions();} 

Изменим модель

Добавим следущий метод в класс UserPeer в директории askeet/lib/model/.

public static function retrieveByNickname($nickname){  $c = new Criteria();  $c->add(self::NICKNAME, $nickname)return self::doSelectOne($c);} 

Изменим шаблон

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

В шаблонах question/showSuccess.php и question/_list.php замените:

<?php echo link_to($question->getUser(), 'user/show?id='.$question->getUserId()) ?>

на:

<?php echo link_to($question->getUser(), 'user/show?nickname='.$question->getUser()->getNickname()) ?>

Те же изменения сделайте в шаблоне answer/_answer.php.

Добавление правила маршрутизации

Добавим новое правило в настройки маршрутизации для этого экшна так, чтобы шаблон url содержал параметр nickname:

user_profile:
url: /user/:nickname
param: { module: user, action: show }

После symfony clear-cache можете проверить изменения.

Маршрутизация

Многие экшны, написанные ранее, всё ещё используют стандартную маршрутизацию, содержащую имя модуля и имя экшна в адресной строке браузера. Теперь вы знаете как это поправить, так давайте определим URLы для всех экшнов. Отредактируйте askeet/apps/frontend/config/routing.yml:

# question
question:
url: /question/:stripped_title
param: { module: question, action: show }

popular_questions:
url: /index/:page
param: { module: question, action: list, page: 1 }

recent_questions:
url: /recent/:page
param: { module: question, action: recent, page: 1 }

add_question:
url: /add_question
param: { module: question, action: add }

# answer
recent_answers:
url: /recent/answers/:page
param: { module: answer, action: recent, page: 1 }

# user
login:
url: /login
param: { module: user, action: login }

logout:
url: /logout
param: { module: user, action: logout }

user_profile:
url: /user/:nickname
param: { module: user, action: show }

# default rules
homepage:
url: /
param: { module: question, action: list }

default_symfony:
url: /symfony/:action/*
param: { module: default }

default_index:
url: /:module
param: { action: index }

default:
url: /:module/:action/*

Если Вы просматриваете в production environment, настоятельно рекомендуем Вам почистить кэш перед проверкой этих изменений.

Один хороший приём в маршрутизации symfony - это использование имён правил в хелпере link_to() вместо module/action. Это не только быстрее (движок маршрутизатора не должен просматривать всю таблицу маршрутизации, чтобы найти правило), но так же позволяет Вам изменять экшн независимо от имени правила. В главе Links and the Routing System из книги по symfony рассказывается об этом подробнее.

<?php link_to('@user_profile?id='.$user->getId()) ?>// is better than<?php link_to('user/show?id='.$user->getId()) ?>

"Спроси-ка" следует хорошим приёмам symfony, поэтому код, который Вы скачаете в конце сегодняшнего урока, содержит только имена правил в ссылочных хелперах. Замена action/module на @rule во всех шаблонах и своих хелперах не очень прикольное занятие, поэтому последний совет про маршрутизацию: пишите правила маршрутизации как только создаёте экшн и используйте имена правил в ссылочных хелперах с самого начала.

Увидимся завтра

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

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

Все хорошие примеры, рассмотренные здесь (использование внешних библиотек вместо переписывания на symfony, не показывать первичные ключи в приложении, использование имён правил вместо module/action) сделают ваше приложение чистым, безопасным и быстрым.

Но приложение "Спроси-ка" далеко от финиша! Недостающая функциональность - это добавление нового вопроса и ответа. Этим мы и займёмся завтра.

У Вас есть идеи, что можно добавить в 21 день? Отправляйте всё на почту "Спроси-ка". Так держать!

Свой комментарий: