4
\$\begingroup\$

I was trying to keep it SRP. I'm new into PHP OOP and I'm wondering how I can make it better.

Route.php

class Route
{
    private $_url;
    private $_callback;
    private $_name;

    public function __construct($_url, $callback)
    {
        $this->_url = '/^' . str_replace('/', '\\/', $_url) . '$/';

        if (is_callable($callback)) {
            $this->_callback = $callback;
            return;
        }

        if (is_string($callback) 
        && substr_count($callback, '@') == 1 
        && file_exists(CONTROLLER_PATH . '/' . explode('@', $callback)[0] . '.php')) {
            $this->_callback = explode('@', $callback);
            require_once CONTROLLER_PATH . '/' . $this->_callback[0] . '.php';
            $this->_callback[0] = new $this->_callback[0];
            return;
        }

        exit('Error!');
    }

    public function getURL()
    {
        return $this->_url;
    }

    public function getCallback()
    {
        return $this->_callback;
    }
}

Router.php

<?php

class Router
{
const GET = 'GET';
const POST = 'POST';

private static $_routes = array(
    'GET' => array(),
    'POST' => array()
);

public static function add(Route $route, $method)
{
    switch ($method) {
        case 'GET':
            self::$_routes['GET'][$route->getURL()] = $route->getCallback();
        break;

        case 'POST':
            self::$_routes['POST'][$route->getURL()] = $route->getCallback();
        break;

        default:
            exit('Error!');
        break;
    }
}

public static function get(Route $route)
{
    self::add($route, self::GET);
}

public static function post(Route $route)
{
    self::add($route, self::POST);
}

public static function run()
{
    $path = array_key_exists('path', $_GET) ? '/' . $_GET['path'] : '/';

    foreach (self::$_routes[$_SERVER['REQUEST_METHOD']] as $url => $callback) {
        if (preg_match($url, $path, $matches)) {
            array_shift($matches);
            call_user_func_array($callback, array_values($matches));
            return;
        }
    }
}
}

controller/HomeController.php

<?php

class HomeController
{
    public function index()
    {
        echo 'Hello World!'
    }
}

Using example

<?php

Router::get(new Route('/', 'HomeController@index'));
Router::run();

?>
\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review / Stack Exchange! Please edit your title to simply state what problem your code is solving; every single question on this site is asking about "code improving" ;-) ...also feel free to include more of the surrounding context code, for example a whole method is easier to review than just an if block taken out of context. See how to get the best value out of Code Review on meta for more info. \$\endgroup\$ – Mathieu Guindon Feb 20 '16 at 2:28
  • \$\begingroup\$ You're edit made the question way better! I hope you'll have good reviews. \$\endgroup\$ – Marc-Andre Feb 20 '16 at 2:57
  • 2
    \$\begingroup\$ Can you add a usage example? I think it's a bit confusing how callback is used, and url isn't used at all, which adds to the confusion. \$\endgroup\$ – tim Feb 20 '16 at 8:41
2
\$\begingroup\$

In general your code is a good start. Keep going :)

However the class Route should be refactored.

Issuelist:

  • Missing setter
  • In the constructor are multiple returns
  • Script is terminated in the Route class
  • The if-conditions are not well-readable

Description:

This section describes missing setter, multiple returns and if-conditions.

In the constructor of the class Route are multiple returns. This makes the code less maintainable. Having more than one return means there are multiple scenarios when the constructor can be stopped. In case of a bug one need to debug through the whole method to figure out the return-point.

So, at this point a switch to if-else-if-else should be performed. Below you see a minimalistic sample.

<?php
if (is_callable($callback))
{
     // simple assignment.
}
else if (is_string())
{
      // parsing, then assigning
 }
 else
 {
      exit();
 }

As you notice it is better readable. From here there are two approaches how the code can be optimized further.

Approach 1:

IsValid-methods should be created. These kind of methods performs a validation.

<?php
public function IsValidCallback($callback)
{
     // Validate $callback
}

The IsValidCallback-method may check is_callable as well.

The constructor looks like that now:

<?php
if ($this->IsValidCallback($callback))
{
      if (is_callable($callback))
      {
          // simple assignment
      }
      else
      {
          // parsing and assigning
      }
 }
 else
 {
       exit();
 }

The code became even better readable and when the validation has to be adjusted it is obvious where to make the changes plus you can validate the Route at any line in your software with the guarantee the validation is up to date.

Approach 2:

Move the assignment to a respective setter and throw an InvalidArgumentException.

<?php
protected function _setCallback($callback)
{
    $callback = (string) $callback;
    $aCallback = explode('@', $callback);

    if (is_callable($callback)) {
        $this->_callback = $callback;
    }
    else if (count($aCallback) == 2 && file_exists(CONTROLLER_PATH . '/' . $aCallback[0] . '.php'))
    {
        require_once CONTROLLER_PATH . '/' . $aCallback[0] . '.php'; // instead of including the class you can make usage of http://php.net/manual/en/function.spl-autoload-register.php
        $this->_callback = new $aCallback[0];
    }
    else
    {
       throw new InvalidArgumentException('$callback is invalid.');
    }
}

public function __constructor(...)
{
       try
        {
            $this->_setCallback($callback);
        }
        catch(InvalidArgumentException $e)
        {
            exit();
        }
 }

Script is terminated in the constructor

The script should not be terminated in a random class but by an ExceptionHandler. The type of the exception decides wether the script has to be terminated. Commonly a FatalException leads to a termination.

Possible Route class

class Route
{
private $_url;
private $_callback;
private $_name;

// REFACTORED
public function __construct($_url, $callback)
{
    $this->_setURL($_url);

    try
    {
        $this->_setCallback($callback);
    }
    catch(InvalidArgumentException $e)
    {
       exit('Error!');
    }
}

// ADDED
protected function _setCallback($callback)
{
    $callback = (string) $callback;
    $aCallback = explode('@', $callback);

    if (is_callable($callback)) {
        $this->_callback = $callback;
    }
    else if (count($aCallback) == 2 && file_exists(CONTROLLER_PATH . '/' . $aCallback[0] . '.php'))
    {
        require_once CONTROLLER_PATH . '/' . $aCallback[0] . '.php'; // instead of including the class you can make usage of http://php.net/manual/en/function.spl-autoload-register.php
        $this->_callback = new $aCallback[0];
    }
    else
    {
        throw new InvalidArgumentException('$callback is invalid.');
    }
}

// ADDED
protected function _setURL($url)
{
    $this->_url = '/^' . str_replace('/', '\\/', $url) . '$/';
}

public function getURL()
{
    return $this->_url;
}

public function getCallback()
{
    return $this->_callback;
}
}
\$\endgroup\$
  • \$\begingroup\$ Thank you so much, I have waited so long for answer like this! :) \$\endgroup\$ – HeuHeu Feb 23 '16 at 1:44
  • 1
    \$\begingroup\$ @HeuHeu I have updated the answer. \$\endgroup\$ – AMartinNo1 Feb 23 '16 at 19:27
  • 1
    \$\begingroup\$ Good to hear. Regarding the second approach: I have returned $blIsValid in an earlier version of the method. In the latest answer it is removed. What do you mean with "Closure"? "Script is terminated in the constructor": You are using the core-method die in the constructor which terminates the software. Sure, going to do this now. \$\endgroup\$ – AMartinNo1 Feb 23 '16 at 22:02
  • 1
    \$\begingroup\$ I am not using an anomyous function in my approaches. I have added a sample route class. Keep in mind that this is not the best solution. You can make usage of an autoloader for example. You can find more about it on php.net Despite of that think about SRP while having a look at your and my code. Reminder: Both a class and a method should solve one task only and it's naming has to describe clearly what it does. \$\endgroup\$ – AMartinNo1 Feb 23 '16 at 22:25
  • 1
    \$\begingroup\$ Okay, everything is clear, thank you a lot! :) \$\endgroup\$ – HeuHeu Feb 23 '16 at 22:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.