Lekce 10 - Generátor testovacích dat v PHP - Návrh, entity a jádro
V minulé lekci, Dokončení knihovny Image v PHP, jsme si dokončili knihovnu
Image
.
Vítám vás v tutoriálu o tvorbě generátoru náhodných dat do databáze. Tento doplněk, chcete-li skript, nám vygeneruje různé (náhodné) kombinace dat a ty odešle do databáze. Tutoriál počítá s tím, že rozumíte databázovému ovladači PDO a OOP.
Představme si, že děláme aplikaci, která pracuje s databází – častý model. Aby aplikace fungovala, potřebujete ji otestovat, ale kde data sehnat? Představme si, že tvoříme tabulku uživatelů, potřebujeme na nějakých uživatelích aplikaci otestovat a k tomuto účelu se bude hodit náš skript, který databázovou tabulku (uživatelů) naplní náhodnými daty a to zcela automaticky.
Pro lepší představu si ukažme obrázek, na kterém jsou vidět řádky v tabulce uživatelů. Tyto řádky sama vygenerovala aplikace podle jejího nastavení (na obrázku jich je jen několik, doopravdy jich je v databázi 500):

Tu je možno vidět i počet záznamů v tabulkách:

K vygenerování testovacích uživatelů stačí napsat tento kód:
$generator->generate("users", 500, array( "id" => "Id", "name" => "Name", "surname" => "Surname", "email" => "Email:name,surname", "phone" => "Phone", "birthdate" => "Date:1970-01-01,2013-01-01", "card_expiration" => "Date:2015-01-01, 2030-01-01") );
Jednoduché, že?
Návrh
Aby mělo vše větší smysl, budeme skript tvořit objektově. Před tvorbou budeme přemýšlet nad návrhem. Náš skript bude velmi robustní a bude velmi dobře rozšířitelný. Možná jste si zatím pod pojmy robustní a dobře rozšířitelný nic nepředstavili. Tak se zase podívejme na databázovou tabulku. Co budeme generovat? Jména, příjmení, data, čísla, emailové adresy, ... A aby to bylo ještě zábavnější, budeme umět data i propojovat s dalšími tabulkami. Teď již víme, co znamená to robustní, s tím mírně souvisí i snadno rozšířitelný. Proč by si každý programátor, který script použije, nemohl dopsat své vlastní datové entity? Co když náš script nebude umět generovat PSČ a on ho zrovna bude potřebovat?
V lekci budu používat dva pojmy:
- Entita - jednotka, která nám bude generovat hodnoty (např. jméno, příjmení, tel. číslo, ...)
- Jádro - jádro našeho skriptu, které jednak bude pracovat se samotnou databází, všemi tabulkami a samozřejmě s entitami.
Entita
Entita musí vždy předat nějaké informace jádru. Možná si říkáte, co víc než náhodnou hodnotu budeme potřebovat? Ale uvědomte si, že ID je entita a když v databázové tabulce používáme primární klíče, tak víme, že při vkládání do databáze se tyto klíče neuvádí. Stejně tak, proč by naše jádro mělo složitě zjišťovat, jaký SQL typ má použít, když mu to entity mohou snadno říct.
Pro entitu si napíšeme rozhraní IRandomizableItem
. Rozhraní
bude mít dvě bezparametrické metody:
randomValue()
- Vrací náhodnou hodnotucolumnSqlType()
- Vrací string s SQL typem (např.varchar(55)
).
Tímto entita však nebude končit.
Vezměme si, že i propojení sloupců bude vlastně entita (ostatně proč
ne, také pracuje se sloupci a hodnotami), proto všem Entitám předáme v
metodě load()
několik parametrů, se kterými by mohli pracovat.
Tím prvním bude rozpracovaná položka, která se odešle do databáze. Druhý
parametr budou parametry, které budou předány při definici datového typu (k
tomu se dostaneme později). Třetí parametr bude pole všech tabulek a jejich
hodnot a čtvrtý, poslední, parametr bude definice sloupců všech
tabulek.
Každá Entita musí mít metodu load()
, ale kvůli tomu, že ne
každá entita potřebuje všechny parametry volání, metodu
load()
nemůžeme zařadit do rozhraní. Nakonec bude mít každá
entita veřejné atributy $insertToDefinition
a
$insertToTable
. Atribut $insertToDefinition
říká,
jestli se sloupec vkládá do definice tabulky při vytváření a
$insertToTable
zas zda se má hodnota vkládat do tabulky při
vkládání dat. Kvůli těmto atributům parametry předáváme v
load()
a ne v konstruktoru. Rozhraní bude vypadat
následovně:
interface IRandomizableItem { function randomValue(); function columnSqlType(); }
Nyní si pojďme implementovat první entitu. Začněme třeba jménem.
Vytvořme si třídu Name
, která implementuje rozhraní
IRandomizableItem
.
Doporučuji používat autoloader.
Třídě nastavíte veřejné atributy $insertToDefinition
a
$insertToTable
na true
, jméno se bude vkládat
všude. Naše třída bude mít pole jmen a z toho vybere náhodnou položku.
Přidejme si statické pole $dataJmena
s nějakými hodnotami:
static $dataNames = array("Michal", "David", "Ondřej", "Matěj", "Ludík", "Josef", "Adam", "Tomáš");
Metoda randomValue()
tedy vybere jednu náhodnou hodnotu a
metoda columnSqlType()
vrátí tento string:
varchar(64) COLLATE utf8_czech_ci
Nakonec nezapomeňte implementovat metodu load()
, ta v
případě jména nebude brát žádný parametr a nebude nic dělat. Celá
třída by mohla vypadat následovně:
class Name implements IRandomizableItem { static $dataNames= array("Michal", "David", "Ondřej", "Matěj", "Ludvík", "Josef", "Adam", "Tomáš"); public $insertToDefinition = true; public $insertToTable = true; function load() {} function randomValue() { return self::$dataJmena[rand(0,count(self::$dataNames) - 1)]; } function columnSqlType() { return "varchar(64) COLLATE utf8_czech_ci"; } }
Podobné budou následující entity, od těch se ale teď vzdálíme k jádru.
Jádro
Přidejme si do projektu soubor SampleDataGenerator.php
a
přidejme si do něj stejnojmennou třídu. Do souboru si nakonec přiřaďte i
třídu DatabaseManager
, což bude pro nás takový na míru
přizpůsobený databázový wrapper a třídu DataRow
, což je
jakási jednotka pro vytváření řádek, ostatně to bude právě objekt,
který bude entitám předán v prvním parametru.
Třída
SampleDataGenerator
Náš SampleDataGenerator
bude mít dvě veřejné vlastnosti
$tables
a $columns
. Vlastnost $tables
bude asociativní pole, které bude mapovat název tabulky na pole
DataRow
. Vlastnost $tableColumns
bude také
slovníkové pole v podobném formátu, které bude mapovat název tabulky na
asociativní pole
[sloupec] => datový typ sloupce
. Čili vlastnost
$tableColumns
bude mít ještě vnořená slovníková pole.
Třída SampleDataGenerator
bude mít metody
generate()
a save()
. Metoda save()
nepřijímá žádný parametr a pouze odesílá na databázi dotazy. Metoda
generate()
přijímá tři parametry: název tabulky, počet
řádků a sloupce. Název tabulky a počet řádku je myslím docela jasný,
parametr sloupce bude opět slovníkové pole ve formátu
[název sloupce] => (string)
jeho typ s parametry. Zde si
ještě všimněte typu s parametry, ten se bude zapisovat ve formátu
typ:parametry
. Parametry budou vždy ve stringu a
nebude pro ně platit žádný separátor. Každá entita si však může
"nadefinovat" a implementovat vlastní. No, pokud jste se v této snůšce
odborných pojmů trochu ztratili, nevadí, vlastně jsme si řekli, jak se
skript bude používat. Ukážeme si ještě kód:
$generator = new SampleDataGenerator(); $generator->generate("users", 500, array( "id" => "Id", "name" => "Name", "surname" => "Surname", "email" => "Email:name,surname", "phone" => "Phone", "birthdate" => "Date:1970-01-01,2013-01-01", "card_expiration" => "Date:2015-01-01, 2030-01-01") ); $generator->save();
Na prvním řádku si vytvoříme generátor, snadné. Poté mu řekneme, ať
vytvoří tabulku users
a tu naplní 500 uživateli, přičemž má
naplnit sloupce id
, name
, surname
,
email
, phone
a data v minulosti a budoucnosti.
Všimněte si emailu, tam v parametrech předávám názvy sloupců, ze kterých
poté bude email čerpat. Dále jsou zajímavá data, těm specifikujeme rozsah
od kdy do kdy. Výsledné náhodné datum bude v tomto rozmezí. Nakonec to
pošleme do databáze. Nejdříve ale vytvoříme třídu reprezentující
řádek - DataRow
.
Třída DataRow
Třída DataRow
bude potřebovat sloupce tabulky, kam patří a
všechny tabulky. Tyto hodnoty mu předáme v konstruktoru. Nakonec budeme
potřebovat atribut $values
což bude slovníkové pole ve formátu
[název sloupce] => [hodnota sloupce pro položku]
:
public $tables; public $columns; public $values = array(); function __construct($columns, $tables) { $this->columns = $columns; $this->tables = $tables; }
Třída DataRow
bude mít také metodu generate()
,
která nebude přijímat žádný parametr a vygeneruje hodnoty řádku. Metoda
projde cyklem foreach
sloupce ($this->columns
), z
nich v tomto cyklu dostane jméno a jeho typ:
foreach ($this->columns as $columnName => $columnType)
Typ sloupce, jak již jsme si řekli, obsahuje název a parametry. Název je
název naší entity a parametry jsou volitelné parametry. Tyto dvě části
jsou oddělené dvojtečkou. Hodnoty dosadíme do proměnných
$generatorName
a $paramsForGenerator
, přičemž
proměnná $paramsForGenerator
bude mít výchozí hodnotu
prázdný string:
$generatorParts = explode(":", $columnType); $generatorName = $generatorParts[0]; $paramsForGenerator = ""; if (isset($generatorParts[1])) { $paramsForGenerator = $generatorParts[1]; }
Asi vás napadne, jak ale vytvořit instanci entity ze stringu? PHP má na
toto krásnou syntaxi, jednoduše mu řekneme
new $nazevPromenne(parametry)
a on vytvoří novou instanci
třídy, jejíž název obsahuje proměnná $nazevPromenne
,
přičemž můžeme samozřejmě doplnit parametry. Jak již víme, parametry se
entitám nepředávají v konstruktoru ale v metodě load()
:
$generator = new $generatorName(); $generator->load($this, $columnName, $paramsForGenerator, $this->tables, $this->columns);
Měli bychom ověřit, jestli uživatel neposílá skriptu injekci a nechce
vytvořit instanci něčeho, co není entita. Ověříme to jednoduše, entity
musí implementovat rozhraní IRandomizableItem
, to ověříme
operátorem instanceof
, která vrátí pravdivostní hodnotu, zdali
levý operand implementuje pravý operand. Pokud tato podmínka platit nebude
(instance není entita) ukončíme práci skriptu a vypíšeme chybovou
hlášku, k tomu se dobře hodí funkce die()
:
if(!$generator instanceof IRandomizableItem) { die("Chyba: Pokus o PHP injekci"); }
Nakonec, pokud sloupec chce, aby byla jeho hodnota vkládaná do databáze (třeba entita ID to nechce) tak ji tam vložíme:
if ($generator->insertToTable) { $this->values[$columnName] = $generator->Randomvalue(); }
Nyní máme řádek kompletní. Vraťme se k samotnému generátoru.
Implementujeme vytváření řádků kódu. V třídě
SampleDataGenerator
vytvořte metodu generate()
s
parametry table
, count
a columns
. V prvé
řadě si uložíme sloupce generované tabulky:
$this->tableColumns[$table] = $columns;
Nyní cyklem začneme vytvářet jednotlivé řádky. V každé iteraci
vytvoříme nový řádek, předáme mu potřebné parametry a zavoláme na něj
metodu generate()
. Nakonec řádek přidáme do tabulky:
for ($i = 0; $i < $count; $i++) { $random = new DataRow($columns, $this->tables); $random->Generate(); $this->tables[$table][] = $random; }
Tak už umíme i vygenerovat data. To je pro tento díl vše, ještě je
před námi kus práce. Zdrojové kódy budou ke stažení, až si vše
doděláme
Příště, v lekci Generátor testovacích dat v PHP - Tvorba wrapperu, se podíváme na specializovaný databázový
wrapper a poprvé si vygenerujeme tabulku plnou jmen