first commit

This commit is contained in:
Parziphal 2013-10-02 01:12:58 -05:00
commit e0f47af18b
261 changed files with 18942 additions and 0 deletions

20
LICENSE Executable file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 RailsPHP Framework
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

4
README.md Executable file
View File

@ -0,0 +1,4 @@
RailsPHP Framework
==================
This is yet another PHP framework based on Ruby on Rails, for PHP 5.4+.

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();
}
}

808
Rails/ActionController/Base.php Executable file
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);
}
}

175
Rails/ActionDispatch/Request.php Executable file
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
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;
}
}

230
Rails/ActionMailer/Deliverer.php Executable file
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
{
}

32
Rails/ActionMailer/Template.php Executable file
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;
}
}

88
Rails/ActionView/ActionView.php Executable file
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
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
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
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();
}
}

17
Rails/ActionView/Partial.php Executable file
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
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
{
}

109
Rails/ActionView/ViewHelpers.php Executable file
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
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;
}
}

366
Rails/ActiveModel/Collection.php Executable file
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
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
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;
}
}

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