atelierspierrot/mvc-fundamental

View on GitHub
src/MVCFundamental/FrontController.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * This file is part of the MVC-Fundamental package.
 *
 * Copyright (c) 2013-2016 Pierre Cassat <me@e-piwi.fr> and contributors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The source code of this package is available online at
 * <http://github.com/atelierspierrot/mvc-fundamental>.
 */

namespace MVCFundamental;

use \MVCFundamental\Interfaces\FrontControllerInterface;
use \MVCFundamental\Interfaces\ResponseInterface;
use \MVCFundamental\Interfaces\RequestInterface;
use \MVCFundamental\Exception\Exception;
use \MVCFundamental\Exception\ErrorException;
use \MVCFundamental\Exception\NotFoundException;
use \MVCFundamental\Exception\InternalServerErrorException;
use \MVCFundamental\Exception\AccessForbiddenException;
use \MVCFundamental\Commons\ServiceContainerProviderTrait;
use \MVCFundamental\Commons\Helper;
use \Patterns\Traits\OptionableTrait;
use \Patterns\Traits\SingletonTrait;
use \Library\Helper\Directory as DirectoryHelper;

/**
 * The default FrontController
 */
class FrontController
    implements FrontControllerInterface
{

    /**
     * This class inherits from \Patterns\Traits\OptionableTrait
     * This class inherits from \Patterns\Traits\SingletonTrait
     * This class inherits from \MVCFundamental\Commons\ServiceContainerProviderTrait
     */
    use OptionableTrait, SingletonTrait, ServiceContainerProviderTrait;

    /**
     * @var array
     */
    protected static $_defaults = array(
        // API
        'router'                    => 'MVCFundamental\Basic\Router',
        'route_item'                => 'MVCFundamental\Basic\Route',
        'response'                  => 'MVCFundamental\Basic\Response',
        'request'                   => 'MVCFundamental\Basic\Request',
        'template_engine'           => 'MVCFundamental\Basic\TemplateEngine',
        'template_item'             => 'MVCFundamental\Basic\Template',
        'layout_item'               => 'MVCFundamental\Basic\Layout',
        'locator'                   => 'MVCFundamental\Basic\Locator',
        'error_controller'          => 'MVCFundamental\Basic\ErrorController',
        'event_manager'             => 'MVCFundamental\Basic\EventManager',
        'event_item'                => 'MVCFundamental\Basic\Event',
        'controller_locator'        => null,
        'view_file_locator'         => null,
        'controller_name_finder'    => '%sController',
        'action_name_finder'        => '%sAction',
        'default_controller_name'   => 'default',
        'default_action_name'       => 'index',
        // defaults
        'default_content_type'      => 'html',
        'default_charset'           => 'utf8',
        'routes'                    => array(),
        'default_template'          => 'default.php',
        'default_layout'            => 'layout.php',
        'default_layout_class'      => 'MVCFundamental\Commons\DefaultLayout',
        // default error messages
        '500_error_info'            => 'An internal error occurred :(',
        '404_error_info'            => 'The requested page cannot be found :(',
        '403_error_info'            => 'Access to this page is forbidden :(',
        // app dev
        'mode'                      => 'production', // dev , test , production
        'convert_error_to_exception'=> false,
        'minimum_log_level'         => null, // one of the \Library\Logger levels
        'app_logger'                => 'Library\Logger',
    );

// -------------------------------
// Constructors
// -------------------------------

    /**
     * @var bool
     */
    protected static $_is_booted = false;

    /**
     * @param array $options
     */
    public function __construct(array $options = array())
    {
        $this
            ->setOptions(self::$_defaults)
            ->setOptions($options)
        ;
    }

    /**
     * @return $this
     */
    public function boot()
    {
        if (!self::$_is_booted) {
            if (empty($this->_options['temp_dir'])) {
                $this->_options['temp_dir'] =
                    DirectoryHelper::slashDirname(dirname($_SERVER['SCRIPT_FILENAME'])).'tmp';
            }

            if ($this->isMode('production')) {
                $this->setOption('convert_error_to_exception', true);
            }

            AppKernel::boot($this);

            $charset = $this->get('response')->getCharset();
            if (empty($charset)) {
                $this->get('response')->setCharset($this->getOption('default_charset'));
            }
            $content_type = $this->get('response')->getContentType();
            if (empty($content_type)) {
                $this->get('response')->setContentType($this->getOption('default_content_type'));
            }

            $routes = $this->getOption('routes');
            if (!empty($routes)) {
                $this->get('router')->setRoutes($routes);
            }

            self::$_is_booted = true;
            $this->trigger('boot');
        }
        return $this;
    }

    /**
     * @param string $mode
     * @return bool
     */
    public function isMode($mode)
    {
        if (!isset($this->_options['mode']) || !in_array($this->_options['mode'], array('dev', 'test', 'production'))) {
            $this->_options['mode'] = 'production';
        }
        return (strtolower($mode)==$this->_options['mode']);
    }

// -------------------------------
// OptionableInterface
// -------------------------------

    /**
     * @param   array   $options
     * @return  $this
     */
    public function setOptions(array $options)
    {
        $this->_options = array_merge($this->_options, $options);
        return $this;
    }

// ---------------------------
// FrontControllerInterface
// ---------------------------

    /**
     * @param   \MVCFundamental\Interfaces\RequestInterface $request
     * @return  void
     * @throws  \MVCFundamental\Exception\Exception
     * @throws  \MVCFundamental\Exception\NotFoundException
     */
    public function run(RequestInterface $request = null)
    {
        $this->boot();
        $response = $this->handle($request);
        $this->send($response);
        AppKernel::terminate($this);
    }

    /**
     * @param   \MVCFundamental\Interfaces\RequestInterface $request
     * @return  \MVCFundamental\Interfaces\ResponseInterface
     */
    public function handle(RequestInterface $request = null)
    {
        $this->boot();
        if (is_null($request)) {
            $request = $this->get('request');
        }
        $arguments  = $request->getArguments();
        $response   = $this->callRoute(
            $request->getUri(),
            !empty($arguments) ? $arguments : array(),
            $request->getMethod()
        );
        if (is_string($response)) {
            $this->get('response')->addContent($response);
            $response = $this->get('response');
        }
        return $response;
    }

    /**
     * @param   \MVCFundamental\Interfaces\ResponseInterface $response
     * @return  void
     */
    public function send(ResponseInterface $response = null)
    {
        $this->boot();
        if (is_null($response)) {
            $response = $this->get('response');
        }
        $response->send();
    }

    /**
     * @param   string  $route
     * @param   mixed   $callback
     * @param   string  $method
     * @return  $this
     */
    public function addRoute($route, $callback, $method = 'get')
    {
        $this->boot();
        $class = $this->getOption('route_item');
        $this->get('router')->addRoute(new $class($route, $callback, $method));
        return $this;
    }

    /**
     * @param   string  $view_file
     * @param   array   $params
     * @param   string  $type
     * @return  string
     */
    public function render($view_file, array $params = array(), $type = 'global')
    {
        $this->boot();
        return $this->get('template_engine')->renderTemplate($view_file, $params);
    }

    /**
     * @param   string  $route
     * @param   array   $arguments
     * @param   string  $method
     * @return  string
     * @throws  \MVCFundamental\Exception\Exception
     * @throws  \MVCFundamental\Exception\InternalServerErrorException
     * @throws  \MVCFundamental\Exception\NotFoundException
     */
    public function callRoute($route, array $arguments = array(), $method = 'get')
    {
        $this->boot();

        @list($callback, $params) = $this->get('router')->distribute(
            $route, $arguments, $method
        );

/*/
header('Content-Type: text/plain');
echo "route is: ".var_export($route,1).PHP_EOL;
echo "callback is: ".var_export($callback,1).PHP_EOL;
echo "arguments are: ".var_export($arguments,1).PHP_EOL;
exit('-- out --');
//*/

        if (empty($callback)) {
            throw new NotFoundException(
                sprintf('Route "%s" not found!', $route)
            );
        }

        if (!empty($params)) {
            $arguments = array_merge($arguments, $params);
        }

        // callable callback
        if (is_callable($callback)) {
            $return = Helper::fetchArguments($callback, $arguments);
            if (!empty($return) && is_string($return)) {
                return $return;
            }

        // array like ( controller , method )
        } elseif (is_array($callback)) {
            $ctrl_name = $this->get('locator')->locateController($callback[0]);
            if (empty($ctrl_name)) {
                throw new Exception(
                    sprintf('Unknown controller "%s"!', $callback[0])
                );
            }

            $action_name = $this->get('locator')->locateControllerAction($callback[1], $ctrl_name);
            if (empty($action_name)) {
                throw new Exception(
                    sprintf('Unknown controller\'s action "%s" (in controller "%s")!', $callback[1], $ctrl_name)
                );
            }

            return $this->callControllerAction($ctrl_name, $action_name, $arguments);

        // string
        } elseif (is_string($callback)) {

            // controller name
            $ctrl_name = $this->get('locator')->locateController($callback);
            if (!empty($ctrl_name)) {
                $default_action_name    = $this->getOption('default_action_name');
                $action_name            = $this->get('locator')->locateControllerAction($default_action_name, $ctrl_name);
                if (empty($action_name)) {
                    throw new Exception(
                        sprintf('Unknown controller\'s default action "%s" (in controller "%s")!', $default_action_name, $ctrl_name)
                    );
                }
                return $this->callControllerAction($ctrl_name, $action_name, $arguments);
            } else {

                // default controller method
                $default_ctrl_name  = $this->getOption('default_controller_name');
                $ctrl_name          = $this->get('locator')->locateController($default_ctrl_name);
                $action_name        = $this->get('locator')->locateControllerAction($callback, $ctrl_name);
                if (!empty($action_name)) {
                    return $this->callControllerAction($ctrl_name, $action_name, $arguments);
                } else {

                    // view file path
                    $view_file = $this->get('template_engine')->getTemplate($callback);
                    if (!empty($view_file)) {
                        return $this->render($view_file, $arguments);
                    }
                }
            }
        }

        throw new NotFoundException('Route result not understood!');
    }

    /**
     * @param   null|string     $controller
     * @param   null|string     $action
     * @param   array           $arguments
     * @return  string
     * @throws  \MVCFundamental\Exception\InternalServerErrorException
     */
    public function callControllerAction($controller = null, $action = null, array $arguments = array())
    {
        $this->boot();
        $controller_name = $controller;

        if (empty($controller)) {
            $controller = $this->getOption('default_controller_name');
        }
        if (empty($action)) {
            $action = $this->getOption('default_action_name');
        }
        if (!empty($controller)) {
            if (!is_object($controller)) {
                $ctrl = $this->get('locator')->locateController($controller);
                if (!empty($ctrl)) {
                    $controller = new $ctrl();
                } else {
                    throw new InternalServerErrorException(
                        sprintf('Unknown controller "%s"!', $controller_name)
                    );
                }
            }

            $arguments  = array_merge(
                Helper::getDefaultEnvParameters(), $arguments
            );

            $action = $this->get('locator')->locateControllerAction($action, $controller);
            if (!method_exists($controller, $action) || !is_callable(array($controller, $action))) {
                throw new InternalServerErrorException(
                    sprintf('Action "%s" in controller "%s" is not known or not callable!', $action, $controller_name)
                );
            }
            $result = Helper::fetchArguments(
                $action, $arguments, $controller
            );

            // result as a raw content: a string
            if (is_string($result)) {
                return $result;

            // result as an array like ( view_file , params )
            } elseif (is_array($result)) {
                $view_file = $this->get('template_engine')->getTemplate($result[0]);
                if (!empty($view_file)) {
                    return $this->render(
                        $view_file,
                        isset($result[1]) && is_array($result[1]) ? array_merge($arguments, $result[1]) : $arguments
                    );
                }

            // a reponse object
            } elseif (is_object($result) && ($ri = AppKernel::getApi('response')) && ($result instanceof $ri)) {
                $this->set('response', $result);
            }
        }

        return '';
    }

    /**
     * @param   string  $message
     * @param   int     $status
     * @param   int     $code
     * @param   string  $filename
     * @param   int     $lineno
     * @return  void
     * @throws  \MVCFundamental\Exception\AccessForbiddenException
     * @throws  \MVCFundamental\Exception\ErrorException
     * @throws  \MVCFundamental\Exception\InternalServerErrorException
     * @throws  \MVCFundamental\Exception\NotFoundException
     */
    public function error($message, $status = 500, $code = 0, $filename = __FILE__, $lineno = __LINE__)
    {
        $this->boot();
        switch ($status) {
            case 500:
                throw new InternalServerErrorException($message, $code, 1, $code, $filename, $lineno);
                break;
            case 404:
                throw new NotFoundException($message, $code);
                break;
            case 403:
                throw new AccessForbiddenException($message, $code);
                break;
            default:
                throw new ErrorException($message, $code, 1, $code, $filename, $lineno);
                break;
        }
    }

    /**
     * @param   string  $url
     * @param   bool    $follow
     * @return  void
     */
    public function redirect($url, $follow = false)
    {
        $this->boot();
        $base_url = $this->get('request')->getBaseUrl();
        $url = $base_url.str_replace($base_url, '', $url);
        if ($follow) {
            $this->get('response')->redirect($url);
        } else {
            $req_cls = get_class($this->get('request'));
            $this->set('request', new $req_cls($url, 'get'));
            $this->run();
        }
    }

    /**
     * @param $event
     * @param $callback
     * @throws \Exception
     * @return $this
     */
    public function on($event, $callback)
    {
        try {
            $this
                ->boot()
                ->get('event_manager')->addListener($event, $callback);
        } catch (\Exception $e) {
            throw $e;
        }
        return $this;
    }

    /**
     * @param $event
     * @param $callback
     * @throws \Exception
     * @return $this
     */
    public function off($event, $callback)
    {
        try {
            $this
                ->boot()
                ->get('event_manager')->removeListener($event, $callback);
        } catch (\Exception $e) {
            throw $e;
        }
        return $this;
    }

    /**
     * @param string $event
     * @param mixed $subject
     * @throws \Exception
     * @return $this
     */
    public function trigger($event, $subject = null)
    {
        try {
            $this
                ->boot()
                ->get('event_manager')->triggerEvent($event, $subject);
        } catch (\Exception $e) {
            throw $e;
        }
        return $this;
    }
}