Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.travis.yml b/.travis.yml
index 42b9ae7..5fe49d8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,10 @@
language: php
php:
- - 7.1
+ - 7.1
+ - 7.2
script: phpunit --configuration=phpunit.xml tests
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- if [ -f /tmp/coverage.xml ]; then php ocular.phar code-coverage:upload --format=php-clover /tmp/coverage.xml; fi;
diff --git a/autoload_core_files.php b/autoload_core_files.php
index cb7943b..3fe919e 100644
--- a/autoload_core_files.php
+++ b/autoload_core_files.php
@@ -1,105 +1,95 @@
<?php
use spitfire\autoload\AutoLoad;
#Define default classes and their locations
AutoLoad::registerClass('Controller', SPITFIRE_BASEDIR.'/mvc/controller.php');
-AutoLoad::registerClass('_SF_ViewElement', SPITFIRE_BASEDIR.'/mvc/view_element.php');
AutoLoad::registerClass('Time', SPITFIRE_BASEDIR.'/time.php');
AutoLoad::registerClass('Image', SPITFIRE_BASEDIR.'/image.php');
#Database related imports
-AutoLoad::registerClass('spitfire\storage\database\QueryTable', SPITFIRE_BASEDIR.'/db/querytable.php');
-AutoLoad::registerClass('spitfire\storage\database\QueryField', SPITFIRE_BASEDIR.'/db/queryfield.php');
-AutoLoad::registerClass('spitfire\storage\database\Restriction', SPITFIRE_BASEDIR.'/db/restriction.php');
-AutoLoad::registerClass('spitfire\storage\database\CompositeRestriction', SPITFIRE_BASEDIR.'/db/restrictionComposite.php');
AutoLoad::registerClass('Pagination', SPITFIRE_BASEDIR.'/db/pagination.php');
-AutoLoad::registerClass('Model', SPITFIRE_BASEDIR.'/db/databaseRecord.php');
-
AutoLoad::registerClass('Validatable', SPITFIRE_BASEDIR.'/validatable.php');
AutoLoad::registerClass('Schema', SPITFIRE_BASEDIR.'/model/model.php');
AutoLoad::registerClass('spitfire\model\Field', SPITFIRE_BASEDIR.'/model/field.php');
AutoLoad::registerClass('IntegerField', SPITFIRE_BASEDIR.'/model/fields/integer.php');
AutoLoad::registerClass('FloatField', SPITFIRE_BASEDIR.'/model/fields/float.php');
AutoLoad::registerClass('FileField', SPITFIRE_BASEDIR.'/model/fields/file.php');
AutoLoad::registerClass('TextField', SPITFIRE_BASEDIR.'/model/fields/text.php');
AutoLoad::registerClass('StringField', SPITFIRE_BASEDIR.'/model/fields/string.php');
AutoLoad::registerClass('EnumField', SPITFIRE_BASEDIR.'/model/fields/enum.php');
AutoLoad::registerClass('BooleanField', SPITFIRE_BASEDIR.'/model/fields/boolean.php');
AutoLoad::registerClass('DatetimeField', SPITFIRE_BASEDIR.'/model/fields/datetime.php');
AutoLoad::registerClass('ManyToManyField', SPITFIRE_BASEDIR.'/model/fields/manytomany.php');
AutoLoad::registerClass('Reference', SPITFIRE_BASEDIR.'/model/reference.php');
AutoLoad::registerClass('ChildrenField', SPITFIRE_BASEDIR.'/model/children.php');
AutoLoad::registerClass('spitfire\model\adapters\ManyToManyAdapter', SPITFIRE_BASEDIR.'/model/adapters/m2madapter.php');
AutoLoad::registerClass('spitfire\model\adapters\BridgeAdapter', SPITFIRE_BASEDIR.'/model/adapters/bridgeadapter.php');
AutoLoad::registerClass('spitfire\model\adapters\ChildrenAdapter', SPITFIRE_BASEDIR.'/model/adapters/childrenadapter.php');
-AutoLoad::registerClass('spitfire\model\defaults\userModel', SPITFIRE_BASEDIR.'/model/defaults/usermodel_default.php');
-
-
AutoLoad::registerClass('spitfire\storage\database\drivers\stdSQLDriver', SPITFIRE_BASEDIR.'/db/drivers/stdSQL.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\stdSQLTable', SPITFIRE_BASEDIR.'/db/drivers/stdSQLTable.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\mysqlPDODriver', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDO.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDOTable', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDOTable.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\mysqlPDOField', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDOField.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDOQuery', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDOQuery.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDOQueryTable', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDOQueryTable.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDOQueryField', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDOQueryField.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDOJoin', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDOJoin.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDORecord', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDORecord.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDORestriction',SPITFIRE_BASEDIR.'/db/drivers/mysqlPDORestriction.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDOCompositeRestriction',SPITFIRE_BASEDIR.'/db/drivers/mysqlPDORestrictionComposite.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\MysqlPDORestrictionGroup',SPITFIRE_BASEDIR.'/db/drivers/mysqlPDORestrictionGroup.php');
AutoLoad::registerClass('spitfire\storage\database\drivers\mysqlPDOResultSet', SPITFIRE_BASEDIR.'/db/drivers/mysqlPDORes.php');
AutoLoad::registerClass('spitfire\InputSanitizer', SPITFIRE_BASEDIR.'/security_io_sanitization.php');
AutoLoad::registerClass('CoffeeBean', SPITFIRE_BASEDIR.'/io/beans/coffeebean.php');
AutoLoad::registerClass('spitfire\io\beans\Field', SPITFIRE_BASEDIR.'/io/beans/field.php');
AutoLoad::registerClass('spitfire\io\beans\BasicField', SPITFIRE_BASEDIR.'/io/beans/basic_field.php');
AutoLoad::registerClass('spitfire\io\beans\TextField', SPITFIRE_BASEDIR.'/io/beans/text_field.php');
AutoLoad::registerClass('spitfire\io\beans\IntegerField', SPITFIRE_BASEDIR.'/io/beans/integer_field.php');
AutoLoad::registerClass('spitfire\io\beans\LongTextField', SPITFIRE_BASEDIR.'/io/beans/long_text_field.php');
AutoLoad::registerClass('spitfire\io\beans\EnumField', SPITFIRE_BASEDIR.'/io/beans/enum_field.php');
AutoLoad::registerClass('spitfire\io\beans\BooleanField', SPITFIRE_BASEDIR.'/io/beans/boolean_field.php');
AutoLoad::registerClass('spitfire\io\beans\ReferenceField', SPITFIRE_BASEDIR.'/io/beans/reference_field.php');
AutoLoad::registerClass('spitfire\io\beans\ManyToManyField', SPITFIRE_BASEDIR.'/io/beans/manytomany_field.php');
AutoLoad::registerClass('spitfire\io\beans\FileField', SPITFIRE_BASEDIR.'/io/beans/file_field.php');
AutoLoad::registerClass('spitfire\io\beans\DateTimeField', SPITFIRE_BASEDIR.'/io/beans/datetime_field.php');
AutoLoad::registerClass('spitfire\io\beans\ChildBean', SPITFIRE_BASEDIR.'/io/beans/childbean.php');
AutoLoad::registerClass('spitfire\io\beans\renderers\Renderer', SPITFIRE_BASEDIR.'/io/beans/renderers/renderer.php');
AutoLoad::registerClass('spitfire\io\beans\renderers\SimpleBeanRenderer', SPITFIRE_BASEDIR.'/io/beans/renderers/simpleBeanRenderer.php');
AutoLoad::registerClass('spitfire\io\beans\renderers\SimpleFieldRenderer', SPITFIRE_BASEDIR.'/io/beans/renderers/simpleFieldRenderer.php');
AutoLoad::registerClass('_SF_Invoke', SPITFIRE_BASEDIR.'/mvc/invoke.php');
AutoLoad::registerClass('spitfire\io\html\HTMLElement', SPITFIRE_BASEDIR.'/io/html/element.php');
AutoLoad::registerClass('spitfire\io\html\HTMLUnclosedElement', SPITFIRE_BASEDIR.'/io/html/unclosed.php');
AutoLoad::registerClass('spitfire\io\html\HTMLDiv', SPITFIRE_BASEDIR.'/io/html/div.php');
AutoLoad::registerClass('spitfire\io\html\HTMLSpan', SPITFIRE_BASEDIR.'/io/html/span.php');
AutoLoad::registerClass('spitfire\io\html\HTMLInput', SPITFIRE_BASEDIR.'/io/html/input.php');
AutoLoad::registerClass('spitfire\io\html\HTMLTextArea', SPITFIRE_BASEDIR.'/io/html/textarea.php');
AutoLoad::registerClass('spitfire\io\html\HTMLSelect', SPITFIRE_BASEDIR.'/io/html/select.php');
AutoLoad::registerClass('spitfire\io\html\HTMLOption', SPITFIRE_BASEDIR.'/io/html/option.php');
AutoLoad::registerClass('spitfire\io\html\HTMLLabel', SPITFIRE_BASEDIR.'/io/html/label.php');
AutoLoad::registerClass('spitfire\io\html\HTMLForm', SPITFIRE_BASEDIR.'/io/html/form.php');
AutoLoad::registerClass('spitfire\io\html\HTMLTable', SPITFIRE_BASEDIR.'/io/html/table.php');
AutoLoad::registerClass('spitfire\io\html\HTMLTableRow', SPITFIRE_BASEDIR.'/io/html/table_row.php');
AutoLoad::registerClass('spitfire\io\html\dateTimePicker', SPITFIRE_BASEDIR.'/io/html/date_picker.php');
AutoLoad::registerClass('Strings', SPITFIRE_BASEDIR.'/Strings.php');
AutoLoad::registerClass('spitfire\registry\Registry', SPITFIRE_BASEDIR.'/io/registry/registry.php');
AutoLoad::registerClass('spitfire\registry\JSRegistry', SPITFIRE_BASEDIR.'/io/registry/jsregistry.php');
AutoLoad::registerClass('spitfire\registry\CSSRegistry', SPITFIRE_BASEDIR.'/io/registry/cssregistry.php');
AutoLoad::registerClass('Pluggable', SPITFIRE_BASEDIR.'/plugins/pluggable.php');
AutoLoad::registerClass('URL', SPITFIRE_BASEDIR.'/url.php');
AutoLoad::registerClass('AbsoluteURL', SPITFIRE_BASEDIR.'/AbsoluteURL.php');
AutoLoad::registerClass('spitfire\Context', SPITFIRE_BASEDIR.'/core/context.php');
AutoLoad::registerClass('Email', SPITFIRE_BASEDIR.'/mail.php');
diff --git a/storage/database/restrictionmaker/CompositeWorker.php b/console.php
similarity index 53%
copy from storage/database/restrictionmaker/CompositeWorker.php
copy to console.php
index 44f6fae..ac64686 100644
--- a/storage/database/restrictionmaker/CompositeWorker.php
+++ b/console.php
@@ -1,58 +1,28 @@
-<?php namespace spitfire\storage\database\restrictionmaker;
-
-use spitfire\storage\database\RestrictionGroup;
+<?php
/*
* The MIT License
*
- * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ * Copyright 2018 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
-class CompositeWorker implements WorkerInterface
-{
-
- /**
- *
- * @param RestrictionGroup $parent
- * @param string $field
- * @param string $operator
- * @param mixed $value
- */
- public function make(RestrictionGroup$parent, $field, $operator, $value) {
-
- /*
- * Find the appropriate field for the maker to assemble a restriction. If
- * this returns an empty value, then this maker can't assemble a restriction
- */
- $logical = $parent->getQuery()->getTable()->getSchema()->getField($field);
-
- /*
- * If the field is null or the value is null, then this maker is not a match
- * for the behavior needed.
- */
- if ($logical === null || $value === null) {
- return false;
- }
-
- return $parent->getQuery()->compositeRestrictionInstance($logical, $value, $operator);
- }
-
-}
+var_dump($argv);
+var_dump(file_get_contents('php://stdin'));
\ No newline at end of file
diff --git a/core/Collection.php b/core/Collection.php
index 8f39c0b..e00b0d7 100644
--- a/core/Collection.php
+++ b/core/Collection.php
@@ -1,272 +1,296 @@
<?php namespace spitfire\core;
use ArrayAccess;
use BadMethodCallException;
use spitfire\exceptions\OutOfBoundsException;
use spitfire\exceptions\OutOfRangeException;
/**
* The collection class is intended to supercede the array and provide additional
* functionality and ease of use to the programmer.
*/
class Collection implements ArrayAccess, CollectionInterface
{
-
- /**
- * The elements that this collection maintains.
- *
- * @var mixed
- */
private $items;
/**
* The collection element allows to extend array functionality to provide
* programmers with simple methods to aggregate the data in the array.
*
* @param Collection|mixed $e
*/
public function __construct($e = null) {
if ($e === null) { $this->items = []; }
- elseif ($e instanceof Relation) { $this->items = $e->toArray(); }
+ elseif ($e instanceof Collection) { $this->items = $e->toArray(); }
elseif (is_array($e)) { $this->items = $e; }
else { $this->items = [$e]; }
}
/**
* This method iterates over the elements of the array and applies a provided
* callback to each of them. The value your function returns if placed in the
* array.
*
* @param callable|array $callable
* @return Collection
* @throws BadMethodCallException
*/
public function each($callable) {
/*
* If the callback provided is not a valid callable then the function cannot
* properly continue.
*/
if (!is_callable($callable)) {
throw new BadMethodCallException('Invalid callable provided to collection::each()', 1703221329);
}
return new Collection(array_map($callable, $this->items));
}
/**
* Reduces the array to a single value using a callback function.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduce($callback, $initial = null) {
return array_reduce($this->items, $callback, $initial);
}
- public function has($idx) {
- return isset($this->items[$idx]);
- }
-
- public function contains($e) {
- return array_search($e, $this->items);
+ public function flatten() {
+ $_ret = new self();
+
+ foreach ($this->items as $item) {
+ if ($item instanceof Collection) { $_ret->add($item->flatten()); }
+ elseif (is_array($item)) { $c = new self($item); $_ret->add($c->flatten()); }
+ else { $_ret->push($item); }
+ }
+
+ return $_ret;
}
/**
* This function checks whether a collection contains only elements with a
* given type. This function also accepts base types.
*
* Following base types are accepted:
*
* <ul>
* <li>int</li><li>float</li>
* <li>number</li><li>string</li>
* <li>array</li>
* <ul>
*
* @param string $type Base type or class name to check.
* @return boolean
*/
public function containsOnly($type) {
switch($type) {
case 'int' : return $this->reduce(function ($p, $c) { return $p && is_int($c); }, true);
case 'float' : return $this->reduce(function ($p, $c) { return $p && is_float($c); }, true);
case 'number': return $this->reduce(function ($p, $c) { return $p && is_numeric($c); }, true);
case 'string': return $this->reduce(function ($p, $c) { return $p && is_string($c); }, true);
case 'array' : return $this->reduce(function ($p, $c) { return $p && is_array($c); }, true);
default : return $this->reduce(function ($p, $c) use ($type) { return $p && is_a($c, $type); }, true);
}
}
/**
* Reports whether the collection is empty.
*
* @return boolean
*/
public function isEmpty() {
return empty($this->items);
}
+ public function has($idx) {
+ return isset($this->items[$idx]);
+ }
+
+ public function contains($e) {
+ return array_search($e, $this->items);
+ }
+
/**
* Filters the collection using a callback. This allows a collection to shed
* values that are not useful to the programmer.
*
* Please note that this will return a copy of the collection and the original
* collection will remain unmodified.
*
* @param callable $callback
* @return \spitfire\core\Collection
*/
public function filter($callback = null) {
#If there was no callback defined, then we filter the array without params
if ($callback === null) { return new Collection(array_filter($this->items)); }
#Otherwise we use the callback parameter to filter the array
return new Collection(array_filter($this->items, $callback));
}
/**
- * Counts the number of elements inside the collection.
+ * Removes all duplicates from the collection.
*
- * @return int
+ * @return \spitfire\core\Collection
*/
- public function count() {
- return count($this->items);
+ public function unique() {
+ return new Collection(array_unique($this->items));
}
/**
- * Combines the elements in the collection to a string separated by the glue
- * parameter.
+ * Counts the number of elements inside the collection.
*
- * @param string $glue
+ * @return int
*/
- public function join($glue = '') {
- return implode($glue, $this->items);
+ public function count() {
+ return count($this->items);
}
/**
* Adds up the elements in the collection. Please note that this method will
* double check to see if all the provided elements are actually numeric and
* can be added together.
*
* @return int|float
* @throws BadMethodCallException
*/
public function sum() {
if ($this->isEmpty()) { throw new BadMethodCallException('Collection is empty'); }
if (!$this->containsOnly('number')) { throw new BadMethodCallException('Collection does contain non-numeric types'); }
return array_sum($this->items);
}
+ public function sort($callback = null) {
+ if (!$callback) { return new Collection(sort($this->items)); }
+ else { return new Collection(usort($this->items, $callback)); }
+ }
+
/**
* Returns the average value of the elements inside the collection.
*
* @throws BadMethodCallException If the collection contains non-numeric values
* @return int|float
*/
public function avg() {
return $this->sum() / $this->count();
}
+ public function join($glue) {
+ return implode($glue, $this->items);
+ }
+
/**
* Extracts a certain key from every element in the collection. This requires
* every element in the collection to be either an object or an array.
*
* The method does not accept values that are neither array nor object, but
* will return null if the key is undefined in the array or object being used.
*
* @param mixed $key
*/
public function extract($key) {
return new Collection(array_map(function ($e) use ($key) {
if (is_array($e)) { return isset($e[$key])? $e[$key] : null; }
if (is_object($e)) { return isset($e->$key)? $e->$key : null; }
throw new OutOfBoundsException('Collection::extract requires array to contain only arrays and objects');
}, $this->items));
}
public function push($element) {
$this->items[] = $element;
return $element;
}
public function add($elements) {
+ if ($elements instanceof Collection) { $elements = $elements->toArray(); }
+
$this->items = array_merge($this->items, $elements);
return $this;
}
public function remove($element) {
- unset($this->items[array_search($element, $this->items)]);
+ $i = array_search($element, $this->items);
+ if ($i === false) { throw new OutOfRangeException('Not found', 1804292224); }
+
+ unset($this->items[$i]);
return $this;
}
public function reset() {
$this->items = [];
return $this;
}
public function current() {
return current($this->items);
}
public function key() {
return key($this->items);
}
public function next() {
return next($this->items);
}
public function offsetExists($offset) {
return array_key_exists($offset, $this->items);
}
public function offsetGet($offset) {
if (!array_key_exists($offset, $this->items)) {
throw new OutOfRangeException('Undefined index: ' . $offset, 1703221322);
}
return $this->items[$offset];
}
public function offsetSet($offset, $value) {
$this->items[$offset] = $value;
}
public function offsetUnset($offset) {
unset($this->items[$offset]);
}
public function rewind() {
return reset($this->items);
}
+ public function last() {
+ if (!isset($this->items)) { throw new \spitfire\exceptions\PrivateException('Collection error', 1709042046); }
+ return end($this->items);
+ }
+
public function shift() {
return array_shift($this->items);
}
/**
* Indicates whether the current element in the Iterator is valid. To achieve
* this we use the key() function in PHP which will return the key the array
* is currently forwarded to or (which is interesting to us) NULL in the event
* that the array has been forwarded past it's end.
*
* @see key
* @return boolean
*/
public function valid() {
return null !== key($this->items);
}
public function toArray() {
return $this->items;
}
public function __isset($name) {
return isset($this->items[$name]);
}
}
diff --git a/core/functions.php b/core/functions.php
index 4174ecf..50c5ad0 100644
--- a/core/functions.php
+++ b/core/functions.php
@@ -1,258 +1,279 @@
<?php
use spitfire\App;
use spitfire\core\Collection;
use spitfire\core\Context;
use spitfire\core\Environment;
use spitfire\core\http\URL;
use spitfire\locale\Domain;
use spitfire\locale\DomainGroup;
use spitfire\locale\Locale;
use spitfire\SpitFire;
use spitfire\storage\database\DB;
use spitfire\validation\ValidationException;
use spitfire\validation\Validator;
use spitfire\validation\ValidatorInterface;
/**
* This is a quick hand method to use Spitfire's main App class as a singleton.
* It allows you to quickly access many of the components the framework provides
* to make it easier to read and maintain the code being created.
*
* @staticvar type $sf
* @return SpitFire
*/
function spitfire() {
static $sf = null;
if ($sf !== null) {
return $sf;
} else {
$sf = new SpitFire();
$sf->prepare();
return $sf;
}
}
/**
*
* Registers a new Application in Spitfire, allowing it to handle requests directed
* to it.
*
* @param string $name The name of the Application
* @param string $namespace The namespace in which the requests will be sent to
* the application.
* @return App The App created by the system, use this to pass parameters and
* configuration to the application.
*/
function app($name, $namespace) {
$appName = $name . 'App';
$app = new $appName(APP_DIRECTORY . $name . DIRECTORY_SEPARATOR, $namespace);
spitfire()->registerApp($app, $namespace);
return $app;
}
/**
* Shorthand function to create / retrieve the model the application is using
* to store data. We could consider this a little DB handler factory.
*
* @param \spitfire\storage\database\Settings $options
* @return spitfire\storage\database\DB
*/
function db(\spitfire\storage\database\Settings$options = null) {
static $db = null;
#If we're requesting the standard driver and have it cached, we use this
if ($options === null && $db !== null) { return $db; }
#If no options were passed, we try to fetch them from the environment
$settings = \spitfire\storage\database\Settings::fromURL($options? : Environment::get('db'));
#Instantiate the driver
$driver = 'spitfire\storage\database\drivers\\' . $settings->getDriver() . '\Driver';
$driver = new $driver($settings);
#If no options were provided we will assume that this is the standard DB handler
if ($options === null) { $db = $driver; }
#Return the driver
return $driver;
}
/**
* Returns HTML escaped string and if desired it adds ellipsis. If the string is
* numeric it will reduce unnecessary decimals.
*
* @param String $str
* @param int $maxlength
* @return String
*/
function __($str, $maxlength = false) {
if ($maxlength) { $str = Strings::ellipsis ($str, $maxlength); }
if (defined('ENT_HTML5'))
{ $str = htmlspecialchars($str, ENT_HTML5, Environment::get('system_encoding')); }
else
{ $str = htmlspecialchars($str, ENT_COMPAT, Environment::get('system_encoding')); }
return $str;
}
/**
* Translation helper.
*
* Depending on the arguments this function receives, it will have one of several
* behaviors.
*
* If the first argument is a spitfire\locale\Locale and the function receives a
* optional second parameter, then it will assign the locale to either the global
* domain / the domain provided in the second parameter.
*
* Otherwise, if the first parameter is a string, it will call the default locale's
* say method. Which will translate the string using the standard locale.
*
* If no parameters are provided, this function returns a DomainGroup object,
* which provides access to the currency and date functions as well as the other
* domains that the system has for translations.
*
* @return string|DomainGroup
*/
function _t() {
static $domains = null;
#If there are no domains we need to set them up first
if ($domains === null) { $domains = new DomainGroup(); }
#Get the functions arguments afterwards
$args = func_get_args();
#If the first parameter is a Locale, then we proceed to registering it so it'll
#provide translations for the programs
if (isset($args[0]) && $args[0] instanceof Locale) {
$locale = array_shift($args);
$domain = array_shift($args);
return $domains->putDomain($domain, new Domain($domain, $locale));
}
#If the args is empty, then we give return the domains that allow for printing
#and localizing of the data.
if (empty($args)) {
return $domains;
}
return call_user_func_array(Array($domains->getDefault(), 'say'), $args);
}
function current_context(Context$set = null) {
static $context = null;
if ($set!==null) {$context = $set;}
return $context;
}
function validate($target = null) {
$targets = array_filter(is_array($target)? $target : func_get_args());
if (!empty($targets) && reset($targets) instanceof ValidatorInterface) {
$messages = Array();
#Retrieve the messages from the validators
foreach ($targets as $target) {
$messages = array_merge($messages, $target->getMessages());
}
if (!empty($messages)) { throw new ValidationException('Validation failed', 1604200115, $messages); }
return $targets;
} else {
$validator = new Validator();
$validator->setValue($target);
return $validator;
}
}
/**
* Retrieves the current path from the request. This will retrieve the path
* without query string or document root.
*
* @see http://www.spitfirephp.com/wiki/index.php/NgiNX_Configuration For NGiNX setup
* @return string
*/
function getPathInfo() {
$base_url = spitfire()->baseUrl();
list($path) = explode('?', substr($_SERVER['REQUEST_URI'], strlen($base_url)));
if (strlen($path) !== 0) { return $path; }
else { return '/'; }
}
function _def(&$a, $b) {
return ($a)? $a : $b;
}
/**
* This function is a shorthand for "new Collection" which also allows fluent
* usage of the collection in certain environments where the PHP version still
* limits that behavior.
*
* @param mixed $elements
* @return Collection
*/
function collect($elements) {
return new Collection($elements);
}
/**
* Creates a new URL. Use this class to generate dynamic URLs or to pass
* URLs as parameters. For consistency (double base prefixes and this
* kind of misshaps aren't funny) use this object to pass or receive URLs
* as paramaters.
*
* Please note that when passing a URL that contains the URL as a string like
* "/hello/world?a=b&c=d" you cannot pass any other parameters. It implies that
* you already have a full URL.
*
* You can pass any amount of parameters to this class,
* the constructor will try to automatically parse the URL as good as possible.
* <ul>
* <li>Arrays are used as _GET</li>
* <li>App objects are used to identify the namespace</li>
* <li>Strings that contain / or ? will be parsed and added to GET and path</li>
* <li>The rest of strings will be pushed to the path.</li>
* </ul>
*/
function url() {
#Get the parameters the first time
$sf = spitfire();
$params = func_get_args();
#Extract the app
if (reset($params) instanceof App || $sf->appExists(reset($params))) {
$app = array_shift($params);
}
else {
$app = $sf;
}
#Get the controller, and the action
$controller = null;
$action = null;
$object = Array();
#Get the object
while(!empty($params) && !is_array(reset($params)) ) {
if (!$controller) { $controller = array_shift($params); }
elseif (!$action) { $action = array_shift($params); }
else { $object[] = array_shift($params); }
}
#Get potential environment variables that can be used for additional information
#like loccalization
$get = array_shift($params);
$environment = array_shift($params);
return new URL($app, $controller, $action, $object, 'php', $get, $environment);
+}
+
+/**
+ * The within function is a math function that allows to determine whether a
+ * value is within a range and returns either the value, or the closest range
+ * delimiter.
+ *
+ * The first and the last parameter delimit the range. The second parameter is
+ * the one being tested.
+ *
+ * <code>within(1, 50, 100); //Outputs: 50</code>
+ * <code>within(1, 500, 100); //Outputs: 100</code>
+ * <code>within(1, -50, 100); //Outputs: 1</code>
+ *
+ * @param number $min
+ * @param number $val
+ * @param number $max
+ * @return number
+ */
+function within($min, $val, $max) {
+ return min(max($min, $val), $max);
}
\ No newline at end of file
diff --git a/core/http/AbsoluteURL.php b/core/http/AbsoluteURL.php
index 4ec86ca..b8b73b6 100644
--- a/core/http/AbsoluteURL.php
+++ b/core/http/AbsoluteURL.php
@@ -1,166 +1,170 @@
<?php namespace spitfire\core\http;
use spitfire\core\Environment;
use spitfire\core\router\Router;
use spitfire\core\router\Server;
class AbsoluteURL extends URL
{
const PROTO_HTTP = 'http';
const PROTO_HTTPS = 'https';
private $domain;
private $proto = self::PROTO_HTTP;
/**
* The reverser property acts as a cache, removing the need to cycle through
* the different reversers and their rules to check if they're a fit
* candidate.
*
* @var \spitfire\core\router\reverser\ServerReverserInterface
*/
private $reverser = null;
/**
* Set the domain name this URL points to. This is intended to address
* Spitfire apps that work on a multi-domain environment / subdomains
* and require linking to itself on another domain. They are also good
* for sharing / email links where the URL without server name would
* be useless.
*
* Since April 2017, you can provide this method with an array of parameters
* that the router parses when handling a request. This allows your application
* to not only manage custom server names but also to write URLs pointing
* there depending on your settings.
*
* @param string $domain The domain of the URL. I.e. www.google.com
* @return absoluteURL
*/
public function setDomain($domain) {
$this->domain = $domain;
$this->reverser = $this->getReverser();
return $this;
}
public function getDomain() {
return $this->domain;
}
+ public function setProtocol($proto) {
+ $this->proto = $proto;
+ }
+
/**
*
* @return string
*/
public function getServerName() {
/*
* The user provided parameters as the domain, therefore he expects Spitfire
* to look up a valid server for the route.
*/
if (is_array($this->domain)) {
/*
* Given an array as path and no reverser means that the user provided
* parameters for an impossible route and therefore the application
* cannot properly continue.
*/
if (!$this->reverser) {
throw new \spitfire\exceptions\PrivateException('No server found for given params', 1706212055);
}
return $this->reverser->reverse($this->domain);
}
if (is_string($this->domain)) {
return $this->domain;
}
#Default
return Environment::get('server_name')? Environment::get('server_name') : $_SERVER['SERVER_NAME'];
}
public static function current() {
$ctx = current_context();
if (!$ctx) {
throw new PrivateException("No context for URL generation");
}
return new self($ctx->app, $ctx->app->getControllerURI($ctx->controller), $ctx->action, $ctx->object, $ctx->extension, $_GET);
}
public static function asset($asset_name, $app = null) {
$path = parent::asset($asset_name, $app);
$proto = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'? self::PROTO_HTTPS : self::PROTO_HTTP;
$domain = Environment::get('server_name')? Environment::get('server_name') : $_SERVER['SERVER_NAME'];
return $proto . '://' . $domain . $path;
}
public static function canonical() {
#Get the relative canonical URI
$canonical = URL::canonical();
#Prepend protocol and server and return it
return $canonical->absolute();
}
public function getRoutes() {
/*
* If the developer provided a set of parameters to reverse the route we
* use those.
*/
if ($this->reverser) {
return $this->getReverser()->getServer()->getRoutes()->toArray();
}
/*
* Otherwise, if the dev provided a server which potentially has no routes,
* then we return either the server's routes or the default ones.
*/
elseif($this->domain) {
$router = Router::getInstance();
return $router->server()->getRoutes()->toArray()? : $router->getRoutes()->toArray();
}
/*
* Otherwise we use the globals.
*/
else {
return parent::getRoutes();
}
}
public function getReverser() {
#If the user didn't pass parameters, this operation is worthless.
if (!is_array($this->domain)) { return null; }
#Get the servers we registered for the router
$router = Router::getInstance();
$servers = $router->getServers();
foreach ($servers as $s) {
/*@var $s Server*/
/*@var $r BaseServerReverser*/
$r = $s->getReverser();
if ($r->reverse($this->domain)) {
return $this->reverser = $r;
}
}
return $this->reverser = null;
}
public function __toString() {
$rel = parent::__toString();
$proto = $this->proto;
$domain = $this->getServerName();
return $proto . '://' . $domain . $rel;
}
}
diff --git a/core/http/URL.php b/core/http/URL.php
index 3d76220..716dd96 100644
--- a/core/http/URL.php
+++ b/core/http/URL.php
@@ -1,227 +1,228 @@
<?php namespace spitfire\core\http;
use ReflectionClass;
use spitfire\core\Path;
use spitfire\core\router\Route;
use spitfire\core\router\Router;
use spitfire\exceptions\PrivateException;
use spitfire\io\Get;
use spitfire\SpitFire;
/**
*
* This dinamically generates system urls this allows us to validate URLs if needed
* or generate different types of them depending on if pretty links is enabled
*
* @author César de la Cal <cesar@magic3w.com>
*/
class URL
{
/**
* @var Path Contains information about the controller / action
* / object combination that will be used for this URL.
*/
private $path;
/**
* @var mixed|Get[] Contains data about the _GET parameters this URL will pass
* to the system if invoked by the user.
*/
private $params = Array();
public function __construct($app, $controller = null, $action = null, $object = null, $extension = null, $get = null, $environment = null) {
$this->params = $get;
$this->path = new Path($app, $controller, $action, $object, $extension, $environment);
}
public function setExtension($extension) {
$this->path->setFormat($extension);
return $this;
}
public function getExtension() {
return $this->path->getFormat();
}
public function getPath() {
return $this->path;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function setApp($app) {
$this->path->setApp($app);
return $this;
}
public function getApp() {
return $this->path->getApp();
}
/**
* Sets a parameter for the URL's GET
* @param string $param
* @param string $value
*
* [NOTICE] This function accepts parameters like controller,
* action or object that are part of the specification of nlive's
* core. It is highly recommended not to use this "reserved words"
* as parameters as they may cause the real values of these to be
* overwritten when the browser requests the site linked by these.
*
* @return self
*/
public function setParam($param, $value) {
$this->params[$param] = $value;
return $this;
}
public function setParams($values) {
$this->params = $values;
return $this;
}
/**
* Returns the value of a parameter set in the current URL.
*
* @param string $parameter
* @return mixed
*/
public function getParameter($parameter) {
if (isset($this->params[$parameter])) {
return $this->params[$parameter];
} else {
return null;
}
}
public function appendParameter($param, $value) {
if ( isset($this->params[$param]) ) {
if ( is_array($this->params[$param]) ){
$this->params[$param][] = $value;
} else {
$this->params[$param] = Array($this->params[$param], $value);
}
}
else {
$this->params[$param] = Array($value);
}
}
/**
* Serializes the URL. This method ill check if a custom serializer was defined
* and will then use the appropriate serializer OR fall back to the default
* one.
*
* @see URL::defaultSerializer() For the standard behavior.
*/
public function __toString() {
$routes = $this->getRoutes();
$url = false;
while($url === false && !empty($routes)) {
/*@var $route Route*/
$route = array_shift($routes);
$rev = $route->getReverser();
if ($rev === null) { continue; }
$url = $rev->reverse($this->path);
}
foreach ($this->getRedirections() as $red) {
try { $url = $red->reverse($url); }
catch (\Exception$e) { /*Ignore*/ }
}
if (empty(trim($url, '/')) && $this->path->getFormat() !== 'php') {
$url = $rev->reverse($this->path, true);
}
#If the extension provided is special, we print it
if ($this->path->getFormat() !== 'php') { $url.= ".{$this->path->getFormat()}"; }
else { $url = rtrim($url, '/') . '/'; }
if ($this->params instanceof Get) {
$url.= '?' . http_build_query($this->params->getRaw());
}
elseif (!empty($this->params)) {
$url.= '?' . http_build_query($this->params);
}
return '/' . implode('/', array_filter([trim(SpitFire::baseUrl(), '/'), ltrim($url, '/')]));
}
/**
* @param string $asset_name
* @param SpitFire $app
*
* @return string
*/
public static function asset($asset_name, $app = null) {
#If there is no app defined we can use the default directory
#Otherwise use the App specific directory
$fpath = (!isset($app) ? ASSET_DIRECTORY : '/assets/').$asset_name;
$modifiedAt = filemtime($fpath);
$fpath .= "?$modifiedAt";
return SpitFire::baseUrl() . '/' . $fpath;
}
public static function make($url) {
return SpitFire::baseUrl() . $url;
}
public static function current() {
$ctx = current_context();
if (!$ctx) {
throw new PrivateException("No context for URL generation");
}
return new URL($ctx->app, $ctx->app->getControllerURI($ctx->controller), $ctx->action, $ctx->object, $ctx->extension, $_GET);
}
public static function canonical() {
$ctx = current_context();
if (!$ctx) {
throw new PrivateException("No context for URL generation");
}
return new URL($ctx->app, $ctx->controller, $ctx->action, $ctx->object, $ctx->extension, $_GET->getCanonical());
}
public function absolute($domain = null) {
$t = new AbsoluteURL($this->getApp());
$t->setExtension($this->getExtension());
$t->setParams($this->params);
$t->setPath($this->path);
+ $t->setProtocol(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'? AbsoluteURL::PROTO_HTTPS : AbsoluteURL::PROTO_HTTP);
return $t->setDomain($domain);
}
public function getRoutes() {
$router = Router::getInstance();
return array_filter(array_merge(
$router->server()->getRoutes()->toArray(), $router->getRoutes()->toArray()
));
}
/**
*
* @return \spitfire\core\router\Redirection[]
*/
public function getRedirections() {
$router = Router::getInstance();
return array_merge(
$router->server()->getRedirections()->toArray(), $router->getRedirections()->toArray()
);
}
}
diff --git a/db/databaseRecord.php b/db/databaseRecord.php
deleted file mode 100644
index 539b45f..0000000
--- a/db/databaseRecord.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-
-trigger_error('Using the deprecated Model in databaseRecord.php. Please use spitfire\Model', E_USER_DEPRECATED);
-
-abstract class Model extends spitfire\Model {}
\ No newline at end of file
diff --git a/db/drivers/mysqlPDO.php b/db/drivers/mysqlPDO.php
deleted file mode 100644
index ce4ec63..0000000
--- a/db/drivers/mysqlPDO.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php namespace spitfire\storage\database\drivers;
-
-
-/**
- * MySQL driver via PDO. This driver does <b>not</b> make use of prepared
- * statements, prepared statements become too difficult to handle for the driver
- * when using several JOINs or INs. For this reason the driver has moved from
- * them back to standard querying.
- */
-class mysqlPDODriver extends mysqlpdo\Driver
-{
-}
diff --git a/db/drivers/mysqlPDOQueryField.php b/db/drivers/mysqlPDOQueryField.php
deleted file mode 100644
index c4b7705..0000000
--- a/db/drivers/mysqlPDOQueryField.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-namespace spitfire\storage\database\drivers;
-
-use spitfire\storage\database\QueryField;
-
-class MysqlPDOQueryField extends QueryField
-{
- public function __toString() {
- return "{$this->getQuery()->getQueryTable()}.`{$this->getField()->getName()}`";
- }
-}
\ No newline at end of file
diff --git a/db/drivers/mysqlPDOQueryTable.php b/db/drivers/mysqlPDOQueryTable.php
deleted file mode 100644
index eb5abe7..0000000
--- a/db/drivers/mysqlPDOQueryTable.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php namespace spitfire\storage\database\drivers;
-
-use spitfire\storage\database\QueryTable;
-
-class MysqlPDOQueryTable extends QueryTable
-{
- /**
- *
- * @todo Move the aliasing thing over to the queryTable completely.
- * @return string
- */
- public function __toString() {
- return "`{$this->getAlias()}`";
- }
-
- public function definition() {
- return "{$this->getTable()->getLayout()} AS `{$this->getAlias()}`";
- }
-}
diff --git a/db/drivers/mysqlPDORestriction.php b/db/drivers/mysqlPDORestriction.php
deleted file mode 100644
index 4541cae..0000000
--- a/db/drivers/mysqlPDORestriction.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-namespace spitfire\storage\database\drivers;
-
-use \spitfire\storage\database\Restriction;
-use \spitfire\storage\database\Query;
-use \Exception;
-use spitfire\Model;
-use spitfire\exceptions\PrivateException;
-
-class MysqlPDORestriction extends Restriction
-{
- public function __toString() {
-
- try {
-
- if (!$this->getField() instanceof \spitfire\storage\database\QueryField)
- throw new PrivateException();
-
- $value = $this->getValue();
- $field = $this->getField()->getField();
-
- if ($field instanceof \Reference || $field instanceof \ManyToManyField || $field instanceof \ChildrenField) {
-
- if ($value instanceof Model) {
- $value = $value->getQuery();
- }
-
- if ($value instanceof Query) {
- return sprintf('(%s)', implode(' AND ', $value->getRestrictions()));
- }
-
- elseif ($value === null) {
- return "`{$this->getQuery()->getTable()}`.`{$this->getField()->getName()}` {$this->getOperator()} null}";
- }
-
- else {
- throw new PrivateException("Invalid data");
- }
-
- }
-
- else {
-
- if (is_array($value)) {
- foreach ($value as &$v) {
- $v = $this->getTable()->getDb()->quote($v);
- }
-
- $quoted = implode(',', $value);
- return "{$this->getField()} {$this->getOperator()} ({$quoted})";
- }
-
- elseif ($value instanceof MysqlPDOQueryField) {
- return "{$this->getField()} {$this->getOperator()} {$this->getValue()}";
- }
-
- else {
- $quoted = $this->getTable()->getDb()->quote($this->getValue());
- return "{$this->getField()} {$this->getOperator()} {$quoted}";
- }
- }
-
- }catch (Exception $e) {
- error_log($e->getMessage());
- error_log($e->getTraceAsString());
- return '';
- }
- }
-
-}
diff --git a/db/drivers/mysqlPDORestrictionComposite.php b/db/drivers/mysqlPDORestrictionComposite.php
deleted file mode 100644
index d8ff388..0000000
--- a/db/drivers/mysqlPDORestrictionComposite.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php namespace spitfire\storage\database\drivers;
-
-use \spitfire\storage\database\CompositeRestriction;
-
-class MysqlPDOCompositeRestriction extends CompositeRestriction
-{
-
- public function __toString() {
- //TODO: This should just print the PK IS / IS NOT NULL
- $field = $this->getField();
- $value = $this->getValue();
-
- if ($field === null || $value === null) {
- return implode(' AND ', $this->getSimpleRestrictions());
- }
- else {
- $fields = $this->getValue()->getQueryTable()->getTable()->getPrimaryKey()->getFields();
- $_ret = Array();
-
- foreach($fields as $field) {
- $f = $this->getValue()->queryFieldInstance($field);
- $o = 'IS NOT';
- $_ret[] = "{$f} {$o} NULL";
- }
-
- /**
- *
- * @var MysqlPDOQuery The query
- */
- $value = $this->getValue();
- $group = $this->getQuery()->getTable()->getDb()->getObjectFactory()->restrictionGroupInstance($this->getQuery(), \spitfire\storage\database\RestrictionGroup::TYPE_AND);
-
- /**
- * The system needs to create a copy of the subordinated restrictions
- * (without the simple ones) to be able to syntax a proper SQL query.
- *
- * @todo Refactor this to look proper
- */
- foreach ($value as $r) {
- if ($r instanceof \spitfire\storage\database\Restriction) { $c = $r; }
- if ($r instanceof CompositeRestriction) { $c = $r; }
- if ($r instanceof \spitfire\storage\database\RestrictionGroup) {
- $c = clone $r;
- $c->filterEmptyGroups();
- }
-
- if (!$c instanceof \spitfire\storage\database\RestrictionGroup || !$c->isEmpty()) {
- $group->push($c);
- }
- }
-
- if (!$group->isEmpty()) {
- $_ret[] = $group;
- }
-
- if ($this->getOperator() === '=') {
- return sprintf('(%s)', implode(' AND ', array_filter($_ret)));
- }
- else {
- return sprintf('NOT(%s)', implode(' AND ', array_filter($_ret)));
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/db/drivers/mysqlPDORestrictionGroup.php b/db/drivers/mysqlPDORestrictionGroup.php
deleted file mode 100644
index bade8ef..0000000
--- a/db/drivers/mysqlPDORestrictionGroup.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-namespace spitfire\storage\database\drivers;
-
-use \spitfire\storage\database\RestrictionGroup;
-
-class MysqlPDORestrictionGroup extends RestrictionGroup
-{
- public function __toString() {
- if ($this->isEmpty()) { return ''; }
- return sprintf('(%s)', implode(' ' . $this->getType() .' ', $this->getRestrictions()));
- }
-}
\ No newline at end of file
diff --git a/db/drivers/mysqlPDOTable.php b/db/drivers/mysqlPDOTable.php
deleted file mode 100644
index 9e29d00..0000000
--- a/db/drivers/mysqlPDOTable.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php namespace spitfire\storage\database\drivers;
-
-use Exception;
-
-/**
- * Represents table specific properties and methods for the MySQLPDO Driver.
- *
- * @deprecated since version 0.1-dev 20170807
- */
-class MysqlPDOTable extends stdSQLTable
-{
-
- public function repair() {
- $table = $this;
- $stt = "DESCRIBE $table";
- $fields = $table->getFields();
- //Fetch the DB Fields and create on error.
- try {
- $query = $this->getDb()->execute($stt, Array(), false);
- }
- catch(Exception $e) {
- return $this->create();
- }
- //Loop through the exiting fields
- while (false != ($f = $query->fetch())) {
- try {
- $field = $this->getField($f['Field']);
- unset($fields[$field->getName()]);
- }
- catch(Exception $e) {/*Ignore*/}
- }
-
- foreach($fields as $field) $field->add();
- }
-
- public function create() {
-
- $table = $this;
- $definitions = $table->columnDefinitions();
- $foreignkeys = $table->foreignKeyDefinitions();
- $pk = $table->getPrimaryKey();
-
- foreach($pk as &$f) { $f = '`' . $f->getName() . '`'; }
-
- if (!empty($foreignkeys)) $definitions = array_merge ($definitions, $foreignkeys);
-
- if (!empty($pk)) $definitions[] = 'PRIMARY KEY(' . implode(', ', $pk) . ')';
-
- #Strip empty definitions from the list
- $clean = array_filter($definitions);
-
- $stt = sprintf('CREATE TABLE %s (%s) ENGINE=InnoDB CHARACTER SET=utf8',
- $table,
- implode(', ', $clean)
- );
-
- return $table->getDb()->execute($stt);
- }
-
- public function destroy() {
- $this->getDb()->execute('DROP TABLE ' . $this->getTable());
- }
-
- /**
- * Returns the name of a table as DB Object reference (with quotes).
- *
- * @return string The name of the table escaped and ready for use inside
- * of a query.
- */
- public function __toString() {
- return strval($this->getLayout());
- }
-}
\ No newline at end of file
diff --git a/db/drivers/stdSQLTable.php b/db/drivers/stdSQLTable.php
deleted file mode 100644
index 8b1c8a2..0000000
--- a/db/drivers/stdSQLTable.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-namespace spitfire\storage\database\drivers;
-
-use spitfire\storage\database\Table;
-use Reference;
-
-abstract class stdSQLTable extends Table
-{
-
- /**
- * Creates the column definitions for each column
- *
- * @return mixed
- */
- protected function columnDefinitions() {
- $fields = $this->getFields();
- foreach ($fields as $name => $f) {
- $fields[$name] = '`'. $name . '` ' . $f->columnDefinition();
- }
- return $fields;
- }
-
- /**
- * Creates a list of definitions for CONSTRAINTS defined by the references
- * this table's model makes to other models.
- *
- * @return array
- */
- protected function foreignKeyDefinitions() {
-
- $ret = Array();
- $refs = $this->schema->getFields();
-
- foreach ($refs as $name => $ref) {
- if (!$ref instanceof Reference) unset($refs[$name]);
- }
-
- if (empty($refs)) return Array();
-
- foreach ($refs as $ref) {
- //Check the integrity of the remote table
- if ($ref->getTarget() !== $this->schema) {
- $this->getDb()->table($ref->getTarget())->getTable()->repair();
- }
-
- #Get the fields the model references from $ref
- $fields = $ref->getPhysical();
- foreach ($fields as &$field) $field = $field->getName();
- unset($field);
- #Get the table that represents $ref
- $referencedtable = $ref->getTarget()->getTable();
- $primary = $referencedtable->getPrimaryKey();
- foreach ($primary as &$field) $field = $field->getName();
- unset($field);
- //Prepare the statement
- $refstt = sprintf('FOREIGN KEY %s (%s) REFERENCES %s(%s) ON DELETE CASCADE ON UPDATE CASCADE',
- 'fk_' . rand(), #Constraint name. Temporary fix, constraints should have proper names
- implode(', ', $fields),
- $referencedtable,
- implode(', ', $primary)
- );
-
- $ret[] = $refstt;
- }
-
- return $ret;
- }
-}
diff --git a/db/pagination.php b/db/pagination.php
deleted file mode 100644
index 6014677..0000000
--- a/db/pagination.php
+++ /dev/null
@@ -1,217 +0,0 @@
-<?php
-
-use spitfire\storage\database\Query;
-
-/**
- * This class is the base for the database query pagination inside of Spitfire.
- * It provides the necessary tools to generate a list of pages inside your
- * applications so queries aren't able to collapse your system / clients.
- *
- * By default this class includes a getEmpty method that returns a message when
- * no results are available. Although it is not a good practice to allow classes
- * perform actions that aren't strictly related to their task. But the improvement
- * on readability gained in Views is worth the change.
- *
- * @link http://www.spitfirephp.com/wiki/index.php/Database/pagination Related data and tutorials
- *
- * @todo Somehow this class should cache the counts, so the database doesn't need to read the data every time.
- * @todo This class should help paginating without the use of LIMIT
- */
-class Pagination
-{
- private $query;
- private $maxJump = 3;
-
- /** @var URL */
- private $url;
- private $pageCount;
- private $param = 'page';
- private $pages = Array();
-
- public function __construct(Query $query = null, $name = null) {
- if ($query !== null && $query->getResultsPerPage() < 1) {
- $query->setResultsPerPage(20);
- }
-
- $this->query = $query;
- $this->name = $name;
- $this->query->setPage((int)$_GET[$this->param][$this->getName()]);
- }
-
- public function getCurrentPage () {
- return $this->query->getPage();
- }
-
- public function getPageCount() {
- if ($this->pageCount !== null) return $this->pageCount;
-
- $rpp = $this->query->getResultsPerPage();
- $this->query->setResultsPerPage(-1);
- $results = $this->query->count();
- $this->query->setResultsPerPage($rpp);
-
- return $this->pageCount = ceil($results/$rpp);
- }
-
- /**
- * Returns the paginator URL. The URL will be used to replace the value of the
- * parameter this class uses to add an entry for this pagination.
- *
- * @return URL
- */
- public function getURL() {
- if (isset($this->url)) {
- return $this->url;
- } else {
- return $this->url = URL::current();
- }
- }
-
- /**
- * @param int $page
- *
- * @return URL
- */
- public function makeURL($page) {
- if (!$this->isValidPageNumber($page)) return null;
-
- $url = $this->getURL();
- $pages = $url->getParameter($this->param);
- $name = $this->getName();
-
- if (!is_array($pages)) $pages = Array();
- $pages[$name] = $page;
- $url->setParam($this->param, $pages);
- return $url;
- }
-
- public function getName() {
- return ($this->name !== null)? $this->name : '*';
- }
-
- /**
- * This function calculates the pages to be displayed in the pagination. It
- * calculates the ideal amount of pages to be displayed (based on the max you want)
- * and generates an array with the numbers for those pages.
- *
- * If you use the default maxJump of 3 you will always receive up to 9 pages.
- * Those include the first, the last, the current and the three higher and lower
- * pages. For page 7/20 you will receive (1,4,5,6,7,8,9,10,20).
- *
- * In case the pagination doesn't find enough elements whether on the right or
- * left it will try to extend this with results on the other one. This avoids
- * broken looking paginations when reaching the final results of a set.
- *
- * @return array
- */
- public function getPageNumbers() {
- #Adds the maxjump up with the special pages (first, last, current)
- $iterationLimit = $slots = $this->maxJump * 2 + 3;
- $current = $this->getCurrentPage();
-
- if ($this->addPage($current)) $slots--;
- if ($this->addPage(1)) $slots--;
- if ($this->addPage($this->getPageCount())) $slots--;
-
- for ($i = 0; $i < $iterationLimit; $i++) {
- if ($slots > 0) if ($this->addPage ($current + $i)) $slots--;
- if ($slots > 0) if ($this->addPage ($current - $i)) $slots--;
- }
-
- $this->pages = array_filter($this->pages);
- sort($this->pages);
-
- return $this->pages;
- }
-
- public function addPage($number) {
- if (in_array($number, $this->pages)) return false;
- return $this->pages[] = $this->isValidPageNumber($number);
- }
-
- /**
- * This function checks whether the page is a good candidate for being added.
- * Therefore performing three checks before allowing a pagination to add it:
- * <ul>
- * <li>If the page already exists</li>
- * <li>If the page is lower than one</li>
- * <li>If the page number is higher than the highest</li>
- * </ul>
- * If any of those fails the page won't be added to the set.
- *
- * @param int $number The page number we wanted to add to the query.
- * @return boolean If the page was added to the pagination
- */
- public function isValidPageNumber($number) {
- if ($number < 1) return false;
- if ($number > $this->getPageCount()) return false;
-
- return $number;
- }
-
- /**
- * Sets the URL base that is used for pagination URL's. By default no
- * URL and page are used for parameters
- * @param URL $url
- * @param string $param
- */
- public function setURL(URL $url, $param) {
- $this->url = $url;
- $this->param = $param;
- }
-
- /**
- * This function receives a caption and a URL to generate one page's link for
- * your pagination. This function is by default designed to work best with
- * the most common CSS frameworks out there (Bootstrap and Foundation) and will
- * probably work with many others.
- *
- * If you want to change the output of every page do this here. Simply create
- * a class that extends this one and replace this method with whatever you
- * fancy printing.
- *
- * @param string $caption
- * @param URL $url
- * @return string
- */
- public function stringifyPage($caption, $url, $current = false) {
- if ($url !== null) {
- $class = $current? ' class="active current"' : '';
- return sprintf('<li%s><a href="%s">%s</a></li>', $class, $url, $caption);
- }
- else {
- return sprintf('<li class="disabled unavailable"><a>%s</a></li>', $caption);
- }
- }
-
- public function getEmpty() {
- return '<!--Automatically generated by Pagination::getEmpty()-->'
- . '<div style="text-align:center"><em>No results to display&hellip;</em></div>'
- . '<!---Automatically generated by Pagination::getEmpty()-->';
- }
-
- public function __toString() {
- $pages = $this->getPageNumbers();
- $previous = 0;
- $current = $this->getCurrentPage();
- $pages_html = Array();
-
- if (empty($pages)) return $this->getEmpty();
-
- //Previous
- $pages_html[] = $this->stringifyPage('&laquo;', $this->makeURL($current-1));
- //Pages
- foreach ($pages as $page) {
- if ($previous + 1 < $page) {
- $pages_html[] = $this->stringifyPage('...', null);
- }
- $pages_html[] = $this->stringifyPage($page, $this->makeURL($page), $page === $current);
- $previous = $page;
- }
- //Next
- $pages_html[] = $this->stringifyPage('&raquo;', $this->makeURL($current+1));
-
- return '<ul class="pagination">' . implode('', $pages_html) . '</ul>';
- }
-
-}
diff --git a/db/queryfield.php b/db/queryfield.php
deleted file mode 100644
index 78ba4d8..0000000
--- a/db/queryfield.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php namespace spitfire\storage\database;
-
-use spitfire\model\Field as Logical;
-
-abstract class QueryField
-{
- /** @var Logical */
- private $field;
- /** @var Query */
- private $query;
-
- public function __construct(Query$query, $field) {
- $this->query = $query;
- $this->field = $field;
- }
-
- public function setQuery($query) {
- $this->query = $query;
- }
-
- /**
- * @return Query
- */
- public function getQuery() {
- return $this->query;
- }
-
- /**
- * @return Logical
- */
- public function getField() {
- return $this->field;
- }
-
- /**
- * @return bool
- */
- public function isLogical() {
- return $this->field instanceof Logical;
- }
-
- public function getPhysical() {
- if ($this->isLogical()) {
- $fields = $this->field->getPhysical();
- foreach ($fields as &$field) $field = $this->query->queryFieldInstance($field);
- unset($field);
- return $fields;
- }
- }
-
- abstract public function __toString();
-}
diff --git a/db/restriction.php b/db/restriction.php
deleted file mode 100644
index 699fd86..0000000
--- a/db/restriction.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php namespace spitfire\storage\database;
-
-use spitfire\Model;
-use spitfire\exceptions\PrivateException;
-use spitfire\model\Field as Logical;
-
-abstract class Restriction
-{
- /** @var Query */
- private $query;
- /** @var Logical */
- private $field;
- private $value;
- private $operator;
-
- const LIKE_OPERATOR = 'LIKE';
- const EQUAL_OPERATOR = '=';
-
- public function __construct($parent, $field, $value, $operator = '=') {
- if (is_null($operator)) $operator = self::EQUAL_OPERATOR;
-
- if (!$parent instanceof RestrictionGroup && $parent !== null)
- { throw new PrivateException("A restriction's parent can only be a group"); }
-
- if ($value instanceof Model)
- $value = $value->getQuery();
-
- if (!$field instanceof QueryField)
- throw new PrivateException("Invalid field");
-
- $this->query = $parent;
- $this->field = $field;
- $this->value = $value;
- $this->operator = trim($operator);
- }
-
- public function getTable(){
- return $this->field->getField()->getTable();
- }
-
- public function setTable() {
- throw new PrivateException('Deprecated');
- }
-
- public function getField() {
- return $this->field;
- }
-
- /**
- * Returns the query this restriction belongs to. This allows a query to
- * define an alias for the table in order to avoid collissions.
- *
- * @return \spitfire\storage\database\Query
- */
- public function getQuery() {
- return $this->query->getQuery();
- }
-
- public function getParent() {
- return $this->query;
- }
-
- public function setParent($parent) {
- $this->query = $parent;
- }
-
- /**
- *
- * @param Query $query
- * @deprecated since version 0.1-dev 1604162323
- */
- public function setQuery($query) {
- $this->query = $query;
- $this->field->setQuery($query);
- }
-
- public function getOperator() {
- if (is_array($this->value) && $this->operator != 'IN' && $this->operator != 'NOT IN') return 'IN';
- return $this->operator;
- }
-
- public function getValue() {
- return $this->value;
- }
-
-
- public function getPhysicalSubqueries() {
- return Array();
- }
-
- public function getConnectingRestrictions() {
- return Array();
- }
-
- abstract public function __toString();
-}
diff --git a/model/children.php b/model/children.php
index 473f774..e94df2b 100644
--- a/model/children.php
+++ b/model/children.php
@@ -1,145 +1,146 @@
<?php
use spitfire\model\Field;
use spitfire\Model;
use spitfire\model\adapters\ChildrenAdapter;
use spitfire\exceptions\PrivateException;
use spitfire\storage\database\Schema;
/**
* The children field allows the application to maintain a relationship in the
* database that acts like an array of models that depend on the current model.
*
* @todo Move to a proper namespaced class, but we need to find a mechanism for
* aliasing classes that works properly.
*/
class ChildrenField extends Field
{
/** @var string|null */
protected $role;
/** @var string|Schema|Model */
protected $target;
/**
* @param string|Schema|Model $target
* @param string $role
*/
public function __construct($target, $role) {
$this->target = $target;
$this->role = $role;
}
/**
* Returns the model this fields is pointing to, the child model. It is referred
* as target due to the fact that this field is pointing at it.
*
* @return Schema
*/
public function getTarget() {
#If the target is actually a class name.
if (is_string($this->target) && Strings::endsWith($this->target, 'Model')) {
$this->target = trim(substr($this->target, 0, 0 - strlen('Model')), '\/');
}
#Check if the passed argument already is a model
if ($this->target instanceof Schema) {
return $this->target;
}
elseif ($this->target === $this->getModel()->getName()) {
return $this->target = $this->getModel();
}
else {
return $this->target = $this->getModel()->getTable()->getDB()->table($this->target)->getSchema();
}
}
/**
* Retrieves the field that this child field references. Although the children
* field has no representation in the DBMS it does provide a simplified
* mechanism to access the data in a table that references the current model.
*
* If the programmer provided a target we can just check that the field exists
* and return it, otherwise we need to search for a field that references
* this model and return that.
*
* @return \Reference
* @throws PrivateException
*/
public function getReferencedField() {
if (!empty($this->role)) {
#If a role is predefined, we already know what to get
return $this->getTarget()->getField($this->role);
} else {
$fields = $this->getTarget()->getFields();
#Since we could have several items pointing at our Schema we will be
#filtering the remote fields looking for candidates.
$candidates = array_filter($fields, function ($f) {
return $f instanceof \Reference && $f->getTarget() === $this->getModel();
});
#If there were no candidates we need to let the programmer know
if (empty($candidates)) {
throw new PrivateException('Children field pointing at a model that does not reference it back.');
}
#Once we have the candidates we return the first we found, since no other
#option was provided.
return reset($candidates);
}
}
public function getRole() {
return $this->role;
}
public function getPhysical() {
return Array();
}
/*
* Returns the data type, this method allows the logical field to determine
* what kind of physical fields we're gonna need to store the data for this
* type.
*/
public function getDataType() {
return Field::TYPE_CHILDREN;
}
/**
* This method used to provide a mechanism to finding the target. Since
* getReferencedField does the job and does it better, this method is deprecated.
*
* @deprecated since version 0.1-dev 20160905
* @return \Reference
*/
public function findReference() {
$model = $this->getTarget();
if ($model->getField($this->getRole())) return $model->getField($this->getRole());
else {
$fields = $model->getFields();
foreach ($fields as $field) {
if ($field instanceof Reference && $field->getTarget() === $this->getModel())
return $field;
}
}
}
public function getAdapter(Model $model) {
return new ChildrenAdapter($this, $model);
}
public function getConnectorQueries(\spitfire\storage\database\Query $parent) {
$query = $this->getTarget()->getTable()->getCollection()->getAll();
+ $of = $this->getTarget()->getTable()->getDb()->getObjectFactory();
$query->setAliased(true);
foreach ($this->getReferencedField()->getPhysical() as $p) {
- $query->addRestriction($parent->queryFieldInstance($p->getReferencedField()), $query->queryFieldInstance($p));
+ $query->addRestriction($of->queryFieldInstance($parent->getQueryTable(), $p->getReferencedField()), $of->queryFieldInstance($query->getQueryTable(), $p));
}
return Array($query);
}
}
diff --git a/model/defaults/usermodel_default.php b/model/defaults/usermodel_default.php
deleted file mode 100644
index 7edba8b..0000000
--- a/model/defaults/usermodel_default.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace spitfire\model\defaults;
-
-use spitfire\Model;
-use spitfire\storage\database\Schema;
-
-class userModel extends Model
-{
-
- public function definitions(Schema$schema) {
- $schema->username = new \StringField(20);
- $schema->password = new \StringField(40);
- $schema->email = new \StringField(40);
- $schema->admin = new \IntegerField();
- }
-}
\ No newline at end of file
diff --git a/model/reference.php b/model/reference.php
index cfd9824..63ada1e 100644
--- a/model/reference.php
+++ b/model/reference.php
@@ -1,138 +1,157 @@
<?php
use spitfire\model\Field;
use spitfire\Model;
use spitfire\model\adapters\ReferenceAdapter;
use spitfire\storage\database\Schema;
/**
* Elements of this class indicate a connection between two Models, this allows
* spitfire to let you use relational databases just like they were objects
* (connected to each other). While real elements are scattered around tables
* in no apparent manner.
*
* This class doesn't indicate how the fields inside objects relate as their
* names are automatically inherited by the source model when referencing. Names
* for the fields are always given in a <i>alias</i>_<i>srcfield</i> fashion to
* improve understanding.
*
* [NOTICE] The model doesn't check for the availability of field names, in case
* you have duplicate field names due to references the system may behave in an
* unexpected way.
*
* @author César de la cal <cesar@magic3w.com>
* @last-revision 2013-05-16
*/
class Reference extends Field
{
/**
* Indicates which model is the target of the reference. If model A
* references model B, then B is the target.
* @var Schema
*/
private $target;
/**
* Creates a new reference between two models, this allows them to access
* data through the ORM, therefore not requiring additional user generated
* queries to retrieve data.
*
* @param Schema|string $target
*/
public function __construct($target) {
$this->target = $target;
}
/**
* Defines the target model, this model is the one 'donating' it's
* primary key to the source so source can reference it as a parent
* model.
*
* @param Schema $target
*/
public function setTarget(Schema$target) {
$this->target = $target;
}
/**
* Returns the target model (the parent model of the source). Which offers
* it's primary keys to the target so it can reference them.
*
* @return Schema
*/
public function getTarget() {
#If the target is actually a class name.
if (is_string($this->target) && Strings::endsWith($this->target, 'Model')) {
$this->target = trim(substr($this->target, 0, 0 - strlen('Model')), '\/');
}
#Check if the passed argument already is a model
if ($this->target instanceof Schema) {
return $this->target;
}
elseif (strtolower($this->target) === strtolower($this->getModel()->getName())) {
return $this->target = $this->getModel();
}
else {
return $this->target = $this->getModel()->getTable()->getDb()->table($this->target)->getSchema();
}
}
/**
* Returns a list of the fields this reference generates on the source
* Model. It copies the list of primary fields from the target model and
* prepends the role (separated by underscores) to the name of the field.
*
* This function is just included for convenience. It just generates the
* data in a easy way to pick, so you cannot modify the way this is done
* or the fields below.
*
* @return Field[]
*/
public function makePhysical() {
- $fields = $this->getTarget()->getPrimary();
+ $fields = $this->getTarget()->getPrimary()->getFields()->toArray();
$physical = Array();
$_return = Array();
foreach ($fields as $field) {
$children = $field->getPhysical();
foreach ($children as $child)
$physical[] = $child;
}
foreach ($physical as $remote_field) {
$field = clone $remote_field;
$field->setName($this->getName() . '_' . $field->getName());
$field->setLogicalField($this);
$field->setReferencedField($remote_field);
$_return[] = $field;
}
return $_return;
}
/**
* Defines this field and all of it's children as reference fields.
*
* @return string
*/
public function getDataType() {
return Field::TYPE_REFERENCE;
}
public function getAdapter(Model $model) {
return new ReferenceAdapter($this, $model);
}
public function getConnectorQueries(\spitfire\storage\database\Query $parent) {
- $query = $this->getTable()->getDb()->getObjectFactory()->queryInstance($this->getTarget()->getTable());
+ $of = $this->getTable()->getDb()->getObjectFactory();
+ $query = $of->queryInstance($this->getTarget()->getTable());
$query->setAliased(true);
foreach ($this->getPhysical() as $field) {
- $query->addRestriction($parent->queryFieldInstance($field), $query->queryFieldInstance($field->getReferencedField()));
+ /*
+ * Get the field being referenced. Check if it is a valid reference value,
+ * if the field is null something went terribly wrong during assembly
+ */
+ $referenced = $field->getReferencedField();
+
+ if ($referenced === null) {
+ throw new PrivateException('Unexpected value. Refernced field is null', 1804031133);
+ }
+
+ /*
+ * Generate a query field for the local and remote queries. The order
+ * of the queries is actually not relevant.
+ */
+ $local = $of->queryFieldInstance($parent->getQueryTable(), $field);
+ $remote = $of->queryFieldInstance($query->getQueryTable(), $referenced);
+
+
+ $query->where($local, $remote);
}
return Array($query);
}
}
diff --git a/mvc/View.php b/mvc/View.php
index db74cfe..dabe86d 100644
--- a/mvc/View.php
+++ b/mvc/View.php
@@ -1,162 +1,161 @@
<?php namespace spitfire\mvc;
-use _SF_ViewElement;
use spitfire\core\Context;
use spitfire\exceptions\FileNotFoundException;
use spitfire\exceptions\PrivateException;
use spitfire\registry\CSSRegistry;
use spitfire\registry\JSRegistry;
class View extends MVC
{
private $file = '';
private $data = Array();
private $js;
private $css;
private $render_template = true;
private $render_layout = true;
private $layout;
private $extension;
const default_view = 'default.php';
/**
* Creates a new view. The view allows to present the data your application
* manages in a consistent way and manage and locate the templates the app
* needs.
*
* @param \spitfire\Context $context
*/
public function __construct(Context$context) {
parent::__construct($context);
#Get the answer format
$this->extension = $context->request->getPath()->getFormat();
#Create registries
$this->js = new JSRegistry();
$this->css = new CSSRegistry();
#Initialize the files
$this->getFiles();
}
public function getFiles() {
/*
* Set default files. This includes the view's file, layout and
* the basedir for elements.
*/
$basedir = $this->app->getTemplateDirectory();
$controller = strtolower(implode(DIRECTORY_SEPARATOR, $this->app->getControllerURI($this->controller)));
$action = $this->action;
$extension = $this->extension === 'php'? '' : '.' . $this->extension;
spitfire()->getRequest()->getResponse()->getHeaders()->contentType($extension);
if ( file_exists("$basedir$controller/$action$extension.php"))
$this->file = "$basedir$controller/$action$extension.php";
else
$this->file = "$basedir$controller$extension.php";
if ( file_exists("{$basedir}layout$extension.php"))
$this->layout = "{$basedir}layout$extension.php";
}
/**
* Defines a variable inside the view.
* @param String $key
* @param mixed $value
*/
public function set($key, $value) {
//echo $key;
$this->data[$key] = $value;
return $this;
}
/**
* Sets the file to be used by the template system. Please note that it can
* accept either the full patch to the file (like bin/templates/controller/action.php)
* or just the path relative to the template directory.
*
* Using the relative Path to the template directory would be a recommended
* practice in order to reduce the need to change your coding when changing
* your directory structure.
*
* If the file is provided with no extension, this method will consider the
* standard extension scheme for views inside spitfire
*
* @param string $fileName
* @throws FileNotFoundException
*/
public function setFile ($fileName) {
if (!file_exists($fileName)) {
$fileName = $this->app->getTemplateDirectory() . $fileName;
}
$extension = ($this->extension === 'php'? '' : '.' . $this->extension) . '.php';
if (!file_exists($fileName) && file_exists($fileName . $extension)) {
$fileName.= $extension;
}
if (file_exists($fileName)) { $this->file = $fileName; }
else { throw new FileNotFoundException('File ' . $fileName . 'not found. View can\'t use it'); }
}
public function setLayoutFile($filename) {
$filename = $this->app->getTemplateDirectory() . $filename;
if (file_exists($filename)) { $this->layout = $filename; }
else { throw new FileNotFoundException('File ' . $filename . ' not found. View can\'t use it as layout'); }
}
public function element($file) {
$filename = $this->app->getTemplateDirectory() . 'elements/' . $file . '.php';
if (!file_exists($filename)) throw new PrivateException('Element ' . $file . ' missing');
- return new _SF_ViewElement($filename, $this->data);
+ return new ViewElement($filename, $this->data);
}
public function setRenderTemplate($set) {
$this->render_template = $set;
}
public function setRenderLayout($set) {
$this->render_layout = $set;
}
public function render () {
#If the template is not to be rendered at all. Use this.
if (!$this->render_template) { echo $this->data['_SF_DEBUG_OUTPUT']; return; }
#Consider that a missing template file that should be rendered is an error
if (!file_exists($this->file)) { throw new PrivateException('Missing template file for ' . get_class($this->controller) . '::' . $this->action); }
ob_start();
foreach ($this->data as $data_var => $data_content) {
$$data_var = $data_content;
}
include $this->file;
$content_for_layout = ob_get_clean();
if ($this->render_layout && file_exists($this->layout) ) { include ($this->layout); }
else { echo $content_for_layout; }
}
public function css($add = null) {
if ($add) { $this->css->add ($add); }
else { return $this->css; }
}
public function js($add = null) {
if ($add) $this->js->add ($add);
else return $this->js;
}
}
\ No newline at end of file
diff --git a/mvc/view_element.php b/mvc/ViewElement.php
similarity index 90%
rename from mvc/view_element.php
rename to mvc/ViewElement.php
index 2e97255..e2eb95f 100644
--- a/mvc/view_element.php
+++ b/mvc/ViewElement.php
@@ -1,33 +1,33 @@
-<?php
+<?php namespace spitfire\mvc;
use spitfire\mvc\MVC;
-class _SF_ViewElement extends MVC
+class ViewElement extends MVC
{
private $file;
private $data;
public function __construct($file, $data) {
$this->file = $file;
$this->data = $data;
}
public function set ($key, $value) {
$this->data[$key] = $value;
return $this;
}
public function render () {
ob_start();
foreach ($this->data as $k => $v) $$k = $v;
echo '<!-- Started: ' . $this->file .' -->' . "\n";
include $this->file;
echo "\n" . '<!-- Ended: ' . $this->file .' -->';
return ob_get_clean();
}
public function __toString() {
return $this->render();
}
}
\ No newline at end of file
diff --git a/storage/database/CompositeRestriction.php b/storage/database/CompositeRestriction.php
index db0f9b5..fee0f8f 100644
--- a/storage/database/CompositeRestriction.php
+++ b/storage/database/CompositeRestriction.php
@@ -1,150 +1,143 @@
<?php namespace spitfire\storage\database;
use spitfire\model\Field as Logical;
use spitfire\Model;
+use BadMethodCallException;
class CompositeRestriction
{
private $parent;
private $field;
private $value;
private $operator;
public function __construct(RestrictionGroup$parent, Logical$field = null, $value = null, $operator = Restriction::EQUAL_OPERATOR) {
if ($value instanceof Model) { $value = $value->getQuery(); }
if ($value instanceof Query) { $value->setAliased(true); }
+ else { throw new BadMethodCallException('Composite restriction requires a query / model as value', 1804201334); }
$this->parent = $parent;
$this->field = $field;
$this->value = $value;
$this->operator = $operator;
}
/**
*
* @return Query
*/
public function getQuery() {
return $this->parent? $this->parent->getQuery() : null;
}
/**
*
* @return RestrictionGroup
*/
public function getParent() {
return $this->parent;
}
public function setQuery(Query$query) {
$this->parent = $query;
}
public function setParent(RestrictionGroup$query) {
$this->parent = $query;
+ return $this;
}
public function getField() {
return $this->field;
}
public function setField(Logical$field) {
$this->field = $field;
}
-
+
+ /**
+ *
+ * @return Query
+ */
public function getValue() {
if ($this->value instanceof Model) { $this->value = $this->value->getQuery(); }
return $this->value;
}
public function setValue($value) {
$this->value = $value;
}
public function getOperator() {
return $this->operator === null? '=' : $this->operator;
}
public function setOperator($operator) {
$this->operator = $operator;
}
- /**
- * This method handles NULL scenarios.
- *
- * This method simplifies complex restrictions when null values are involved.
- * Usually, when querying you will define an equivalence between two values and
- * launch the query. This method is called when that involves null.
- *
- * You can either have a null value, which will force the database to check that
- * the physical fields composing your logical field are null.
- *
- * Or you can have a null field. Which will force the database to check that
- * one of the fields that this table has equals to the value you specified.
- *
- * Please note that the usage of this function for other scenarios has been
- * deprecated since 11/2014
- *
- *
- * @deprecated since version 0.1-dev 20171115
- * @return type
- */
- public function getSimpleRestrictions() {
-
- trigger_error('CompositeRestriction::getSimpleRestrictions() is deprecated', E_USER_DEPRECATED);
+ public function getSubqueries() {
+ $r = array_merge($this->getValue()->getSubqueries(), [$this->getValue()]);
+ return $r;
+ }
+
+ public function replaceQueryTable($old, $new) {
- if ($this->field === null) {
- $table = $this->getQuery()->getTable();
- $fields = $table->getFields();
- $restrictions = $this->getQuery()->restrictionGroupInstance();
-
- foreach ($fields as $field) {
- if (!$field->getLogicalField() instanceof \Reference) {
- $restrictions->addRestriction($field, $this->getValue(), $this->operator);
- }
- }
- return Array($restrictions);
- }
+ //TODO: The fact that the composite restriction is not using query tables is off-putting
+ return true;
- if ($this->value === null) {
- $restrictions = Array();
- foreach ($fields = $this->getField()->getPhysical() as $field) {
- $f = $this->getQuery()->queryFieldInstance($field);
- $v = null;
- $r = $this->getQuery()->restrictionInstance($f, $v, $this->operator);
- $restrictions[] = $r;
- }
- return $restrictions;
- }
}
-
- public function getPhysicalSubqueries() {
- if ($this->field === null || $this->value === null) { return Array(); }
-
+ public function makeConnector() {
$field = $this->getField();
+ $value = $this->getValue();
+ $of = $this->getQuery()->getTable()->getDb()->getObjectFactory();
$connector = $field->getConnectorQueries($this->getQuery());
- $last = end($connector);
+ $last = array_pop($connector);
$last->setId($this->getValue()->getId());
- /*
- * Since layered composite restrictions cannot be handled in the same way
- * as their "higher" counterparts we need to reorganize the restrictions
- * for subsqueries of subqueries.
- *
- * Basically, in higher levels we indicate that the top query should either
- * include or not the lower levels. This is not supported on tables that
- * get joined.
- *
- * This currently causes a redundant restrictions to appear, but these shouldn't
- * harm the operation as it is.
+ if ($field === null || $value === null) {
+ throw new PrivateException('Deprecated: Composite restrictions do not receive null parameters', 2801191504);
+ }
+
+ /**
*
+ * @var MysqlPDOQuery The query
+ */
+ $group = $of->restrictionGroupInstance($this->getQuery(), RestrictionGroup::TYPE_AND);
+
+ /**
+ * The system needs to create a copy of the subordinated restrictions
+ * to be able to syntax a proper SQL query.
*/
- $subqueries = $this->getValue()->getPhysicalSubqueries();
+ $group->add($last->toArray());
- return array_merge($connector, $subqueries);
+ /*
+ * Once we looped over the sub restrictions, we can determine whether the
+ * additional group is actually necessary. If it is, we add it to the output
+ */
+ if (!$group->isEmpty()) {
+ $this->getValue()->push($group);
+ }
+
+ return $connector;
+ }
+
+ public function negate() {
+ switch ($this->operator) {
+ case '=':
+ return $this->operator = '<>';
+ case '<>':
+ return $this->operator = '=';
+ case '!=':
+ return $this->operator = '=';
+ }
+ }
+
+ public function __clone() {
+ $this->value = clone $this->value;
}
}
diff --git a/storage/database/DB.php b/storage/database/DB.php
index ccf6b11..1d61a31 100644
--- a/storage/database/DB.php
+++ b/storage/database/DB.php
@@ -1,189 +1,185 @@
<?php namespace spitfire\storage\database;
use BadMethodCallException;
use spitfire\cache\MemoryCache;
use spitfire\core\Environment;
use spitfire\exceptions\PrivateException;
use spitfire\io\CharsetEncoder;
use spitfire\storage\database\restrictionmaker\RestrictionMaker;
/**
* This class creates a "bridge" beetwen the classes that use it and the actual
* driver.
*
* @author César de la Cal <cesar@magic3w.com>
*/
abstract class DB
{
- const MYSQL_PDO_DRIVER = 'mysqlPDO';
-
private $settings;
- private $tableCache;
+ private $tables;
private $encoder;
private $restrictionMaker;
/**
* Creates an instance of DBInterface. If options are set it will import
* them. Otherwise it will try to read them from the current environment.
*
- * @param Settings $settings Name of the database driver to be used. You can
- * choose one of the DBInterface::DRIVER_? consts.
+ * @param Settings $settings Parameters passed to the database
*/
public function __construct(Settings$settings) {
- $this->settings = $settings;
-
- $this->tableCache = new TablePool($this);
- $this->encoder = new CharsetEncoder(Environment::get('system_encoding'), $settings->getEncoding());
+ $this->settings = $settings;
+ $this->tables = new TablePool($this);
+ $this->encoder = new CharsetEncoder(Environment::get('system_encoding'), $settings->getEncoding());
$this->restrictionMaker = new RestrictionMaker();
}
/**
* The encoder will allow the application to encode / decode database that
* is directed towards or comes from the database.
*
* @return CharsetEncoder
*/
public function getEncoder() {
return $this->encoder;
}
/**
* Gets the connection settings for this connection.
*
* @return Settings
*/
public function getSettings() {
return $this->settings;
}
/**
* Attempts to repair schema inconsistencies. These method is not meant
* to be called by the user but aims to provide an endpoint the driver
* can use when running into trouble.
*
* This method does not actually repair broken databases but broken schemas,
* if your database is broken or data on it corrupt you need to use the
* DBMS specific tools to repair it.
*
* Repairs the list of tables/models <b>currently</b> loaded into the db.
* If a model hasn't been accessed during execution it won't be listed
* here.
*
* Please note, that this function is used only for maintenance and repair
* works on tables. Meaning that it is not relevant if <b>all</b> tables
* were imported.
*/
public function repair() {
- $tables = $this->tableCache->getCache()->getAll();
+ $tables = $this->tables->getCache()->getAll();
foreach ($tables as $table) {
$table->getLayout()->repair();
}
}
/**
* Returns a table adapter for the database table with said name to allow
* querying and data-manipulation..
*
* @param string|Schema $tablename Name of the table that should be used.
* If you pass a model to this function it will automatically
* read the name from the model and use it to find the
* table.
*
* @throws PrivateException If the table could not be found
* @return Table The database table adapter
*/
public function table($tablename) {
#If the parameter is a Model, we get it's name
if ($tablename instanceof Schema) {
- return $this->tableCache->set($tablename->getName(), new Table($this, $tablename));
+ return $this->tables->set($tablename->getName(), new Table($this, $tablename));
}
#We just tested if it's a Schema, let's see if it's a string
if (!is_string($tablename)) {
throw new BadMethodCallException('DB::table requires Schema or string as argument');
}
#Check if the table can be found in the table cache
- return $this->tableCache->get(strtolower($tablename));
+ return $this->tables->get(strtolower($tablename));
}
/**
* Returns our table cache. This allows an application that uses Spitfire (or
* it's own core) to check whether a table is already cached or inject it's
* own tables.
*
- * @return MemoryCache
+ * @return TablePool
*/
public function getTableCache() {
- return $this->tableCache;
+ return $this->tables;
}
/**
*
* @return RestrictionMaker
*/
public function getRestrictionMaker() {
return $this->restrictionMaker;
}
/**
* Allows short-hand access to tables by using: $db->tablename
*
* @param string $table Name of the table
* @return Table
*/
public function __get($table) {
#Otherwise we try to get the table with this name
return $this->table($table);
}
/**
* Returns the handle used by the system to connect to the database.
* Depending on the driver this can be any type of content. It should
* only be used by applications with special needs.
*
* @abstract
* @return mixed The connector used by the system to communicate with
* the database server. The data-type of the return value depends on
* the driver used by the system.
*/
abstract public function getConnection();
/**
* Allows the application to create the database needed to store the tables
* and therefore data for the application. Some DBMS like SQLite won't support
* multiple databases - so this may not do anything.
*
* @abstract
* @return bool Returns whether the operation could be completed successfully
*/
abstract public function create();
/**
* Destroys the database and all of it's contents. Drivers may not allow
* this method to be called unless they're being operated in debug mode or
* a similar mode.
*
* @abstract
* @throws PrivateException If the driver rejected the operation
* @return bool Whether the operation could be completed
*/
abstract public function destroy();
/**
* In modern spitfire drivers, all the object creation for a database is handled
* by the object factories. This factories allow the system to create any object
* they need: Queries, Tables, Fields...
*
* This removes the need to have some driver specific objects just for the
* sake of providing a certain type. This way all SQL drivers can share some
* standard components while replacing the ones they specifically need.
*
* @return ObjectFactoryInterface
*/
abstract public function getObjectFactory();
}
diff --git a/storage/database/ObjectFactoryInterface.php b/storage/database/ObjectFactoryInterface.php
index 1681f0d..4268791 100644
--- a/storage/database/ObjectFactoryInterface.php
+++ b/storage/database/ObjectFactoryInterface.php
@@ -1,170 +1,158 @@
<?php namespace spitfire\storage\database;
use spitfire\model\Field as LogicalField;
/*
* The MIT License
*
* Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
/**
* The database object factory is a class that allows a driver to provide SF's
* ORM with all the required bits and pieces to operate. Usually a driver needs
* to provide it's own Table, Query, Field... objects that implement / extend
* the behavior required for the ORM to work.
*
* Historically, a query would provide only the pieces it needed, as well as the
* table would. But for consistency, and to avoid generating classes that only
* need to extend in order to provide factories we're merging those behaviors
* in this single factory.
*/
interface ObjectFactoryInterface
{
- /**
- * Returns an instance of the class the child tables of this class have
- * this is used to create them when requested by the table() method.
- *
- * @deprecated since version 0.1-dev 20170801
- *
- * @param DB $db
- * @param string $tablename
- *
- * @return Table Instance of the table class the driver wants the system to use
- */
- function getTableInstance(DB$db, $tablename);
-
/**
* Creates a relation. These wrap the typical record operations on a table
* into a separate layer.
*
* @param Table $table
*
* @return Relation
*/
function makeRelation(Table$table);
/**
* Creates a table layout to generate an appropriate schema for the DBMS to
* store the data.
*
* @param Table $table
*
* @return LayoutInterface The layout for the table
*/
function makeLayout(Table$table);
/**
* Creates a new On The Fly Schema. These allow the system to interact with a
* database that was not modeled after Spitfire's models or that was not
* reverse engineered previously.
*
* @param string $modelname
*
* @return Table Instance of the table class the driver wants the system to use
* @todo Rename to generateSchema
*/
- function getOTFSchema($modelname);
+ function getOTFSchema(DB$db, $modelname);
/**
* Creates an instance of the Database field compatible with the current
* DBMS. As opposed to the Logical fields, physical fields do not accept
* complex values, just basic types that any DBMS can handle.
*
* @param Field $field
* @param string $name
* @param Field $references
*
* @return Field Field
* @todo Rename to makeField
*/
function getFieldInstance(LogicalField$field, $name, Field$references = null);
/**
* Creates a new restriction. This combines a query with a field and a value
* which allows to create the queries that we need to construct in order to
* retrieve data.
*
* @param string $query
* @param QueryField $field
* @param mixed $value
* @param string|null $operator
*
* @return Restriction|CompositeRestriction
* @todo Rename to makeRestriction
*/
function restrictionInstance($query, QueryField$field, $value, $operator = null);
/**
*
* @todo This is supposed to take a RestrictionGroup
- * @param Query $query
+ * @param RestrictionGroup $parent
* @param LogicalField $field
* @param mixed $value
* @param string $operator
*/
- function restrictionCompositeInstance(Query$query, LogicalField$field = null, $value, $operator);
+ function restrictionCompositeInstance(RestrictionGroup$parent, LogicalField$field = null, $value = null, $operator = null);
/**
* Creates a restriction group. This allows to associate several restrictions
* with each other to create more complicated queries when writing.
*
* @param RestrictionGroup $parent
* @param int $type
* @return RestrictionGroup A restriction group
*/
function restrictionGroupInstance(RestrictionGroup$parent = null, $type = RestrictionGroup::TYPE_OR);
/**
* Creates a new query. A query is created with a table to provide information
* where the data should be retrieved some and some information on the fields
* that we want it to provide.
*
* @param Table|Relation $table
*
* @return Query
* @todo Rename to makeQuery
*/
function queryInstance($table);
/**
* These objects connect a field with a query, providing an aliased name for
* the field when necessary.
*
- * @param Query $query
- * @param QueryField $field
+ * @todo The second parameter should only accept physical and not logical fields
+ *
+ * @param QueryTable $queryTable
+ * @param Field|QueryField $field
* @return QueryField
*/
- function queryFieldInstance($query, $field);
+ function queryFieldInstance(QueryTable$queryTable, $field);
/**
* These objects connect a field with a query, providing an aliased name for
* the field when necessary.
*
- * @param Query $query
* @param QueryTable|Table $table
* @return QueryTable
*/
- function queryTableInstance($query, $table);
+ function queryTableInstance($table);
}
diff --git a/storage/database/Query.php b/storage/database/Query.php
index 5e707e4..d6e5f36 100644
--- a/storage/database/Query.php
+++ b/storage/database/Query.php
@@ -1,366 +1,394 @@
<?php namespace spitfire\storage\database;
use Exception;
use spitfire\exceptions\PrivateException;
+use spitfire\exceptions\PublicException;
use spitfire\Model;
use spitfire\model\Field as LogicalField;
abstract class Query extends RestrictionGroup
{
/**
* The result for the query. Currently, this is attached to the query - this
* means that whenever the query is "re-executed" the result is overwritten
* and could potentially damage the resultset.
*
* This would require a significant change in the API, since it requires the
* app to not loop fetch() calls over the query but actually retrieve the
* result element and loop over that.
*
* @todo This should be removed in favor of an actual collector for the results
* @deprecated since version 0.1-dev 20170414
* @var \spitfire\storage\database\ResultSetInterface|null
*/
protected $result;
/**
* The table this query is retrieving data from. This table is wrapped inside
* a QueryTable object to ensure that the table can refer back to the query
* when needed.
*
* @var QueryTable
*/
protected $table;
- protected $page = 1;
- protected $rpp = -1;
- protected $order;
- protected $groupby = null;
-
/**
*
- * @deprecated since version 0.1-dev 20160406
- * @var int|null
+ * @todo We should introduce a class that allows these queries to sort by multiple,
+ * and even layered (as in, in other queries) columns.
+ * @var string
*/
- private $count = null;
+ protected $order;
+ protected $groupby = null;
/** @param Table $table */
public function __construct($table) {
- $this->table = $this->queryTableInstance($table);
+ $this->table = $table->getDb()->getObjectFactory()->queryTableInstance($table);
#Initialize the parent
parent::__construct(null, Array());
$this->setType(RestrictionGroup::TYPE_AND);
}
/**
*
* @param string $fieldname
* @param string $value
* @param string $operator
* @deprecated since version 0.1-dev 20170414
* @return Query
*/
public function addRestriction($fieldname, $value, $operator = '=') {
$this->result = null;
return parent::addRestriction($fieldname, $value, $operator);
}
/**
*
* @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
* @param boolean $aliased
*/
public function setAliased($aliased) {
$this->table->setAliased($aliased);
}
/**
*
* @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
* @return boolean
*/
public function getAliased() {
return $this->table->isAliased();
}
/**
*
* @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
* @return int
*/
public function getId() {
return $this->table->getId();
}
/**
*
* @param int $id
* @deprecated since version 0.1-dev 20160406
+ * @remove 20180711
* @return \spitfire\storage\database\Query
*/
public function setId($id) {
$this->table->setId($id);
return $this;
}
/**
* Since a query is the top Level of any group we can no longer climb up the
* ladder.
*
* @throws PrivateException
*/
public function endGroup() {
throw new PrivateException('Called endGroup on a query', 1604031547);
}
public function getQuery() {
return $this;
}
/**
* Sets the amount of results returned by the query.
*
+ * @deprecated since version 0.1-dev 20180509
+ * @remove 20180911
* @param int $amt
*
* @return self
*/
public function setResultsPerPage($amt) {
- $this->rpp = $amt;
- return $this;
+ trigger_error('Deprecated Query::setResultsPerPage() invoked', E_USER_DEPRECATED);
+ throw new PrivateException('Pagination has been moved. Please refer to the documentation', 1805111238);
}
/**
+ *
+ * @deprecated since version 0.1-dev 20180509
+ * @remove 20180911
* @return int The amount of results the query returns when executed.
*/
public function getResultsPerPage() {
- return $this->rpp;
+ return false;
}
/**
* @deprecated since version 0.1-dev 20170414
* @param int $page The page of results currently displayed.
+ * @removed 20180511
* @return boolean Returns if the page se is valid.
*/
public function setPage ($page) {
- #The page can't be lower than 1
- if ($page < 1) return false;
- $this->page = $page;
- return true;
+ trigger_error('Deprecated Query::setPage() invoked', E_USER_DEPRECATED);
+ throw new PrivateException('Pagination has been moved. Please refer to the documentation', 1805111238);
}
/**
*
* @deprecated since version 0.1-dev 20170414
- * @return type
+ * @remove 20180911
+ * @return int
*/
public function getPage() {
- return $this->page;
+ trigger_error('Deprecated Query::getPage() invoked', E_USER_DEPRECATED);
+ return false;
}
//@TODO: Add a decent way to sorting fields that doesn't resort to this awful thing.
public function setOrder ($field, $mode) {
try {
$this->order['field'] = $this->table->getTable()->getField($field);
} catch (Exception $ex) {
$physical = $this->table->getTable()->getModel()->getField($field)->getPhysical();
$this->order['field'] = reset($physical);
}
$this->order['mode'] = $mode;
return $this;
}
/**
* Returns a record from a database that matches the query we sent.
*
* @deprecated since version 0.1-dev 20170414
* @return Model
*/
public function fetch() {
if (!$this->result) { $this->query(); }
$data = $this->result->fetch();
return $data;
}
+
+ /**
+ * This method returns a single record from a query, allowing your application
+ * to quickly query the database for a record it needs.
+ *
+ * The onEmpty parameter allows you to inject a callback in the event the query
+ * returns no value. You can provide a callable or an Exception (which will
+ * be thrown), reducing the amount of if/else in your controllers.
+ *
+ * The code
+ * <code>
+ * $user = db()->table('user')->get('_id', $uid)->first();
+ * if (!$user) { throw new PublicException('No user found', 404); }
+ * </code>
+ *
+ * Can therefore be condensed into:
+ * <code>
+ * $user = db()->table('user')->get('_id', $uid)->first(new PublicException('No user found', 404));
+ * </code>
+ *
+ * If you wish to further condense this, you can just provide onEmpty as `true`
+ * which will then cause the system to raise a `PublicException` with a standard
+ * 'No %tablename found' and a 404 code. Causing this code to boil down to:
+ *
+ * <code>
+ * $user = db()->table('user')->get('_id', $uid)->first(true);
+ * </code>
+ *
+ * While this seems more unwieldy at first, the code gains a lot of clarity
+ * when written like this
+ *
+ * @param callable|\Exception|true|null $onEmpty
+ * @return Model|null
+ */
+ public function first($onEmpty = null) {
+ $res = $this->execute(null, 0, 1)->fetch();
+
+ if (!$res && $onEmpty) {
+ if ($onEmpty instanceof \Exception) { throw $onEmpty;}
+ elseif(is_callable($onEmpty)) { return $onEmpty(); }
+ elseif($onEmpty === true) { throw new PublicException(sprintf('No %s found', $this->getTable()->getSchema()->getName()), 404); }
+ }
+
+ return $res;
+ }
+
+ /**
+ * This method returns a finite amount of items matching the parameters from
+ * the database. This method always returns a collection, even if the result
+ * is empty (no records matched the query)
+ *
+ * @param int $skip
+ * @param int $amt
+ * @return \spitfire\core\Collection
+ */
+ public function range($skip = 0, $amt = 1) {
+ if ($skip < 0 || $amt < 1) {
+ throw new \InvalidArgumentException('Query received invalid arguments', 1805091416);
+ }
+
+ return $this->execute(null, $skip, $amt)->fetchAll();
+ }
+
+ /**
+ *
+ * @return \spitfire\core\Collection
+ */
+ public function all() {
+ return $this->execute()->fetchAll();
+ }
/**
* Returns all the records that the query matched. This method wraps the records
* inside a collection object to make them easier to access.
- *
+ *
+ * @deprecated since version 0.1-dev 20180509
* @return \spitfire\core\Collection[]
*/
public function fetchAll() {
if (!$this->result) { $this->query(); }
return new \spitfire\core\Collection($this->result->fetchAll());
}
-
+
+ /**
+ *
+ * @deprecated since version 0.1-dev 20180509 We do no longer provide the option to not return the result
+ * @param mixed[] $fields
+ * @param bool $returnresult
+ * @return type
+ */
protected function query($fields = null, $returnresult = false) {
$result = $this->execute($fields);
if ($returnresult) { return $result; }
else { return $this->result = $result; }
}
/**
* Deletes the records matching this query. This will not retrieve the data and
* therefore is more efficient than fetching and later deleting.
*
* @todo Currently does not support deleting of complex queries.
* @return int Number of affected records
*/
public abstract function delete();
/**
* Counts the number of records a query would return. If there is a grouping
* defined it will count the number of records each group would return.
*
- * @return type
+ * @todo This method's behavior is extremely inconsistent
+ * @return int
*/
public function count() {
- if (!$this->groupby) {
- //This is a temporary fix that will only count distinct values in complex
- //queries.
- $query = $this->query(Array('COUNT(DISTINCT ' . $this->table->getTable()->getPrimaryKey()->getFields()->join(', ') . ')'), true)->fetchArray();
- $count = reset($query);
- return $this->count = (int)$count;
- }
- elseif(count($this->groupby) === 1) {
- $_ret = Array();
- $cursor = $this->query(Array(reset($this->groupby), 'count(*)'), true);
-
- while ($row = $cursor->fetchArray()) { $_ret[reset($row)] = end($row); }
- return $_ret;
- }
+ //This is a temporary fix that will only count distinct values in complex
+ //queries.
+ $query = $this->query(Array('COUNT(DISTINCT ' . $this->table->getTable()->getPrimaryKey()->getFields()->join(', ') . ')'), true)->fetchArray();
+ $count = reset($query);
+ return (int)$count;
}
/**
* Defines a column or array of columns the system will be using to group
* data when generating aggregates.
*
+ * @todo When adding aggregation, the system should automatically use the aggregation for extraction
+ * @todo Currently the system only supports grouping and not aggregation, this is a bit of a strange situation that needs resolution
+ *
* @param LogicalField|FieLogicalFieldld[]|null $column
* @return Query Description
*/
public function aggregateBy($column) {
if (is_array($column)) { $this->groupby = $column; }
elseif($column === null) { $this->groupby = null; }
else { $this->groupby = Array($column); }
return $this;
}
/**
* Creates the execution plan for this query. This is an array of queries that
* aid relational DBMSs' drivers when generating SQL for the database.
*
* This basically generate the connecting queries between the tables and injects
* your restrictions in between so the system egenrates logical routes that
* will be understood by the relational DB.
*
+ * @deprecated since version 0.1-dev 20180510
+ * @todo Move somewhere else. This only pertains to relational DBMS systems
* @return Query[]
*/
public function makeExecutionPlan() {
$_ret = $this->getPhysicalSubqueries();
array_push($_ret, $this);
return $_ret;
}
public function getOrder() {
return $this->order;
}
- public function getQueryTable() {
- return $this->table;
- }
-
/**
* Returns the current 'query table'. This is an object that allows the query
* to alias it's table if needed.
*
* @return QueryTable
*/
- public function getTable() {
- return $this->table->getTable();
- }
-
- public function __toString() {
- return $this->getTable() . implode(',', $this->getRestrictions());
+ public function getQueryTable() {
+ return $this->table;
}
- /**
- * This method is used to clean empty restriction groups and restrictions from
- * a query. This allows to 'optimize' the speed of SQL due to removing potentially
- * unnecessary joins and subqueries.
- *
- * @todo This function needs to go.
- * @deprecated since version 0.1-dev 201704142031
- * @param Restriction|CompositeRestriction|RestrictionGroup $restriction
- * @return boolean
- */
- public static function restrictionFilter($restriction) {
- #In case the data contained is a restriction we consider it valid.
- #Restrictions can by default not be empty (they always have a field attached)
- if ($restriction instanceof Restriction) {
- return true;
- }
+ public function cloneQueryTable() {
+ $table = clone $this->table;
+ $table->newId();
- #Composite restrictions are the most common source of possible empty elements
- #If they contain a query and it is empty it will not add any value to the query
- if ($restriction instanceof CompositeRestriction) {
- return true;
- }
+ $this->replaceQueryTable($this->table, $table);
- #Restriction groups that are empty will not do anything useful and maybe
- #even generate invalid SQL like '() AND' so we clean them beforehand.
- if ($restriction instanceof RestrictionGroup) {
- $restrictions = array_filter($restriction->getRestrictions(), Array(get_class(), __METHOD__));
-
- if (empty($restrictions)) {
- return false;
- }
- else {
- $restriction->setRestrictions($restrictions);
- return true;
- }
- }
+ $this->table = $table;
+ return $this->table;
}
- public abstract function execute($fields = null);
-
/**
+ * Returns the actual table this query is searching on.
*
- * @deprecated since version 0.1-dev 2017102609
+ * @return Table
*/
- public abstract function restrictionInstance(QueryField$field, $value, $operator);
-
- /**
- *
- * @deprecated since version 0.1-dev 2017102609
- */
- public abstract function compositeRestrictionInstance(LogicalField$field = null, $value, $operator);
-
- /**
- * Creates a new instance of a restriction group for this query. The instance
- * is already created with a reference to this element. This is just used in
- * a set of cases, when creating a restriction (so it keeps the reference to
- * the query) and when "ending the group" which basically returns the call flow
- * over to the query.
- *
- * @deprecated since version 0.1-dev 20171110
- * @return \spitfire\storage\database\RestrictionGroup
- */
- public abstract function restrictionGroupInstance($parent);
+ public function getTable() {
+ return $this->table->getTable();
+ }
- /**
- *
- * @deprecated since version 0.1-dev 20171110
- */
- public abstract function queryFieldInstance($field);
+ public function __toString() {
+ return $this->getTable()->getLayout()->getTableName() . implode(',', $this->toArray());
+ }
/**
*
- * @deprecated since version 0.1-dev 20171110
+ * @return ResultSetInterface
*/
- public abstract function queryTableInstance($table);
+ public abstract function execute($fields = null, $offset = null, $max = null);
}
diff --git a/storage/database/QueryField.php b/storage/database/QueryField.php
new file mode 100644
index 0000000..134edfa
--- /dev/null
+++ b/storage/database/QueryField.php
@@ -0,0 +1,109 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\model\Field as Logical;
+
+/**
+ * The query field object is a component that allows a Query to wrap a field and
+ * connect it to itself. This is important for the DBA since it allows the app
+ * to establish connections between the different queries when assembling SQL
+ * or similar.
+ *
+ * When a query is connected to a field, you may use this to establish relationships
+ * and create complex queries that can properly be joined.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ * @abstract
+ */
+abstract class QueryField
+{
+ /**
+ * The actual database field. Note that this field is
+ *
+ * @var Logical
+ */
+ private $field;
+
+ /**
+ *
+ * @var QueryTable
+ */
+ private $table;
+
+ public function __construct(QueryTable$table, $field) {
+ $this->table = $table;
+ $this->field = $field;
+ }
+
+ /**
+ * Returns the parent Table for this field.
+ *
+ * @return QueryTable
+ */
+ public function getQueryTable() : QueryTable {
+ return $this->table;
+ }
+
+ public function setQueryTable(QueryTable $table) {
+ $this->table = $table;
+ return $this;
+ }
+
+ /**
+ * Returns the source field for this object.
+ *
+ * @return Logical|Field
+ */
+ public function getField() {
+ return $this->field;
+ }
+
+ /**
+ *
+ *
+ * @return bool
+ */
+ public function isLogical() : bool {
+ return $this->field instanceof Logical;
+ }
+
+ /**
+ * Returns an array of fields that compose the physical components of the
+ * field. This method automatically converts the fields to QueryField so they
+ * can be used again.
+ *
+ * @return Field[]
+ */
+ public function getPhysical() : array {
+ /*
+ * Get the object factory for the current DB connection. It is then used
+ * to create physical copies of logical fields.
+ */
+ $of = $this->table->getTable()->getDb()->getObjectFactory();
+
+ if ($this->isLogical()) {
+ $fields = $this->field->getPhysical();
+
+ foreach ($fields as &$field) {
+ $field = $of->queryFieldInstance($this->table, $field);
+ }
+
+ return $fields;
+ }
+
+ return [$of->queryFieldInstance($this->table, $this->field)];
+ }
+
+ /**
+ * Many drivers use this objects to generate "object identifiers", strings that
+ * indicate what field in which table is being adressed. So we're forcing driver
+ * vendors to implement the __toString method to achieve the most consistent
+ * result possible.
+ *
+ * This may not be the case for your driver. In this event, just return a string
+ * that may be used for debugging and create an additional method for your
+ * driver.
+ *
+ * @return string
+ */
+ abstract public function __toString();
+}
diff --git a/db/querytable.php b/storage/database/QueryTable.php
similarity index 62%
rename from db/querytable.php
rename to storage/database/QueryTable.php
index b18ea9a..91e18c3 100644
--- a/db/querytable.php
+++ b/storage/database/QueryTable.php
@@ -1,86 +1,88 @@
<?php
namespace spitfire\storage\database;
abstract class QueryTable
{
private $table;
- /**
- * The query is no longer needed for the aliasing, since the table now takes
- * care of it autonomously.
- *
- * @deprecated since version 0.1-dev 20160406
- * @var Query
- */
- private $query;
-
/**
* The following variables manage the aliasing system inside spitfire. To avoid
* having different tables with the same name in them, Spitfire uses aliases
* for the tables. These aliases are automatically generated by adding a unique
* number to the table's name.
*
* The counter is in charge of making sure that every table is uniquely named,
* every time a new query table is created the current value is assigned and
* incremented.
*
* @var int
*/
private static $counter = 1;
private $id;
private $aliased = false;
- public function __construct(Query$query, Table$table) {
+ public function __construct(Table$table) {
#In case this table is aliased, the unique alias will be generated using this.
$this->id = self::$counter++;
- $this->query = $query;
$this->table = $table;
}
- /**
- *
- * @return \spitfire\storage\database\Query
- */
- public function getQuery() {
- return $this->query;
- }
-
- public function setQuery($query) {
- $this->query = $query;
- }
-
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
+ public function newId() {
+ $this->id = self::$counter++;
+ }
+
public function setAliased($aliased) {
$this->aliased = $aliased;
}
public function isAliased() {
return $this->aliased;
}
public function getAlias() {
- return $this->aliased?
- sprintf('%s_%s', $this->table->getTablename(), $this->id) :
- $this->table->getTablename();
+ /*
+ * Get the name for the table. We use it to provide a consistent naming
+ * system that makes it easier for debugging.
+ */
+ $name = $this->table->getLayout()->getTablename();
+
+ return $this->aliased? sprintf('%s_%s', $name, $this->id) : $name;
+ }
+
+ public function getField($name) {
+ $of = $this->table->getDb()->getObjectFactory();
+ return $of->queryFieldInstance($this, $this->table->getField($name));
+ }
+
+ public function getFields() {
+ $of = $this->table->getDb()->getObjectFactory();
+ $fields = $this->table->getLayout()->getFields();
+
+ foreach ($fields as &$field) {
+ $field = $of->queryFieldInstance($this, $field);
+ }
+
+ return $fields;
}
/**
*
* @return \spitfire\storage\database\Table
*/
public function getTable() {
return $this->table;
}
abstract public function definition();
abstract public function __toString();
}
\ No newline at end of file
diff --git a/storage/database/Restriction.php b/storage/database/Restriction.php
new file mode 100644
index 0000000..9f9241b
--- /dev/null
+++ b/storage/database/Restriction.php
@@ -0,0 +1,155 @@
+<?php namespace spitfire\storage\database;
+
+use spitfire\exceptions\PrivateException;
+
+/**
+ * A restriction indicates a condition a record in a database's relation must
+ * satisfy to be returned by a database query.
+ *
+ * Restrictions can be either simple (like these) or composite. These simple ones
+ * can only contain basic data-types like integers, floats, strings or enums as
+ * their value.
+ *
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+abstract class Restriction
+{
+ /**
+ *
+ * @var RestrictionGroup
+ */
+ private $parent;
+
+ /**
+ *
+ * @var QueryField
+ */
+ private $field;
+ private $value;
+ private $operator;
+
+ const LIKE_OPERATOR = 'LIKE';
+ const EQUAL_OPERATOR = '=';
+
+ public function __construct($parent, $field, $value, $operator = '=') {
+ if (is_null($operator)) {
+ $operator = self::EQUAL_OPERATOR;
+ }
+
+ if (!$parent instanceof RestrictionGroup && $parent !== null) {
+ throw new PrivateException("A restriction's parent can only be a group", 1804292129);
+ }
+
+ if (!$field instanceof QueryField) {
+ throw new PrivateException("Invalid field");
+ }
+
+ $this->parent = $parent;
+ $this->field = $field;
+ $this->value = $value;
+ $this->operator = trim($operator);
+ }
+
+ public function getTable(){
+ return $this->field->getField()->getTable();
+ }
+
+ public function setTable() {
+ throw new PrivateException('Deprecated');
+ }
+
+ public function getField() {
+ return $this->field;
+ }
+
+ /**
+ * Returns the query this restriction belongs to. This allows a query to
+ * define an alias for the table in order to avoid collissions.
+ *
+ * @return Query
+ */
+ public function getQuery() {
+ return $this->parent->getQuery();
+ }
+
+ public function getParent() {
+ return $this->parent;
+ }
+
+ public function setParent($parent) {
+ $this->parent = $parent;
+ }
+
+ /**
+ *
+ * @param Query $query
+ * @deprecated since version 0.1-dev 1604162323
+ */
+ public function setQuery($query) {
+ $this->parent = $query;
+ $this->field->setQuery($query);
+ }
+
+ public function getOperator() {
+ if (is_array($this->value) && $this->operator != 'IN' && $this->operator != 'NOT IN') return 'IN';
+ return $this->operator;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+
+ public function getPhysicalSubqueries() {
+ return Array();
+ }
+
+
+ public function getSubqueries() {
+ return Array();
+ }
+
+ public function getConnectingRestrictions() {
+ return Array();
+ }
+
+ public function replaceQueryTable($old, $new) {
+
+ if ($this->field->getQueryTable() === $old) {
+ $this->field->setQueryTable($new);
+ }
+
+ if ($this->value instanceof QueryField && $this->value->getQueryTable() === $old) {
+ $this->value->setQueryTable($new);
+ }
+ }
+
+ public function negate() {
+ switch ($this->operator) {
+ case '=':
+ return $this->operator = '<>';
+ case '<>':
+ return $this->operator = '=';
+ case '>':
+ return $this->operator = '<';
+ case '<':
+ return $this->operator = '>';
+ case 'IS':
+ return $this->operator = 'IS NOT';
+ case 'IS NOT':
+ return $this->operator = 'IS';
+ case 'LIKE':
+ return $this->operator = 'NOT LIKE';
+ case 'NOT LIKE':
+ return $this->operator = 'LIKE';
+ }
+ }
+
+ /**
+ * Restrictions must be able to be casted to string. This is not only often
+ * necessary for many drivers to generate queries but also for debugging.
+ *
+ * @return string
+ */
+ abstract public function __toString();
+}
diff --git a/storage/database/RestrictionGroup.php b/storage/database/RestrictionGroup.php
index 2d11742..0f75dde 100644
--- a/storage/database/RestrictionGroup.php
+++ b/storage/database/RestrictionGroup.php
@@ -1,287 +1,295 @@
<?php namespace spitfire\storage\database;
-use Exception;
use InvalidArgumentException;
-use Reference;
use spitfire\core\Collection;
use spitfire\exceptions\PrivateException;
/**
* A restriction group contains a set of restrictions (or restriction groups)
* that can be used by the database to generate more complex queries.
*
* This groups can be different of two different types, they can be 'OR' or 'AND',
* changing the behavior of the group by making it more or less restrictive. This
* OR and AND types are known from most DBMS.
*/
abstract class RestrictionGroup extends Collection
{
const TYPE_OR = 'OR';
const TYPE_AND = 'AND';
private $parent;
private $type = self::TYPE_OR;
+ private $negated = false;
public function __construct(RestrictionGroup$parent = null, $restrictions = Array() ) {
$this->parent = $parent;
parent::__construct($restrictions);
}
/**
*
* @deprecated since version 0.1-dev 20170720
- * @param type $r
- */
- public function removeRestriction($r) {
- parent::remove($r);
- }
-
- /**
- *
- * @deprecated since version 0.1-dev 20170720
+ * @remove 20180711
* @param type $restriction
*/
public function putRestriction($restriction) {
parent::push($restriction);
}
- /**
- *
- * @deprecated since version 0.1-dev 20170720
- * @param type $restrictions
- */
- public function setRestrictions($restrictions) {
- parent::reset();
- parent::add($restrictions);
- }
-
/**
* Adds a restriction to the current query. Restraining the data a field
* in it can contain.
*
* @todo This method does not accept logical fields as parameters
* @see http://www.spitfirephp.com/wiki/index.php/Method:spitfire/storage/database/Query::addRestriction
*
* @deprecated since version 0.1-dev 20170923
+ * @remove 20180711
* @param string $fieldname
* @param mixed $value
* @param string $operator
* @return RestrictionGroup
* @throws PrivateException
*/
public function addRestriction($fieldname, $value, $operator = '=') {
return $this->where($fieldname, $operator, $value);
}
/**
* Adds a restriction to the current query. Restraining the data a field
* in it can contain.
*
* @todo This method does not accept logical fields as parameters
* @see http://www.spitfirephp.com/wiki/index.php/Method:spitfire/storage/database/Query::addRestriction
*
* @param string $fieldname
* @param mixed $value
* @param string $_
* @return RestrictionGroup
* @throws PrivateException
*/
public function where($fieldname, $value, $_ = null) {
$params = func_num_args();
$rm = $this->getQuery()->getTable()->getDb()->getRestrictionMaker();
/*
* Depending on how the parameters are provided, where will appropriately
* shuffle them to make them look correctly.
*/
if ($params === 3) { list($operator, $value) = [$value, $_]; }
else { $operator = '='; }
- parent::push($rm->make($this, $fieldname, $operator, $value));
+ $this->push($rm->make($this, $fieldname, $operator, $value));
return $this;
}
/**
*
* @deprecated since version 0.1-dev 20170720
* @param type $restrictions
*/
public function getRestrictions() {
return parent::toArray();
}
- public function importRestrictions(RestrictionGroup$query) {
- $restrictions = $query->getRestrictions();
-
- foreach($restrictions as $r) {
- $copy = clone $r;
- $copy->setParent($this);
- $this->putRestriction($copy);
- }
- }
-
/**
*
* @deprecated since version 0.1-dev 20170720
* @param type $index
*/
public function getRestriction($index) {
return parent::offsetGet($index);
}
/**
*
- * @deprecated since version 0.1-dev 20170720
- * @return type
- */
- public function getConnectingRestrictions() {
- trigger_error('Method RestrictionGroup::getConnectingRestrictions() is deprecated', E_USER_DEPRECATED);
- $_ret = Array();
-
- foreach ($this->toArray() as $r) { $_ret = array_merge($_ret, $r->getConnectingRestrictions());}
-
- return $_ret;
- }
-
- /**
- *
- * @deprecated since version 0.1-dev 20171110
- */
- public function filterCompositeRestrictions() {
- $restrictions = $this->toArray();
-
- foreach ($restrictions as $r) {
- if ($r instanceof CompositeRestriction) { $this->removeRestriction($r); }
- if ($r instanceof RestrictionGroup) { $r->filterCompositeRestrictions(); }
- }
- }
-
- /**
- *
- * @deprecated since version 0.1-dev 20171110
*/
- public function filterSimpleRestrictions() {
- $restrictions = $this->toArray();
+ public function getCompositeRestrictions() {
- foreach ($restrictions as $r) {
- if ($r instanceof Restriction) { $this->removeRestriction($r); }
- if ($r instanceof RestrictionGroup) { $r->filterSimpleRestrictions(); }
- }
- }
-
- /**
- * Removes empty groups from the group. This is important since otherwise a
- * query generator will most likely generate malformed SQL for this query.
- */
- public function filterEmptyGroups() {
- $restrictions = $this->toArray();
-
- foreach ($restrictions as $r) {
- if ($r instanceof RestrictionGroup) { $r->filterEmptyGroups(); }
- if ($r instanceof RestrictionGroup && $r->isEmpty()) { $this->remove($r); }
- }
+ return $this->each(function ($r) {
+ if ($r instanceof CompositeRestriction) { return $r; }
+ if ($r instanceof RestrictionGroup) { return $r->getCompositeRestrictions(); }
+ return null;
+ })
+ ->flatten()
+ ->filter(function ($e) {
+ return $e !== null && ($e instanceof CompositeRestriction || !$e->isEmpty());
+ });
}
/**
* @param string $type
* @return RestrictionGroup
*/
public function group($type = self::TYPE_OR) {
#Create the group and set the type we need
- $group = $this->getQuery()->restrictionGroupInstance($this);
+ $group = $this->getQuery()->getTable()->getDb()->getObjectFactory()->restrictionGroupInstance($this);
$group->setType($type);
#Add it to our restriction list
return $this->push($group);
}
public function endGroup() {
return $this->parent;
}
+ /**
+ *
+ * @deprecated since version 0.1-dev 20180420
+ * @param \spitfire\storage\database\Query $query
+ * @return $this
+ */
public function setQuery(Query$query) {
$this->parent = $query;
-
- foreach ($this->restrictions as $restriction) { $restriction->setQuery($query);}
+ return $this;
}
public function setParent(RestrictionGroup$query) {
$this->parent = $query;
- foreach ($this->restrictions as $restriction) { $restriction->setParent($query);}
+ return $this;
}
public function getParent() {
return $this->parent;
}
/**
* As opposed to the getParent method, the getQuery method will ensure that
* the return is a query.
*
* This allows the application to quickly get information about the query even
* if the restrictions are inside of several layers of restriction groups.
*
* @return Query
*/
public function getQuery() {
return $this->parent->getQuery();
}
public function setType($type) {
if ($type === self::TYPE_AND || $type === self::TYPE_OR) {
$this->type = $type;
return $this;
}
else {
throw new InvalidArgumentException("Restriction groups can only be of type AND or OR");
}
}
public function getType() {
return $this->type;
}
+ public function getSubqueries() {
+
+ /*
+ * First, we extract the physical queries from the underlying queries.
+ * These queries should be executed first, to make it easy for the system
+ * to retrieve the data the query depends on.
+ */
+ $_ret = Array();
+
+ foreach ($this as $r) {
+ $_ret = array_merge($_ret, $r->getSubqueries());
+ }
+
+ return $_ret;
+ }
+
+ public function replaceQueryTable($old, $new) {
+
+
+ foreach ($this->getRestrictions() as $r) {
+ $r->replaceQueryTable($old, $new);
+ }
+ }
+
+ public function negate() {
+ $this->negated = !$this->negated;
+ return $this;
+ }
+
+ public function normalize() {
+ if ($this->negated) {
+ $this->flip();
+ }
+
+ $this
+ /*
+ * We normalize the children first. This ensures that the normalization
+ * the parent performs is still correct.
+ */
+ ->filter(function ($e) { return $e instanceof RestrictionGroup; })
+ ->each(function (RestrictionGroup$e) { return $e->normalize(); })
+
+ /*
+ * We remove the groups that satisfy any of the following:
+ * * They're empty
+ * * They only contain one restriction
+ * * They have the same type as the current one. Based on (A AND B) AND C == A AND B AND C
+ */
+ ->filter(function (RestrictionGroup$e) { return $e->getType() === $this->getType() || $e->count() < 2; })
+ ->each(function ($e) {
+ $this->add($e->each(function ($e) { $e->setParent($this); return $e; })->toArray());
+ $this->remove($e);
+ });
+
+ return $this;
+ }
+
/**
- * This is the equivalent of makeExecutionPlan on the root query for any subquery.
- * Since subqueries are logical root queries and can be executed just like
- * normal ones they require an equivalent method that is named differently.
+ * When a restriction group is flipped, the system will change the type from
+ * AND to OR and viceversa. When doing so, all the restrictions are negated.
*
- * It retrieves all the subqueries that are needed to be executed on a relational
- * DB before the main query.
+ * This means that <code>$a == $a->flip()</code> even though they have inverted
+ * types. This is specially interesting for query optimization and negation.
*
- * We could have used a single method with a flag, but this way seems cleaner
- * and more hassle free than otherwise.
- *
- * @return Query[]
+ * @return RestrictionGroup
*/
- public function getPhysicalSubqueries() {
- $_ret = Array();
+ public function flip() {
+ $this->negated = !$this->negated;
+ $this->type = $this->type === self::TYPE_AND? self::TYPE_OR : self::TYPE_AND;
+
+ foreach ($this as $restriction) {
+ if ($restriction instanceof Restriction ||
+ $restriction instanceof CompositeRestriction ||
+ $restriction instanceof RestrictionGroup) { $restriction->negate(); }
+ }
- foreach ($this->getRestrictions() as $r) {
- $_ret = array_merge($_ret, $r->getPhysicalSubqueries());
+ return $this;
+ }
+
+ public function isMixed() {
+ $found = false;
+
+ foreach ($this as $r) {
+ if ($r instanceof RestrictionGroup && ($r->getType() !== $this->getType() || $r->isMixed())) {
+ $found = true;
+ }
}
- return $_ret;
+ return $found;
}
/**
* When cloning a restriction group we need to ensure that the new restrictions
* are assigned to the parent, and not some other object.
*
* TODO: This would be potentially much simpler if the collection provided a
* walk method that would allow to modify the elements from within.
*/
public function __clone() {
- $restrictions = $this->getRestrictions();
+ $restrictions = $this->toArray();
foreach ($restrictions as &$r) {
$r = clone $r;
$r->setParent($this);
}
$this->reset()->add($restrictions);
}
abstract public function __toString();
}
diff --git a/storage/database/Schema.php b/storage/database/Schema.php
index f9cf3e6..0cc8602 100644
--- a/storage/database/Schema.php
+++ b/storage/database/Schema.php
@@ -1,314 +1,315 @@
<?php namespace spitfire\storage\database;
use IntegerField;
use spitfire\core\Collection;
use spitfire\exceptions\PrivateException;
use spitfire\model\Field;
use spitfire\model\Index;
use spitfire\storage\database\Query;
use spitfire\storage\database\Table;
/**
* A Schema is a class used to define how Spitfire stores data into a DBMS. We
* usually consider a DBMS as relational database engine, but Spitfire can
* be connected to virtually any engine that stores data. Including No-SQL
* databases and directly on the file system. You should even be able to use
* tapes, although that would be extra slow.
*
* Every model contains fields and references. Fields are direct data-types, they
* allow storing things directly into them, while references are pointers to
* other models allowing you to store more complex data into them.
*
* @property IntegerField $_id This default primary key integer helps the system
* locating the records easily.
*
* @todo Add index support, so models can create indexes that are somewhat more complex
* @author César de la Cal <cesar@magic3w.com>
*/
class Schema
{
/**
* Contains a list of the fields that this model uses t ostore data.
* Fields are stored in a FILO way, so the earlier you register a field
* the further left will it be on a database table (if you look at it
* in table mode).
*
* @var Field[]
*/
private $fields;
/**
* The indexes the table can use to optimize the search performance.
*
* @var Collection <Index>
*/
private $indexes;
/**
* Contains a reference to the table this model is 'templating'. This
* means that the current model is attached to said table and offers to
* it information about the data that is stored to the DBMS and the format
* it should hold.
*
* @var Table
*/
private $table;
/**
* The name of the table that represents this schema on DBMS' side. This will
* be automatically generated from the class name and will be replacing the
* invalid inverted bar (\) with hyphens (-) that are not valid as a class name.
*
* @var string
*/
private $name;
/**
* Creates a new instance of the Model. This allows Spitfire to create
* and manage data accordingly to your wishes on a DB engine without
* requiring you to integrate with any concrete engine but writing code
* that SF will translate.
*
* @param string $name
* @param Table $table
*/
public final function __construct($name, Table$table = null) {
#Define the Model's table as the one just received
$this->table = $table;
$this->name = strtolower($name);
#Create a field called ID that automatically identifies records
$this->_id = new IntegerField(true);
$this->_id->setAutoIncrement(true);
#Create a default index for the primary key
$pk = new Index([$this->_id]);
$pk->setPrimary(true);
#Create the index collection
$this->indexes = new Collection([$pk]);
}
/**
* Imports a set of fields. This allows to back them up in case they're
* needed. Please note that the parent setting for them will be rewritten.
*
* @param Field[] $fields
*/
public function setFields($fields) {
#Loop through the fields to import them
foreach($fields as $field) {
$this->{$field->getName()} = $field; #This triggers the setter
}
}
/**
* Returns a logical field for this model. "Logical field" refers to fields
* that can also contain complex datatypes aka References.
*
* You can use the Field::getPhysical() method to retrieve the physical fields
* the application uses to interact with the DBMS.
*
* @param string $name
* @return Field|null
*/
public function getField($name) {
if (isset($this->fields[$name])) { return $this->fields[$name]; }
else { return null; }
}
/**
* Returns the whole list of fields this model contains. This are logical fields
* and therefore can contain data that is too complex to be stored directly
* by a DB Engine, the table object is in charge of providing a list of
* DB Friendly fields.
*
* @return Field[]
*/
public function getFields() {
return $this->fields;
}
/**
* Returns the 'name' of the model. The name of a model is obtained by
* removing the Model part of tit's class name. It's best practice to
* avoid the usage of this function for anything rather than logging.
*
* This function has a special use case, it also defines the name of the
* future table. By changing this you change the table this model uses
* on DBMS, this is particularly useful when creating multiple models
* that refer to a single dataset like 'People' and 'Adults'.
*
* @staticvar string $name
* @return string
*/
public final function getName() {
return $this->name;
}
/**
* Returns the tablename spitfire considers best for this Model. This
* value is calculated by using the Model's name and replacing any
* <b>\</b> with hyphens to make the name database friendly.
*
* Hyphens are the only chars that DBMS tend to accept that class names
* do not. So this way we avoid any colissions in names that could be
* coincidentally similar.
*
* @return string
*/
public function getTableName() {
return trim(str_replace('\\', '-', $this->getName()), '-_ ');
}
/**
* Returns the table the Schema represents. The schema is the logical representation
* of a Table. While the Schema will manage logical fields that the programmer
* can directly write data to, the Table will take that data and translate it
* so the database engine can use it.
*
* @return Table
*/
public function getTable() {
return $this->table;
}
/**
* Sets the table this schema manages. This connection is used to determine
* what DBMS table it should address and to make correct data conversion.
*
* @param Table $table
*/
public function setTable($table) {
$this->table = $table;
}
/**
* Extending this function on models allows you to add restrictions to a
* query when this is made for this model. This way you can generate a
* behavior similar to a view where the records are always limited by
* the restriction set in this function.
*
* This is especially useful when fake deleting records from the database.
* You use a flag to indicate a record is deleted and use this function
* to hide any records that have that flag.
*
* @todo Move to the model. It makes no sense having it here
* @param Query $query The query that is being prepared to be executed.
* @return type
*/
public function getBaseRestrictions(Query$query) {
//Do nothing, this is meant for overriding
}
public function index() {
$fields = func_get_args();
$index = new Index($fields);
$this->indexes->push($index);
return $index;
}
/**
* Returns the collection of indexes that are contained in this model.
*
* @return Collection <Index>
*/
public function getIndexes() {
return $this->indexes;
}
/**
* Returns a list of fields which compound the primary key of this model.
* The primary key is a set of records that identify a unique record.
*
- * @deprecated since version 0.1-dev 20170824
- * @return Field[]
+ * @return Index
*/
public function getPrimary() {
#Fetch the field list
- $fields = $this->getFields();
- #Drop the fields which aren't primary
- foreach ($fields as $name => $field) {
- if (!$field->isPrimary()) { unset($fields[$name]); }
+ $indexes = $this->indexes;
+
+ #Loop over the indexes and get the primary one
+ foreach ($indexes as $index) {
+ if ($index->isPrimary()) { return $index; }
}
- #Return the cleared array
- return $fields;
+
+ #If there was no index, then return a null value
+ return null;
}
/**
* The getters and setters for this class allow us to create fields with
* a simplified syntax and access them just like they were properties
* of the object. Please note that some experts recommend avoiding magic
* methods for performance reasons. In this case you can use the field()
* method.
*
* @param string $name
* @param Field $value
*/
public function __set($name, $value) {
/*
* First we need to check if the field already exists. In the event of us
* overwriting the field we need to remove it from the already existing
* indexes
*/
if (isset($this->fields[$name])) {
unset($this->$name);
}
/*
* Check if the schema received a field. Because if that's not the case we
* can't deal with it properly.
*/
if (!$value instanceof Field) {
throw new PrivateException('Schema received something else than a field', 1710181717);
}
$value->setName($name);
$value->setSchema($this);
$this->fields[$name] = $value;
}
/**
* The getters and setters for this class allow us to create fields with
* a simplified syntax and access them just like they were properties
* of the object. Please note that some experts recommend avoiding magic
* methods for performance reasons. In this case you can use the field()
* method.
*
* @param string $name
* @throws PrivateException
* @return Field
*/
public function __get($name) {
if (isset($this->fields[$name])) { return $this->fields[$name]; }
else { throw new PrivateException('Schema: No field ' . $name . ' found'); }
}
/**
* Removes a field from the Schema. This is a somewhat rare method, since you
* should avoid it's usage in production environments and you should REALLY
* know what you're doing before using it.
*
* @param string $name
* @throws PrivateException
*/
public function __unset($name) {
#Check if the field actually exists.
if (!isset($this->fields[$name])) {
throw new PrivateException('Schema: Could not delete. No field ' . $name . ' found');
}
#Get the field
$f = $this->fields[$name];
unset($this->fields[$name]);
#Find an index that may contain the field and remove it too
$this->indexes = $this->indexes->filter(function ($e) use ($f) {
return $e->contains($f);
});
}
}
diff --git a/storage/database/Table.php b/storage/database/Table.php
index d4448a2..7acce6e 100644
--- a/storage/database/Table.php
+++ b/storage/database/Table.php
@@ -1,305 +1,294 @@
<?php namespace spitfire\storage\database;
use CoffeeBean;
use Model;
use spitfire\exceptions\PrivateException;
use spitfire\storage\database\Schema;
/**
* This class simulates a table belonging to a database. This way we can query
* and handle tables with 'compiler-friendly' code that will inform about errors.
*
* @author César de la Cal <cesar@magic3w.com>
*/
class Table
{
/**
* A reference to the database driver loaded. This allows the system to
* use several databases without the models colliding.
*
* @var DB
*/
protected $db;
/**
* The model this table uses as template to create itself on the DBMS. This is
* one of the key components to Spitfire's ORM as it allows the DB engine to
* create the tables automatically and to discover the data relations.
*
* @var Schema
*/
protected $schema;
/**
* Provides access to the table's layout (physical schema)
*
* @var LayoutInterface
*/
private $layout = false;
/**
* Provides access to the table's record operations. Basically, a relational
* table is composed of schema + relation (data).
*
* @var Relation
*/
private $relation;
/**
* Contains the bean this table uses to generate forms for itself. The bean
* contains additional data to make the data request more user friendly.
*
* @var CoffeeBean
*/
protected $bean;
/**
* Caches a list of fields that compound this table's primary key. The property
* is empty when the table is constructed and collects the primary key's fields
* once they are requested for the first time.
*
* @var \spitfire\storage\database\Index|null
*/
protected $primaryK;
/**
* Just like the primary key field, this property caches the field that contains
* the autonumeric field. This will usually be the ID that the DB refers to
* when working with the table.
*
* @var Field
*/
protected $autoIncrement;
/**
* Creates a new Database Table instance. The tablename will be used to find
* the right model for the table and will be stored prefixed to this object.
*
* @param DB $db
* @param string|Schema $schema
*
* @throws PrivateException
*/
public function __construct(DB$db, $schema) {
$this->db = $db;
$factory = $this->db->getObjectFactory();
if (!$schema instanceof Schema) {
throw new PrivateException('Table requires a Schema to be passed');
}
#Attach the schema to this table
$this->schema = $schema;
$this->schema->setTable($this);
#Create a database table layout (physical schema)
$this->layout = $factory->makeLayout($this);
#Create the relation
$this->relation = $factory->makeRelation($this);
}
/**
* Fetch the fields of the table the database works with. If the programmer
* has defined a custom set of fields to work with, this function will
* return the overridden fields.
*
* @return Field[] The fields this table handles.
*/
public function getFields() {
trigger_error('Deprecated function Table::getFields() called', E_USER_DEPRECATED);
return $this->layout->getFields();
}
/**
*
* @deprecated since version 0.1-dev 20171128
* @param type $name
* @return type
*/
public function getField($name) {
trigger_error('Deprecated function Table::getField() called', E_USER_DEPRECATED);
return $this->layout->getField($name);
}
- /**
- * Returns the name of the table that is being used. The table name
- * includes the database's prefix.
- *
- * @return string
- */
- public function getTablename() {
- trigger_error('Deprecated function Table::getTablename() called', E_USER_DEPRECATED);
- return $this->layout->getTableName();
- }
-
/**
* Returns the database the table belongs to.
* @return DB
*/
public function getDb() {
return $this->db;
}
/**
* Get's the table's primary key. This will always return an array
* containing the fields the Primary Key contains.
*
* @return IndexInterface
*/
public function getPrimaryKey() {
/*
* If the primary was already determined, we use the cached version.
*/
if ($this->primaryK) { return $this->primaryK; }
$indexes = $this->layout->getIndexes();
return $this->primaryK = $indexes->filter(function (IndexInterface$i) { return $i->isPrimary(); })->rewind();
}
public function getAutoIncrement() {
if ($this->autoIncrement) { return $this->autoIncrement; }
//Implicit else
$fields = $this->layout->getFields();
foreach($fields as $field) {
if ($field->getLogicalField()->isAutoIncrement()) { return $this->autoIncrement = $field; }
}
return null;
}
/**
* Looks for a record based on it's primary data. This can be one of the
* following:
* <ul>
* <li>A single basic data field like a string or a int</li>
* <li>A string separated by : to separate those fields (SF POST standard)</li>
* <li>An array with the data</li>
* </ul>
* This function is intended to be used to provide controllers with prebuilt
* models so they don't need to fetch it again.
*
* @todo Move to relation
*
* @param mixed $id
*
* @return Model
*/
public function getById($id) {
#If the data is a string separate by colons
if (!is_array($id)) { $id = explode(':', $id); }
#Create a query
$table = $this;
$primary = $table->getPrimaryKey()->getFields();
$query = $table->getDb()->getObjectFactory()->queryInstance($this);
#Add the restrictions
while(!$primary->isEmpty()) {
$query->where($primary->shift(), array_shift($id));
}
#Return the result
$_return = $query->fetch();
return $_return;
}
/**
*
* @deprecated since version 0.1-dev 20160902
* @return Schema
*/
public function getModel() {
return $this->schema;
}
/**
*
* @deprecated since version 0.1-dev 20170801
* @return Relation
*/
public function getCollection() {
return $this->relation;
}
/**
* Gives access to the relation, the table's component that manages the data
* that the table contains.
*
* @return Relation
*/
public function getRelation() {
return $this->relation;
}
/**
*
* @return LayoutInterface
*/
public function getLayout(): LayoutInterface {
return $this->layout;
}
/**
*
* @return Schema
*/
public function getSchema() {
return $this->schema;
}
/**
* Returns the bean this model uses to generate Forms to feed itself with data
* the returned value normally is a class that inherits from CoffeeBean.
*
* @deprecated since version 0.1-dev 20161220
* @return CoffeeBean
*/
public function getBean($name = null) {
if (!$name) { $beanName = $this->schema->getName() . 'Bean'; }
else { $beanName = $name . 'Bean'; }
$bean = new $beanName($this);
return $bean;
}
public function get($field, $value, $operator = '=') {
return $this->relation->get($field, $value, $operator);
}
public function getAll() {
return $this->relation->getAll();
}
public function newRecord($data = Array()) {
return $this->relation->newRecord($data);
}
/**
* If the table cannot handle the request it will pass it on to the db
* and add itself to the arguments list.
*
* @param string $name
* @param mixed $arguments
*
* @return mixed
*/
public function __call($name, $arguments) {
#We basically reject __call since it is a bad programming habit to rely on
#redirecting every call
trigger_error("Called Table::__call() requesting $name()", E_USER_DEPRECATED);
#Add the table to the arguments for the db
array_unshift($arguments, $this);
#Pass on
return call_user_func_array(Array($this->db, $name), $arguments);
}
}
diff --git a/storage/database/TablePool.php b/storage/database/TablePool.php
index d19ede1..8665eec 100644
--- a/storage/database/TablePool.php
+++ b/storage/database/TablePool.php
@@ -1,124 +1,124 @@
<?php namespace spitfire\storage\database;
use InvalidArgumentException;
use spitfire\cache\MemoryCache;
use spitfire\exceptions\PrivateException;
use spitfire\storage\database\tablelocator\CacheLocator;
use spitfire\storage\database\tablelocator\NameLocator;
use spitfire\storage\database\tablelocator\OTFTableLocator;
use spitfire\storage\database\tablelocator\TableLocatorInterface;
use spitfire\storage\database\tablelocator\TypoCacheLocator;
use spitfire\storage\database\tablelocator\TypoLocator;
use Strings;
/*
* The MIT License
*
* Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
/**
* Contains a table list for a database. Please note that this system is neither
* caps sensitive nor is it plural sensitive. When looking for the table
* "deliveries" it will automatically check for "delivery" too.
*
* @author César de la Cal Bretschneider <cesar@magic3w.com>
*/
class TablePool
{
/**
* The database this contains tables for. This is important since the database
* offloads table "makes" to the pool.
*
* @var TableLocatorInterface[]
*/
private $tableLocators;
private $cache;
/**
* Creates a new Table pool object. This object is designed to cache tables
* across several queries, allowing for them to refer to the same schemas and
* data-caches that the tables provide.
*
* @param DB $db
*/
public function __construct(DB$db) {
$this->cache = new MemoryCache();
$this->tableLocators = [
new CacheLocator($this->cache),
new NameLocator($db),
- new TypoCacheLocator($this->cache, function ($e) { Strings::singular($e); }),
- new TypoCacheLocator($this->cache, function ($e) { Strings::plural($e); }),
- new TypoLocator($db, function ($e) { Strings::singular($e); }),
- new TypoLocator($db, function ($e) { Strings::plural($e); }),
+ new TypoCacheLocator($this->cache, function ($e) { return Strings::singular($e); }),
+ new TypoCacheLocator($this->cache, function ($e) { return Strings::plural($e); }),
+ new TypoLocator($db, function ($e) { return Strings::singular($e); }),
+ new TypoLocator($db, function ($e) { return Strings::plural($e); }),
new OTFTableLocator($db)
];
}
/**
* Pushes a table into the pool. This method will check that it's receiving a
* proper table object.
*
* @param string $key
* @param Table $value
* @return Table
* @throws InvalidArgumentException
*/
public function set($key, $value) {
if (!$value instanceof Table) {
throw new InvalidArgumentException('Table is required');
}
return $this->cache->set($key, $value);
}
/**
* Returns the Table that the user is requesting from the pool. The pool will
* automatically check if the table was misspelled.
*
* @param string $key
* @return Table
* @throws PrivateException
*/
public function get($key) {
$table = false;
$locators = $this->tableLocators;
while (!$table && $locators) {
$locator = array_shift($locators);
$table = $locator->locate($key);
}
if ($table) {
return $this->set($key, $table);
}
throw new PrivateException(sprintf('Table %s was not found', $key));
}
public function getCache() {
return $this->cache;
}
}
\ No newline at end of file
diff --git a/storage/database/drivers/mysqlpdo/CompositeRestriction.php b/storage/database/drivers/mysqlpdo/CompositeRestriction.php
new file mode 100644
index 0000000..a209632
--- /dev/null
+++ b/storage/database/drivers/mysqlpdo/CompositeRestriction.php
@@ -0,0 +1,57 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\CompositeRestriction as ParentClass;
+use spitfire\storage\database\RestrictionGroup;
+
+class CompositeRestriction extends ParentClass
+{
+
+ /**
+ * When a query is serialized, the composite restrictions generate a list of
+ * simple ones that can be passed onto the database for querying.
+ *
+ * In the case of MySQLPDO, the driver assumes that the query has been properly
+ * denormalized to be serialized.
+ *
+ * @return RestrictionGroup
+ */
+ public function makeSimpleRestrictions() {
+
+ $of = $this->getQuery()->getTable()->getDb()->getObjectFactory();
+
+ /*
+ * Extract the primary fields for the remote table so we can indicate to the
+ * database whether they should be null or not.
+ *
+ * Please note that we will always use "IS NOT NULL" so the connectors stay
+ * consistent with the rest of the restrictions
+ */
+ $fields = $this->getValue()->getQueryTable()->getTable()->getPrimaryKey()->getFields();
+ $group = $of->restrictionGroupInstance($this->getParent());
+
+ /*
+ * Loop over the fields and put them in an array so it can be concatenated
+ * before being returned.
+ */
+ foreach($fields as $field) {
+ $qt = $this->getValue()->getRedirection()? $this->getValue()->getRedirection()->getQueryTable() : $this->getValue()->getQueryTable();
+ $group->push($of->restrictionInstance($group, $of->queryFieldInstance($qt, $field), null, $this->getOperator() === '='? 'IS NOT' : 'IS'));
+ }
+
+ return $group;
+ }
+
+ public function __toString() {
+ $field = $this->getField();
+ $value = $this->getValue();
+
+ if ($field === null || $value === null) {
+ throw new PrivateException('Deprecated: Composite restrictions do not receive null parameters', 2801191504);
+ }
+
+ return strval($this->makeSimpleRestrictions());
+
+ }
+
+}
\ No newline at end of file
diff --git a/db/drivers/mysqlPDOField.php b/storage/database/drivers/mysqlpdo/Field.php
similarity index 85%
rename from db/drivers/mysqlPDOField.php
rename to storage/database/drivers/mysqlpdo/Field.php
index 11584a4..1879f3a 100644
--- a/db/drivers/mysqlPDOField.php
+++ b/storage/database/drivers/mysqlpdo/Field.php
@@ -1,67 +1,67 @@
-<?php namespace spitfire\storage\database\drivers;
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
use spitfire\model\Field as LogicalField;
-use spitfire\storage\database\Field;
+use spitfire\storage\database\Field as ParentClass;
use \Reference;
-class mysqlPDOField extends Field
+class Field extends ParentClass
{
public function columnType() {
$logical = $this->getLogicalField();
if ($logical instanceof Reference) {
$referenced = $this->getReferencedField();
while($referenced->getReferencedField()) { $referenced = $referenced->getReferencedField(); }
$logical = $referenced->getLogicalField();
}
switch ($logical->getDataType()) {
case LogicalField::TYPE_INTEGER:
return 'INT(11)';
case LogicalField::TYPE_FLOAT:
return 'DOUBLE';
case LogicalField::TYPE_LONG:
return 'BIGINT';
case LogicalField::TYPE_STRING:
return "VARCHAR({$logical->getLength()})";
case LogicalField::TYPE_FILE:
return "VARCHAR(255)";
case LogicalField::TYPE_TEXT:
return "TEXT";
case LogicalField::TYPE_DATETIME:
return "DATETIME";
case LogicalField::TYPE_BOOLEAN:
return "TINYINT(4)";
}
}
public function columnDefinition() {
$definition = $this->columnType();
if (!$this->getLogicalField()->getNullable()) $definition.= " NOT NULL ";
if ($this->getLogicalField()->isAutoIncrement()) $definition.= "AUTO_INCREMENT ";
return $definition;
}
public function add() {
- $stt = "ALTER TABLE `{$this->getTable()->getTableName()}`
+ $stt = "ALTER TABLE `{$this->getTable()->getLayout()->getTableName()}`
ADD COLUMN (`{$this->getName()}` {$this->columnDefinition()} )";
$this->getTable()->getDb()->execute($stt);
if ($this->getLogicalField()->isPrimary()) {
$pk = implode(', ', array_keys($this->getTable()->getPrimaryKey()));
- $stt = "ALTER TABLE {$this->getTable()->getTableName()}
+ $stt = "ALTER TABLE {$this->getTable()->getLayout()->getTableName()}
DROP PRIMARY KEY,
ADD PRIMARY KEY(" . $pk . ")";
$this->getTable()->getDb()->execute($stt);
}
}
public function __toString() {
return "`{$this->getTable()->getLayout()->getTableName()}`.`{$this->getName()}`";
}
}
\ No newline at end of file
diff --git a/storage/database/drivers/mysqlpdo/Layout.php b/storage/database/drivers/mysqlpdo/Layout.php
index 8fdc6ec..fb59a00 100644
--- a/storage/database/drivers/mysqlpdo/Layout.php
+++ b/storage/database/drivers/mysqlpdo/Layout.php
@@ -1,242 +1,240 @@
<?php namespace spitfire\storage\database\drivers\mysqlpdo;
use Reference;
use spitfire\cache\MemoryCache;
use spitfire\core\Collection;
use spitfire\exceptions\PrivateException;
use spitfire\model\Index as LogicalIndex;
use spitfire\storage\database\Field;
use spitfire\storage\database\IndexInterface;
use spitfire\storage\database\LayoutInterface;
use spitfire\storage\database\Table;
/*
* The MIT License
*
* Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
/**
* The MySQL PDO driver for the table layouts. This allows Spitfire to create,
* destroy and repair tables on a MySQL or MariaDB database.
*
* @todo This driver contains a lot of legacy code imported from the previous
* iteration and would require a lot of fixing and maintaining.
*/
class Layout implements LayoutInterface
{
/**
* The table that the system uses to connect layout and relation.
*
* @var Table
*/
private $table;
/**
* The prefixed name of the table. The prefix is defined by the environment
* and allows to have several environments on the same database.
*
* @var string
*/
private $tablename;
/**
* List of the physical fields this table handles. This array is just a
* shortcut to avoid looping through model-fields everytime a query is
* performed.
*
* @var Field[]
*/
private $fields;
/**
* An array of indexes that this table defines to manage it's queries and
* data.
*
* @var MemoryCache
*/
private $indexes;
/**
*
* @param Table $table
*/
public function __construct(Table$table) {
#Assume the table
$this->table = $table;
#Get the physical table name. This will use the prefix to allow multiple instances of the DB
$this->tablename = $this->table->getDb()->getSettings()->getPrefix() . $table->getSchema()->getTableName();
#Create the physical fields
$fields = $this->table->getSchema()->getFields();
$columns = Array();
foreach ($fields as $field) {
$physical = $field->getPhysical();
while ($phys = array_shift($physical)) { $columns[$phys->getName()] = $phys; }
}
$this->fields = $columns;
$this->indexes = new MemoryCache();
}
public function create() {
$table = $this;
$definitions = $table->columnDefinitions();
$indexes = $table->getIndexes();
- $indexes->each(function (Index$index) use (&$definitions) {
- $definitions[] = $index->definition();
- });
-
#Strip empty definitions from the list
- $clean = array_filter($definitions);
+ $clean = array_filter(array_merge(
+ $definitions,
+ $indexes->each(function($e) { return $e->definition(); })->toArray()
+ ));
$stt = sprintf('CREATE TABLE %s (%s) ENGINE=InnoDB CHARACTER SET=utf8',
$table,
implode(', ', $clean)
);
- //echo $stt;
return $this->table->getDb()->execute($stt);
}
public function destroy() {
$this->table->getDb()->execute('DROP TABLE ' . $this);
}
/**
* Fetch the fields of the table the database works with. If the programmer
* has defined a custom set of fields to work with, this function will
- * return the overriden fields.
+ * return the overridden fields.
*
* @return Field[] The fields this table handles.
*/
public function getFields() {
return $this->fields;
}
public function getField($name) : Field {
#If the data we get is already a DBField check it belongs to this table
if ($name instanceof Field) {
if ($name->getTable() === $this->table) { return $name; }
else { throw new PrivateException('Field ' . $name . ' does not belong to ' . $this); }
}
if (is_object($name)) {
throw new PrivateException('Expected a field name, got an object', 1708101329);
}
#Otherwise search for it in the fields list
if (isset($this->fields[(string)$name])) { return $this->fields[(string)$name]; }
#The field could not be found in the Database
throw new PrivateException('Field ' . $name . ' does not exist in ' . $this);
}
/**
*
* @return Collection <Index>
*/
public function getIndexes() {
return $this->indexes->get('indexes', function() {
/*
* First we get the defined indexes.
*/
$logical = $this->table->getSchema()->getIndexes();
$indexes = $logical->each(function (LogicalIndex$e) {
return new Index($e);
});
/*
* Then we get those implicitly defined by reference fields. These are
* defined by the driver, sicne they're required for it to work.
*/
$fields = array_filter($this->table->getSchema()->getFields(), function($e) { return $e instanceof Reference;});
foreach ($fields as $field) {
$indexes->push(new ForeignKey(new LogicalIndex([$field])));
}
return $indexes;
});
}
public function getTableName() : string {
return $this->tablename;
}
public function repair() {
$table = $this->table;
$stt = "DESCRIBE {$this}";
$fields = $table->getFields();
foreach ($this->table->getSchema()->getFields() as $f) {
if ($f instanceof Reference && $f->getTarget() !== $this->table->getSchema()) {
$f->getTarget()->getTable()->getLayout()->repair();
}
}
//Fetch the DB Fields and create on error.
try {
$query = $this->table->getDb()->execute($stt, Array(), false);
}
catch(\Exception $e) {
return $this->create();
}
//Loop through the exiting fields
while (false != ($f = $query->fetch())) {
try {
$field = $this->getField($f['Field']);
unset($fields[$field->getName()]);
}
catch(Exception $e) {/*Ignore*/}
}
foreach($fields as $field) $field->add();
}
/**
* Creates the column definitions for each column
*
* @return mixed
*/
protected function columnDefinitions() {
$fields = $this->getFields();
foreach ($fields as $name => $f) {
$fields[$name] = '`'. $name . '` ' . $f->columnDefinition();
}
return $fields;
}
/**
* Returns the name of a table as DB Object reference (with quotes).
*
* @return string The name of the table escaped and ready for use inside
* of a query.
*/
public function __toString() {
return "`{$this->tablename}`";
}
}
diff --git a/storage/database/drivers/mysqlpdo/ObjectFactory.php b/storage/database/drivers/mysqlpdo/ObjectFactory.php
index a8746b3..63dddee 100644
--- a/storage/database/drivers/mysqlpdo/ObjectFactory.php
+++ b/storage/database/drivers/mysqlpdo/ObjectFactory.php
@@ -1,181 +1,163 @@
<?php namespace spitfire\storage\database\drivers\mysqlpdo;
use BadMethodCallException;
use spitfire\exceptions\PrivateException;
use spitfire\model\Field as LogicalField;
use spitfire\storage\database\DB;
-use spitfire\storage\database\drivers\MysqlPDOCompositeRestriction;
-use spitfire\storage\database\drivers\mysqlPDOField;
-use spitfire\storage\database\drivers\MysqlPDOQuery;
-use spitfire\storage\database\drivers\MysqlPDOQueryField;
-use spitfire\storage\database\drivers\MysqlPDOQueryTable;
-use spitfire\storage\database\drivers\MysqlPDORestriction;
-use spitfire\storage\database\drivers\MysqlPDORestrictionGroup;
-use spitfire\storage\database\drivers\MysqlPDOTable;
+use spitfire\storage\database\drivers\mysqlpdo\Field as MysqlField;
+use spitfire\storage\database\drivers\mysqlpdo\Query;
+use spitfire\storage\database\drivers\mysqlpdo\Restriction;
+use spitfire\storage\database\drivers\mysqlpdo\RestrictionGroup;
use spitfire\storage\database\Field;
use spitfire\storage\database\LayoutInterface;
use spitfire\storage\database\ObjectFactoryInterface;
-use spitfire\storage\database\Query;
-use spitfire\storage\database\QueryField;
-use spitfire\storage\database\QueryTable;
-use spitfire\storage\database\RestrictionGroup;
+use spitfire\storage\database\QueryField as AbstractQueryField;
+use spitfire\storage\database\QueryTable as AbstractQueryTable;
+use spitfire\storage\database\Relation as RelationAbstract;
+use spitfire\storage\database\RestrictionGroup as AbstractRestrictionGroup;
use spitfire\storage\database\Schema;
use spitfire\storage\database\Table;
use TextField;
-use function db;
/*
* The MIT License
*
* Copyright 2016 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
/**
* The object factory class allows a Database to centralize a point where the
* database objects can retrieve certain items from. As opposed to having this
* algorithms in every class, as some classes would just be overriding one factory
* method they needed in a completely standard class.
*
* This allows Spitfire to define certain behaviors it expects from DB objects
* and then have the driver provide this to not disturb Spitfire's logic.
*
* @author César de la Cal Bretschneider <cesar@magic3w.com>
*/
class ObjectFactory implements ObjectFactoryInterface
{
/**
* Creates a new on the fly model. This means that the model is created during
* runtime, and by reverse engineering the tables that the database already
* has.
*
* Please note, that this model would not perfectly replicate a model you could
* build with a proper definition yourself.
*
* @todo At the time of writing this, the method does not use adequate types.
* @param string $modelname
* @return Table
*/
- public function getOTFSchema($modelname) {
+ public function getOTFSchema(DB$db, $modelname) {
#Create a Schema we can feed the data into.
$schema = new Schema($modelname);
#Make the SQL required to read in the data
$sql = sprintf('DESCRIBE `%s%s`', $schema->getTableName(), $modelname);
/** @var $fields Query */
- $fields = db()->execute($sql, false);
+ $fields = $db->execute($sql, false);
while ($row = $fields->fetch()) {
$schema->{$row['Field']} = new TextField();
}
- return $schema->getTable();
- }
-
- /**
- * Creates a new driver specific table. The table is in charge of providing
- * the necessary tools for records to be updated, inserted, deleted, etc.
- *
- * @param DB $db
- * @param string $tablename
- * @deprecated since version 0.1-dev 20170807
- * @return MysqlPDOTable
- */
- public function getTableInstance(DB $db, $tablename) {
- return new MysqlPDOTable($db, $tablename);
+ return new Table($db, $schema);
}
/**
* Creates a new MySQL PDO Field object. This receives the fields 'prototype',
* name and reference (in case it references an externa field).
*
* This represents an actual field in the DBMS as opposed to the ones in the
* model. That's why here we talk of "physical" fields
*
* @todo This should be moved over to a DBMS specific object factory.
* @param Field $field
* @param string $name
* @param Field $references
- * @return mysqlPDOField
+ * @return MysqlField
*/
public function getFieldInstance(LogicalField$field, $name, Field$references = null) {
- return new mysqlPDOField($field, $name, $references);
+ return new MysqlField($field, $name, $references);
}
- public function restrictionInstance($query, QueryField$field, $value, $operator = null) {
- return new MysqlPDORestriction($query, $field, $value, $operator);
+ public function restrictionInstance($query, AbstractQueryField$field, $value, $operator = null) {
+ return new Restriction($query, $field, $value, $operator);
}
/**
* Makes a new query on a certain table.
*
* @param Table $table
*
- * @return MysqlPDOQuery
+ * @return Query
* @throws PrivateException
*/
public function queryInstance($table) {
- if ($table instanceof Relation){ $table = $table->getTable(); }
+ if ($table instanceof RelationAbstract){ $table = $table->getTable(); }
if (!$table instanceof Table) { throw new PrivateException('Need a table object'); }
- return new MysqlPDOQuery($table);
+ return new Query($table);
}
public function makeRelation(Table $table) {
return new Relation($table);
}
public function __call($name, $args) {
throw new BadMethodCallException("Called ObjectFactory::$name. Method does not exist");
}
public function makeLayout(Table $table): LayoutInterface {
return new Layout($table);
}
- public function restrictionGroupInstance(RestrictionGroup $parent = null, $type = RestrictionGroup::TYPE_OR): RestrictionGroup {
- $g = new MysqlPDORestrictionGroup($parent);
+ public function restrictionGroupInstance(AbstractRestrictionGroup$parent = null, $type = AbstractRestrictionGroup::TYPE_OR): AbstractRestrictionGroup {
+ $g = new RestrictionGroup($parent);
$g->setType($type);
return $g;
}
- public function queryFieldInstance($query, $field) {
- if ($field instanceof QueryField) {return $field; }
- return new MysqlPDOQueryField($query, $field);
+ public function queryFieldInstance(AbstractQueryTable$queryTable, $field) {
+ if ($field instanceof AbstractQueryField) {return $field; }
+ return new QueryField($queryTable, $field);
}
- public function queryTableInstance($query, $table) {
+ public function queryTableInstance($table) {
if ($table instanceof Relation) { $table = $table->getTable(); }
- if ($table instanceof QueryTable) { $table = $table->getTable(); }
+ if ($table instanceof AbstractQueryTable) { $table = $table->getTable(); }
if (!$table instanceof Table) { throw new PrivateException('Did not receive a table as parameter'); }
- return new MysqlPDOQueryTable($query, $table);
+ return new QueryTable($table);
}
- public function restrictionCompositeInstance(Query $query, LogicalField$field = null, $value, $operator) {
- return new MysqlPDOCompositeRestriction($query, $field, $value, $operator);
+ public function restrictionCompositeInstance(AbstractRestrictionGroup$parent, LogicalField$field = null, $value = null, $operator = null) {
+ return new CompositeRestriction($parent, $field, $value, $operator);
}
}
diff --git a/db/drivers/mysqlPDOQuery.php b/storage/database/drivers/mysqlpdo/Query.php
similarity index 80%
rename from db/drivers/mysqlPDOQuery.php
rename to storage/database/drivers/mysqlpdo/Query.php
index 585cabc..c3d2e83 100644
--- a/db/drivers/mysqlPDOQuery.php
+++ b/storage/database/drivers/mysqlpdo/Query.php
@@ -1,196 +1,190 @@
-<?php namespace spitfire\storage\database\drivers;
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
use spitfire\exceptions\PrivateException;
use spitfire\model\Field;
-use spitfire\storage\database\Relation;
-use spitfire\storage\database\Table;
-use spitfire\storage\database\Query;
+use spitfire\storage\database\drivers\mysqlpdo\CompositeRestriction;
+use spitfire\storage\database\drivers\sql\SQLQuery;
use spitfire\storage\database\QueryField;
use spitfire\storage\database\QueryTable;
+use spitfire\storage\database\Relation;
+use spitfire\storage\database\Table;
-class MysqlPDOQuery extends Query
+class Query extends SQLQuery
{
- public function execute($fields = null) {
+ public function execute($fields = null, $offset = null, $max = null) {
$this->setAliased(false);
- #Declare vars
- $rpp = $this->getResultsPerPage();
- $offset = ($this->getPage() - 1) * $rpp;
+
+ #Import tables for restrictions from remote queries
+ $plan = $this->makeExecutionPlan();
+ $last = array_shift($plan);
+ $joins = Array();
+
+ foreach ($plan as $q) {
+ $joins[] = sprintf('LEFT JOIN %s ON (%s)', $q->getQueryTable()->definition(), implode(' AND ', $q->getRestrictions()));
+ }
$selectstt = 'SELECT';
$fromstt = 'FROM';
- $tablename = $this->getTable()->getLayout();
+ $tablename = $last->getQueryTable()->definition();
$wherestt = 'WHERE';
/** @link http://www.spitfirephp.com/wiki/index.php/Database/subqueries Information about the filter*/
- $restrictions = $this->getRestrictions();
+ $restrictions = $last->getRestrictions();
$orderstt = 'ORDER BY';
$order = $this->getOrder();
$groupbystt = 'GROUP BY';
$groupby = $this->groupby;
$limitstt = 'LIMIT';
- $limit = $offset . ', ' . $rpp;
-
-
- #Import tables for restrictions from remote queries
- $subqueries = $this->getPhysicalSubqueries();
- $joins = Array();
-
- foreach ($subqueries as $q) {
- $joins[] = sprintf('LEFT JOIN %s ON (%s)', $q->getQueryTable()->definition(), implode(' AND ', $q->getRestrictions()));
- }
+ $limit = $offset . ', ' . $max;
if ($fields === null) {
- $fields = $this->table->getTable()->getLayout()->getFields();
+ $fields = $last->getQueryTable()->getFields();
/*
* If there is subqueries we default to grouping data in a way that will
* give us unique records and the amount of times they appear instead
* of repeating them.
*
* Example: The users followed by users I follow. Even though I cannot
* follow a user twice, two different users I follow can again follow
* the same user. A regular join would produce a dataset where the user
* is included twice, by adding the grouping mechanism we're excluding
* that behavior.
*/
- if (!empty($subqueries)) {
+ if (!empty($plan)) {
$groupby = $fields;
$fields[] = 'COUNT(*) AS __META__count';
}
}
$join = implode(' ', $joins);
#Restrictions
if (empty($restrictions)) {
$restrictions = '1';
}
else {
$restrictions = implode(' AND ', $restrictions);
}
- if ($rpp < 0) {
+ if ($max === null) {
$limitstt = '';
$limit = '';
}
if (empty($order)) {
$orderstt = '';
$order = '';
}
else {
$order = "{$order['field']} {$order['mode']}";
}
if (empty($groupby)) {
$groupbystt = '';
$groupby = '';
}
else {
$groupby = implode(', ', $groupby);
}
$stt = array_filter(Array( $selectstt, implode(', ', $fields), $fromstt, $tablename, $join,
$wherestt, $restrictions, $groupbystt, $groupby, $orderstt, $order, $limitstt, $limit));
- return new mysqlPDOResultSet($this->getTable(), $this->getTable()->getDb()->execute(implode(' ', $stt)));
+ return new ResultSet($this->getTable(), $this->getTable()->getDb()->execute(implode(' ', $stt)));
}
- /**
- *
- * @deprecated since version 0.1-dev 20171110
- * @param type $parent
- * @return \spitfire\storage\database\drivers\MysqlPDORestrictionGroup
- */
- public function restrictionGroupInstance($parent = null) {
- return new MysqlPDORestrictionGroup($parent? : $this);
- }
-
/**
*
* @deprecated since version 0.1-dev 20171110
* @param QueryField $field
* @param type $value
* @param type $operator
- * @return \spitfire\storage\database\drivers\MysqlPDORestriction
+ * @return MysqlPDORestriction
*/
public function restrictionInstance(QueryField$field, $value, $operator) {
return new MysqlPDORestriction($this, $field, $value, $operator);
}
/**
*
* @deprecated since version 0.1-dev 20171110
* @param QueryField $field
* @return \spitfire\storage\database\drivers\MysqlPDOQueryField|QueryField
*/
public function queryFieldInstance($field) {
+ trigger_error('Deprecated: mysqlPDOQuery::queryFieldInstance is deprecated', E_USER_DEPRECATED);
+
if ($field instanceof QueryField) {return $field; }
return new MysqlPDOQueryField($this, $field);
}
/**
*
* @deprecated since version 0.1-dev 20171110
* @param type $table
* @return \spitfire\storage\database\drivers\MysqlPDOQueryTable
* @throws PrivateException
*/
public function queryTableInstance($table) {
if ($table instanceof Relation) { $table = $table->getTable(); }
if ($table instanceof QueryTable) { $table = $table->getTable(); }
if (!$table instanceof Table) { throw new PrivateException('Did not receive a table as parameter'); }
return new MysqlPDOQueryTable($this, $table);
}
/**
*
* @deprecated since version 0.1-dev 20171110
*/
public function compositeRestrictionInstance(Field $field = null, $value, $operator) {
- return new MysqlPDOCompositeRestriction($this, $field, $value, $operator);
+ return new CompositeRestriction($this, $field, $value, $operator);
}
-
+
+ /**
+ *
+ * @fixme
+ */
public function delete() {
$this->setAliased(false);
#Declare vars
$selectstt = 'DELETE';
$fromstt = 'FROM';
$tablename = $this->getTable();
$wherestt = 'WHERE';
/** @link http://www.spitfirephp.com/wiki/index.php/Database/subqueries Information about the filter*/
$restrictions = array_filter($this->getRestrictions(), Array('spitfire\storage\database\Query', 'restrictionFilter'));
#Import tables for restrictions from remote queries
$subqueries = $this->getPhysicalSubqueries();
$joins = Array();
foreach ($subqueries as $q) {
$joins[] = sprintf('LEFT JOIN %s ON (%s)', $q->getQueryTable()->definition(), implode(' AND ', $q->getRestrictions()));
}
$join = implode(' ', $joins);
#Restrictions
if (empty($restrictions)) {
$restrictions = '1';
}
else {
$restrictions = implode(' AND ', $restrictions);
}
$stt = array_filter(Array( $selectstt, $fromstt, $tablename, $join,
$wherestt, $restrictions));
$this->getTable()->getDb()->execute(implode(' ', $stt));
}
}
\ No newline at end of file
diff --git a/storage/database/drivers/mysqlpdo/QueryField.php b/storage/database/drivers/mysqlpdo/QueryField.php
new file mode 100644
index 0000000..369b1fb
--- /dev/null
+++ b/storage/database/drivers/mysqlpdo/QueryField.php
@@ -0,0 +1,10 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\QueryField as ParentClass;
+
+class QueryField extends ParentClass
+{
+ public function __toString() {
+ return "{$this->getQueryTable()}.`{$this->getField()->getName()}`";
+ }
+}
\ No newline at end of file
diff --git a/storage/database/drivers/mysqlpdo/QueryTable.php b/storage/database/drivers/mysqlpdo/QueryTable.php
new file mode 100644
index 0000000..0eb320f
--- /dev/null
+++ b/storage/database/drivers/mysqlpdo/QueryTable.php
@@ -0,0 +1,24 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\QueryTable as ParentClass;
+
+class QueryTable extends ParentClass
+{
+ /**
+ *
+ * @todo Move the aliasing thing over to the queryTable completely.
+ * @return string
+ */
+ public function __toString() {
+ return "`{$this->getAlias()}`";
+ }
+
+ public function definition() {
+ if ($this->isAliased()) {
+ return "{$this->getTable()->getLayout()} AS `{$this->getAlias()}`";
+ }
+ else {
+ return "{$this->getTable()->getLayout()}";
+ }
+ }
+}
diff --git a/storage/database/drivers/mysqlpdo/Restriction.php b/storage/database/drivers/mysqlpdo/Restriction.php
new file mode 100644
index 0000000..0a32149
--- /dev/null
+++ b/storage/database/drivers/mysqlpdo/Restriction.php
@@ -0,0 +1,29 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\Restriction as AbstractRestriction;
+
+class Restriction extends AbstractRestriction
+{
+ public function __toString() {
+ $value = $this->getValue();
+
+ if (is_array($value)) {
+ foreach ($value as &$v) {
+ $v = $this->getTable()->getDb()->quote($v);
+ }
+
+ $quoted = implode(',', $value);
+ return "{$this->getField()} {$this->getOperator()} ({$quoted})";
+ }
+
+ elseif ($value instanceof QueryField) {
+ return "{$this->getField()} {$this->getOperator()} {$this->getValue()}";
+ }
+
+ else {
+ $quoted = $this->getTable()->getDb()->quote($this->getValue());
+ return "{$this->getField()} {$this->getOperator()} {$quoted}";
+ }
+ }
+
+}
diff --git a/storage/database/drivers/mysqlpdo/RestrictionGroup.php b/storage/database/drivers/mysqlpdo/RestrictionGroup.php
new file mode 100644
index 0000000..71c96e8
--- /dev/null
+++ b/storage/database/drivers/mysqlpdo/RestrictionGroup.php
@@ -0,0 +1,11 @@
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
+
+use spitfire\storage\database\drivers\sql\SQLRestrictionGroup;
+
+class RestrictionGroup extends SQLRestrictionGroup
+{
+ public function __toString() {
+ if ($this->isEmpty()) { return ''; }
+ return sprintf('(%s)', implode(' ' . $this->getType() .' ', $this->getRestrictions()));
+ }
+}
\ No newline at end of file
diff --git a/db/drivers/mysqlPDORes.php b/storage/database/drivers/mysqlpdo/ResultSet.php
similarity index 80%
rename from db/drivers/mysqlPDORes.php
rename to storage/database/drivers/mysqlpdo/ResultSet.php
index 4ed45c5..e16b60b 100644
--- a/db/drivers/mysqlPDORes.php
+++ b/storage/database/drivers/mysqlpdo/ResultSet.php
@@ -1,67 +1,71 @@
-<?php namespace spitfire\storage\database\drivers;
+<?php namespace spitfire\storage\database\drivers\mysqlpdo;
use PDO;
+use PDOStatement;
+use spitfire\core\Collection;
+use spitfire\storage\database\ResultSetInterface;
+use spitfire\storage\database\Table;
/**
* This class works as a traditional resultset. It acts as an adapter between the
* driver's raw data retrieving and the logical record classes.
*
* @author César de la Cal <cesar@magic3w.com>
*/
-class mysqlPDOResultSet implements \spitfire\storage\database\ResultSetInterface
+class ResultSet implements ResultSetInterface
{
/**
* Contains the raw pointer that PDO has created when executing the query.
* This allows spitfire to retrieve all the data needed to create a complete
* database record.
*
* @var PDOStatement
*/
private $result;
/**
* This is a reference to the table this resultset belongs to. This allows
* Spitfire to retrieve data about the model and the fields the datatype has.
*
- * @var \spitfire\storage\database\Table
+ * @var Table
*/
private $table;
- public function __construct(\spitfire\storage\database\Table$table, $stt) {
+ public function __construct(Table$table, $stt) {
$this->result = $stt;
$this->table = $table;
}
public function fetch() {
$data = $this->result->fetch(PDO::FETCH_ASSOC);
#If the data does not contain anything we return a null object
if (!$data) { return null; }
$_record = array_map( Array($this->table->getDB()->getEncoder(), 'decode'), $data);
$record = $this->table->newRecord($_record);
return $record;
}
public function fetchAll() {
$data = $this->result->fetchAll(PDO::FETCH_ASSOC);
foreach ($data as &$record) {
- $record = $this->table->getDb()->table($this->table->getModel()->getName())->newRecord(
+ $record = $this->table->newRecord(
array_map( Array($this->table->getDB()->getEncoder(), 'decode'), $record)
);
}
- return $data;
+ return new Collection($data);
}
/**
* Returns the data the way any associative adapter would return it. This allows
* your app to withdraw raw data without it being treated by the framework.
*
* @return mixed
*/
public function fetchArray() {
return $this->result->fetch(PDO::FETCH_ASSOC);
}
}
diff --git a/storage/database/drivers/sql/SQLQuery.php b/storage/database/drivers/sql/SQLQuery.php
new file mode 100644
index 0000000..71008b1
--- /dev/null
+++ b/storage/database/drivers/sql/SQLQuery.php
@@ -0,0 +1,178 @@
+<?php namespace spitfire\storage\database\drivers\sql;
+
+use spitfire\core\Collection;
+use spitfire\exceptions\PrivateException;
+use spitfire\storage\database\drivers\mysqlpdo\CompositeRestriction;
+use spitfire\storage\database\Query;
+use spitfire\storage\database\RestrictionGroup;
+
+abstract class SQLQuery extends Query
+{
+
+ /**
+ * The redirection object is required only when assembling queries. Sometimes,
+ * a query has unmet dependencies that it cannot satisfy. In this case, it's
+ * gonna copy itself and move all of it's restrictions to the new query.
+ *
+ * This means that when serializing the query, the composite restriction should
+ * not print <code>old.primary IS NOT NULL</code> but <code>new.primary IS NOT NULL</code>.
+ *
+ * But! When the parent injects the restrictions to connect the queries with
+ * the parent, the old query must answer the call and assimilate them.
+ *
+ * To achieve this behavior, I found it reasonable that the query introduces
+ * a redirection property. When a composite restriction finds this, it will
+ * automatically use the target of the redirection.
+ *
+ * NOTE: Composite queries do not follow multiple redirections.
+ *
+ * @var SQLQuery|null
+ */
+ private $redirection = null;
+
+ /**
+ * It retrieves all the subqueries that are needed to be executed on a relational
+ * DB before the main query.
+ *
+ * We could have used a single method with a flag, but this way seems cleaner
+ * and more hassle free than otherwise.
+ *
+ * @return Query[]
+ */
+ public function makeExecutionPlan() {
+
+ /*
+ * Inject the current query into the array. The data for this query needs
+ * to be retrieved last.
+ */
+ $copy = clone $this;
+ $_ret = $copy->physicalize(true);
+
+ $copy->denormalize(true);
+
+ foreach ($_ret as $q) {
+ $q->normalize();
+ }
+
+ return $_ret;
+ }
+
+ public function physicalize($top = false) {
+
+ $copy = $this;
+ $_ret = [$this];
+
+ $composite = $copy->getCompositeRestrictions();
+
+ foreach ($composite as $r) {
+
+ $q = $r->getValue();
+ $p = $q->physicalize();
+ $c = $r->makeConnector();
+ $_ret = array_merge($_ret, $c, $p);
+ }
+
+ if (!$top && $copy->isMixed() && !$composite->isEmpty()) {
+
+ $clone = clone $copy;
+ $of = $copy->getTable()->getDb()->getObjectFactory();
+
+ $clone->cloneQueryTable();
+ $group = $of->restrictionGroupInstance($clone);
+
+ foreach ($copy->getTable()->getPrimaryKey()->getFields() as $field) {
+ $group->where(
+ $of->queryFieldInstance($copy->getQueryTable(), $field),
+ $of->queryFieldInstance($clone->getQueryTable(), $field)
+ );
+ }
+
+ $copy->reset();
+ $copy->setRedirection($clone);
+ $clone->push($group);
+ $_ret[] = $clone;
+ }
+
+ return $_ret;
+ }
+
+ /**
+ *
+ * @todo This method could be much simpler with an import function in rGroups which take all the children
+ * @param type $root
+ * @return Collection
+ * @throws PrivateException
+ */
+ public function denormalize($root = false) {
+ if (!$root && $this->isMixed()) {
+ throw new PrivateException('Impossible condition satisfied. This is a bug.', 1804292159);
+ }
+
+ $_ret = new Collection();
+
+ $composite = $this->getCompositeRestrictions();
+ $of = $this->getQuery()->getTable()->getDb()->getObjectFactory();
+
+ /*
+ * Loop over the composite restrictions inside this query. This allows the
+ * system to extract the conditions that need to assimilated at the end of
+ * a query.
+ *
+ * Please note that this only can be achieved because the driver has
+ * previously physicalized the queries and prepared them in a manner that
+ * allows for their denormalization (deferred the mixed ones).
+ */
+ foreach ($composite as /*@var $r CompositeRestriction*/$r) {
+
+ $sg = $of->restrictionGroupInstance($r->getParent(), RestrictionGroup::TYPE_AND);
+ $d = $r->getValue()->denormalize();
+
+ /*
+ * Once the subquery has been denormalized, we enter to retrieve the
+ * previously denormalized blocks. To do so, we loop over the collection
+ * containing the references, remove the group from their actual location,
+ * put it in their new home and append that to the $sg variable.
+ */
+ foreach ($d as $v) {
+ $group = $of->restrictionGroupInstance($this, RestrictionGroup::TYPE_AND);
+ $group->push($v);
+ $v->getParent()->remove($v);
+ $v->setParent($group);
+ $sg->push($group->setParent($sg));
+ }
+
+ $sg->push($r);
+
+ if ($r->getOperator() !== '=') {
+ $sg->negate();
+ }
+
+ $r->getParent()->remove($r)->push($sg);
+ $r->setParent($sg);
+ $_ret->push($sg);
+ }
+
+ return $_ret;
+ }
+
+ /**
+ *
+ * @return SQLQuery|null
+ */
+ public function getRedirection() {
+ return $this->redirection;
+ }
+
+ /**
+ * This is a driver specific method. If you're not exactly sure what a query
+ * redirection is, please avoid using this method.
+ *
+ * @param SQLQuery|null $redirection
+ * @return $this
+ */
+ public function setRedirection($redirection = null) {
+ $this->redirection = $redirection;
+ return $this;
+ }
+
+}
diff --git a/storage/database/drivers/sql/SQLRestrictionGroup.php b/storage/database/drivers/sql/SQLRestrictionGroup.php
new file mode 100644
index 0000000..3a2c094
--- /dev/null
+++ b/storage/database/drivers/sql/SQLRestrictionGroup.php
@@ -0,0 +1,26 @@
+<?php namespace spitfire\storage\database\drivers\sql;
+
+use spitfire\storage\database\CompositeRestriction;
+use spitfire\storage\database\RestrictionGroup;
+
+
+abstract class SQLRestrictionGroup extends RestrictionGroup
+{
+
+
+ public function physicalize() {
+ $_ret = [];
+
+ foreach ($this as $restriction) {
+ if ($restriction instanceof SQLRestrictionGroup) {
+ $_ret = array_merge($_ret, $restriction->physicalize());
+ }
+
+ elseif ($restriction instanceof CompositeRestriction) {
+ $_ret = array_merge($_ret, $restriction->makeConnector());
+ }
+ }
+
+ return $_ret;
+ }
+}
diff --git a/storage/database/restrictionmaker/CompositeWorker.php b/storage/database/pagination/MockPaginator.php
similarity index 51%
copy from storage/database/restrictionmaker/CompositeWorker.php
copy to storage/database/pagination/MockPaginator.php
index 44f6fae..386cc9e 100644
--- a/storage/database/restrictionmaker/CompositeWorker.php
+++ b/storage/database/pagination/MockPaginator.php
@@ -1,58 +1,84 @@
-<?php namespace spitfire\storage\database\restrictionmaker;
-
-use spitfire\storage\database\RestrictionGroup;
+<?php namespace spitfire\storage\database\pagination;
/*
* The MIT License
*
- * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ * Copyright 2018 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
-class CompositeWorker implements WorkerInterface
+class MockPaginator implements PaginationInterface
{
- /**
- *
- * @param RestrictionGroup $parent
- * @param string $field
- * @param string $operator
- * @param mixed $value
- */
- public function make(RestrictionGroup$parent, $field, $operator, $value) {
-
- /*
- * Find the appropriate field for the maker to assemble a restriction. If
- * this returns an empty value, then this maker can't assemble a restriction
- */
- $logical = $parent->getQuery()->getTable()->getSchema()->getField($field);
-
- /*
- * If the field is null or the value is null, then this maker is not a match
- * for the behavior needed.
- */
- if ($logical === null || $value === null) {
- return false;
- }
-
- return $parent->getQuery()->compositeRestrictionInstance($logical, $value, $operator);
+ private $current;
+
+ public function __construct($current = 1) {
+ $this->current = $current;
+ }
+
+ public function after() {
+ return '::after' . PHP_EOL;
+ }
+
+ public function before() {
+ return '::before' . PHP_EOL;
+ }
+
+ public function current() {
+ return $this->current;
+ }
+
+ public function emptyResultMessage() {
+ return '::empty' . PHP_EOL;
+ }
+
+ public function first() {
+ return '::first' . PHP_EOL;
+ }
+
+ public function last($number) {
+ return '::last' . PHP_EOL;
+ }
+
+ public function next() {
+ return '::next' . PHP_EOL;
+ }
+
+ public function page($number) {
+ return '::page #' . $number . PHP_EOL;
+ }
+
+ public function previous() {
+ return '::previous' . PHP_EOL;
+ }
+
+ public function gap() {
+ return '::gap' . PHP_EOL;
+ }
+
+ public function jumpTo($total) {
+ return '::jumpTo ' . $total . PHP_EOL;
+ }
+
+ public function pageOf($total) {
+ return '::pageOf ' . $total . PHP_EOL;
}
}
diff --git a/storage/database/restrictionmaker/CompositeWorker.php b/storage/database/pagination/PaginationInterface.php
similarity index 54%
copy from storage/database/restrictionmaker/CompositeWorker.php
copy to storage/database/pagination/PaginationInterface.php
index 44f6fae..1484b0e 100644
--- a/storage/database/restrictionmaker/CompositeWorker.php
+++ b/storage/database/pagination/PaginationInterface.php
@@ -1,58 +1,61 @@
-<?php namespace spitfire\storage\database\restrictionmaker;
-
-use spitfire\storage\database\RestrictionGroup;
+<?php namespace spitfire\storage\database\pagination;
/*
* The MIT License
*
- * Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
+ * Copyright 2018 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
-class CompositeWorker implements WorkerInterface
+/**
+ * This interface allows applications to quickly implement custom user interfaces
+ * while maintaining the behavior of the standard pagination system.
+ *
+ * @todo Find a better name for this
+ * @todo Document the different methods
+ * @author César de la Cal Bretschneider <cesar@magic3w.com>
+ */
+interface PaginationInterface
{
-
/**
+ * Returns the current page
*
- * @param RestrictionGroup $parent
- * @param string $field
- * @param string $operator
- * @param mixed $value
+ * @return int
*/
- public function make(RestrictionGroup$parent, $field, $operator, $value) {
-
- /*
- * Find the appropriate field for the maker to assemble a restriction. If
- * this returns an empty value, then this maker can't assemble a restriction
- */
- $logical = $parent->getQuery()->getTable()->getSchema()->getField($field);
-
- /*
- * If the field is null or the value is null, then this maker is not a match
- * for the behavior needed.
- */
- if ($logical === null || $value === null) {
- return false;
- }
-
- return $parent->getQuery()->compositeRestrictionInstance($logical, $value, $operator);
- }
-
+ public function current();
+
+ public function emptyResultMessage();
+
+ public function page($number);
+
+ public function previous();
+ public function next();
+
+ public function first();
+ public function last($number);
+
+ public function before();
+ public function after();
+
+ public function gap();
+ public function jumpTo($total);
+ public function pageOf($total);
+
}
diff --git a/storage/database/pagination/Paginator.php b/storage/database/pagination/Paginator.php
new file mode 100644
index 0000000..9961f8d
--- /dev/null
+++ b/storage/database/pagination/Paginator.php
@@ -0,0 +1,231 @@
+<?php namespace spitfire\storage\database\pagination;
+
+use spitfire\core\http\URL;
+use spitfire\storage\database\Query;
+use function collect;
+use function within;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2018 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+/**
+ * This class is the base for the database query pagination inside of Spitfire.
+ * It provides the necessary tools to generate a list of pages inside your
+ * applications so queries aren't able to collapse your system / clients.
+ *
+ * By default this class includes a getEmpty method that returns a message when
+ * no results are available. Although it is not a good practice to allow classes
+ * perform actions that aren't strictly related to their task. But the improvement
+ * on readability gained in Views is worth the change.
+ *
+ * @link http://www.spitfirephp.com/wiki/index.php/Database/pagination Related data and tutorials
+ *
+ * @todo Somehow this class should cache the counts, so the database doesn't need to read the data every time.
+ * @todo This class should help paginating without the use of LIMIT
+ * @todo Update the pertinent documentation on the wiki
+ */
+class Paginator
+{
+ /**
+ * The query to paginate. The offset and limit properties of the query may be
+ * overwritten by the paginator.
+ *
+ * @var Query
+ */
+ private $query;
+
+ /**
+ * This property allows the paginator to communicate with the user to retrieve
+ * the current page and present a set of options to jump to another page.
+ *
+ * @var PaginationInterface
+ */
+ private $io;
+
+ /**
+ * The number of records to be fit in a single page. Once the number of records
+ * is exceeded, another page is added.
+ *
+ * @var int
+ */
+ private $pageSize;
+
+ /**
+ * The distance suggests spitfire the amount of pages a user should be able
+ * to jump from the current page.
+ *
+ * When there's enough pages, the user should be able to jump at least (distance)
+ * and at most (distance * 2).
+ *
+ * @var int
+ */
+ private $distance = 3;
+
+ /**
+ * Paginates a query. Paginating is the process in which a query that delivers
+ * a resultset too large to be properly transferred over a network or fit into
+ * a window is broken into individual pages.
+ *
+ * @param Query $query
+ * @param int $pageSize
+ */
+ public function __construct(Query$query, PaginationInterface$io = null, $pageSize = 20) {
+ $this->query = $query;
+ $this->io = $io? : new SimplePaginator(URL::current(), 'page');
+ $this->pageSize = $pageSize;
+ }
+
+ /**
+ * Estimates the number of pages needed to fit all the results for this query.
+ *
+ * @return int
+ */
+ public function getPageCount() {
+ $results = $this->query->count();
+ return ceil($results/$this->pageSize);
+ }
+
+ /**
+ * This function calculates the pages to be displayed in the pagination. It
+ * calculates the ideal amount of pages to be displayed (based on the max you want)
+ * and generates an array with the numbers for those pages.
+ *
+ * If you use the default distance of 3 you will always receive up to 9 pages.
+ * Those include the first, the last, the current and the three higher and lower
+ * pages. For page 7/20 you will receive (1,4,5,6,7,8,9,10,20).
+ *
+ * In case the pagination doesn't find enough elements whether on the right or
+ * left it will try to extend this with results on the other one. This avoids
+ * broken looking paginations when reaching the final results of a set.
+ *
+ * @return \spitfire\core\Collection <int>
+ */
+ public function pages() {
+ $count = $this->getPageCount();
+
+ /*
+ * Determine the range within the user can jump. The system will try to find
+ * a value that allows it to render as many pages as possible with the given
+ * distance.
+ *
+ * Optimizing for drawing the most possible pages provides the user with a
+ * consistent interface and with the biggest amount of options.
+ */
+ $start = within(2, $this->io->current() - $this->distance, $count - $this->distance * 2 - 2);
+
+ /*
+ * Generate the page numbers from start to start + 2(distance). This ensures
+ * that we always receive either all the pages or distance * 2 + 3 pages.
+ */
+ $pages = collect(range(
+ $start,
+ $start + $this->distance * 2 + 1
+ ));
+
+ /*
+ * We finally add the first and last page to the result set. Please note that
+ * we do not check if the pages have already been added at this stage since
+ * we simply remove the duplicates once all of them have been added.
+ */
+ $pages->push(1);
+ $pages->push($count);
+
+ return $pages->unique()->sort()->filter(function($e) { return $e > 0 && $e <= $this->getPageCount(); });
+ }
+
+ /**
+ * Gets the records that populate the current page.
+ */
+ public function records() {
+ return $this->query->range($this->io->current() * $this->pageSize, $this->pageSize);
+ }
+
+ /**
+ * This method makes it quick and comfortable to present the pagination, just
+ * by running <code>echo $pagination;</code> wherever your application whishes
+ * to present the pages.
+ *
+ * Please note, the pagination will also render HTML whenever your query returns
+ * an empty result. Therefore, you don't need to check whether the result set
+ * is empty or not.
+ *
+ * @return string
+ */
+ public function __toString() {
+ $pages = $this->pages();
+ $previous = 0;
+
+ if (empty($pages)) {
+ return $this->io->emptyResultMessage();
+ }
+
+ /*
+ * It's common for HTML forms to have some kind of boilerplate to prepare
+ * for the output. Since HTML is the most common form of pagination, it's
+ * easy to make adjustments to ensure it's easy to generate HTML.
+ */
+ $_ret = $this->io->before();
+
+ /*
+ * Render a "Jump to first page button". These are usually the first element
+ * in a pagination. While it functions exactly like the page 1 link, it allows
+ * for the application to customize the experience.
+ */
+ $_ret.= $this->io->first();
+
+ /*
+ * Print a "Jump to previous page". Again, these can have special layouts
+ * or styling. The previous page is always included in the pages.
+ */
+ $_ret.= $this->io->previous();
+
+ /*
+ * Render the pages. All the pages sent to the io component are valid, but
+ * can be pages the user should be unable to jump to. For example, the
+ * current page is renderer, but many developers prefer to have a faux
+ * link in place, if this is the case, your renderer needs to gray it out.
+ */
+ foreach ($pages as $page) {
+ if ($page > $previous + 1) { $_ret.= $this->io->gap(); }
+ $_ret.= $this->io->page($page);
+ $previous = $page;
+ }
+
+ /*
+ * Just like the before and first jump buttons, the system terminates by
+ * adding the next and last pages.
+ */
+ $_ret.= $this->io->next();
+ $_ret.= $this->io->last($this->getPageCount());
+
+ /*
+ * In case the boilerplate requires it, your renderer can use this to present
+ * any boilerplate it requires to end the pagination.
+ */
+ $_ret.= $this->io->after();
+
+ return $_ret;
+ }
+
+}
diff --git a/storage/database/pagination/SimplePaginator.php b/storage/database/pagination/SimplePaginator.php
new file mode 100644
index 0000000..ad89b52
--- /dev/null
+++ b/storage/database/pagination/SimplePaginator.php
@@ -0,0 +1,107 @@
+<?php namespace spitfire\storage\database\pagination;
+
+/*
+ * The MIT License
+ *
+ * Copyright 2018 César de la Cal Bretschneider <cesar@magic3w.com>.
+ *
+ * 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.
+ */
+
+class SimplePaginator implements PaginationInterface
+{
+
+ /**
+ *
+ * @var \spitfire\core\http\URL
+ */
+ private $url;
+ private $parameter;
+
+ public function __construct(\spitfire\core\http\URL $url, $parameter) {
+ $this->url = $url;
+ $this->parameter = $parameter;
+ }
+
+ public function after() {
+ return '</ul>';
+ }
+
+ public function before() {
+ return '<ul class="pagination">';
+ }
+
+ public function current() {
+ return isset($_GET[$this->parameter])? $_GET[$this->parameter] : 1;
+ }
+
+ public function emptyResultMessage() {
+ return '<!--Automatically generated by Pagination::getEmpty()-->'
+ . '<div style="text-align:center"><em>No results to display&hellip;</em></div>'
+ . '<!---Automatically generated by Pagination::getEmpty()-->';
+ }
+
+ public function first() {
+ $url = clone $this->url;
+ $url->setParam($this->parameter, 1);
+
+ return sprintf('<li><a href="%s">%s</a></li>', $url, 1);
+ }
+
+ public function last($number) {
+ $url = clone $this->url;
+ $url->setParam($this->parameter, $number);
+
+ return sprintf('<li><a href="%s">%s</a></li>', $url, $number);
+ }
+
+ public function next() {
+ $url = clone $this->url;
+ $url->setParam($this->parameter, $this->current() + 1);
+
+ return sprintf('<li><a href="%s">&raquo;</a></li>', $url);
+ }
+
+ public function page($number) {
+ $url = clone $this->url;
+ $url->setParam($this->parameter, $number);
+
+ return sprintf('<li><a href="%s">%s</a></li>', $url, $number);
+ }
+
+ public function previous() {
+ $url = clone $this->url;
+ $url->setParam($this->parameter, $this->current() - 1);
+
+ return sprintf('<li><a href="%s">&laquo;</a></li>', $url);
+ }
+
+ public function gap() {
+ return sprintf('<li class="disabled unavailable"><a>...</a></li>');
+ }
+
+ public function jumpTo($total) {
+ return '';
+ }
+
+ public function pageOf($total) {
+ return '';
+ }
+
+}
diff --git a/storage/database/restrictionmaker/CompositeRemoteFieldWorker.php b/storage/database/restrictionmaker/CompositeRemoteFieldWorker.php
index f9444ec..947eec0 100644
--- a/storage/database/restrictionmaker/CompositeRemoteFieldWorker.php
+++ b/storage/database/restrictionmaker/CompositeRemoteFieldWorker.php
@@ -1,72 +1,76 @@
<?php namespace spitfire\storage\database\restrictionmaker;
use ChildrenField;
use Reference;
use spitfire\storage\database\RestrictionGroup;
/*
* The MIT License
*
* Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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.
*/
/**
* This class creates restrictions in the event of the field provided being provided
* as a field from a different table that references a field on the query's local
* table.
*
* This is the exact same as querying by a child field, only that, in this case,
* the child was never defined on the host table.
*/
class CompositeRemoteFieldWorker implements WorkerInterface
{
/**
*
* @param RestrictionGroup $parent
* @param Reference $field
* @param string $operator
* @param mixed $value
*/
public function make(RestrictionGroup $parent, $field, $operator, $value) {
$query = $parent->getQuery();
$of = $query->getTable()->getDb()->getObjectFactory();
/*
* There's several very specific requisites for this component to work.
- * First of all, it is required to
+ * The field...
+ *
+ * * must be a reference
+ * * must not belong to the same table as the query
+ * * must point to the same table as the query
*/
if (!($field instanceof Reference && $field->getTable() !== $query->getTable() && $field->getTarget() === $query->getTable()->getSchema())) {
return false;
}
/*
* Create a child field that connects the tables appropriately. Generally
* speaking, we could be using the field directly. But it makes the whole
* ordeal simpler.
*/
$child = new ChildrenField($field->getSchema(), $field->getName());
$child->setSchema($query->getTable()->getSchema());
return $of->restrictionCompositeInstance($parent, $child, $value, $operator);
}
}
diff --git a/storage/database/restrictionmaker/CompositeWorker.php b/storage/database/restrictionmaker/CompositeWorker.php
index 44f6fae..6af3701 100644
--- a/storage/database/restrictionmaker/CompositeWorker.php
+++ b/storage/database/restrictionmaker/CompositeWorker.php
@@ -1,58 +1,59 @@
<?php namespace spitfire\storage\database\restrictionmaker;
use spitfire\storage\database\RestrictionGroup;
/*
* The MIT License
*
* Copyright 2017 César de la Cal Bretschneider <cesar@magic3w.com>.
*
* 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