Askeet. День второй. Создание модели

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

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

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

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

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

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

ПереводыAskeet. День второй. Создание модели

В предыдущих сериях...

Во время первого дня этого длинного но невероятно интересного учебника мы научились устанавливать фреймворк symfony, создавать новые приложения и среду разработки, а так же добавили безопасности коду, введя контроль версий. Кстати, код первого дня доступен в репозитории svn по адресу:

http://svn.askeet.com/

Целями второго дня являются: определение финального функционала результирующего приложения, наброски модели данных и начало программирования. Это вберёт в себя генерацию объекто-ориентированной карты и использование её интерактивности для создания, получение и обновления записей в базе данных в приложении.

Весьма объёмно. Поехали!

Совершенно секретно

Итак, что мы хотим знать? Интересный вопрос. Вообще интерсных вопросов много:

  • Чем заняться вечером с подружкой?
  • Как сгенить трафик в блоге
  • Какой веб-фреймворк лучший?
  • Какой самый известный ресторан в Париже?
  • В чём смысл жизни? Откуда взялась вселенная и всё остальное?

На все эти вопросы нужен не просто ответ, а лучший ответ. Невероятно, но факт - вопросы, имеющий один ответ являются самыми неинтересными (вроде "сколько будет 1+1") и лишь немногие удостоены награды быть решёнными в вебе. Так нечестно. 

Познакомтесь - "Спросика". Сайт, направленный для получения ответов на вопросы. Кто будет отвечать на эти дурацкие вопросы? Да все! И все смогут ставить оценки за ответы, а самый лучший ответ будет лучше виден. Так как количество вопросов растёт, то нереально будет всё хранить в обычных каталогах и подкаталогах, поэтому задающий вопрос сможет ставить метки из любых слов, "аля" http://del.icio.us/. Конечно же популярность тегов будет видна через облако тегов. Если кто-то захочет следить за ответами к определённому вопросу - он сможет подписаться на RSS ленту вопроса. Весь этот функционал должен быть элегантным и лёгким, чтобы все действия, не требующие новых страниц, были доступны через AJAX. Конечно же нужна админка, чтобы отсеивать спам или уничтожать вопросы, которые администратор посчитает вызывающими.

Но вы ведь спросите - а разве я это уже не видел где-то? Ну если вы на самом деле видели - мы пролетели, но если вы ссылаетесь на всякие faqts, eHow, Ask Jeeves или нечто подобное без нормальных ответов, Аякса, тэгов и RSS, то до нашего сайта им далеко. Мы говорим о Web2.0 приложении.

Главная суть "Спросика", в том что это не просто сайт. Это приложение, которое каждый может загрузить, установить дома или в интранете компании, покромсать и добавить пару новых фич. Исходный код будет открытым. Ваш шеф ищет управление базой знаний? Хотите сохранять информацию о трюках при ремонте машины? Не хотите создавать FAQ на своём сайте? Хватит искать! Ведь есть "Спросика"! Ну точнее будет. Подарок на рождество ;)

С чего начать?

С чего начать приложение на symfony? Всё зависит от вас. Вы можете писать рассказы, играть в планирование, найти партнёра для парного программирования, если вы приверженец экстремального программирования, или же написать детальную спецификацию сайта вместе с набросками всех объектов, состояний, взаимодействий и т.д., если вы фанат UML

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

На данном проекте мы воспользуемся базами данных MySQL с установленным типом данных таблиц InnoDB, которые поддерживают связывание и транзакции. Мы бы могли использовать базы данных SQLite на первых шагах и не создавать реальную базу. Для этого потребуется лишь несколько изменения в файле databases.yml, так что оставим это для вашего личного упражнения.

Модель данных

Реляционная модель

Вообще у нас будут таблицы "вопрос"(question) и ответ(answer). Так же нам понадобится таблица пользователей (user), а ещё мы будем сохранять интерес пользователей к вопросам через таблицу interest и релевантность через таблицу relevancy.

Пользователи должны будут авторизоваться, чтобы добавлять вопросы, устанавливать релевантность ответов и определять интерес к вопросам. Пользователи не должны будут авторизоваться для добавления ответов, но ответ всегда будет привязан к пользователю, таким образом пользователя с популярными ответами будут вознаграждены. Ответы, введённый без авторизации будут показаны как ответы основного пользователя, известного как 'Трус-аноним'. Всё проще понимаемо при помощи реляционной диаграммы:

Реляционная диаграмма

Заметьте - мы определили для каждой таблицы поле created_at. Symfony видит такие поля и устанавливаем им текущие дату и время системы при сохранении записи. Тоже самое и для полей updated_at - их значения устанавливаются в соответствии с текущим временем при обновлении записи.

schema.xml

Реляционная модель должна быть переведена в конфигурацию, понятную для symfony. Для этой цели существует файл schema.yml, расположенный в каталоге /config. Symfony поддерживает схемы как в xml так и yml форматах.

Есть 2 пути создания данного файла - написать от руки (этот путь нам нравится больше) или из существующей базы. Воспользуемся первым способом.

Для начала удалим пример YAML файла:

$ svn delete config/schema.yml

Синтаксис файла schema.xml. объясняемый детально на сайте Propel, весьма прост - это XML файл, в котором тэги

содержат тэги , и . Уже после первого применения вы сможете спокойно писать их дальше. Вот пример файда schema.xml отвечающий реляционной модели, описаной ранее:
<?xml version="1.0" encoding="UTF-8"?> <database name="propel" defaultIdMethod="native" noxsd="true">   <table name="ask_question" phpName="Question">     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"   />     <column name="user_id" type="integer"   />     <foreign-key foreignTable="ask_user">       <reference local="user_id" foreign="id"  />     </foreign-key>     <column name="title" type="longvarchar"   />     <column name="body" type="longvarchar"   />     <column name="created_at" type="timestamp"   />     <column name="updated_at" type="timestamp"   />   </table>    <table name="ask_answer" phpName="Answer">     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"   />     <column name="question_id" type="integer"   />     <foreign-key foreignTable="ask_question">       <reference local="question_id" foreign="id"  />     </foreign-key>     <column name="user_id" type="integer"   />     <foreign-key foreignTable="ask_user">       <reference local="user_id" foreign="id"  />     </foreign-key>     <column name="body" type="longvarchar"   />     <column name="created_at" type="timestamp"   />   </table>    <table name="ask_user" phpName="User">     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"   />     <column name="nickname" type="varchar" size="50"   />     <column name="first_name" type="varchar" size="100"   />     <column name="last_name" type="varchar" size="100"   />     <column name="created_at" type="timestamp"   />   </table>    <table name="ask_interest" phpName="Interest">     <column name="question_id" type="integer" primaryKey="true"   />     <foreign-key foreignTable="ask_question">       <reference local="question_id" foreign="id"  />     </foreign-key>     <column name="user_id" type="integer" primaryKey="true"   />     <foreign-key foreignTable="ask_user">       <reference local="user_id" foreign="id"  />     </foreign-key>     <column name="created_at" type="timestamp"   />   </table>    <table name="ask_relevancy" phpName="Relevancy">     <column name="answer_id" type="integer" primaryKey="true"   />     <foreign-key foreignTable="ask_answer">       <reference local="answer_id" foreign="id"  />     </foreign-key>     <column name="user_id" type="integer" primaryKey="true"   />     <foreign-key foreignTable="ask_user">       <reference local="user_id" foreign="id"  />     </foreign-key>     <column name="score" type="integer"   />     <column name="created_at" type="timestamp"   />   </table>  </database>

Думаю вы заметили что имя базы данных установлено как "propel", независимо от типа баз данных. Этот параметр используется для подключения Propel к symfony. Реальная база данных будет определена в файле databases.yml

Есть и другой путь создания schema.xml, если у вас уже есть существующая база данных. Если вы привыкли работать с графическими редакторами баз данных, то конечно вы предпочтёте создать схему из уже созданной структуры данных. Прежде чем сделать это, вам нужно лишь поправить propel.ini, расположенный в каталоге /config и ввести опции подключения к базе данных:

propel.database.url = mysql://username:password@localhost/databasename

Где username, password, localhost и databasename это имя пользователя, пароль, хост и имя базы данных. Теперь вы можете вызвать команду propel-build-schema для создания файла schema.xml:

$ symfony propel-build-schema

hint от переводчика.

Задать конфигурацию подключения можно и через консоль при помощи команды:

$ symfony configure:database mysql://username:password@localhost/databasename

Параметр, передаваемый этой команде, есть ни что иное как DSN адрес. Подробнее можно найти на просторах интернета ;)
Так же в symfony 1.1 для создания схемы можно вызвать команду

$ symfony propel:build-schema

Данный синтаксис предпочтительнее, по моему мнению.
Некоторые инструменты позволяют вам строить базу данных графически (например Fabforce's Dbdesigner) и генерировать прямо в файл schema.xml (например при помощи DB Designer 4 TO Propel Schema Converter).

schema.xml мы можем легко заменить на schema.yml, используя форматирование YAML:

propel:  _attributes:   { noXsd: false, defaultIdMethod: none, package: lib.model }   ask_question:    _attributes: { phpName: Question, idMethod: native }    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id }    title:       { type: longvarchar }    body:        { type: longvarchar }    created_at:  ~    updated_at:  ~   ask_answer:    _attributes: { phpName: Answer, idMethod: native }    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }    question_id: { type: integer, foreignTable: ask_question, foreignReference: id }    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id }    body:        { type: longvarchar }    created_at:  ~   ask_user:    _attributes: { phpName: User, idMethod: native }    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }    nickname:    { type: varchar(50), required: true, index: true }    first_name:  varchar(100)    last_name:   varchar(100)    created_at:  ~   ask_interest:    _attributes: { phpName: Interest, idMethod: native }    question_id: { type: integer, foreignTable: ask_question, foreignReference: id, primaryKey: true }    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true }    created_at:  ~   ask_relevancy:    _attributes: { phpName: Relevancy, idMethod: native }    answer_id:   { type: integer, foreignTable: ask_answer, foreignReference: id, primaryKey: true }    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true }    score:       { type: integer }    created_at:  ~

Создание объектной модели

Чтобы использовать движок InnoDB придётся изменить одну строчку кода в propel.ini в каталоге /config (если мне не изменяет память, то в symfony 1.1 так уже и есть, но информация лишней не будет):

propel.mysql.tableType = InnoDB

Раз у нас уже создан schema.xml, создадим объектную модель, основанную на реляционной. В symfony объекто-реляционное связывание управляется через Propel, но встроено в комманду symfony:

$ symfony propel-build-model

или, используя новый синтаксис:

$ symfony propel:build-model

Эта команда (а вызывать её нужно из корневой папки проекта "Спросика") автоматически создаст классы, отвечающие таблицам, определённым в схеме, наравне вместе с функциями доступа (->get() и ->set()). Вы можете взглянуть на свежевыжатый код в каталоге /lib/model/om. Если вам интересно почему же у нас 2 класса на 2 таблицу, то пора бы сбегать к главе про модели книги по symfony. Данные классы будут перезаписываться каждый раз при вызове build-model, а это в нашем проекте будет случаться нередко. Так что, если хотите добавить новые методы в объекты моделей, то лучше править те файлы, которые расположены в каталоге /lib/model. Эти классы расширяют классы, лежащие в /om

The базаданных

Подключение

Теперь у symfony есть объектная модель данных, самое время подключить ваш проект к MySQL. Для начала создадим базу данных в MySQL

$ mysqladmin -u youruser -p create askeet

Теперь откройте конфигурационный файл /config/databases.yml. Это ваша первая встреча с symfony. Как видите файлы конфигурации symfony написаны в формате YAML. Синтаксис очень прост, но есть один ньюанс в YAML файлах - никогда не используйте табуляцию, только пробелы (hint от переводчика: вообще лучше всегда использовать в редакторах именно пробелы, т.о. код для всех будет одинаков по внешнему виду. Zend и Eclipse это позволяют точно). Теперь, раз вы уже знаете этот аспект, самое время поправить файл и ввести актуальные данные для подключения к вашей базе данных:

 

all:  propel:    class:      sfPropelDatabase    param:      phptype:  mysql      host:     localhost      database: askeet      username: youruser      password: yourpasswd

Если хотите узнать больше про конфигурацию symfony и YAML файл, прочтите главу о конфигурации в книге по symfony

hint от переводчика

ранее я уже сказал про команду configure:database, она затрагивает и этот файл. Но есть одно но. Скачав symfony sandbox мы сразу получаем уже готовый проект, frontend приложения и ... databases.yml, настроенный на волну SQLite. Данная команда не убила директивы SQLite и поэтому обращения к MySQL не было. Так что будьте бдительны.

Строители

Если вы не писали файл schema.xml от руки, то вы видимо уже имеете необходимые таблицы в своей базе данных. Можете пропускать эту часть.

Для фанатов клавиатур (попросту "трактористов") у нас есть сюрприз - создавать таблицы и колонки вручную не придётся. Вы уже это сделали в schema.xml, так что symfony сам построит sql-запросы для создания таблиц:

$ symfony propel-build-sql

или

$ symfony propel:build-sql

Эта команда создаст файл lib.model.schema.sql в папке /data/sql. Используйте его для команды в MySQL:

$ mysql -u youruser -p askeet < data/sql/lib.model.schema.sql

Есть и альтернативный способ:

$ symfony propel-insert-sql

или для версии 1.1

$ symfony propel:insert-sql

Тестируем данные через СПОУ(CRUD)

Всегда приятно видеть, что работа выполнена не зря. До сих пор вы даже не использовали ваш браузер, а мы тут собрались делать веб-приложение... Так что давайте уже создадим базовый набор шаблонов и действий для управления данными таблицы question. Это позволит вам создать несколько вопросов и даже отобразить их.

В консоли введите:

$ symfony propel-generate-crud frontend question Question

или для 1.1

$ symfony propel:generate-crud frontend question Question

Команда создаст шаблоны для модуля вопросов внутри frontend приложения, основанных на объектной модели Question, с базовыми действиями Создания Получения Обновления Удаления (Create Retrieve Update Delete). Не пугайтесь - шаблон это ещё не готовое приложение, но базовая структура, на основании которой вы уже можете разрабатывать и добавлять бизнесс-правила, а так же улучшать внешний вид, у вас уже есть.

Список всех действий, созданных генератором СПОУ:

 

Action name Description
list показывает все записи таблицы
index отправляет на list
show Показывает поля определённой записи
edit отображает форму для создания или редактированяи записи
update меняет запись в соответствии с параметрами и отправляет на show
delete deletes a given record from the table

Про генерацию можно узнать в главе про генераторацию кода

В каталоге apps/frontend/modules вы можете найти модуль для вопросов и посмотреть его внутренности.

Если вы когда либо добавили новый класс, который должен быть в автозагрузке, не забудьте почистить кэш:

$ symfony cc frontend config

Самое время протестировать по адресу http://askeet/question

Создание новой записи

Вывод всех записей

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

Ещё увидимся!

Вы не написали ещё ни единой строчки PHP-кода, а ведь у вас уже есть базовое приложение. Не так уж плохо для второго дня. В следующий раз мы начнём уже писать потихоньку код и создадим страницу приветствия, на которой отобразим список вопросов. Так же мы введём тестовые данных в нашу базу используя скрипты и научимся расширять модели.

Коментарии:

MihaKot 2008-12-05 01:27:15

Пытался сделать как написано.
в итоге получил
Fatal error: Class 'QuestionForm' not found in /usr/lib/php/symfony/generator/sfModelGenerator.class.php on line 321

ответить
Alex 2009-11-15 14:15:33

Используете позднюю версию Symfony. Вы хоть бы написали, в каком месте именно эта ошибка появилась. Вероятно вам нужно использовать propel:generate-module вместо propel:generate-crud.

ответить
Shin 2008-12-25 07:40:22

У меня получилось то же самое. Та же ошибка.

ответить

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