Search code examples
phpmodel-view-controllerregistryrouter

Getting router parameters using the Registry pattern


I have this Router.php at the core of my application.

Router.php

<?php

final class Router
{
    protected $routes = [];
    protected $params = [];

    public function add($route, $params = [])
    {
        $route = preg_replace('/\//', '\\/', $route);
        $route = preg_replace('/\{([a-z]+)\}/', '(?P<\1>[a-z-]+)', $route);
        $route = preg_replace('/\{([a-z]+):([^\}]+)\}/', '(?P<\1>\2)', $route);
        $route = '/^' . $route . '$/i';

        $this->routes[$route] = $params;
    }

    public function getRoutes()
    {
        return $this->routes;
    }

    public function match($url)
    {
        foreach ($this->routes as $route => $params) {
            if (preg_match($route, $url, $matches)) {
                foreach ($matches as $key => $match) {
                    if (is_string($key)) {
                        $params[$key] = $match;
                    }
                }

                $this->params = $params;
                return true;
            }
        }

        return false;
    }

    public function getParams()
    {
        return $this->params;
    }

    public function dispatch($url)
    {
        $url = $this->removeQueryStringVariables($url);

        if ($this->match($url)) {
            $controller = $this->params['controller'];
            $controller = $this->convertToStudlyCaps($controller);
            $controller = $this->getNamespace() . $controller;

            if (class_exists($controller)) {
                $controller_object = new $controller($this->params);
                $action = $this->params['action'];
                $action = $this->convertToCamelCase($action);

                if (is_callable([$controller_object, $action])) {
                    $controller_object->$action();

                } else {
                    echo "Method $action (in controller $controller) not found";
                }
            } else {
                echo "Controller class $controller not found";
            }
        } else {
            echo 'No route matched.';
        }
    }

    protected function convertToStudlyCaps($string)
    {
        return str_replace(' ', '', ucwords(str_replace('-', ' ', $string)));
    }

    protected function convertToCamelCase($string)
    {
        return lcfirst($this->convertToStudlyCaps($string));
    }

    protected function removeQueryStringVariables($url)
    {
        if ($url != '') {
            $parts = explode('&', $url, 2);

            if (strpos($parts[0], '=') === false) {
                $url = $parts[0];
            } else {
                $url = '';
            }
        }

        return $url;
    }

    protected function getNamespace()
    {
        $namespace = 'catalog\controller\\';

        if (array_key_exists('namespace', $this->params)) {
            $namespace .= $this->params['namespace'] . '\\';
        }

        return $namespace;
    }
}

To implement a central storage for objects, I have implemented this registry pattern, which is at the core of the structure.

Registry.php

<?php
final class Registry
{
    private $data = array();

    public function get($key)
    {
        return (isset($this->data[$key]) ? $this->data[$key] : null);
    }

    public function set($key, $value)
    {
        $this->data[$key] = $value;
    }

    public function has($key)
    {
        return isset($this->data[$key]);
    }
}

The base/core controller further has $registry at its construct function.

CoreController.php

<?php
abstract class CoreController
{
    protected $registry;

    public function __construct($registry)
    {
        $this->registry = $registry;
    }

    public function __get($key)
    {
        return $this->registry->get($key);
    }

    public function __set($key, $value)
    {
        $this->registry->set($key, $value);
    }
}

The CoreController is extended by all app controller to inherit the properties.

Posts.php

<?php
class Posts extends CoreController
{
    public function index() {
        echo 'Hello from the index action in the posts controller';
    }

    public function addNew() {
        echo 'Hello from the addNew action in the posts controller';
    }

    public function edit() {
        echo '<p>Route parameters: <pre>'.var_dump($this->registry).'</pre></p>';
    }
}

To instantiate the registry and router this is the what is in the

index.php

<?php
// Instantiate registry
$registry = new \system\core\Registry();

// Database
$db = new DB(DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_DATABASE);
$registry->set('db', $db);


$router = new \system\core\Router();
$registry->set('router', $router);


// Add the routes
$router->add('', ['controller'=>'HomeController', 'action'=>'index']);
$router->add('posts', ['controller'=>'posts', 'action'=>'index']);
//$router->add('posts/new', ['controller'=>'posts', 'action'=>'new']);
$router->add('{controller}/{action}');
$router->add('{controller}/{id:\d+}/{action}');
$router->add('admin/{controller}/{action}');

$router->dispatch($_SERVER['QUERY_STRING']);

After the url http://localhost/mvcsix/posts/1235/edit this is what is displayed

enter image description here

All this looks good and works fine.

Somehow, this doesn't feel right. I var_dumped $this->registry and I have the parameters of route being displayed but I feel that to get the parameters from the route I should have var_dumped $this->router->getParams(). When I var_dump $this->router->getParams(), I get an error which says

Fatal error: Call to a member function get() on array in

I say this because I have the database object in the registry too and to get the query to display I do $result = $this->db->query("SELECT * FROM members");

Why do I have the parameters displayed on $this->registry and not on $this->router->getParams(); ?

P.S. the above code is strip down of the original code. There are namespaces and few more things which wasn't necessary for this post.


Solution

  • As alex_edev noticed, you are trying to call get method on an array. But where does it come from?

    What is wrong.

    Posts controller is initialized in the router's method dispatch. The url /posts/1235/edit does match the second route rule, so the following lines are executed

    $controller_object = new $controller($this->params);
    $action = $this->params['action'];
    $action = $this->convertToCamelCase($action);
    

    Pay attention to what is passed to the controller constructor. You pass route params property! Looking at Posts.php, Posts controller extends CoreController, so it expects Registry as a parameter of a constructor, but you pass an array - Route::params property. So it is the wrong object construction that brakes the party.

    Why does it work fine normally.

    Everything works fine without var_dump since you don't call to Posts::__get method. When you call $this->router->getParams() in Posts controller, it tries to get undefined router property with getter and fails due to wrong registry - remember, you injected an array to the controller.

    What should be done

    You should initiate controller this way

    $controller_object = new $controller($this->registry);
    

    where registry is injected in the __construct:

    final class Router
    {
        // add definition
        private $registry;
    
        // pass it to the router
        public function __construct($registry) {
             $this->registry = $registry;
        }
        ....
     }
    

    The router is initiated as follows

    $registry->set('db', $db);
    
    
    $router = new \system\core\Router($registry);
    

    So, you just need to edit 6 lines of code.

    P.S. Use Type declarations to avoid that kind of errors. If you write public function __construct(Registry $registry) php throws a TypeError exception when array is passed.