directory restructured

This commit is contained in:
Parziphal 2013-10-02 11:14:53 -05:00
parent daec4cdf66
commit 723dc3afba
261 changed files with 25726 additions and 5 deletions

View File

@ -9,10 +9,7 @@
},
"autoload": {
"psr-0": {
"Rails\\": ""
"Rails\\": "lib/Rails"
}
},
"config": {
"vendor-dir": "./.."
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Rails\ActionController;
use Rails;
use Rails\ActionController\ExceptionHandler;
use Rails\ActionController\UrlFor;
use Rails\ActionController\Exception;
abstract class ActionController
{
/**
* Helps with _run_filters, which is protected.
*/
static public function run_filters_for(ApplicationController $ctrlr, $type)
{
$ctrlr->run_filters($type);
}
static public function load_exception_handler($class_name, $status)
{
// $path = Rails::config()->paths->controllers;
// $file = $path . '/' . Rails::services()->get('inflector')->underscore($class_name) . '.php';
// if (!is_file($file))
// throw new ActionController_Exception(sprintf("File for exception handler controller not found. Searched for %s", $file));
// require $file;
// if (!class_exists($class_name, false))
// throw new ActionController_Exception(sprintf("Class for exception handler controller not found. File: %s", $file));
$handler = new $class_name();
if (!$handler instanceof ExceptionHandler) {
throw new ActionController_Exception(sprintf("Exception handler class %s must extend %s. File: %s", $class_name, "ActionController\ExceptionHandler", $file));
}
$handler->class_name = $class_name;
$handler->setStatus((int)$status);
return $handler;
}
public function params($name = null, $val = null)
{
if ($name)
$this->_dispatcher()->parameters()->$name = $val;
return $this->_dispatcher()->parameters();
}
public function request()
{
return $this->_dispatcher()->request();
}
public function response()
{
return Rails::application()->dispatcher()->response();
}
public function session()
{
return $this->_dispatcher()->session();
}
/**
* Retrieves Cookies instance, retrieves a cookie or
* sets a cookie.
*/
public function cookies($name = null, $val = null, array $params = array())
{
$num = func_num_args();
if (!$num)
return $this->_dispatcher()->response()->cookies();
elseif ($num == 1)
return $this->_dispatcher()->response()->cookies()->get($name);
else {
if ($val === null)
$this->_dispatcher()->response()->cookies()->delete($name);
else
return $this->_dispatcher()->response()->cookies()->add($name, $val, $params);
}
}
protected function _dispatcher()
{
return Rails::application()->dispatcher();
}
}

View File

@ -0,0 +1,808 @@
<?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));
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Rails\ActionController;
use Rails;
/**
* This class is expected to be used only by Rails_ActionDispatch_CookieJar
*/
class Cookie
{
protected $name;
protected $value;
protected $expire;
protected $path;
protected $domain;
protected $secure = false;
protected $httponly = false;
protected $raw = false;
public function __construct($name, $value, $expire = null, $path = null, $domain = null, $secure = false, $httponly = false, $raw = false)
{
$this->setDefaultValues();
$this->name = (string)$name;
$this->domain = (string)$domain;
$this->value = $value === null ? '' : $value;
$this->expire = $expire;
$this->path = $path;
$this->secure = $secure;
$this->httponly = $httponly;
$this->raw = $raw;
if (!$this->name) {
throw new Exception\InvalidArgumentException('Cookies must have a name');
}
if (preg_match("/[=,; \t\r\n\013\014]/", $this->name)) {
throw new Exception\InvalidArgumentException("Cookie name cannot contain these characters: =,; \\t\\r\\n\\013\\014 ({$this->name})");
}
if (is_string($this->expire)) {
$time = strtotime($this->expire);
if (!$time) {
throw new Exception\InvalidArgumentException(
sprintf("Invalid expiration time: %s", $time)
);
}
$this->expire = $time;
}
if ($this->expire !== null && !is_int($this->expire)) {
throw new Exception\InvalidArgumentException(
sprintf("Cookie expiration time must be an integer, %s passed (%s)", gettype($this->expire), $this->expire)
);
}
if ($this->raw && preg_match("/[=,; \t\r\n\013\014]/", $this->value)) {
throw new Exception\InvalidArgumentException(
sprintf("Raw cookie value cannot contain these characters: =,; \\t\\r\\n\\013\\014 (%s)", $this->value)
);
}
if (!is_scalar($this->value)) {
throw new Exception\InvalidArgumentException(
sprintf("Cookie value must be a scalar value, %s passed", gettype($this->value))
);
}
}
public function value()
{
return $this->value;
}
public function set()
{
if ($this->raw) {
setrawcookie($this->name, $this->value, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly);
} else {
setcookie($this->name, $this->value, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly);
}
}
private function setDefaultValues()
{
$config = Rails::application()->config()->cookies;
foreach ($config as $prop => $val) {
# Silently ignore unknown properties.
if (property_exists($this, $prop)) {
$this->$prop = $val;
}
}
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Rails\ActionController;
/**
* This class will be available in controllers by
* calling '$this->cookies()'.
*
* Cookies can be set through this class. The cookies won't
* be actually sent to the browser until the controller
* finishes its work.
*
* Doing '$cookies->some_cookie' will call __get,
* which checks the $_COOKIE variable. In other words, it will
* check if the request sent a cookie named 'some_cookie'.
* If not, it will return null.
*
* To set a cookie, use add(), to remove them use remove().
* To check if a cookie was set by the controller, use in_jar().
*/
class Cookies
{
/**
* Holds cookies that will be added at the
* end of the controller.
*/
private $jar = array();
/**
* To know if cookies were set or not.
*/
private $cookiesSet = false;
public function __get($name)
{
if (isset($this->jar[$name]))
return $this->jar[$name]->value();
elseif (isset($_COOKIE[$name]))
return $_COOKIE[$name];
else
return null;
}
public function __set($prop, $params)
{
if (is_array($params)) {
if (isset($params['value'])) {
$value = $params['value'];
unset($params['value']);
} else {
$value = '';
}
} else {
$value = $params;
$params = [];
}
$this->add($prop, $value, $params);
}
public function add($name, $value, array $params = array())
{
$p = array_merge($this->defaultParams(), $params);
$this->jar[$name] = new Cookie($name, $value, $p['expires'], $p['path'], $p['domain'], $p['secure'], $p['httponly'], $p['raw']);
return $this;
}
public function delete($name, array $params = [])
{
$this->add($name, '', array_merge($params, [
'expires' => time() - 172800
]));
return $this;
}
/**
* Actually sets cookie in headers.
*/
public function set()
{
if (!$this->cookiesSet) {
foreach ($this->jar as $c)
$c->set();
$this->cookiesSet = true;
}
}
private function defaultParams()
{
$defaults = \Rails::application()->config()->cookies->toArray();
if (!$defaults['path']) {
$defaults['path'] = \Rails::application()->router()->basePath() . '/';
}
return $defaults;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Exception;
class BadMethodCallException extends \Rails\Exception\BadMethodCallException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Exception;
class BodyAlreadySetException extends \Rails\Exception\LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Rails\ActionController\Exception;
class DoubleRenderException extends Rails\Exception\LogicException implements ExceptionInterface
{
protected $title = 'Double Render Error';
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Exception;
class InvalidArgumentException extends \Rails\Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Exception;
class RuntimeException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace Rails\ActionController\Exception;
class UnknownActionException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
protected $title = 'Unknown action';
protected $status = 404;
protected $skip_info = true;
}

View File

@ -0,0 +1,50 @@
<?php
namespace Rails\ActionController;
use Rails;
use Rails\ActionController\Base;
/**
* Basic use:
* Create a class that extends this one.
* According to Exception or status, change the value of $template
* The system will render that template.
*/
abstract class ExceptionHandler extends Base
{
protected $exception;
protected $template = 'exception';
public function handle()
{
switch ($this->status()) {
case 404:
$this->template = '404';
break;
default:
$this->template = '500';
break;
}
}
public function handleException(\Exception $e)
{
$this->exception = $e;
$this->setLayout(false);
$this->runAction("handle");
if (!$this->responded()) {
$this->render(['action' => $this->template]);
}
$this->_create_response_body();
}
public function actionRan()
{
return true;
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Rails\ActionController;
use Rails\ActionDispatch\ActionDispatch;
class Response extends ActionDispatch
{
private
/**
* ActionController_Cookies instance
*/
$cookies,
$body;
/**
* If set to false, cookies and headers won't be send.
*/
protected $send_headers = true;
public function cookies()
{
if (!$this->cookies)
$this->cookies = new Cookies();
return $this->cookies;
}
public function headers()
{
return \Rails::application()->dispatcher()->headers();
}
public function body($body = null)
{
if (null !== $body) {
$this->body = $body;
return $this;
} else {
return $this->body;
}
}
public function sendHeaders($value = null)
{
if ($value === null)
return $this->send_headers;
else {
$this->send_headers = $value;
return $this;
}
}
public function send_headers($value = null)
{
if ($value === null)
return $this->send_headers;
else {
$this->send_headers = $value;
return $this;
}
}
protected function _init()
{
}
protected function _respond()
{
$this->_send_headers();
echo $this->body;
$this->body = null;
}
private function _send_headers()
{
if ($this->send_headers) {
$this->headers()->send();
$this->cookies()->set();
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Rails\ActionController\Response;
use Rails\ActionController;
abstract class Base extends ActionController\Response
{
public function __construct(array $params = array())
{
$this->_params = $params;
}
public function render_view()
{
$this->_render_view();
return $this;
}
public function get_contents()
{
return $this->_print_view();
}
abstract public function _render_view();
abstract public function _print_view();
}

View File

@ -0,0 +1,94 @@
<?php
namespace Rails\ActionController\Response;
/**
* This class also logs the errors.
*/
class Error extends Base
{
private
$_e,
$_buffer = '',
$_report;
public function __construct(\Exception $e, array $params)
{
$this->_params = $params;
$this->_e = $e;
}
public function _render_view()
{
$buffer = '';
$this->_report = $this->_params['report'];
unset($this->_params['report']);
if (\Rails::application()->config()->consider_all_requests_local) {
$no_html = \Rails::cli();
if ($no_html) {
$buffer .= strip_tags($this->_report);
$buffer .= "\n";
} else {
$buffer .= $this->_header();
$buffer .= $this->_report;
$buffer .= $this->_footer();
}
} else {
$file = \Rails::publicPath() . '/' . $this->_params['status'] . '.html';
if (is_file($file)) {
$buffer = file_get_contents($file);
}
}
$this->_buffer = $buffer;
}
public function _print_view()
{
return $this->_buffer;
}
private function _header()
{
$h = <<<HEREDOC
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Exception caught</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: helvetica, verdana, arial, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
overflow: auto;
}
pre.scroll {
max-height:400px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
</style>
</head>
<body>
HEREDOC;
return $h;
}
private function _footer()
{
return "</body>\n</html>";
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Response\Exception;
class ActionNotFoundException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionController\Response\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Rails\ActionController\Response\Exception;
class ViewNotFoundException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
protected $title = "View not found";
}

View File

@ -0,0 +1,28 @@
<?php
namespace Rails\ActionController\Response;
use Rails\ActionView;
# TODO
class File extends Base
{
private $_template;
public function _render_view()
{
# Include helpers.
ActionView\ViewHelpers::load();
$layout = !empty($this->_params['layout']) ? $this->_params['layout'] : false;
$this->_template = new ActionView\Template($this->_params['file'], ['layout' => $layout]);
$this->_template->setLocals(\Rails::application()->controller()->vars());
$this->_template->renderContent();
}
public function _print_view()
{
return $this->_template->content();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Rails\ActionController\Response;
use Rails\ActionView;
# TODO
class Inline extends Base
{
private $_template;
public function _render_view()
{
# Include helpers.
ActionView\ViewHelpers::load();
$layout = !empty($this->_params['layout']) ? $this->_params['layout'] : false;
# Create a template so we can call render_inline;
$this->_template = new ActionView\Template(['inline' => $this->_params['code']], ['layout' => $layout]);
$this->_template->setLocals(Rails::application()->controller()->vars());
$this->_template->renderContent();
}
public function _print_view()
{
return $this->_template->content();
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Rails\ActionController\Response;
class Json extends Base
{
private $_json;
private $_header_params;
public function __construct($json)
{
$this->_json = $json;
}
public function _render_view()
{
if ($this->_json instanceof \Rails\ActiveRecord\Base)
$this->_json = $this->_json->toJson();
elseif ($this->_json instanceof \Rails\ActiveRecord\Collection) {
$this->_json = $this->_json->toJson();
} elseif (isset($this->_json['json'])) {
if (!is_string($this->_json['json']))
$this->_json = json_encode($this->_json['json']);
else
$this->_json = $this->_json['json'];
} elseif (!is_string($this->_json)) {
if (is_array($this->_json)) {
$json = [];
foreach ($this->_json as $key => $val) {
$json[$key] = $this->_to_array($val);
}
$this->_json = $json;
}
$this->_json = json_encode($this->_json);
}
}
public function _print_view()
{
return $this->_json;
}
private function _to_array($val)
{
if ($val instanceof \Rails\ActiveRecord\Collection) {
$json = [];
foreach ($val as $obj)
$json[] = $this->_to_array($obj);
return $json;
} elseif (is_object($val)) {
return (array)$val;
} else
return $val;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Rails\ActionController\Response;
use Rails\ActionView;
# TODO
class Lambda extends Base
{
private $_template;
public function _render_view()
{
# Include helpers.
ActionView\ViewHelpers::load();
$layout = !empty($this->_params['layout']) ? $this->_params['layout'] : false;
$this->_template = new ActionView\Template(['lambda' => $this->_params['lambda']], ['layout' => $layout]);
$this->_template->setLocals(\Rails::application()->controller()->locals());
$this->_template->renderContent();
}
public function _print_view()
{
return $this->_template->content();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rails\ActionController\Response;
use Rails\ActionView;
# TODO
class ActionController_Response_Partial extends Base
{
public function _render_view()
{
$params = [$this->_params['partial']];
if (isset($this->_params['locals']))
$params = array_merge($params, [$this->_params['locals']]);
# Include helpers.
ActionView\ViewHelpers::load();
# Create a template so we can call render_partial.
# This shouldn't be done this way.
$template = new ActionView\Template([]);
$this->_body = call_user_func_array([$template, 'render_partial'], $params);
}
public function _print_view()
{
return $this->_body;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Rails\ActionController\Response;
class ActionController_Response_Redirect extends Base
{
private $_redirect_params;
private $_header_params;
public function __construct(array $redirect_params)
{
// vde($redirect_params);
# Todo: not sure what will be in second index
# for now, only http status.
list($this->_redirect_params, $this->_header_params) = $redirect_params;
}
protected function _render_view()
{
$url = \Rails::application()->router()->urlFor($this->_redirect_params);
\Rails::application()->dispatcher()->response()->headers()->location($url);
}
protected function _print_view()
{
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Rails\ActionController\Response;
use Rails;
use Rails\ActionView;
class Template extends Base
{
private $_xml;
protected $renderer;
protected $template_file_name;
public function _render_view()
{
try {
ActionView\ViewHelpers::load();
$this->build_template_file_name();
$params = [
'layout' => $this->_params['layout']
];
$this->renderer = new ActionView\Template($this->template_file_name, $params);
$locals = Rails::application()->controller()->vars();
if (!empty($this->_params['is_xml'])) {
$this->_xml = new ActionView\Xml();
$locals->xml = $this->_xml;
}
$this->renderer->setLocals($locals);
$this->renderer->renderContent();
} catch (ActionView\Template\Exception\ExceptionInterface $e) {
switch (get_class($e)) {
case 'Rails\ActionView\Template\Exception\TemplateMissingException':
$route = Rails::application()->router()->route();
if (Rails::application()->dispatcher()->controller()->actionRan()) {
throw new Exception\ViewNotFoundException(
sprintf("View file not found: %s", $this->template_file_name)
);
} else {
if ($route->namespaces())
$namespaces = ' [ namespaces => [ ' . implode(', ', $route->namespaces()) . ' ] ]';
else
$namespaces = '';
throw new Exception\ActionNotFoundException(
// sprintf("Action '%s' not found for controller '%s'%s", $route->action(), $route->controller(), $namespaces)
sprintf("Action '%s' not found for controller '%s'%s", $route->action(), $route->controller(), $namespaces)
);
}
break;
// default:
// break;
}
throw $e;
}
}
public function _print_view()
{
// if (!empty($this->_params['is_xml']))
// return $this->_xml->output();
// else
return $this->renderer->get_buffer_and_clean();
}
private function build_template_file_name()
{
$views_path = Rails::config()->paths->views;
if (is_array($this->_params['extension']))
$ext = implode('.', $this->_params['extension']);
else
$ext = $this->_params['extension'];
$this->template_file_name = $views_path . DIRECTORY_SEPARATOR . $this->_params['template_name'] . '.' . $ext;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Rails\ActionController\Response;
use Rails;
use Rails\ActionView;
class View extends Base
{
private $_xml;
public function _render_view()
{
try {
ActionView\ViewHelpers::load();
$this->_renderer = new ActionView\Template($this->_params, $this->_params['layout']);
$locals = Rails::application()->controller()->vars();
if (!empty($this->_params['is_xml'])) {
$this->_xml = new ActionView\Xml();
$locals->xml = $this->_xml;
}
$this->_renderer->setLocals($locals);
$this->_renderer->render_content();
} catch (ActionView\Template\Exception\ExceptionInterface $e) {
switch (get_class($e)) {
case 'Rails\ActionView\Template\Exception\LayoutMissingException':
case 'Rails\ActionView\Template\Exception\TemplateMissingException':
$route = $this->router()->route();
if (Rails::application()->dispatcher()->action_ran()) {
$token = $route->to();
throw new Exception\ViewNotFoundException(
sprintf("View for %s not found", $token)
);
} else {
if ($route->namespaces())
$namespaces = ' [ namespaces => [ ' . implode(', ', $route->namespaces()) . ' ] ]';
else
$namespaces = '';
throw new Exception\ActionNotFoundException(
sprintf("Action '%s' not found for controller '%s'%s", $route->action(), $route->controller(), $namespace)
);
}
break;
default:
throw $e;
break;
}
}
}
public function _print_view()
{
if (!empty($this->_params['is_xml']))
return $this->_xml->output();
else
return $this->_renderer->get_buffer_and_clean();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rails\ActionController\Response;
class Xml extends Base
{
private $_xml;
public function _render_view()
{
$el = array_shift($this->_params);
if ($el instanceof \Rails\ActiveRecord\Collection) {
$this->_xml = new \Rails\ActionView\Xml();
$root = $this->_params['root'];
$this->_xml->instruct();
$this->_xml->$root([], function() use ($el) {
foreach ($el as $model) {
$model->toXml(['builder' => $this->_xml, 'skip_instruct' => true]);
}
});
} else
$this->_xml = new \Rails\Xml\Xml($el, $this->_params);
}
public function _print_view()
{
return $this->_xml->output();
}
}

View File

@ -0,0 +1,143 @@
<?php
namespace Rails\ActionDispatch;
use Rails;
use Rails\ActionController\Response;
use Rails\Routing;
class ActionDispatch
{
/**
* ActionController_Response instance.
*/
private $_response;
private
$_parameters,
$_request,
$_session,
$_headers;
private $_router;
private $_action_ran = false;
private $_action_dispatched = false;
private $_view;
private $_responded = false;
public function init()
{
$this->_response = new Response();
$this->_router = new Routing\Router();
$this->_response->_init();
$this->_headers = new Http\Headers();
$this->load_request_and_params();
}
public function load_request_and_params()
{
if (!$this->_parameters) {
$this->_request = new Request();
$this->_parameters = new Http\Parameters();
$this->_session = new Http\Session();
} else {
throw new Exception\LogicException("Can't call init() more than once");
}
}
public function find_route()
{
$this->_router->find_route();
$this->_route_vars_to_params();
}
public function router()
{
return $this->_router;
}
/**
* This method shouldn't be accessed like Rails::application()->dispatcher()->parameters();
*/
public function parameters()
{
return $this->_parameters;
}
public function request()
{
return $this->_request;
}
public function headers()
{
return $this->_headers;
}
public function controller()
{
return Rails::application()->controller();
}
public function session()
{
return $this->_session;
}
public function response()
{
return $this->_response;
}
public function respond()
{
// if ($this->_responded && !Rails::response_params()) {
if ($this->_responded) {
throw new Exception\LogicException("Can't respond to request more than once");
} else {
$this->_response->_respond();
$this->_responded = true;
}
}
// public function clear_responded()
// {
// static $cleared = false;
// if ($cleared) {
// throw new Rails_ActionDispatch_Exception("Can't clear response more than once");
// } else {
// $cleared = true;
// $this->_responded = false;
// }
// }
private function _route_vars_to_params()
{
$vars = $this->_router->route()->vars();
unset($vars['controller'], $vars['action']);
foreach ($vars as $name => $val) {
if ($this->_parameters->$name === null)
$this->_parameters->$name = $val;
}
}
private function _action_name()
{
return $this->router()->route()->action;
}
private function _action_exists()
{
$controller = Rails::application()->controller();
return method_exists($controller, $this->_action_name()) && is_callable(array($controller, $this->_action_name()));
}
private function _app()
{
return Rails::application();
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionDispatch\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionDispatch\Exception;
class LogicException extends \Rails\Exception\LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionDispatch\Exception;
class RuntimeException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\Http\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\Http\Exception;
class InvalidArgumentException extends \Rails\Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\Http\Exception;
class LogicException extends \Rails\Exception\LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,166 @@
<?php
namespace Rails\ActionDispatch\Http;
use Rails;
class Headers
{
private $headers = array();
private $status = 200;
private $status_sent = false;
private $_content_type;
// public function status()
// {
// return $this->status;
// }
public function status($status = null)
{
if (null === $status) {
return $this->status;
} else {
if (ctype_digit((string)$status))
$this->status = (int)$status;
elseif (is_string($status))
$this->status = $status;
else
throw new Exception\InvalidArgumentException(
sprintf("%s accepts string, %s passed.", __METHOD__, gettype($value))
);
return $this;
}
}
public function location($url, $status = 302)
{
if (!is_string($url))
throw new Exception\InvalidArgumentException(
sprintf("%s accepts string as first parameter, %s passed.", __METHOD__, gettype($value))
);
elseif (!is_int($status) && !is_string($status))
throw new Exception\InvalidArgumentException(
sprintf("%s accepts string or int as second parameter, %s passed.", __METHOD__, gettype($status))
);
$this->status($status)->set('Location', $url);
$this->status = $status;
return $this;
}
public function set($name, $value = null)
{
return $this->add($name, $value);
}
public function add($name, $value = null)
{
if (!is_string($name))
throw new Exception\InvalidArgumentException(
sprintf("First argument for %s must be a string, %s passed.", __METHOD__, gettype($value))
);
elseif (!is_null($value) && !is_string($value) && !is_int($value))
throw new Exception\InvalidArgumentException(
sprintf("%s accepts null, string or int as second argument, %s passed.", __METHOD__, gettype($value))
);
if (strpos($name, 'Content-type') === 0) {
if ($value !== null) {
$name = $name . $value;
}
$this->contentType($name);
} elseif ($name == 'status') {
$this->status($value);
} elseif (strpos($name, 'HTTP/') === 0) {
$this->status($name);
} else {
if ($value === null) {
if (count(explode(':', $name)) < 2)
throw new Exception\InvalidArgumentException(
sprintf("%s is not a valid header", $name)
);
$this->headers[] = $name;
} else {
$this->headers[$name] = $value;
}
}
return $this;
}
public function send()
{
if ($this->status_sent) {
throw new Exception\LogicException("Headers have already been sent, can't send them twice");
}
if (!$this->_content_type) {
$this->_set_default_content_type();
}
header($this->_content_type);
// vpe($this->_content_type);
foreach ($this->headers as $name => $value) {
if (!is_int($name))
$value = $name . ': ' . $value;
header($value);
}
if (is_int($this->status))
header('HTTP/1.1 ' . $this->status);
else
header($this->status);
$this->status_sent = true;
}
public function contentType($content_type = null)
{
if (null === $content_type) {
return $this->_content_type;
} else {
return $this->setContentType($content_type);
}
}
public function setContentType($content_type)
{
// static $i = 0;
if (!is_string($content_type)) {
throw new Exception\InvalidArgumentException(
sprintf("Content type must be a string, %s passed", gettype($content_type))
);
}
// if($content_type != "text/html; charset=utf-8")
// if ($i)
// vpe($content_type);
switch ($content_type) {
case 'html':
$content_type = 'text/html';
break;
case 'json':
$content_type = 'application/json';
break;
case 'xml':
$content_type = 'application/xml';
break;
}
if (strpos($content_type, 'Content-type:') !== 0)
$content_type = 'Content-type: ' . $content_type;
$this->_content_type = $content_type;
// $i++;
// vpe($content_type);
return $this;
}
private function _set_default_content_type()
{
// $this->set_content_type('text/html; charset='.Rails::application()->config()->encoding);
}
}

View File

@ -0,0 +1,314 @@
<?php
namespace Rails\ActionDispatch\Http;
use Rails\Toolbox\ArrayTools;
use Rails\ArrayHelper\GlobalVar;
class Parameters implements \IteratorAggregate
{
private
$deleteVars = [],
$putVars = [],
$_json_params_error = null,
$patchVars = [],
# Parameters for request methods other than
# delete, put, post, get, patchVars (need to support head requests).
$other_params = [];
private $files;
public function getIterator()
{
return new ArrayIterator($this->toArray());
}
public function __construct()
{
$method = \Rails::application()->dispatcher()->request()->method();
if ($method != 'GET' && $method != 'POST') {
$params = file_get_contents('php://input');
$decoded = [];
if (!empty($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] == "application/json") {
$decoded = json_decode($params, true);
if ($decoded === null) {
$decoded = [];
$this->_json_params_error = json_last_error();
}
} else {
parse_str($params, $decoded);
}
if ($method == 'DELETE')
$this->deleteVars = $decoded;
elseif ($method == 'PUT')
$this->putVars = $decoded;
elseif ($method == 'PATCH')
$this->patchVars = $decoded;
else
$this->other_params = $decoded;
}
$this->_import_files();
// vpe($this->files);
}
public function __set($prop, $value)
{
if ($var = $this->_search($prop))
global ${$var};
if (is_object($value)) {
if ($var)
$this->$prop = ${$var}[$prop];
else
$this->$prop = $value;
} elseif (is_array($value)) {
if ($var)
$value = new GlobalVar($value, $var, $prop);
$this->$prop = $value;
} else {
if ($var)
${$var}[$prop] = $value;
else
$this->$prop = $value;
}
}
public function __get($prop)
{
$ret = null;
$var = $this->_search($prop);
if ($var) {
global ${$var};
if (is_array(${$var}[$prop])) {
// if (isset($this->files[$prop])) {
// ${$var}[$prop] = array_merge(${$var}[$prop], $this->files[$prop]);
// }
$this->$prop = new GlobalVar(${$var}[$prop], $var, $prop);
# Return here.
return $this->$prop;
} elseif (is_object(${$var}[$prop])) {
$this->$prop = ${$var}[$prop];
$ret = $this->$prop;
} else {
$ret = ${$var}[$prop];
}
} else {
if (isset($this->putVars[$prop]))
$ret = $this->putVars[$prop];
elseif (isset($this->deleteVars[$prop]))
$ret = $this->deleteVars[$prop];
elseif (isset($this->patchVars[$prop])) {
$ret = $this->patchVars[$prop];
// elseif (isset($this->files[$prop])) {
# Return here.
// return $this->files[$prop];
} elseif (isset($this->other_params[$prop]))
$ret = $this->other_params[$prop];
}
// if ($ret && $this->files) {
// vpe($this->files);
// $this->mergeWithFiles($ret, $prop);
// }
return $ret;
}
public function __isset($prop)
{
return $this->_search($prop) || isset($this->deleteVars[$prop]) || isset($this->putVars[$prop]);
}
public function del($prop)
{
unset($this->$prop, $_GET[$prop], $_POST[$prop], $this->deleteVars[$prop], $this->putVars[$prop]);
}
public function get()
{
return $_GET;
}
public function post()
{
return $_POST;
}
public function delete()
{
return $this->deleteVars;
}
public function put()
{
return $this->putVars;
}
public function patch()
{
return $this->patchVars;
}
public function files()
{
return $this->files;
}
public function user()
{
get_object_vars($this);
}
public function toArray()
{
$obj_vars = get_object_vars($this);
unset($obj_vars['deleteVars'], $obj_vars['putVars'], $obj_vars['_json_params_error'], $obj_vars['patchVars'], $obj_vars['other_params'], $obj_vars['files']);
$ret = array_merge_recursive($_POST, $_GET, $obj_vars, $this->deleteVars, $this->putVars, $this->patchVars, $this->other_params/*, $this->files*/);
return $ret;
}
public function all()
{
return $this->toArray();
}
public function merge()
{
$params = func_get_args();
array_unshift($params, $this->all());
return call_user_func_array('array_merge', $params);
}
public function json_params_error()
{
return $this->_json_params_error;
}
private function _search($prop)
{
if (isset($_GET[$prop]))
return '_GET';
elseif (isset($_POST[$prop]))
return '_POST';
else
return false;
}
private function mergeWithFiles(&$array, $prop)
{
if (isset($this->files->$prop)) {
foreach ($this->files->$prop as $key => $value) {
if (is_array($value)) {
if (!isset($array[$key])) {
$array[$key] = [];
} elseif (!is_array($array[$key])) {
$array[$key] = [ $array[$key] ];
}
$array[$key] = array_merge($array[$key], $value);
} else {
$array[$key] = $value;
}
}
}
}
private function _import_files()
{
if (empty($_FILES)) {
return;
}
$this->files = new \stdClass();
foreach ($_FILES as $mainName => $data) {
if (!is_array($data['name']) && $data['error'] != UPLOAD_ERR_NO_FILE) {
$this->files->$mainName = new UploadedFile($_FILES[$mainName]);
} else {
$this->files->$mainName = $this->_get_subnames($data);
}
}
}
private function _get_subnames(array $arr)
{
$arranged = new \ArrayObject();
// $arranged = [];
foreach ($arr['name'] as $k => $value) {
if (is_string($value)) {
if ($arr['error'] != UPLOAD_ERR_NO_FILE) {
$arranged[$k] = [
'name' => $value,
'type' => $arr['type'][$k],
'tmp_name' => $arr['tmp_name'][$k],
'error' => $arr['error'][$k],
'size' => $arr['size'][$k],
];
}
} else {
$keys = ['name', $k];
$this->_get_subnames_2($arranged, $keys, $arr);
}
}
return $arranged->getArrayCopy();
}
private function _get_subnames_2($arranged, $keys, $arr)
{
$baseArr = $arr;
foreach ($keys as $key) {
$baseArr = $baseArr[$key];
}
foreach ($baseArr as $k => $value) {
if (is_string($value)) {
$this->setArranged($arranged, array_merge($keys, [$k]), [
'name' => $value,
'type' => $this->foreachKeys(array_merge(['type'] + $keys, [$k]), $arr),
'tmp_name' => $this->foreachKeys(array_merge(['tmp_name'] + $keys, [$k]), $arr),
'error' => $this->foreachKeys(array_merge(['error'] + $keys, [$k]), $arr),
'size' => $this->foreachKeys(array_merge(['size'] + $keys, [$k]), $arr),
]);
// vpe($arranged, $key, $k);
// $arranged[$k] = $arranged[$k]->getArrayCopy();
} else {
$tmpKeys = $keys;
$tmpKeys[] = $k;
$this->_get_subnames_2($arranged, $tmpKeys, $arr);
}
}
}
private function foreachKeys($keys, $arr)
{
$baseArr = $arr;
foreach ($keys as $key) {
$baseArr = $baseArr[$key];
}
return $baseArr;
}
private function setArranged($arr, $keys, $val)
{
if ($val['error'] == UPLOAD_ERR_NO_FILE) {
return;
}
array_shift($keys);
$lastKey = array_pop($keys);
$baseArr = &$arr;
foreach ($keys as $key) {
if (!isset($baseArr[$key])) {
// $baseArr[$key] = new \ArrayObject();
$baseArr[$key] = [];
}
$baseArr = &$baseArr[$key];
}
$baseArr[$lastKey] = new UploadedFile($val);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Rails\ActionDispatch\Http;
use Rails\ArrayHelper\GlobalVar;
class Session implements \IteratorAggregate
{
public function getIterator()
{
return new \ArrayIterator($_SESSION);
}
public function __set($prop, $value)
{
$this->set($prop, $value);
}
public function __get($prop)
{
return $this->get($prop);
}
public function set($prop, $value)
{
if (is_object($value)) {
$this->$prop = $value;
$_SESSION[$prop] = $value;
} elseif (is_array($value)) {
$arr = new GlobalVar($value, '_SESSION', $prop);
$this->$prop = $arr;
} else {
$_SESSION[$prop] = $value;
}
return $this;
}
public function get($prop)
{
if (isset($_SESSION[$prop])) {
if (is_array($_SESSION[$prop])) {
$this->$prop = new GlobalVar($_SESSION[$prop], '_SESSION', $prop);
return $this->$prop;
} elseif (is_object($_SESSION[$prop])) {
$this->$prop = $_SESSION[$prop];
return $this->$prop;
} else {
return $_SESSION[$prop];
}
}
return null;
}
public function delete($prop)
{
unset($this->$prop, $_SESSION[$prop]);
}
public function merge()
{
$params = func_get_args();
array_unshift($params, $_SESSION);
return call_user_func_array('array_merge', $params);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Rails\ActionDispatch\Http;
class UploadedFile
{
protected $name;
protected $type;
protected $tempName;
protected $error;
protected $size;
public function __construct(array $data)
{
$this->name = $data['name'];
$this->type = $data['type'];
$this->tempName = $data['tmp_name'];
$this->error = $data['error'];
$this->size = $data['size'];
}
public function name()
{
return $this->name;
}
public function type()
{
return $this->type;
}
public function tempName()
{
return $this->tempName;
}
public function size()
{
return $this->size;
}
public function errorCode()
{
return $this->error;
}
public function error()
{
return !($this->error == UPLOAD_ERR_OK);
}
public function move($newName)
{
return move_uploaded_file($this->tempName, $newName);
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace Rails\ActionDispatch;
class Request
{
const LOCALHOST = '127.0.0.1';
/**
* List of methods allowed through the _method parameter
* in a POST request.
*/
static private $allowedHackMethods = [
'PUT',
'PATCH',
'DELETE',
];
/**
* Request path without the query string.
* The application's basePath (i.e. if the app is ran under a subdirectory),
* is cut off.
* To get the complete path, call originalPath.
*
* @return string
*/
public function path()
{
return substr($this->originalPath(), strlen(\Rails::application()->router()->basePath()));
}
/**
* Request path without the query string.
* The application's basePath is included.
*
* @return string
*/
public function originalPath()
{
if (is_int($pos = strpos($this->get('REQUEST_URI'), '?'))) {
return substr($this->get('REQUEST_URI'), 0, $pos);
}
return substr($this->get('REQUEST_URI'), 0);
}
/**
* Full request path, includes query string, but excludes basePath.
*
* @return string
*/
public function fullPath()
{
return substr($this->get('REQUEST_URI'), strlen(\Rails::application()->router()->basePath()));
}
/**
* Full request path, includes both basePath and query string.
*
* @return string
*/
public function originalFullPath()
{
return $this->get('REQUEST_URI');
}
public function controller()
{
if (!($router = \Rails::application()->dispatcher()->router()) || !($route = $router->route()))
return false;
return $route->controller;
}
public function action()
{
if (!($router = \Rails::application()->dispatcher()->router()) || !($route = $router->route()))
return false;
return $route->action;
}
public function isGet()
{
return $this->method() === 'GET';
}
public function isPost()
{
return $this->method() == 'POST';
}
public function isPut()
{
return $this->method() == 'PUT';
}
public function isDelete()
{
return $this->method() == 'DELETE';
}
public function isPatch()
{
return $this->method() == 'PATCH';
}
/**
* Checks the request method.
*/
public function is($method)
{
$method = strtoupper($method);
return $this->method() == $method;
}
public function isLocal()
{
return \Rails::config()->consider_all_requests_local ?: $this->remoteIp() == self::LOCALHOST;
}
public function remoteIp()
{
if ($this->get('HTTP_CLIENT_IP'))
$remoteIp = $this->get('HTTP_CLIENT_IP');
elseif ($this->get('HTTP_X_FORWARDED_FOR'))
$remoteIp = $this->get('HTTP_X_FORWARDED_FOR');
else
$remoteIp = $this->get('REMOTE_ADDR');
return $remoteIp;
}
/**
* Returns the overridden method name.
*
* @return string
*/
public function method()
{
if (isset($_POST['_method'])) {
$method = strtoupper($_POST['_method']);
if (in_array($method, self::$allowedHackMethods)) {
return $method;
}
}
return $this->get('REQUEST_METHOD');
}
public function protocol()
{
$protocol = ($val = $this->get('HTTPS')) && $val !== 'off' ? 'https' : 'http';
return $protocol . '://';
}
public function isXmlHttpRequest()
{
return (($var = $this->get("HTTP_X_REQUESTED_WITH"))) && $var === "XMLHttpRequest";
}
public function format()
{
if ($route = \Rails::application()->dispatcher()->router()->route())
return $route->format;
}
/**
* Get an index in the $_SERVER superglobal.
*
* @return null|string
*/
public function get($name)
{
$name = strtoupper($name);
if (isset($_SERVER[$name])) {
return $_SERVER[$name];
}
return null;
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace Rails\ActionMailer;
use Zend\Mail;
use Rails;
abstract class ActionMailer
{
static protected $transport;
static public function load_mailer($name, $raise_exception = true)
{
if (!class_exists($name, false)) {
$mails_path = Rails::config()->paths->mailers;
$file = $mails_path . '/' . Rails::services()->get('inflector')->underscore($name) . '.php';
if (!is_file($file)) {
if ($raise_exception)
throw new Exception\RuntimeException(
sprintf('No file found for mailer %s (searched in %s)', $name, $file)
);
return false;
}
require $file;
if (!class_exists($name, false)) {
if ($raise_exception)
throw new Exception\RuntimeException(
sprintf("File for mailer %s doesn't contain expected class", $name)
);
return false;
}
}
self::init();
return true;
}
static public function transport(Mail\Transport\TransportInterface $transport = null)
{
if (null !== $transport) {
self::$transport = $transport;
} elseif (!self::$transport) {
self::setDefaultTransport();
}
return self::$transport;
}
static public function filenameGenerator()
{
return 'action_mailer_' . $_SERVER['REQUEST_TIME'] . '_' . mt_rand() . '.tmp';
}
static protected function setDefaultTransport()
{
$config = Rails::application()->config()->action_mailer;
switch ($config['delivery_method']) {
/**
* Rails to Zend options:
* address => name
* domain => host
* port => port
* authentication => connection_class
* user_name => connection_config[username]
* password => connection_config[password]
* enable_starttls_auto (true) => connection_config[ssl] => 'tls'
* enable_starttls_auto (false) => connection_config[ssl] => 'ssl'
*/
case 'smtp':
$defaultConfig = [
'address' => '127.0.0.1',
'domain' => 'localhost',
'port' => 25,
'user_name' => '',
'password' => '',
'enable_starttls_auto' => true,
/**
* Differences with RoR:
* - ZF2 adds the "smtp" option
* - The "cram_md5" option is called "crammd5"
*/
'authentication' => 'login'
];
$smtp = array_merge($defaultConfig, $config['smtp_settings']->toArray());
$options = [
'host' => $smtp['address'],
'name' => $smtp['domain'],
'port' => $smtp['port'],
'connection_class' => $smtp['authentication'],
'connection_config' => [
'username' => $smtp['user_name'],
'password' => $smtp['password'],
'ssl' => $smtp['enable_starttls_auto'] ? 'tls' : null,
],
];
$options = new Mail\Transport\SmtpOptions($options);
$transport = new Mail\Transport\Smtp();
$transport->setOptions($options);
break;
/**
* location => path
* name_generator => callback
*/
case 'file':
$customOpts = $config['file_settings'];
$options = [];
if ($customOpts['location'] === null) {
$dir = Rails::root() . '/tmp/mail';
if (!is_dir($dir))
mkdir($dir, 0777, true);
$customOpts['location'] = $dir;
}
$options['path'] = $customOpts['location'];
if ($customOpts['name_generator'] === null) {
$options['callback'] = 'Rails\ActionMailer\ActionMailer::filenameGenerator';
} else {
$options['callback'] = $customOpts['name_generator'];
}
$fileOptions = new Mail\Transport\FileOptions($options);
$transport = new Mail\Transport\File();
$transport->setOptions($fileOptions);
break;
case ($config['delivery_method'] instanceof Closure):
$transport = $config['delivery_method']();
break;
}
self::transport($transport);
}
}

135
lib/Rails/ActionMailer/Base.php Executable file
View File

@ -0,0 +1,135 @@
<?php
namespace Rails\ActionMailer;
use stdClass;
use Zend\Mime;
use Rails;
use Rails\Mail\Mail;
abstract class Base
{
public $from;
public $to;
public $subject;
/**
* TODO: this isn't used anywhere.
*/
public $partsOrder = ['text/plain', 'text/html'];
public $templatePath;
public $charset;
public $textCharset;
public $htmlCharset;
public $templateName;
public $attachments = [];
public $calledMethod;
public $headers = [];
protected $vars;
static public function mail($method, array $params = [], array $headers = [])
{
$cn = get_called_class();
$mailer = new $cn();
$mailer->calledMethod = $method;
$mailer->headers = array_merge($mailer->headers, $headers);
if (false !== call_user_func_array([$mailer, $method], $params))
return $mailer->createMail();
}
public function __construct()
{
$this->vars = new stdClass();
$this->init();
}
public function __set($prop, $value)
{
$this->vars->$prop = $value;
}
public function __get($prop)
{
if (!isset($this->vars->$prop))
return null;
return $this->vars->$prop;
}
/**
* Just a quicker way to add an attachment.
*/
public function attachment($name, $content)
{
if (!is_string($content) && (!is_resource($content) || get_resource_type($content) != 'stream')) {
throw new Exception\InvalidArgumentException(
sprintf("Attachment content must be either string or stream, %s passed")
);
}
$this->attachments[$name] = [
'content' => $content
];
}
/**
* Just a quicker way to add an inline attachment.
*/
public function inlineAttachment($name, $content)
{
if (!is_string($content) && (!is_resource($content) || get_resource_type($content) != 'stream')) {
throw new Exception\InvalidArgumentException(
sprintf("Attachment content must be either string or stream, %s passed")
);
}
$this->attachments[$name] = [
'content' => $content,
'inline' => true
];
}
public function vars()
{
return $this->vars;
}
/**
* Default values for properties can be set in this method.
*/
protected function init()
{
}
private function createMail()
{
if (Rails::config()->action_mailer->defaults) {
if (!Rails::config()->action_mailer->defaults instanceof \Closure) {
throw new Exception\RuntimeException(
'Configuration action_mailer.defaults must be a closure'
);
}
$closure = clone Rails::config()->action_mailer->defaults;
$closure = $closure->bindTo($this);
$closure();
}
if (!$this->templateName)
$this->templateName = $this->calledMethod;
if (!$this->templatePath)
$this->templatePath = Rails::services()->get('inflector')->underscore(get_called_class());
$deliverer = new Deliverer($this);
return $deliverer;
}
}

View File

@ -0,0 +1,230 @@
<?php
namespace Rails\ActionMailer;
use stdClass;
use Rails;
use Rails\ActionView;
use Zend\Mail;
use Zend\Mime;
/**
* Builds and delivers mail.
*
* This class should only be used by Rails\ActionMailer\Base.
* In order to create a custom Mail, Zend\Mail should be used
* directly instead.
*/
class Deliverer
{
/**
* Rails mail that will be processed.
*
* @var Rails\ActionMailer\Base
*/
protected $mail;
/**
* Mail message that will be delivered.
*
* @var Zend\Mail\Message
*/
protected $message;
protected $textTemplate;
protected $htmlTemplate;
/**
* @var Zend\Mime\Message
*/
protected $body;
public function __construct(Base $mail)
{
$this->mail = $mail;
$this->buildMessage();
}
public function deliver()
{
ActionMailer::transport()->send($this->message);
return $this;
}
public function mail()
{
return $this->mail;
}
public function message()
{
return $this->message;
}
private function buildMessage()
{
$this->message = new Mail\Message();
$this->setCharset();
$this->setFrom();
$this->setTo();
$this->setSubject();
ActionView\ViewHelpers::load();
$this->createTextPart();
$this->createHtmlPart();
$this->body = new Mime\Message();
$this->addTemplates();
$this->addAttachments();
$this->message->setBody($this->body);
unset($this->textTemplate, $this->htmlTemplate);
}
private function setCharset()
{
if (!$charset = $this->mail->charset) {
$charset = mb_detect_encoding($this->mail->subject);
if (!$charset)
$charset = null;
}
$this->message->setEncoding($charset);
}
private function setFrom()
{
if (!is_array($this->mail->from)) {
$email = $this->mail->from;
$name = null;
} else {
list($email, $name) = $this->mail->from;
}
$this->message->setFrom($email, $name);
}
private function setTo()
{
$this->message->addTo($this->mail->to);
}
private function setSubject()
{
$this->message->setSubject($this->mail->subject);
}
private function createTextPart()
{
$template_file = $this->templateBasename() . '.text.php';
try {
$template = new Template($template_file);
$template->setLocals($this->mail->vars());
$this->textTemplate = $template;
} catch (Exception\ExceptionInterface $e) {
}
}
private function createHtmlPart()
{
$template_file = $this->templateBasename() . '.php';
try {
$template = new Template($template_file);
$template->setLocals($this->mail->vars());
$this->htmlTemplate = $template;
} catch (Exception\ExceptionInterface $e) {
}
}
private function templateBasename()
{
return Rails::config()->paths->views . '/' .
$this->mail->templatePath . '/' .
$this->mail->templateName;
}
private function addTemplates()
{
if ($this->textTemplate) {
$content = $this->textTemplate->renderContent();
$part = new Mime\Part($content);
$part->type = 'text/plain';
$part->encoding = Mime\Mime::ENCODING_QUOTEDPRINTABLE;
$this->body->addPart($part);
}
if ($this->htmlTemplate) {
$content = $this->htmlTemplate->renderContent();
$part = new Mime\Part($content);
$part->type = 'text/html';
$part->encoding = Mime\Mime::ENCODING_QUOTEDPRINTABLE;
$this->body->addPart($part);
}
}
/**
* Requires Fileinfo.
*/
private function addAttachments()
{
if (class_exists('Finfo', false)) {
$finfo = new \Finfo(FILEINFO_MIME_TYPE);
} else {
$finfo = false;
}
foreach ($this->mail->attachments as $filename => $attachment) {
if (!is_array($attachment)) {
throw new Exception\RuntimeException(
sprintf("Attachments must be array, %s passed", gettype($attachment))
);
} elseif (
!is_string($attachment['content']) &&
(
!is_resource($attachment['content']) ||
!get_resource_type($attachment['content']) == 'stream'
)
) {
throw new Exception\RuntimeException(
sprintf(
"Attachment content must be string or stream, %s passed",
gettype($attachment['content'])
)
);
}
$type = null;
if (empty($attachment['mime_type']) && $finfo) {
if (is_resource($attachment['content'])) {
$type = $finfo->buffer(stream_get_contents($attachment['content']));
rewind($attachment['content']);
} else {
$type = $finfo->buffer($attachment['content']);
}
}
$part = new Mime\Part($attachment['content']);
if (empty($attachment['encoding'])) {
$attachment['encoding'] = Mime\Mime::ENCODING_BASE64;
}
$part->encoding = $attachment['encoding'];
if ($type) {
$part->type = $type;
}
$part->disposition = !empty($attachment['inline']) ?
Mime\Mime::DISPOSITION_INLINE :
Mime\Mime::DISPOSITION_ATTACHMENT;
$this->body->addPart($part);
}
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionMailer\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionMailer\Exception;
class InvalidArgumentException extends \Rails\Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionMailer\Exception;
class RuntimeException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,32 @@
<?php
namespace Rails\ActionMailer;
class Template extends \Rails\ActionView\Base
{
private
$_template_file,
$_contents;
public function __construct($template_file)
{
if (!is_file($template_file)) {
throw new Exception\RuntimeException(
sprintf("Template file %s doesn't exist", $template_file)
);
}
$this->_template_file = $template_file;
}
public function renderContent()
{
ob_start();
require $this->_template_file;
$this->_contents = ob_get_clean();
return $this->_contents;
}
public function contents()
{
return $this->_contents;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Rails\ActionView;
use Closure;
use ReflectionClass;
use Rails;
abstract class ActionView
{
/**
* Stores the contents for.
* Static because contents_for are available
* for everything.
*/
private static $_content_for = array();
/**
* Stores the names of the active contentFor's.
*/
private static $_content_for_names = array();
protected $_buffer;
static public function clean_buffers()
{
if ($status = ob_get_status()) {
foreach (range(0, $status['level']) as $lvl)
ob_end_clean();
}
}
/**
* Creates content for $name by calling $block().
* If no $block is passed, it's checked if content for $name
* exists.
*/
public function contentFor($name, Closure $block = null, $prefix = false)
{
if (!$block) {
return isset(self::$_content_for[$name]);
}
if (!isset(self::$_content_for[$name]))
self::$_content_for[$name] = '';
ob_start();
$block();
$this->_add_content_for($name, ob_get_clean(), $prefix);
}
public function provide($name, $content)
{
$this->_add_content_for($name, $content, false);
}
public function clear_content_for($name)
{
unset(self::$_content_for[$name]);
}
/**
* Yield seems to be a reserved keyword in PHP 5.5.
* Forced to change the name of the yield method to "content".
*/
public function content($name = null)
{
if ($name && isset(self::$_content_for[$name]))
return self::$_content_for[$name];
}
/**
* Passing a closure to do_content_for() will cause it
* to do the same as end_content_for(): add the buffered
* content. Thus, this method.
*
* @param string $name content's name
* @param string $value content's body
* @param bool $prefix to prefix or not the value to the current value
*/
private function _add_content_for($name, $value, $prefix)
{
!array_key_exists($name, self::$_content_for) && self::$_content_for[$name] = '';
if ($prefix)
self::$_content_for[$name] = $value . self::$_content_for[$name];
else
self::$_content_for[$name] .= $value;
}
}

204
lib/Rails/ActionView/Base.php Executable file
View File

@ -0,0 +1,204 @@
<?php
namespace Rails\ActionView;
use stdClass;
use Rails;
use Rails\Routing\Traits\NamedPathAwareTrait;
/**
* Base class for layouts, templates and partials.
*/
abstract class Base extends ActionView
{
use NamedPathAwareTrait;
/**
* Local variables passed.
* This could be either an array (partials) or an stdClass
* (layouts and templates). They're accessed through __get();
*/
protected $locals = [];
public function __get($prop)
{
return $this->getLocal($prop);
}
public function __set($prop, $val)
{
if (!$this->locals) {
$this->locals = new stdClass();
}
$this->setLocal($prop, $val);
}
public function __call($method, $params)
{
if ($helper = ViewHelpers::findHelperFor($method)) {
$helper->setView($this);
return call_user_func_array(array($helper, $method), $params);
} elseif ($this->isNamedPathMethod($method)) {
return $this->getNamedPath($method, $params);
}
throw new Exception\BadMethodCallException(
sprintf("Called to unknown method/helper: %s", $method)
);
}
# Remove for 2.0
public function __isset($prop)
{
return $this->localExists($prop);
}
public function localExists($name)
{
if ($this->locals instanceof stdClass) {
return property_exists($this->locals, $name);
} else {
return array_key_exists($name, $this->locals);
}
}
public function getLocal($name)
{
if (!$this->localExists($name)) {
throw new Exception\RuntimeException(
sprintf("Undefined local '%s'", $name)
);
}
if ($this->locals instanceof stdClass) {
return $this->locals->$name;
} else {
return $this->locals[$name];
}
}
public function setLocal($name, $value)
{
if ($this->locals instanceof stdClass) {
$this->locals->$name = $value;
} else {
$this->locals[$name] = $value;
}
return $this;
}
// public function isset_local($name)
// {
// if ($this->locals instanceof stdClass)
// return property_exists($this->locals, $name);
// elseif (is_array($this->locals))
// return array_key_exists($name, $this->locals);
// }
public function I18n()
{
return Rails::application()->I18n();
}
public function t($name, array $params = [])
{
$trans = $this->I18n()->t($name, $params);
if (false === $trans) {
return '<span class="translation_missing">#' . $name . '</span>';
}
return $trans;
}
/**
* This method could go somewhere else.
*/
public function optionsFromEnumColumn($model_name, $column_name, array $extra_options = [])
{
$options = [];
foreach ($model_name::table()->enumValues($column_name) as $val) {
$options[$this->humanize($val)] = $val;
}
if ($extra_options) {
$options = array_merge($extra_options, $options);
}
return $options;
}
/**
* This is meant to be a way to check if
* there are contentFor awaiting to be ended.
*/
public function activeContentFor()
{
return self::$_content_for_names;
}
public function setLocals($locals)
{
if (!is_array($locals) && !$locals instanceof stdClass)
throw new Exception\InvalidArgumentException(
sprintf('Locals must be either an array or an instance of stdClass, %s passed.', gettype($locals))
);
$this->locals = $locals;
}
public function params()
{
return Rails::application()->dispatcher()->parameters();
}
public function request()
{
return Rails::application()->dispatcher()->request();
}
public function partial($name, array $locals = array())
{
$ctrlr_name = Rails::services()->get('inflector')->camelize($this->request()->controller(), false);
// $ctrlr_name = substr_replace(Rails::services()->get('inflector')->camelize($ctrlr_name), substr($ctrlr_name, 0, 1), 0, 1);
if (!isset($locals[$ctrlr_name]) && $this->localExists($ctrlr_name)) {
$locals[$ctrlr_name] = $this->getLocal($ctrlr_name);
}
// if (!Rails::config()->ar2) {
// if (!isset($locals[$name]) && $this->localExists($name)) {
// $locals[$name] = $this->getLocal($name);
// }
// } else {
$camelized = Rails::services()->get('inflector')->camelize($name, false);
if (!isset($locals[$camelized]) && $this->localExists($camelized)) {
$locals[$camelized] = $this->getLocal($camelized);
}
// }
$base_path = Rails::config()->paths->views;
if (is_int(strpos($name, '/'))) {
$pos = strrpos($name, '/');
$name = substr_replace($name, '/_', $pos, 1) . '.php';
$filename = $base_path . '/' . $name;
} else {
if ($namespaces = Rails::application()->dispatcher()->router()->route()->namespaces())
$base_path .= '/' . implode('/', $namespaces);
$filename = $base_path . '/' . $this->request()->controller() . '/_' . $name . '.php';
}
if (isset($locals['collection'])) {
$collection = $locals['collection'];
unset($locals['collection']);
$contents = '';
foreach ($collection as $member) {
$locals[$name] = $member;
$contents .= (new Partial($filename, [], $locals))->render_content();
}
} else {
$partial = new Partial($filename, [], $locals);
$contents = $partial->render_content();
}
return $contents;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Exception;
class BadMethodCallException extends \Rails\Exception\BadMethodCallException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Exception;
class InvalidArgumentException extends \Rails\Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Exception;
class RuntimeException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,61 @@
<?php
namespace Rails\ActionView;
class FormBuilder
{
protected $helper;
protected $model;
protected $inputNamespace;
public function __construct($helper, $model)
{
$this->helper = $helper;
$this->model = $model;
$this->inputNamespace = \Rails::services()->get('inflector')->underscore(get_class($model));
}
public function textField($property, array $attrs = array())
{
$this->helper->setDefaultModel($this->model);
return $this->helper->textField($this->inputNamespace, $property, $attrs);
}
public function hiddenField($property, array $attrs = array())
{
$this->helper->setDefaultModel($model);
return $this->helper->hiddenField($this->inputNamespace, $property, $attrs);
}
public function passwordField($property, array $attrs = array())
{
$this->helper->setDefaultModel($model);
return $this->helper->passwordField($this->inputNamespace, $property, $attrs);
}
public function checkBox($property, array $attrs = array(), $checked_value = '1', $unchecked_value = '0')
{
$this->helper->setDefaultModel($model);
return $this->helper->passwordField($this->inputNamespace, $property, $attrs, $checked_value, $unchecked_value);
}
public function textArea($property, array $attrs = array())
{
$this->helper->setDefaultModel($model);
return $this->helper->textArea($this->inputNamespace, $property, $attrs);
}
public function select($property, $options, array $attrs = array())
{
$this->helper->setDefaultModel($model);
return $this->helper->select($this->inputNamespace, $property, $options, $attrs);
}
public function radioButton($property, $tag_value, array $attrs = array())
{
$this->helper->setDefaultModel($model);
return $this->helper->radioButton($this->inputNamespace, $property, $tag_value, $attrs);
}
}

123
lib/Rails/ActionView/Helper.php Executable file
View File

@ -0,0 +1,123 @@
<?php
namespace Rails\ActionView;
use Rails;
use Rails\ActionController\ActionController;
use Rails\ActionView\Helper\Methods;
use Rails\Routing\Traits\NamedPathAwareTrait;
/**
* Some parts of this class was taken from Ruby on Rails helpers.
*/
abstract class Helper extends ActionView
{
use NamedPathAwareTrait;
/**
* ActionView_Base children for methods that
* require it when passing Closures, like form().
*/
private $_view;
public function __call($method, $params)
{
if ($this->isNamedPathMethod($method)) {
return $this->getNamedPath($method, $params);
} elseif ($helper = ViewHelpers::findHelperFor($method)) {
$helper->setView($this);
return call_user_func_array(array($helper, $method), $params);
}
throw new Exception\BadMethodCallException(
sprintf("Called to unknown method/helper: %s", $method)
);
}
/**
* Returns instance of Helper\Base
*/
public function base()
{
return ViewHelpers::getBaseHelper();
}
public function setView(ActionView $view)
{
$this->_view = $view;
}
public function view()
{
return $this->_view;
}
public function urlFor($params)
{
return Rails::application()->router()->urlFor($params);
}
public function params()
{
return Rails::application()->dispatcher()->parameters();
}
public function request()
{
return Rails::application()->dispatcher()->request();
}
public function controller()
{
return Rails::application()->controller();
}
public function u($str)
{
return urlencode($str);
}
public function hexEncode($str)
{
$r = '';
$e = strlen($str);
$c = 0;
$h = '';
while ($c < $e) {
$h = dechex(ord(substr($str, $c++, 1)));
while (strlen($h) < 3)
$h = '%' . $h;
$r .= $h;
}
return $r;
}
public function h($str, $flags = null, $charset = null)
{
$flags === null && $flags = ENT_COMPAT;
!$charset && $charset = Rails::application()->config()->encoding;
return htmlspecialchars($str, $flags, $charset);
}
public function I18n()
{
return Rails::services()->get('i18n');
}
public function t($name)
{
return $this->I18n()->t($name);
}
# TODO: move this method somewhere else, it doesn't belong here.
protected function parseUrlParams($url_params)
{
if ($url_params != '#' && (is_array($url_params) || (strpos($url_params, 'http') !== 0 && strpos($url_params, '/') !== 0))) {
if (!is_array($url_params))
$url_params = array($url_params);
$url_to = Rails::application()->router()->urlFor($url_params, true);
} else {
$url_to = $url_params;
}
return $url_to;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Rails\ActionView\Helper;
/**
* This class shouldn't be extended.
* Having all these methods in a separed class will make
* possible to override them in other helpers.
* Calling one of these methods within a method with the same name
* can be done by calling base(), which will return the instance of
* this class.
*/
class Base extends \Rails\ActionView\Helper
{
use Methods\Form, Methods\Date, Methods\FormTag, Methods\Header,
Methods\Html, Methods\Number, Methods\Tag, Methods\Text,
Methods\JavaScript, Methods\Inflections, Methods\Assets;
}

View File

@ -0,0 +1,30 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait Assets
{
public function assetPath($source, array $options = [])
{
if (strpos($source, '/') !== 0 && strpos($source, 'http') !== 0) {
if (!isset($options['digest'])) {
$options['digest'] = true;
}
if (\Rails::config()->assets->enabled) {
if (\Rails::config()->serve_static_assets && $options['digest']) {
if ($url = \Rails::assets()->findCompiledFile($source)) {
return $url;
}
}
if ($file = \Rails::assets()->findFile($source)) {
return $file->url();
}
}
return \Rails::application()->router()->rootPath() . $source;
} else {
return $source;
}
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait Date
{
public function timeAgoInWords($fromTime, $includeSeconds = false)
{
return $this->distanceOfTimeInWords($fromTime, time(), $includeSeconds);
}
public function distanceOfTimeInWords($fromTime, $toTime = 'now', $includeSeconds = false)
{
if (!is_int($fromTime)) {
$fromTime = strtotime($fromTime);
}
if (!is_int($toTime)) {
$toTime = strtotime($toTime);
}
$distanceInSeconds = round($toTime - $fromTime);
if ($distanceInSeconds < 0) {
$distanceInSeconds = round($fromTime - $toTime);
}
$distanceInMinutes = ceil($distanceInSeconds/60);
if ($distanceInSeconds < 30)
$t = 'less_than_a_minute';
elseif ($distanceInSeconds < 90)
$t = 'one_minute';
elseif ($distanceInSeconds < 2670)
$t = ['x_minutes', 't' => $distanceInMinutes];
elseif ($distanceInSeconds < 5370)
$t = 'about_one_hour';
elseif ($distanceInSeconds < 86370)
$t = ['about_x_hours', 't' => ceil($distanceInMinutes/60)];
elseif ($distanceInSeconds < 151170)
$t = 'one_day';
elseif ($distanceInSeconds < 2591970)
$t = ['x_days', 't' => ceil(($distanceInMinutes/60)/24)];
elseif ($distanceInSeconds < 5183970)
$t = 'about_one_month';
elseif ($distanceInSeconds < 31536059)
$t = ['x_months', 't' => ceil((($distanceInMinutes/60)/24)/31)];
elseif ($distanceInSeconds < 39312001)
$t = 'about_one_year';
elseif ($distanceInSeconds < 54864001)
$t = 'over_a_year';
elseif ($distanceInSeconds < 31536001)
$t = 'almost_two_years';
else
$t = ['about_x_years', 't' => ceil($distanceInMinutes/60/24/365)];
if (is_array($t))
$t[0] = 'actionview.helper.date.' . $t[0];
else
$t = 'actionview.helper.date.' . $t;
return $this->t($t);
}
}

View File

@ -0,0 +1,224 @@
<?php
namespace Rails\ActionView\Helper\Methods;
use Rails;
use Rails\Routing\UrlToken;
use Rails\ActionController\ActionController;
/**
* $property may be a method by adding () at the end.
* E.g. $this->text_field('artist', 'member_names()');
*/
trait Form
{
private $default_model;
public function formField($type, $model, $property, array $attrs = array())
{
return $this->_form_field($type, $model, $property, $attrs);
}
public function formFor(Rails\ActiveRecord\Base $model, $attrs, \Closure $block = null)
{
if ($attrs instanceof \Closure) {
$block = $attrs;
$attrs = [];
}
if (!isset($attrs['html']))
$attrs['html'] = [];
if (!isset($attrs['url'])) {
// if (Rails::config()->ar2) {
$className = get_class($model);
if (($primaryKey = $className::table()->primaryKey()) && $model->getAttribute($primaryKey)) {
$action = 'update';
} else {
$action = 'create';
}
// } else {
// $action = $model->id ? 'update' : 'create';
// }
$attrs['url'] = ['#' . $action];
} else {
$token = new UrlToken($attrs['url'][0]);
$action = $token->action();
}
$html_attrs = $attrs['html'];
if (!isset($html_attrs['method'])) {
if ($action == 'create')
$html_attrs['method'] = 'post';
elseif ($action == 'destroy')
$html_attrs['method'] = 'delete';
else
$html_attrs['method'] = 'put';
}
# Check special attribute 'multipart'.
if (!empty($html_attrs['multipart'])) {
$html_attrs['enctype'] = 'multipart/form-data';
unset($html_attrs['multipart']);
}
if ($html_attrs['method'] != 'post') {
$method = $html_attrs['method'];
$html_attrs['method'] = 'post';
} else {
$method = 'post';
}
$url_token = new UrlToken($attrs['url'][0]);
if ($url_token->action() == 'create') {
$action_url = Rails::application()->router()->urlFor($url_token->token());
} else {
list($route, $action_url) = Rails::application()->router()->url_helpers()->find_route_for_token($url_token->token(), $model);
}
$html_attrs['action'] = $action_url;
ob_start();
if ($method != 'post')
echo $this->hiddenFieldTag('_method', $method, ['id' => '']);
$block(new \Rails\ActionView\FormBuilder($this, $model));
return $this->contentTag('form', ob_get_clean(), $html_attrs);
}
public function textField($model, $property, array $attrs = array())
{
return $this->_form_field('text', $model, $property, $attrs);
}
public function hiddenField($model, $property, array $attrs = array())
{
return $this->_form_field('hidden', $model, $property, $attrs);
}
public function passwordField($model, $property, array $attrs = array())
{
return $this->_form_field('password', $model, $property, array_merge($attrs, ['value' => '']));
}
public function checkBox($model, $property, array $attrs = array(), $checked_value = '1', $unchecked_value = '0')
{
if ($this->_get_model_property($model, $property))
$attrs['checked'] = 'checked';
$attrs['value'] = $checked_value;
$hidden = $this->tag('input', array('type' => 'hidden', 'name' => $model.'['.$property.']', 'value' => $unchecked_value));
$check_box = $this->_form_field('checkbox', $model, $property, $attrs);
return $hidden . "\n" . $check_box;
}
public function textArea($model, $property, array $attrs = array())
{
if (isset($attrs['size']) && is_int(strpos($attrs['size'], 'x'))) {
list($attrs['cols'], $attrs['rows']) = explode('x', $attrs['size']);
unset($attrs['size']);
}
return $this->_form_field('textarea', $model, $property, $attrs, true);
}
public function select($model, $property, $options, array $attrs = array())
{
if (!is_string($options)) {
$value = $this->_get_model_property($model, $property);
$options = $this->optionsForSelect($options, $value);
}
if (isset($attrs['prompt'])) {
$options = $this->contentTag('option', $attrs['prompt'], ['value' => '', 'allow_blank_attrs' => true]) . "\n" . $options;
unset($attrs['prompt']);
}
$attrs['value'] = $options;
return $this->_form_field('select', $model, $property, $attrs, true);
}
public function radioButton($model, $property, $tag_value, array $attrs = array())
{
(string)$this->_get_model_property($model, $property) == (string)$tag_value && $attrs['checked'] = 'checked';
$attrs['value'] = $tag_value;
return $this->_form_field('radio', $model, $property, $attrs);
}
public function fileField($model, $property, array $attrs = array())
{
return $this->_form_field('file', $model, $property, $attrs);
}
public function setDefaultModel(\Rails\ActiveRecord\Base $model)
{
$this->default_model = $model;
}
private function _form_field($field_type, $model, $property, array $attrs = array(), $content_tag = false)
{
$value = array_key_exists('value', $attrs) ? $attrs['value'] : $this->_get_model_property($model, $property);
# Note here that the name tag attribute is forced to be underscored.
$underscoreProperty = preg_match('/[A-Z]/', $property) ?
Rails::services()->get('inflector')->underscore($property) : $property;
$attrs['name'] = $model.'['.$underscoreProperty.']';
if (!isset($attrs['id']))
$attrs['id'] = $model . '_' . $underscoreProperty;
if ($content_tag) {
unset($attrs['value']);
return $this->contentTag($field_type, $value, $attrs);
} else {
$attrs['type'] = $field_type;
if ($value !== '')
$attrs['value'] = $value;
return $this->tag('input', $attrs);
}
}
private function _get_model_property($model, $property)
{
$value = '';
$mdl = false;
if ($this->default_model) {
$mdl = $this->default_model;
} else {
$vars = Rails::application()->dispatcher()->controller()->vars();
if (!empty($vars->$model)) {
$mdl = $vars->$model;
}
}
if ($mdl) {
// if (!Rails::config()->ar2) {
if ($mdl->isAttribute($property)) {
$value = (string)$mdl->$property;
} elseif (($modelProps = get_class_vars(get_class($mdl))) && array_key_exists($property, $modelProps)) {
$value = (string)$mdl->$property;
} else {
# It's assumed this is a method.
$value = (string)$mdl->$property();
}
// } else {
// /**
// *
// */
// $value = (string)$mdl->$property();
// }
}
$this->default_model = null;
return $value;
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace Rails\ActionView\Helper\Methods;
use Closure;
use Rails;
use Rails\ActionController\ActionController;
use Rails\ActionView\Exception;
use Rails\Toolbox\ArrayTools;
trait FormTag
{
/**
* Passing an empty value as $action_url will cause the form
* to omit the "action" attribute, causing the form to be
* submitted to the current uri.
*
* To avoid passing an empty array for $attrs,
* pass a Closure as second argument and it
* will be taken as $block.
*
* Likewise, passing Closure as first argument
* (meaning the form will be submitted to the current url)
* will work too, instead of passing an empty value as
* $action_url.
*
* Note that if $action_url is an array like [ 'controller' => 'ctrl', 'action' => ... ],
* it will be taken as $attrs. Therefore the action url should be passed as [ 'ctrl#action', ... ].
*/
public function formTag($action_url = null, $attrs = [], Closure $block = null)
{
if (func_num_args() == 1 && $action_url instanceof Closure) {
$block = $action_url;
$action_url = null;
} elseif (func_num_args() == 2 && is_array($action_url) && is_string(key($action_url))) {
$block = $attrs;
$attrs = $action_url;
$action_url = null;
} elseif ($attrs instanceof Closure) {
$block = $attrs;
$attrs = [];
}
if (!$block instanceof Closure)
throw new Exception\BadMethodCallException("One of the arguments must be a Closure");
if (empty($attrs['method'])) {
$attrs['method'] = 'post';
$method = 'post';
} elseif (($method = strtolower($attrs['method'])) != 'get') {
$attrs['method'] = 'post';
}
# Check special attribute 'multipart'.
if (!empty($attrs['multipart'])) {
$attrs['enctype'] = 'multipart/form-data';
unset($attrs['multipart']);
}
if ($action_url)
$attrs['action'] = Rails::application()->router()->urlFor($action_url);
ob_start();
if ($method != 'get' && $method != 'post')
echo $this->hiddenFieldTag('_method', $method, ['id' => '']);
$block();
return $this->contentTag('form', ob_get_clean(), $attrs);
}
public function formFieldTag($type, $name, $value = null, array $attrs = [])
{
return $this->_form_field_tag($type, $name, $value, $attrs);
}
public function submitTag($value, array $attrs = [])
{
$attrs['type'] = 'submit';
$attrs['value'] = $value;
!isset($attrs['name']) && $attrs['name'] = 'commit';
return $this->tag('input', $attrs);
}
public function textFieldTag($name, $value = null, array $attrs = [])
{
if (is_array($value)) {
$attrs = $value;
$value = null;
}
return $this->_form_field_tag('text', $name, $value, $attrs);
}
public function hiddenFieldTag($name, $value, array $attrs = [])
{
return $this->_form_field_tag('hidden', $name, $value, $attrs);
}
public function passwordFieldTag($name, $value = null, array $attrs = array())
{
return $this->_form_field_tag('password', $name, $value, $attrs);
}
public function checkBoxTag($name, $value = '1', $checked = false, array $attrs = [])
{
if ($checked) {
$attrs['checked'] = 'checked';
}
return $this->_form_field_tag('checkbox', $name, $value, $attrs);
}
public function textAreaTag($name, $value, array $attrs = [])
{
if (isset($attrs['size']) && is_int(strpos($attrs['size'], 'x'))) {
list($attrs['cols'], $attrs['rows']) = explode('x', $attrs['size']);
unset($attrs['size']);
}
return $this->_form_field_tag('textarea', $name, $value, $attrs, true);
}
public function radioButtonTag($name, $value, $checked = false, array $attrs = [])
{
if ($checked)
$attrs['checked'] = 'checked';
return $this->_form_field_tag('radio', $name, $value, $attrs);
}
# $options may be closure, collection or an array of name => values.
public function selectTag($name, $options, array $attrs = [])
{
# This is found also in Form::select()
if (!is_string($options)) {
if (is_array($options) && ArrayTools::isIndexed($options) && count($options) == 2)
list ($options, $value) = $options;
else
$value = null;
$options = $this->optionsForSelect($options, $value);
}
if (isset($attrs['prompt'])) {
$options = $this->contentTag('option', $attrs['prompt'], ['value' => '', 'allow_blank_attrs' => true]) . "\n" . $options;
unset($attrs['prompt']);
}
$attrs['value'] = $attrs['type'] = null;
$select_tag = $this->_form_field_tag('select', $name, $options, $attrs, true);
return $select_tag;
}
/**
* Note: For options to recognize the $tag_value, it must be identical to the option's value.
*/
public function optionsForSelect($options, $tag_value = null)
{
# New feature: accept anonymous functions that will return options.
if ($options instanceof Closure) {
$options = $options();
/**
* New feature: accept collection as option in index 0, in index 1 the option name and in index 2 the value
* which are the properties of the models that will be used.
* Example: [ Category::all(), 'name', 'id' ]
* The second and third indexes can be either:
* - An attribute name
* - A public property
* - A method of the model that will return the value for the option/name
*/
} elseif (is_array($options) && count($options) == 3 && ArrayTools::isIndexed($options) && $options[0] instanceof \Rails\ActiveModel\Collection) {
list($models, $optionName, $valueName) = $options;
$options = [];
if ($models->any()) {
$modelClass = get_class($models[0]);
foreach ($models as $m) {
if ($modelClass::isAttribute($optionName)) {
$option = $m->getAttribute($optionName);
} elseif ($modelClass::hasPublicProperty($optionName)) {
$option = $m->$optionName;
} else {
# We assume it's a method.
$option = $m->$optionName();
}
if ($modelClass::isAttribute($valueName)) {
$value = $m->getAttribute($valueName);
} elseif ($modelClass::hasPublicProperty($optionName)) {
$option = $m->$optionName;
} else {
# We assume it's a method.
$value = $m->$valueName();
}
$options[$option] = $value;
}
}
}
$tag_value = (string)$tag_value;
$tags = [];
foreach ($options as $name => $value) {
$value = (string)$value;
$tags[] = $this->_form_field_tag('option', null, $name, array('selected' => $value === $tag_value ? '1' : '', 'id' => '', 'value' => $value), true);
}
return implode("\n", $tags);
}
private function _form_field_tag($field_type, $name = null, $value, array $attrs = [], $content_tag = false)
{
!isset($attrs['id']) && $attrs['id'] = trim(str_replace(['[', ']', '()', '__'], ['_', '_', '', '_'], $name), '_');
$name && $attrs['name'] = $name;
$value = (string)$value;
if ($content_tag) {
return $this->contentTag($field_type, $value, $attrs);
} else {
$attrs['type'] = $field_type;
$value !== '' && $attrs['value'] = $value;
return $this->tag('input', $attrs);
}
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Rails\ActionView\Helper\Methods;
use Rails;
trait Header
{
public function stylesheetLinkTag($url, array $attrs = array())
{
empty($attrs['type']) && $attrs['type'] = 'text/css';
empty($attrs['rel']) && $attrs['rel'] = 'stylesheet';
$assets_config = Rails::application()->config()->assets;
# If assets are enabled and fails to find the wanted file, normal behaviour of
# this method will be executed.
if ($assets_config->enabled) {
if (Rails::application()->config()->serve_static_assets) {
if ($fileUrl = Rails::assets()->findCompiledFile($url . '.css')) {
// $attrs['href'] = Rails::assets()->prefix() . '/' . $file;
$attrs['href'] = $fileUrl;
return $this->tag('link', $attrs);
}
} else {
$asset_file = $url . '.css';
if ($assets_config->concat) {
if ($href = Rails::assets()->getFileUrl($asset_file )) {
$attrs['href'] = $href;
return $this->tag('link', $attrs);
}
} elseif ($paths = Rails::assets()->getFileUrls($asset_file)) {
$tags = [];
foreach ($paths as $path) {
$attrs['href'] = $path;
$tags[] = $this->tag('link', $attrs);
}
return implode("\n", $tags);
}
}
}
$attrs['href'] = $this->_parse_url($url, '/stylesheets/', 'css');
return $this->tag('link', $attrs);
}
public function javascriptIncludeTag($url, array $attrs = array())
{
empty($attrs['type']) && $attrs['type'] = 'text/javascript';
$assets_config = Rails::application()->config()->assets;
# If assets are enabled and fails to find the wanted file, normal behaviour of
# this method will be executed.
if ($assets_config->enabled) {
if (Rails::application()->config()->serve_static_assets) {
if ($fileUrl = Rails::assets()->findCompiledFile($url . '.js')) {
// $attrs['src'] = Rails::assets()->prefix() . '/' . $file;
$attrs['src'] = $fileUrl;
return $this->contentTag('script', '', $attrs);
}
} else {
$asset_file = $url . '.js';
if ($assets_config->concat) {
if ($src = Rails::assets()->getFileUrl($asset_file)) {
$attrs['src'] = $src;
return $this->contentTag('script', '', $attrs);
}
} elseif ($paths = Rails::assets()->getFileUrls($asset_file)) {
$tags = [];
foreach ($paths as $path) {
$attrs['src'] = $path;
$tags[] = $this->contentTag('script', '', $attrs);
}
return implode("\n", $tags);
}
}
}
$attrs['src'] = $this->_parse_url($url, '/javascripts/', 'js');
return $this->contentTag('script', '', $attrs);
}
private function _parse_url($url, $default_base_url, $ext)
{
$base_path = Rails::application()->router()->basePath();
if (strpos($url, '/') === 0) {
$url = $base_path . $url;
} elseif (strpos($url, 'http') !== 0 && strpos($url, 'www') !== 0) {
$url = $base_path . $default_base_url . $url . '.' . $ext;
}
return $url;
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace Rails\ActionView\Helper\Methods;
use Rails;
trait Html
{
private $_form_attrs;
public function linkTo($link, $url_params, array $attrs = array())
{
$url_to = $this->parseUrlParams($url_params);
$onclick = '';
if (isset($attrs['method'])) {
$onclick = "var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'post';";
if ($attrs['method'] != 'post') {
$onclick .= "var m = document.createElement('input'); m.type = 'hidden'; m.name = '_method'; m.value = '".$attrs['method']."'; f.appendChild(m);";
}
$onclick .= "f.action = this.href;f.submit();return false;";
// $attrs['data-method'] = $attrs['method'];
unset($attrs['method']);
}
if (isset($attrs['confirm'])) {
if (!$onclick)
$onclick = "if (!confirm('".$attrs['confirm']."')) return false;";
else
$onclick = 'if (confirm(\''.$attrs['confirm'].'\')) {'.$onclick.'}; return false;';
unset($attrs['confirm']);
}
if ($onclick)
$attrs['onclick'] = $onclick;
$attrs['href'] = $url_to;
return $this->contentTag('a', $link, $attrs);
}
public function linkToIf($condition, $link, $url_params, array $attrs = array())
{
if ($condition)
return $this->linkTo($link, $url_params, $attrs);
else
return $link;
}
public function autoDiscoveryLinkTag($type = 'rss', $url_params = null, array $attrs = array())
{
if (!$url_params) {
$url_params = Rails::application()->dispatcher()->router()->route()->controller . '#' .
Rails::application()->dispatcher()->router()->route()->action;
}
$attrs['href'] = $this->parseUrlParams($url_params);
empty($attrs['type']) && $attrs['type'] = 'application/' . strtolower($type) . '+xml';
empty($attrs['title']) && $attrs['title'] = strtoupper($type);
return $this->tag('link', $attrs);
}
public function imageTag($source, array $attrs = array())
{
$source = $this->assetPath($source);
if (!isset($attrs['alt']))
$attrs['alt'] = $this->humanize(pathinfo($source, PATHINFO_FILENAME));
if (isset($attrs['size']))
$this->_parse_size($attrs);
$attrs['src'] = $source;
return $this->tag('img', $attrs);
}
public function mailTo($address, $name = null, array $options = array())
{
if ($name === null) {
$name = $address;
if (isset($options['replace_at']))
$name = str_replace('@', $options['replace_at'], $address);
if (isset($options['replace_dot']))
$name = str_replace('.', $options['replace_dot'], $address);
}
$encode = isset($options['encode']) ? $options['encode'] : false;
if ($encode == 'hex') {
$address = $this->hexEncode($address . '.');
$address = str_replace(['%40', '%2e'], ['@', '.'], $address);
}
$address_options = array('subject', 'body', 'cc', 'bcc');
$query = array_intersect_key($options, array_fill_keys($address_options, null));
if ($query)
$query = '?' . http_build_query($query);
else
$query = '';
$address .= $query;
$attrs = array_diff_key($options, $address_options, array_fill_keys(array('replace_at', 'replace_dot', 'encode'), null));
$attrs['href'] = 'mailto:' . $address;
$tag = $this->contentTag('a', $name, $attrs);
if ($encode = 'javascript') {
$tag = "document.write('" . $tag . "');";
return $this->javascriptTag('eval(decodeURIComponent(\'' . $this->hexEncode($tag) . '\'))');
} else
return $tag;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait Inflections
{
public function pluralize($word, $locale = 'en')
{
return $this->inflector()->pluralize($word, $locale);
}
public function singularize($word, $locale = 'en')
{
return $this->inflector()->singularize($word, $locale);
}
public function camelize($term, $uppercaseFirstLetter = true)
{
return $this->inflector()->camelize($term, $uppercaseFirstLetter);
}
public function underscore($camelCasedWord)
{
return $this->inflector()->underscore($camelCasedWord);
}
public function humanize($lowerCaseAndUnderscoredWord)
{
return $this->inflector()->humanize($lowerCaseAndUnderscoredWord);
}
public function titleize($word)
{
return $this->inflector()->titleize($word);
}
public function tableize($className)
{
return $this->inflector()->tableize($className);
}
public function classify($tableName)
{
return $this->inflector()->classify($tableName);
}
public function ordinal($number)
{
return $this->inflector()->ordinal($number);
}
public function inflector()
{
return \Rails::services()->get('inflector');
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait JavaScript
{
static private $JS_ESCAPE_MAP = [
'\\' => '\\\\',
'</' => '<\/',
"\r\n" => '\n',
"\n" => '\n',
"\r" => '\n',
'"' => '\\"',
"'" => "\\'"
];
static private $JSON_ESCAPE = [
'&' => '\u0026',
'>' => '\u003E',
'<' => '\u003C'
];
public function linkToFunction($link, $function, array $attrs = array())
{
$attrs['href'] = '#';
if ($function) {
$function = trim($function);
if (strpos($function, -1) != ';')
$function .= ';';
$function .= ' return false;';
} else
$function = 'return false;';
$attrs['onclick'] = $function;
return $this->contentTag('a', $link, $attrs);
}
public function buttonToFunction($name, $function, array $attrs = array())
{
$attrs['href'] = '#';
$function = trim($function);
if (strpos($function, -1) != ';')
$function .= ';';
$function .= ' return false;';
$attrs['onclick'] = $function;
$attrs['type'] = 'button';
$attrs['value'] = $name;
return $this->tag('input', $attrs);
}
public function j($str)
{
return $this->escapeJavascript($str);
}
public function escapeJavascript($str)
{
return str_replace(array_keys(self::$JS_ESCAPE_MAP), self::$JS_ESCAPE_MAP, $str);
}
/**
* $this->javascriptTag('alert("foo")', ['defer' => 'defer']);
* $this->javascriptTag(['defer' => 'defer'], function() { ... });
* $this->javascriptTag(function() { ... });
*/
public function javascriptTag($contentOrOptionWithBlock, $htmlOptions = null)
{
if ($contentOrOptionWithBlock instanceof \Closure) {
ob_start();
$contentOrOptionWithBlock();
$contents = ob_get_clean();
$htmlOptions = [];
} elseif ($htmlOptions instanceof \Closure) {
ob_start();
$htmlOptions();
$contents = ob_get_clean();
$htmlOptions = $contentOrOptionWithBlock;
} elseif (is_string($contentOrOptionWithBlock)) {
$contents = $contentOrOptionWithBlock;
if (!is_array($htmlOptions)) {
$htmlOptions = [];
}
}
return $this->contentTag('script', $contents, $htmlOptions);
}
public function jsonEscape($str)
{
return str_replace(array_keys(self::$JSON_ESCAPE), self::$JSON_ESCAPE, $str);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait Number
{
public function numberToHumanSize($number, array $options = array())
{
$size = $number / 1024;
if ($size < 1024) {
$size = number_format($size, 1);
$size .= ' KB';
} else {
if (($size = ($size / 1024)) < 1024) {
$size = number_format($size, 1);
$size .= ' MB';
} elseif (($size = ($size / 1024)) < 1024) {
$size = number_format($size, 1);
$size .= ' GB';
} elseif (($size = ($size / 1024)) < 1024) {
$size = number_format($size, 1);
$size .= ' TB';
}
}
return $size;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait Tag
{
public function tag($name, array $options = array(), $open = false, $escape = false)
{
return '<' . $name . ' ' . $this->_options($options, $escape) . ($open ? '>' : ' />');
}
public function contentTag($name, $content, array $options = array(), $escape = false)
{
if ($content instanceof \Closure) {
$content = $content($this->view());
}
return $this->_content_tag_string($name, $content, $options, $escape);
}
protected function _options(array $options = array(), $escape = false)
{
$opts = array();
if (isset($options['allow_blank_attrs'])) {
$allow_blank_attrs = true;
unset($options['allow_blank_attrs']);
} else
$allow_blank_attrs = false;
foreach ($options as $opt => $val) {
# "class" attribute allows array.
if ($opt == 'class' && is_array($val)) {
$val = implode(' ', \Rails\Toolbox\ArrayTools::flatten($val));
}
if (is_array($val))
$val = implode(' ', $val);
if ((string)$val === '' && !$allow_blank_attrs)
continue;
if (is_int($opt))
$opts[] = $val;
else {
$escape && $val = htmlentities($val);
$opts[] = $opt . '="' . $val . '"';
}
}
return implode(' ', $opts);
}
protected function _parse_size(&$attrs)
{
if (is_int(strpos($attrs['size'], 'x'))) {
list ($attrs['width'], $attrs['height']) = explode('x', $attrs['size']);
unset($attrs['size']);
}
}
private function _content_tag_string($name, $content, array $options, $escape = false)
{
return '<' . $name . ' ' . $this->_options($options) . '>' . ($escape ? $this->h($content) : $content) . '</' . $name . '>';
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Rails\ActionView\Helper\Methods;
trait Text
{
private $_cycle_count = 0,
$_cycle_vars = [];
public function cycle()
{
$args = func_get_args();
# Clear vars if null was passed.
if (count($args) == 1 && $args[0] === []) {
$this->_cycle_vars = [];
$this->_cycle_count = 0;
return;
# Reset cycle if new options were given.
} elseif ($this->_cycle_vars && $this->_cycle_vars !== $args) {
$this->_cycle_vars = [];
$this->_cycle_count = 0;
}
if (empty($this->_cycle_vars))
$this->_cycle_vars = $args;
if ($this->_cycle_count > count($this->_cycle_vars) - 1)
$this->_cycle_count = 0;
$value = $this->_cycle_vars[$this->_cycle_count];
$this->_cycle_count++;
return $value;
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Rails\ActionView\Helper\WillPaginate;
use Rails;
use Rails\ActiveRecord\Collection;
abstract class AbstractRenderer
{
protected $collection;
protected $options;
protected $helper;
protected $pages;
protected $page;
protected $url;
public function __construct($helper, Collection $collection, array $options = [])
{
$this->collection = $collection;
$this->helper = $helper;
$this->options = array_merge($this->defaultOptions(), $options);
}
public function toHtml()
{
$html = implode(array_map(function($item){
if (is_int($item))
return $this->pageNumber($item);
else
return $this->$item();
}, $this->pagination()), $this->options['link_separator']);
return $this->options['container'] ? $this->htmlContainer($html) : $html;
}
protected function defaultOptions()
{
return [
'previous_label' => '&#8592; ' . $this->helper->t('actionview.helper.will_paginate.previous'),
'next_label' => $this->helper->t('actionview.helper.will_paginate.next') . ' &#8594;',
'container' => true,
'link_separator' => ' '
];
}
protected function pageNumber($page)
{
if ($page != $this->collection->currentPage())
return $this->link($page, $page, ['rel' => $this->relValue($page)]);
else
return $this->tag('span', $page, ['class' => 'current']);
}
protected function gap()
{
return '<span class="gap">&hellip;</span>';
}
protected function previousPage()
{
$num = $this->collection->currentPage() > 1 ?
$this->collection->currentPage() - 1 : false;
return $this->previousOrNextPage($num, $this->options['previous_label'], 'previousPage');
}
protected function nextPage()
{
$num = $this->collection->currentPage() < $this->collection->totalPages() ?
$this->collection->currentPage() + 1 : false;
return $this->previousOrNextPage($num, $this->options['next_label'], 'nextPage');
}
protected function previousOrNextPage($page, $text, $classname)
{
if ($page)
return $this->link($text, $page, ['class' => $classname]);
else
return $this->tag('span', $text, ['class' => $classname . ' disabled']);
}
protected function htmlContainer($html)
{
return $this->tag('div', $html, $this->containerAttributes());
}
protected function containerAttributes()
{
return ['class' => 'pagination'];
}
protected function relValue($page)
{
if ($this->collection->currentPage() - 1 == $page)
return 'prev' . ($page == 1 ? ' start' : '');
elseif ($this->collection->currentPage() + 1 == $page)
return 'next';
elseif ($page == 1)
return 'start';
}
protected function pagination()
{
$pages = $this->collection->totalPages();
$page = $this->collection->currentPage();
$pagination = [];
$pagination[] = 'previousPage';
$pagination[] = 1;
if ($pages < 10){
for ($i = 2; $i <= $pages; $i++){
$pagination[] = $i;
}
} elseif ($page > ($pages - 4)) {
$pagination[] = 'gap';
for ($i = ($pages - 4); $i < ($pages); $i++) {
$pagination[] = $i;
}
} elseif ($page > 4) {
$pagination[] = 'gap';
for ($i = ($page - 1); $i <= ($page + 2); $i++) {
$pagination[] = $i;
}
$pagination[] = 'gap';
} else {
if ($page >= 3){
for ($i = 2; $i <= $page+2; $i++) {
$pagination[] = $i;
}
} else {
for ($i = 2; $i <= 5; $i++) {
$pagination[] = $i;
}
}
$pagination[] = 'gap';
}
if ($pages >= 10) {
if ($pages == $page)
$pagination[] = $i;
else
$pagination[] = $pages;
}
$pagination[] = 'nextPage';
return $pagination;
}
protected function link($text, $page, array $attrs = [])
{
return $this->helper->linkTo($text, array_merge(['#index'], $this->params()->query_parameters(), ['page' => $page]), $attrs);
}
protected function tag($type, $content, array $attrs = [])
{
return $this->helper->contentTag($type, $content, $attrs);
}
protected function params()
{
return Rails::application()->dispatcher()->parameters();
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Rails\ActionView\Helper\WillPaginate;
class BootstrapRenderer extends AbstractRenderer
{
public function toHtml()
{
$html = implode(array_map(function($item){
if (is_int($item))
return $this->pageNumber($item);
else
return $this->$item();
}, $this->pagination()), $this->options['link_separator']);
return $this->htmlContainer($this->tag('ul', $html));
}
protected function pageNumber($page)
{
if ($page != $this->collection->currentPage())
return $this->tag('li', $this->link($page, $page, ['rel' => $this->relValue($page)]));
else
return $this->tag('li', $this->tag('span', $page), ['class' => 'current']);
}
protected function gap()
{
return $this->tag('li', $this->link('&hellip;', "#"), ['class' => 'disabled']);
}
protected function previousPage()
{
$num = $this->collection->currentPage() > 1 ?
$this->collection->currentPage() - 1 : false;
return $this->previousOrNextPage($num, $this->options['previous_label'], 'prev');
}
protected function nextPage()
{
$num = $this->collection->currentPage() < $this->collection->totalPages() ?
$this->collection->currentPage() + 1 : false;
return $this->previousOrNextPage($num, $this->options['next_label'], 'next');
}
protected function previousOrNextPage($page, $text, $classname)
{
if ($page)
return $this->tag('li', $this->link($text, $page), ['class' => $classname]);
else
return $this->tag('li', $this->tag('span', $text), ['class' => $classname . ' disabled']);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Rails\ActionView\Helper\WillPaginate;
use Rails\ActiveRecord\Collection;
class LegacyRenderer extends AbstractRenderer
{
}

31
lib/Rails/ActionView/Layout.php Executable file
View File

@ -0,0 +1,31 @@
<?php
namespace Rails\ActionView;
use Rails;
class Layout extends Template
{
protected $template_filename;
protected $filename;
public function __construct($layoutName, array $params = [], $locals = [])
{
$this->template_filename = $layoutName;
$this->filename = $this->resolve_layout_file($layoutName);
if (isset($params['contents'])) {
$this->_buffer = $params['contents'];
}
$locals && $this->setLocals($locals);
}
public function renderContent()
{
ob_start();
require $this->filename;
$this->_buffer = ob_get_clean();
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Rails\ActionView;
class Partial extends Template
{
public function render_content()
{
ob_start();
$this->_init_render();
return ob_get_clean();
}
public function t($name, array $params = [])
{
return parent::t($name, $params);
}
}

288
lib/Rails/ActionView/Template.php Executable file
View File

@ -0,0 +1,288 @@
<?php
namespace Rails\ActionView;
use Rails;
use Rails\ActionView\Template\Exception;
class Template extends Base
{
protected $_filename;
/**
* Layout filename.
*/
protected $_layout;
protected $_layout_name;
private $_params;
private $_template_token;
private $_initial_ob_level;
/**
* Used by Inline responder.
*/
private $_inline_code;
/**
* To render for first time, render() must be
* called, and this will be set to true.
* Next times render() is called, it will need
* parameters in order to work.
*/
private $_init_rendered = false;
/**
* Template file.
*/
protected $template_file;
/**
* Full template path.
*/
protected $template_filename;
protected $type;
protected $params;
protected $contents;
protected $inline_code;
protected $lambda;
/**
* $render_params could be:
* - A string, that will be taken as "file".
* - An array specifying the type of parameter:
* - file: a file that will be required. Can be a relative path (to views folder) or absolute path.
* - contents: string that will be included to the layout, if any.
* - inline: inline code that will be evaluated.
* - lambda: pass an anonyomous function that will be bound to the template and ran.
*/
public function __construct($render_params, array $params = [], $locals = array())
{
if (!is_array($render_params)) {
$render_params = ['file' => $render_params];
}
$this->type = key($render_params);
switch ($this->type) {
case 'file':
$this->template_file = array_shift($render_params);
break;
/**
* Allows to receive the contents for this template, i.e.
* no processing is needed. It will just be added to the layout, if any.
*/
case 'contents':
$this->contents = array_shift($render_params);
break;
/**
* Contents will be evalued.
*/
case 'inline':
$this->inline_code = array_shift($render_params);
break;
case 'lambda':
$this->lambda = array_shift($render_params);
break;
}
$this->params = $params;
if (isset($params['locals'])) {
$locals = $params['locals'];
unset($params['locals']);
}
$locals && $this->setLocals($locals);
}
public function renderContent()
{
Rails\ActionView\ViewHelpers::load();
$this->_set_initial_ob_level();
if (!$this->_init_rendered) {
ob_start();
$this->_init_rendered = true;
$this->_init_render();
$this->_buffer = ob_get_clean();
if (!$this->_validate_ob_level()) {
$status = ob_get_status();
throw new Exception\OutputLeakedException(
sprintf('Buffer level: %s; File: %s<br />Topmost buffer\'s contents: <br />%s',
$status['level'], substr($this->_filename, strlen(Rails::root()) + 1), htmlentities(ob_get_clean()))
);
}
}
return $this;
}
protected function render(array $params)
{
$type = key($params);
switch ($type) {
case 'template':
$file = array_shift($params);
$template = new self(['file' => $file]);
$template->renderContent();
return $template->content();
break;
}
}
public function content($name = null)
{
if (!func_num_args())
return $this->_buffer;
else
return parent::content($name);
}
public function contents()
{
return $this->content();
}
/**
* Finally prints all the view.
*/
public function get_buffer_and_clean()
{
$buffer = $this->_buffer;
$this->_buffer = null;
return $buffer;
}
public function t($name, array $params = [])
{
if (is_array($name)) {
$params = $name;
$name = current($params);
}
if (strpos($name, '.') === 0) {
if (is_int(strpos($this->template_filename, Rails::config()->paths->views->toString()))) {
$parts = array();
$path = substr(
$this->template_filename, strlen(Rails::config()->paths->views) + 1,
strlen(pathinfo($this->template_filename, PATHINFO_BASENAME)) * -1
)
. pathinfo($this->template_filename, PATHINFO_FILENAME);
foreach (explode('/', $path) as $part) {
$parts[] = ltrim($part, '_');
}
$name = implode('.', $parts) . $name;
}
}
return parent::t($name, $params);
}
protected function _init_render()
{
$layout_wrap = !empty($this->params['layout']);
if ($layout_wrap) {
$layout_file = $this->resolve_layout_file($this->params['layout']);
if (!is_file($layout_file))
throw new Exception\LayoutMissingException(
sprintf("Missing layout '%s' [ file => %s ]",
$this->params['layout'], $layout_file)
);
ob_start();
}
switch ($this->type) {
case 'file':
$this->build_template_filename();
if (!is_file($this->template_filename)) {
throw new Exception\TemplateMissingException(
sprintf("Missing template file %s", $this->template_filename)
);
}
require $this->template_filename;
break;
case 'contents':
echo $this->contents;
break;
case 'inline':
eval('?>' . $this->inline_code . '<?php ');
break;
case 'lambda':
$lambda = $this->lambda;
$this->lambda = null;
$lambda = $lambda->bindTo($this);
$lambda();
break;
}
if ($layout_wrap) {
$this->_buffer = ob_get_clean();
$layout = new Layout($layout_file, ['contents' => $this->_buffer], $this->locals);
$layout->renderContent();
echo $layout->content();
// require $layout_file;
}
}
private function _set_initial_ob_level()
{
$status = ob_get_status();
$this->_initial_ob_level = $this->_get_ob_level();
}
private function _validate_ob_level()
{
$status = ob_get_status();
return $this->_initial_ob_level == $this->_get_ob_level();
}
protected function resolve_layout_file($layout)
{
if (strpos($layout, '/') === 0 || preg_match('/^[A-Za-z]:(?:\\\|\/)/', $layout))
return $layout;
else {
if (is_int(strpos($layout, '.')))
return Rails::config()->paths->layouts . '/' . $layout;
else
return Rails::config()->paths->layouts . '/' . $layout . '.php';
}
}
private function build_template_filename()
{
$template = $this->template_file;
if (strpos($template, '/') === 0 || preg_match('/^[A-Za-z]:(?:\\\|\/)/', $template))
$this->template_filename = $template;
else {
if (is_int(strpos($template, '.'))) {
$this->template_filename = Rails::config()->paths->views . '/' . $template;
} else {
$this->template_filename = Rails::config()->paths->views . '/' . $template . '.php';
}
}
}
private function _get_ob_level()
{
$status = ob_get_status();
return !empty($status['level']) ? $status['level'] : 0;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Template\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Rails\ActionView\Template\Exception;
class LayoutMissingException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
protected $title = "Layout missing";
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Template\Exception;
class OutputLeakedException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActionView\Template\Exception;
class TemplateMissingException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,109 @@
<?php
namespace Rails\ActionView;
use Rails;
class ViewHelpers
{
const BASE_HELPER_NAME = 'Rails\ActionView\Helper\Base';
static protected $registry = [];
/**
* Helpers instances.
*/
static protected $helpers = [];
static protected $helpersLoaded = false;
/**
* Helpers queue list. They will be available in this order.
*/
static protected $queue = [];
/**
* Searches for the helper that owns $method.
*
* @return object | false
*/
static public function findHelperFor($method)
{
if (isset(self::$registry[$method])) {
return self::$helpers[self::$registry[$method]];
}
foreach (self::$helpers as $helperName => $helper) {
if (method_exists($helper, $method)) {
self::$registry[$method] = $helperName;
return $helper;
}
}
return false;
}
static public function getBaseHelper()
{
return self::getHelper(self::BASE_HELPER_NAME);
}
static public function getHelper($name)
{
return self::$helpers[$name];
}
/**
* Adds class names to the helper queues. These classes will be instantiated
* in includeHelpers().
*
* @var name string helper name
* @see load()
*/
static public function addHelper($className)
{
self::$queue[] = $className;
}
static public function addHelpers(array $classNames)
{
self::$queue = array_merge(self::$queue, $className);
}
/**
* For application helpers. Class names passed will be appended with "Helper".
*/
static public function addAppHelpers(array $helpers)
{
self::$queue = array_merge(self::$queue, array_map(function($c) { return $c . 'Helper'; }, $helpers));
}
/**
* Actually include the helpers files.
*
* Application and current controller's helper are added here,
* to make sure they're top on the list.
*/
static public function load()
{
if (!self::$helpersLoaded) {
if (($router = Rails::application()->dispatcher()->router()) && ($route = $router->route())) {
$controllerHelper = Rails::services()->get('inflector')->camelize($route->controller()) . 'Helper';
array_unshift(self::$queue, $controllerHelper);
}
$appHelper = 'ApplicationHelper';
array_unshift(self::$queue, $appHelper);
foreach (array_unique(self::$queue) as $name) {
try {
Rails::loader()->loadClass($name);
self::$helpers[$name] = new $name();
} catch (Rails\Loader\Exception\ExceptionInterface $e) {
}
}
# Add base helper
self::$helpers[self::BASE_HELPER_NAME] = new Helper\Base();
self::$helpersLoaded = true;
}
}
}

69
lib/Rails/ActionView/Xml.php Executable file
View File

@ -0,0 +1,69 @@
<?php
namespace Rails\ActionView;
/**
* This file could belong somewhere else.
*/
class Xml
{
private $_buffer = '';
public function __call($method, $params)
{
array_unshift($params, $method);
call_user_func_array([$this, 'create'], $params);
}
public function instruct()
{
$this->_buffer .= '<?xml version="1.0" encoding="UTF-8"?>'."\n";
}
/**
* $content could be passed as second argument.
*/
public function create($root, $attrs, $content = null)
{
$this->_buffer .= '<'.$root;
if (!is_array($attrs)) {
$content = $attrs;
$attrs = [];
}
if ($attrs) {
$attrs_str = [];
foreach ($attrs as $name => $val)
$attrs_str[] = $name . '="'.htmlspecialchars($val).'"';
$this->_buffer .= ' ' . implode(' ', $attrs_str);
}
if (!$content) {
$this->_buffer .= ' />';
} else {
$this->_buffer .= ">\n";
if (is_string($content))
$this->_buffer .= $content;
elseif ($content instanceof Closure)
$this->_buffer .= $content();
else
throw new Exception\InvalidArgumentError(
sprintf('Expecting Closure or string as third argument, %s passed.', gettype($content))
);
$this->_buffer .= '</'.$root.'>';
}
}
public function build($el, array $params = [])
{
$this->_buffer .= (new \Rails\Xml\Xml($el, $params))->output() . "\n";
}
public function output()
{
!$this->_buffer && $this->create();
return $this->_buffer;
}
}

View File

@ -0,0 +1,366 @@
<?php
namespace Rails\ActiveModel;
use Closure;
use Rails;
class Collection implements \ArrayAccess, \Iterator
{
/* ArrayAccess { */
protected $members = array();
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->members[] = $value;
} else {
$this->members[$offset] = $value;
}
}
public function offsetExists($offset)
{
return isset($this->members[$offset]);
}
public function offsetUnset($offset)
{
unset($this->members[$offset]);
}
public function offsetGet($offset)
{
return isset($this->members[$offset]) ? $this->members[$offset] : null;
}
/* } Iterator {*/
protected $position = 0;
public function rewind()
{
reset($this->members);
$this->position = key($this->members);
}
public function current()
{
return $this->members[$this->position];
}
public function key()
{
return key($this->members);
}
public function next()
{
next($this->members);
$this->position = key($this->members);
}
public function valid()
{
return array_key_exists($this->position, $this->members);
}
/* } */
public function __construct(array $members = array())
{
$this->members = $members;
}
public function merge()
{
foreach (func_get_args() as $coll) {
if ($coll instanceof self)
$coll = $coll->members();
$this->members = array_merge($this->members, $coll);
}
return $this;
}
public function members()
{
return $this->members;
}
# Another way to get members.
public function toArray()
{
return $this->members;
}
/**
* Each (experimental)
*
* If string is passed, it'll be taken as method name to be called.
* Eg. $posts->each('destroy'); - All posts will be destroyed.
* In this case, $params for the method may be passed.
*
* A Closure may also be passed.
*/
public function each($function, array $params = array())
{
if (is_string($function)) {
foreach ($this->members() as $m) {
call_user_func_array(array($m, $function), $params);
}
} elseif ($function instanceof Closure) {
foreach ($this->members() as $idx => $m) {
$function($m, $idx);
}
} else {
throw new Exception\InvalidArgumentException(
sprintf('Argument must be an either a string or a Closure, %s passed.', gettype($model))
);
}
}
public function reduce($var, Closure $block)
{
foreach ($this->members() as $m) {
$var = $block($var, $m);
}
return $var;
}
public function sort(Closure $criteria)
{
usort($this->members, $criteria);
$this->rewind();
return $this;
}
public function unshift($model)
{
if ($model instanceof Base)
$model = array($model);
elseif (!$model instanceof self) {
throw new Exception\InvalidArgumentException(
sprintf('Argument must be an instance of either ActiveRecord\Base or ActiveRecord\Collection, %s passed.', gettype($model))
);
}
foreach ($model as $m)
array_unshift($this->members, $m);
return $this;
}
/**
* Searches objects for a property with a value and returns object.
*/
public function search($prop, $value)
{
foreach ($this->members() as $obj) {
if ($obj->$prop == $value)
return $obj;
}
return false;
}
# Returns a Collection with the models that matches the options.
# Eg: $posts->select(array('is_active' => true, 'user_id' => 4));
# If Closure passed as $opts, the model that returns == true on the function
# will be added.
public function select($opts)
{
$objs = array();
if (is_array($opts)) {
foreach ($this as $obj) {
foreach ($opts as $prop => $cond) {
if (!$obj->$prop || $obj->$prop != $cond)
continue;
$objs[] = $obj;
}
}
} elseif ($opts instanceof Closure) {
foreach ($this->members() as $obj) {
$opts($obj) && $objs[] = $obj;
}
}
return new self($objs);
}
/**
* Removes members according to attributes.
*/
public function remove($attrs)
{
!is_array($attrs) && $attrs = array('id' => $attrs);
foreach ($this->members() as $k => $m) {
foreach ($attrs as $a => $v) {
if ($m->getAttribute($a) != $v)
continue 2;
}
unset($this->members[$k]);
}
$this->members = array_values($this->members);
return $this;
}
public function max($criteria)
{
if (!$this->members)
return false;
$current = key($this->members);
if (count($this->members) < 2)
return $this->members[$current];
$max = $this->members[$current];
if ($criteria instanceof Closure) {
$params = $this;
foreach($params as $current) {
if (!$next = next($params))
break;
$max = $criteria($max, $next);
}
} else {
}
return $max;
}
# Deprecated in favor of none.
public function blank()
{
return empty($this->members);
}
public function none()
{
return empty($this->members);
}
public function any()
{
return (bool)$this->members;
}
/**
* TODO: xml shouldn't be created here.
*/
public function toXml()
{
if ($this->blank())
return;
$t = get_class($this->current());
$t = $t::t();
$xml = '<?xml version="1.0" encoding="UTF-8"?>';
$xml .= '<' . $t . '>';
foreach ($this->members() as $obj) {
$xml .= $obj->toXml(array('skip_instruct' => true));
}
$xml .= '</' . $t . '>';
return $xml;
}
public function asJson()
{
$json = [];
foreach ($this->members as $member)
$json[] = $member->asJson();
return $json;
}
public function toJson()
{
return json_encode($this->asJson());
}
/**
* Returns an array of the attributes in the models.
* $attrs could be a string of a single attribute we want, and
* an indexed array will be returned.
* If $attrs is an array of many attributes, an associative array will be returned.
*
* $collection->attributes(['id', 'createdAt']);
* No -> $collection->attributes('id', 'name');
*/
public function getAttributes($attrs)
{
$models_attrs = array();
if (is_string($attrs)) {
foreach ($this as $m) {
// if (Rails::config()->ar2) {
// $models_attrs[] = $m->$attrs();
// } else {
$models_attrs[] = $m->$attrs;
// }
}
} else {
foreach ($this->members() as $m) {
$model_attrs = [];
foreach ($attrs as $attr) {
// if (!Rails::config()->ar2) {
$model_attrs[$attr] = $m->$attr;
// } else {
// $model_attrs[$attr] = $m->$attr();
// }
}
$models_attrs[] = $model_attrs;
}
}
return $models_attrs;
}
public function size()
{
return count($this->members);
}
# Removes dupe models based on id or other attribute.
public function unique($attr = 'id')
{
$checked = array();
foreach ($this->members() as $k => $obj) {
if (in_array($obj->$attr, $checked))
unset($this->members[$k]);
else
$checked[] = $obj->$attr;
}
return $this;
}
# array_slices the collection.
public function slice($offset, $length = null)
{
$clone = clone $this;
$clone->members = array_slice($clone->members, $offset, $length);
return $clone;
}
public function deleteIf(Closure $conditions)
{
$deleted = false;
foreach ($this->members() as $k => $m) {
if ($conditions($m)) {
unset($this[$k]);
$deleted = true;
}
}
if ($deleted)
$this->members = array_values($this->members);
}
public function replace($replacement)
{
if ($replacement instanceof self)
$this->members = $replacement->members();
elseif (is_array($replacement))
$this->members = $replacement;
else
throw new Exception\InvalidArgumentException(sprintf("%s expects a %s or an array, %s passed", __METHOD__, __CLASS__, gettype($replacement)));
}
}

107
lib/Rails/ActiveModel/Errors.php Executable file
View File

@ -0,0 +1,107 @@
<?php
namespace Rails\ActiveModel;
class Errors
{
const BASE_ERRORS_INDEX = 'model_base_errors';
private $errors = array();
public function add($attribute, $msg = null)
{
if (!isset($this->errors[$attribute]))
$this->errors[$attribute] = array();
$this->errors[$attribute][] = $msg;
}
public function addToBase($msg)
{
$this->base($msg);
}
public function base($msg)
{
$this->add(self::BASE_ERRORS_INDEX, $msg);
}
public function on($attribute)
{
if (!isset($this->errors[$attribute]))
return null;
elseif (count($this->errors[$attribute]) == 1)
return current($this->errors[$attribute]);
else
return $this->errors[$attribute];
}
public function onBase()
{
return $this->on(self::BASE_ERRORS_INDEX);
}
# $glue is a string that, if present, will be used to
# return the messages imploded.
public function fullMessages($glue = null)
{
$fullMessages = array();
foreach ($this->errors as $attr => $errors) {
foreach ($errors as $msg) {
if ($attr == self::BASE_ERRORS_INDEX)
$fullMessages[] = $msg;
else
$fullMessages[] = $this->_propper_attr($attr) . ' ' . $msg;
}
}
if ($glue !== null)
return implode($glue, $fullMessages);
else
return $fullMessages;
}
public function invalid($attribute)
{
return isset($this->errors[$attribute]);
}
# Deprecated in favor of none().
public function blank()
{
return !(bool)$this->errors;
}
public function none()
{
return !(bool)$this->errors;
}
public function any()
{
return !$this->blank();
}
public function all()
{
return $this->errors;
}
public function count()
{
$i = 0;
foreach ($this->errors as $errors) {
$i += count($errors);
}
return $i;
}
private function _propper_attr($attr)
{
$attr = ucfirst(strtolower($attr));
if (is_int(strpos($attr, '_'))) {
$attr = str_replace('_', ' ', $attr);
}
return $attr;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActiveModel\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActiveModel\Exception;
class InvalidArgumentException extends \Rails\Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,174 @@
<?php
namespace Rails\ActiveRecord;
use PDOStatement;
use Rails;
use Rails\ActiveRecord\Connection;
abstract class ActiveRecord
{
static private $_prev_connection = '';
static private
$_connection_data = [],
/**
* The different connections.
*
* $name => ActiveRecord\Connection
*/
$_connections = [];
/**
* Name of the active connection.
*/
static private $activeConnectionName;
/**
* If an error ocurred when calling execute_sql(),
* it will be stored here.
*/
static private $_last_error;
static public function setLastError(array $error, $connection_name = null)
{
self::$_last_error = $error;
}
/**
* Adds a connection configuration to the list of available connections.
*/
static public function addConnection($config, $name)
{
self::$_connection_data[$name] = $config;
}
/**
* Sets the default active connection.
*/
static public function setConnection($name)
{
self::create_connection($name);
self::$activeConnectionName = $name;
return true;
}
/**
* Stablishes a connection from the available connections list.
*/
static protected function create_connection($name)
{
# If the connection is already created, return.
if (isset(self::$_connections[$name]))
return;
elseif (self::connectionExists($name))
self::$_connections[$name] = new Connection(self::$_connection_data[$name], $name);
else
throw new Exception\RuntimeException(
sprintf("Connection '%s' does not exist", $name)
);
}
static public function connectionExists($name)
{
return isset(self::$_connection_data[$name]);
}
static public function set_environment_connection($environment)
{
if (self::connectionExists($environment))
self::setConnection($environment);
elseif (self::connectionExists('default'))
self::setConnection('default');
}
/**
* Returns connection. By default, returns the
* currenct active one, or the one matching the $name.
*
* @return ActiveRecord\Connection
*/
static public function connection($name = null)
{
if ($name === null) {
$name = self::$activeConnectionName;
if (!$name)
throw new Exception\RuntimeException("No database connection is active");
} else {
self::create_connection($name);
}
return self::$_connections[$name];
}
/**
* Returns active connection's name.
*/
static public function activeConnectionName()
{
return self::$activeConnectionName;
}
/**
* Returns active connection's data.
*/
static public function activeConnectionData()
{
if (!self::$activeConnectionName)
throw new Exception\RuntimeException("No database connection is active");
return array_merge(self::$_connection_data[self::$activeConnectionName], ['name' => self::$activeConnectionName]);
}
/**
* Used by the system when creating schema files.
*/
static public function connections()
{
return self::$_connection_data;
}
static public function lastError()
{
return self::$_last_error;
}
static private function _include_additional_connection($name)
{
$config = Rails::application()->config()->active_record;
if (!empty($config['additional_connections']) && !empty($config['additional_connections'][$name])) {
self::addConnection($config['additional_connections'][$name], $name);
return true;
}
return false;
}
/**
* This could be somewhere else.
*/
static public function proper_adapter_name($adapter_name)
{
$adapter_name = strtolower($adapter_name);
switch ($adapter_name) {
case 'mysql':
return 'MySql';
break;
case 'sqlite':
return 'Sqlite';
break;
default:
return $adapter_name;
break;
}
}
static public function restore_connection()
{
if (self::$_prev_connection) {
self::setConnection(self::$_prev_connection);
self::$_prev_connection = null;
}
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Rails\ActiveRecord\Adapter;
use Rails\ActiveRecord\ActiveRecord;
use Rails\ActiveRecord\Connection;
use Rails\ActiveRecord\Relation;
use Rails\ActiveRecord\Relation\AbstractRelation;
abstract class AbstractQueryBuilder extends Relation
{
protected
$_sql,
$_params,
$_stmt,
$_row_count,
$_will_paginate,
$complete_sql,
$query,
$connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function executeSql()
{
$this->_stmt = $this->connection->executeSql($this->_params);
if ($this->will_paginate)
$this->calculate_found_rows();
}
abstract protected function calculate_found_rows();
abstract protected function _build_sql();
public function build_sql(AbstractRelation $query)
{
$complete_sql = $query->complete_sql();
$this->query = $query;
if ($complete_sql) {
list ($sql, $params) = $complete_sql;
array_unshift($params, $sql);
$this->_params = $params;
$this->will_paginate = $query->will_paginate();
} else {
$this->will_paginate = $query->will_paginate();
$this->_build_sql();
}
}
public function stmt()
{
return $this->_stmt;
}
public function row_count()
{
return $this->_row_count;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Rails\ActiveRecord\Adapter;
use PDO;
use PDOStatement;
use Rails;
use Rails\ActiveRecord\ActiveRecord;
use Rails\ActiveRecord\Connection;
abstract class AbstractTable
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActiveRecord\Adapter\Exception;
class BadMethodCallException extends \Rails\Exception\BadMethodCallException implements ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActiveRecord\Adapter\Exception;
interface ExceptionInterface
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Rails\ActiveRecord\Adapter\Exception;
class RuntimeException extends \Rails\Exception\RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,101 @@
<?php
namespace Rails\ActiveRecord\Adapter\MySql;
use Rails\ActiveRecord\Adapter\Exception;
use Rails\ActiveRecord\Adapter\AbstractQueryBuilder;
class QueryBuilder extends AbstractQueryBuilder
{
protected function calculate_found_rows()
{
$rows = $this->connection->query("SELECT FOUND_ROWS()");
$rows = array_shift($rows);
$rows = array_shift($rows);
$this->_row_count = (int)$rows;
}
protected function _build_sql()
{
$query = $this->query;
$params = [];
$sql = 'SELECT';
if ($this->will_paginate)
$sql .= ' SQL_CALC_FOUND_ROWS';
if ($query->distinct)
$sql .= ' DISTINCT';
$sql .= ' ' . ($query->select ? implode(', ', $query->select) : '`' . $query->from . '`.*');
$sql .= " FROM `" . $query->from . "`";
$sql .= ' ' . implode(' ', $query->joins);
if ($where = $this->_build_where_clause($query)) {
$sql .= " WHERE " . $where[0];
$params = $where[1];
unset($where);
}
if ($query->group)
$sql .= ' GROUP BY ' . implode(', ', $query->group);
if ($query->order)
$sql .= ' ORDER BY ' . implode(', ', $query->order);
if ($query->having) {
$sql .= ' HAVING ' . implode(' ', $query->having);
$params = array_merge($params, $query->having_params);
}
if ($query->offset && $query->limit)
$sql .= ' LIMIT ' . $query->offset . ', ' . $query->limit;
elseif ($query->limit)
$sql .= ' LIMIT ' . $query->limit;
array_unshift($params, $sql);
$this->_params = $params;
}
private function _build_where_clause($query)
{
if (!$query->where)
return;
// if (end($query->_where) == 'name in (?)')
// vpe($query->where_params);
$where = $where_params = [];
$param_count = 0;
foreach ($query->where as $condition) {
# Case: ["foo" => $foo, "bar_baz" => $bar];
if (is_array($condition)) {
foreach ($condition as $column => $value) {
$where[] = $column . ' = ?';
$where_params[] = $value;
}
} else {
if ($count = substr_count($condition, '?')) {
foreach (range(0, $count - 1) as $i) {
if (!array_key_exists($param_count, $query->where_params))
throw new Exception\RuntimeException(sprintf("Value for question mark placeholder for WHERE clause section wasn't found: %s", $condition));
if (is_array($query->where_params[$param_count])) {
$condition = preg_replace('/\?/', implode(', ', array_fill(0, count($query->where_params[$param_count]), '?')), $condition, 1);
$where_params = array_merge($where_params, $query->where_params[$param_count]);
} else {
$where_params[] = $query->where_params[$param_count];
}
$param_count++;
}
} elseif ($count = substr_count($condition, ':')) {
$where_params = $query->where_params;
}
$where[] = $condition;
}
}
return [implode(' AND ', $where), $where_params];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Rails\ActiveRecord\Adapter\MySql;
use Rails;
use Rails\ActiveRecord\Connection;
use Rails\ActiveRecord\Adapter\AbstractTable;
use Rails\ActiveRecord\Exception;
class Table/* extends AbstractTable*/
{
static public function fetchSchema(Connection $connection, $table_name)
{
$stmt = $connection->executeSql("DESCRIBE `".$table_name."`");
if (!$rows = $stmt->fetchAll()) {
throw new Exception\RuntimeException(
sprintf("Couldn't DESCRIBE %s:\n%s", $table_name, var_export($stmt->errorInfo(), true))
);
}
$table_data = $table_indexes = $pri = $uni = [];
foreach ($rows as $row) {
$data = ['type' => $row['Type']];
if (strpos($row['Type'], 'enum') === 0) {
$enum_values = [];
foreach (explode(',', substr($row['Type'], 5, -1)) as $opt)
$enum_values[] = substr($opt, 1, -1);
$data['enum_values'] = $enum_values;
}
$table_data[$row['Field']] = $data;
}
$stmt = $connection->executeSql("SHOW INDEX FROM `".$table_name."`");
$idxs = $stmt->fetchAll();
if ($idxs) {
foreach ($idxs as $idx) {
if ($idx['Key_name'] == 'PRIMARY') {
$pri[] = $idx['Column_name'];
} elseif ($idx['Non_unique'] === '0') {
$uni[] = $idx['Column_name'];
}
}
}
if ($pri)
$table_indexes['pri'] = $pri;
elseif ($uni)
$table_indexes['uni'] = $uni;
return [$table_data, $table_indexes];
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace Rails\ActiveRecord\Adapter\Sqlite;
use Rails\ActiveRecord\Adapter\Exception;
use Rails\ActiveRecord\Adapter\AbstractQueryBuilder;
class QueryBuilder extends AbstractQueryBuilder
{
public function calculate_found_rows()
{
$this->query->except('limit')->except('offset')->except('select')->select('COUNT(*) as count_all');
$this->_build_sql();
if ($stmt = $this->connection->executeSql($this->_params)) {
$rows = $stmt->fetchAll();
if (isset($rows[0]['count_all']))
return $rows[0]['count_all'];
}
}
protected function _build_sql()
{
$query = $this->query;
$params = [];
$sql = 'SELECT';
if ($query->distinct)
$sql .= ' DISTINCT';
$sql .= ' ' . ($query->select ? implode(', ', $query->select) : '`' . $query->from . '`.*');
$sql .= " FROM `" . $query->from . "`";
$sql .= ' ' . implode(' ', $query->joins);
if ($where = $this->_build_where_clause($query)) {
$sql .= " WHERE " . $where[0];
$params = $where[1];
unset($where);
}
if ($query->group)
$sql .= ' GROUP BY ' . implode(', ', $query->group);
if ($query->order)
$sql .= ' ORDER BY ' . implode(', ', $query->order);
if ($query->having) {
$sql .= ' HAVING ' . implode(' ', $query->having);
$params = array_merge($params, $query->having_params);
}
if ($query->offset && $query->limit)
$sql .= ' LIMIT ' . $query->offset . ', ' . $query->limit;
elseif ($query->limit)
$sql .= ' LIMIT ' . $query->limit;
array_unshift($params, $sql);
$this->_params = $params;
}
private function _build_where_clause($query) {
if (!$query->where)
return;
$where = $where_params = [];
$param_count = 0;
foreach ($query->where as $condition) {
# Case: ["foo" => $foo, "bar" => $bar];
if (is_array($condition)) {
foreach ($condition as $column => $value) {
$where[] = '`' . $column . '` = ?';
$where_params[] = $value;
}
} else {
if ($count = substr_count($condition, '?')) {
foreach (range(0, $count - 1) as $i) {
if (!isset($query->where_params[$param_count]))
throw new Exception\RuntimeException(sprintf("Value for question mark placeholder for WHERE clause part wasn't found (%s)", $condition));
if (is_array($query->where_params[$param_count])) {
$condition = preg_replace('/\?/', implode(', ', array_fill(0, count($query->where_params[$param_count]), '?')), $condition, 1);
}
$where_params[] = $query->where_params[$param_count];
$param_count++;
}
} elseif ($count = substr_count($condition, ':')) {
$where_params = $query->where_params;
}
$where[] = $condition;
}
}
return [implode(' AND ', $where), $where_params];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Rails\ActiveRecord\Adapter\Sqlite;
use PDO;
use Rails;
use Rails\ActiveRecord\Connection;
use Rails\ActiveRecord\Adapter\AbstractTable;
class Table/* extends AbstractTable*/
{
static public function fetchSchema(Connection $connection, $table_name)
{
$stmt = $connection->executeSql("PRAGMA table_info(`".$table_name."`);");
if (!$rows = $stmt->fetchAll(PDO::FETCH_ASSOC)) {
return $stmt;
}
$table_data = $pri = $uni = [];
$table_indexes = [
'pri' => [],
'uni' => []
];
foreach ($rows as $row) {
$data = ['type' => $row['type']];
$table_data[$row['name']] = $data;
if ($row['pk'])
$table_indexes['pri'][] = $row['name'];
}
return [$table_data, $table_indexes];
}
}

1029
lib/Rails/ActiveRecord/Base.php Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
<?php

View File

@ -0,0 +1,265 @@
<?php
namespace Rails\ActiveRecord\Base\Methods;
use Rails;
use Rails\ActiveRecord\Exception;
/**
* Attributes are properties that correspond to a column in the table.
* However, these "properties" are actually stored in the actual instance
* property $attributes.
*
* Models *should* define getters and setters for each attribute, but they
* can be called overloadingly (see Rails\ActiveRecord\Base::__call()).
* I say *should* because it is said that overloading is bad for performance.
*
* For convenience (I'd say), in the case of getter methods, the "get" prefix is
* omitted (except for methods that require a parameter, like getAttribute($attrName)),
* so the expected name of the getter methods is the camel-cased name of the corresponding attribute,
* for example createdAt(). This method can either check itself if the index for the attribute exists
* in the $attributes array and return it, or simply return getAttribute($attrName).
*
* Setter methods have the "set" prefix, and they should set the new value in the $attributes array.
*/
trait AttributeMethods
{
/**
* Calling attributes throgh magic methods would be like:
* $post->createdAt()
* The corresponding column for this attribute would be "created_at",
* therefore, the attribute name will be converted.
* For some cases, to disable the camel to lower conversion,
* this property can be set to false.
*/
static protected $convertAttributeNames = true;
/**
* Expected to hold only the model's attributes.
*/
protected $attributes = [];
/**
* Holds data grabbed from the database for models
* without a primary key, to be able to update them.
* Hoever, models should always have a primary key.
*/
private $storedAttributes = array();
private $changedAttributes = array();
static public function convertAttributeNames($value = null)
{
if (null !== $value) {
static::$convertAttributeNames = (bool)$value;
} else {
return static::$convertAttributeNames;
}
}
static public function isAttribute($name)
{
// if (!Rails::config()->ar2) {
// return static::table()->columnExists(static::properAttrName($name));
// } else {
return static::table()->columnExists($name);
// }
}
/**
* This method allows to "overloadingly" get attributes this way:
* $model->parentId; instead of $model->parent_id.
*/
static public function properAttrName($name)
{
if (static::convertAttributeNames()) {
$name = \Rails::services()->get('inflector')->underscore($name);
}
return $name;
}
/**
* @throw Exception\InvalidArgumentException
*/
public function getAttribute($name)
{
if (array_key_exists($name, $this->attributes)) {
return $this->attributes[$name];
// } elseif (!Rails::config()->ar2 && static::table()->columnExists(static::properAttrName($name))) {
// return null;
} elseif (static::table()->columnExists($name)) {
return null;
}
throw new Exception\InvalidArgumentException(
sprintf("Trying to get non-attribute '%s' from model %s", $name, get_called_class())
);
}
public function setAttribute($name, $value)
{
if (!self::isAttribute($name)) {
throw new Exception\InvalidArgumentException(
sprintf("Trying to set non-attribute '%s' for model %s", $name, get_called_class())
);
}
if ((string)$this->getAttribute($name) != (string)$value) {
$this->setChangedAttribute($name, $value);
}
$this->attributes[$name] = $value;
return $this;
}
public function issetAttribute($name)
{
if (!self::isAttribute($name)) {
throw new Exception\InvalidArgumentException(
sprintf("'%s' isn't an attribute for model %s", $name, get_called_class())
);
}
return isset($this->attributes[$name]);
}
/**
* Add/change attributes to model
*
* Filters protected attributes of the model.
* Also calls the "getAttribute()" method, if exists, of the model,
* in case extra operation is needed when changing certain attribute.
* It's intended to be an equivalent to "def attribute=(val)" in rails.
* E.g. "is_held" for post model.
*
* @see _run_setter()
*/
public function assignAttributes(array $attrs, array $options = [])
{
if (!$attrs) {
return;
}
if (empty($options['without_protection'])) {
$this->filterProtectedAttributes($attrs);
}
// if (!Rails::config()->ar2) {
// foreach ($attrs as $attr => $v) {
// if ($this->setterExists($attr)) {
// $this->_run_setter($attr, $v);
// } else {
// $this->$attr = $v;
// }
// }
// return;
// }
// $inflector = Rails::services()->get('inflector');
// $reflection = new \ReflectionClass(get_called_class());
foreach ($attrs as $attrName => $value) {
if (self::isAttribute($attrName)) {
$this->setAttribute($attrName, $value);
} else {
if ($setterName = $this->setterExists($attrName)) {
$this->$setterName($value);
// $setter = 'set' . $inflector->camelize($attrName);
} elseif (self::hasPublicProperty($attrName)) {
$this->$attrName = $value;
// if ($reflection->hasMethod($setter) && $reflection->getMethod($setter)->isPublic()) {
// $this->$setter($value);
} else {
throw new Exception\RuntimeException(
sprintf("Can't write unknown attribute '%s' for model %s", $attrName, get_called_class())
);
}
}
}
}
public function attributes()
{
return $this->attributes;
}
/**
* The changedAttributes array is filled upon updating a record.
* When updating, the stored data of the model is retrieved and checked
* against the data that will be saved. If an attribute changed, the old value
* is stored in this array.
*
* Calling a method that isn't defined, ending in Changed, for example nameChanged() or
* categoryIdChanged(), is the same as calling attributeChanged('name') or
* attributeChanged('category_id').
*
* @return bool
* @see attributeWas()
*/
public function attributeChanged($attr)
{
return array_key_exists($attr, $this->changedAttributes);
}
/**
* This method returns the previous value of an attribute before updating a record. If
* it was not changed, returns null.
*/
public function attributeWas($attr)
{
return $this->attributeChanged($attr) ? $this->changedAttributes[$attr] : null;
}
public function changedAttributes()
{
return $this->changedAttributes;
}
/**
* List of the attributes that can't be changed in the model through
* assignAttributes().
* If both attrAccessible() and attrProtected() are present in the model,
* only attrAccessible() will be used.
*
* Return an empty array so no attributes are protected (except the default ones).
*/
protected function attrProtected()
{
return null;
}
/**
* List of the only attributes that can be changed in the model through
* assignAttributes().
* If both attrAccessible() and attrProtected() are present in the model,
* only attrAccessible() will be used.
*
* Return an empty array so no attributes are accessible.
*/
protected function attrAccessible()
{
return null;
}
protected function setChangedAttribute($attr, $oldValue)
{
$this->changedAttributes[$attr] = $oldValue;
}
private function filterProtectedAttributes(&$attributes)
{
# Default protected attributes
$default_columns = ['created_at', 'updated_at', 'created_on', 'updated_on'];
if ($pk = static::table()->primaryKey()) {
$default_columns[] = $pk;
}
$default_protected = array_fill_keys(array_merge($default_columns, $this->_associations_names()), true);
$attributes = array_diff_key($attributes, $default_protected);
if (is_array($attrs = $this->attrAccessible())) {
$attributes = array_intersect_key($attributes, array_fill_keys($attrs, true));
} elseif (is_array($attrs = $this->attrProtected())) {
$attributes = array_diff_key($attributes, array_fill_keys($attrs, true));
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Rails\ActiveRecord\Base\Methods;
trait CounterMethods
{
static public function incrementCounter($counter_name, $id)
{
return self::updateCounters([$id], [$counter_name => 1]);
}
static public function decrementCounter($counter_name, $id)
{
return self::updateCounters([$id], [$counter_name => -1]);
}
static public function updateCounters(array $ids, array $counters)
{
if (!is_array($ids))
$ids = [$ids];
$values = [];
foreach ($counters as $name => $value)
$values[] = "`" . $name . "` = `".$name."` " . ($value > 0 ? '+' : '-') . " 1";
$sql = "UPDATE `".self::tableName()."` SET ".implode(', ', $values)." WHERE id IN (?)";
return self::connection()->executeSql($sql, $ids);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Rails\ActiveRecord\Base\Methods;
use Rails\ActiveRecord\ModelSchema;
/**
* These methods offer information about the table corresponding
* to this model, with which the ModelSchema object will be created.
*/
trait ModelSchemaMethods
{
static public function table()
{
$cn = get_called_class();
if (!isset(self::$tables[$cn])) {
$table = static::initTable();
$table->reloadSchema();
self::$tables[$cn] = $table;
}
return self::$tables[$cn];
}
static public function tableName()
{
$cn = str_replace('\\', '_', get_called_class());
$inf = \Rails::services()->get('inflector');
$tableName = $inf->underscore($inf->pluralize($cn));
return static::tableNamePrefix() . $tableName . static::tableNameSuffix();
}
static public function tableNamePrefix()
{
return '';
}
static public function tableNameSuffix()
{
return '';
}
/**
* @return ModelSchema
*/
static protected function initTable()
{
return new ModelSchema(static::tableName(), static::connection());
}
}

View File

@ -0,0 +1,240 @@
<?php
namespace Rails\ActiveRecord\Base\Methods;
use PDO;
use Rails;
use Rails\ActiveRecord\Relation;
use Rails\ActiveRecord\Relation\AbstractRelation;
use Rails\ActiveRecord\Relation\Association;
trait RelationMethods
{
/**
* Find a model by id.
* @return Rails\ActiveRecord\Base
* @raises ActiveRecord\RecordNotFound
*/
static public function find($id)
{
$id = (int)$id;
if ($model = self::where(['id' => $id])->first())
return $model;
throw new Rails\ActiveRecord\Exception\RecordNotFoundException(
sprintf("Couldn't find %s with id = %d.", self::cn(), $id)
);
}
/**
* "Instantiator" methods {
*/
static public function from()
{
return self::createRelation('select', func_get_args());
}
static public function group()
{
return self::createRelation('select', func_get_args());
}
static public function having()
{
return self::createRelation('having', func_get_args());
}
static public function joins()
{
return self::createRelation('joins', func_get_args());
}
static public function limit()
{
return self::createRelation('limit', func_get_args());
}
# TODO
static public function unscoped()
{
return new Relation(get_called_class(), static::tableName());
}
/**
* This is the correct method to instantiate an empty relation.
*/
static public function none()
{
// $cn = self::cn();
// return new Relation($cn, static::tableName());
return self::createRelation();
}
static public function offset()
{
return self::createRelation('offset', func_get_args());
}
static public function order()
{
return self::createRelation('order', func_get_args());
}
static public function select()
{
return self::createRelation('select', func_get_args());
}
static public function distinct()
{
return self::createRelation('distinct', func_get_args());
}
static public function where()
{
return self::createRelation('where', func_get_args());
}
/**
* }
*/
/**
* Take all records.
*/
static public function all()
{
return self::none()->take();
}
/**
* For directly pagination without conditions.
*/
static public function paginate($page, $per_page)
{
$query = self::createRelation();
return $query->paginate($page, $per_page);
}
/**
* @return ActiveRecord\Collection
*/
static public function createModelsFromQuery(AbstractRelation $query)
{
if ($query->will_paginate()) {
$params = [
'page' => $query->get_page(),
'perPage' => $query->get_per_page(),
'offset' => $query->get_offset(),
'totalRows' => $query->get_row_count()
];
} else
$params = [];
return self::_create_collection($query->get_results()->fetchAll(PDO::FETCH_ASSOC), $params);
}
static protected function createRelation($init_method = null, array $init_args = [])
{
$cn = self::cn();
$query = new Relation($cn, static::tableName());
if ($init_method)
call_user_func_array([$query, $init_method], $init_args);
return $query;
}
/**
* When calling this method for pagination, how do we tell it
* the values for page and per_page? That's what extra_params is for...
* Although maybe it's not the most elegant solution.
* extra_params accepts 'page' and 'per_page', they will be sent to Query
* where they will be parsed.
*/
static public function findBySql($sql, array $params = array(), array $extra_params = array())
{
$query = self::createRelation();
$query->complete_sql($sql, $params, $extra_params);
return $query->take();
}
private function _find_has_one($prop, array $params)
{
empty($params['class_name']) && $params['class_name'] = Rails::services()->get('inflector')->camelize($prop);
$builder = new Association($params, $this);
$builder->build_query();
return $builder->get_query()->first() ?: false;
}
/**
* @param array $params - Additional parameters to customize the query for the association
*/
private function _find_has_many($prop, $params)
{
empty($params['class_name']) && $params['class_name'] = rtrim(ucfirst($prop), 's');
$builder = new Association($params, $this);
$builder->build_query();
return $builder->get_query()->take() ?: false;
}
private function _find_belongs_to($prop, array $params)
{
empty($params['class_name']) && $params['class_name'] = ucfirst($prop);
$foreign_key = !empty($params['foreign_key']) ? $params['foreign_key'] : Rails::services()->get('inflector')->underscore($prop) . '_id';
if ($this->getAttribute($foreign_key)) {
return $params['class_name']::where(['id' => $this->getAttribute($foreign_key)])->first() ?: false;
} else {
return false;
}
}
private function _find_has_and_belongs_to_many($prop, array $params)
{
$find_params = [];
empty($params['class_name']) && $params['class_name'] = ucfirst(Rails::services()->get('inflector')->singularize($prop));
$find_params['class_name'] = $params['class_name'];
$find_params['from'] = $find_params['class_name']::tableName();
empty($find_params['join_table']) && $find_params['join_table'] = $find_params['class_name']::tableName() . '_' . $find_params['from'];
empty($find_params['join_type']) && $find_params['join_type'] = 'join';
empty($find_params['join_table_key']) && $find_params['join_table_key'] = 'id';
empty($find_params['association_foreign_key']) && $find_params['association_foreign_key'] = substr($find_params['from'], 0, -1) . '_id';
$joins = strtoupper($find_params['join_type']) . ' `' . $find_params['join_table'] . '` ON `' . $find_params['from'] . '`.`' . $find_params['join_table_key'] . '` = `' . $find_params['join_table'] . '`.`' . $find_params['association_foreign_key'] . '`';
## Having Post model with has_and_belongs_to_many => tags (Tag model):
// 'join_table' => posts_tags [not this => (or tags_posts)]
// 'join_table_key' => id
// 'foreign_key' => post_id
// 'association_foreign_key' => tag_id
# Needed SQL params
// 'select' => posts.*
// 'joins' => JOIN posts_tags ON posts.id = posts_tags.post_id',
// 'conditions' => array('posts_tags.post_id = ?', $this->id)
/**
'has_and_belongs_to_many' => [
'tags' => [
'join_type' => 'join',
'join_table_key' => 'id',
'foreign_key' => 'post_id',
'association_foreign_key' => 'tag_id',
'select' => 'posts.*'
]
]
*/
$relation = new Association($find_params, $this);
$relation->build_query();
$relation->get_query()->joins($joins);
return $relation->get_query()->take() ?: false;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Rails\ActiveRecord\Base\Methods;
use Rails\ActiveRecord\Relation;
trait ScopingMethods
{
/**
* Checks if scope exists. If it does, executes it and
* returns the relation; otherwise, returns false.
*/
static public function scope($name, array $params)
{
$cn = self::cn();
self::$preventInit = true;
$scopes = (new $cn())->scopes();
if (isset($scopes[$name])) {
$lambda = $scopes[$name];
$relation = new Relation($cn, static::tableName());
$lambda = $lambda->bindTo($relation);
call_user_func_array($lambda, $params);
return $relation;
} else
return false;
}
/**
* Defines scopes.
*
* Eg:
*
class Book extends ActiveRecord\Base
{
static protected function scopes()
{
return [
'fantasy' => function() {
$this->where(['genre' => 'fantasy']);
},
'available' => function() {
$this->where(['status' => 'available']);
},
'author' => function($author_name, $limit = 0) {
$this->where('author_name = ?', $author_name);
if ($limit)
$this->limit($limit);
}
];
}
}
* The anonymous function is binded to an ActiveRecord\Relation object.
*
* Not supporting default scope for now.
* Not supporting static methods as scopes, as they couldn't
* be called from a model instance.
*/
protected function scopes()
{
return [];
}
}

Some files were not shown because too many files have changed in this diff Show More