Router and solid principle
Router should not accept request object in constructor. You will need a new router for each request.
I know PHP spawns a new process for each request so it gets created for each request anyway. But in tests it may be different...
And anyway it doesn't make much sense to hide the request inside the router from the logical point of view.
Hiding a constructor dependency means that the dependency is an implementation detail. And sure you agree that request is not just an implementation detail of a router. It is the input for routing.
Also the route should not be aware of how to determine controller class and surely it doesn't want to know or, and thats even worse, force no constructor arguments for all controllers. There should be a controller factory that knows the details.
And you don't want to pass controller factory to every route. So maybe instead, route should just define path mapping and controller factory should take a route as an input. Router would then pass the matched route to the controller factory.
Another thing is that the addRoute method doesn't really belong on a router, it belong on a router builder. You register routes on the builder and finalize by calling it's createRouter method.
What's entirely missing in your code is an &quot;application&quot; class - that's the one where run method belongs, not the router, router should really have a method that accepts request object and returns response object. Application might have a request factory, the router and some kind of response emitter which takes the response object returned by router and send it's representation to the client.