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 кода, но у нас уже есть рабочий модуль для модели вакансий. В следующей части мы будем работать с контроллером и представлением. Увидимся в следующей части!

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

Комментарии

15
Ирина, 1 год назад
0

Добрый день!подскажите почему не работает эта команда php app/console doctrine:fixtures:load???

devacademy, 1 год назад
0

Добрый день, это значит, что у вас какая-то ошибка, а так команда работает.

Aleksandr, 11 месяцев назад
0

Вроде установил Symfony той же версии, что и у вас "2.5.*", а расположение файлов и папок постоянно отличаются. Например у вас src/App/JoboardBundle/, а у меня эти бандлы расположены так: \src\AppJoboardBundle{тут папки resources, controller..}. Ещё у меня нет файлов app_dev.php и config.php в основной директории. И по ссылке http://joboard.local/app_dev.php у меня открывается "Object not found!". Так же в папке var нет поддиректории www, как у вас (AcmeDemoBundle я не ставил, может из-за этого?). Дальше отличия будут продолжатся или я что-то неправильно сделал?

devacademy, 11 месяцев назад
0

app_dev.php должна лежать в директории web да и вообще статья устарела уже, сложно сказать что не так

Aleksandr, 11 месяцев назад
0

Точно, он там. Всё говорит о том, что я себе поставил Symfony 3

devacademy, 11 месяцев назад
0

ну папка var точно только в 3 появилась

Aleksandr, 11 месяцев назад
0

И ещё можно вопрос. Если у меня расположение папки другое, что мне писать в файле Category.orm.yml? Вот так: repositoryClass: src\AppJoboardBundle\Repository\ или всё-равно нужно написать так же, как у вас: repositoryClass: App\JoboardBundle\Repository\CategoryRepository?

devacademy, 11 месяцев назад
0

нужно писать как в namespace указан путь до файла App\JoboardBundle\Repository\CategoryRepository/

acanthis, 11 месяцев назад
0

А как быть со связями oneToMany? К примеру есть сущность контрагнет и счет контрагнета. У котрагента можеть быть сколь угодно счетов. Если я начну заполнять данные с добавления счетов то получу ошибку о том что я не заполнил обязательно поле ID контрагнета. Но где мне его взять если я его еще не создал? Выходит, что в начале нужно создать контаргента , а затем привязывать к нему счета? Немного запутался в последовательноти...

devacademy, 11 месяцев назад
1

Нужно создать грамотную форму и использовать трансформеры (но не те что в терминаторе 2) - http://symfony.com/doc/current/form/data_transformers.html

acanthis, 11 месяцев назад
0

Что Вы имеете ввиду под фразой "Грамотная форма"? У меня обычная коллекция. Добавляю через прототип новую запись. Примерно вот так: http://joxi.ru/a2XQb00cyRwjYA Это можно считать грамотной формой? :)

devacademy, 11 месяцев назад
0

у вас обычная форма, грамотная - это когда используются свои поля со своими шаблонами и обработчиками.

acanthis, 11 месяцев назад
1

Ну, по-крайней мере каждая моя форма имеет свой тип, отнаследованный от AbstractType со всеми вытекающими )

acanthis, 11 месяцев назад
0

Или, например, у меня есть обязательно поле при создании контрагента - его статус. Статус - это связь manyToOne. И как быть в этом случае? С одной стороны я не могу создать контрагента потому что не заполнено поле статус. С другой не могу создать статус, т.к. он требует айди контрагнета)

Алекcей, 2 месяца назад
0

при выполнении команды php app/console doctrine:fixtures:load появляется ошибка:

purging database loading [2] App\JoboardBundle\DataFixtures\ORM\LoadJobData

[OutOfBoundsException]
Reference to: (category-programming) does not exist