Issues (3)

src/App.php (2 issues)

1
<?php
2
/*
3
 * This file is part of the Scrawler package.
4
 *
5
 * (c) Pranjal Pandey <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Scrawler;
14
15
use Scrawler\Router\Router;
16
17
/**
18
 * @method \PHLAK\Config\Config    config()
19
 * @method \Scrawler\Http\Request  request()
20
 * @method \Scrawler\Http\Response response()
21
 * @method \Scrawler\Pipeline      pipeline()
22
 */
23
class App
24
{
25
    use Traits\Container;
26
    use Traits\Router;
27
28
    public static ?App $app = null;
29
30
    private Router $router;
31
32
    private \DI\Container $container;
33
34
    /**
35
     * @var array<\Closure|callable>
36
     */
37
    private array $handler = [];
38
39
    private string $version = '2.x';
40
41
    public function __construct()
42
    {
43
        self::$app = $this;
44
        $this->router = new Router();
45
        $this->container = new \DI\Container();
46
        $this->register('config', value: $this->create(\PHLAK\Config\Config::class));
47
        $this->register('pipeline', value: $this->create(Pipeline::class));
48
        $this->config()->set('debug', false);
49
        $this->config()->set('api', false);
50
        $this->config()->set('middlewares', []);
51
52
        $this->handler('404', function (): array|string {
53
            if ($this->config()->get('api')) {
54
                return ['status' => 404, 'msg' => '404 Not Found'];
55
            }
56
57
            return '404 Not Found';
58
        });
59
        $this->handler('405', function (): array|string {
60
            if ($this->config()->get('api')) {
61
                return ['status' => 405, 'msg' => '405 Method Not Allowed'];
62
            }
63
64
            return '405 Method Not Allowed';
65
        });
66
        $this->handler('500', function (): array|string {
67
            if ($this->config()->get('api')) {
68
                return ['status' => 500, 'msg' => '500 Internal Server Error'];
69
            }
70
71
            return '500 Internal Server Error';
72
        });
73
    }
74
75
    public static function engine(): self
76
    {
77
        if (null == self::$app) {
78
            self::$app = new self();
79
        }
80
81
        return self::$app;
82
    }
83
84
    /**
85
     * Register a new service in the container.
86
     */
87
    public function container(): \DI\Container
88
    {
89
        return $this->container;
90
    }
91
92
    /**
93
     * Register a new handler in scrawler
94
     * currently uselful hadlers are 404,405,500 and exception.
95
     */
96
    public function handler(string $name, \Closure|callable $callback): void
97
    {
98
        //@codeCoverageIgnoreStart
99
        $callback = \Closure::fromCallable(callback: $callback);
100
        if ('exception' === $name) {
101
            set_error_handler($callback);
102
            set_exception_handler($callback);
103
        }
104
        //@codeCoverageIgnoreEnd
105
        $this->handler[$name] = $callback;
106
    }
107
108
    /**
109
     * Get the handler by key.
110
     */
111
    public function getHandler(string $key): \Closure|callable
112
    {
113
        return $this->handler[$key];
114
    }
115
116
    /**
117
     * Dispatch the request to the router and create response.
118
     */
119
    public function dispatch(?Http\Request $request = null): Http\Response
120
    {
121
        if (is_null($request)) {
122
            $request = $this->request();
123
        }
124
        $pipeline = new Pipeline();
125
126
        return $pipeline->middleware($this->config()->get('middlewares'))->run($request, fn ($request): \Scrawler\Http\Response => $this->dispatchRouter($request));
127
    }
128
129
    /**
130
     * Dispatch the request to the router and create response.
131
     */
132
    private function dispatchRouter(Http\Request $request): Http\Response
133
    {
134
        $httpMethod = $request->getMethod();
135
        $uri = $request->getPathInfo();
136
        $response = $this->makeResponse('', 200);
137
138
        try {
139
            [$status, $handler, $args, $debug] = $this->router->dispatch($httpMethod, $uri);
140
            switch ($status) {
141
                case Router::NOT_FOUND:
142
                    $response = $this->handleNotFound($debug);
143
                    break;
144
                case Router::METHOD_NOT_ALLOWED:
145
                    $response = $this->handleMethodNotAllowed($debug);
146
                    break;
147
                case Router::FOUND:
148
                    // call the handler
149
                    $response = $this->container->call($handler, $args);
150
                    $response = $this->makeResponse($response, 200);
151
                    // Send Response
152
            }
153
        } catch (\Exception $e) {
154
            if ($this->config()->get('debug', false)) {
155
                throw $e;
156
            } else {
157
                $response = $this->container->call($this->handler['500']);
158
                $response = $this->makeResponse($response, 500);
159
            }
160
        }
161
162
        return $response;
163
    }
164
165
    /**
166
     * Handle 404 error.
167
     *
168
     * @throws Exception\NotFoundException
169
     */
170
    private function handleNotFound(string $debug): Http\Response
171
    {
172
        if ($this->config()->get('debug')) {
173
            throw new Exception\NotFoundException($debug);
174
        }
175
        $response = $this->container->call($this->handler['404']);
176
177
        return $this->makeResponse($response, 404);
178
    }
179
180
    /**
181
     * Handle 405 error.
182
     *
183
     * @throws Exception\MethodNotAllowedException
184
     */
185
    private function handleMethodNotAllowed(string $debug): Http\Response
186
    {
187
        if ($this->config()->get('debug')) {
188
            throw new Exception\MethodNotAllowedException($debug);
189
        }
190
        $response = $this->container->call($this->handler['405']);
191
192
        return $this->makeResponse($response, 405);
193
    }
194
195
    /**
196
     * Dipatch request and send response on screen.
197
     */
198
    public function run(): void
199
    {
200
        $response = $this->dispatch();
201
        $response->send();
202
    }
203
204
    /**
205
     * Builds response object from content.
206
     *
207
     * @param array<string,mixed>|string|\Scrawler\Http\Response $content
208
     */
209
    private function makeResponse(array|string|Http\Response $content, int $status = 200): Http\Response
210
    {
211
        if (!$content instanceof Http\Response) {
0 ignored issues
show
$content is never a sub-type of Scrawler\Http\Response.
Loading history...
212
            $response = new Http\Response();
213
            $response->setStatusCode($status);
214
215
            if (is_array($content)) {
0 ignored issues
show
The condition is_array($content) is always true.
Loading history...
216
                $this->config()->set('api', true);
217
                $response->json($content);
218
            } elseif ($this->config()->get('api')) {
219
                $response->json($content);
220
            } else {
221
                $response->setContent($content);
222
            }
223
        } else {
224
            $response = $content;
225
        }
226
227
        return $response;
228
    }
229
230
    /**
231
     * Magic method to call container functions.
232
     *
233
     * @param array<mixed> $args
234
     */
235
    public function __call(string $function, mixed $args): mixed
236
    {
237
        try {
238
            if (!$this->container->has($function) && function_exists($function)) {
239
                return $this->container->call($function, $args);
240
            }
241
242
            return $this->container->get($function);
243
        } catch (\DI\NotFoundException $e) {
244
            throw new Exception\ContainerException($e->getMessage());
245
        }
246
    }
247
248
    /**
249
     * Add middleware(s).
250
     *
251
     * @param \Closure|callable|array<callable>|string $middlewares
252
     */
253
    public function middleware(\Closure|callable|array|string $middlewares): void
254
    {
255
        $this->config()->append('middlewares', $middlewares);
256
        $middlewares = $this->pipeline()->validateMiddleware(middlewares: $this->config()->get('middlewares'));
257
        $this->config()->set('middlewares', $middlewares);
258
    }
259
260
    /**
261
     * Get the build version of scrawler.
262
     */
263
    public function getVersion(): string
264
    {
265
        return $this->version;
266
    }
267
}
268