Symfony 2 Joboard : Модель данных

Первым делом мы определим модель данных для Joboard, а для взаимодействия с базой данных будем использовать ORM и в конце этой статьи у вас будет создан первый модуль приложения. Но так как Symfony делает много работы за нас, то этот полностью функциональный веб-модуль мы создадим без написания большого количество кода на PHP.

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

Пользовательские истории в предыдущей части, описывают основные объекты нашего проекта: вакансии, партнёров и категории. Вот диаграмма отношений соответствующих сущностей:

Схема базы данных

В дополнение к полям описанных в пользовательских историях, мы также добавим столбцы created_at и updated_at. Symfony будет автоматически устанавливать их значение при сохранении или обновлении объекта.

База данных

Для сохранения вакансий, партнёров и категорий в базе данных, Symfony 2 по умолчанию использует библиотеку Doctrine ORM. Чтобы определить параметры подключения к базе данных, вы должны отредактировать файл app/config/parameters.yml (для этого руководства мы будем использовать MySQL):

parameters:
    database_driver: pdo_mysql
    database_host: localhost
    database_port: null
    database_name: joboard
    database_user: root
    database_password: password
# ...

Теперь, когда Doctrine знает о вашей базе данных, вы можете создать её с помощью консольной команды, откройте терминал перейдите в директорию вашего проекта и выполните:

php app/console doctrine:database:create

Схема

Чтобы рассказать Doctrine о наших объектах, мы создадим файлы «метаданных», в которых описывается то, как объекты будут храниться в базе данных. Теперь перейдите в редактор кода и внутри каталога src/App/JoboardBundle/Resources/config создайте каталог doctrine (должно получиться - src/App/JoboardBundle/Resources/config/doctrine).

Директория doctrine будет содержать три файла: Category.orm.yml, Job.orm.yml и Affiliate.orm.yml.

Category.orm.yml

App\JoboardBundle\Entity\Category:
    type: entity
    repositoryClass: App\JoboardBundle\Repository\CategoryRepository
    table: category
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        name:
            type: string
            length: 255
            unique: true
    oneToMany:
        jobs:
            targetEntity: Job
            mappedBy: category
    manyToMany:
        affiliates:
            targetEntity: Affiliate
            mappedBy: categories

Job.orm.yml

App\JoboardBundle\Entity\Job:
    type: entity
    repositoryClass: App\JoboardBundle\Repository\JobRepository
    table: job
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        type:
            type: string
            length: 255
            nullable: true
        company:
            type: string
            length: 255
        logo:
            type: string
            length: 255
            nullable: true
        url:
            type: string
            length: 255
            nullable: true
        position:
            type: string
            length: 255
        location:
            type: string
            length: 255
        description:
            type: text
        how_to_apply:
            type: text
        token:
            type: string
            length: 255
            unique: true
        is_public:
            type: boolean
            nullable: true
        is_activated:
            type: boolean
            nullable: true
        email:
            type: string
            length: 255
        expires_at:
            type: datetime
        created_at:
            type: datetime
        updated_at:
            type: datetime
            nullable: true
    manyToOne:
        category:
            targetEntity: Category
            inversedBy: jobs
            joinColumn:
                name: category_id
                referencedColumnName: id
    lifecycleCallbacks:
        prePersist: [setCreatedAtValue]
        preUpdate: [setUpdatedAtValue]

Affiliate.orm.yml

App\JoboardBundle\Entity\Affiliate:
    type: entity
    repositoryClass: App\JoboardBundle\Repository\AffiliateRepository
    table: affiliate
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        url:
            type: string
            length: 255
        email:
            type: string
            length: 255
            unique: true
        token:
            type: string
            length: 255
        is_active:
            type: boolean
            nullable: true
        created_at:
            type: datetime
    manyToMany:
        categories:
            targetEntity: Category
            inversedBy: affiliates
            joinTable:
                name: category_affiliate
                joinColumns:
                    affiliate_id:
                        referencedColumnName: id
                inverseJoinColumns:
                    category_id:
                        referencedColumnName: id
    lifecycleCallbacks:
        prePersist: [setCreatedAtValue]

ORM

Теперь Doctrine может создавать классы моделей (entities). Выполните в терминале команду:

php app/console doctrine:generate:entities AppJoboardBundle

Если вы посмотрите в каталог Entity в JoboardBundle, то найдёте там, только что созданные классы: Category.php, Job.php и Affiliate.php. Откройте Job.php и задайте значения created_at и updated_at, как показано ниже:

<?php

// ...

/**
* @ORM\PrePersist
*/
public function setCreatedAtValue()
{
    if(!$this->getCreatedAt())
    {
        $this->created_at = new \DateTime();
    }
}

/**
* @ORM\PreUpdate
*/
public function setUpdatedAtValue()
{
    $this->updated_at = new \DateTime();
}

Сделайте то же самое для значения created_at класса партнёра (Affiliate.php):

<?php

// ...

/**
* @ORM\PrePersist
*/
public function setCreatedAtValue()
{
    $this->created_at = new \DateTime();
}

// ...

Docrine будет автоматически обновлять эти поля при сохранении или обновлении записи. Это поведение было определено в файлах Affiliate.orm.yml и Job.orm.yml, с помощью раздела lifecycleCallbacks и значения prePersist: [setCreatedAtValue].

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

php app/console doctrine:schema:update --force

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

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

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

Добавьте следующий текст в файл composer.json, в разделе require:

// ...
"require": {
    // ...
    "doctrine/doctrine-fixtures-bundle": "dev-master",
    "doctrine/data-fixtures": "dev-master"
},
// ...

Обновите библиотеки:

php composer.phar update

Зарегистрируйте пакет DoctrineFixturesBundle в app/AppKernel.php:

<?php

// ...
public function registerBundles()
{
    $bundles = array(
        // ...
        new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle()
    );
// ...
}

Теперь, когда все установлено, в JoboardBundle нужно создать новую директорию с именем DataFixtures/ORM, там мы создадим несколько новых классов для загрузки начальных данных LoadCategoryData.php и LoadJobData.php (файлы должны называться также как и классы):

LoadCategoryData.php

<?php

# src/App/JoboardBundle/DataFixtures/ORM/LoadCategoryData.php

namespace App\JoboardBundle\DataFixtures\ORM;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use App\JoboardBundle\Entity\Category;

class LoadCategoryData extends AbstractFixture implements OrderedFixtureInterface
{
    public function load(ObjectManager $em)
    {
        $design = new Category();
        $design->setName('Дизайн');
        $programming = new Category();
        $programming->setName('Программирование');
        $manager = new Category();
        $manager->setName('Менеджмент');
        $administrator = new Category();
        $administrator->setName('Администрирование');
        $em->persist($design);
        $em->persist($programming);
        $em->persist($manager);
        $em->persist($administrator);
        $em->flush();
        $this->addReference('category-design', $design);
        $this->addReference('category-programming', $programming);
        $this->addReference('category-manager', $manager);
        $this->addReference('category-administrator', $administrator);
    }

    public function getOrder()
    {
        return 1;
    }
}

LoadJobData.php

<?php

# src/App/JoboardBundle/DataFixtures/ORM/LoadJobData

namespace App\JoboardBundle\DataFixtures\ORM;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use App\JoboardBundle\Entity\Job;

class LoadJobData extends AbstractFixture implements OrderedFixtureInterface
{
    public function load(ObjectManager $em)
    {
        $jobFullTime = new Job();
        $jobFullTime->setCategory($em->merge($this->getReference('category-programming')));
        $jobFullTime->setType('full-time');
        $jobFullTime->setCompany('ООО Компания');
        $jobFullTime->setLogo('company_logo.png');
        $jobFullTime->setUrl('http://example.com/');
        $jobFullTime->setPosition('Web Разработчик');
        $jobFullTime->setLocation('Москва');
        $jobFullTime->setDescription('Нужен опытный PHP разработчик');
        $jobFullTime->setHowToApply('Высылайте резюме на resume@example.com');
        $jobFullTime->setIsPublic(true);
        $jobFullTime->setIsActivated(true);
        $jobFullTime->setToken('job_example_com');
        $jobFullTime->setEmail('resume@example.com');
        $jobFullTime->setExpiresAt(new \DateTime('+30 days'));

        $jobPartTime = new Job();
        $jobPartTime->setCategory($em->merge($this->getReference('category-design')));
        $jobPartTime->setType('part-time');
        $jobPartTime->setCompany('ООО Дизайн Компания');
        $jobPartTime->setLogo('design_company_logo.gif');
        $jobPartTime->setUrl('http://design.example.com/');
        $jobPartTime->setPosition('Web Дизайнер');
        $jobPartTime->setLocation('Москва');
        $jobPartTime->setDescription('Ищем профессионального дизайнера');
        $jobPartTime->setHowToApply('Высылайте резюме на designer_resume@example.com');
        $jobPartTime->setIsPublic(true);
        $jobPartTime->setIsActivated(true);
        $jobPartTime->setToken('designer_resume@example.com');
        $jobPartTime->setEmail('resume@example.com');
        $jobPartTime->setExpiresAt(new \DateTime('+30 days'));
        $em->persist($jobFullTime);
        $em->persist($jobPartTime);
        $em->flush();
    }

    public function getOrder()
    {
        return 2;
    }
}

После того, как были написаны фикстуры, вы можете загрузить их через командную строку с помощью команды doctrine:fixtures:load:

php app/console doctrine:fixtures:load

Теперь если вы проверите базу данных, то в таблицах вы должны увидеть загруженные из фикстур данные.

Смотрим на данные в браузере

Если вы запустите следующую команду, то она создаст новый контроллер src/App/JoboardBundle/Controller/JobController.php с действиями для просмотра, создания, редактирования и удаление вакансий (и их соответствующие шаблоны, формы и маршруты):

php app/console doctrine:generate:crud --entity=AppJoboardBundle:Job --route-prefix=app_job --with-write --format=yml

Во время выполнения этой команды, вам нужно будет указать некоторые параметры, можете просто выбрать значения по умолчанию (которые в квадратных скобках). Для просмотра списка вакансий в браузере, мы должны импортировать новые маршруты, которые были созданы в src/App/JoboardBundle/Resources/config/routing/job.yml в файл маршрутизации основного бандла, добавьте следующий код в основной файл маршрутизации src/App/JoboardBundle/Resources/config/routing.yml:

AppJoboardBundle_job:
    resource: "@AppJoboardBundle/Resources/config/routing/job.yml"
    prefix: /job

Нам также нужно будет добавить метод _toString() в класс категории src\App\JoboardBundle\Entity\Category.php, чтобы модель Category могла использоваться в выпадающем списке в форме редактирования вакансии:

<?php

// ...
public function __toString()
{
    return $this->getName() ? $this->getName() : "";
}
// ...

Очистите кэш:

php app/console cache:clear --env=dev
php app/console cache:clear --env=prod

Теперь можно проверить работу контроллера в браузере: http://joboard.local/job/ или в окружении разработки, http://joboard.local/app_dev.php/job/.

Список вакансий

Теперь можно создавать и редактировать вакансии. Попробуйте оставить обязательные поля незаполненными или попробуйте ввести недопустимые данные. Это верно, Symfony создает основные правила проверки просмотрев схему базы данных. Вот и все. Сегодня мы написали мало PHP кода, но у нас уже есть рабочий модуль для модели вакансий. В следующей части мы будем работать с контроллером и представлением. Увидимся в следующей части!

Статьи из серии