jeudi 4 février 2021

Using Reflection in Calling Method from URL parameters in MVC PHP

Currently I am working on a simple MVC framework of mine. So my Core library loads the controller and method based on the URL with the given parameters. This happens in the following way:
localhost/application/controller/method/param1/param2

Core: Code for loading controller and method

class Core {
    protected $currentController = "Pages";
    protected $currentMethod = "Index";
    protected $params = [];
    
    public function __construct() {
        $url = $this->getURL();

        // Look in controllers/ for first value
        if(!is_null($url)) {
            if(file_exists("../app/controllers/".ucwords($url[0]).".php")) {
                // Set as Controller
                $this->currentController = ucwords($url[0]);
                // Unset 0th index
                unset($url[0]);
            }
        }

        // Require Controller
        require_once '../app/controllers/'.$this->currentController.'.php';

        // Instantiate Controller
        $this->currentController = new $this->currentController;
        // Check for second part of URL
        if(isset($url[1])) {
            // If method exists in controller
            if(method_exists($this->currentController, $url[1])) {
                $this->currentMethod = $url[1];
                unset($url[1]);
            }
        }
    }
    public function getURL() {
        if(isset($_GET['url'])) {
            $url = rtrim($_GET['url'], '/');
            $url = filter_var($url, FILTER_SANITIZE_URL);
            $url = explode('/', $url);
            return $url;
        }
    }
}

The parameters are an array after exploding the URL. This is done by using call_user_func_array([$controller, $method], $params). To prevent user from passing incorrect parameters in URL I came up with the following solution using Reflection to check whether correct number and types of parameters are passed.

Core

class Core {

    public function __construct(){
      $url = $this->getUrl();
      // After loading correct Model and Controller (and called method)...

      // Get params - Any values left over in url are params 
      $this->params = $url ? array_values($url) : [];
      $this->params = $this->correctURLTypes($this->params);
      
      $r = new ReflectionMethod($this->currentController, $this->currentMethod);
      $requiredArgs = $this->numberOfRequiredArgs(); // Only Required params not optional
      $totalArgs = $r->getNumberOfParameters(); // Total no. of params in method (required + optional)
      $givenArgs = count($this->params);
      if($givenArgs >= $requiredArgs && $givenArgs <= $totalArgs) {
        // get params at index 1, 2, 3 ... $givenArgs
        for ($i = 0; $i < $givenArgs; $i++) { 
          $param = new ReflectionParameter(array($this->currentController, $this->currentMethod), $i);
          $paramType = $param->getType();

          if($paramType == 'string') $this->params[$i] = (string) $this->params[$i]; 
          // cast int/bool/float if expecting string

          $givenType = Utils::typeOf($this->params[$i]); // static function to return type of variable
          
          if(!is_null($paramType) && $paramType != $givenType) { // if type specified and dont match
            die("Types dont match");
          }
        }
        call_user_func_array([$this->currentController, $this->currentMethod], $this->params);
      }  else {
        die("Arg count incorrect");
      }
    }

    public function getUrl(){
        if(isset($_GET['url'])){
          $url = rtrim($_GET['url'], '/');
          $url = filter_var($url, FILTER_SANITIZE_URL);
          $url = explode('/', $url);
          return $url;
        }
    }

    public function correctURLTypes($arr) {
      foreach ($arr as $key => $value) {
        // string, int, float, bool ONLY
        if(is_numeric($value)) {
          if(ctype_digit($value)) {
            $arr[$key] = (int) $value;
          } else {
            $val = $value+0;
            if(is_float($val)) {
              $arr[$key] = (float) $value;
            }
          }
        } else {
          if(in_array($value, ['true', 'false'])) {
            if($value == 'true') 
              $arr[$key] = true;
            else 
              $arr[$key] = false;
          } else {
            $arr[$key] = (string) $value;
          }
        }
      }
      return $arr;
    }

    public function numberOfRequiredArgs() {
      $r = new ReflectionMethod($this->currentController, $this->currentMethod);
      $required = 0;

      foreach ($r->getParameters() as $key => $val) {
        if(!$val->isDefaultValueAvailable()) $required++;
      }

      return $required;
    }
}

So a method can be called which has required type specified and may have optional parameters like: public function show(int $id, bool $edit = true) {

I was unsure if this is a good way of doing it and if its a hack. I wanted feedback and suggestions for loading the methods and parameters in such a way. Complete Core: Core





Aucun commentaire:

Enregistrer un commentaire