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, false); } 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)); } }