Skip to content
Browse files

Merge remote-tracking branch 'upstream/dev' into dev

  • Loading branch information...
2 parents f4586af + dda0ddf commit aafe115b132d5d8ead33bbe8ab5c13d64b8aa34c @coogle committed
View
38 README.md
@@ -1,38 +0,0 @@
-You asked for a better framework. Here it is.
----------------------------------------------
-
-Lithium is the fast, flexible and most RAD development framework for PHP 5.3 and up.
-
-A framework of firsts
----------------------
-
-Lithium is the first and only major PHP framework built from the ground up for PHP 5.3+, and the first to break ground into major new technologies, including bridging the gap between relational and non-relational databases through a single, unified API.
-
-Promiscuously opinionated
--------------------------
-
-Some frameworks give you a solid set of classes, but little or no default project organization, leaving you to fend for yourself on each project you create, and spend time wiring up framework classes that should just work together. Others provide you with great organizational conventions, but no way to break out of those conventions if you need to, and too often, no way to override or replace core framework classes.
-
-Lithium is the first framework to give you the best of both worlds, without compromising either. In fact, Lithium's API is intentionally designed to allow you to "grow out of" the framework and into your own custom code over the course of your application's lifecycle, if your needs require.
-
-Technology
-----------
-
-Lithium takes full advantage of the latest PHP 5.3 features, including namespaces, late static binding and closures. Lithium's innovative [method filter system](http://lithify.me/docs/lithium/util/collection/Filters) makes extensive use of closures and anonymous functions to allow application developers to "wrap" framework method calls, intercepting parameters before, and return values after.
-
-Lithium also complies with the PHP 5.3 namespacing standard, allowing you to easily integrate other PHP 5.3 standard libraries and frameworks with Lithium applications, and vice-versa.
-
-Lithium integrates the latest storage technologies, including MongoDB, CouchDB and Redis, with plugin support for Cassandra, ElasticSearch and others.
-
-Flexibility
------------
-
-Lithium gives you full control over your application, from filters to dynamically modify framework internals, to dynamic dependencies to extend and replace core classes with application or plugin classes, to heavy use of adapter-oriented configurations, to make it seamless to move between different technologies and options.
-
-Every component of the Lithium framework stack is replaceable through the robust plugin architecture. Swap out the default ORM / ODM implementation for [Doctrine 2](https://github.com/mariano/li3_doctrine2/) or [PHP ActiveRecord](http://dev.lithify.me/li3_activerecord). Don't like the templating? Use [ Twig](http://dev.lithify.me/li3_twig), [ Mustache](https://github.com/bobthecow/mustache.php), or roll your own.
-
-If you don't even need to write a full application, build a micro-app in a single file using the routing system, without giving up the maintainability of the framework's structure.
-
-Build status
------------
-[![Build Status](https://secure.travis-ci.org/UnionOfRAD/lithium.png?branch=master)](http://travis-ci.org/UnionOfRAD/lithium)
View
34 data/model/Query.php
@@ -98,7 +98,7 @@ class Query extends \lithium\core\Object {
protected $_alias = array();
/**
- * Map the generated aliases to their corresponding relation path
+ * Map beetween generated aliases and corresponding relation paths
*
* @see lithium\data\model\Query::alias()
*
@@ -107,7 +107,16 @@ class Query extends \lithium\core\Object {
protected $_paths = array();
/**
- * Map the generated aliases to their corresponding model
+ * Map beetween relation paths and their corresponding fieldname paths
+ *
+ * @see lithium\data\model\Query::alias()
+ *
+ * @var array
+ */
+ protected $_relationNames = array();
+
+ /**
+ * Map beetween generated aliases and corresponding models.
*
* @see lithium\data\model\Query::alias()
*
@@ -720,23 +729,40 @@ public function alias($alias = true, $relpath = null) {
}
$this->_paths[$alias] = $relpath;
+ $fieldname = array();
foreach ($paths as $path) {
if (!$relation = $model::relations($path)) {
$model = null;
break;
}
+ $fieldname[] = $relation->fieldName();
$model = $relation->to();
}
$this->_models[$alias] = $model;
+ $this->_relationNames[$relpath] = join('.', $fieldname);
return $alias;
}
/**
+ * Return the relation paths mapped to their corredponding fieldname paths.
+ *
+ * @param object $source Instance of the data source (`lithium\data\Source`) to use for
+ * conversion.
+ * @return array Map between relation paths and their corresponding fieldname paths.
+ */
+ public function relationNames(Source $source = null) {
+ if ($source) {
+ $this->applyStrategy($source);
+ }
+ return $this->_relationNames;
+ }
+
+ /**
* Return the generated aliases mapped to their relation path
*
* @param object $source Instance of the data source (`lithium\data\Source`) to use for
* conversion.
- * @return array Map between alias and their corresponding dotted relation
+ * @return array Map between aliases and their corresponding dotted relation paths.
*/
public function paths(Source $source = null) {
if ($source) {
@@ -750,7 +776,7 @@ public function paths(Source $source = null) {
*
* @param object $source Instance of the data source (`lithium\data\Source`) to use for
* conversion.
- * @return array Map between alias and their corresponding model
+ * @return array Map between aliases and their corresponding fully-namespaced model names.
*/
public function models(Source $source = null) {
if ($source) {
View
66 data/source/Database.php
@@ -182,7 +182,7 @@ public function __construct(array $config = array()) {
$with = $context->with();
- $strategy = function($me, $model, $tree, $path, $from, $needPks) use ($self, $context, $with) {
+ $strategy = function($me, $model, $tree, $path, $from, &$deps) use ($self, $context, $with) {
foreach ($tree as $name => $childs) {
if (!$rel = $model::relations($name)) {
throw new QueryException("Model relationship `{$name}` not found.");
@@ -205,9 +205,8 @@ public function __construct(array $config = array()) {
}
$to = $context->alias($alias, $relPath);
- if ($needPks) {
- $context->fields(array($to => (array) $model::meta('key')));
- }
+ $deps[$to] = $deps[$from];
+ $deps[$to][] = $from;
if ($context->relationships($relPath) === null) {
$context->relationships($relPath, array(
@@ -221,19 +220,27 @@ public function __construct(array $config = array()) {
}
if (!empty($childs)) {
- $me($me, $rel->to(), $childs, $relPath, $to, $needPks);
+ $me($me, $rel->to(), $childs, $relPath, $to, $deps);
}
}
};
$tree = Set::expand(Set::normalize(array_keys($with)));
$alias = $context->alias();
- $needPks = false;
- if ($context->fields()) {
- $needPks = true;
- $context->fields(array($alias => (array) $model::meta('key')));
+ $deps = array($alias => array());
+ $strategy($strategy, $model, $tree, '', $alias, $deps);
+
+ $models = $context->models();
+ foreach ($context->fields() as $field) {
+ list($alias, $field) = $self->invokeMethod('_splitFieldname', array($field));
+ $alias = $alias ?: $field;
+ if ($alias && isset($models[$alias])) {
+ foreach ($deps[$alias] as $depAlias) {
+ $depModel = $models[$depAlias];
+ $context->fields(array($depAlias => (array) $depModel::meta('key')));
+ }
+ }
}
- $strategy($strategy, $model, $tree, '', $context->alias(), $needPks);
},
'nested' => function($self, $model, $context) {
throw new QueryException("This strategy is not yet implemented.");
@@ -322,7 +329,7 @@ public function name($name) {
*/
protected function _splitFieldname($field) {
if (is_string($field)) {
- if (preg_match('/^[a-z0-9_-]+\.[a-z0-9_-]+$/i', $field)) {
+ if (preg_match('/^[a-z0-9_-]+\.([a-z0-9_-]+|\*)$/i', $field)) {
return explode('.', $field, 2);
}
}
@@ -465,18 +472,30 @@ public function read($query, array $options = array()) {
return $result;
case 'array':
$columns = $args['schema'] ?: $self->schema($query, $result);
- $records = array();
- if (is_array(reset($columns))) {
- $columns = reset($columns);
+
+ if (!isset($columns['']) || !is_array($columns[''])) {
+ $columns = array('' => $columns);
}
- while ($data = $result->next()) {
- // @hack: Fix this to support relationships
- if (count($columns) != count($data) && is_array(current($columns))) {
- $columns = current($columns);
+
+ $relationNames = is_object($query) ? $query->relationNames($self) : array();
+ $i = 0;
+ $records = array();
+ foreach ($result as $data) {
+ $offset = 0;
+ $records[$i] = array();
+ foreach ($columns as $path => $cols) {
+ $len = count($cols);
+ $values = array_combine($cols, array_slice($data, $offset, $len));
+ if ($path) {
+ $records[$i][$relationNames[$path]] = $values;
+ } else {
+ $records[$i] += $values;
+ }
+ $offset += $len;
}
- $records[] = array_combine($columns, $data);
+ $i++;
}
- return $records;
+ return Set::expand($records);
case 'item':
return $self->item($query->model(), array(), compact('query', 'result') + array(
'class' => 'set'
@@ -697,6 +716,7 @@ public function _schema($query, $fields = null) {
$model = $query->model();
$paths = $query->paths($this);
$models = $query->models($this);
+ $alias = $query->alias();
$result = array();
if (!$model) {
@@ -730,6 +750,8 @@ public function _schema($query, $fields = null) {
unset($fields[0]);
}
+ $fields = isset($fields[$alias]) ? array($alias => $fields[$alias]) + $fields : $fields;
+
foreach ($fields as $field => $value) {
if (is_array($value)) {
if (isset($value['*'])) {
@@ -901,6 +923,7 @@ public function _processConditions($key, $value, $context, $schema = null, $glue
public function fields($fields, $context) {
$type = $context->type();
$schema = $context->schema()->fields();
+ $alias = $context->alias();
if (!is_array($fields)) {
return $this->_fieldsReturn($type, $context, $fields, $schema);
@@ -918,6 +941,8 @@ public function fields($fields, $context) {
unset($fields[0]);
}
+ $fields = isset($fields[$alias]) ? array($alias => $fields[$alias]) + $fields : $fields;
+
foreach ($fields as $field => $value) {
if (is_array($value)) {
if (isset($value['*'])) {
@@ -965,6 +990,7 @@ protected function _fields($fields, $context) {
}
return $list;
}
+
protected function _fieldsQuote($alias, $field) {
$open = $this->_quotes[0];
$close = $this->_quotes[1];
View
353 test/Mocker.php
@@ -0,0 +1,353 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test;
+
+use lithium\util\String;
+use ReflectionClass;
+use ReflectionMethod;
+use Reflection;
+
+/**
+ * The Mocker class aids in the creation of Mocks on the fly, allowing you to
+ * use Lithium filters on most methods in the class.
+ *
+ * To enable the autoloading of mocks you simply need to make a simple method
+ * call.
+ * {{{
+ * use lithium\core\Environment;
+ * use lithium\test\Mocker;
+ * if(!Environment::is('production')) {
+ * Mocker::register();
+ * }
+ * }}}
+ *
+ * You can also enable autoloading inside the setup of a unit test class. This
+ * method can be called redundantly.
+ * {{{
+ * use lithium\test\Mocker;
+ * class MockerTest extends \lithium\test\Unit {
+ * public function setUp() {
+ * Mocker::register();
+ * }
+ * }
+ * }}}
+ *
+ * Using Mocker is the fun magical part, it's autoloaded so simply call the
+ * class you want to mock with the '\Mock' at the end. The autoloader will
+ * detect you want to autoload it, and create it for you. Now you can filter
+ * any method.
+ * {{{
+ * use lithium\console\dispatcher\Mock as DispatcherMock;
+ * $dispatcher = new DispatcherMock();
+ * $dispatcher->applyFilter('config', function($self, $params, $chain) {
+ * return array();
+ * });
+ * $results = $dispatcher->config();
+ * }}}
+ * {{{
+ * use lithium\analysis\parser\Mock as ParserMock;
+ * $code = 'echo "foobar";';
+ * ParserMock::applyFilter('config', function($self, $params, $chain) {
+ * return array();
+ * });
+ * $tokens = ParserMock::tokenize($code, array('wrap' => true));
+ * }}}
+ */
+class Mocker {
+
+ /**
+ * A list of code to be generated for the delegator.
+ *
+ * The MockDelgate directly extends the mocker and makes all methods
+ * publically available to other classes but should not be accessed directly
+ * by any other application. This should be called only by the mocker and
+ * the mockee and never by the consumer.
+ *
+ * @var array
+ */
+ protected static $_mockDelegateIngredients = array(
+ 'startClass' => array(
+ 'namespace {:namespace};',
+ 'class MockDelegate extends \{:mocker} {'
+ ),
+ 'constructor' => array(
+ '{:modifiers} function __construct({:args}) {',
+ ' $args = func_get_args();',
+ ' $this->parent = array_pop($args);',
+ ' $this->parent->mocker = $this;',
+ ' call_user_func_array("parent::__construct", $args);',
+ '}',
+ ),
+ 'method' => array(
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' $token = spl_object_hash($this);',
+ ' $id = count($args) - 1;',
+ ' if (!isset($args[$id]) || $args[$id] !== $token) {',
+ ' $method = array($this->parent, "{:method}");',
+ ' return call_user_func_array($method, $args);',
+ ' }',
+ ' array_pop($args);',
+ ' return call_user_func_array("parent::{:method}", $args);',
+ '}',
+ ),
+ 'staticMethod' => array(
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' $token = "1f3870be274f6c49b3e31a0c6728957f";',
+ ' $id = count($args) - 1;',
+ ' if (!isset($args[$id]) || $args[$id] !== $token) {',
+ ' $method = \'{:namespace}\Mock::{:method}\';',
+ ' return call_user_func_array($method, $args);',
+ ' }',
+ ' array_pop($args);',
+ ' return call_user_func_array("parent::{:method}", $args);',
+ '}',
+ ),
+ 'endClass' => array(
+ '}',
+ ),
+ );
+
+ /**
+ * A list of code to be generated for the mocker.
+ *
+ * The Mock class directly extends the mock class but only directly
+ * interacts with the MockDelegate directly. This class is the actual
+ * interface for consumers, instantiation or static method calls, and can
+ * have most of its methods filtered.
+ *
+ * @var array
+ */
+ protected static $_mockIngredients = array(
+ 'startClass' => array(
+ 'namespace {:namespace};',
+ 'class Mock extends \{:mocker} {',
+ ' public $mocker;',
+ ' protected $_safeVars = array(',
+ ' "_classes",',
+ ' "_methodFilters",',
+ ' "mocker",',
+ ' "_safeVars"',
+ ' );',
+ ),
+ 'get' => array(
+ 'public function __get($key) {',
+ ' return $this->mocker->$key;',
+ '}',
+ ),
+ 'constructor' => array(
+ '{:modifiers} function __construct({:args}) {',
+ ' $args = array_values(get_defined_vars());',
+ ' array_push($args, $this);',
+ ' foreach ($this as $key => $value) {',
+ ' if (!in_array($key, $this->_safeVars)) {',
+ ' unset($this->$key);',
+ ' }',
+ ' }',
+ ' $class = new \ReflectionClass(\'{:namespace}\MockDelegate\');',
+ ' $class->newInstanceArgs($args);',
+ '}',
+ ),
+ 'destructor' => array(
+ 'public function __destruct() {}',
+ ),
+ 'staticMethod' => array(
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' array_push($args, "1f3870be274f6c49b3e31a0c6728957f");',
+ ' $method = \'{:namespace}\MockDelegate::{:method}\';',
+ ' return self::_filter("{:method}", $args, function($self, $args) use(&$method) {',
+ ' return call_user_func_array($method, $args);',
+ ' });',
+ '}',
+ ),
+ 'method' => array(
+ '{:modifiers} function {:method}({:args}) {',
+ ' $args = func_get_args();',
+ ' array_push($args, spl_object_hash($this->mocker));',
+ ' $method = array($this->mocker, "{:method}");',
+ ' return $this->_filter(__METHOD__, $args, function($self, $args) use(&$method) {',
+ ' return call_user_func_array($method, $args);',
+ ' });',
+ '}',
+ ),
+ 'endClass' => array(
+ '}',
+ ),
+ );
+
+ /**
+ * A list of methods we should not overwrite in our mock class.
+ *
+ * @var array
+ */
+ protected static $_blackList = array(
+ '__destruct', '__call', '__callStatic', '_parents',
+ '__get', '__set', '__isset', '__unset', '__sleep',
+ '__wakeup', '__toString', '__clone', '__invoke',
+ '_stop', '_init', 'applyFilter', 'invokeMethod',
+ '__set_state', '_instance', '_filter',
+ );
+
+ /**
+ * Will register this class into the autoloader.
+ *
+ * @return void
+ */
+ public static function register() {
+ spl_autoload_register(array(__CLASS__, 'create'));
+ }
+
+ /**
+ * The main entrance to create a new Mock class.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return void
+ */
+ public static function create($mockee) {
+ if (!self::_validateMockee($mockee)) {
+ return;
+ }
+
+ $mocker = self::_mocker($mockee);
+
+ $tokens = array(
+ 'namespace' => self::_namespace($mockee),
+ 'mocker' => $mocker,
+ 'mockee' => 'MockDelegate',
+ );
+ $mockDelegate = self::_dynamicCode('mockDelegate', 'startClass', $tokens);
+ $mock = self::_dynamicCode('mock', 'startClass', $tokens);
+
+ $reflectedClass = new ReflectionClass($mocker);
+ $reflecedMethods = $reflectedClass->getMethods();
+ foreach ($reflecedMethods as $method) {
+ if (!in_array($method->name, self::$_blackList)) {
+ $key = $method->isStatic() ? 'staticMethod' : 'method';
+ $key = $method->name === '__construct' ? 'constructor' : $key;
+ $tokens = array(
+ 'namespace' => self::_namespace($mockee),
+ 'method' => $method->name,
+ 'modifiers' => self::_methodModifiers($method),
+ 'args' => self::_methodParams($method),
+ 'mocker' => $mocker,
+ );
+ $mockDelegate .= self::_dynamicCode('mockDelegate', $key, $tokens);
+ $mock .= self::_dynamicCode('mock', $key, $tokens);
+ }
+ }
+
+ $mockDelegate .= self::_dynamicCode('mockDelegate', 'endClass');
+ $mock .= self::_dynamicCode('mock', 'get');
+ $mock .= self::_dynamicCode('mock', 'destructor');
+ $mock .= self::_dynamicCode('mock', 'endClass');
+
+ eval($mockDelegate . $mock);
+ }
+
+ /**
+ * Will determine what method mofifiers of a method.
+ *
+ * For instance: 'public static' or 'private abstract'
+ *
+ * @param ReflectionMethod $method
+ * @return string
+ */
+ protected static function _methodModifiers(ReflectionMethod $method) {
+ $modifierKey = $method->getModifiers();
+ $modifierArray = Reflection::getModifierNames($modifierKey);
+ $modifiers = implode(' ', $modifierArray);
+ return str_replace(array('private', 'protected'), 'public', $modifiers);
+ }
+
+ /**
+ * Will determine what parameter prototype of a method.
+ *
+ * For instance: 'ReflectionMethod $method' or '$name, array $foo = null'
+ *
+ * @param ReflectionMethod $method
+ * @return string
+ */
+ protected static function _methodParams(ReflectionMethod $method) {
+ $pattern = '/Parameter #[0-9]+ \[ [^\>]+>([^\]]+) \]/';
+ $replace = array(
+ 'from' => array('Array', 'or NULL'),
+ 'to' => array('array()', ''),
+ );
+ preg_match_all($pattern, $method, $matches);
+ $params = implode(', ', $matches[1]);
+ return str_replace($replace['from'], $replace['to'], $params);
+ }
+
+ /**
+ * Will generate the code you are wanting.
+ *
+ * This pulls from $_mockDelegateIngredients and $_mockIngredients.
+ *
+ * @param string $type The name of the array of ingredients to use
+ * @param string $key The key from the array of ingredients
+ * @param array $tokens Tokens, if any, that should be inserted
+ * @return string
+ */
+ protected static function _dynamicCode($type, $key, $tokens = array()) {
+ $name = '_' . $type . 'Ingredients';
+ $code = implode("\n", self::${$name}[$key]);
+ return String::insert($code, $tokens) . "\n";
+ }
+
+ /**
+ * Will generate the mocker from the current mockee.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return array
+ */
+ protected static function _mocker($mockee) {
+ $matches = array();
+ preg_match_all('/^(.*)\\\\([^\\\\]+)\\\\Mock$/', $mockee, $matches);
+ if (!isset($matches[1][0])) {
+ return;
+ }
+ return $matches[1][0] . '\\' . ucfirst($matches[2][0]);
+ }
+
+ /**
+ * Will generate the namespace from the current mockee.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return string
+ */
+ protected static function _namespace($mockee) {
+ $matches = array();
+ preg_match_all('/^(.*)\\\\Mock$/', $mockee, $matches);
+ return isset($matches[1][0]) ? $matches[1][0] : null;
+ }
+
+ /**
+ * Will validate if mockee is a valid class we should mock.
+ *
+ * @param string $mockee The fully namespaced `\Mock` class
+ * @return bool
+ */
+ protected static function _validateMockee($mockee) {
+ if (class_exists($mockee) || preg_match('/\\\\Mock$/', $mockee) !== 1) {
+ return false;
+ }
+ $mocker = self::_mocker($mockee);
+ $isObject = is_subclass_of($mocker, 'lithium\core\Object');
+ $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
+ if (!$isObject && !$isStatic) {
+ return false;
+ }
+ return true;
+ }
+
+}
+
+?>
View
23 tests/cases/data/model/QueryTest.php
@@ -633,6 +633,27 @@ public function testModels() {
$this->assertEqual($expected, $query->models($this->db));
}
+ public function testRelationNames() {
+ $model = 'lithium\tests\mocks\data\model\MockQueryPost';
+ $query = new Query(compact('model'));
+ $query->alias(null, 'MockQueryComment');
+
+ $expected = array(
+ 'MockQueryComment' => 'mock_query_comments'
+ );
+ $this->assertEqual($expected, $query->relationNames($this->db));
+
+ $query->alias('Post');
+ $query->alias('Comment', 'MockQueryComment');
+ $query->alias('Post2', 'MockQueryComment.MockQueryPost');
+
+ $expected = array(
+ 'MockQueryComment' => 'mock_query_comments',
+ 'MockQueryComment.MockQueryPost' => 'mock_query_comments.mock_query_post'
+ );
+ $this->assertEqual($expected, $query->relationNames($this->db));
+ }
+
public function testExportWithJoinedStrategy() {
$query = new Query(array(
'alias' => 'MyAlias',
@@ -656,7 +677,7 @@ public function testExportWithJoinedStrategy() {
'alias' => 'AS {MyAlias}',
'comment' => '/* No comment */',
'conditions' => 'WHERE {MyAlias}.{id} = 2',
- 'fields' => '{Tag}.*, {MyAlias}.{id}, {Image}.{id}, {ImageTag}.{id}',
+ 'fields' => '{MyAlias}.{id}, {Tag}.*, {Image}.{id}, {ImageTag}.{id}',
'having' => '',
'group' => null,
'order' => null,
View
120 tests/cases/data/source/DatabaseTest.php
@@ -16,6 +16,7 @@
use lithium\tests\mocks\data\model\MockDatabaseComment;
use lithium\tests\mocks\data\model\MockDatabaseTagging;
use lithium\tests\mocks\data\model\MockDatabasePostRevision;
+use lithium\tests\mocks\data\model\mock_database\MockResult;
class DatabaseTest extends \lithium\test\Unit {
@@ -42,6 +43,7 @@ public function setUp() {
public function tearDown() {
$this->db->logs = array();
+ $this->db->return = array();
}
public function testDefaultConfig() {
@@ -181,7 +183,7 @@ public function testSchema() {
$options['fields'] = array('id', 'title');
$result = $this->db->schema(new Query($options));
- $expected = array($modelName => $options['fields'], 'MockDatabaseComment' => array('id'));
+ $expected = array($modelName => $options['fields']);
$this->assertEqual($expected, $result);
$options['fields'] = array(
@@ -192,7 +194,7 @@ public function testSchema() {
$result = $this->db->schema(new Query($options));
$expected = array(
$modelName => array('id', 'title'),
- 'MockDatabaseComment' => array('body', 'id')
+ 'MockDatabaseComment' => array('body')
);
$this->assertEqual($expected, $result);
@@ -203,7 +205,7 @@ public function testSchema() {
$result = $this->db->schema(new Query($options));
$expected = array(
$modelName => array('id', 'title'),
- 'MockDatabaseComment' => array('body', 'created', 'id')
+ 'MockDatabaseComment' => array('body', 'created')
);
$this->assertEqual($expected, $result);
@@ -1154,6 +1156,9 @@ public function testsplitFieldname() {
$result = $this->db->invokeMethod('_splitFieldname', array('lower(Alias.fieldname)'));
$this->assertEqual(array(null, 'lower(Alias.fieldname)'), $result);
+
+ $result = $this->db->invokeMethod('_splitFieldname', array('Alias.*'));
+ $this->assertEqual(array('Alias', '*'), $result);
}
public function testOn() {
@@ -1407,7 +1412,7 @@ public function testExportedFieldsWithJoinedStrategy() {
'with' => array('Image.ImageTag.Tag')
));
$result = $query->export($this->db);
- $expected = '{Gallery}.{id}, {Image}.{id}, {ImageTag}.{id}, {Tag}.{id}';
+ $expected = '{Gallery}.{id}';
$this->assertEqual($expected, $result['fields']);
$query = new Query(array(
@@ -1416,7 +1421,7 @@ public function testExportedFieldsWithJoinedStrategy() {
'with' => array('Image.ImageTag.Tag')
));
$result = $query->export($this->db);
- $expected = '{Tag}.{id}, {Gallery}.{id}, {Image}.{id}, {ImageTag}.{id}';
+ $expected = '{Gallery}.{id}, {Tag}.{id}, {Image}.{id}, {ImageTag}.{id}';
$this->assertEqual($expected, $result['fields']);
$query = new Query(array(
@@ -1425,7 +1430,7 @@ public function testExportedFieldsWithJoinedStrategy() {
'with' => array('Image.ImageTag.Tag')
));
$result = $query->export($this->db);
- $expected = '{Tag}.*, {Gallery}.{id}, {Image}.{id}, {ImageTag}.{id}';
+ $expected = '{Gallery}.{id}, {Tag}.*, {Image}.{id}, {ImageTag}.{id}';
$this->assertEqual($expected, $result['fields']);
$query = new Query(array(
@@ -1434,7 +1439,7 @@ public function testExportedFieldsWithJoinedStrategy() {
'with' => array('Image.ImageTag.Tag')
));
$result = $query->export($this->db);
- $expected = '{Tag}.*, {Gallery}.{id}, {Image}.{id}, {ImageTag}.{id}';
+ $expected = '{Gallery}.{id}, {Tag}.*, {Image}.{id}, {ImageTag}.{id}';
$this->assertEqual($expected, $result['fields']);
}
@@ -1453,7 +1458,7 @@ public function testExportedFieldsWithJoinedStrategyAndRecursiveRelation() {
'with' => array('Parent.Parent')
));
$result = $query->export($this->db);
- $expected = '{Parent}.{name}, {Parent}.{id}, {Gallery}.{id}, {Parent__2}.{id}';
+ $expected = '{Gallery}.{id}, {Parent}.{name}';
$this->assertEqual($expected, $result['fields']);
$query = new Query(array(
@@ -1462,7 +1467,7 @@ public function testExportedFieldsWithJoinedStrategyAndRecursiveRelation() {
'with' => array('Parent.Parent' => array('alias' => 'ParentOfParent'))
));
$result = $query->export($this->db);
- $expected = '{ParentOfParent}.{name}, {ParentOfParent}.{id}, {Gallery}.{id}, {Parent}.{id}';
+ $expected = '{Gallery}.{id}, {ParentOfParent}.{name}, {Parent}.{id}';
$this->assertEqual($expected, $result['fields']);
}
@@ -1497,6 +1502,103 @@ public function testCustomField() {
$result = $this->db->read($query);
$this->assertEqual($expected, $this->db->sql);
$this->assertEqual($map, $query->map());
+
+ $query = new Query(array(
+ 'type' => 'read',
+ 'model' => $this->_gallery,
+ 'fields' => array('count(Image.id) as count', 'Image'),
+ 'group' => 'Gallery.id',
+ 'with' => array('Image')
+ ));
+ $result = $this->db->read($query);
+ $expected = 'SELECT count(Image.id) as count, {Gallery}.{id}, {Image}.* FROM ';
+ $expected .= '{mock_gallery} AS {Gallery} LEFT JOIN {mock_image} AS {Image} ON ';
+ $expected .= '{Gallery}.{id} = {Image}.{gallery_id} GROUP BY Gallery.id;';
+
+ $this->assertEqual($expected, $this->db->sql);
+ $map = array(
+ '' => array('count', 'id'),
+ 'Image' => array('id', 'title', 'image', 'gallery_id')
+ );
+ $this->assertEqual($map, $query->map());
+ }
+
+ public function testReturnArrayOnReadWithString() {
+ $data = new MockResult(array('records' => array(
+ array ('id', 'int(11)', 'NO', 'PRI', null, 'auto_increment'),
+ array ('name', 'varchar(256)', 'YES', '', null, '')
+ )));
+ $this->db->return = array(
+ 'schema' => array('field', 'type', 'null', 'key', 'default', 'extra'),
+ '_execute' => $data
+ );
+ $result = $this->db->read('DESCRIBE {table};', array('return' => 'array'));
+ $expected = array(
+ array(
+ 'field' => 'id',
+ 'type' => 'int(11)',
+ 'null' => 'NO',
+ 'key' => 'PRI',
+ 'default' => null,
+ 'extra' => 'auto_increment',
+ ),
+ array(
+ 'field' => 'name',
+ 'type' => 'varchar(256)',
+ 'null' => 'YES',
+ 'key' => '',
+ 'default' => null,
+ 'extra' => '',
+ )
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testReturnArrayOnReadWithQuery() {
+ $data = new MockResult(array('records' => array(array(
+ '1',
+ '2',
+ 'Post title',
+ '2012-12-17 17:04:00',
+ '3',
+ '1',
+ '2',
+ 'Very good post',
+ '2012-12-17 17:05:00',
+ '1',
+ '2',
+ 'Post title',
+ '2012-12-17 17:04:00',
+ ))));
+ $this->db->return = array(
+ '_execute' => $data
+ );
+ $query = new Query(array(
+ 'type' => 'read',
+ 'model' => $this->_model,
+ 'with' => array('MockDatabaseComment.MockDatabasePost')
+ ));
+ $result = $this->db->read($query, array('return' => 'array'));
+ $expected = array(array(
+ 'id' => '1',
+ 'author_id' => '2',
+ 'title' => 'Post title',
+ 'created' => '2012-12-17 17:04:00',
+ 'mock_database_comments' => array(
+ 'id' => '3',
+ 'post_id' => '1',
+ 'author_id' => '2',
+ 'body' => 'Very good post',
+ 'created' => '2012-12-17 17:05:00',
+ 'mock_database_post' => array(
+ 'id' => '1',
+ 'author_id' => '2',
+ 'title' => 'Post title',
+ 'created' => '2012-12-17 17:04:00',
+ )
+ )
+ ));
+ $this->assertEqual($expected, $result);
}
}
View
144 tests/cases/test/MockerTest.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\test;
+
+use lithium\test\Mocker;
+
+/**
+ * WARNING:
+ * No unit test should mock the same test as another
+ */
+class MockerTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Mocker::register();
+ }
+
+ public function testAutoloadRegister() {
+ Mocker::register();
+ $registered = spl_autoload_functions();
+ $this->assertTrue(in_array(array(
+ 'lithium\test\Mocker',
+ 'create'
+ ), $registered));
+ }
+
+ public function testBasicCreation() {
+ $mockee = 'lithium\console\command\Mock';
+ Mocker::create($mockee);
+ $this->assertTrue(class_exists($mockee));
+ }
+
+ public function testBasicCreationExtendsCorrectParent() {
+ $mocker = 'lithium\console\Request';
+ $mockeeObj = new \lithium\console\request\Mock();
+ $this->assertTrue(is_a($mockeeObj, $mocker));
+ }
+
+ public function testCannotMockNonLithiumClasses() {
+ $mockee = 'stdClass\Mock';
+ Mocker::create($mockee);
+ $this->assertTrue(!class_exists($mockee));
+ }
+
+ public function testCannotCreateNonStandardMockClass() {
+ $mockee = 'lithium\console\request\Mocker';
+ Mocker::create($mockee);
+ $this->assertTrue(!class_exists($mockee));
+ }
+
+ public function testFilteringNonStaticClass() {
+ $dispatcher = new \lithium\console\dispatcher\Mock();
+
+ $originalResult = $dispatcher->config();
+
+ $dispatcher->applyFilter('config', function($self, $params, $chain) {
+ return array();
+ });
+
+ $filteredResult = $dispatcher->config();
+
+ $this->assertEqual(0, count($filteredResult));
+ $this->assertNotEqual($filteredResult, $originalResult);
+ }
+
+ public function testFilteringNonStaticClassCanReturnOriginal() {
+ $response = new \lithium\console\response\Mock();
+
+ $originalResult = $response->styles();
+
+ $response->applyFilter('styles', function($self, $params, $chain) {
+ return $chain->next($self, $params, $chain);
+ });
+
+ $filteredResult = $response->styles();
+
+ $this->assertEqual($filteredResult, $originalResult);
+ }
+
+ public function testFilteringStaticClass() {
+ $mockee = 'lithium\analysis\parser\Mock';
+
+ $code = 'echo "foobar";';
+
+ $originalResult = $mockee::tokenize($code, array('wrap' => true));
+
+ $mockee::applyFilter('tokenize', function($self, $params, $chain) {
+ return array();
+ });
+
+ $filteredResult = $mockee::tokenize($code, array('wrap' => true));
+
+ $this->assertEqual(0, count($filteredResult));
+ $this->assertNotEqual($filteredResult, $originalResult);
+ }
+
+ public function testFilteringStaticClassCanReturnOriginal() {
+ $mockee = 'lithium\analysis\inspector\Mock';
+
+ $originalResult = $mockee::methods('lithium\analysis\Inspector');
+
+ $mockee::applyFilter('tokenize', function($self, $params, $chain) {
+ return $chain->next($self, $params, $chain);
+ });
+
+ $filteredResult = $mockee::methods('lithium\analysis\Inspector');
+
+ $this->assertEqual($filteredResult, $originalResult);
+ }
+
+ public function testOriginalMethodNotCalled() {
+ $http = new \lithium\tests\mocks\security\auth\adapter\mockHttp\Mock;
+
+ $this->assertEqual(0, count($http->headers));
+
+ $http->_writeHeader('Content-type: text/html');
+
+ $this->assertEqual(1, count($http->headers));
+
+ $http->applyFilter('_writeHeader', function($self, $params, $chain) {
+ return false;
+ });
+
+ $http->_writeHeader('Content-type: application/pdf');
+
+ $this->assertEqual(1, count($http->headers));
+ }
+
+ public function testFilteringAFilteredMethod() {
+ $adapt = 'lithium\core\adaptable\Mock';
+ $adapt::applyFilter('_initAdapter', function($self, $params, $chain) {
+ return false;
+ });
+ $this->assertFalse($adapt::_initAdapter('foo', array()));
+ }
+
+}
+
+?>
View
21 tests/cases/util/ValidatorTest.php
@@ -1091,16 +1091,25 @@ public function testCheckMultiplePasses() {
}
public function testIsInRange() {
- $value = 5;
$lower = 1;
$upper = 10;
- $result = Validator::isInRange($value, null, compact('lower', 'upper'));
- $this->assertTrue($result);
$value = 0;
$result = Validator::isInRange($value, null, compact('lower', 'upper'));
$this->assertFalse($result);
+ $value = 1;
+ $result = Validator::isInRange($value, null, compact('lower', 'upper'));
+ $this->assertTrue($result);
+
+ $value = 5;
+ $result = Validator::isInRange($value, null, compact('lower', 'upper'));
+ $this->assertTrue($result);
+
+ $value = 10;
+ $result = Validator::isInRange($value, null, compact('lower', 'upper'));
+ $this->assertTrue($result);
+
$value = 11;
$result = Validator::isInRange($value, null, compact('lower', 'upper'));
$this->assertFalse($result);
@@ -1108,6 +1117,9 @@ public function testIsInRange() {
$result = Validator::isInRange(-1, null, array('upper' => 1));
$this->assertTrue($result);
+ $result = Validator::isInRange(1, null, array('upper' => 1));
+ $this->assertTrue($result);
+
$result = Validator::isInRange(2, null, array('upper' => 1));
$this->assertFalse($result);
@@ -1115,6 +1127,9 @@ public function testIsInRange() {
$this->assertTrue($result);
$result = Validator::isInRange(1, null, array('lower' => 1));
+ $this->assertTrue($result);
+
+ $result = Validator::isInRange(0, null, array('lower' => 1));
$this->assertFalse($result);
$this->assertTrue(Validator::isInRange(0));
View
12 tests/mocks/data/model/MockDatabase.php
@@ -41,6 +41,8 @@ class MockDatabase extends \lithium\data\source\Database {
public $log = false;
+ public $return = array();
+
protected $_quotes = array('{', '}');
public function __construct(array $config = array()) {
@@ -94,9 +96,19 @@ protected function _execute($sql) {
if ($this->log) {
$this->logs[] = $sql;
}
+ if (isset($this->return['_execute'])) {
+ return $this->return['_execute'];
+ }
return new MockResult();
}
+ public function schema($query, $resource = null, $context = null) {
+ if (isset($this->return['schema'])) {
+ return $this->return['schema'];
+ }
+ return parent::schema($query, $resource = null, $context = null);
+ }
+
protected function _insertId($query) {
$query = $query->export($this);
ksort($query);
View
6 util/Validator.php
@@ -309,11 +309,11 @@ public static function __init() {
}
switch (true) {
case (!is_null($options['upper']) && !is_null($options['lower'])):
- return ($value > $options['lower'] && $value < $options['upper']);
+ return ($value >= $options['lower'] && $value <= $options['upper']);
case (!is_null($options['upper'])):
- return ($value < $options['upper']);
+ return ($value <= $options['upper']);
case (!is_null($options['lower'])):
- return ($value > $options['lower']);
+ return ($value >= $options['lower']);
}
return is_finite($value);
},

0 comments on commit aafe115

Please sign in to comment.
Something went wrong with that request. Please try again.