Przeciążanie pól klasy
__get(string $propertyName): mixed
__set(string $propertyName, mixed $value): void
✅ gdy pole klasy jest z modyfikatorem dostępu protected i private,
✅ pola które nie są jawnie zadeklarowane,
❌ gdy pole jest publiczne,
❌ gdy pole jest publiczne i z wartością NULL,
❌ gdy wcześniej dynamicznie przypisaliśmy do pola jakąś wartość,
__isset(string $propertyName): bool
__unset(string $propertyName): void
__isset() wzbudzane przez:
- isset(),
- empty(),
- operator ??
__unset() wzbudzane przez:
- unset(),
✅ gdy pole klasy jest z modyfikatorem dostępu protected i private,
✅ gdy pole klasy nie jest jawnie zadeklarowane,
❌ gdy pole jest publiczne,
❌ gdy pole jest publiczne i z wartością NULL,
❌ gdy wcześniej dynamicznie przypisaliśmy do pola jakąś wartość,
❌ gdy pole jest publiczne,
❌ gdy pole jest publiczne i z wartością NULL,
❌ gdy wcześniej dynamicznie przypisaliśmy do pola jakąś wartość,
Tutaj możemy być zdezorientowani przez to, że pola klasy mogą przyjmować wartości które np. w normalnym użyciu z konstruktem empty() mogą być rozpatrywane pozytywnie (zwracać true) dla wartości: (int) 0, (string) '0', (double) 0.0, (array) [], null. Tak naprawdę __isset() będzie wzbudzane tylko gdy pole jest ukryte przez modyfikator protected/private albo nie jest zadeklarowane - wartość nie ma znaczenia.
Przeciążanie metod klasy
Do przeciążania metod klasy można wykorzystać magiczną metodę __call() wzbudzaną w w przypadku gdy metoda klasy jest prywatna bądź nie jest zadeklarowana.
class Person {
/**
* @var string
*/
private $email;
public function __construct(string $email) {
$this->email = $email;
}
public function getEmail(): string {
return $this->email;
}
private function getFirstName(): string {
return 'nie wywołane';
}
public function __call(string $methodName, array $args) {
return $args[0];
}
}
$obj = new Person('johny@mail.com');
echo $obj->getEmail(); // johny@mail.com
echo $obj->getFirstName('Jason'); // jason
echo $obj->getLastName('Doe'); // Doe
Przeciążanie konstruktora klasy
By przeciążyć konstruktor też musimy zastosować obejście ponieważ PHP nie pozwala na deklarowanie kilku konstruktorów naraz. Tak jak w przypadku przeciążania metod klasy, tak i tutaj problemem może być brak wsparcia ze strony IDE (nie będzie podpowiadać oczekiwanych parametrów) jak i ścisła dyscyplina przestrzegana przez developerów podczas projektowania i instancjonowania klas. Do wykonania zadania wykorzystam funkcję:
- func_num_args(): int
- Zwraca wartość typu Integer reprezentującej ilość przekazanych do funkcji argumentów,
- func_get_arg(int $argNumber): mixed
- Zwraca przekazany do funkcji argument o zadanym indeksie, zaczynając od zera,
- func_get_args(): array
- Zwraca argumenty w kolejności w jakiej zostały przekazane w kodzie klienckim w tablicy - opcjonalnie zamiast dwóch poprzednich funkcji,
W normalnym przypadku klasę można zaprojektować w taki sposób:
class Person {
public function __construct(
string $username,
DateTime $birthDate) {
$this->username = $username;
$this->birthDate = $birthDate;
}
}
Jak widać każde z pól jest TypeHint'owane na konkretny typ co jest dobrą praktyką ;). Jednak nas interesuje bardziej dynamiczne rozwiązanie, które będzie w stanie imitować przeciążanie konstruktora - oczywiście z perspektywy kodu klienckiego. Oto przykład takiej implementacji:
class Person {
public function __construct() {
if (func_num_args() === 2) {
if (false === is_string(func_get_arg(0))) {
throw new TypeError('First param must be string');
}
if (false === func_get_arg(1) instanceof DateTime) {
throw new TypeError('Second param must DateTime instance');
}
$this->username = func_get_arg(0);
$this->birthDate = func_get_arg(1);
} else if (func_num_args() === 4) {
if (false === is_string(func_get_arg(0))) {
throw new TypeError('First param must be string');
}
if (false === is_string(func_get_arg(1))) {
throw new TypeError('First param must be string');
}
if (false === is_string(func_get_arg(2))) {
throw new TypeError('First param must be string');
}
if (false === func_get_arg(3) instanceof DateTime) {
throw new TypeError('Second param must DateTime instance');
}
$this->username = func_get_arg(0);
$this->firstName = func_get_arg(1);
$this->lastName = func_get_arg(2);
$this->birthDate = func_get_arg(3);
} else {
throw new ArgumentCountError('only 2 or 4 arguments');
}
}
}
Efekt końcowy jest taki, że nasz konstruktor jest przygotowany pod dwa zestawy argumentów, ciało funkcji jest dość obszerne, a przez to mało czytelne. W przypadku np. czterech zestawów argumentów, implementacja konstruktora w najlepszym przypadku była by tylko dwa razy większa. Czy jest to dobre podejście do projektowania klas? moim zdaniem nie. Za każdym razem przed tworzeniem obiektu trzeba będzie zaglądać do pliku z klasą bo nasze IDE nie podpowie nam nic o wymaganych argumentach. Jedynym ratunkiem będzie właśnie przeczytanie DocBlocka, analiza kodu bądź zdanie się na własną pamięć. Można było by wykorzystać opcjonalne argumenty w sygnaturze konstruktora - oszczędzała by ilość kodu potrzebną do implementacji, lecz sprawiła by że całość stała by się mniej plastyczna :
class Person {
public function __construct(
string $username,
DateTime $birthDate,
?string $firstName = null,
?string $lastName = null
) {
$this->username = $username;
$this->birthDate = $birthDate;
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
Teraz musimy pilnować by dodatkowe argumenty zawsze były na końcu no i nie wszystkie przypadki dynamicznych zestawów możemy 'obsłużyć'.
- metody magiczne możemy wywoływać bezpośrednio w kodzie klienckim,
- jak zadeklarujemy metodę __call() tylko z jednym argumentem to dostaniemy nieprzechwytywalny Fatal Error,
- w przypadku gdy w metodzie __set() zadeklarujemy return, nie zostanie wyrzucony Fatal Error ale słowo kluczowe return zostanie zignorowane,
- Gdy poza funkcją będziemy próbowali użyć funkcji func_get_args(), func_num_args(), func_get_arg(1): otrzymamy Warning'a,
Źródła:
Brak komentarzy:
Prześlij komentarz