Autoload, call_user_func_array i parent::__construct

Podziel się z innymi!

    Pracuje właśnie nad mechanizmem automatycznie generującym klasy potomne dziedziczące po istniejącej już w systemie klasie bazowej.

    class A
    {
    	public function __construct()
    	{}
     
    	public function index()
    	{
    		// do something
    	}
    }

    Mając powyższą klasę bazową wystarczy wygenerować klasę potomną, która w konstruktorze wywoła konstruktor parenta.

    class B extends A
    {
    	public function __construct()
    	{
    		parent::__construct();
    	}
    }

    Tyle, że konstruktor klasy bazowej może mieć jakieś parametry.

    class A
    {
    	private $param;
     
    	public function __construct($param)
    	{
    		$this->param = $param;
    	}
     
    	public function index()
    	{
    		// do something
    	}
    }

    Ponieważ chcę napisać generator w miarę uniwersalny, a nie wiem ile parametrów i jakie może mieć, każda z klas bazowych, które będę chciał rozszerzyć dobrym rozwiązaniem wydawało by się wywoływanie klasy parenta przy pomocy funkcji „call_user_func_array”.

    class B extends A
    {
    	public function __construct()
    	{
    		$args = func_get_args();
    		call_user_func_array(array('parent', '__construct'), $args);
    	}
    }

    Po utworzeniu obiektu:

    $obj = new B('foo');

    Wszystko działało zgodnie z oczekiwaniami, było fajnie tylko jak włączyłem raportowanie błędów ze sprawdzaniem składni

    error_reporting(E_ALL^E_STRICT);
    ini_set("display_errors", 1);

    To zobaczyłem taki nieprzyjemny komunikat „**Strict standards: Non-static method A::__construct() cannot be called statically, assuming $this from compatible context B**”.

    Powiem szczerze, że tak nie za bardzo wiedziałem co mam począć w tej sytuacji bo najbardziej intuicyjne rozwiązanie okazało się niezgodne ze standardami. Zajrzałem oczywiście do manuala gdzie znalazłem rozwiązanie. Przy okazji dowiedziałem się jeszcze, że inaczej powinno to wyglądać dla PHP w wersji 5.0 – 5.2, a już inaczej w PHP 5.3 i prawdopodobnie 6.

    class D extends A
    {
    	public function __construct()
    	{
    		$args = func_get_args();
    		if (version_compare(PHP_VERSION, '5.3.0', '>')) {
    			call_user_func_array('parent::__construct', $args);
    		} else {
    			call_user_func_array(array($this, 'parent::__construct'), $args);
    		}
    	}
    }

    Gdyby to zamykało temat zapewne nie stworzyłbym tego wpisu, tyle, że pojawił się kolejny problem.

    W moim eksperymentalnym frameworku używam loadera klas, który w razie nieznalezienia wywoływanej klasy skanuje zadane foldery w poszukiwaniu plików php, a następnie sprawdza czy znajdują się w nich definicje klas lub interfejsów. Następnie tworzy mapę w postaci tablicy, w której kluczem jest nazwa klasy, a wartością ścieżka do pliku zawierającego definicję owej klasy.

    Otóż zauważyłem, że przy każdym wywoływaniu „call_user_func_array(array($this, ‚parent::__construct’), $args);” następuje uruchomienie autoloadera i w moim przypadku skanowanie plików w poszukiwaniu klasy „parent”. Gdyby nie związane z tym wydłużenie obsługi requesta zapewne nie zauważyłbym nawet wywołanie autoloadera gdyż pomimo, że funkcja autoloadera zwraca false kod wykonuje się prawidłowo nie generując żadnych błędów lub ostrzeżeń.

    Nie mniej koniecznym okazało się spatchowanie autoloadera

    function autoload($class) {
    	if ($class == 'parent') {
    		return false;
    	}
    	// ...
    }
    spl_autoload_register('autoload');
    Podziel się z innymi!

      1 Comment

      1. Adam

        Dzięki. Robiłem dokładnie to samo. Tzn, mniej więcej w czasie pisania tego posta robiłem klasę do złudzenia przypominającą PresistantObject, z dokładnie identyczną funkcją w konstruktorze. Aktualnie pracuję nad debuggerem, testuję go na różnych próbkach kodu – i okazuje się że mój konstruktor wyrzucał wspomniany E_STRICT… Tak więc wkleiłem żywcem rozwiązanie powyższe i… Oczywiście klops, autoloader się wysypał, ale używam autoloadera wewnętrznego SPL, nie chcę go nadpisywać ze względu na wydajność… Więc można albo darować sobie opcję E_STRICT (jeśli zwraca takie bogusy), albo:

            if (version_compare(PHP_VERSION, '5.3.0', '>')) {
              call_user_func_array('parent::__construct', $args);
            } else {
              $er = error_reporting();
              error_reporting($er ^ E_STRICT);
              call_user_func_array(array('parent', '__construct'), $args);
              error_reporting($er);
            }

        Trochę ekstra pisania, można np wyłączanie E_STRICT-a wyrzucić do osobnej funkcji, nadpisać call_user_func_array() solidniejszą (ale wolniejszą) wersją. Albo darować sobie E_STRICT-a. W sumie jest sens włączać go tylko raz – przy ostatnim teście przed wersją produkcyjną. Dla kosmetycznych poprawek. Do normalnego, codziennego debugowania, zwłaszcza PHP 5.2, wystarczy E_ALL, a nawet E_ALL ^ E_NOTICE – bo jeszcze w życiu nie udało mi się znaleźć buga z tym związanego. Dopiero E_WARNING sygnalizuje realny problem (najczęściej brak tablicy dla foreach albo innej funkcji tablicowej)

        Można jeszcze dodać @ przed call_user_func_array() – ale to samo zło, bo jak konstruktor się wyłoży, to kod się sypnie białym ekranem, względnie coś zacznie się krzaczyć nie wiedzieć czemu.

        Aktualnie pracuję nad czymś związanym – debugowanie wyjścia requesta AJAX-owego, które odpala kod przez kilka pośrednich wywołań, z czego jedno jest w osobnym procesie, żeby przechwycić parse i fatal errory. Request odpala całą serię skryptów użytkownika, z których każdy ma prawo się wysypać – ale nie może to mieć wpływu na pozostałe, ponadto główny kod musi wiedzieć, który skrypt się wysypał, w której linii i dlaczego. I uwierz, że tego się nie dało inaczej zrobić. Na StackOverflow czytałem, że takiego czegoś nie da się debuggować – więc mam wyzwanie 🙂

      Dodaj komentarz

      Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *