Nette a Doctrine

Účast na mé první Posobotě předčila veškeré očekávání. Perfektní workshop, skvělé přednášky a úžasní lidé kteří nic netají. Na workshopu jsem díky Filipovi a Chemixovi pronikl do základů Doctrine. Zkusím tyto informace sepsat.

Instalujeme Nette

Ani nemyslete na to že to budete zkoušet bez composeru 🙂

composer create-project nette/sandbox nette-doctrine

nette-doctrine je název složky do které se sandbox vytvoří.

Instalujeme Kdyby/Doctrine

cd nette-doctrine
composer require kdyby/doctrine

Pokud máte PHP verzi alespoň 5.4 hned z konzole můžete spustit web server:

php -S localhost:8888 -t www

webserver se spustí na portu 8888. Pokud do prohlížeče zadáte http://localhost:8888 uvidíte stránku Nette Congratulations!

Práda! Můžeme začít spouštět IDE a zažneme kódovat.

PhpStorm a live templaty pro Doctrine

Rozhodně doporučuji PhpStorm do kterého Filip napsal skvělé live templaty. Ušetří spoustu psaní a času.

Live templaty stáhněte a nahrajte do:

<your home directory>\.WebIde<version>\config\templates

Rozhodně doporučuji soubory: Doctrine.xml, Nette.icls, Nette.xml

Po nahrání je nutné znovu spustit PhpStorm.

Vytvoření databáze

Pojďme si vytvořit databázi a uživatele pro přístup k databázi:

CREATE DATABASE `doctrine_devel` COLLATE 'utf8_czech_ci';
CREATE USER 'doctrine'@'localhost' IDENTIFIED BY 'doctrine_pass';
GRANT USAGE ON * . * TO 'doctrine'@'localhost' IDENTIFIED BY 'doctrine_pass' WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0;
GRANT ALL PRIVILEGES ON `doctrine\_%` . * TO 'doctrine'@'localhost';
FLUSH PRIVILEGES;

Příkaz vykonáme v Admineru: http://localhost:8888/adminer

Klikneme na tlačítko SQL příkaz. Do okna vložíme kód výše a jeho vykonání potvrdíme tlačítkem Provést.

Tím jsme vytvořili databázi doctrine_devel a uživatele doctrine s heslem doctrine_pass.

Zaregistrování Doctrine do Nette

Do config.neon zamergujeme následující kód:

parameters:
    database:
        host: localhost
        dbname: production_db
        user: production
        password: production_pass

extensions:
    console: Kdyby\Console\DI\ConsoleExtension
    events: Kdyby\Events\DI\EventsExtension
    annotations: Kdyby\Annotations\DI\AnnotationsExtension
    doctrine: Kdyby\Doctrine\DI\OrmExtension

doctrine:
    dbname      : %database.dbname%
    host        : %database.host%
    user        : %database.user%
    password    : %database.password%
    metadata:
        App: %appDir%

Do config.local.neon uložíme přístupové údaje do DB pro vývoj u nás na localhostu:

parameters:
    database:
        host: localhost
        dbname: doctrine_devel
        user: doctrine
        password: doctrine_pass

Podle dokuentace Kdyby/Doctrine si vytvoříme první entitu:

app/model/Article.php

<?php

namespace App;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Article extends \Kdyby\Doctrine\Entities\BaseEntity
{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

}

V entitě Article definuje id (celé číslo, které se bude inkrementovat) a title (text).

Vytvoření tabulky z entity

V první řadě ověříme zda funguje Doctrine/Console:

php ./www/index.php

Výstup by měl vypadat takto a měl by zobrazovat všechny dostupné příkazy pro Doctrine:

D:\www\nette-doctrine>php ./www/index.php
Nette Framework version 2.2.7

Usage:
 [options] command [arguments]

Options:
 --help (-h)           Display this help message
 --quiet (-q)          Do not output any message
 --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
 --version (-V)        Display this application version
 --ansi                Force ANSI output
 --no-ansi             Disable ANSI output
 --no-interaction (-n) Do not ask any interactive question

Available commands:
 help                       Displays help for a command
 list                       Lists commands dbal
 dbal:import                Import SQL file(s) directly to Database. orm
 orm:clear-cache:metadata   Clear all metadata cache of the various cache drivers.
 orm:clear-cache:query      Clear all query cache of the various cache drivers.
 orm:clear-cache:result     Clear all result cache of the various cache drivers.

 orm:convert-mapping        Convert mapping information between supported formats.
 orm:convert:mapping        Convert mapping information between supported formats.
 orm:generate-entities      Generate entity classes and method stubs from your mapping information.
 orm:generate-proxies       Generates proxy classes for entity classes.
 orm:generate:entities      Generate entity classes and method stubs from your mapping information.
 orm:generate:proxies       Generates proxy classes for entity classes.
 orm:info                   Show basic information about all mapped entities
 orm:schema-tool:create     Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.
 orm:schema-tool:drop       Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output.
 orm:schema-tool:update     Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata.
 orm:validate-schema        Validate the mapping files.

Provedeme validaci:

php ./www/index.php orm:validate-schema

OK, to je v pořádku:

[Mapping]  OK - The mapping files are correct.
[Database] FAIL - The database schema is not in sync with the current mapping file.

Podíváme se jaký SQL kód nám Doctrine nabízí pro synchronizaci databáze:

php ./www/index.php orm:schema-tool:update --dump-sql

SQL kód pro synchronizaci databáze:

CREATE TABLE article (id INT AUTO_INCREMENT NOT NULL, author_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, text VARCHAR(255) DEFAULT NULL, INDEX IDX_23A0E66F675F31B (author_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE author (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE tag (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE tag_article (tag_id INT NOT NULL, article_id INT NOT NULL, INDEX IDX_300B23CCBAD26311 (tag_id), INDEX IDX_300B23CC7294869C (article_id), PRIMARY KEY(tag_id, article_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES author (id);
ALTER TABLE tag_article ADD CONSTRAINT FK_300B23CCBAD26311 FOREIGN KEY (tag_id)
REFERENCES tag (id) ON DELETE CASCADE;
ALTER TABLE tag_article ADD CONSTRAINT FK_300B23CC7294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE;

Tento kód můžete vykonat v admineru, neb pokud máte koule můžete ho nechat vykonat přímo z příkazové řádky (hlavouni to nedoporučují):

php ./www/index.php orm:schema-tool:update --force

Nyní když znovu spustíme validaci, vidíme:

D:\www\nette-doctrine>php ./www/index.php orm:validate-schema
[Mapping]  OK - The mapping files are correct.
[Database] OK - The database schema is in sync with the mapping files.

Namapování a databáze jsou OK, pojďme na ukládání a zobrazení dat.

Doctrine – ukládání dat

V HomepagePresenter.php si uděláme add akci na přidání záznamů do entity Article:

<?php

namespace App\Presenters;

use App\Article;
use Nette;
use Kdyby;

/**
 * Homepage presenter.
 */
class HomepagePresenter extends BasePresenter
{

	/**
	 * @inject
	 * @var Kdyby\Doctrine\EntityManager
	 */
	public $entityManager;

	public function actionAdd()
	{
		$daoArticle = $this->entityManager->getRepository(Article::getClassName());

		$article = new Article();
		$article->title= "Slabikář";
		$daoArticle->save($article);

		$article = new Article();
		$article->title= "Čítanka";
		$daoArticle->save($article);

		exit();
	}

}

Definovali jsme $entityManager do které jsme injectli Kdyby\Doctrine\EntityManager.

V actionAdd jsme vytvořili 2 články které uložíme do DB. První s názvem Slabikář a druhý s názvem Čítanka. V DB se můžete přesvědčit že došlo k jejich uložení do tabulky article.

Doctrine výpis dat

Velice podobným způsobem jako jsme ukládali data je budeme vypisovat. Opět budeme potřebovat EntityManager:

<?php

namespace App\Presenters;

use App\Article;
use Nette;
use Kdyby;

/**
 * Homepage presenter.
 */
class HomepagePresenter extends BasePresenter
{

	/**
	 * @inject
	 * @var Kdyby\Doctrine\EntityManager
	 */
	public $entityManager;

	public function renderDefault()
	{
		$daoArticle = $this->entityManager->getDao(Article::getClassName());
		$this->template->articles = $daoArticle->findAll();
	}
}

v šabloně tyto články zobrazíme:

{block content}

{foreach $articles as $article}

    {$article->title}<br/>

{/foreach}

Jednoduché že ano?

Přidání nového položky do entity

Naše aplikace je známá po celém světě a používají ji milony lidí kteří píší podměty že jim pouze název článku nestačí a chtěli by ještě text článku 🙂 Vyhovíme jim a přidáme do entity Article ještě položku text.

Nyní nám perfektně poslouží Filipovo live templaty. Otevřeme si soubor Article.php a pod proměnou private $title odřádkujeme a napíšeme: col <tab> (napíšeme col a stiskneme klávesu Tab). Ide nám předvygeneruje následující kód:

    /**
     * @ORM\Column(type="string", nullable=TRUE)
     * @var string
     */
    protected $;

ve kterém se pohybujeme klávesou Tab. Napíšeme název proměnné (text) a definujeme typ (text). Výsledek vypadá takto:

    /**
     * @ORM\Column(type="text", nullable=TRUE)
     * @var text
     */
    protected $text;

Provedeme validaci:

php ./www/index.php orm:validate-schema
D:\www\nette-doctrine>php ./www/index.php orm:validate-schema
[Mapping]  OK - The mapping files are correct.
[Database] FAIL - The database schema is not in sync with the current mapping file.

která nám oznámí že nemáme synchronizovanou databázi. Podíváme se jaké úpravy databáze nám Doctrine nabízí:

D:\www\nette-doctrine>php ./www/index.php orm:schema-tool:update --dump-sql
ALTER TABLE article ADD text LONGTEXT DEFAULT NULL;

Kód vykonáme v admineru, nebo 🙂

php ./www/index.php orm:schema-tool:update --force

Následná validace by měla být OK. Tím máme přidán sloupec text a můžeme do něho rovnou ukládat data. Vytáhneme námi uložené publikace a donastavíme jim text:

    public function actionUpdate()
    {
        $daoArticle = $this->entityManager->getRepository( Article::getClassName() );

        $article = $daoArticle->findOneBy( array( "title" => "Slabikář" ) );
        $article->text = "Text knihy slabikář";

        $article = $daoArticle->findOneBy( array( "title" => "Čítanka" ) );
        $article->text = "Text knihy čítanka";

        $this->entityManager->flush();
        exit();
    }

Upravíme šablonu abychom vypisovali text:

{block content}

{foreach $articles as $article}

    <h2>{$article->title}</h2>
    <p>{$article->text}</p>

{/foreach}

 Vyšší liga – vazby mezi entitami

Toto jsme splodili na Workshopu:

Article.php

<?php

namespace App;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Article extends \Kdyby\Doctrine\Entities\BaseEntity
{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

    /**
     * @ORM\Column(type="string", nullable=TRUE)
     * @var string
     */
    protected $text;

    /**
     * @ORM\ManyToOne(targetEntity="\App\Author", inversedBy="articles", cascade={"persist"})
     * @var Author
     */
    protected $author;

    /**
     * @ORM\ManyToMany(targetEntity="\App\Tag", mappedBy="articles", cascade={"persist"}, orphanRemoval=true)
     * @var \App\Tag[]|\Doctrine\Common\Collections\ArrayCollection
     */
    protected $tags;


    function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    public function addTag(Tag $tag)
    {
        $tag->addArticle($this);
        $this->tags->add( $tag );
    }

}

Author.php

<?php

namespace App;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;


/**
 * @ORM\Entity
 * @property $name
 */
class Author extends \Kdyby\Doctrine\Entities\BaseEntity
{


    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $name;

    /**
     * @ORM\OneToMany(targetEntity="\App\Article", mappedBy="author", cascade={"persist"})
     * @var Article[]|\Doctrine\Common\Collections\ArrayCollection
     */
    protected $articles;

    function __construct()
    {
        $this->articles = new ArrayCollection();
    }

    public function addArticle(Article $article)
    {
        $this->articles->add( $article );
    }
}

Tag.php

<?php


namespace App;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;


/**
 * @ORM\Entity
 * @property $name
 */
class Tag extends \Kdyby\Doctrine\Entities\BaseEntity
{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $name;


    /**
     * @ORM\ManyToMany(targetEntity="\App\Article", inversedBy="tags", cascade={"persist"})
     * @var \App\Article[]|\Doctrine\Common\Collections\ArrayCollection
     */
    protected $articles;


    function __construct()
    {
        $this->articles = new ArrayCollection();
    }

    public function addArticle(Article $article)
    {
        $this->articles->add( $article );
    }
}

HomepagePresenter.php

<?php

namespace App\Presenters;

use App\Author;
use App\Tag;
use Nette,
	App\Model;
use App\Article;

/**
 * Homepage presenter.
 */
class HomepagePresenter extends BasePresenter
{

	/**
	 * @inject
	 * @var \Kdyby\Doctrine\EntityManager
	 */
	public $EntityManager;

	public function renderDefault()
	{
		$this->template->anyVariable = 'any value';
		$dao = $this->EntityManager->getRepository(Article::getClassName());
		$this->template->articles = $dao->findAll();

	}

	public function actionAdd()
	{

		$daoArticle = $this->EntityManager->getRepository(Article::getClassName());
		$daoAuthor = $this->EntityManager->getRepository(Author::getClassName());

		//$author = new Author();
		//$author->name = "Pokusne jmeno";

		$author = $daoAuthor->findOneBy( array("name" => "Pokusne jmeno"));

		$article = new Article();
		$article->title = "titulke sntagem";
		$article->text = "text novinky s tagem";
		$article->author = $author;
		$author->addArticle($article);

		$tag = new Tag();
		$tag->name = "kniha";

		$article->addTag($tag);

		$tag = new Tag();
		$tag->name = "kniha dvojita";

		$article->addTag($tag);

		$daoArticle->save($article);

		exit();
	}

}

Šablona:

{block content}

{foreach $articles as $article}

    {$article->title}<br>
    {$article->author->name}<br/>
    {dump $article->tags}
    Tags: {if $article->tags}{foreach $article->tags as $tag}{$tag->name}, {/foreach}{/if}<br/>
<hr/>
{/foreach}

{/block}

 

Celé databázové schéma smažeme a vytvoříme znovu:

php ./www/index.php orm:schema-tool:drop --force

php ./www/index.php orm:schema-tool:create

Celou ukázku jsem nahrál na GitHub: https://github.com/venca-x/nette-sandbox-doctrine