poniedziałek, 1 czerwca 2015

[PHP][Doctrine2] Instalacja i wstęp do Doctrine2 ORM

TRUE
1915472901869042583
Doctrine jest systemem ORM dla relacyjnych baz danych, napisanym w PHP. ORM (Object-relational Mapping), czyli po polsku Mapowanie Obiektowo-relacyjne jest to ogólnie rzecz biorąc technika zamiany danych opisanych za pomocą obiektów w ich relacyjne modele w bazie danych. W systemach ORM pracujemy nie na rekordach z bazy, lecz na obiektach, gdyż każdy rekord reprezentowany jest jako pełnoprawny obiekt. Doctrine jest właśnie jednym z najpopularniejszych rozwiązań, które oferują takie mapowanie dla PHP.

Korzystając z bibliotek Doctrine każda tabela w bazie danych prezentowana jest jako jedna wielka klasa, gdzie poszczególne rekordy (wiersze) są jej obiektami. Na obiektach takich pracujemy dokładnie tak samo jak na zwykłych obiektach, utworzonych w tradycyjny sposób. W artykule tym zainstalujemy sobie Doctrine na własnym serwerze i nauczymy się podstaw pracy z nim.

Instalacja

Instalację Doctrine zaczynamy od pobrania najnowszej wersji za pomocą Composera (tutorial dla Composera znajdziesz tutaj). Strona oficjalna z najnowszą wersją Doctrine znajduje się tutaj: http://www.doctrine-project.org/projects/orm.html 
Wchodzimy na nią, sprawdzamy numerek najnowszej wersji i sporządzamy odpowiedni plik composer.json. W tym artykule pobierzemy wersję 2.4.7, więc zawartość pliku composer.json wyglądać będzie tak:
[code]
{
  "require": {
    "doctrine/orm": "2.4.7"
  }
}[/code]
Zapisujemy plik, np. w katalogu:
[code]C:/www/doctrine [/code]
i w terminalu (będąc w w/w wymienionym katalogu) pobieramy paczkę za pomocą biblioteki Composer:
[code]$ composer install[/code]
Pobierze nam to do folderu /vendor najnowszą wersję wraz ze wszystkimi wymaganymi zależnościami. Od tej chwili jesteśmy już prawie gotowi do pracy z Doctrine, zanim to jednak zrobimy przyjrzyjmy się krótkiemu opisowi jak wygląda praca z techniką ORM.


Model ORM

Jak wiemy, dane w bazie danych przechowywane są w formie rekordów, czyli wierszy, przy czym kolumny w tych wierszach reprezentują pola rekordu. Pobierając takie dane z bazy zazwyczaj otrzymujemy natomiast tablicę reprezentującą dany wiersz, w której to tablicy poszczególne pola to klucze/indeksy tej tablicy. Przykładowo, pobierając z tabeli o nazwie customers pola o nazwach customer_id, first_name i last_name otrzymujemy tablicę (rekord) postaci:
[code]
$row['customer_id'];
$row['first_name'];
$row['last_name'];[/code]
W modelu ORM będzie to wyglądało nieco inaczej. Nie otrzymamy wiersza jako tablicy, lecz jako obiekt, którego właściwości będą odpowiadać polom kolumn w wierszu:
[code]
class Customer {

  protected $customer_id,
  protected $first_name;
  protected $last_name;
  [...]
}
$customer->new Customer();
[/code]
Do właściwości takich będziemy mieli dostęp za pomocą utworzonych metod getXXX(), jak np.:
[code]
$customer->getId();
$customer->getFirstName();
$customer->getLastName();
[/code]

Również tworzenie nowego rekordu w bazie nie będzie już polegało na wysłaniu do niej zapytania typu INSERT, lecz na zwyczajnym utworzeniu nowego obiektu:
[code]$customer = new Customer();[/code]
a następnie na wysłaniu obiektu do systemu ORM, który już odpowiednio przetworzy wszystko tak, aby nasz obiekt zamienić na odpowiedni rekord i umieścić go w bazie.
Analogicznie - aktualizowanie wartości pól rekordu nie będzie też już polegało na zapytaniu UPDATE, lecz na wywoływaniu odpowiednich metod obiektu, jak np.:
[code]
$customer->setFirstName($first_name);
$customer->setLastName($last_name);[/code]
Podczas pracy w modelu ORM zapomnimy całkowicie o bazie danych - wszystkie, ale to WSZYSTKIE operacje na rekordach będą odbywały się jak na zwykłych obiektach. Sposób taki więc powoduje, iż nasz kod staje się jedną wielką, zintegrowaną strukturą, w której nie musimy nagle wybijać z metodami operującymi na bazie i burzącymi pełną obiektowość kodu.

Podejście takie ma całą listę zalet i chyba tylko jeden minus, którym jest konieczność ręcznego przygotowania schematu bazy i konieczność aktualizacji takiego modelu po każdorazowej modyfikacji struktury tabel. Nie jest to jednak trudne i nauczymy się tutaj tego krok po kroku.

Tak na marginesie - Doctrine oferuje coś w rodzaju inżynierii wstecznej, czyli mechanizm który może na podstawie analizy struktury bazy przygotować taki model automatycznie, jednakże po pierwsze nie zawsze zadziała to tak jak jakbyśmy tego oczekiwali, a po drugie - lepiej nauczyć się przygotowywania schematu manualnie, gdyż wiedza ta będzie konieczna, jeśli chcemy pracować na poważnie.

Wiemy już na czym mniej więcej polega zasada działania, pora więc na praktykę, ale żeby stworzyć swój pierwszy projekt, musimy najpierw nauczyć się kilku podstawowych pojęć z jakich będziemy korzystać i z jakich korzysta Doctrine.

Entity

Na rodzimy język możemy to przetłumaczyć jako byt, jednostka. Terminem Entity nazywamy w Doctrine każdy obiekt mapowany na bazę. Każdy taki obiekt to oddzielny byt. Główną i podstawową klasą, która zarządza naszymi bytami jest w Doctrine EntityManager. Aby zrozumieć to obrazowo, spójrzmy na przykład: mając tabelę o nazwie users składającą się z pól:

  • id (int, primary index)
  • login (varchar 50)
  • password (varchar 80)

i chcąc ją zmapować jako obiekt, musimy utworzyć dla niej klasę:
[code]
<?php
// Entity
class User {

  protected $id;
  protected $login;
  protected $password;
}
?>[/code]
To co powyżej to właśnie nasz Entity, czyli obiekt mapowany na bazę danych.
Widzimy tutaj, że został on opisany za pomocą definicji klasy zwykłym kodem PHP, ale w Doctrine możemy użyć do opisu także XML i YAML-a.

W XML-u nasz Entity opisany będzie tak:
[code]
<doctrine-mapping>
  <entity name="User">
    <field name="id" type="integer" />
    <field name="login" length="50" name="login" />
    <field name="password" length="80" />
  </entity>
</doctrine-mapping>[/code]

A w YAML-u tak:
[code]
User:
  type: entity
  fields:
    id:
      type: integer
    login:
      length: 50
      name: login
    password:
      length: 80[/code]
   
W PHP podałem powyżej jedynie obrazowy przykład, właściwy opis bytu wyglądałby tak:
[code]
<?php
/** @Entity */
class User {

  /** @Column(type="integer") */
  protected $id;
  /** @Column(length=50, name="login") */
  protected $login;
  /** @Column(length=80) */
  protected $password;
}
?>[/code]
Te dodatkowe pola z opisami właściwości danej kolumny wyglądające na zwykłe komentarze blokowe to adnotacje docblock i są one odczytywane i przetwarzane przez Doctrine. Analogicznych używamy dla PHPDocumentatora.

Właściwości mapowanych kolumn

Właściwości opisujących dane pole jest oczywiście więcej:

  • type - typ kolumny, np. integer, domyślnie:  string.
  • name - nazwa kolumny w bazie, domyślnie: nazwa taka sama jak deklaracja.
    UWAGA: W przypadku nazwy będącem słowem kluczowym ujmujemy w cytat: np. `name`
  • length - jeśli pole jest typu string to określa jego długość, domyślnie: 255
  • unique - określa, czy pole posiada unikalny klucz, domyślnie: false
  • nullable - czy kolumna może przyjąć NULL, domyślnie: false
  • precision - precyzja dla liczb dziesiętnych, czyli maksymalna liczba wszystkich cyfr dziesiętnych, domyślnie: 0
  • scale - precyzja dla liczb dziesiętnych, ale tylko dla wartości po przecinku, domyślnie: 0, nie może być większa niż określona w precision
  • columnDefinition - własna definicja wykonywana podczas tworzenia kolumny, np. CHAR(3) NOT NULL
  • options - dodatkowe opcje podawane na zasadzie klucz-wartość


Jeśli nie określamy typu pola ręcznie to przyjmowane jest, że jest ono typu string, czyli standardowy VARCHAR w bazie.

Mapowane typy kolumn 

Typy kolumn dostępne w Doctrine to:
(podaję nazwę typu dla PHP i odpowiednik w bazie SQL)

  • string - mapuje VARCHAR w SQL na string w PHP
  • integer - mapuje INT w SQL na integer w PHP
  • smallint - mapuje SMALLINT w SQL na integer w PHP
  • bigint - mapuje BIGLLINT w SQL na integer w PHP
  • boolean - mapuje TINYINT/boolean w SQL na boolean w PHP
  • decimal - mapuje DECIMAL w SQL na string w PHP
  • date - mapuje DATETIME w SQL na obiekt DateTime w PHP
  • time - mapuje TIME w SQL na obiekt DateTime w PHP
  • datetime - mapuje DATETIME/TIMESTAMP w SQL na obiekt DateTime w PHP
  • datetimetz - mapuje DATETIME/TIMESTAMP w SQL na obiekt DateTime ze strefą czasową w PHP
  • text - mapuje CLOB w SQL na string w PHP
  • object - mapuje CLOB w SQL na obiekt w PHP, z użyciem serialize() i unserialize()
  • array - mapuje CLOB w SQL na tablicę w PHP, z użyciem serialize() i unserialize()
  • simple_array - mapuje CLOB w SQL na tablicę w PHP, z użyciem implode() i explode(), rozdzialając przecinkiem (,)
  • json_array - mapuje CLOB w SQL na tablicę w PHP, z użyciem json_encode() i json_decode()
  • float - mapuje FLOAT w SQL na double w PHP (wymagana kropka jako separator części po przecinku)
  • guid - mapuje GUID/UUID w SQL na string w PHP
  • blob - mapuje BLOB w SQL na resource w PHP


Identyfikator

Określa on primary key dla tabeli w bazie danych. Jego zdefiniowanie jest wymagane.
Klucz podstawowy denifiujemy następująco:

PHP:
[code]
<?php
/** @Entity */
class User {
  /**
   * @Id @Column(type="integer")
   * @GeneratedValue(strategy="AUTO")
   */
  protected $id;
  //[...]
}
?>[/code]

XML:
[code]
<doctrine-mapping>
  <entity name="User">
    <id name="id" type="integer">
       <generator strategy="AUTO" />
    </id>
    <!-- [...]-->
  </entity>
</doctrine-mapping>[/code]

YAML:
[code]
User:
  type: entity
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    # [...][/code]


Podczas definicji identyfikatora możemy określić sposób w jaki wartość ma być inkrementowana, do czego służy wyżej widoczny parametr GENERATOR STRATEGY.
Możliwe do ustawienia wartości to: auto (domyślnie), sequence, identity i none. Więcej o nich napiszę w pozostałych artykułach.

Próba ognia

Opiszę teraz dokładnie krok po kroku, co zrobić, żeby wszystko nam zadziałało, gdyż trzeba tutaj wykonać kilka manewrów, podczas których niedoświadczeni z Doctrine mogliby coś pominąć. Zwłaszcza, że w oficjalnym poradniku jest to wszystko opisane trochę nie po kolei. Na początek musimy stworzyć sobie jeden folder i trochę usprawnić swoją instalację Doctrine - a użyjemy do tego Composera.

Krok 1 - tworzymy katalog na definicje Entities

Stwórzmy folder o nazwie:
[code]c:/www/doctrine/src/Model[/code]
W folderze tym zapiszemy pliki z deklaracjami naszych Entities, które wykorzysta Doctrine.

Krok 2 - modyfikujemy composer.json i aktualizujemy autoloader

Następnie zmodyfikujmy lekko plik:
[code]C:/www/doctrine/composer.json[/code]
zmieniając jego zawartość na:
[code]
{
  "require": {
    "doctrine/orm": "2.4.7",
    "symfony/yaml": "*"
  },
  "autoload": {
      "psr-0": {"": "src/Model/"}
  }
}[/code]
gdzie w miejsce wersji Doctrine podajmy tą, którą pobieraliśmy na początku.
Powyższy wpis spowoduje, że doinstalujemy sobie obsługę plików YAML, a także zmodyfikujemy lekko autoloader Composera, tak aby uwzględniał nam pliki z katalogu /src/Model, czyli katalogu, w którym zapiszemy pliki z deklaracjami Entities. Bez tego manewru napotkalibyśmy w kolejnych krokach na błąd mówiący o braku definicji klasy.

Mając tak zmodyfikowany composer.json wchodzimy teraz do konsoli/terminala i z poziomu katalogu:
[code]C:/www/doctrine/[/code]
wpisujemy:
[code]$ composer update[/code]
Doinstaluje nam to pakiet YAML i zaktualizuje autoloader.



Krok 3 - tworzymy definicję prostego obiektu Entity

Stwórzmy teraz sobie prosty obiekt mapujący tabelę users.
Utwórzmy zatem plik:
[code]C:/www/doctrine/src/Model/User.php[/code]
i jako jego zawartość wklejmy:
[code]
<?php
// /src/Model/User.php
/**
 * @Entity @Table(name="users")
 **/
class User {

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

  /** @Column(length=50, name="login") */
  protected $login;

  /** @Column(length=80) */
  protected $password;


  public function getId()
  {
    return $this->id;
  }

  public function getLogin()
  {
    return $this->login;
  }

  public function setLogin($login)
  {
   $this->login = $login;
  }

  public function getPassword()
  {
    return $this->password;
  }

  public function setPassword($password)
  {
   $this->password = $password;
  }
}[/code]
Będzie to definicja obiektu, który będziemy mapować na tabelę users.
Definiujemy tutaj 3 pola: id, login i password oraz metody getXXX() i setXXX() dla tych pól.
Zauważmy, że dla pola $id nie definiujemy metody setId(). Mając tak przygotowaną konfigurację prostego bytu lecimy dalej.
Zauważmy też, iż definiujemy nazwę tabeli jako users mimo iż nazwa klasy to User.

Krok 4 - bootstrap.php

Tworzymy teraz plik:
[code]C:/www/doctrine/bootstrap.php[/code]
i wpisujemy mu taką zawartość:
[code]
<?php
// bootstrap.php
// Dołączamy autoloader Composera
require_once "/vendor/autoload.php";

// Określamy przestrzenie nazw z jakich skorzystamy
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;

// określamy ścieżkę do plików z Entities
$paths = array(__DIR__.'/src/Model');

// informujemy, że pracujemy w środowisku developerskim, a nie na produkcji
$isDevMode = true;

// konfigurujemy dostęp do bazy
$dbParams = array(
    'driver'   => 'pdo_mysql',
    'user'     => 'root',
    'password' => '',
    'dbname'   => 'doctrine_test',
);

// konfigurujemy Doctrine
$config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode);
$entityManager = EntityManager::create($dbParams, $config);
?>[/code]

Krok 5 - index.php

Tworzymy teraz plik:
[code]C:/www/doctrine/index.php[/code]
i includujemy w nim plik bootrstrapa:
[code]
<?php
// index.php
require_once(__DIR__.'/bootstrap.php');
?>[/code]
Czemu taki układ? Po to, żebyśmy przyzwyczaili się do używania plików bootstrap i to w nich trzymali kod. Plik bootstrap tak w ogóle powinniśmy umieścić w innym miejscu, ale na potrzeby próby z Doctrine nie jest to teraz ważne. Napiszę o tym kiedyś artykuł, na razie niech to po prostu pozostanie w takiej formie.

Krok 6 - generujemy tabelę

Doctrine jeśli może juz zauważyliśmy, do katalogu /vendor/bin pobrał nam kilka użytecznych narzędzi. Będziemy z nimi pracować później, podczas normalnej pracy.
Wykorzystamy je także teraz, ale najpierw musimy utworzyć plik:
[code]C:/www/doctrine/cli-config.php[/code]
w którym wpisujemy dwie poniższe linijki:
[code]
<?php
// cli-config.php
require_once "bootstrap.php";
return \Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($entityManager);[/code]

Tworzymy teraz bazę danych do jakiej parametry podaliśmy w pliku bootstrap.php i NIE TWORZYMY w niej żadnych tabel.
Następnie z poziomu naszego katalogu wpisujemy w terminalu:
[code]$ "vendor/bin/doctrine" orm:schema-tool:create[/code]
Uruchomi nam to narzędzie z pakietu Doctrine służące do operacji na bazie danych, w tym przypadku wygeneruje nam ono tabelę opartą na naszym Entity opisanym w pliku User.php.


Narzędzia tego będziemy zresztą używać później w celu aktualizacji struktury bazy podczas edycji naszego obiektu.

Krok 7 - dodajemy rekord za pomocą ORM

Jeśli wykonaliśmy już wszystkie poprzednie kroki, jesteśmy gotowi do pierwszej operacji za pomocą Doctrine.
Wróćmy do pliku bootrstrap.php i dopiszmy na jego końcu to co poniżej:
[code]
// tworzymy nowy obiekt
$user = new User();
$user->setLogin('janusz');
$user->setPassword('12345');

$entityManager->persist($user); // mówimy Doctrine, że tworzymy nowy obiekt
$entityManager->flush(); // wykonujemy zapis do bazy

echo "Dodano usera o id: " . $user->getId() . "\n";[/code]
Jak widać tworzymy tutaj nowy obiekt, ustawiamy mu dwie właściwości - login oraz password, a następnie zachowujemy go w bazie.
Metoda $entityManager->persist() odpowiada w Doctrine za umieszczenie rekordu w bazie, ale jeszcze nie wykonywania zapytania do niej.
Do bazy nasz obiekt leci dopiero w momencie wykonania metody $entityManager->flush(). Skąd taka konstrukcja? Otóż tutaj mamy jedynie prosty kod odpowiadający za dodanie raptem 1 rekordu, natomiast w całej aplikacji tego kodu może być bardzo dużo. W przypadku takim wykonanie metody flush() dopiero na samym końcu spowoduje, że wszystkie zapytania jakie zebrał w całym kodzie Doctrine zostaną objęte jedną transakcją i dopiero wykonane, co zwiększy wydajność i zmniejszy czas połączenia z serwerem bazy.

Dołączmy zatem powyższy kod do bootstrapa:
[code]
<?php
// bootstrap.php
// Dołączamy autoloader Composera
require_once "/vendor/autoload.php";

// Określamy przestrzenie nazw z jakich skorzystamy
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;

// określamy ścieżkę do plików z Entities
$paths = array(__DIR__.'/src/Model');

// informujemy, że pracujemy w środowisku developerskim, a nie na produkcji
$isDevMode = true;

// konfigurujemy dostęp do bazy
$dbParams = array(
    'driver'   => 'pdo_mysql',
    'user'     => 'root',
    'password' => '',
    'dbname'   => 'doctrine_test',
);

// konfigurujemy Doctrine
$config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode);
$entityManager = EntityManager::create($dbParams, $config);


// tworzymy nowy obiekt
$user = new User();
$user->setLogin('janusz');
$user->setPassword('12345');

$entityManager->persist($user); // mówimy Doctrine, że tworzymy nowy obiekt
$entityManager->flush(); // wykonujemy zapis do bazy

echo "Dodano usera o id: " . $user->getId() . "\n";
?>[/code]
Następnie wejdźmy na:
[code]http://localhost/doctrine/index.php[/code]

Jeśli wszystko się udało to właśnie dodaliśmy swój pierwszy obiekt do bazy za pomocą Doctrine'a:



Jak widać, mimo kilku niezbędnych kroków jakie wykonać trzeba podczas rozpoczęcia pracy z Doctrine całość nie jest bardzo skomplikowana. Schodki zaczną się jednak później, gdyż Doctrine to naprawdę potężna i złożona biblioteka, której opanowanie zajmie trochę czasu. Mam jednak nadzieję, że w kolejnych artykułach omówimy sobie i przeanalizujemy wszystko dokładnie krok po kroku.

Na chwilę obecną znamy podstawy i umiemy już dołączyć Doctrine do projektu, więc myślę, że tyle tytułem wstępu powinno na teraz wystarczyć.

4 komentarze:

  1. Dobry wstęp do Doctrine. Dzięki!

    OdpowiedzUsuń
  2. Świetny artykuł.
    Na stronie twórców sam wstęp jest zagmatwany.
    Pozdrawiam

    OdpowiedzUsuń
  3. Świetny artykuł, przydał się bardzo :)
    _________________________
    https://www.aptusshop.pl/sklepy-internetowe-bialystok.php

    OdpowiedzUsuń
  4. Thanks for sharing such types of articles on your website. I have read the complete article that you have explained properly. Thanks a lot for this article. For any kind of website development in the PHP Language,

    OdpowiedzUsuń

Masz sugestię? Znalazłeś błąd? Napisz komentarz! :)

webmaester.pl - profesjonalne projektowanie WWW i webaplikacji
webmaester.pl - profesjonalne projektowanie WWW i webaplikacji