Browse Source

Moving result type conversion to the statements layer

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
Jose Lorenzo Rodriguez 10 years ago
parent
commit
8477fa8bb8

+ 74 - 0
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 - 0
src/Database/Query.php

@@ -19,6 +19,7 @@ use Cake\Database\Expression\OrderClauseExpression;
 use Cake\Database\Expression\QueryExpression;
 use Cake\Database\Expression\ValuesExpression;
 use Cake\Database\Statement\CallbackStatement;
+use Cake\Database\TypeMap;
 use IteratorAggregate;
 use RuntimeException;
 
@@ -122,6 +123,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.
      *
      * @param \Cake\Datasource\ConnectionInterface $connection The connection
@@ -172,6 +180,13 @@ class Query implements ExpressionInterface, IteratorAggregate
     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;
@@ -1678,6 +1693,29 @@ class Query implements ExpressionInterface, IteratorAggregate
     }
 
     /**
+     * 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.
      *

+ 10 - 0
src/Database/TypeMap.php

@@ -136,4 +136,14 @@ class TypeMap
         }
         return null;
     }
+
+    /**
+     * Returns an array of all types mapped types
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->_types + $this->_defaults;
+    }
 }

+ 33 - 8
src/ORM/Query.php

@@ -17,6 +17,7 @@ namespace Cake\ORM;
 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;
@@ -173,7 +174,7 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
         $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);
 
@@ -669,6 +670,8 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
         $clone->offset(null);
         $clone->mapReduce(null, null, true);
         $clone->formatResults(null, true);
+        $clone->selectTypeMap(new TypeMap());
+        $clone->decorateResults(null, true);
         return $clone;
     }
 
@@ -869,6 +872,7 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
             $decorator = $this->_decoratorClass();
             return new $decorator($this->_results);
         }
+
         $statement = $this->eagerLoader()->loadExternal($this, $this->execute());
         return new ResultSet($this, $statement);
     }
@@ -880,22 +884,23 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
      * 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();
     }
 
     /**
@@ -920,6 +925,26 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
     }
 
     /**
+     * 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}
      *
      * @see \Cake\ORM\Table::find()

+ 5 - 43
src/ORM/ResultSet.php

@@ -179,7 +179,6 @@ class ResultSet implements ResultSetInterface
         $this->_useBuffering = $query->bufferResults();
         $this->_defaultAlias = $this->_defaultTable->alias();
         $this->_calculateColumnMap();
-        $this->_calculateTypeMap();
 
         if ($this->_useBuffering) {
             $count = $this->count();
@@ -397,34 +396,11 @@ class ResultSet implements ResultSetInterface
      * 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]
-            );
-        }
     }
 
     /**
@@ -499,12 +475,9 @@ class ResultSet implements ResultSetInterface
 
         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();
@@ -519,12 +492,6 @@ class ResultSet implements ResultSetInterface
             $presentAliases[$table] = true;
         }
 
-        if (isset($presentAliases[$defaultAlias])) {
-            $results[$defaultAlias] = $this->_castValues(
-                $defaultAlias,
-                $results[$defaultAlias]
-            );
-        }
         unset($presentAliases[$defaultAlias]);
 
         foreach ($this->_containMap as $assoc) {
@@ -550,8 +517,6 @@ class ResultSet implements ResultSetInterface
             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 !== []) {
@@ -602,14 +567,11 @@ class ResultSet implements ResultSetInterface
      *
      * @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;
     }
 

+ 35 - 0
tests/TestCase/Database/QueryTest.php

@@ -17,6 +17,7 @@ namespace Cake\Test\TestCase\Database;
 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;
 
@@ -3489,6 +3490,40 @@ class QueryTest extends TestCase
     }
 
     /**
+     * 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.
      *
      * @param string $table

+ 2 - 0
tests/TestCase/ORM/Association/BelongsToTest.php

@@ -77,6 +77,8 @@ class BelongsToTest extends TestCase
             'id' => 'integer',
             'Companies.company_name' => 'string',
             'company_name' => 'string',
+            'Companies__id' => 'integer',
+            'Companies__company_name' => 'string'
         ]);
     }
 

+ 3 - 0
tests/TestCase/ORM/Association/HasOneTest.php

@@ -65,6 +65,9 @@ class HasOneTest extends TestCase
             'first_name' => 'string',
             'Profiles.user_id' => 'integer',
             'user_id' => 'integer',
+            'Profiles__first_name' => 'string',
+            'Profiles__user_id' => 'integer',
+            'Profiles__id' => 'integer',
         ]);
     }
 

+ 11 - 0
tests/TestCase/ORM/EagerLoaderTest.php

@@ -87,6 +87,9 @@ class EagerLoaderTest extends TestCase
             'name' => 'string',
             'clients.phone' => 'string',
             'phone' => 'string',
+            'clients__id' => 'integer',
+            'clients__name' => 'string',
+            'clients__phone' => 'string',
         ]);
         $this->ordersTypeMap = new TypeMap([
             'orders.id' => 'integer',
@@ -95,26 +98,34 @@ class EagerLoaderTest extends TestCase
             'total' => 'string',
             'orders.placed' => 'datetime',
             'placed' => 'datetime',
+            'orders__id' => 'integer',
+            'orders__total' => 'string',
+            'orders__placed' => 'datetime',
         ]);
         $this->orderTypesTypeMap = new TypeMap([
             'orderTypes.id' => 'integer',
             'id' => 'integer',
+            'orderTypes__id' => 'integer',
         ]);
         $this->stuffTypeMap = new TypeMap([
             'stuff.id' => 'integer',
             'id' => 'integer',
+            'stuff__id' => 'integer',
         ]);
         $this->stuffTypesTypeMap = new TypeMap([
             'stuffTypes.id' => 'integer',
             'id' => 'integer',
+            'stuffTypes__id' => 'integer',
         ]);
         $this->companiesTypeMap = new TypeMap([
             'companies.id' => 'integer',
             'id' => 'integer',
+            'companies__id' => 'integer',
         ]);
         $this->categoriesTypeMap = new TypeMap([
             'categories.id' => 'integer',
             'id' => 'integer',
+            'categories__id' => 'integer',
         ]);
     }
 

+ 9 - 1
tests/TestCase/ORM/QueryTest.php

@@ -824,14 +824,20 @@ class QueryTest extends TestCase
         $typeMap = new TypeMap([
             'foo.id' => 'integer',
             'id' => 'integer',
+            'foo__id' => 'integer',
             'articles.id' => 'integer',
+            'articles__id' => 'integer',
             'articles.author_id' => 'integer',
+            'articles__author_id' => 'integer',
             'author_id' => 'integer',
             'articles.title' => 'string',
+            'articles__title' => 'string',
             'title' => 'string',
             'articles.body' => 'text',
+            'articles__body' => 'text',
             'body' => 'text',
             'articles.published' => 'string',
+            'articles__published' => 'string',
             'published' => 'string',
         ]);
 
@@ -2372,10 +2378,12 @@ class QueryTest extends TestCase
             'sql' => $query->sql(),
             'params' => $query->valueBinder()->bindings(),
             'defaultTypes' => [
+                'authors__id' => 'integer',
                 'authors.id' => 'integer',
                 'id' => 'integer',
+                'authors__name' => 'string',
                 'authors.name' => 'string',
-                'name' => 'string'
+                'name' => 'string',
             ],
             'decorators' => 0,
             'executed' => false,

+ 24 - 25
tests/TestCase/ORM/TableTest.php

@@ -82,25 +82,35 @@ class TableTest extends TestCase
         $this->usersTypeMap = new TypeMap([
             'Users.id' => 'integer',
             'id' => 'integer',
+            'Users__id' => 'integer',
             'Users.username' => 'string',
+            'Users__username' => 'string',
             'username' => 'string',
             'Users.password' => 'string',
+            'Users__password' => 'string',
             'password' => 'string',
             'Users.created' => 'timestamp',
+            'Users__created' => 'timestamp',
             'created' => 'timestamp',
             'Users.updated' => 'timestamp',
+            'Users__updated' => 'timestamp',
             'updated' => 'timestamp',
         ]);
         $this->articlesTypeMap = new TypeMap([
             'Articles.id' => 'integer',
+            'Articles__id' => 'integer',
             'id' => 'integer',
             'Articles.title' => 'string',
+            'Articles__title' => 'string',
             'title' => 'string',
             'Articles.author_id' => 'integer',
+            'Articles__author_id' => 'integer',
             'author_id' => 'integer',
             'Articles.body' => 'text',
+            'Articles__body' => 'text',
             'body' => 'text',
             'Articles.published' => 'string',
+            'Articles__published' => 'string',
             'published' => 'string',
         ]);
     }
@@ -1756,7 +1766,7 @@ class TableTest extends TestCase
                 'entityClass' => 'Cake\ORM\Entity',
             ]
         );
-        
+
         $authors->hasMany('Articles', ['saveStrategy' => 'replace']);
 
         $entity = $authors->newEntity([
@@ -1775,9 +1785,9 @@ class TableTest extends TestCase
         $articleId = $entity->articles[0]->id;
         unset($entity->articles[0]);
         $entity->dirty('articles', true);
-        
+
         $authors->save($entity, ['associated' => ['Articles']]);
-        
+
         $this->assertEquals($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
         $this->assertTrue($authors->Articles->exists(['id' => $articleId]));
     }
@@ -1798,7 +1808,7 @@ class TableTest extends TestCase
                 'entityClass' => 'Cake\ORM\Entity',
             ]
         );
-        
+
         $authors->hasMany('Articles', ['saveStrategy' => 'replace']);
 
         $entity = $authors->newEntity([
@@ -1815,9 +1825,9 @@ class TableTest extends TestCase
         $this->assertCount($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']]));
 
         $entity->set('articles', []);
-        
+
         $entity = $authors->save($entity, ['associated' => ['Articles']]);
-        
+
         $this->assertCount(0, $authors->Articles->find('all')->where(['author_id' => $entity['id']]));
     }
 
@@ -1852,13 +1862,13 @@ class TableTest extends TestCase
         $sizeArticles = count($entity->articles);
 
         $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
-        
+
         $articleId = $entity->articles[0]->id;
         unset($entity->articles[0]);
         $entity->dirty('articles', true);
-        
+
         $authors->save($entity, ['associated' => ['Articles']]);
-        
+
         $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
         $this->assertTrue($authors->Articles->exists(['id' => $articleId]));
     }
@@ -1916,9 +1926,9 @@ class TableTest extends TestCase
         $articleId = $entity->articles[0]->id;
         unset($entity->articles[0]);
         $entity->dirty('articles', true);
-        
+
         $authors->save($entity, ['associated' => ['Articles']]);
-        
+
         $this->assertEquals($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
         $this->assertFalse($authors->Articles->exists(['id' => $articleId]));
     }
@@ -1962,7 +1972,7 @@ class TableTest extends TestCase
 
         $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count());
         $this->assertTrue($articles->Comments->exists(['id' => $commentId]));
-        
+
         unset($article->comments[0]);
         $article->dirty('comments', true);
         $article = $articles->save($article, ['associated' => ['Comments']]);
@@ -2011,7 +2021,7 @@ class TableTest extends TestCase
 
         $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count());
         $this->assertTrue($articles->Comments->exists(['id' => $commentId]));
-        
+
         unset($article->comments[0]);
         $article->comments[] = $articles->Comments->newEntity([
             'user_id' => 1,
@@ -3351,18 +3361,7 @@ class TableTest extends TestCase
         $this->assertInstanceOf('Cake\ORM\Query', $result);
         $this->assertNull($result->clause('limit'));
         $expected = new QueryExpression();
-        $expected->typeMap()->defaults([
-            'Users.id' => 'integer',
-            'id' => 'integer',
-            'Users.username' => 'string',
-            'username' => 'string',
-            'Users.password' => 'string',
-            'password' => 'string',
-            'Users.created' => 'timestamp',
-            'created' => 'timestamp',
-            'Users.updated' => 'timestamp',
-            'updated' => 'timestamp',
-        ]);
+        $expected->typeMap()->defaults($this->usersTypeMap->toArray());
         $expected->add(
             ['or' => ['Users.author_id' => 1, 'Users.published' => 'Y']]
         );