poniedziałek, 25 maja 2020

Analiza Symfony - HttpKernel::handleRaw()

private function handleRaw(Request $request, int $type = self::MASTER_REQUEST): Response
{
    $this->requestStack->push($request);

    // request
    $event = new RequestEvent($this, $request, $type);
    $this->dispatcher->dispatch($event, KernelEvents::REQUEST);

    if ($event->hasResponse()) {
        return $this->filterResponse($event->getResponse(), $request, $type);
    }

    // load controller
    if (false === $controller = $this->resolver->getController($request)) {
        throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
    }

    $event = new ControllerEvent($this, $controller, $request, $type);
    $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
    $controller = $event->getController();

    // controller arguments
    $arguments = $this->argumentResolver->getArguments($request, $controller);

    $event = new ControllerArgumentsEvent($this, $controller, $arguments, $request, $type);
    $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
    $controller = $event->getController();
    $arguments = $event->getArguments();

    // call controller
    $response = $controller(...$arguments);

    // view
    if (!$response instanceof Response) {
        $event = new ViewEvent($this, $request, $type, $response);
        $this->dispatcher->dispatch($event, KernelEvents::VIEW);

        if ($event->hasResponse()) {
            $response = $event->getResponse();
        } else {
            $msg = sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this->varToString($response));

            // the user may have forgotten to return something
            if (null === $response) {
                $msg .= ' Did you forget to add a return statement somewhere in your controller?';
            }

            throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17);
        }
    }

    return $this->filterResponse($response, $request, $type);
}
Zależności klasy HttpKernel do innych komponentów Symfony


1. Zgłoszenie zdarzenia 


    Metoda przyjmuje jako pierwszy parametr obiekt żądania klasy Request pochodzącej z komponentu HttpFoundation, zwracając przy tym obiekt Response z tego samego komponentu.

    Parametr Request w pierwszej kolejności wrzucany jest na stos innych żądań, który jest instancją klasy HttpFoundation\RequestStack

    Następnie zgłaszane jest zdarzenie żądania za pomocą wstrzykniętego do konstruktora klasy HttpKernel,  obiektu implementującego EventDispatcherInterface pochodzącego z zewnętrznego komponentu EventDispatcher. Zdarzenie symbolizowane jest przez instancją klasy HttpKernel\Event\RequestEvent, a powiązana z nim nazwa zdarzenia to kernel.request. W przypadku gdy do aktualnego żądania istnieje powiązana odpowiedź, metoda w tym momencie ją zwróci.


2. Dobieranie kontrolera


    Za to zadanie odpowiedzialny jest obiekt ControllerResolverInterface (Constructor Injection) będący częścią komponentu jądra. Metoda getController przyjmuje jako parametr obiekt Request i w przypadku powodzenia operacji zwraca zmienną typu callable (akcja kontrolera) bądź - gdy nie zostanie sparowany odpowiedni kontroler - bool'owski false.  W klasie konkretnej resolver'a ControllerResolver,  znajdują się szczegóły implementacyjne dotyczące doboru kontrolera, zagadnienie to wymaga rozszerzenia w oddzielnym wpisie. W przypadku gdy:

  • nie dojdzie do sparowania kontrolera z obiektem żądania, wyrzucony zostanie wyjątek HttpKernel\Exception\NotFoundHttpException
  • \InvalidArgumentException będzie wyrzucony gdy dojdzie do wewnętrznych problemów resolver'a np. gdy sparowana akcja kontrolera nie będzie typu callable,
    Po pomyślnym wybraniu kontrolera na podstawie obiektu Request, zostanie zgłoszone zdarzenie kernel.controller za pośrednictwem  instancje EventDispatcher przechowywanej w chronionym polu $dispatcher

Ewentualna podmiana kontrolera


    Jako że mamy możliwość nasłuchiwania na zdarzenie kernel.controller (KernelEvents::CONTROLLER)  oraz dostęp do utworzonego obiektu Event\ControllerEvent w user land'owych klasach Listener/Subscriber, który posiada metodę publicznego interfejsu function setController(callable $controller): void, w ciele omawianej metody handleRaw musi znajdować się ewentualne pobranie innej instancji kontrolera:

$event = new ControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
$controller = $event->getController();



3. Pobranie argumentów akcji


    Za to zadanie odpowiedzialny jest wstrzyknięty do konstruktora klasy HttpKernel obiekt implementujący interfejs ArgumentResolverInterface (należący do tego samego komponentu). W tym przypadku jako argumenty należącej do niego metody getArguments, przekazywana jest instancja klasy Request oraz callable $controllerLogika ukryta za tą warstwą abstrakcji nie będzie tutaj opisywana ponieważ zasługuje na osobny wpis - ważne jest, że metoda zwraca tablicę ze wszystkimi zadeklarowanymi w akcji parametrami.

    Następne kroki procesu przebiegają podobnie co w przypadku kontrolera: 

  1. utworzenie obiektu zdarzenia ControllerArgumentsEvent
  2. wysłanie go za pośrednictwem EventDispatcher'a z przypisaną nazwą kernel.controller_arguments 
  3. pobranie do zmiennych $controller i $arguments prawdopodobnie zmienionych instancji z obiektów typu *Event



4. Wywołanie akcji kontrolera


// call controller
$response = $controller(...$arguments);
   
    Posiadając callable akcji oraz wszystkie wymagane argumenty, nic nie stoi na przeszkodzie by pozyskać obiekt Response będący zwrotką każdej akcji kontrolera w Symfony. Mogłaby do tego procesu zostać użyta funkcja call_user_func_array() jednak autorzy zdecydowali się na wykorzystanie Operatora Variadic.


5. Zwrócenie obiektu Response


    W przypadku gdy zmienna $response nie przechowuje obiektu klasy Response, emitowane jest zdarzenie kernel.view. W tym miejscu, algorytm oczekuje, że istnieją nasłuchiwacze, które zapewnią obiektowi zdarzenia ViewEvent, pożądaną przez niego instancje klasy Response. Jeżeli takowa nie zostanie wykryta, prawdopodobnie developer nie zwrócił nic w akcji kontrolera, czego konsekwencją będzie wyrzucenie wyjątku ControllerDoesNotReturnResponseException. W procesie implementacji można uchronić się przed tego typu problemem, deklarując typ zwracany przez akcję. Dzięki temu w przypadku ewentualnego pominięcia return'a, zostaniemy o tym poinformowani już podczas kompilacji.

    Gdy mamy dostęp do obiektu Response,  metoda go zwraca i emituje - pośrednio, przez metody prywatne - dwa zdarzenia: kernel.response oraz kernel.finish_request (ściągając przy tym instancje Request ze stosu RequestStack). 


Podsumowanie wszystkich emitowanych zdarzeń w metodzie handleRaw