Pull to refresh

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

Reading time 11 min
Views 27K
Original author: Fabien Potencier
Ну что, начнем? Давайте напишем какой-нибудь небольшой, но законченный проект. Выделим на это ровно 1 час. Предлагайте название. Книжный магазин? Если других идей нет, то будем писать блог :)

Предполагается, что на вашей локальной машине установлен и запущен Apache, а также PHP не ниже 5.1.3.

Установка symfony


Чтобы начать побыстрее, используем sandbox-версию Симфонии. Это версия, которая просто устанавливается, и позволяет сразу же приступить к разработке, используя настройки проекта по умолчанию. Есть и другой вариант – установить Симфонию из голого дистрибутива. В этом случае потребуется ряд несложных действий по разворачиванию проекта и некоторой его настройке. В нашем случае пойдем по пути наименьшего сопротивления. Поэтому качаем sf_sandbox_1_2.tgz или sf_sandbox_1_2.zip и распаковываем содержимое в корень папки проекта. Под Линуксом рекомендуется оставить те права на файлы, которые изначально стоят внутри tar-архива (используем ключ –p команды tar). Как обычно, если что-то не получается, читаем readme-файл. В результате получаем примерно следующую структуру папок:

www/
  sf_sandbox/
    apps/
      frontend/
    cache/
    config/
    data/
    doc/
    lib/
    log/
    plugins/
    test/
    web/
      css/
      images/
      js/


Тут видно, что проект (project) под названием sf_sandbox содержит приложение (application) под названием frontend. Пишем в браузере

http://localhost/sf_sandbox/web/index.php/


и убеждаемся, что все работает: должна появиться страница



Также, конечно, можно установить Симфонию в произвольную папку и настроить виртуальные хосты на веб-сервере. Подробнее об этом читаем в главах symfony installation и symfony directory structure полнотекстового справочника по Симфонии.

Создаем модель данных


Блог, товарищи, – это посты (posts), которые можно комментировать. Соответственно, нам нужны таблицы в базе данных, где будут храниться посты и комментарии. Поскольку мы работаем в среде фреймворка, то используем его встроенные средства для создания модели. Делаем файл schema.yml и кладем его в папку sf_sandbox/config/. В этот файл записываем следующее:

propel:
  blog_post:
    id:           ~
    title:        { type: varchar(255), required: true }
    excerpt:      { type: longvarchar }
    body:         { type: longvarchar }
    created_at:   ~
  blog_comment:
    id:           ~
    blog_post_id: ~
    author:       { type: varchar(255) }
    email:        { type: varchar(255) }
    body:         { type: longvarchar }
    created_at:   ~


Это конфигурационный файл, и сделали мы его в соответствии с синтаксисом YAML. Он очень простой, и позволяет делать XML-подобные структуры с помощью отступов. Но он гораздо более читабельный, чем тот же XML. Вот только запомните одну штуку: для отступов надо использовать пробелы, а не табуляцию, иначе парсер файла споткнется. Подробнее о YAML можно почитать в главе configuration chapter.

Вышеприведенная схема (то, что написано в файле schema.yml называется схемой данных) описывает структуру тех двух таблиц, которые нам понадобятся в нашем проекте. Вся фишка в том, что на основе этой схемы автоматически будут созданы классы blog_post и blog_comment. Но об этом чуть позже. Сохраняем файл, и в командной строке пишем следующее:

$ php symfony propel:build-model


Перед тем, как выполнить эту команду, убедитесь, что текущая папка – это корневая папка проекта (именно там лежит файл symfony, который мы будем постоянно запускать с параметрами).

Итак, после выполнения этой команды в папке sf_sandbox/lib/model/ будет создано несколько файлов с классами. Это классы, которые отражают структуру таблиц БД на объекты РНР (такое отражение носит название ORM). «Отражает» в данном случае означает, что читать/добавлять/изменять/удалять записи в таблице можно будет путем вызова некоторых методов соответствующего этой таблице класса. То есть вызываем метод класса – пишем данные в таблицу. Вызываем другой метод – удаляем записи (все, или по определенному условию). И все это без написания SQL-запросов! Просто вызываем методы. Существует несколько реализаций ORM. По умолчанию Симфония работает с Propel. Эти классы являются частью модели нашего приложения (подробнее читаем в главе model).

Файлы – это хорошо, но нам нужны таблицы в БД! Что ж, сконвертируем файлы в таблицы средствами Симфонии. По умолчанию sandbox-версия Симфонии сконфигурирована так, что она работает с SQLite, то есть не требуется инициализации БД. Сейчас вам надо убедиться, что SQLite работает как надо (для этого смотрим php.ini и читаем документацию РНР).

По умолчанию проект sf_sandbox использует БД под названием sandbox.db, расположенную в папке sf_sandbox/data/.
Если хотите использовать MySQL (неужели вы хотите использовать MySQL? :)), то надо просто попросить Симфонию об этом:

$ php symfony configure:database "mysql:dbname=symfony_project;host=localhost" root mypassword


Естественно, надо убедиться, что БД symfony_project существует и доступна пользователю root с паролем mypassword.
Теперь открываем sf_sandbox/config/databases.yml и заменяем 'phptype' на 'mysql', и прописываем там название БД (разберетесь где, я думаю).
Если все-таки решили остановиться на SQLite, то надо будет поменять кое-какие права на *nix-системах:

$ chmod 777 data data/sandbox.db


Теперь запускаем

$ php symfony propel:build-sql


В результате чего будет создан файл lib.model.schema.sql в папке sf_sandbox/data/sql/. В этот файл запишется SQL-запрос, который можно выполнить, чтобы создать нужные нам таблицы. Чтобы выполнить этот запрос, запускаем

$ php symfony propel:insert-sql


Не пугайтесь сильно, если выскочит предупреждение. Так и должно быть. Команда propel:insert-sql удаляет таблицы перед созданием, и естественно должна предупредить вас, что она сейчас их похерит.

Поскольку нам нужна возможность создавать и редактировать посты в нашем блоге, то нам понадобятся несколько форм. Сделаем их из схемы за пару секунд:

$ php symfony propel:build-forms


Эта команда создаст файлы классов в папке sf_sandbox/lib/form/. Эти классы используются для генерации форм добавления и редактирования элементов (постов и комментариев в нашем проекте).

Забудьте все, что я писал до этого. Все делается одной командой: propel:build-all :)

Делаем приложение


Упростим все до предела. Будем считать, что все, что нам надо делать с постами и комментариями, это добавлять (Create), получать (Retrive), обновлять (Update) и удалять (Delete). Для краткости эти четыре операции называют CRUD.

Поскольку мы новички в Симфонии, нам надо по-быстрому получить какой-нибудь результат, чтобы потом колдовать над ним. Самый простой способ добиться результата – это воспользоваться генератором CRUD-форм, который смотрит на схему данных и создает нужные файлы. Запускаем

$ php symfony propel:generate-module --non-verbose-templates --with-show frontend post BlogPost
$ php symfony propel:generate-module --non-verbose-templates frontend comment BlogComment
$ php symfony cache:clear


Когда мы запускаем propel:generate-module, то мы должны использовать ключ --non-verbose-templates. Если хотите узнать, для чего это надо, запускайте команду с ключом help:

$ php symfony help propel:generate-module


Итак, получаем два модуля (post и comment), которые будут управлять объектами, которые реализуются классами BlogPost и BlogComment. Модуль в Симфонии представляется в виде одной или нескольких страниц схожего назначения. Наши новые модули хранятся в папке sf_sandbox/apps/frontend/modules/, и их можно даже посмотреть по адресам:

http://localhost/sf_sandbox/web/frontend_dev.php/post
http://localhost/sf_sandbox/web/frontend_dev.php/comment


Если вы прямо сейчас попробуете добавить пост, то вас ждет засада. Симфония не знает (пока) как отобразить пост (объект!) в списке. Для этого надо показать ей, как это делается. Для этого надо поправить класс BlogPost ( файл lib/model/BlogPost.php): добавить в него метод __toString():

class BlogPost extends BaseBlogPost
{
  public function __toString()
  {
    return $this->getTitle();
  }
}


Этот метод подсказывает РНР (а значит и Симфонии) как нужно преобразовывать объект в строку. Теперь можно выводить посты (объекты!) списком (строками!)
Теперь немного украшательств в виде CSS (файл sf_sandbox/web/css/main.css):

body, td
{
  font-family: Arial, Verdana, sans-serif;
  font-size: 12px;
}

td { margin: 4px; padding: 4px; }


И, вуаля! Можно добавлять посты!



О генераторах можно почитать в главе generators, а о структуре (проект, приложение, модуль) читаем в главе structure.

По ссылкам выше (ссылки на модули) видим, что название исполняемого скрипта, который в Симфонии называется фронт-контроллер (front controller), было изменено с index.php на frontend_dev.php. Эти два скрипта запускают одно и то же приложение (frontend) в разных окружениях. Скриптом frontend_dev.php мы запускаем наше приложение в окружении разработки, в котором можно попробовать поиграть с всякими программистскими штучками типа дебага, логов и т.п. Характерная черта того, что мы работаем в окружении разработки – панелька в правом верхнем углу страницы, а также подсистема конфигурирования на-лету. По этой причине этот скрипт работает чуть медленнее, чем index.php, который является фронт-контроллером рабочего окружения, оптимизированного на скорость. Если вы хотите посмотреть, что получится в рабочем окружении, замените в ссылках выше frontend_dev.php/ на index.php/ и при этом не забудьте почистить кэш (я уже говорил, что при каждом чихе при разработке надо чистить кэш?):

$ php symfony cache:clear


http://localhost/sf_sandbox/web/index.php/


Читаем подробнее в главе environments.

Редактируем главный шаблон


Понятно, что для того, чтобы переключаться между двумя модулями, нам нужна какая-то навигация по сайту. Ее мы будем рисовать в главном шаблоне.
Отредактируем файл главного шаблона sf_sandbox/apps/frontend/templates/layout.php и заменим все, что находится внутри
<body>
на следующее:

<div id="container" style="width:700px;margin:0 auto;border:1px solid grey;padding:10px">
  <div id="navigation" style="display:inline;float:right">
    <ul>
      <li><?php echo link_to('List of posts', 'post/index') ?></li>
      <li><?php echo link_to('List of comments', 'comment/index') ?></li>
    </ul>
  </div>
  <div id="title">
    <h1><?php echo link_to('My first symfony project', '@homepage') ?></h1>
  </div>
 
  <div id="content" style="clear:right">
    <?php echo $sf_data->getRaw('sf_content') ?>
  </div>
</div>


Поскольку у нас всего 1 час на разработку, не будем обращать внимание на внешний вид и будем поступать нехорошо: стили пропишем прямо в HTML.
Что получается?



Раз уж мы тут, давайте заменим заголовок страницы. Редактируем конфигурационный файл Вида (view) приложения (тут Вид – это V из аббревиатуры MVC): (sf_sandbox/apps/frontend/config/view.yml, ищем строку, где прописано свойство title и меняем ее на что-то, что подскажет ваша фантазия. Тут же видим несколько строк, закомментированных символом #. По желанию, их можно раскомментировать.

default:
  http_metas:
    content-type: text/html

  metas:
    title:        The best blog ever
    #description:  symfony project
    #keywords:     symfony, project
    #language:     en
    robots:       index, follow


Главную страницу тоже надо бы заменить на собственную. Для этого будем править модуль default, который хранится в фреймворке, а не в папке вашего приложения. Его можно переписать (override) собственным модулем, который назовем main:

$ php symfony generate:module frontend main


По умолчанию экшен (action) index показывает какие-то приветственные слова. Чтобы убрать эту фигню, откроем sf_sandbox/apps/frontend/modules/main/actions/actions.class.php и очистим все, что содержится в методе executeIndex():

/**
 * Executes index action
 *
 * @param sfRequest $request A request object
 */
public function executeIndex($request)
{
}


Отредактируем файл sf_sandbox/apps/frontend/modules/main/templates/indexSuccess.php для того, чтобы сделать более вменяемое приветствие:

<h1>Welcome to my new blog</h1>
<p>You are the <?php echo rand(1000,5000); ?>th visitor today.</p>


Теперь мы должны сообщить Симфонии, какой экшен надо запустить, когда запрашивается главная страница. Для этого редактируем файл sf_sandbox/apps/frontend/config/routing.yml и меняем правила для homepage следующим образом:

homepage:
  url:   /
  param: { module: main, action: index }


Проверяем чего мы наколдовали:

http://localhost/sf_sandbox/web/frontend_dev.php/




Ну что ж, это уже похоже на самодельный сайт. Можно даже создать тестовый пост и прикрепить к нему тестовый комментарий.
Тема раскрыта здесь: views and templates.

Передаем данные из экшена в шаблон


Ну при определенной сноровке все, что мы сделали до этого момента, занимает весьма мало времени. Сейчас мы сделаем так, чтобы модуль comment зависел от модуля post иначе говоря, надо вывести комментарии под постом.
Для начала нам надо сделать так, чтобы комментарии были доступны для отображения в шаблоне вывода поста. В Симфонии, когда если надо что-то сделать при определенных условиях, это делается посредством экшенов (например, делается вывод комментариев при условии открытия страницы). Открываем файл sf_sandbox/apps/frontend/modules/post/actions/actions.class.php и переписываем метод executeShow():

public function executeShow($request)
{
  $this->blog_post = BlogPostPeer::retrieveByPk($request->getParameter('id'));
  $this->forward404Unless($this->blog_post);
 
  $c = new Criteria();
  $c->add(BlogCommentPeer::BLOG_POST_ID, $request->getParameter('id'));
  $c->addAscendingOrderByColumn(BlogCommentPeer::CREATED_AT);
  $this->comments = BlogCommentPeer::doSelect($c);
}


Объекты Criteria и -Peer являются частью ORM Propel. Эти четыре строки кода и будут реализовывать связь на уровне SQL между комментариями blog_comment и постами blog_post. Осталось подправить файл шаблона поста: sf_sandbox/apps/frontend/modules/post/templates/showSuccess.php, добавим в конец такие строки:

// ...
<?php use_helper('Text', 'Date') ?>
 
<hr />
<?php if ($comments) : ?>
  <p><?php echo count($comments) ?> comments to this post.</p>
  <?php foreach ($comments as $comment): ?>
    <p><em>posted by <?php echo $comment->getAuthor() ?> on <?php echo format_date($comment->getCreatedAt()) ?></em></p>
    <div class="comment" style="margin-bottom:10px;">
      <?php echo simple_format_text($comment->getBody()) ?>
    </div>
  <?php endforeach; ?>
<?php endif; ?>


На этой странице мы использовали замечательную вещь, которая называется хелпер (helpers) – это РНР-код, который вставляет на страницу определенную часто используемую часть готовой HTML-страницы. Создайте комментарий к вашему первому посту и проверьте, что он вывелся под постом:

http://localhost/sf_sandbox/web/frontend_dev.php/post/show?id=1




Так должна выглядеть страница, если все сделано правильно.
Читаем подробнее в главе naming conventions.

Вставляем в БД записи, связанные с другой таблицей


Все это классно, но пока что нельзя написать комментарий, чтобы он автоматически прикрепился к посту, который мы комментируем. Для этого сейчас требуется зайти в на страницу редактирования комментария и выбрать пост, к которому он привязан, используя выпадающий список. Ну как-то так:



Было бы куда лучше, если бы комментарий прикреплялся к посту как-то попроще. Ну так сделаем это! Правим файл шаблона sf_sandbox/apps/frontend/modules/post/templates/showSuccess.php и добавляем в него строки:

<?php echo link_to('Add a comment', 'comment/edit?post_id='.$blog_post->getId()) ?>


Хелпер link_to() создает гиперссылку на экшен edit модуля comment, и теперь комментарии прикрепляются к посту сразу после написания. При этом выбор поста выпдающим списком на странице редактирования комментария никуда не девается, и по-прежнему доступен. Думаю, что неплохо было бы заменить этот селектор hidden-полем, содержащим ID поста.
Формы в Симфонии управляются с помощью классов. Поэтому чтобы поправить форму, надо поправить класс, в данном случае это класс BlogCommentForm, он лежит в папке sf_sandbox/lib/form/:

class BlogCommentForm extends BaseBlogCommentForm
{
  /**
   * Configure method, called when the form is instantiated
   */
  public function configure()
  {
    $this->widgetSchema['blog_post_id'] = new sfWidgetFormInputHidden();
  }
}


Про формы в Симфонии читаем тут Forms Book.

Сейчас мы имеем то, что новый коммент автоматически привязывается к посту, к которому он был написан:



Теперь, когда мы уже научили систему добавлять комменты, хотелось бы, чтобы после добавления комментария пользователя перекидывало на страницу поста, который он комментировал. Чтобы это реализовать, надо поправить метод processForm. Ищем следующий код sf_sandbox/apps/frontend/modules/comment/actions/actions.class.php:

if ($request->isMethod('post'))
{
  $this->form->bind($request->getParameter('blog_comment'));
  if ($this->form->isValid())
  {
    $blog_comment = $this->form->save();
 
    $this->redirect('comment/edit?id='.$blog_comment->getId());
  }
}


И меняем редирект на следующий:

$this->redirect('post/show?id='.$blog_comment->getBlogPostId());


Есть две вещи, на которые стоит обратить внимание: во-первых, чтобы сохранить изменения в объекте, надо просто вызвать метод save() в классе объекта формы (класс формы ассоциирован с моделью данных Propel, и поэтому он знает, как надо преобразовать данные из формы в вид, пригодный для сохранения в БД). Во-вторых, мы делаем редирект сразу после сохранения, поэтому если пользователь после сохранения формы нажмет F5 в браузере, форма не будет отправлена заново, и соответственно, объект не будет повторно сохранен. Это очень полезная практика!
Ну что ж, вступление окончено. Где же блог?? У нас уже есть блог. Только он совсем сырой.

Смотрите в следующих сериях:
  • Валидация форм
  • Изменение вида URL для разных модулей проекта
  • Доводим до ума публичную часть проекта
  • Делаем админку
  • Ограничиваем доступ в админку


Часть вторая

Уважаемые хабравчане, стоит ли переводить вторую часть урока? Или же это мартышкин труд?

Tags:
Hubs:
+38
Comments 74
Comments Comments 74

Articles