¡Hola! My name is

Martin Zlámal

← back to the archive

Fixněte si databázi

Možná to znáte. Již nějaký čas nepoužíváte žádný SQL soubor a strukturu databáze si generujete z entit pomocí Doctrine. Je to super, rychlé a funguje to. Paráda. Jenže málokterá databáze se obejde bez nějakých inicializačních dat. Jenže jak na to?

První přístup #

Nebudu ho popisovat moc dlouho, protože ukazuje přesně to, co nechci ukázat. Jendoduše si napíšete nějaké to SQL, které pak nahrnete do databáze. Třeba nějak takto:

REPLACE INTO `options` (`key`, `value`)
VALUES
('option1', 'value1'),
('option2', 'value2'),
('option3', 'value3');

To jak si to pošlete do databáze je celkem jedno. Jestli ručně, nebo přes PHP. Pořád někde zůstává SQL. Proč mi to vadí? Tak třeba zde na blogu je nějaká instalace. A protože jsem se ještě nedokopal k tomu to přepsat, tak musím mít tyto soubory dva. Jeden pro MySQL a druhý pro PosgreSQL. (Jo správně, blog jde nainstalovat na více databází...) A to je voser.

Ale jsou i projekty, kde jsem to udělal rovnou pořádně (i když jsou jen na jedné databázi).

Lepší přístup pomocí fixtures #

Znáte Doctrine Data Fixtures Extension? Neznáte? Tak to doporučuji, protože vám pomohou vyřešit přesně tento problém. Lépe tuto knihovnu poznáte pomocí composeru:

composer require doctrine/data-fixtures

Samozřejmě je takový nepsaný předpoklad, že používáte Doctrine... :) Co dál? Ještě než se pustím do dalšího vysvětlování, bylo by fajn napsat si nějaký command. Na takový command objekt se nejlépe hodí moje oblíbená knihovna Kdyby/Console, která integruje command ze Symfony. Už jsem o tom psal něco málo dříve. A díky této přehršli odkazů již víte jak na to a můžeme rovnou nějaký psát. A protože jsem líný programátor, tak se podívám jak to vyřešil někdo jiný. A trošku si to zjedoduším:

<?php

use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Loader;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Kdyby\Doctrine\EntityManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class DefaultData extends Command
{

    /** @var EntityManager @inject */
    public $em;

    protected function configure()
    {
        $this
            ->setName('orm:demo-data:load')
            ->setDescription('Load data fixtures to your database.');
            //->addOption...
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $loader = new Loader();
            $loader->loadFromDirectory(__DIR__ . '/../basic');
            $fixtures = $loader->getFixtures();

            $purger = new ORMPurger($this->em);

            $executor = new ORMExecutor($this->em, $purger);
            $executor->setLogger(function ($message) use ($output) {
                $output->writeln(sprintf('  <comment>></comment> <info>%s</info>', $message));
            });
            $executor->execute($fixtures);
            return 0; // zero return code means everything is ok
        } catch (\Exception $exc) {
            $output->writeLn("<error>{$exc->getMessage()}</error>");
            return 1; // non-zero return code means error
        }
    }
}

Ok, to jsem to možná ořezal více než je třeba. Mrkněte na tu ukázku pro Symfony, bude to velmi podobné. A teď už konečně k samotným fixture objektům. To jsou ty co načítám ze složky basic pomocí loadFromDirectory. Jedná o objekty, které implementují interface FixtureInterface, nebo možná lépe dědí od abstraktní třídy AbstractFixture. Obojí je v Doctrine\Common\DataFixtures namespace. Objekt obsahující defaultní uživatele může vypadat takto:

<?php

use Doctrine\Common\Persistence\ObjectManager;
use Nette\Security\Passwords;

class UsersFixture extends \Doctrine\Common\DataFixtures\AbstractFixture
{

    public function load(ObjectManager $manager)
    {
        $admin = new \Users\User('[email protected]');
        $admin->setPassword(Passwords::hash('admin'));
        $admin->addRole($this->getReference('admin-role'));
        $manager->persist($admin);

        $demo = new \Users\User('[email protected]');
        $demo->setPassword(Passwords::hash('demo'));
        $demo->addRole($this->getReference('demo-role'));
        $manager->persist($demo);

        $manager->flush();

        $this->addReference('admin-user', $admin);
        $this->addReference('demo-user', $demo);
    }

}

V čem je to tak parádní? Používám PHP kód, používám vlastní nadefinované entity. Hned vidím, že mi to fugnuje, ověřuji svůj návrh databáze a rovnou poskytuji dalším ukázku toho, jak jsem to myslel. Za povšimnutí stojí funkce addReference a getReference. Je jasné, že v každé relační databázi budou nějaké relace a právě k tomu tyto funkce slouží. Vytvořím si tedy nějaké ukazatele a ty pak mohu použít v jiné části demo dat. Lépe to bude vidět na druhé tabulce:

<?php

use Doctrine\Common\Persistence\ObjectManager;

class RolesFixture extends \Doctrine\Common\DataFixtures\AbstractFixture
{

    public function load(ObjectManager $manager)
    {
        $user = new \Users\Role();
        $user->setName(\Users\Role::DEMO_USER);
        $manager->persist($user);

        $admin = new \Users\Role();
        $admin->setName(\Users\Role::ADMIN);
        $manager->persist($admin);

        $manager->flush();

        $this->addReference('demo-role', $user);
        $this->addReference('admin-role', $admin);
    }

}

Vidíte? Mám role, vytvořím si na ně odkaz a používám je při definici uživatele. Vyzkoušejte si to. Uvidíte, jak se krásně naplní referenční tabulky a vše bude tak, jak to má být...

Jen pozor na jedno věc. Ohlídejte si pořadí těchto objektů. To lze vyřešit implementací rozhraní OrderedFixtureInterface, nebo DependentFixtureInterface, což je o něco lepší přístup.

A jak toto celé použít? Však už to znáte:

λ php index.php
λ php index.php orm:schema-tool:create
λ php index.php orm:demo-data:load

První příkaz vám nabídne všechny dostupné příkazy, druhý vygeneruje strukturu databáze bez dat a poslední spustí natažení demo dat. Pak už se jen kocháte:

λ php index.php orm:demo-data:load --demo
Careful, database will be purged. Do you want to continue Y/N ? y
  > purging database
  > loading RolesFixture
  > loading UsersFixture
  > loading ArticlesFixture
  > loading ProductsFixture
  ...

Do you have any comments? That's great! Tweet them so everyone can hear you…

← back to the archive