Skip to content

Commit

Permalink
Moving result type conversion to the statements layer
Browse files Browse the repository at this point in the history
This is part of the work needed for automatically converting SQL
functions to their correspondiny PHP types.

I also cleans up quite a bit the ResultSet class
  • Loading branch information
lorenzo committed Nov 22, 2015
1 parent 64a0b11 commit 8477fa8
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 77 deletions.
74 changes: 74 additions & 0 deletions src/Database/FieldTypeConverter.php
@@ -0,0 +1,74 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.2.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;

/**
* A callable class to be used for processing each of the rows in a statement
* result, so that the values are converted to the right PHP types.
*/
class FieldTypeConverter
{
/**
* An array containing the name of the fields and the Type objects
* each should use when converting them.
*
* @var array
*/
protected $_typeMap;

/**
* The driver object to be used in the type conversion
*
* @var \Cake\Database\Driver
*/
protected $_driver;

/**
* Builds the type map
*
* @param \Cake\Database\TypeMap $typeMap Contains the types to use for converting results
* @param \Cake\Database\Driver $driver The driver to use for the type conversion
*/
public function __construct(TypeMap $typeMap, Driver $driver)
{
$this->_driver = $driver;
$map = $typeMap->toArray();
$types = array_keys(Type::map());
$types = array_map(['Cake\Database\Type', 'build'], array_combine($types, $types));
$result = [];

foreach ($map as $field => $type) {
if (isset($types[$type])) {
$result[$field] = $types[$type];
}
}
$this->_typeMap = $result;
}

/**
* Converts each of the fields in the array that are present in the type map
* using the corresponding Type class.
*
* @param array $row The array with the fields to be casted
* @return array
*/
public function __invoke($row)
{
foreach ($this->_typeMap as $field => $type) {
$row[$field] = $type->toPHP($row[$field], $this->_driver);
}
return $row;
}
}
38 changes: 38 additions & 0 deletions src/Database/Query.php
Expand Up @@ -19,6 +19,7 @@
use Cake\Database\Expression\QueryExpression;
use Cake\Database\Expression\ValuesExpression;
use Cake\Database\Statement\CallbackStatement;
use Cake\Database\TypeMap;
use IteratorAggregate;
use RuntimeException;

Expand Down Expand Up @@ -121,6 +122,13 @@ class Query implements ExpressionInterface, IteratorAggregate
*/
protected $_useBufferedResults = true;

/**
* The Type map for fields in the select clause
*
* @var \Cake\Database\TypeMap
*/
protected $_selectTypeMap;

/**
* Constructor.
*
Expand Down Expand Up @@ -172,6 +180,13 @@ public function connection($connection = null)
public function execute()
{
$statement = $this->_connection->run($this);
$driver = $this->_connection->driver();
$typeMap = $this->selectTypeMap();

if ($typeMap->toArray()) {
$this->decorateResults(new FieldTypeConverter($typeMap, $driver));
}

$this->_iterator = $this->_decorateStatement($statement);
$this->_dirty = false;
return $this->_iterator;
Expand Down Expand Up @@ -1677,6 +1692,29 @@ public function bufferResults($enable = null)
return $this;
}

/**
* Sets the TypeMap class where the types for each of the fields in the
* select clause are stored.
*
* When called with no arguments, the current TypeMap object is returned.
*
* @param \Cake\Database\TypeMap $typeMap The map object to use
* @return $this|\Cake\Database\TypeMap
*/
public function selectTypeMap(TypeMap $typeMap = null)
{
if ($typeMap === null && $this->_selectTypeMap === null) {
$this->_selectTypeMap = new TypeMap();
}

if ($typeMap === null) {
return $this->_selectTypeMap;
}

$this->_selectTypeMap = $typeMap;
return $this;
}

/**
* Auxiliary function used to wrap the original statement from the driver with
* any registered callbacks.
Expand Down
10 changes: 10 additions & 0 deletions src/Database/TypeMap.php
Expand Up @@ -136,4 +136,14 @@ public function type($column)
}
return null;
}

/**
* Returns an array of all types mapped types
*
* @return array
*/
public function toArray()
{
return $this->_types + $this->_defaults;
}
}
41 changes: 33 additions & 8 deletions src/ORM/Query.php
Expand Up @@ -17,6 +17,7 @@
use ArrayObject;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query as DatabaseQuery;
use Cake\Database\TypeMap;
use Cake\Database\ValueBinder;
use Cake\Datasource\QueryInterface;
use Cake\Datasource\QueryTrait;
Expand Down Expand Up @@ -173,7 +174,7 @@ public function addDefaultTypes(Table $table)
$map = $table->schema()->typeMap();
$fields = [];
foreach ($map as $f => $type) {
$fields[$f] = $fields[$alias . '.' . $f] = $type;
$fields[$f] = $fields[$alias . '.' . $f] = $fields[$alias . '__' . $f] = $type;
}
$this->typeMap()->addDefaults($fields);

Expand Down Expand Up @@ -669,6 +670,8 @@ public function cleanCopy()
$clone->offset(null);
$clone->mapReduce(null, null, true);
$clone->formatResults(null, true);
$clone->selectTypeMap(new TypeMap());
$clone->decorateResults(null, true);
return $clone;
}

Expand Down Expand Up @@ -869,6 +872,7 @@ protected function _execute()
$decorator = $this->_decoratorClass();
return new $decorator($this->_results);
}

$statement = $this->eagerLoader()->loadExternal($this, $this->execute());
return new ResultSet($this, $statement);
}
Expand All @@ -880,22 +884,23 @@ protected function _execute()
* specified and applies the joins required to eager load associations defined
* using `contain`
*
* It also sets the default types for the columns in the select clause
*
* @see \Cake\Database\Query::execute()
* @return void
*/
protected function _transformQuery()
{
if (!$this->_dirty) {
if (!$this->_dirty || $this->_type !== 'select') {
return;
}

if ($this->_type === 'select') {
if (empty($this->_parts['from'])) {
$this->from([$this->_repository->alias() => $this->_repository->table()]);
}
$this->_addDefaultFields();
$this->eagerLoader()->attachAssociations($this, $this->_repository, !$this->_hasFields);
if (empty($this->_parts['from'])) {
$this->from([$this->_repository->alias() => $this->_repository->table()]);
}
$this->_addDefaultFields();
$this->eagerLoader()->attachAssociations($this, $this->_repository, !$this->_hasFields);
$this->_addDefaultSelectTypes();
}

/**
Expand All @@ -919,6 +924,26 @@ protected function _addDefaultFields()
$this->select($aliased, true);
}

/**
* Sets the default types for converting the fields in the select clause
*
* @return void
*/
protected function _addDefaultSelectTypes()
{
$typeMap = $this->typeMap()->defaults();
$selectTypeMap = $this->selectTypeMap();
$select = array_keys($this->clause('select'));
$types = [];

foreach ($select as $alias) {
if (isset($typeMap[$alias])) {
$types[$alias] = $typeMap[$alias];
}
}
$this->selectTypeMap()->addDefaults($types);
}

/**
* {@inheritDoc}
*
Expand Down
48 changes: 5 additions & 43 deletions src/ORM/ResultSet.php
Expand Up @@ -179,7 +179,6 @@ public function __construct($query, $statement)
$this->_useBuffering = $query->bufferResults();
$this->_defaultAlias = $this->_defaultTable->alias();
$this->_calculateColumnMap();
$this->_calculateTypeMap();

if ($this->_useBuffering) {
$count = $this->count();
Expand Down Expand Up @@ -397,34 +396,11 @@ protected function _calculateColumnMap()
* Creates a map of Type converter classes for each of the columns that should
* be fetched by this object.
*
* @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
* @return void
*/
protected function _calculateTypeMap()
{
if (isset($this->_map[$this->_defaultAlias])) {
$this->_types[$this->_defaultAlias] = $this->_getTypes(
$this->_defaultTable,
$this->_map[$this->_defaultAlias]
);
}

foreach ($this->_matchingMapColumns as $alias => $keys) {
$this->_types[$alias] = $this->_getTypes(
$this->_matchingMap[$alias]['instance']->target(),
$keys
);
}

foreach ($this->_containMap as $assoc) {
$alias = $assoc['alias'];
if (isset($this->_types[$alias]) || !$assoc['canBeJoined'] || !isset($this->_map[$alias])) {
continue;
}
$this->_types[$alias] = $this->_getTypes(
$assoc['instance']->target(),
$this->_map[$alias]
);
}
}

/**
Expand Down Expand Up @@ -499,12 +475,9 @@ protected function _groupResult($row)

foreach ($this->_matchingMapColumns as $alias => $keys) {
$matching = $this->_matchingMap[$alias];
$results['_matchingData'][$alias] = $this->_castValues(
$alias,
array_combine(
$keys,
array_intersect_key($row, $keys)
)
$results['_matchingData'][$alias] = array_combine(
$keys,
array_intersect_key($row, $keys)
);
if ($this->_hydrate) {
$options['source'] = $matching['instance']->registryAlias();
Expand All @@ -519,12 +492,6 @@ protected function _groupResult($row)
$presentAliases[$table] = true;
}

if (isset($presentAliases[$defaultAlias])) {
$results[$defaultAlias] = $this->_castValues(
$defaultAlias,
$results[$defaultAlias]
);
}
unset($presentAliases[$defaultAlias]);

foreach ($this->_containMap as $assoc) {
Expand All @@ -550,8 +517,6 @@ protected function _groupResult($row)
unset($presentAliases[$alias]);

if ($assoc['canBeJoined']) {
$results[$alias] = $this->_castValues($assoc['alias'], $results[$alias]);

$hasData = false;
foreach ($results[$alias] as $v) {
if ($v !== null && $v !== []) {
Expand Down Expand Up @@ -602,14 +567,11 @@ protected function _groupResult($row)
*
* @param string $alias The table object alias
* @param array $values The values to cast
* @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
* @return array
*/
protected function _castValues($alias, $values)
{
foreach ($this->_types[$alias] as $field => $type) {
$values[$field] = $type->toPHP($values[$field], $this->_driver);
}

return $values;
}

Expand Down
35 changes: 35 additions & 0 deletions tests/TestCase/Database/QueryTest.php
Expand Up @@ -17,6 +17,7 @@
use Cake\Core\Configure;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Query;
use Cake\Database\TypeMap;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\TestCase;

Expand Down Expand Up @@ -3488,6 +3489,40 @@ public function testDeepClone()
$this->assertNotEquals($query->clause('order'), $dupe->clause('order'));
}

/**
* Tests the selectTypeMap method
*
* @return void
*/
public function testSelectTypeMap()
{
$query = new Query($this->connection);
$typeMap = $query->selectTypeMap();
$this->assertInstanceOf(TypeMap::class, $typeMap);
$another = clone $typeMap;
$query->selectTypeMap($another);
$this->assertSame($another, $query->selectTypeMap());
}

/**
* Tests the automatic type conversion for the fields in the result
*
* @return void
*/
public function testSelectTypeConversion()
{
$query = new Query($this->connection);
$time = new \DateTime('2007-03-18 10:50:00');
$query
->select(['id', 'comment', 'the_date' => 'created'])
->from('comments')
->limit(1)
->selectTypeMap()->types(['id' => 'integer', 'the_date' => 'datetime']);
$result = $query->execute()->fetchAll('assoc');
$this->assertInternalType('integer', $result[0]['id']);
$this->assertInstanceOf('DateTime', $result[0]['the_date']);
}

/**
* Assertion for comparing a table's contents with what is in it.
*
Expand Down

0 comments on commit 8477fa8

Please sign in to comment.