Pull to refresh

Первый проект на symfony, часть 2

Reading time 10 min
Views 7.6K
Это вторая часть перевода (первая часть) статьи о том, как сделать простенький проект на Симфонии за 1 час. В ней мы наладим валидацию форм, изменим формат URL, сделаем админку и закроем в нее доступ.

Валидация форм


В прошлой серии мы сделали форму для добавления комментариев (и попутно заметили, что автор оригинальной статьи накосячил в пункте прицепления комментариев к посту). Теперь нам надо следовать фундаментальному принципу безопасности: не доверяй введенным пользователем данным. Поэтому организуем проверку того, что пользователь будет отправлять через форму.

Когда мы просим Симфонию сгенерировать классы, она генерирует не только их, но еще и элементы формы. А еще она добавляет к этим элементам несколько проверок, которые посчитает нужными, опираясь на схему модели данных. Поскольку в схеме написано, что для таблицы blog_post поле title является обязательным (required), у пользователя ничего не выйдет, если он попробует отправить форму, не заполняя это поле. Кроме того, Симфония поймет, что нельзя отправлять в этом поле строку длиннее 255 символов (опять-таки, она черпает эти знания из схемы модели данных).
Давайте заменим (перезапишем, override) некоторые из этих проверок в классе BlogCommentForm. Открываем файл (найдете его самостоятельно?) и добавляем в метод configure() следующие строки:

$this->validatorSchema['email'] = new sfValidatorEmail(
  array('required' => false),
  array('invalid' => 'The email address is not valid'));


Переопределяя метод для поля email, мы переопределяем поведение по умолчанию (default behaviour).
Теперь у нас есть все, для того, чтобы объяснить пользователю, что он неправ, когда тот введет невалидный email-адрес. Форма стала дуракоустойчивее! Тут надо обратить внимание вот на что: во-первых, когда форма содержит какие-то данные, то при отправке формы эти данные сохраняются специальным образом. Поэтому если пользователь отправил форму с частично некорректными данными, ему не придется заполнять форму заново. Во-вторых, ошибки, возникшие при заполнении тех или иных полей, отображаются рядом с этими полями, а не где-нибудь в общей куче ошибок над формой.
Теперь самое время внести ясность в вопрос о том, как обрабатывается форма и сохраняются данные. В этом нелегком деле используются экшены, которые мы правили в прошлой серии. Они содержатся в этом файле:
 /sf_sandbox/apps/frontend/modules/comment/actions/actions.class.php:

Вот что нам надо:

$this->form = new BlogCommentForm(BlogCommentPeer::retrieveByPk($request->getParameter('id')));
 
if ($request->isMethod('post'))
{
  $this->form->bind($request->getParameter('blog_comment'));
  if ($this->form->isValid())
  {
    $blog_comment = $this->form->save();
 
    $this->redirect('post/show?id='.$blog_comment->getBlogPostId());
  }
}


После создания экземпляра класса формы происходит следующее:
  • Происходит проверка, что форма была передана методом POST
  • Действительно был получен параметр-массив blog_comment. Метод getParameter() сумеет определить, что этот параметр является массив значений, а не одиночным значением, и возвращает ассоциативный массив (например, значение поля blog_comment[author] будет записано в этом массиве под ключом author)
  • Полученный ассоциативный массив будет подцеплен к форме, и этот процесс называется биндингом (binding), в результате которого значения из этого массива заполнят соответствующие поля в экземпляре класса формы. После этого запускается проверка формы, и поле за полем происходит проверка, корректные ли данные в форме, или нет.
  • Только в том случае, если все проверки прошли, происходит сохранение данных, и страница перенаправляется на экшен show.




Внимание! В Симфонии 1.2 кое-что изменилось (оригинальная статья написана для Симфонии 1.1). Во-первых, создание формы и проверка на метод POST вынесены в экшен executeCreate:

  public function executeCreate(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post'));

    $this->form = new BlogCommentForm();

    $this->processForm($request, $this->form);

    $this->setTemplate('new');
  }


Возможно, тут возникнет некоторая путаница относительно того, какой экшен зачем нужен. Например, executeNew — это экшен вывода формы при добавлении нового комментария. А executeCreate — это экшен обработки данных при отправке формы добавления нового комментария.

Чтобы сделать какое-то поле обязательным, достаточно в файле схемы данных (schema.yml) прописать required:true для данного поля. Например,

    author:       { type: varchar(255),required:true }

О валидации данных читаем подробнее в главе form validation.

Меняем формат URL


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

Проблема в том, что название может содержать специальные символы, типа пробелов. Если вы просто преобразуете их в escape-последовательность, то URL будет содержать страшненькие %20, поэтому нам надо расширить модель новым методом класса BlogPost, чтобы он делал из заголовка поста что-нибудь URL-красивенькое. Чтобы сделать это, открываем файл BlogPost.php из папки sf_sandbox/lib/model/ и добавляем туда следующее:

public function getStrippedTitle()
{
  $result = strtolower($this->getTitle());
 
  // strip all non word chars
  $result = preg_replace('/\W/', ' ', $result);
 
  // replace all white space sections with a dash
  $result = preg_replace('/\ +/', '-', $result);
 
  // trim dashes
  $result = preg_replace('/\-$/', '', $result);
  $result = preg_replace('/^\-/', '', $result);
 
  return $result;
}


Думаю, вам несложно будет дописать пару замен, чтобы сделать из русского заголовка поста хорошую транслитерацию.
А теперь надо сделать экшен permalink для модуля post. Добавляем новый метод в sf_sandbox/apps/frontend/modules/post/actions/actions.class.php:

public function executePermalink($request)
{
  $posts = BlogPostPeer::doSelect(new Criteria());
  $title = $request->getParameter('title');
  foreach ($posts as $post)
  {
    if ($post->getStrippedTitle() == $title)
    {
      $request->setParameter('id', $post->getId());
 
      return $this->forward('post', 'show');
    }
  }
 
  $this->forward404();
}


Вопрос! ребята, я понимаю, что это просто жесть так программировать! Выбрать для каждой записи остальные стопицот записей и искать: а нет ли среди них того, чего нам надо? Нет ли способа поизящнее подобрать ID по заданному title? Или я зря парюсь, и подсистема кэширование Симфонии все сделает как надо?


Список постов теперь может вызывать экшен permalink вместо show для каждого поста. В файле sf_sandbox/apps/frontend/modules/post/templates/indexSuccess.php удалим колонку с ID поста, и заменим содержимое ячейки, где выводится заголовок Title с этого:

<td><?php echo $blog_post->getTitle() ?></td>


на это:

<td><?php echo link_to($blog_post->getTitle(), '@post?title='.$blog_post->getStrippedTitle()) ?></td>


Остался всего один шаг: подредактировать routing.yml, расположенный в папке sf_sandbox/apps/frontend/config/ и в самом начале добавить правила:

list_of_posts:
  url:   /latest_posts
  param: { module: post, action: index }

post:
  url:   /blog/:title
  param: { module: post, action: permalink }


Теперь смотрим в браузер и видим новые URL в действии. Если вдруг выскочила ошибка, значит надо почистить кэш – это правила роутинга еще не начали работать. Как это делается, мы уже говорили:

$ php symfony cc




Об адресах читаем в главе smart URLs.

Чистим фронтенд (публичную часть)


Окей, наш блог становится все краше. Только вот незадача: каждый умник сейчас может запросто зайти и поправить любой пост. Не гуд это, как вы догадываетесь. Вобщем, сейчас нам надо убрать всю функциональность по редактированию постов и комментариев из публичной части (а позже – добавить ее в админку).
В шаблоне sf_sandbox/apps/frontend/modules/post/templates/showSuccess.php удалим ссылку на редактирование поста. Найдите сами чего там надо удалить (кто совсем в танке, эти строки есть в оригинальной статье).
То же самое делаем для шаблона sf_sandbox/apps/frontend/modules/post/templates/indexSuccess.php, там мы удаляем ссылку на создание поста (танкисты ищут это место в оригинальной статье).

Естественно, удалением ссылок мы не отделаемся. Функционал тоже надо удалить. Открываем sf_sandbox/apps/frontend/modules/post/actions/actions.class.php и удаляем экшены executeEdit и executeDelete.

Примечание! В Симфонии 1.2 надо еще удалить экшен executeUpdate. Кто читал все внимательно, тот уже сам догадался об этом.

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

Делаем админку


А если кто-то начнет в нашем блоге писать про расовую неравноправность? Или оставлять комментарии в духе «+1»? Нам нужен инструмент, который позволил бы чистить блог от всякого мусора. Ну и по настроению, исправлять опечатки. Нам нужна админка.

Админка – это отдельное приложение (application), и его надо создать. Запускаем в командной строке:

$ php symfony generate:app backend
$ php symfony propel:init-admin backend post BlogPost
$ php symfony propel:init-admin backend comment BlogComment


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

Точно так же, как мы делали для приложения frontend, отредактируем главный шаблон (layout) (apps/backend/templates/layout.php) и добавим глобальную навигацию:

<div id="navigation">
  <ul style="list-style:none;">
    <li><?php echo link_to('Посты', 'post/index') ?></li>
    <li><?php echo link_to('Комментарии', 'comment/index') ?></li>
  </ul>
</div>
<div id="content">
  <?php echo $sf_data->getRaw('sf_content') ?>
</div>


Теперь мы можем получить доступ к админке в окружении разработки, набрав следующее:

http://localhost/sf_sandbox/web/backend_dev.php/post




Тут проявляется великая мощь генератора админки, пользоваться которой можно ловко и умело, редактируя конфиг. Заменим содержимое файла sf_sandbox/apps/backend/modules/post/config/generator.yml на следующее:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      BlogPost
    theme:            default
    fields:
      title:          { name: Title }
      excerpt:        { name: Excerpt }
      body:           { name: Body }
      nb_comments:    { name: Comments }
      created_at:     { name: Creation date }
    list:
      title:          Post list
      layout:         tabular
      display:        [=title, excerpt, nb_comments, created_at]
      object_actions:
        _edit:        ~
        _delete:      ~
      max_per_page:   5
      filters:        [title, created_at]
    edit:
      title:          Post detail
      fields:
        title:        { type: input_tag, params: size=53 }
        excerpt:      { type: textarea_tag, params: size=50x2 }
        body:         { type: textarea_tag, params: size=50x10 }
        created_at:   { type: input_date_tag, params: rich=on }


Посмотрите: среди полей таблицы blog_post админка будет искать поле (или getter-метод) nb_comments. Поскольку это не хранимое, а генерируемое значение, нам надо добавить getter-метод в нашу модель (sf_sandbox/lib/model/BlogPost.php):

public function getNbComments()
{
  return count($this->getBlogComments());
}

Теперь обновим админку и любуемся изменениями:



Ограничиваем доступ в админку


По-прежнему каждый умник может залезть в админку и стереть все посты про Ксению Собчак. Не дадим ему это сделать: запаролим админку. В папке apps/backend/config/, правим файл security.yml, пишем в него следующее:

all:
  is_secure: on


Теперь нельзя войти в модули админки, не авторизуясь. Постойте, откуда взять логин с паролем и вообще всякие формы авторизации-регистрации? Есть гениальное решение — плагины! Воспользуемся плагином, который как раз занимается этими вопросами — sfGuardPlugin. Пишем следующее в командной строке:

$ php symfony plugin:install sfGuardPlugin


Тут у вас может возникнуть проблема, если не установлен PEAR. Самое время его установить.
Эта команда скачает плагин из хранилища плагинов Симфонии. В конце должно быть выдано сообщение, что все прошло успешно:

$ php symfony plugin:install sfGuardPlugin
>> plugin    installing plugin "sfGuardPlugin"
>> sfPearFrontendPlugin Attempting to discover channel "pear.symfony-project.com"...
>> sfPearFrontendPlugin downloading channel.xml ...
>> sfPearFrontendPlugin Starting to download channel.xml (663 bytes)
>> sfPearFrontendPlugin .
>> sfPearFrontendPlugin ...done: 663 bytes
>> sfPearFrontendPlugin Auto-discovered channel "pear.symfony-project.com", alias
>> sfPearFrontendPlugin "symfony", adding to registry
>> sfPearFrontendPlugin Attempting to discover channel
>> sfPearFrontendPlugin "plugins.symfony-project.org"...
>> sfPearFrontendPlugin downloading channel.xml ...
>> sfPearFrontendPlugin Starting to download channel.xml (639 bytes)
>> sfPearFrontendPlugin ...done: 639 bytes
>> sfPearFrontendPlugin Auto-discovered channel "plugins.symfony-project.org", alias
>> sfPearFrontendPlugin "symfony-plugins", adding to registry
>> sfPearFrontendPlugin downloading sfGuardPlugin-2.2.0.tgz ...
>> sfPearFrontendPlugin Starting to download sfGuardPlugin-2.2.0.tgz (18,589 bytes)
>> sfPearFrontendPlugin ...done: 18,589 bytes
>> sfSymfonyPluginManager Installation successful for plugin "sfGuardPlugin"


Теперь плагин надо включить. Правим файл sf_sandbox/apps/backend/config/settings.yml, включаем системный логин следующим образом. Раскомментируем все, что содержится в ключе all: и добавим следующее:
# (Some stuff here)
all:
  .actions:
    login_module:    sfGuardAuth   # To be called when a non-authenticated user
    login_action:    signin     # Tries to access a secure page

    secure_module:   sfGuardAuth   # To be called when a user doesn't have
    secure_action:   secure    # The credentials required for an action

  .settings:
    enabled_modules: [default, sfGuardAuth, sfGuardGroup, sfGuardPermission, sfGuardUser]


Теперь добавим нового системного пользователя, пишем в файл sf_sandbox/apps/backend/lib/myUser.class.php вместо всего что там есть следующее:

class myUser extends sfGuardSecurityUser
{
}


Теперь мы должны пересоздать модель, формы и фильтры, и обновить БД:

$ php symfony propel:build-model
$ php symfony propel:build-forms
$ php symfony propel:build-filters
$ php symfony propel:build-sql
$ php symfony propel:insert-sql


Как и раньше, при запуске задания propel:insert-sql, Стифония удалит все таблицы и создаст их заново. Поскольку во время разработки это будет встречаться довольно часто, имеет смысл записать начальные и тестовые данные в fixtures (см. подробнее в главе populating a database).
Теперь снова надо почистить кэш. После этого, наконец, создаем нового пользователя:

$ symfony guard:create-user habr oh_no_123456


Примечание: почему-то в оригинальном тексте в параметрах к этой команде указывается еще и название приложения. Возможно, это было нужно ранее, но в 1.2 точно не нужно.

Теперь сделаем модуль управления постами post модулем по умолчанию при входе в админку. Для этого открываем файл apps/backend/config/routing.yml и ищем там ключ homepage. Меняем default на post.
Итак, если мы теперь попытаемся залезть в админку, мы получим следующую картинку:



Читаем больше в главе security.

Заключение


Час прошел. Я думаю, что у вас, как и у меня, в первый раз все заняло куда больше времени. Поверьте, при совсем небольшой сноровке все будет получаться очень легко и непринужденно – Симфония располагает к этому.
Теперь можно использовать оба приложения в рабочем окружении:
frontend:   http://localhost/sf_sandbox/web/index.php/
backend:    http://localhost/sf_sandbox/web/backend.php/

Тут у вас, возможно, возникнут сообщения об ошибках. Это все из-за того, что мы поменяли модель, и не обновили кэш. Чистим кэш быстренько:

$ php symfony cc


А теперь смотрим и любуемся скоростью работы приложений! Без комментариев.

К сожалению, ошибся с выбором типа топика. Это перевод статьи My first symfony project, автор Fabien Potencier. Не знаю как поправить.

Tags:
Hubs:
+14
Comments 17
Comments Comments 17

Articles