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:

niedziela, 20 października 2019

Standardowe błędy PHP


Dzielą się na FATAL i NON-FATAL:

                                                                                                                               
+---------------------------------------------------------+         
|                          E_ALL                          |
+---------------------------------------------------------+ 
   __            __             __          __          __
  |  |          |  |           |  |        |  |        |  | 
  ERROR   RECOVERABLE_ERROR   WARNING     NOTICE    DEPRECATED
  |  |          |  |           |  |        |  |        |  | 
CORE_ERROR      |  |        CORE_WARNING   |  |        |  |  
  |  |          |  |           |  |        |  |        |  |
COMPILE_ERROR   |  |       COMPILE_WARNING |  |        |  | 
  |  |          |  |           |  |        |  |        |  |
USER_ERROR      |  |       USER_WARNING USER_NOTICE USER_DEPRECATED
  |  |          |  |           |  |        |  |        |  |
  PARSE         |  |           |  |        |  |       STRICT
  |  |          |  |           |  |        |  |        |  | 
__|  |__      __|  |__       __|  |__    __|  |__    __|  |__  
\      /      \      /       \      /    \      /    \      / 
 \    /        \    /         \    /      \    /      \    / 
  \  /          \  /           \  /        \  /        \  /
   \/            \/             \/          \/          \/  

+-----------------------+    +-------------------------------+
|         FATAL         |    |           NON FATAL           |
+-----------------------+    +-------------------------------+ 


Możemy też przyjąć inny podział, ze względu na poziom zagrożenia: 


+-----------------+  +-----------------+  +-----------------+
|      NOTICE     |  |     WARNING     |  |      ERROR      |
|                 |  |                 |  |                 |
| *NOTICE         |  | *WARNING        |  | *ERROR          |
| *USER_NOTICE    |  | *USER_WARNING   |  | *USER_ERROR     |
*STRICT         |  | *COMPILE_WARNING|  | *COMPILE_ERROR  | 
| *DEPRECATED     |  | *CORE_WARNING   |  | *CORE_ERROR     |
| *USER_DEPRECATED|  |                 |  | *PARSE          |
|                 |  |                 |  | *RECOVERABLE    |
+-----------------+  +-----------------+  +-----------------+

ze względu na jego źródło:

+-----------------+  +-----------------+  +-----------------+
|       USER      |  |    COMPILER     |  |     RUNTIME     |
|                 |  |                 |  |                 |
| *USER_ERROR     |  | *PARSE          |  | *ERROR          |
| *USER_WARNING   |  | *CORE_ERROR     |  | *STRICT         |
| *USER_ERROR     |  | *CORE_WARNING   |  | *RECOVERABLE    |
| *USER_DEPRECATED|  | *COMPILE_ERROR  |  | *WARNING        |
|                 |  | *COMPILE_WARNING|  | *NOTICE         |
|                 |  |                 |  | *DEPRECATED     |
+-----------------+  +-----------------+  +-----------------+

czy są możliwe do przechwycenia przez procedurę obsługi zadeklarowaną w funkcji set_error_handler():

⛔ E_ERROR
⛔ E_PARSE
⛔ E_CORE_ERROR, E_CORE_WARNING
⛔ E_COMPILE_ERROR, E_COMPILE_WARNING
⛔ E_STRICT (większość)
✅ E_NOTICE
✅ E_WARNING
✅ E_DEPRECATED
✅ E_RECOVERABLE_ERROR
✅ E_USER_ERROR, E_USER_WARNING, E_USER_DEPRECATED, E_USER_NOTICE

Jest tak ponieważ możliwe do obsługi są jedynie błędy występujące w czasie run-time, wyjątkiem od tej reguły jest E_ERROR i E_STRICT. Warto nadmienić, że konfiguracja zawarta w funkcji error_reporting() nie będzie mieć wpływu na to czy handler przechwyci błąd czy nie.


 E_WARNING  - (2) ostrzeżenie powstałe w czasie wykonywania (run-time) - nie powoduje zatrzymania wykonywania kodu. Wyrzucane gdy:

 inkludujemy nieistniejący plik,
❌ użyjemy nieistniejącej stałej,
❌ wywołujemy nie-statyczną metodę klasy w statyczny sposób,
❌ nie zapewnimy wszystkich wymaganych argumentów funkcji/metody klasy, ale tylko do wersji 7.0 włącznie. Od PHP 7.1 wyrzucony będzie Error ArgumentCountError. W przypadku gdy funkcja będzie wymagała dwa argumenty, a wywołując ją nie dodamy żadnego, będziemy mieli dwa wywołane komunikaty ostrzegające.

Różne sposoby by ostrzeżenia NIE pokazywać: 
  • error_reporting(E_ALL ^ E_WARNING);
  • error_reporting(E_ALL & ~E_WARNING);
  • ini_set('error_reporting', E_ALL & ~E_WARNING);
  • error_reporting(E_ERROR | E_STRICT);

 E_COMPILE_WARNING  (128) - ostrzeżenia wygenerowane w czasie kompilowania skryptu przez silnik Zend domyślnie logowany do pliku error_log. Tego typu komunikaty otrzymać można wywołując taki kod:

echo "some message";
declare(unexistedDirective='someValue');

Tak jak w przypadku zwykłego ostrzeżenia, wykonywanie programu nie zostanie przerwane lecz z tą różnicą, że komunikat ostrzeżenia wyświetli się na samym początku:

Warning: Unsupported declare 'unexistedDirective'
some message

Jak widać developer nie jest jawnie informowany o ostrzeżeniu w czasie kompilacji, więc można to łatwo pomylić ze zwykłym ostrzeżeniem. Warto mieć to na uwadze podczas implementacji custom'owego handler'a z rozróżnieniem na poszczególne błędy. 

 E_CORE_WARNING  (32)   - generowany przez silnik PHP w czasie jego inicjalizacji.
 E_USER_WARNING  (512) - możliwy do wygenerowania przez developera za pomocą funkcji trigger_error().


 E_NOTICE  - (8) nie do końca jest to błąd ponieważ podczas wykonywania kodu powodującego ten komunikat, proces nie zostaje wstrzymany. Jest to po prostu informacja dla programisty, że coś przebiegło nie tak. Kilka przykładów, które powodują wygenerowanie tego komunikatu:

❌ użycie zmiennej która nie istnieje,
❌ użyjemy pola obiektu które nie istnieje,
❌ odwołanie się do indeksu tablicy, który nie istnieje,


Ukrycie komunikatu: error_reporting(E_ALL ^ E_NOTICE);

bądź w pliku php.ini deklarując wartość dyrektywy na to samo co w nawiasach funkcji error_reporting().

 E_USER_NOTICE  (1024) - możliwy do wygenerowania przez developera za pomocą funkcji trigger_error() przekazując tylko jeden argument typu String będący wiadomością. Drugi argument nie jest konieczny ponieważ jego wartość domyślna to 1024.


 E_STRICT  - (2048) - Jest to podpowiedź od interpretera, że konkretne rozwiązanie nie jest najlepszym standardem/dobrą praktyką lub może zostać wycofane w przyszłych wersjach języka. Wchodzi w skład predefiniowanej stałej E_ALL dopiero od wersji PHP 5.4 choć dostępny jest od 5.0. Od wersji 7.0 już nie zdarzy się nam ujrzeć tego typu błędu ponieważ został zastąpiony komunikatami NOTICE, WARNING czy DEPRECATED. Ze względu na utrzymanie kompatybilności wstecznej stała E_STRICT została jednak zachowana. Powodem tych zmian było zapewnienie prostszego systemu błędów w PHP w którym rola E_STRICT była dość niejasna. Więcej informacji na ten temat znajduje się w linku RFC:reclassify_e_strict. We wcześniejszych wersjach języka błąd E_STRICT otrzymywaliśmy np. gdy:


❌ odwoływaliśmy się do nie-statycznej metody klasy w sposób statyczny,
❌ sygnatura metody w klasie jest inna niż sygnatura tej samej metody w nad klasie,
❌ w klasie zadeklarujesz dwa konstruktory: za pomocą metody magicznej __construct() i starym sposobem gdzie nazwa konstruktora jest taka sama jak nazwa klasy. 
masz metodę która zwraca array'a, wywołujesz ją w funkcji array_pop() - dostajesz komunikat: DEBUG Only variables should be passed by reference.
❌ zadeklarujemy sygnaturę abstrakcyjnej metody statycznej,


 E_DEPRECATED  (8192)  E_USER_DEPRECATED  - (16384) - dostępne od wersji 5.3. Jest to ostrzeżenie wyrzucane w czasie run-time o użytych w kodzie funkcjach\klasach\konstruktach czy rozwiązaniach, które będą wycofane w przyszłych wersjach języka. O tym czy dany wyżej wymienione będą uznawane za przestarzałe decydują ludzie zaangażowani w rozwój języka. Komunikaty te będą się różnić w zależności od wersji języka z której aktualnie korzystasz. W przypadku własnych bibliotek, sam możesz wygenerować błąd E_USER_DEPRECATED za pomocą wbudowanej w język funkcji user_error() będącej aliasem trigger_error() np w taki sposób:
trigger_error('Some message', E_USER_DEPRECATED);

Co powoduje wyrzucenie komunikatu E_DEPRECATED:

7.3
❌ podczas deklarowania stałej case-insensitive za pomocą funkcji define() - czyli przekazując jako trzeci parametr TRUE,
❌ flagi dla funkcji filter_var(): FILTER_FLAG_SCHEME_REQUIRED, FILTER_FLAG_HOST_REQUIRED.

7.2
❌ korzystanie z create_function(), each()__autoload(),
❌ rzutowanie wartości na NULL: (unset) $someVariable,
❌ dyrektywa php.ini track_errors,
❌ assert() z parametrem typu String,
❌ zmienna $php_errormsg,
używanie funkcji parse_str() bez podawania drugiego argumentu,

7.1
❌ korzystanie z rozszerzenia mcrypt z racji tego, że nie jest wspierane i trudne w użyciu. Usunięty z PHP w wersji 7.2 i zasobów PECL,
❌ modyfikator e dla funkcji (Multibyte String Function) mb_ereg_replace() i mb_eregi_replace(),

7.0
❌ konstruktory klasy w stylu PHP 4 czyli taki gdzie nazwa funkcji jest taka sama jak nazwa klasy w której się znajduje,

Wymienione wyżej przypadki zostaną usunięte w PHP 8.0.

 E_RECOVERABLE_ERROR  (4096) - dostępny od wersji 5.2. Jest to właściwie Fatal Error możliwy do przechwycenia przez handler zdefiniowany za pomocą funkcji set_error_handler(). Kod który powoduje wyrzucenie tego komunikatu jest poważnym błędem, ale nie pozostawia silnika PHP w niestabilnym stanie, dlatego jeżeli tylko zostanie przechwycony, wykonywanie programu nie zostaje przerwane (w przeciwnym razie program zachowa się jak w przypadku E_ERROR).

Co wyrzuca 'odzyskiwalne błędy' pisałem w tym wpisie. To jedyne przypadki, które udało mi się jak dotąd namierzyć. Ale... instancjonowanie obiektu Closure inaczej można obsłużyć w zależności od wersji:

PHP 5.6 w set_error_handler() ponieważ wyrzucany jest E_RECOVERABLE_ERROR.

PHP 7.0: w bloku try/catch ponieważ wyrzuci to E_ERROR.

Co ciekawe w przypadku błędów związanych z __toString() (ze wspomnianego wpisu) one w dalszym ciągu są obsługiwane przez set_error_handler().




 E_ERROR  (1) - błąd krytyczny wywoływany w czasie wykonywania powodujący wyświetlenie komunikatu typu Fatal Error, a w związku tym natychmiastowe przerwanie skryptu. Przechwycenie tego typu błędu we własnej procedurze obsługi zadeklarowanej za pomocą funkcji set_error_handler() nie jest możliwe. Różnica między PHP 5.6 a 7.0 jest tak, że teraz mamy możliwość godnego obsłużenia niektórych błędów krytycznych:

Błędy które do wersji 5.6  powodowały nieobsługiwane Fatal Error'y, a od 7.0 można je przechwytywać w bloku try/catch, TypeHint'ując na klasę Error:
 wywołanie nieistniejącej funkcji,
 instancjonowanie nieistniejącej klasy,
 instancjonowanie interfejsu,
 odwołanie się do nieistniejącego statycznego pola klasy,
 odwołanie się do nieistniejącej metody statycznej klasy,

Błędy które nawet w wersjach 7.x nie są możliwe do obsługi:
❌ implementowanie interfejsu który nie istnieje,
❌ dziedziczenie po nieistniejącej klasie,
❌ próba przypisania do pola, pola statycznego, stałej klasy innej wartości niż literał np. funkcji anonimowej,
❌ dodanie ciała metody w interfejsie,
próba przypisania wartości do $this,
❌ stosowanie isset() na literałach np. isset(0.0);



 E_CORE_ERROR  (16) - generowany przez silnik PHP. Błąd wystąpi gdy mamy PHP 5.4 (i większe) a w php.ini mamy zadeklarowaną dyrektywę safe_mode=On. Kod developera nie zostaje w ogóle wykonywany.

 E_COMPILE_ERROR  (64) - wyrzucane gdy na zmiennej będziemy próbowali zastosować ::class. Otrzymamy wtedy komunikat: FATAL ERROR Dynamic class names are not allowed in compile-time ::class fetch.

 E_USER_ERROR  (256) - możliwy do wygenerowania przez developera za pomocą funkcji trigger_error()i tym razem - inaczej niż w przypadku E_ERROR - możliwy do obsługi za pomocą set_error_handler().


 E_PARSE  (4) - pojawia się gdy kompilator nie może zinterpretować kodu developera - niemożliwy do obsługi w jakikolwiek sposób. Powoduje wyświetlenie komunikatu typu Parse error: syntax errorKod developera nie zostaje w ogóle wykonywany. Występuje między innymi w następujących przypadkach zaimplementowanego kodu:

❌ final interface MyInterface {}
❌ abstract interface MyInterface {}
❌ class MyClass() {}
❌ echo 'something'
❌ if (if(true) {} else {}) {}


 FATAL ERROR  - jest to błąd krytyczny i gdy wystąpi następuje przerwanie wykonywania programu. Domyślne zachowanie sprowadza się do wyświetlenia komunikatu błędu.

 FATAL ERROR: UNCAUGHT ERROR  - jest to Fatal Error możliwy do przechwycenia. Dopisek Uncaught error daje informację o tym iż jest to błąd do przechwycenia w klauzuli try/catch, a więc TypeHint'ując w bloku catch na klasę Error albo Throwable mamy możliwość jego obsługi. Procedura obsługi w funkcji set_error_handler() nie jest w stanie obsłużyć tego błędu. Rozszerzony komunikat Fatal Error pojawił się dopiero w PHP 7.0.

 FATAL ERROR: UNCAUGHT EXCEPTION  - tak samo jak powyżej analogicznie do nieprzechwyconych wyjątków (czyli jak ich jawnie nie obsłużymy to zamieniają się w Fatal Error'y).

Standardowe błędy PHP i klasa ErrorException


Dokumentacja zaleca by przechwytywać ostrzeżenia WARNING czy komunikaty NOTICE w procedurze obsługi zdefiniowanej w funkcji set_error_handler() i tam wyrzucać wyjątek ErrorException (PHP 5.1) stworzony z intencją o takim zastosowaniu. Dzięki temu będziemy mogli obsłużyć wyrzucone ostrzeżenie (przykład poniżej) w kontekście jego wywołania:


set_error_handler(function(int $errorCode) {
    if ($errorCode === E_WARNING) {
        throw new ErrorException('Some warning');
    }
});

try {
    echo A;
} catch (ErrorException $e) {
    echo $e->getMessage();
}



Na sam koniec wrzucam fajny kalkulator do wyliczania kodu błędów. 



Źródła: