
Зберігання та зчитування інформації з бази даних, є звичним завданням і водночас містить виклик для будь-якого додатку. Хоча повністю укомплектований Symfony Framework не включає об’єктно-реляційного відображення за замовчуванням, Symfony Standard Edition, найчастіше використовуваний дистрибутив, містить інтегровану бібліотеку Doctrine, яка має на меті дати вам потужні інструменти для полегшення роботи. У цьому розділі ви ознайомитесь з базовою філософією бібліотеки Doctrine, і побачите наскільки простою може бути робота з базами даних.
Doctrine повністю відокремлена від Symfony, тому ви можете її і не використовувати. Цей розділ повністю присвячений Doctrine ORM (об’єктно-реляційне відображення), який дозволяє відображати об’єкти у реляційній базі даних (таких як MySQL, PostgreSQL, або Microsoft SQL). Якщо ви надаєте перевагу користуванню необробленими запитами, це напрочуд легко. Пояснення ви можете знайти у статті Довідника "How to Use Doctrine DBAL". Ви також можете зберігати дані у MongoDB, використовуючи бібліотеку Doctrine ORM. більше інформації стосовно цього питання читайте у документації: "DoctrineMongoDBBundle".
Простий приклад: об’єкт Product
Щоб зрозуміти як працює бібліотека Doctrine, найкраще побачити це в дії. У цьому підрозділі ми налаштуємо базу даних, створимо об’єкт Product, збережем його у базі даних та отримаємо його назад.
Налаштування бази даних
Перш ніж розпочати, налаштуємо інформацію щодо підключення бази даних. За звичай, ця інформація налаштовується у файлі app/config/parameters.yml:
# app/config/parameters.yml parameters: database_driver: pdo_mysql database_host: localhost database_name: test_project database_user: root database_password: password # ...
YAML
# app/config/config.yml doctrine: dbal: driver: "%database_driver%" host: "%database_host%" dbname: "%database_name%" user: "%database_user%" password: "%database_password%"
XML
<!-- app/config/config.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> <doctrine:config> <doctrine:dbal driver="%database_driver%" host="%database_host%" dbname="%database_name%" user="%database_user%" password="%database_password%" /> </doctrine:config> </container>
PHP
// app/config/config.php $configuration->loadFromExtension('doctrine', array( 'dbal' => array( 'driver' => '%database_driver%', 'host' => '%database_host%', 'dbname' => '%database_name%', 'user' => '%database_user%', 'password' => '%database_password%', ), ));
Відділяючи інформацію, що знаходиться у базі даних до іншого файлу, ви легко можете мати різні версії файлів на кожному сервері. Також, зберігайте налаштування бази даних (або будь-яку іншу інформацію) за межами вашого проекту, наприклад у налаштуваннях Apach. Щоб дізнатися більше, читайте розділ у Довіднику: “Як налаштувати зовнішні параметри у сервісі контейнером”.
Тепер, коли бібліотека Doctrine знає про вашу базу даних, вона може створити вам її:
$ php app/console doctrine:database:create
Налаштування кодування бази даних: UTF8
Помилкою, яку роблять навіть досвідчені розробники починаючи проекти на Symfony, є те, що вони забувають налаштувати кодування за замовчуванням, і порядок зіставлення та сортування символів у базі даних. Це все закінчується тим, що у базі даних так і залишається латинське кодування, так як воно налаштоване за замовчуванням майже в усіх базах даних. Вони пам’ятають про це при налаштуванні перший раз, але забувають, що після запуску відносно звичної всім команди під час розробки, ці налаштування анулюються:
$ php app/console doctrine:database:drop --force $ php app/console doctrine:database:create
Немає жодного способу, щоб налаштувати ці значення за замовчуванням всередині Doctrine, так як цей додаток має тенденцію бути якомога більш “агностичним”, в плані налаштування середовища. Одним із способів вирішення проблеми є налаштування значення за замовчуванням на рівні серверу. Налаштувати UTF8 для MySQL так само просто як і додати декілька рядків до конфігураційного файлу (типового my.cnf):
[mysqld] # Version 5.5.3 introduced "utf8mb4", which is recommended collation-server = utf8mb4_general_ci # Replaces utf8_general_ci character-set-server = utf8mb4 # Replaces utf8
Ми рекомендуємо вам використовувати новий набір символів для кодування - utf8mb4, замість звичного для MySQL UTF8, тому що він не підтримує 4-рьох байтних символів шифрування, а рядки, які містять таке шифрування будуть скорочуватися.
Якщо ви хочете використовувати SQLite, як вашу базу даних, вам слід написати команду і вказати де повинен знаходитися файл з базою даних:
YAML
# app/config/config.yml doctrine: dbal: driver: pdo_sqlite path: "%kernel.root_dir%/sqlite.db" charset: UTF8
XML
<!-- app/config/config.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> <doctrine:config> <doctrine:dbal driver="pdo_sqlite" path="%kernel.root_dir%/sqlite.db" charset="UTF-8" /> </doctrine:config> </container>
PHP
// app/config/config.php $container->loadFromExtension('doctrine', array( 'dbal' => array( 'driver' => 'pdo_sqlite', 'path' => '%kernel.root_dir%/sqlite.db', 'charset' => 'UTF-8', ), ));
Створюємо клас-сутність
Припустимо, що ви ствоюєте додаток, у якому повинні відображатися продукти. Навіть без роздумів про бази даних чи Doctrine, ви вже знаєте, що вам потрібен об’єкт Product, який і покаже ці продукти. Давайте створимо цей клас у директорії Entity вашого AppBundle:
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; class Product { protected $name; protected $price; protected $description; }
Клас, як його ще часто називають “сутність”, є звичайним базовим класом що містить певні дані. Будучи простим, він стає у нагоді при виконанні вимог потрібного продукту у вашому додатку. Поки цей клас не може бути збереженим у базі даних, так як це простий клас PHP.
Після того, як ви ознайомитесь з певними поняттями бібліотеки Doctrine, ви зможете створювати потрібні класи з її допомогою. Інтерактивні питання допоможуть вам збудувати будь-який клас:
$ php app/console doctrine:generate:entity
Додаємо інформацію про відображення
Doctrine дозволяє працювати з базами даних значно цікавішим способом ніж просто робити вибірку рядків з таблиці до масиву. Натомість, з Doctrine ви можете зберігати цілі об’єкти до бази даних та робити вибірку цих самих об’єктів. Це спрацює при відображенні PHP класу до таблиці бази даних, та властивостей цього класу PHP до колонок в таблиці.
Для того, аби бібліотека Doctrine могла таке зробити, вам необхідно створити “метадані”, або такі налаштування які “повідомлять” Doctrine, як клас Product і його властивості повинні відповідати базі даних. Ці метадані можуть бути у різних форматах, включаючи YAML і XML, або знаходитися прямо всередині класу Product через анотації:
Aнотації
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="product") */ class Product { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string", length=100) */ protected $name; /** * @ORM\Column(type="decimal", scale=2) */ protected $price; /** * @ORM\Column(type="text") */ protected $description; }
# src/AppBundle/Resources/config/doctrine/Product.orm.yml AppBundle\Entity\Product: type: entity table: product id: id: type: integer generator: { strategy: AUTO } fields: name: type: string length: 100 price: type: decimal scale: 2 description: type: text
XML
<!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml --> <?xml version="1.0" encoding="UTF-8" ?> <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> <entity name="AppBundle\Entity\Product" table="product"> <id name="id" type="integer"> <generator strategy="AUTO" /> </id> <field name="name" type="string" length="100" /> <field name="price" type="decimal" scale="2" /> <field name="description" type="text" /> </entity> </doctrine-mapping>
Пакет може прийняти лише один формат дефініцій метаданих. Наприклад, неможливо перемішати дефініції метаданих YAML з дефініціями анотованого PHP класу.
Ім’я таблиці не є обов’язковим, тому якщо воно не вказане, воно буде визначене автоматично, базуючись на імені класу об’єкта.
Doctrine дозволяє обирати будь-який тип поля з власними опціями. Для того щоб дізнатися більше інформації про доступні типи полів, читайте у підрозділі цого ж розділу: Типи полів у Doctrine.
Також, перегляньте базову документацію відображення Doctrine (Basic Mapping Documentation), для того щоб дізнатися деталі про відображення інформації. Якщо ви користуєтеся анотаціями, тоді вам слід обумовити їх усі з ORM\ (наприклад, ORM\Column(...)), що не показано у документації Doctrine. Вам також потрібно включити вираз use Doctrine\ORM\Mapping as ORM;, який імпортуватиме префікс анотації ORM.
Слідкуйте за тим, щоб ім’я вашого класу та властивостей не співпадало з ключовим словом SQL (group або user). Наприклад, якщо ім’я вашого класу-сутності Group, тоді, відповідно, за замовчуванням ім’я вашої таблиці буде group, що може спричинити помилку SQL на деяких движках. Якщо ви прагнете навчитися як уникати імен такого типу, читайте у документації: Reserved SQL keywords documentation. Проте, є ще одна альтернатива. Якщо ви вільно почуваєтеся у виборі схеми бази даних, тоді просто зробіть відображення до іншого імені таблиці, чи імені колонки. Детальніше читайте у документації: Creating Classes for the Database та Property Mapping.
При використанні іншої бібліотеки або програми (наприклад, Doxygen), яка використовує анотації, слід помістити анотацію @IgnoreAnnotation у клас, для того щоб вказати які саме анотації Symfony повинна ігнорувати. Наприклад, щоб не дозволити анотації @fn вивести на екран виняток, додайте наступне:
/** * @IgnoreAnnotation("fn") */ class Product // ...
Створення геттерів і сеттерів (Getters and Setters)
Не дивлячись на те, що Doctrine тепер знає як зберегти об’єкт Product у базі даних, у самий по собі клас не приносить користі. Так як Product є простим, регулярним класом у PHP, нам потрібно створити геттери і сеттери, наприклад, getName(), setName(). Ми це робимо для того, щоб отримати доступ до їхніх властивостей, так як вони є захищеними (protected). На щастя, Doctrine зробить це для вас, запустивши ось такий код:
$ php app/console doctrine:generate:entities AppBundle/Entity/Product
Ця команда “хоче переконатися”, що усі геттери і сеттери є згенерованими для класу Product. Це досить безпечна команда - ви можете запускати її знову і знову: так як вона генерує лише ті геттери і сеттери, які взагалі не існують (тобто, ця команда не замінить уже існуючих методів).
Запам’ятайте, що генератор об’єктів у Doctrine створює лише прості геттери і сеттери. Тому, вам слід перевіряти уже згенеровані об’єкти та пристосовувати код геттерів і сеттерів до ваших потреб.
Детальніше про команду doctrine:generate:entities
За допомогою команди doctrine:generate:entities ви зможете:
1) генерувати геттери і сеттери;
2) генерувати класи-сховища налаштовані за анотацією @ORM\Entity(repositoryClass="...");
3) генерувати відповідний конструктор у відношенні 1:m та n:m.
Команда doctrine:generate:entities зберігає копію Product.php з назвою Product.php~. У деяких випадках наявність даного файлу може спричинити таку помилку: “Клас не можна перевизначити” (англ. “Cannot redeclare class”). Але її можна позбутися. Скористайтеся опцією --no-backup, щоб запобігти створенню такого типу копій.
Зверніть увагу, що вам не потрібно використовувати цю команду. Doctrine не покладається на генерацію коду. Як і зі звичайним PHP класом, просто переконайтеся, що захищені (приватні) властивості містять геттери і сеттери. Цю команду було створено для полегшення роботи, так як це досить банальна річ яку слід робити при використанні бібліотеки Doctrine.
Ви також можете створювати загальновідомі класи (такі як клас PHP з інформацією про відображення бібліотеки Doctrine) пакета або цілого заповнювача.
# generates all entities in the AppBundle $ php app/console doctrine:generate:entities AppBundle # generates all entities of bundles in the Acme namespace $ php app/console doctrine:generate:entities Acme
Для Doctrine немає різниці якими є властивості: protected чи private, або чи наявна функція геттерів і сеттерів для тієї чи іншої властивості. Геттери і сеттери створюються лише для взаємодії з об’єктом PHP.
Створюємо таблиці/схему бази даних
Тепер у нас є потрібний клас Product з відображеною інформацією, і Doctrine знає як правильно зберегти його. Звичайно, у вас немає відповідної таблиці product у базі даних. На щастя, Doctrine може автоматично створити всі потрібні для кожного класу таблиці. Для цього, виконайте команду:
$ php app/console doctrine:schema:update --force
Насправді, ця команда надзвичайно потужна. Вона порівнює те, як ваша база даних повинна виглядати (опираючись на відповідну інформацію ваших класів) з тим, як вона насправді виглядає. Також генерує вирази SQL, які потрібні для оновлення БД, для того щоб вона знаходилася там де потрібно. Іншими словами, коли ви додаєте нову властивість з відображеними метаданими до класу Product і знову запускаєте це завдання, програма виконає команду "alter table", яка використовується для додавання нової колонки у вже існуючій таблиці product.
Ще краще використати цю функцію через пакет migrations. Це дасть змогу генерувати та зберігати SQL вирази у так званих міграційних класах, які можна систематично запускати на вашому робочому сервері, для того, щоб безпечно і надійно простежувати та переміщувати схеми баз даних.
Тепер база даних містить функціональну таблицю product з колонками, що збігаються з метаданими, які ви вказали.
Зберігаємо об’єкти до бази даних
Тепер коли клас Product відображується, і ми створили відповідну йому таблицю з таким же іменем, можемо зберегти дані у БД. З середини контролера, це дуже просто. Давайте додамо наступний метод дo DefaultController у пакеті:
// src/AppBundle/Controller/DefaultController.php // ... use AppBundle\Entity\Product; use Symfony\Component\HttpFoundation\Response; // ... public function createAction() { $product = new Product(); $product->setName('A Foo Bar'); $product->setPrice('19.99'); $product->setDescription('Lorem ipsum dolor'); $em = $this->getDoctrine()->getManager(); $em->persist($product); $em->flush(); return new Response('Created product id '.$product->getId()); }
Якщо ви виконали все як подано у прикладі, вам необхідно створити маршрут, який вказуватиме на цю дію, для того щоб побачити як все працює.
У цій статті розповідається про те, як працювати з бібліотекою Doctrine з середини контролера використовуючи метод конролера getDoctrine(). Цей метод є найкоротшим шляхом, за допомогою якого можна добратися до сервісу doctrine. Ви можете працювати з Doctrine будь-де вставивши цей сервіс у інший. Щоб дізнатися про це детальніше читайте розділ “Сервіс контейнером”.
Давайте розглянемо попередній приклад більш детально:
1) 10-13 рядки. Ви ілюструєте конкретним прикладом і працюєте разом з об’єктом $product, як і з будь-яким іншим звичайним об’єктом PHP.
2) 15 рядок. Ця команда добуває і вносить об’єкт Doctrine entity manager, який відповідає за проведення процесу зберігання, добування та внесення об’єктів до бази даних.
3) 16 рядок. Метод persist() дає команду Doctrine керувати об’єктом $product, але поки не відсилає запиту до бази даних.
4) 17 рядок. Коли робиться запит на виконання методу flush(), Doctrine переглядає всі об’єкти якими керує, щоби перевірити чи їх потрібно зберегти у базі даних. У цьому прикладі об’єкт $product ще поки не збережений, тому entity manager виконує запит INSERT, і тоді створюється рядок у таблиці product.
Насправді, так як Doctrine вже знає усі ваші класи, якими ви керуєте, коли ви даєте команду на виконання методу flush(), Doctrine підраховує усі зміни і тоді виконує запити у правильному порядку. Doctrine використовує вже підготовлені закешовані вирази, щоби трохи покращити продуктивність. Наприклад, якщо ви загалом зберегли 100 об’єктів Product, і потім даєте команду на виконання методу flush(), відповідно Doctrine виконає 100 запитів INSERT, використовуючи один підготовлений об’єкт.
Процес створення чи оновлення об’єктів завжди однаковий. У наступному підрозділі, ви побачите наскільки досвідченою є бібліотека Doctrine, оскільки вона автоматично виконує запит UPDATE, якщо запис вже існує у базі даних.
Doctrine надає бібліотеку, яка дозволяє програмно завантажувати тестовані дані у ваш проект, Щоб дізнатися все детальніше читайте розділ "DoctrineFixturesBundle".