sformisano/jetski-router

View on GitHub
src/RequestDispatcher.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * Takes an HTTP Request's data and tries to match it to a route.
 * If a match is found it runs the matching route's handler and returns the 
 * output in a format decided either by config or request type.
 * 
 * @package    JetRouter
 * @subpackage Router
 */

namespace JetRouter;

class RequestDispatcher
{

  /*** CONST ***/

  /**
   * The status code returned when a route is not dispatched,
   * used to allow WordPress to take routing over.
   */
  const NOT_DISPATCHED = 0;


  /*** STATIC PROPERTIES ***/

  public static $outputFormats = ['auto', 'html', 'json'];


  /*** STATIC METHODS ***/

  /**
   * Determines if the passed parameter is a respond_to array.
   * 
   * The respond_to array is a feature of the route handler, allowing to determine
   * different behaviour and output for html and json. For json one can simply
   * return data in json format, whereas html runs the callback and does whatever
   * it needs to do (e.g. redirects etc.).
   * 
   * This is inspired by the Ruby on Rails respond_to block feature.
   *
   * @param      <type>   $output  The output
   *
   * @return     boolean  True if respond to, False otherwise.
   */
  private static function isRespondTo($output)
  {
    return (
      is_array($output) && 
      array_key_exists('respond_to', $output)
    );
  }


  /*** PROPERTIES ***/

  private $outputFormat;


  /*** PUBLIC METHODS ***/

  /**
   * Initializes the object and sets the output format
   *
   * @param  string  $outputFormat  The output format
   */
  public function __construct($outputFormat)
  {
    $this->outputFormat = $this->parseOutputFormat($outputFormat);
  }

  /**
   * Dispatches the HTTP request or returns a status code if no routes matches
   *
   * Tries to find a route in the route store by http method and request path.
   * Returns a NOT_DISPATCHED status code if no route matches.
   *
   * If a match is found, the route handler is called and the appropriate output
   * is printed/returned depending on request type and config.
   *
   * @param  RouteStore  $routeStore         The route store object
   * @param  string      $requestHttpMethod  The request http method
   * @param  string      $requestPath        The request path
   *
   * @return integer|mixed|string|mixed  Returns the RouteStore::NOT_FOUND status code (int), mixed if it prints json or returning $output, and finally if $output is a callback returns its returned value
   */
  public function dispatch($routeStore, $requestHttpMethod, $requestPath)
  {
    $route = $routeStore->findRouteByRequestMethodAndPath($requestHttpMethod, $requestPath);

    if($route === RouteStore::NOT_FOUND){
      // route does not exist, fallback to WordPress routing
      return self::NOT_DISPATCHED;
    }

    $output = $this->parseHandlerOutput(
      call_user_func_array($route['routeHandler'], $route['routeArgs'])
    );

    if( $this->shouldReturnJson() ){
      return wp_send_json($output);
    }

    if( is_callable($output) ){
      return call_user_func($output);
    }

    return $output;
  } 


  /*** PRIVATE METHODS ***/

  /**
   * Parses the output format parameter.
   * 
   * Makes sure the output format parameter has one of the predefined values set
   * in the $outputFormats static property set in this class.
   *
   * @param   string  $outputFormat  The output format
   *
   * @throws  Exception\InvalidOutputFormatException  If the output format is invalid
   *
   * @return  string The output format value
   */
  private function parseOutputFormat($outputFormat)
  {
    if( ! in_array($outputFormat, self::$outputFormats, true) ){
      throw new Exception\InvalidOutputFormatException(
        "'$outputFormat' is not a valid output format.",
        1
      );
    }

    return $outputFormat;
  }

  /**
   * Parses and returns the route handler output.
   * 
   * Checks that the output is a respond_to array. If it's not it simply returns 
   * the output as it was passed in.
   * 
   * If it is, it validates it and then it returns the appropriate output format
   * depending on config or request type.
   *
   * @param  mixed  $output  The output of the route handler
   *
   * @return mixed  The parsed output of the route handler
   */
  private function parseHandlerOutput($output)
  {
    if( ! self::isRespondTo($output) ){
      return $output;
    }

    $this->validateRespondTo($output['respond_to']);
    $type = $this->shouldReturnJson() ? 'json' : 'html';

    return $output['respond_to'][$type];
  }

  /**
   * Determines if the route handler respond_to block should return the json data
   * 
   * Looks for a "json" get parameter, if it's missing it checks for conditions
   * that indicate the request we're handling is an ajax request, or finally it
   * simply looks for the output format not being set to html.
   * 
   * If any of these condition is met it returns true
   *
   * @return  boolean  The should return json boolean
   */
  private function shouldReturnJson()
  {
    return (
      isset($_GET['json']) ||
      $this->outputFormat === 'json' ||
      (
        ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && 
        strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' &&
        $this->outputFormat !== 'html'
      )
    );
  }

  /**
   * Validates the respond_to array
   *
   * @param  array  $respondTo  The respond_to array
   *
   * @throws Exception\InvalidRouteHandlerException  If the respond_to is not an array, or missing the 'json' or 'html' keys, or if 'html' property is not callable
   */
  private function validateRespondTo($respondTo)
  {
    if( ! is_array($respondTo) ){
      throw new Exception\InvalidRouteHandlerException(
        'The "respond_to" handler output property must be an array.'
      );
    }

    if( ! array_key_exists('json', $respondTo) ){
      throw new Exception\InvalidRouteHandlerException(
        'Missing json output from respond_to route handler.'
      );
    }

    if( ! array_key_exists('html', $respondTo) ){
      throw new Exception\InvalidRouteHandlerException(
        'Missing html callback from respond_to route handler.'
      );
    }

    if( ! is_callable($respondTo['html']) ){
      throw new Exception\InvalidRouteHandlerException(
        "The html property of the respond_to route handler needs to be callable."
      );
    }
  }

}