Implementace entit v PHP

Malé zamyšlení (anketa) týkající se nejlepší implementace entit v PHP.

Entita je stručně řečeno kontejner na data (více třeba u PHP Gurua). Typická třída entity obsahuje spoustu atributů a settery/gettery pro práci s nimi.

Entitu můžeme implementovat třemi základními způsoby (třeba přijdete na další), ale ani jedna z nich mi nepřijde úplně vyhovující:

  1. Všechny atributy uděláme private a pro každý z nich napíšeme ručně getter a setter.

    class Product {
    
        private $name;
    
        public function getName() {
            return $this->name;
        }
    
        public function setName($name) {
            // kontrola typu
            $this->name = $name;
        }
    }
    Toto řešení je sice čisté, ale velmi pracné.
  2. Společný předek s magickou __call metodou a protected atributy v entitách.

    abstract class Entity {
    
        public function __call($methodName, $params) {
            $action = substr($methodName, 0, 3);
            switch($action) {
                case 'get':
                    // vrácení hodnoty atributu
                case 'set':
                    // kontrola typu podle anotace atributu, uložení hodnoty
            }
        }
    
    }
    
    class Product extends Entity {
    
        /** @var string */
        protected $name;
    
    }
    Toto řešení je méně pracné, ale používáme protected atributy, což není příliš doporučované. Kdokoliv si totiž pak může naši entitu podědit a hrabat se v atributech přímo, bez volání setterů. Tuto nepříjemnost sice můžeme zakázat deklarováním třídy jako final, ale omezujeme tím použitelnost třídy.

  3. Private atributy v entitách a jejich zviditelnění ve společném předkovi pomocí reflexe.

    abstract class Entiry {
    
        public function __call($method, $args) {
            $action = substr($methodName, 0, 3);
            // vytvoření instance ReflectionProperty pro daný atribut
            $property->setAccessible(TRUE); // změna viditelnosti atributu
            switch($action) {
                case 'get':
                    return $property->getValue($this);
                case 'set':
                    // kontrola typu podle anotace atributu
                    $property->setValue($this, $value);
            }    
        }
    }
    
    class Product extends Entity {
    
        private $name;
    
    }
    Toto řešení je stejně pracné jako to předchozí a navíc nemá neduh v podobě protected atributů. Moc se mi ale nelíbí, že entitu znásilňujeme pomocí reflexe, abychom mohli číst a zapisovat do private atributů.

Pokud vás napadá lepší řešení, podělte se v komentářích.

Hodnocení

Komentáře

2012-01-01 19:59:33

Používám první variantu, ale nepovažuji ji za úplně šťastnout.

Podle mě tohle hodně závisí na tom, jakou architekturu bude mít aplikace. Když se budeš snažit přiblížit tomuhle

http://www.augi.cz/programovani/architektura-skalovatelnych-aplikaci/

tak ti nebude vznikat Anémický model, jak ho popisuje Fowler

http://martinfowler.com/bliki/AnemicDomainModel.html

protože ty gettery a settery nebudeš potřebovat.

Až přijdu na to, jak to zadrátovat do Nette a Doctrine tak dám vědět. A doufám v to samé od ostatních :)

[2] Tharos
2012-01-01 21:23:09

V aktuálním projektu používám shodou náhod poslední variantu, a to právě kvůli Tebou popsaným výhodám. Není to ukecané, je to robustní a ve spojení s anotacemi to i leccos svede. Zneužívání reflexe mi ani tak moc nevadí, ale co mi vadí je plýtvání pamětí, ke kterému nejjednodušší implementace této myšlenky vede. Ono totiž když pak pracuješ s 20ti entitami a u každé potřebuješ nastavit například jednu položku, vznikne ti v paměti 20 instancí stejné property reflexe...

Tohle mi vadilo, a tak jsem to vyřešil takovou nadstavbou, která tyto property reflexe umí sdílet napříč instancemi stejných tříd. Bohužel je kód, který to řeší, kvůli absenci late static bindingu pro proměnné (http://blogs.neoseeker.com/tekmosis/5576-late-static-binding-of-variables-constants-in-php-5-3/) možná trochu haluz, ale na druhou stranu svůj účel plní dobře.

Mně osobně nenapadlo v PHP nic lepšího. Má kritéria jsou stručnost, robustnost, paměťová efektivnost a také aby to umožňovalo něco složitějšího... Budu nadšen, pokud sem někdo dorazí z nějakou lepší implementací. :) Takže v každém případě díky za článek!

Kdyby Tě zajímala má implementace, přístup k tomu mému zmíněnému aktuálnímu projektu máš. :)

Na tento komentář odpověděl [4] Dundee
[3] Tharos
2012-01-01 21:26:53

"...když pak pracuješ s 20ti entitami a u každé..."

Tady básníkovi vypadla dvě slova, opravuji na:

"...když pak pracuješ s 20ti entitami shodného typu a u každé..."

[4] Dundee
2012-01-01 21:37:58

#2 Tharos: Díky. My na aktuálním projektu také používáme poslední možnost a zatím myslím k plné spokojenosti. Nelíbí se mi to spíš jen z "filozofického" hlediska :) tak mě napadlo tohle sepsat, jestli někdo nepřijde s lepším řešením.

2012-01-01 23:30:45

Jak u variant 2 a 3 potom ale řešíte napovídání metod v IDE? Pomocí anotací '@method'?

Na tento komentář odpověděl [6] Dundee
[6] Dundee
2012-01-02 01:04:34

#5 Jakub Onderka: Přesně tak. Bohužel ale ne všechny IDE to podporují.

2012-01-02 19:02:44

A není nejjednodušší spojit výhody?

Pročpak si nenecháte Vámi zvolený oblíbený model vygenerovat svým IDE nástrojem nebo jiným?

IMHO první model je nejčistší, druhý a třetí je jen kouzlení jak se co nejméně upsat. Proč si tedy nenecháte první model a nenecháte tvorbu getterů a setterů na IDE nástroji?

Máte všechna plus. Upíšete se dokonce mnohem méně, je to čisté. Kód běží dokonce rychleji a úsporněji, protože vyhodnocování obecné funkce __call, případně __property stojí čas, peníze a paměť. A ještě dražší je samozřejmě reflexe, i když ta v dynamických jazycích není tak drahá.

Je vysloveně nutné, abyste každý znak ve zdrojáku museli napsat svou rukou? Už od dob kdy jsem před léry zkusil vim jsem napsal tak 20 % znaků ve zdrojáku, zbytek jsem mu jen naznačoval a on doplňoval.

Neřku-li dnes, kdy jsou v dispozici daleko luxusnější prostředí.

---

Kromě toho, jen do počtu existují další možnosti. Například nechat vygenerovat třídy ze šablon – nikde není psáno, že zdrojový kód nemůže být produktem nějakého generátoru.

---

Další možnost je doplnit gettery a settery přímo v PHP pomocí eval.

---

Také je možné seznam property předat jako pole s referencemi na skutečné proměnné do univerzální třídy, která vytvoří přístupové metody.

---

Možností je mnoho. A myslím, že v poslední verzi PHP se ještě další objevily.

Miloslav Ponkrác

[8] koubel
2012-01-02 20:10:03

Tady souhlasím s p. Ponkrácem, 1.) a generovat kód klidně z nějakých šablon, prototypů, konfigurace atd.

[9] Tharos
2012-01-02 23:09:06

No, já s argumentací p. Ponkráce z větší části souhlasit nemohu...

1) „Upíšete se dokonce mnohem méně, je to čisté.“

Když založím generování na nějakých šablonách, prototypech či kdo ví jakých ještě definicích, vždy musím napsat minimálně ty definice. Ty už z principu nelze více zkrátit, obsahují prostě jenom jméno položky, datový typ, zda může být null, množinu platných hodnot, business logiku… Přičemž ve 2. a 3. přístupu se toto úplně běžně řeší pomocí jednoduchých anotací. Musím tedy napsat XY znaků kódu a o vše kolem se už postará dynamické PHP. U generátoru musím napsat úplně stejný počet XY znaků (v podstatě to samé napíšu do nějaké šablony) a pak navíc ještě spustit generátor. Rozhodně tedy neplatí, že „upíšete se dokonce mnohem méně“.

2) Chtělo by zde nějak dospecifikovat, jak zde hodnotíme, co je „čisté“ :). Pro mě osobně je čistý kód robustní, dobře testovatelný, dobře čitelný a stručný (tj. jde přímo k věci). Tomu vyhovuje 2. a 3. varianta velmi dobře, používáme-li dynamický jazyk typu PHP. Máte-li jinou definici přívlastku „čistý“, samozřejmě pak z Vašeho úhlu pohledu můžete mít pravdu.

3) Já osobně nebazíruji na tom, abych každý znak ve zdrojáku psal svou rukou, ale nedokážu si představit situaci, kdy bych psal pouze 20 % kódu… Přišla by mi otrava mít všechny hlavní myšlenky a logiku aplikace ve 20 % kódu a pak pořád dokola muset generovat nějakých 80 % „balastu“ kolem. Tohle ale bude možná zásadní rozdíl mezi námi dvěma.

4) Ono to generování z IDE není tak slavné, jak se možná na první pohled zná. Neznám všechna dostupná IDE, ale minimálně NetBeans, PhpStorm a PhpED nevyhoví, pokud chceme automaticky k setterům generovat i typová omezení či automatické konverze, množinu platných hodnot… zkrátka o to, o čem jsem již psal.

5) „Také je možné seznam property předat jako pole s referencemi na skutečné proměnné do univerzální třídy, která vytvoří přístupové metody.“

Tahle implementace by mě tedy vážně zajímala… Nevedla by náhodou k tomu, že by se ve výsledku všude používala jedna univerzální třída „Entity“? To pak ale trochu popírá smysl toho všeho, nemyslíte? Pak se z toho stane obyčejná univerzální přepravka, kterých už po světě chodí mnoho (NotORM_Row, DibiRow…), jen třeba s nějakou validací.

Na tento komentář odpověděl [10] koubel
2012-01-02 23:41:31

#9 Tharos: - Používat reflexy v business objektech mě nepřijde jako správné, reflexe je podle mě určena na věci jako analyzátory a manipulátory s kódem, generátory, DSL atd. Ale do business logiky kvůli ušetření zápisu - jiný význam nemá - nepatří a setAccesible už vůbec ne. S výkonem to bude také dost hrůza, takovéhle __call navíc s reflexion bude oproti getteru několikrát pomalejší. Ostatně výsledky ankety také něco naznačují.

Na tento komentář odpověděl [11] Tharos
[11] Tharos
2012-01-03 00:07:11

#10 koubel: To je právě věc názoru... Ale naštěstí vlastně vůbec nevadí, že se neshodneme. ;)

Vezmi si ale třeba takovou Doctrine 2, což je knihovna, která je z velké části určena pro implementaci doménového modelu. Je zhusta založená na reflexích (přestože umožňuje i alternativní přístupy).

Rozveď prosím, proč Ti použití reflexe v business objektech nepřijde správné. Mně osobně reflexe přijde jako zajímavý nástroj, který má prostě své výhody a nevýhody a nevidím důvod jej někde z principu nepoužívat. Jaký účel má podle Tebe taková metoda setAccesible? Jak bys ji použil v analyzátoru či manipulátoru s kódem?

2012-01-03 01:17:25

Třetí řešení mi přijde absurdní. My sice nejprve stanovíme viditelnost na private, ale pak to sami vědomě porušíme. Můžeme někomu dalšímu zakázat udělat přesně totéž? Ne. Jaký je tedy smysl hrát si na nějakou viditelnost a neudělat všechno public?

Na tento komentář odpověděl [14] Tharos
Na tento komentář odpověděl [15] Tharos
2012-01-03 01:18:50

Ad Tharos:

Já bych předeslal, že tyhle přístupy se vymstí v okamžiku, jakmile začne PHP být trochu zatěžováno, nebo na stroji s mnoha weby.

Reflexe je drahá – ze všech hledisek. A v principu reflexe není nic jiného, než takové „pologenerování“ kódu, tedy zasahování do kódu nikoli tak jak je, ale už na „metaúrovni“, tedy napůl cesty ohledně generování.

Reflexe je drahá z hlediska prostředků – paměti, rychlosti.

Stejně tak jakákoli sofistikovanější dynamika je drahá, ale pořád podstatně levnější.

Jsem ze staré školy a kdysi se programátoři na kód nedívali pouze z hlediska ideologie, protože každé řešení má své pro a proti. To, že tady něco řešíme akademicky a virtuálně bez návaznosti na jakoukoli konkrétní situaci – tím si hrajeme na politiku, ideologie, komunismus, kapitalismus, prostě na nějaký ismus. Povídáme si tu co se komu POLITICKY líbí. Nic jiného.

Které řešení je dobré se zjistí až v rámci KONKRÉTNÍHO nasazení, tehdy to přestane být ideologie, tehdy přestaneme diskutovat jako v politbyru, tehdy přestane být debata subjektivní podle „někdo rád holky a někdo zase vdolky“ a tehdy teprve začne být, přesněji může být debata objektivní.

Kdybych dostal cizí kód, který bych měl opravovat, byl by nejšťastnější za první variantu.

Varianty typu reflexe, kde se zpřístupňují property, které teoreticky nemají být přístupné podle práv – to je o nabití si úst.

Celé sw inženýrství, celá analýza je jen o jednom: Člověk je prostě omezený tvor, který nedokáže udržet v mozku všechny detaily a tak se mu musí jednoduchost snížit. Proto se části celků zapouzdřují do modulů, objektů, logických celků a různých krabiček s API a interfacy – jen pro nedokonalost člověka, který to jinak nezvládne udržovat a administrovat. Jiný důvod v tom není. Pro neschopnost člověka udržet naráz v mozku příliš mnoho vazeb se dělají zapouzdřené celky, které mají minimum vazeb s okolím.

Jakmile do toho začnete vkládat černou magii typu další nenápadné a nečekané vazby, které logicky běžně programátor neočekává. Jakože si zpřístupníte private členy někam, kde nemají být vůbec přístupné, ale reflexí znásilníte leccos, apod. je to ekvivalentní vytváření dalších vazeb a závisslostí. Lžete sami sobě a maskujete interface třídy jak americká CIA. Třída jinému programátorovi lže o své struktuře a právech a o SVÝCH VAZBÁCH NA OKOLÍ. Lže mu, že nějaké property je private a tedy logicky programátor předpokládá, že mimo třídu a metody třídy do ní nikdo nepoloze.

To co dělá reflexe v tomto případě je něco co se nazývá POLITICKÁ KOREKTNOST. Tedy zastírání pravého významu.

Natahejte si podobné černé magie do projektu řekněme o 10 miliónu řádkách zdorjového kódu na každém rohu tucet a dojdete k tomu, že to zahodíte jako celek, protože to bude taková prasečina, že to nikdo nebude moct udržovat.

Lžete si v rozhraních tříd – a časem si z toho hodíte provaz.

Samozřejmě v jednoduchoučkém případě to vypadá pěkně – jako ochcávka i neškodně. Ale problém bude až takhle budete mít na malém projektu třeba několik set podobných ochcávek. Pak vedlejší efekty těchto ochcávek vám přerostou přes hlavu. Protože zase – mozek má omezenou kapacitu na počet vazeb. Jednu ochcávku nečistého použití reflexe zvládne. Ale ne třeba padesát. A pokud se naučíte tento styl, tak velmi brzo nezůstane u jedné ochcávky ála reflexe ale bude to váš styl.

A pak přijde poučení a nabití si úst. Případně měsíce až roky pekla udržování kódu vycpaného ochcávkami kdy si dokonale vyjasníte, že pokud se vám podaří tohodle projektu zbavit, že už to nikdy neuděláte.

Na tento komentář odpověděl [14] Tharos
[14] Tharos
2012-01-03 02:02:06

#12 Jakub Vrána: Smysl neudělat všechno public je (pochopitelně) v tom, že u všech zde zmíněných přístupů lze nějakým způsobem validovat či sanitizovat vkládanou hodnotu (typovost, nullable, množina přípustných hodnot…).

Skrze reflexi lze nastavit private proměnnou na libovolnou hodnotu v PHP úplně všude. Z této perspektivy lze význam private úplně zpochybnit... Nikde v PHP se nemohu spolehnout na to, že někde budu mít nějakou určitou hodnotu...

Výhodu tohoto přístupu vidím prostě v tom, že přináší určité pohodlí, kdy si například další zapojení vývojáři nemusí při psaní entit umlátit prsty o klávesnici, nemusí si ani stahovat nějaké konkrétní IDE, které by umělo vygenerovat to, co potřebují, a nemusí si ani stahovat žádné jiné generátory. Prostě jenom nadefinují položky, pár jednoduchých anotací a mohou si být jisti, že při standardním používání té entity (tj. v souladu s nějakou dokumentací) budou mít všude validní data a i uložené typově správně.

#13 Miloslav Ponkrác: S tím souhlasím, je jasné, že bez konkrétního zadání se zde shody nedojdeme. Já jsem ale tak obšírněji reagoval právě proto, protože mi přišlo, že jste ve svém prvním příspěvku prezentoval pár myšlenek skoro jako hotová fakta, ale ono to tak mnohdy vůbec být nemusí (třeba to, že to vede k mnohem méně kódu). Jinak tu máte všichni pravdu, že reflexe jsou suverénně nejpomalejším řešením. Pak už jde jen o konkrétní aplikaci, totiž jak to vadí či nevadí…

Na rovinu ale říkám, že nemám nejmenší zkušenosti s projektem o rozsahu 10.000.000 řádků a chci se z vlastní vůle soustředit na projekty úplně jiného rozsahu.

[15] Tharos
2012-01-03 02:37:09

#12 Jakub Vrána: Jakube, ještě bych dodal dva odstavce, abychom se skutečně vzájemně pochopili.

Já osobně vůbec netvrdím, že private v kombinaci s přístupovými metody má nějaký neprůstřelný ochranný význam (viz reflexe). Já v tom vidím spíše praktický význam, kdy při použití přístupové metody se mi nepodaří omylem vložit nějakou nevalidní hodnotu, a v neposlední řadě v tom vidím i nástroj k zapouzdření, který bude fungovat tehdy, když bude programátor chtít (a nebude věci obcházet). Také se mi líbí možnost automatické typové sanitizace (pokud vložíme řetězec do položky, která má obsahovat celé číslo, a ten řetězec lze jako celé číslo interpretovat, převede se). To je ohromně praktické (pak lze skoro všude používat ===), protože masivní používání operátoru == vede pochopitelně k nemalému množství těžko odhalitelných chyb…

Kdybych ve svých kódech převedl všechny entity na přepravky s public proměnnými, musel bych zároveň buďto přepsat úplnou plejádu výskytu operátorů === na == (což bych vážně nerad), anebo dopsat konverze a různé kontroly na neúměrně velké množství míst (a v podstatě na nesprávná „místa“)… A to by nebyla zdaleka jediná komplikace. Tolik k významu private a přístupových metod za mě.

Vážně by mě zajímala Tvá oponentura, pokud bys měl prostor. Stojím o ni. :)

2012-01-04 05:05:07

Používám variantu 1. i když mě taky nepřipadá zcela ideální. Ale pořád se mě jeví lépe než ostatní navrhované.

Co se týče spousty psaní tak nadefinuju atributy a nechám si pomocí IDE dogenerovat settery/gettery (pořád to negeneruje takový kód abych na něj nemusel šahat - není nic jednoduššího než o to požádat :-) ). Pak upravím co potřebuju.

Co se týče validace a sanitizace tak v samotných entitách sanitizuji pouze empty string na NULL. O validaci se mě stará až event (listener), který se spustí před pokusem o uložení (jeden čas jsem to měl taky přímo v entitách). Pořád se tak nějak nemůžu rozhodnout jesli je to dobře nebo ne. Metainformace o validačních pravidlech validátor získá z anotací.

Jeden z důvodů proč používám variantu 1. jsou "read-only" property (jsem línej kvůli tomu dělat další anotaci).

Co se varianty 2. a 3. týká jsou lidé, kteří nadávají na vlastnost NetteObject $foo->getBar() == $foo->bar. Že to je fuj, špatné, náchylné k chybám etc. Pro ně musí být představa něčeho takového hotové peklo! :-)

2012-01-08 23:54:37

ad 1: nejpřímočařejší, nejčistší a nejukecanější způsob. Strojově generovatelný. Mám za to, že jakmile lze něco strojově generovat, tak to poukazuje na nedostatek (třeba jazyka) a snázeji to povede k chybám při budoucích úpravách. Když už používáme dynamický jazyk, netřeba se za to stydět a klidně bych __call využil ;)

ad 2: argument proti protected, že "kdokoliv si totiž pak může naši entitu podědit a hrabat se v atributech přímo" krásně zabíjíš příkladem č. 3, kde se hrabeš v cizích atributech i bez podědění. A dodatek "…navíc nemá neduh v podobě protected atributů." je pak argumentační smyčka.

Tady je třeba odlišit mezi "zabezpečením" a "viditelností". OOP není bezpečnostní firewall a programátor není cracker, který se snaží narušit aplikaci. Proto spousta pravidel je zapsaných jen neformálně, jako kontrakty. Například typ návratové hodnoty metody nelze nařídit, ale předpokládá se, že ho potomek u přepsané metody dodrží. Totéž pak platí pro protected atributy.

ad 3: u příkladu s private property chybí ten nejdůležitější řádek (vytvoření instance ReflectionProperty). Reflexi totiž nelze vytvořit. Nevíš, v jaké třídě ta property existuje. Ona může klidně existovat v každé úrovni hierarchie (tj. stejný název, ale úplně jiná proměnná). Řešení by bylo jednoznačné pouze v případě, že by entita byla final. Pokud ovšem bude entita final, je čistější řešení č. 2.

2012-06-14 14:52:25

Docela čistý způsob mi přijde 2 upravený aby používal traits. Zápis bude kratší, vlastnosti mohou být private a třída nebude mít typ Entity.

trait Entity {

	public function __call($methodName, $params) {
		$action = substr($methodName, 0, 3);
		$name = strtolower(substr($methodName, 3, -1));
		switch($action) {
			case 'get':
				return $this->$name;
				break;
			case 'set':
				// kontrola typu přes anotaci
				$this->$name = $params#0#;
		}
	}
}
class Product {

	use Entity;
	/** @var string */
	private $user;

}
Pokud bychom se chtěli vyhnout reflexi úplně, mohli bychom typ proměnné uložit jako řetězec a v konstruktoru udělat analýzu, uložit do interní tabulky a nastavit výchozí hodnoty. To už je ale, uznávám, trochu nestandardní.
trait Entity {

	private $_ = [];

	public function __call($methodName, $params) {
		$action = substr($methodName, 0, 3);
		$name = strtolower(substr($methodName, 3));
		switch($action) {
			case 'get':
				return $this->$name;
				break;
			case 'set':
				if (gettype($params#0#) === $this->_[$name]) {
					$this->$name = $params#0#;
				} else {
					throw new Exception('Invalid type.');
				}
		}
	}
	public function __construct()
	{
		$this->_ = get_object_vars($this);
		unset($this->_["_"]);
		// mozna kontrola, zda typy existuji
		foreach ($this->_ as $name => $value) {
			$this->$name = NULL; // nebo vychozi hodnota
		}
	}
}
class Product {

	use Entity;
	/** @var string */
	private $user = 'string';

}

2012-06-14 15:02:28

Oprava prvního bloku kódu na řádku 5:
$name = strtolower(substr($methodName, 3));

Komentáře již nelze přidávat