SmartObject
SmartObject je leta izboljševal obnašanje objektov v PHP. Od različice PHP 8.4 so vse njegove funkcije že del samega PHP, s čimer je zaključil svojo zgodovinsko misijo biti pionir sodobnega objektnega pristopa v PHP.
Namestitev:
composer require nette/utils
SmartObject je nastal leta 2007 kot revolucionarna rešitev za pomanjkljivosti takratnega objektnega modela PHP. V času, ko je PHP trpel zaradi številnih težav z objektnim načrtovanjem, je prinesel znatno izboljšanje in poenostavitev dela za razvijalce. Postal je legendarni del ogrodja Nette. Ponujal je funkcionalnost, ki jo je PHP pridobil šele mnogo let kasneje – od nadzora dostopa do lastnosti objektov do sofisticiranih sintaktičnih sladkorčkov. S prihodom PHP 8.4 je zaključil svojo zgodovinsko misijo, saj so vse njegove funkcije postale naravni del jezika. Prehitel je razvoj PHP za izjemnih 17 let.
Tehnično je SmartObject doživel zanimiv razvoj. Prvotno je bil implementiran kot razred Nette\Object
, od
katerega so drugi razredi podedovali potrebno funkcionalnost. Ključna sprememba je prišla s PHP 5.4, ki je prinesel podporo za
traite. To je omogočilo preoblikovanje v obliko traite Nette\SmartObject
, kar je prineslo večjo
prilagodljivost – razvijalci so lahko funkcionalnost uporabili tudi v razredih, ki so že dedovali od drugega razreda. Medtem
ko je prvotni razred Nette\Object
izginil s prihodom PHP 7.2 (ki je prepovedal poimenovanje razredov z besedo
Object
), traita Nette\SmartObject
živi naprej.
Poglejmo si lastnosti, ki sta jih nekoč ponujala Nette\Object
in kasneje Nette\SmartObject
. Vsaka od
teh funkcij je v svojem času predstavljala pomemben korak naprej na področju objektno usmerjenega programiranja v PHP.
Konsistentna stanja napak
Eden najbolj perečih problemov zgodnjega PHP je bilo nekonsistentno obnašanje pri delu z objekti. Nette\Object
je v ta kaos vnesel red in predvidljivost. Poglejmo, kako je izgledalo prvotno obnašanje PHP:
echo $obj->undeclared; // E_NOTICE, kasneje E_WARNING
$obj->undeclared = 1; // gre tiho skozi brez poročanja
$obj->unknownMethod(); // Fatal error (neulovljiv s try/catch)
Fatal error je končal aplikacijo brez možnosti kakršnega koli odziva. Tiho zapisovanje v neobstoječe člane brez opozorila
je lahko vodilo do resnih napak, ki jih je bilo težko odkriti. Nette\Object
je vse te primere prestregel in vrgel
izjemo MemberAccessException
, kar je programerjem omogočilo, da se na napake odzovejo in jih rešijo.
echo $obj->undeclared; // vrže Nette\MemberAccessException
$obj->undeclared = 1; // vrže Nette\MemberAccessException
$obj->unknownMethod(); // vrže Nette\MemberAccessException
Od PHP 7.0 jezik ne povzroča več neulovljivih fatalnih napak in od PHP 8.2 se dostop do nedeklariranih članov šteje za napako.
Pomoč “Did you mean?”
Nette\Object
je prišel z zelo prijetno funkcijo: inteligentno pomočjo pri tipkarskih napakah. Ko je razvijalec
naredil napako v imenu metode ali spremenljivke, ni samo sporočil napake, ampak je ponudil tudi pomoč v obliki predloga
pravilnega imena. To ikonično sporočilo, znano kot “did you mean?”, je programerjem prihranilo ure iskanja
tipkarskih napak:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// vrže Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Današnji PHP sicer nima nobene oblike „did you mean?“, vendar ta dodatek zna v napake dodajati Tracy. In celo takšne napake samodejno popravljati.
Lastnosti z nadzorovanim dostopom
Pomembna inovacija, ki jo je SmartObject prinesel v PHP, so bile lastnosti z nadzorovanim dostopom. Ta koncept, običajen v jezikih, kot sta C# ali Python, je razvijalcem omogočil elegantno nadzorovanje dostopa do podatkov objekta in zagotavljanje njihove konsistentnosti. Lastnosti so močno orodje objektno usmerjenega programiranja. Delujejo kot spremenljivke, vendar so dejansko predstavljene z metodami (getterji in setterji). To omogoča validacijo vnosov ali generiranje vrednosti šele v trenutku branja.
Za uporabo lastnosti morate:
- Dodati razredu anotacijo v obliki
@property <type> $xyz
- Ustvariti getter z imenom
getXyz()
aliisXyz()
, setter z imenomsetXyz()
- Zagotoviti, da sta getter in setter public ali protected. Sta izbirna – lahko torej obstajata kot read-only ali write-only lastnost
Poglejmo si praktičen primer na razredu Circle, kjer lastnosti uporabimo za zagotovitev, da bo polmer vedno nenegativno
število. Nadomestimo prvotno public $radius
z lastnostjo:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // ni public!
// getter za lastnost $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter za lastnost $radius
protected function setRadius(float $radius): void
{
// vrednost pred shranjevanjem saniramo
$this->radius = max(0.0, $radius);
}
// getter za lastnost $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // dejansko kliče setRadius(10)
echo $circle->radius; // kliče getRadius()
echo $circle->visible; // kliče isVisible()
Od PHP 8.4 je mogoče doseči enako funkcionalnost z uporabo property hooks, ki ponujajo veliko bolj elegantno in jedrnato sintakso:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Razširitvene metode
Nette\Object
je v PHP prinesel še en zanimiv koncept, navdihnjen s sodobnimi programskimi jeziki –
razširitvene metode. Ta funkcija, prevzeta iz C#, je razvijalcem omogočila elegantno razširjanje obstoječih razredov z novimi
metodami brez potrebe po njihovem urejanju ali dedovanju od njih. Na primer, lahko ste v obrazec dodali metodo
addDateTime()
, ki doda lasten DateTimePicker:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Razširitvene metode so se izkazale za nepraktične, saj njihova imena niso bila predlagana s strani urejevalnikov, nasprotno, poročali so, da metoda ne obstaja. Zato je bila njihova podpora ukinjena. Danes je bolj običajno uporabljati kompozicijo ali dedovanje za razširitev funkcionalnosti razredov.
Ugotavljanje imena razreda
Za ugotavljanje imena razreda je SmartObject ponujal preprosto metodo:
$class = $obj->getClass(); // z uporabo Nette\Object
$class = $obj::class; // od PHP 8.0
Dostop do refleksije in anotacij
Nette\Object
je ponujal dostop do refleksije in anotacij z metodama getReflection()
in
getAnnotation()
. Ta pristop je znatno poenostavil delo z meta-informacijami razredov:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // vrne 'John Doe'
Od PHP 8.0 je mogoče dostopati do meta-informacij v obliki atributov, ki ponujajo še več možnosti in boljši tipski nadzor:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Metodni getterji
Nette\Object
je ponujal eleganten način za posredovanje metod, kot da bi šlo za spremenljivke:
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
Od PHP 8.1 je mogoče uporabiti t.i. first-class callable syntax, ki ta koncept pelje še dlje:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Dogodki
SmartObject ponuja poenostavljeno sintakso za delo z dogodki. Dogodki omogočajo objektom, da obveščajo druge dele aplikacije o spremembah svojega stanja:
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
Koda $this->onChange($this, $radius)
je ekvivalentna naslednji zanki:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Zaradi razumljivosti priporočamo, da se izogibate magični metodi $this->onChange()
. Praktična zamenjava je
na primer funkcija Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);