Dispatch::__construct()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
c 1
b 0
f 0
nc 4
nop 5
dl 0
loc 28
rs 9.8333
1
<?php
2
3
namespace alkemann\h2l;
4
5
use alkemann\h2l\exceptions\InvalidUrl;
6
use alkemann\h2l\exceptions\NoRouteSetError;
7
use alkemann\h2l\util\Chain;
8
9
/**
10
 * @package alkemann\h2l
11
 */
12
class Dispatch
13
{
14
    /**
15
     * @var array<callable>
16
     */
17
    private array $middlewares = [];
18
19
    /**
20
     * @var Request
21
     */
22
    protected Request $request;
23
24
    /**
25
     * Analyze request, provided $_REQUEST, $_SERVER [, $_GET, $_POST] to identify Route
26
     *
27
     * Response type can be set from HTTP_ACCEPT header
28
     * Call setRoute or setRouteFromRouter to set a route
29
     *
30
     * @param array<string, mixed> $request $_REQUEST
31
     * @param array<string, mixed> $server $_SERVER
32
     * @param array<string, mixed> $get $_GET
33
     * @param array<string, mixed> $post $_POST
34
     * @param null|interfaces\Session $session if null, a default Session with $_SESSION will be created
35
     */
36
    public function __construct(
37
        array $request = [],
38
        array $server = [],
39
        array $get = [],
40
        array $post = [],
41
        interfaces\Session $session = null
42
    ) {
43
        unset($get['url']); // @TODO Use a less important keyword, as it blocks that _GET param?
44
45
        if (is_null($session)) {
46
            $session = new Session();
47
        }
48
49
        $request = (new Request())
50
            ->withRequestParams($request)
51
            ->withServerParams($server)
52
            ->withGetData($get)
53
            ->withPostData($post)
54
            ->withSession($session)
55
        ;
56
57
        $body = file_get_contents('php://input');
58
        if (is_string($body)) {
0 ignored issues
show
introduced by
The condition is_string($body) is always true.
Loading history...
59
            // @TODO Always do this? Can we know from $_SERVER or $_REQUEST ?
60
            $request = $request->withBody($body);
61
        }
62
63
        $this->request = $request;
64
    }
65
66
    /**
67
     * Tries to identify route by configured static, dynamic or fallback configurations.
68
     *
69
     * First a string match on url is attempted, then fallback to see if Page is configured
70
     * with a 'content_path'. Then checks if Router has a fallback callback configured.
71
     * Return false if all 3 fails to set a route.
72
     *
73
     * @param string $router class name to use for Router matching
74
     * @return bool true if a route is set on the Request
75
     */
76
    public function setRouteFromRouter(string $router = Router::class): bool
77
    {
78
        /**
79
         * matching dynamic or static routes
80
         * @var Router $router
81
         */
82
        $matched = $router::match($this->request->url(), $this->request->method());
83
        if ($matched) {
84
            $this->request = $this->request->withRoute($matched);
85
            return true;
86
        }
87
        // If content path is configured, enable automatic Page responses
88
        if (Environment::get('content_path')) {
89
            $route = Router::getPageRoute($this->request->url());
90
            $this->request = $this->request->withRoute($route);
91
            return true;
92
        }
93
94
        $fallback = $router::getFallback();
95
        if ($fallback) {
96
            $this->request = $this->request->withRoute($fallback);
97
            return true;
98
        }
99
100
        return false;
101
    }
102
103
    /**
104
     * @return interfaces\Route|null Route identified for request if set
105
     */
106
    public function route(): ?interfaces\Route
107
    {
108
        return $this->request->route();
109
    }
110
111
    /**
112
     * Recreates the `Request` with the specified `Route`. `Request` may be created as side effect.
113
     *
114
     * @param interfaces\Route $route
115
     */
116
    public function setRoute(interfaces\Route $route): void
117
    {
118
        $this->request = $this->request->withRoute($route);
119
    }
120
121
    /**
122
     * Execute the Route's callback and return the Response object that the callback is required to return
123
     *
124
     * Catches InvalidUrl exceptions and returns a response\Error with 404 instead
125
     *
126
     * @return Response|null
127
     * @throws NoRouteSetError if a route has not been set prior to calling this, or by a middleware
128
     */
129
    public function response(): ?Response
130
    {
131
        $cbs = $this->middlewares;
132
        $call_eventual_route_at_end_of_chain = static function(Request $request): ?Response {
133
            $route = $request->route();
134
            if (is_null($route)) {
135
                if (Environment::get('debug')) {
136
                    throw new NoRouteSetError("Response called without Route set");
137
                }
138
                return null;
139
            }
140
            try {
141
                return $route($request);
142
            } catch (InvalidUrl $e) {
143
                // @TODO Backwards breaking, but remove this?
144
                return new response\Error(
145
                    ['message' => $e->getMessage()],
146
                    ['code' => 404, 'request' => $request]
147
                );
148
            }
149
        };
150
        array_push($cbs, $call_eventual_route_at_end_of_chain);
151
        $response = (new Chain($cbs))->next($this->request);
152
        return ($response instanceof Response) ? $response : null;
153
    }
154
155
    /**
156
     * Add a closure to wrap the Route callback in to be called during Request::response
157
     *
158
     * @param callable ...$cbs
159
     */
160
    public function registerMiddle(callable ...$cbs): void
161
    {
162
        foreach ($cbs as $cb) {
163
            $this->middlewares[] = $cb;
164
        }
165
    }
166
}
167