Jak upravit funkčnost getteru a setteru objektu?

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
svezij
Člen | 69
+
0
-

Ahojte, mám dotaz ohledně getteru a setteru. Potřeboval bych „rozšířit“ jejich funkčnost. Chci si vytvořit abstraktní třídu, která bude reprezentovat jeden řádek tabulky databáze. Deklarace by měla vypadat nějak takto:

abstract class DbTableRowObject extends Nette\Object
{
    /** @var String - název tabulky */
    protected $tableName;
    /** @var ArrayObject - seznam sloupců tabulky s jejich hodnotami */
    protected $tableColumns;
    /** @var ArrayObject - seznam sloupců tvořících primární klíč */
    protected $primaryKeys;

    /** Konstruktor
     * 1) projde sloupce tabulky z databáze a s jejich hodnotami je uloží do pole tableColumns
     * 2) rozpozná primární klíče a ty uloží do pole primaryKeys
     */
    function __construct($tableName);

    /** Smaže "sám sebe" - řádek z databáze
     * 1) projde proměnnou s primárními klíči, vezme si jejich hodnoty z proměnné tableColumns a vytvoří klauzuli where pro SQL dotaz
     * 2) smaže řádek odpovídající "sobě" z databáze
     */
    public function delete();

    /** Zkontroluje validitu povinných položek
     * možná nebude abstraktní, ale ve výchozím stavu vrátí rovnou true - to není podstatné
     */
    abstract public function checkMandatories();

    /** Uloží "sám sebe" do databáze - vytvoří nový řádek nebo updatuje existující řádek
     * 1) zavolá metodu checkMandatories a zkontroluje povinné položky
     * 2) projde proměnnou tableColumns a zajistí data pro update / insert
     * 3if) pokud jsou definované hodnoty všech sloupců tvořících primární klíč, projde proměnnou s primárními klíči, vezme si jejich hodnoty z proměnné tableColumns a vytvoří klauzuli where pro SQL dotaz update a updatuje řádek
     * 3else) vloží nový řádek do db
     */
    public function save();
}

A tady se to už asi nabízí samo, potřeboval bych považovat sloupce databáze za properties, abych k nim mohl přistupovat přímo přes getter objektu. Chtěl bych tedy něco takového:

$child = new ChildOfDbTableRowObject('tablename');
echo 'Toto je puvodni hodnota sloupce 1: ' . $child->column1;

>> Toto je puvodni hodnota sloupce 1: hodnota z databaze

$child->column1 = 'nova hodnota';
echo 'Toto je nova hodnota sloupce 1: ' . $child->column1;

>> Toto je puvodni hodnota sloupce 1: nova hodnota

A abych dosáhl tohoto chování, měl by základní getter a setter vypadat následovně (samozřejmě bez nějakých větších bezpečnostních kontrol atd.):

abstract class DbTableRowObject extends Nette\Object
{
    // ... viz kód výše ... //

    public function __get($prop)
    {
        if (array_key_exists($prop, $this->tableColumns)) {
            return $this->tableColumns[$prop];
        } else if (property_exists($this, $prop)) {
            return $this->$prop;
        } else {
            throw new SomeException('Requested property "' . $prop . '" does not exist.');
        }
    }

    public function __set($prop, $value)
    {
        if (array_key_exists($prop, $this->tableColumns)) {
            $this->tableColumns[$prop] = $value;
        } else if (property_exists($this, $prop)) {
            $this->$prop = $value;
        } else {
            throw new Exception('Requested property "' . $prop . '" does not exist.');
        }
    }
}

Rozdíl je tedy v tom, že ke sloupcům tabulky můžu přistupovat, jako by to byly atributy třídy.

Snad tomu jde nějak rozumět. Je možné getter a setter nějak rozšířit, aby si zachoval funkčnost getteru a setteru třídy Nette\Object a zároveň do nich přidat tuhle funkčnost?

Mockrát děkuji.

David Matějka
Moderator | 6445
+
0
-

rozhodne bych tam nedaval moznost ziskat/zmenit protected/private property, to by mely zajistit get*, set* metody

a jak rozsirit: treba takhle

abstract class DbTableRowObject extends Nette\Object
{

    public function &__get($prop)
    {
        if (array_key_exists($prop, $this->tableColumns)) {
            $val = $this->tableColumns[$prop];

        } else {
            $val = parent::__get($prop);
        }
        return $val;
    }

    public function __set($prop, $value)
    {
        if (array_key_exists($prop, $this->tableColumns)) {
            $this->tableColumns[$prop] = $value;
        } else {
            parent::__set($prop, $value);
        }
    }
}
svezij
Člen | 69
+
0
-

Jéjej, já jsem ale tút tút, nj, to je jasný, mockrát děkuji. Že mě to nenapadlo v první vteřině ;-). Díky moc.

A k těm protected/private properties… ano, přístup k „chráněným“ atributům by měly zajistit get a set metody (při striktním dodržení principů OOP ani není moc případů, kdy by se měly používat atributy s viditelností public), ale právě ty jdou v PHP implementovat pomocí tzv. „Magic Methods“ a to konkrétně __get($name) a __set($name, $value). Dokonce se to píše přímo v dokumentaci PHP:

__get() is utilized for reading data from **inaccessible** properties.

Takže budu-li mít ve třídě 50 private atributů, po kterých v get a set metodě nechci nic jiného, než vrátit atribut a nastavit atribut (případně zkontrolovat existenci atributu – zkrátka pro všechny atributy stejné přístupové metody), tak nemusím vytvářet 100 metod (get a set pro každý), ale použiji právě „kouzelné metody“ __get a __set, které nadefinuji tak, jak potřebuji. Chci-li to ještě obecněji, mohu je napsat následovně:

public function __get($prop)
{
  $method = 'get' . ucfirst($prop);
  if (method_exists($this, $method)) {
    return $this->$method();
  } else {
    if (array_key_exists($prop, $this->tableColumns)) {
      return $this->tableColumns[$prop];
    } else if (property_exists($this, $prop)) {
      return $this->$prop;
    } else {
      throw new Exception('Requested property "' . $prop . '" does not exist.');
    }
  }
}

Dále mohu napsat např.:

public function getColumn2()
{
  return 'toto je funkce pro sloupec 2... hodnota = ' . $this->tableColumns['column2'];
}

Pak bude fungovat vše tak, jak má a nemusím definovat 100 metod, ale jen ty, které potřebuji úplně jinak. Přitom ale dodržím princip OOP, a přestože navenek volám $myobject->column1, resp. $myobject->column2, tak uvnitř se budou volat public metody __get(), resp. getColumn2(). A to je, nemýlím-li se, zase vlastnost properties – to že navenek volám atribut, ale uvnitř se volá metoda (ve škole jsme je brali v C# a ten já moc neovládám ;-) ).

Snad jsem to nepopletl a nepíši sem nějaké „nesmysly“. Každopádně za tu radu díky, rozhodně to udělám přesně tak, jak radíš.

Přeji vám pěkný prodloužený víkend :-).

David Matějka
Moderator | 6445
+
0
-

jasne, magicky metody tu jsou, ale pouzivat je timhle zpusobem je podle me spatne. ono kazde pouziti magickych metod uz trochu zavani :)

a porusujes tim zapouzdreni objektu, nemela by zde byt takovahle moznost pristoupit k jakekoliv property z venku. objekt by mel poskytovat rozhrani, pres ktere se necha pristoupit k tomu, co urcis. a kdyz te nebavi psat get, set metody, tak snad kazde IDE ma jednoduche generatory tehle metod :)

to uz bys rovnou mohl dat vsechny property public a nehnat to pres magicky metody

Šaman
Člen | 2666
+
0
-

Osobně bych se takovému návrhu vyhnul, budeš s tím mít problémy. Ale zkušenost je nesdělitelná, asi si tím musí projít každý sám.. Taky jsem býval magič, dnes už si na magii dávám pozor. Je to dobrý sluha, ale špatný pán :)

Na třídu pro databázovou entitu doporučuji použít nějaký ORM (nad NDb je YetORM, nad Dibi LeanMapper), už i samotný návrh ActiveRow, který chceš použít, má svá úskalí.

A obecná OOP omáčka, která ti zatím asi přijde zbytečná, říká, že ke třídě by se mělo přistupovat jen pomocí veřejného rozhraní, tedy public metody a atributy. Pokud budeš přistupovat magicky k něčemu, co není veřejně deklarováno, jak pak uživatel této třídy má vědět, co může a co nemůže? Jaký typ akceptuje každý setter? Co napovídání?
Jedna z možností jak toto částečně odmagičtit je používat anotace a magické metody použít jen existuje-li deklarace alespoň v PhpDoc anotaci třídy.

Tedy například takto. Všechny v anotaci popsané metody a atributy řeší ORM (v tomto případě LeanMapper), nebo jiná magie, ale o všech vím. IDE mi všechno napoví. A ty magické funkce mi zkontrolují, jestli settím správný datový typ (nespadne mi to až u ukládání do db).

<?php
/**
 * Entita reprezentující osobu
 *
 * @property User      $user     m:hasOne
 * @property Group     $group    m:hasOne
 * @property Present[] $presents m:belongsToMany
 *
 * @property int         $id
 * @property string      $nick
 * @property string|NULL $name
 * @property string|NULL $birthdayMD (birthday_md)
 * @property string|NULL $birthdayY (birthday_y)
 * @property string|NULL $namedayMD (nameday_md)
 */
class Person extends Entity
{

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

}
?>

A takhle vypadá magická metoda:

<?php
/**
 * Repozitář pro práci s uživateli
 *
 * @method User getByEmail($email)
 */
class UserRepository extends Repository
{
	...
}
?>

Editoval Šaman (4. 7. 2013 15:45)

svezij
Člen | 69
+
0
-

Aha, ono je to se mnou tak, že já na „Magické metody“ právě přišel (samozřejmě jednu jsem používal odjakživa a to __construct – ta doufám ničím nezavání, naopak já měl pocit, že má jen výhody ;-) ). Do teď jsem vždycky používal přímo metody getSlot a setSlot a pomocí copy, paste jsem jich udělal těch sto a upravoval a upravoval a upravoval. Teprve asi včera jsem přišel na metody __get a __set a myslel jsem, že z tohoto důvodu tu jsou, a tak jsem to pochopil i z manuálu. Možná je to špatně, ale pak nechápu, proč tu tyhle konstrukce vůbec jsou, k čemu tedy vlastně jsou? (to je opravdu dotaz, nijak se nehádám, učím se :-) )

to matej21
Jinak si nemyslím, že se tím porušuje zapouzdření, vždyť jde o to, jak to vypadá navenek a jak uvnitř ne? Já nepřistupuji přímo k proměnným toho objektu, ano, navenek to tak vypadá, ale uvnitř, v objektu volám metodu – i když je kouzelná a umí toho povícero. Navíc nejde o to, že mě to nebaví, nebaví mě mnoho věcí, ale dělám je protože to je správné a vyzkoušené – snažím se využívat návrhových vzorů, dodržovat principy programování, které právě využívám atd., jde o to, že jsem se dle manuálu domníval, že přesně z tohoto důvodu tady tyto „Magické“ metody jsou. Ve skutečnosti mi to ale přijde jako dobrý nápad, taková jednoduchá konstrukce, která vlastně udělá totéž jako v C# konstrukce public int Foo { get; set; }, nebo alespoň jsem si to doteď myslel ;-).
A že jsem mohl dát proměnné třídy rovnou public je podle mě rozhodně nepravda. Když budu chtít kdykoliv přidat logiku ke getteru nebo setteru, tak můžu a neporuším tím konzistenci dat ani nic jiného, prostě vytvořím metodu getSlot a konstrukce, kterou jsem napsal to rozpozná a zavolá moji metodu. Tudíž, kdekoliv jsem volal $myobject->slot, tak se tato změna projeví, a to bych rozhodně využitím public proměnných nedokázal.

to Šaman
Já myslím, že některé zkušenosti jsou sdělitelné, když je na druhé straně nějaký posluchač. Posluchačem ale nemyslím člověka, který udělá vše, co se mu „poradí“. Vím, že mi chcete poradit dobře, ale já rád přemýšlím, a když mi někdo něco řekne, zvážím to a trochu zkoumám, dřív než jeho teorii přijmu (samozřejmě jen v oborech, které mě zajímají). Věřím, že s tím mohou být problémy a rád bych zjistil jaké. Mohl bych tě poprosit o poskytnutí těchto zkušeností – mohou mi ušetřit mnoho času a starostí, to už vím ze zkušenosti vlastní :-). Na mapování se určitě kouknu, děkuji. Anotace samozřejmě píšu. S napovídáním je to jasné, protože zdánlivě přistupuji přímo k proměnné a to mi IDE napoví (i když pravda asi ne zvenku, tam je to horší). Co se týká te OOP omáčky, tak ta mi nepřijde vůbec zbytečná, právě naopak, snažím se té omáčky téměř dogmaticky držet – programuji-li funkcionálně, procedurálně, objektově, paralelně nebo logicky, snažím se dodržovat vše, co o tom vím, takže jestliže něco porušuji, dovím se to ještě raději. A jaký datový typ akceptuje setter v PHP? V uvozovkách „každý“, to je jasné, ale ať v PHP použiji cokoliv, někde se ty údaje musí zkontrolovat a musí se to udělat „ručně“ (ať už mi k tomu pomůže něco vytvořeného nebo si to vytvořím sám), PHP je přece jen dynamicky typovaný.

P.S.: Když jsem příspěvek po sobě četl, někdy mi to vyznělo, jakože se chci hádat, že vás chci kompromitovat a vím to líp, než vy. Za to se omlouvám, rozhodně tomu tak není. Jak jsem psal, učím se a budu celý život ;-), jedná se spíše o protiargumenty, které mě napadají a vysvětlují proč jsem vytvořil konstrukci, kterou jsem výše prezentoval, a proč mi ta konstrukce přijde korektní ve smyslu OOP. Vás tím naopak prosím o protiargumentaci, případně vysvětlení… tedy bude-li se vám chtít.

Ještě jednou vám moc děkuji, jsem stále začátečník, ale snažím se, učím se a mým cílem je psát hlavně kvalitní programy (a to i přestože používám PHP, které je mnoha programátory souzeno velmi negativně – já totiž myslím, že nejde o to, jaký používáš nástroj, ale jak jej používáš ;-) )

Šaman
Člen | 2666
+
0
-

Neboj, jako hozenou rukavici k flamewar to neberem :)

S tou validací dat vstupujících do setteru je to skutečně často tak, že si typ musíš kontrolovat ručně (i když třídy a rozhraní se kontrolují automaticky), ale v těch magických setterech většinou nemáš jak zkontrolovat typ, protože nevíš jaký ti má přijít. Anotace to řeší – zjistím, že daný setter existuje v anotaci, rovnou si zjistím co chce za parametr a otestuji že mi přišel správný typ.
Když máš jednotlivé settery, musíš to dělat ručně.

U toho zapouzdření zkus brát třídu jako černou skříňku, co má nějaké vstupy a výstupy. Pokud nemáš někde jasně popsané co kam do té skříňky můžeš dát a co ti z ní kde leze, tak je pro tebe nepoužitelná. A magické funkce tyto věci skrývají (namísto deseti public setterů a getterů máš jen magický _get, _set a spoustu privátního balastu). Proto je potřeba tuto magii popsat, zařídit aby nešlo používat nedeklarované funkce a aby uživatel této třídy (nejen ty sám, ale třeba i IDE, které má napovídat) věděl co od ní může očekávat.

Jinak konstruktor není magická funkce, i když začíná podtržítkem. Je to jen trochu neobyčejná public metoda a JE součástí rozhraní. Závislosti předávané konstruktorem jsou samozřejmě jasně deklarované. Závislosti nutné předat magickým getterem deklarované nejsou. (Napovídání ti pomůže často odhalit moc magické věci. Když dám new Class, tak mi NetBeans ukáže jaké parametry konstruktor chce a pokud píšu PhpDoc anotace, tak i jakého mají být typu. Když použiješ magické $class->setXxx(), tak ti IDE nenapoví ani ten název metody, protože ho nezná. Jak by tedy mohl znát parametry? Tím ti jasně ukáže, že tato třída náležitě nedeklaruje své public rozhraní.)

Editoval Šaman (4. 7. 2013 18:57)

svezij
Člen | 69
+
0
-

Oki, teď už je mi to jasnější. Raději tedy používat deset setterů a getterů a validovat správné typy, je tak?
Ještě takový malý dotaz k tomu: k čemu a kdy tedy použít __get a __set funkce?

Koukl jsem se na Type hinting a zjistil jsem, jak říkáš, že PHP už od 5.1 kontroluje objekty, rozhraní a pole. Myslíš, že je rozumné, jak kdosi píše v komentářích v manuálu, zabalit základní datové typy (int, string, …) do tříd (konkrétně k tomu využít mechanismu autoboxingu ? (Mně to přijde jako dobrý nápad, obzvláště k validaci parametrů metod – líbí se mi kombinace dynamického typování s možností „otypovat staticky“ ;-) – to umí i můj oblíbený Lisp)

To, že to anotace řeší, myslíš tak, že využiješ metody ReflectionClass::getDocComment a regulárním výrazem rozpoznáš datový typ a pak nějakým switchem validuješ?

Tím, „tuto magii popsat“ jsi myslel, že by stačilo popsat to tou anotací a pak klidně použít __get a __set a „nějak“ v nich validovat typy? To se mi asi moc nelíbí, jestliže to má být tak, jak píšeš, tak se asi smířím s deseti gettery a settery ;-)

A s tím konstruktorem vím, jak to myslíš, jen podle PHP manuálu to patří mezi „Magic Methods“, jinak chápu, a jak jsem psal, je to jediná „magická“ funkce / metoda, kterou jsem kdy použil.

Šaman
Člen | 2666
+
0
-

Ajo, manuál to tvrdí. Bohužel nepopisuje v čem vidí tu magii.
Já za magii považuji to, když například pomocí __get, nebo __set vytvoříš „neexistující“ metodu. Na konstruktoru není nic magického. Je to jen rezervované slovo.

Spoustu set/get funkcí můžeš nahradit magickým voláním, ale za prvé musíš deklarovat rozhraní, tedy nejspíš popsat ty magicky získané metody v anotaci a za druhé zajistit, aby se nedala zavolat nedeklarovaná metoda. Tím tu magii deklaruješ/popíšeš imho to magií být přestává.
Ta kontrola typu je voliteně na tobě, ale pokud pak budeš chtít data hned zapisovat do db, tak je vhodná.

Zabalit základní typy do objektu imho dobrý nápad není, protože se tím omezíš o všechny funkce, které s tímto typem pracují. Tedy konkrétně u stringu by to byl docela problém. Ale já mám rád objektová pole a úspěšně je používám.

svezij
Člen | 69
+
0
-

Ok, myslím, že jsme snad probrali téměř vše, na co jsem v tomto tématu narazil. Děkuji, zase jsem o chlupa chetřéší ;-).