Creating an MVC router
This implementation of the Front Controller Pattern has knowledge over too many concerns. The Single Responsibility Principal states that a class should only have one reason to change. From what I can see if any of the following things change, this class will be affectetd:


The location of controller class files
The name of the controller classes
How controller classes are instantiated
The pattern used to match URLs
How view files are loaded
How controller methods are invoked
Which views correspond to which methods on a controller


All of these concerns mixed into one class means it is near impossible to unit test a very critical part of your application.

To be honest, if this is a small application it isn't too bad. If your application grows in size and complexity this will quickly show its limits.

The answer?


Split each concern into it's own class (Single Responsibility Principle)
Have each class interact with the others through an interface (Interface Segregation Principle)
Pass these objects to each other through their constructors (Dependency Inversion Principle)


Going with a minimum of 4 classes and interfaces gives you a lot more flexibility for your application to grow.

First, the interfaces:

interface IUrlRouter {
    function getRoute(Request $request) : UrlRoute;
    function getErrorRoute() : UrlRoute;
}

interface IControllerFactory {
    function createController($controllerName);
}

interface IActionResultExecutor {
    function executeResult($result);
}


Then your "front controller" really just becomes "the application" which handles the Big Picture steps of the process:

class MvcApplication {
    private $controllerFactory;
    private $resultExecutor;
    private $router;

    public function __construct(IUrlRouter $router, IControllerFactory $controllerFactory, IActionResultExecutor $resultExecutor) {
        $this-&gt;router = $router;
        $this-&gt;controllerFactory = $controllerFactory;
        $this-&gt;resultExecutor = $resultExecutor;
    }

    public function handleRequest(Request $request) {
        try {
            $route = $this-&gt;router-&gt;getRoute($request);
            $controllerName = $route-&gt;getControllerName();
            $actionName = $route-&gt;getActionName();
            $controller = $this-&gt;controllerFactory-&gt;createController($route-&gt;getControllerName());
            $result = $controller-&gt;$actionName($route-&gt;getParams());

            $this-&gt;resultExecutor-&gt;executeResult($result);
        }
        catch (Exception $ex) {
            $route = $this-&gt;router-&gt;getErrorRoute();
            $controller = $this-&gt;controllerFactory-&gt;createController($route-&gt;getControllerName());
            $controller-&gt;error($request, $ex);
        }
    }
}


And then just a smidgen of boiler plate code to kick start the process:

$app = new MvcApplication(new UrlRouter(), new ControllerFactory(), new ActionResultExecutor());
$request = new Request($_SESSION, $_SERVER, $_GET, $_POST);
$app-&gt;handleRequest($request);


And this doesn't even touch on user input validation, or resolving a view and then rendering it.