Proste problemy wymagają prostych rozwiązań - ta myśl przeszyła moje myśli dzisiejszego poranka, kiedy kolega z pracy pokazał mi proste, aczkolwiek genialne w swojej prostocie rozwiązanie problemu dynamicznych pól obiektów w PHP. Jak zapewne wiesz, język PHP pozwala na programiście na wiele sztuczek, czasami aż do przesady. Jedną z nich jest właśnie możliwość dynamicznego deklarowania pól obiektów.

Jak to wygląda w innych językach programowania

Spójrzmy na przykładowy kod aplikacji w języku C# w .NET Core:

using System; namespace Demo { class KittyCat { public string name { get; set; } } class Program { static void Main(string[] args) { var MyKitty = new KittyCat(); MyKitty.name = "MyCat"; MyKitty.age = 12; } } }

Podczas próby kompilacji takiego kodu (ale także podczas wpisywania zawartości ostatniej linii w metodzie Main) pojawia się błąd krytyczny:

[Error] [CS1061] 'KittyCat' does not contain a definition for 'age' and no accessible extension method 'age' accepting a first argument of type 'KittyCat' could be found (are you missing a using directive or an assembly reference?

Jest to jak najbardziej oczekiwane zachowanie w kontekście myślenia obiektowego. Tworząc obiekt na podstawie jego definicji nie możemy ot tak sobie zadecydować, że obiekt może mieć dodatkowe właściwości. Jednak w języku PHP sytuacja jest zupełnie odwrotna. Stwórzmy alternatywny kod w tymże języku:

<?php declare(strict_types=1); namespace Demo; class KittyCat { public $name; } $myKitty = new KittyCat(); $myKitty->name = "MyCat"; $myKitty->age = 12; var_dump($myKitty);

Uruchamiając powyższy kod otrzymamy wynik:

object(Demo\KittyCat)#1 (2) { ["name"]=> string(5) "MyCat" ["age"]=> int(12) }

No to mamy klopsiki w sosie własnym.

Problemy wynikające z dynamicznie generowanych pól

Powyższe zachowanie języka PHP implikuje kilka negatywnych skutków, z którymi borykamy się my, programiści PHP. Po pierwsze: możliwość popełnienia literówki. Ze względu na to, że możemy odwołać się do dowolnego pola w obiekcie i nie uświadczymy żadnego błędu podczas uruchomienia kodu, jeżeli popełnimy literówkę i tego nie zauważymy, błąd może być ciężki do wychwycenia i poprawienia (zarówno teraz, jak i w przyszłości). Po drugie: działanie takie zachęca do wybierania drogi na skróty i ułatwia zaciemnienie kodu (przez niedbalstwo programisty). Zdarzają się bowiem sytuacje, kiedy programista woli dopisać na szybko dodatkowe pole zamiast utworzyć nową, pochodną klasę.

Kolega z pracy wskazał mi dzisiaj jeden prosty trick :D, dzięki któremu możemy się jednak przeciw temu zabezpieczyć.

Ciekawe wykorzystanie dostępnych metod magicznych

W języku PHP mamy metody magiczne, które wywoływane są w określonych momentach. Dwie z nich możemy tutaj z powodzeniem wykorzystać: __get oraz __set, które wywoływane są w chwili próby dostępu do nieistniejącego pola obiektu. Stwórzmy trait jak poniżej:

<?php trait DisableDynamicProperties { public function __set($property, $value) { trigger_error("Can't modify not existing property $property in " . self::class, E_USER_ERROR); } public function __get($property) { trigger_error("Can't access not existing property $property in " . self::class, E_USER_ERROR); } }

Następnie dodajmy go do naszej klasy:

<?php declare(strict_types=1); namespace Demo; trait DisableDynamicProperties { public function __set($property, $value) { trigger_error("Can't modify not existing property $property in " . self::class, E_USER_ERROR); } public function __get($property) { trigger_error("Can't access not existing property $property in " . self::class, E_USER_ERROR); } } class KittyCat { use DisableDynamicProperties; public $name; } $myKitty = new KittyCat(); $myKitty->name = "MyCat"; $myKitty->age = 12; var_dump($myKitty);

Wywołując teraz kod z pliku otrzymamy komunikat:

Fatal error: Can't modify not existing property age in Demo\KittyCat in Z:\Users\rombarte\index.php on line 11

Świetna wiadomość!