piątek, 25 października 2019

Przeciążanie w PHP



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ść,

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