This repository has been archived on 2024-10-25. You can view files and clone it, but cannot push or open issues or pull requests.
Sequenzia/Rails/ActionController/Base.php
2013-10-02 01:12:58 -05:00

808 lines
25 KiB
PHP
Executable File

<?php
namespace Rails\ActionController;
use stdClass;
use Closure;
use ReflectionClass;
use ReflectionMethod;
use ReflectionException;
use ApplicationController;
use Rails;
use Rails\ActionController\ActionController;
use Rails\ActionController\ExceptionHandler;
use Rails\ActionView;
use Rails\Routing\Traits\NamedPathAwareTrait;
use Rails\Routing\UrlFor;
/**
* This class is expected to be extended by the ApplicationController
* class, and that one is expected to be extended by the current controller's class.
* There can't be other extensions for now as they might cause
* unexpected results.
*
* ApplicationController class will be instantiated so its _init()
* method is called, because it is supposed to be called always, regardless
* the actual controller.
*/
abstract class Base extends ActionController
{
use NamedPathAwareTrait;
const CONTENT_TYPE_JSON = 'application/json';
const CONTENT_TYPE_XML = 'application/xml';
const DEFAULT_REDIRECT_STATUS = 302;
const APP_CONTROLLER_CLASS = 'ApplicationController';
static private $RENDER_OPTIONS = [
'action','template', 'lambda', 'partial', 'json', 'xml', 'text', 'inline', 'file', 'nothing'
];
/**
* @see respondWith()
*/
private $respondTo = [];
private $layout;
private $actionRan = false;
# Must not include charset.
private $contentType;
private $charset;
private $status = 200;
private
/**
* Variables set to the controller itself will be stored
* here through __set(), and will be passed to the view.
*
* @var stdClass
* @see vars()
*/
$locals,
$_array_names = [],
/**
* Extra actions to run according to request format.
*
* This accepts the following params:
* Null: Nothing has been set (default).
* True: Can respond to format, no action needed.
* Closure: Can respond to format, run Closure.
* False: Can't respond to format. Render Nothing with 406.
*/
$_respond_action,
/**
* Default variable to respond with.
*
* @see respond_with()
*/
$_respond_with = [],
/**
* Stores the render parameters.
*
* @see render_params()
* @see _render()
* @see _redirect_to()
* @see _set_response_params()
*/
$_response_params = [],
$redirect_params,
$render_params,
$_response_extra_params = [];
/**
* ReflectionClass for ApplicationController.
*/
private $appControllerRefls = [];
private $selfRefl;
/**
* Children classes shouldn't override __construct(),
* they should override init() instead.
*
* The classes "ApplicationController" are practically abstract classes.
* Some methods declared on them (init() and filters()) will be bound to the
* actual controller class and executed.
* This will happen with any class called "ApplicationController"
* under any namespace.
*/
public function __construct()
{
$class = get_called_class();
if (!$this->isAppController($class)) {
$this->locals = new stdClass();
$this->selfRefl = new ReflectionClass($class);
if (!$this instanceof ExceptionHandler) {
$this->_set_default_layout();
$reflection = $this->selfRefl;
while (true) {
$parent = $reflection->getParentClass();
if ($this->isAppController($parent->getName())) {
$this->appControllerRefls[] = $parent;
} elseif ($parent->getName() == __CLASS__) {
break;
}
$reflection = $parent;
}
$this->appControllerRefls = array_reverse($this->appControllerRefls);
$this->run_initializers();
}
}
}
public function __set($prop, $val)
{
$this->setLocal($prop, $val);
}
public function __get($prop)
{
if (!array_key_exists($prop, (array)$this->locals)) {
throw new Exception\RuntimeException(
sprintf("Trying to get undefined local '%s'", $prop)
);
}
return $this->locals->$prop;
}
public function __call($method, $params)
{
if ($this->isNamedPathMethod($method)) {
return $this->getNamedPath($method, $params);
}
throw new Exception\BadMethodCallException(
sprintf("Called to unknown method: %s", $method)
);
}
public function response_params()
{
return $this->_response_params;
}
public function responded()
{
return $this->render_params || $this->redirect_params;
}
public function setLocal($name, $value)
{
if (is_array($value)) {
$this->_array_names[] = $name;
$this->$name = $value;
} else {
$this->locals->$name = $value;
}
return $this;
}
public function locals()
{
foreach ($this->_array_names as $prop_name)
$this->locals->$prop_name = $this->$prop_name;
return $this->locals;
}
public function vars()
{
return $this->locals();
}
/**
* Shortcut
*/
public function I18n()
{
return Rails::services()->get('i18n');
}
public function action_name()
{
return Rails::services()->get('inflector')->camelize(Rails::application()->dispatcher()->router()->route()->action, true);
}
public function run_request_action()
{
$action = $this->action_name();
if (!$this->_action_method_exists($action) && !$this->_view_file_exists()) {
throw new Exception\UnknownActionException(
sprintf("The action '%s' could not be found for %s", $action, get_called_class())
);
}
$this->runFilters('before');
/**
* Check if response params where set by the
* before filters.
*/
if (!$this->responded()) {
if ($this->_action_method_exists($action)) {
$this->actionRan = true;
$this->runAction($action);
}
$this->runFilters('after');
}
if ($this->redirect_params) {
$this->_set_redirection();
} else {
$this->_parse_response_params();
$this->_create_response_body();
}
}
public function actionRan()
{
return $this->actionRan;
}
public function status()
{
return $this->status;
}
/**
* This method was created so it can be extended in
* the controllers with the solely purpose of
* customizing the handle of Exceptions.
*/
protected function runAction($action)
{
$this->$action();
}
protected function init()
{
}
/**
* Adds view helpers to the load list.
* Accepts one or many strings.
*/
public function helper()
{
ActionView\ViewHelpers::addAppHelpers(func_get_args());
}
/**
* Respond to format
*
* Sets to which formats the action will respond.
* It accepts a list of methods that will be called if the
* request matches the format.
*
* Example:
* $this->respondTo(array(
* 'html',
* 'xml' => array(
* '_some_method' => array($param_1, $param_2),
* '_render' => array(array('xml' => $obj), array('status' => 403))
* )
* ));
*
* Note: The way this function receives its parameters is because we can't use Closures,
* due to the protected visibility of the methods such as _render().
*
* In the case above, it's stated that the action is able to respond to html and xml.
* In the case of an html request, no further action is needed to respond; therefore,
* we just list the 'html' format there.
* In the case of an xml request, the controller will call _some_method($param_1, $param_2)
* then it will call the _render() method, giving it the variable with which it will respond
* (an ActiveRecord_Base object, an array, etc), and setting the status to 403.
* Any request with a format not specified here will be responded with a 406 HTTP status code.
*
* By default all requests respond to xml, json and html. If the action receives a json
* request for example, but no data is set to respond with, the dispatcher will look for
* the .$format.php file in the views (in this case, .json.php). If the file is missing,
* which actually is expected to happen, a Dispatcher_TemplateMissing exception will be
* thrown.
*
* @see respond_with()
*/
public function respondTo($responses)
{
$format = $this->request()->format();
foreach ($responses as $fmt => $action) {
if (is_int($fmt)) {
$fmt = $action;
$action = null;
}
if ($fmt !== $format)
continue;
if ($action) {
if (!$action instanceof Closure) {
throw new Exception\InvalidArgumentException(sprinft('Only closure can be passed to respondTo, %s passed', gettype($action)));
}
$action();
} else {
$action = true;
}
$this->_respond_action = $action;
return;
}
/**
* The request format is not acceptable.
* Set to render nothing with 406 status.
*/
Rails::log()->message("406 Not Acceptable");
$this->render(array('nothing' => true), array('status' => 406));
}
public function respondWith($var)
{
$format = $this->request()->format();
if (!in_array($format, $this->respondTo)) {
/**
* The request format is not acceptable.
* Set to render nothing with 406 status.
*/
$this->render(array('nothing' => true), array('status' => 406));
} else {
$responses = [];
foreach ($this->respondTo as $format) {
if ($format == 'html') {
$responses[] = 'html';
} else {
$responses[$format] = function() use ($var, $format) {
$this->render([$format => $var]);
};
}
}
$this->respondTo($responses);
}
}
/**
* Sets layout value.
*/
public function layout($value = null)
{
if (null === $value)
return $this->layout;
else
$this->layout = $value;
}
public function setLayout($value)
{
$this->layout = $value;
}
protected function setRespondTo(array $respondTo)
{
$this->respondTo = $respondTo;
}
/**
* @return array
*/
protected function filters()
{
return [];
}
protected function render($render_params)
{
if ($this->responded()) {
throw new Exception\DoubleRenderException('Can only render or redirect once per action.');
}
$this->render_params = $render_params;
}
/**
* Sets a redirection.
* Accepts ['status' => $http_status] among the $redirect_params.
*/
protected function redirectTo($redirect_params)
{
if ($this->responded()) {
throw new Exception\DoubleRenderException('Can only render or redirect once per action.');
}
$this->redirect_params = $redirect_params;
}
/**
* For now we're only expecting one controller that extends ApplicationController,
* that extends ActionController_Base.
* This could change in the future (using Reflections) so there could be more classes
* extending down to ApplicationController > ActionController_Base
*/
protected function runFilters($type)
{
$closures = $this->getAppControllersMethod('filters');
if ($closures) {
$filters = [];
foreach ($closures as $closure) {
$filters = array_merge_recursive($closure(), $filters);
}
$filters = array_merge_recursive($filters, $this->filters());
} else {
$filters = $this->filters();
}
if (isset($filters[$type])) {
/**
* We have to filter duped methods. We can't use array_unique
* because the the methods could be like 'method_name' => [ 'only' => [ actions ... ] ]
* and that will generate "Array to string conversion" error.
*/
$ranMethods = [];
foreach ($filters[$type] as $methodName => $params) {
if (!is_array($params)) {
$methodName = $params;
$params = [];
}
if ($this->canRunFilterMethod($params, $type) && !in_array($methodName, $ranMethods)) {
$this->$methodName();
/**
* Before-filters may set response params. Running filters stop if one of them does.
*/
if ($type == 'before' && $this->responded()) {
break;
}
$ranMethods[] = $methodName;
}
}
}
}
protected function setStatus($status)
{
$this->status = $status;
}
private function _action_method_exists($action)
{
$method_exists = false;
$called_class = get_called_class();
$refl = new ReflectionClass($called_class);
if ($refl->hasMethod($action)) {
$method = $refl->getMethod($action);
if ($method->getDeclaringClass()->getName() == $called_class && $method->isPublic())
$method_exists = true;
}
return $method_exists;
}
public function urlFor($params)
{
$urlfor = new UrlFor($params);
return $urlfor->url();
}
/**
* If the method for the requested action doesn't exist in
* the controller, it's checked if the view file exists.
*/
private function _view_file_exists()
{
$route = Rails::application()->dispatcher()->router()->route();
/**
* Build a simple path for the view file, as there's no support for
* stuff like modules.
* Note that the extension is PHP, there's no support for different
* request formats.
*/
$base_path = Rails::config()->paths->views;
$view_path = $base_path . '/' . $route->path() . '.php';
return is_file($view_path);
}
private function _set_default_layout()
{
$this->layout(Rails::application()->config()->action_view->layout);
}
private function canRunFilterMethod(array $params = [], $filter_type)
{
$action = Rails::services()->get('inflector')->camelize($this->request()->action(), false);
if (isset($params['only']) && !in_array($action, $params['only'])) {
return false;
} elseif (isset($params['except']) && in_array($action, $params['except'])) {
return false;
}
return true;
}
private function _parse_response_params()
{
if ($this->render_params) {
if (is_string($this->render_params)) {
$this->render_params = [ $this->render_params ];
}
if (isset($this->render_params[0])) {
$param = $this->render_params[0];
unset($this->render_params[0]);
if ($param == 'nothing') {
$this->render_params['nothing'] = true;
} elseif (is_int(($pos = strpos($param, '/'))) && $pos > 0) {
$this->render_params['template'] = $param;
} elseif ($pos === 0 || strpos($param, ':') === 1) {
$this->render_params['file'] = $param;
if (!isset($this->render_params['layout'])) {
$this->render_params['layout'] = false;
}
} else {
$this->render_params['action'] = $param;
}
}
// else {
// }
}
// else {
// $route = Rails::application()->dispatcher()->router()->route();
// $this->render_params = ['action' => $route->action];
// }
}
/*
* Protected so it can be accessed by ExceptionHandler
*/
protected function _create_response_body()
{
$route = Rails::application()->dispatcher()->router()->route();
$render_type = false;
foreach (self::$RENDER_OPTIONS as $option) {
if (isset($this->render_params[$option])) {
$render_type = $option;
$main_param = $this->render_params[$option];
unset($this->render_params[$option]);
}
}
if (!$render_type) {
$render_type = 'action';
$main_param = $route->action;
}
// $render_type = key($this->render_params);
// $main_param = array_shift($this->render_params);
if (isset($this->render_params['status']))
$this->status = $this->render_params['status'];
$class = null;
switch ($render_type) {
case 'action':
# Cut the 'Controller' part of the class name.
$path = Rails::services()->get('inflector')->underscore(substr(get_called_class(), 0, -10)) . '/' . $main_param . '.php';
// if ($route->namespaces())
// $path = implode('/', $route->namespaces()) . '/' . $path;
$main_param = $path;
# Fallthrough
case 'template':
$layout = !empty($this->render_params['layout']) ? $this->render_params['layout'] : $this->layout;
$ext = pathinfo($main_param, PATHINFO_EXTENSION);
if (!$ext) {
$template_name = $main_param;
if ($this->request()->format() == 'html') {
$ext = 'php';
} else {
if ($this->request()->format() == 'xml')
$this->_response_params['is_xml'] = true;
$ext = [$this->request()->format(), 'php'];
$this->response()->headers()->contentType($this->request()->format());
}
} else {
$pinfo = pathinfo($main_param);
if ($sub_ext = pathinfo($pinfo['filename'], PATHINFO_EXTENSION)) {
$pinfo['filename'] = substr($pinfo['filename'], 0, -1 * (1 + strlen($pinfo['filename'])));
$ext = [$sub_ext, $ext];
} else {
}
$template_name = $pinfo['dirname'] . '/' . $pinfo['filename'];
}
$this->_response_params = [
'layout' => $layout,
'template_name' => $template_name,
'extension' => $ext
];
# Here we could choose a different responder according to extensions(?).
$class = 'Rails\ActionController\Response\Template';
break;
case 'lambda':
$class = 'Rails\ActionController\Response\Lambda';
$this->_response_params['lambda'] = $main_param;
break;
case 'partial':
$class = 'Rails\ActionController\Response\Partial';
$this->_response_params['partial'] = $main_param;
break;
case 'json':
$this->contentType = self::CONTENT_TYPE_JSON;
$this->_response_params = $main_param;
$class = "Rails\ActionController\Response\Json";
break;
case 'xml':
$this->contentType = self::CONTENT_TYPE_XML;
array_unshift($this->_response_params, $main_param);
$class = "Rails\ActionController\Response\Xml";
break;
case 'text':
$this->response()->body($main_param);
break;
case 'inline':
$this->_response_params['code'] = $main_param;
$this->_response_params['layout'] = $this->layout;
$class = "Rails\ActionController\Response\Inline";
break;
case 'file':
$this->_response_params['file'] = $main_param;
$this->_response_params['layout'] = $this->layout ?: false;
$class = "Rails\ActionController\Response\File";
break;
case 'nothing':
break;
default:
throw new Exception\RuntimeException(sprintf("Invalid action render type '%s'", $render_type));
break;
}
if ($class) {
$responder = new $class($this->_response_params);
$responder->render_view();
$this->response()->body($responder->get_contents());
}
$this->setHeaders();
}
private function _set_redirection()
{
$redirect_params = $this->redirect_params;
if (!is_array($redirect_params))
$redirect_params = [$redirect_params];
if (!empty($redirect_params['status'])) {
$status = $redirect_params['status'];
unset($redirect_params['status']);
} else {
$status = self::DEFAULT_REDIRECT_STATUS;
}
$url = Rails::application()->router()->urlFor($redirect_params);
$this->response()->headers()->location($url);
$this->response()->headers()->status($status);
}
/**
* Runs initializers for both the actual controller class
* and it's parent ApplicationController, if any.
*/
private function run_initializers()
{
$method_name = 'init';
$cn = get_called_class();
# Run ApplicationController's init method.
if ($inits = $this->getAppControllersMethod($method_name)) {
foreach ($inits as $init) {
$init = $init->bindTo($this);
$init();
}
}
$method = $this->selfRefl->getMethod($method_name);
if ($method->getDeclaringClass()->getName() == $cn) {
$this->$method_name();
}
}
/**
* Searches through all the ApplicationControllers classes for a method,
* and returns them all.
*
* @return array
*/
private function getAppControllersMethod($methodName, $scope = '')
{
if ($this->appControllerRefls) {
$methods = [];
foreach ($this->appControllerRefls as $appRefl) {
if ($appRefl->hasMethod($methodName)) {
$method = $appRefl->getMethod($methodName);
if ($this->isAppController($method->getDeclaringClass()->getName())) {
if ($scope) {
$isScope = 'is' . ucfirst($scope);
if (!$method->$isScope()) {
continue;
}
}
$methods[] = $method->getClosure($this);
}
}
}
if ($methods) {
return $methods;
}
}
return false;
}
private function setHeaders()
{
$headers = $this->response()->headers();
if (null === $this->charset)
$this->charset = Rails::application()->config()->action_controller->base->default_charset;
if (!$headers->contentType()) {
if (null === $this->contentType) {
$this->contentType = 'text/html';
}
$contentType = $this->contentType;
if ($this->charset)
$contentType .= '; charset=' . $this->charset;
$headers->setContentType($contentType);
}
$this->response()->headers()->status($this->status);
}
private function isAppController($class)
{
return strpos($class, self::APP_CONTROLLER_CLASS) === (strlen($class) - strlen(self::APP_CONTROLLER_CLASS));
}
}