summaryrefslogtreecommitdiff
path: root/src/Router.php
blob: ab0c37c190b431a43590fefc40ec8c0edc3ad511 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?php

namespace App;

use App\Errors\ErrorResponse;
use App\Errors\Exception;
use App\Singleton;
use App\Support\Logger;
use Matrix\Enums\ErrorCode;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Loader\AttributeClassLoader;
use Symfony\Component\Routing\Loader\AttributeDirectoryLoader;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class Router
{
  use Singleton;

  private RouteCollection $routes;

  public function __construct()
  {
    // load routes
    $loader = new AttributeDirectoryLoader(new FileLocator(), new class() extends AttributeClassLoader {
      protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $attr) {
        $route->setDefault("_controller", [$class->getName(), $method->getName()]);
      }
    });

    $this->routes = $loader->load(__DIR__ . "/Controllers");
  }

  /**
   * match the current url against the routes.
   * also add CORS headers.
   */
  public function run(): Response
  {
    $request = Request::createFromGlobals();

    $response = new Response();
    $corsHeaders = [
      "Access-Control-Allow-Origin" => "*",
      "Access-Control-Allow-Methods" => "GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD",
      "Access-Control-Allow-Headers" => "X-Requested-With, Content-Type, Authorization",
    ];

    // handle OPTIONS
    if ($request->isMethod("OPTIONS")) {
      $response->headers->add($corsHeaders);

      return $response;
    }

    $context = new RequestContext();
    $context->fromRequest($request);

    try {
      $matcher = new UrlMatcher($this->routes, $context);
      $match = $matcher->match(rtrim($request->getPathInfo(), "/"));

      $class = $match["_controller"][0];
      $method = $match["_controller"][1];

      $request->attributes->add(array_diff_key(
        $match,
        array_flip(["_controller", "_route"])
      ));

      Logger::logRequestToFile($request);

      $response = (new $class)->$method($request);
    } catch (Exception $exception) {
      $response = ErrorResponse::fromException($exception);
    } catch (ResourceNotFoundException $exception) {
      $response = new ErrorResponse(ErrorCode::NOT_FOUND, "404", Response::HTTP_NOT_FOUND);
    } catch (MethodNotAllowedException $exception) {
      $response = new ErrorResponse(ErrorCode::FORBIDDEN, "403", Response::HTTP_FORBIDDEN);
    } catch (\LogicException $exception) { // display logic exceptions normally
      throw $exception;
    } catch (\Exception $exception) {
      $response = new ErrorResponse(
        ErrorCode::UNKNOWN,
        $exception->getMessage() ?: "Unknown error occured",
        Response::HTTP_INTERNAL_SERVER_ERROR
      );
    } catch (\Error $error) {
      error_log($error->getMessage() ?: "Unknown error occured");

      $response = new ErrorResponse(
        ErrorCode::UNKNOWN,
        $error->getMessage() ?: "Unknown error occured",
        Response::HTTP_INTERNAL_SERVER_ERROR
      );
    }

    // add cors headers to all responses
    $response->headers->add($corsHeaders);

    return $response;
  }
}