Browse Source

adds unit tests, callable search argument and optional transactions

thinkingmedia 9 years ago
parent
commit
4b5d450ccc
2 changed files with 220 additions and 29 deletions
  1. 69 17
      src/ORM/Table.php
  2. 151 12
      tests/TestCase/ORM/TableTest.php

+ 69 - 17
src/ORM/Table.php

@@ -1205,8 +1205,8 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * Finds an existing record or creates a new one.
      *
-     * Using the attributes defined in $search a find() will be done to locate
-     * an existing record. If records matches the conditions, the first record
+     * A find() will be done to locate an existing record using the attributes
+     * defined in $search. If records matches the conditions, the first record
      * will be returned.
      *
      * If no record can be found, a new entity will be created
@@ -1214,27 +1214,79 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * called allowing you to define additional default values. The new
      * entity will be saved and returned.
      *
-     * @param array $search The criteria to find existing records by.
+     * If the $search properties do not match the properties of the entity, then you
+     * should disable defaults and define all default values using the callback.
+     *
+     * If your find conditions require custom order, associations or conditions. The $search
+     * parameter can be a callable that takes the Query as the argument. Allowing you to
+     * customize the find results.
+     *
+     * ### Options
+     *
+     * The options array is passed to the save method with exception to the following keys:
+     *
+     * - atomic: Whether to execute the methods for find, save and callbacks inside a database
+     *   transaction (default: true)
+     * - defaults: Whether to use the search criteria as default values for the new entity (default: true)
+     *
+     * @param array|callable $search The criteria to find an existing record by, or a callable tha will
+     *   customize the find query.
      * @param callable|null $callback A callback that will be invoked for newly
      *   created entities. This callback will be called *before* the entity
      *   is persisted.
-     * @return \Cake\Datasource\EntityInterface An entity.
+     * @param array $options The options to use when saving.
+     * @return EntityInterface An entity.
      */
-    public function findOrCreate($search, callable $callback = null)
+    public function findOrCreate($search, callable $callback = null, $options = [])
     {
-        return $this->connection()->transactional(function () use ($search, $callback) {
-            $query = $this->find()->where($search);
-            $row = $query->first();
-            if ($row) {
-                return $row;
-            }
-            $entity = $this->newEntity();
+        $options = array_merge([
+                'atomic' => true,
+                'defaults' => true
+            ], $options);
+
+        if ($options['atomic']) {
+            return $this->connection()->transactional(function () use ($search, $callback, $options) {
+                return $this->_processFindOrCreate($search, $callback, $options);
+            });
+        } else {
+            return $this->_processFindOrCreate($search, $callback, $options);
+        }
+    }
+
+    /**
+     * Performs the actual find and/or create of an entity based on the passed options.
+     *
+     * @param array|callable $search The criteria to find an existing record by, or a callable tha will
+     *   customize the find query.
+     * @param callable|null $callback A callback that will be invoked for newly
+     *   created entities. This callback will be called *before* the entity
+     *   is persisted.
+     * @param array $options The options to use when saving.
+     * @return EntityInterface An entity.
+     */
+    protected function _processFindOrCreate($search, callable $callback = null, $options)
+    {
+        $query = $this->find();
+        if(is_callable($search)) {
+            call_user_func($search, $query);
+        } else if(is_array($search)) {
+            $query->where($search);
+        } else {
+            throw new InvalidArgumentException('Search criteria must be an array or callable');
+        }
+        $row = $query->first();
+        if ($row) {
+            return $row;
+        }
+        $entity = $this->newEntity();
+        if($options['defaults'] && is_array($search)) {
             $entity->set($search, ['guard' => false]);
-            if ($callback) {
-                $callback($entity);
-            }
-            return $this->save($entity) ?: $entity;
-        });
+        }
+        if ($callback) {
+            $callback($entity);
+        }
+        unset($options['defaults']);
+        return $this->save($entity, $options) ?: $entity;
     }
 
     /**

+ 151 - 12
tests/TestCase/ORM/TableTest.php

@@ -22,6 +22,7 @@ use Cake\Database\Exception;
 use Cake\Database\Expression\QueryExpression;
 use Cake\Database\TypeMap;
 use Cake\Datasource\ConnectionManager;
+use Cake\Datasource\EntityInterface;
 use Cake\Event\Event;
 use Cake\Event\EventManager;
 use Cake\I18n\Time;
@@ -5355,26 +5356,43 @@ class TableTest extends TestCase
     }
 
     /**
-     * Test the findOrCreate method.
+     * Test that findOrCreate creates a new entity, and then finds that entity.
      *
      * @return void
      */
-    public function testFindOrCreate()
+    public function testFindOrCreateNewEntity()
     {
         $articles = TableRegistry::get('Articles');
 
-        $article = $articles->findOrCreate(['title' => 'Not there'], function ($article) {
+        $callback_executed = false;
+        $first_article = $articles->findOrCreate(['title' => 'Not there'], function ($article) use (&$callback_executed) {
+            $this->assertTrue($article instanceof EntityInterface);
             $article->body = 'New body';
+            $callback_executed = true;
         });
-        $this->assertFalse($article->isNew());
-        $this->assertNotNull($article->id);
-        $this->assertEquals('Not there', $article->title);
-        $this->assertEquals('New body', $article->body);
+        $this->assertTrue($callback_executed);
+        $this->assertFalse($first_article->isNew());
+        $this->assertNotNull($first_article->id);
+        $this->assertEquals('Not there', $first_article->title);
+        $this->assertEquals('New body', $first_article->body);
 
-        $article = $articles->findOrCreate(['title' => 'Not there']);
-        $this->assertFalse($article->isNew());
-        $this->assertNotNull($article->id);
-        $this->assertEquals('Not there', $article->title);
+        $second_article = $articles->findOrCreate(['title' => 'Not there'], function ($article) {
+            $this->fail('Should not be called for existing entities.');
+        });
+        $this->assertFalse($second_article->isNew());
+        $this->assertNotNull($second_article->id);
+        $this->assertEquals('Not there', $second_article->title);
+        $this->assertEquals($first_article->id, $second_article->id);
+    }
+
+    /**
+     * Test that findOrCreate finds fixture data.
+     *
+     * @return void
+     */
+    public function testFindOrCreateExistingEntity()
+    {
+        $articles = TableRegistry::get('Articles');
 
         $article = $articles->findOrCreate(['title' => 'First Article'], function ($article) {
             $this->fail('Should not be called for existing entities.');
@@ -5382,13 +5400,27 @@ class TableTest extends TestCase
         $this->assertFalse($article->isNew());
         $this->assertNotNull($article->id);
         $this->assertEquals('First Article', $article->title);
+    }
+
+    /**
+     * Test that findOrCreate uses the search conditions as defaults for new entity.
+     *
+     * @return void
+     */
+    public function testFindOrCreateDefaults()
+    {
+        $articles = TableRegistry::get('Articles');
 
+        $callback_executed = false;
         $article = $articles->findOrCreate(
             ['author_id' => 2, 'title' => 'First Article'],
-            function ($article) {
+            function ($article) use (&$callback_executed) {
+                $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
                 $article->set(['published' => 'N', 'body' => 'New body']);
+                $callback_executed = true;
             }
         );
+        $this->assertTrue($callback_executed);
         $this->assertFalse($article->isNew());
         $this->assertNotNull($article->id);
         $this->assertEquals('First Article', $article->title);
@@ -5398,6 +5430,113 @@ class TableTest extends TestCase
     }
 
     /**
+     * Test that findOrCreate adds new entity without using a callback.
+     *
+     * @return void
+     */
+    public function testFindOrCreateNoCallable()
+    {
+        $articles = TableRegistry::get('Articles');
+
+        $article = $articles->findOrCreate(['title'=>'Just Something New']);
+        $this->assertFalse($article->isNew());
+        $this->assertNotNull($article->id);
+        $this->assertEquals('Just Something New', $article->title);
+    }
+
+    /**
+     * Test that findOrCreate executes search conditions as a callable.
+     *
+     * @return void
+     */
+    public function testFindOrCreateSearchCallable()
+    {
+        $articles = TableRegistry::get('Articles');
+
+        $called_1 = false;
+        $called_2 = false;
+        $article = $articles->findOrCreate(function ($query) use (&$called_1) {
+            $this->assertInstanceOf('Cake\ORM\Query', $query);
+            $query->where(['title' => 'Something Else']);
+            $called_1 = true;
+        }, function ($article) use (&$called_2) {
+            $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
+            $article->title = 'Set Defaults Here';
+            $called_2 = true;
+        });
+        $this->assertTrue($called_1);
+        $this->assertTrue($called_2);
+        $this->assertFalse($article->isNew());
+        $this->assertNotNull($article->id);
+        $this->assertEquals('Set Defaults Here', $article->title);
+    }
+
+    /**
+     * Test that findOrCreate options disable defaults.
+     *
+     * @return void
+     */
+    public function testFindOrCreateNoDefaults()
+    {
+        $articles = TableRegistry::get('Articles');
+
+        $article = $articles->findOrCreate(['title' => 'A New Article', 'published' => 'Y'], function ($article) {
+            $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
+            $article->title = 'A Different Title';
+        }, ['defaults' => false]);
+        $this->assertFalse($article->isNew());
+        $this->assertNotNull($article->id);
+        $this->assertEquals('A Different Title', $article->title);
+        $this->assertNull($article->published, 'Expected Null since defaults are disabled.');
+    }
+
+    /**
+     * Test that findOrCreate executes callable inside transaction.
+     *
+     * @return void
+     */
+    public function testFindOrCreateTransactions()
+    {
+        $articles = TableRegistry::get('Articles');
+
+        $article = $articles->findOrCreate(function ($query) {
+            $this->assertInstanceOf('Cake\ORM\Query', $query);
+            $query->where(['title' => 'Find Something New']);
+            $this->assertTrue($this->connection->inTransaction());
+        }, function ($article) {
+            $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
+            $this->assertTrue($this->connection->inTransaction());
+            $article->title = 'Success';
+        });
+        $this->assertFalse($article->isNew());
+        $this->assertNotNull($article->id);
+        $this->assertEquals('Success', $article->title);
+    }
+
+    /**
+     * Test that findOrCreate executes callable without transaction.
+     *
+     * @return void
+     */
+    public function testFindOrCreateNoTransaction()
+    {
+        $articles = TableRegistry::get('Articles');
+
+        $article = $articles->findOrCreate(function ($query) {
+            $this->assertInstanceOf('Cake\ORM\Query', $query);
+            $query->where(['title' => 'Find Something New']);
+            $this->assertFalse($this->connection->inTransaction());
+        }, function ($article) {
+            $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
+            $this->assertFalse($this->connection->inTransaction());
+            $article->title = 'Success';
+        }, ['atomic' => false]);
+        $this->assertFalse($article->isNew());
+        $this->assertNotNull($article->id);
+        $this->assertEquals('Success', $article->title);
+    }
+
+    /**
      * Test that creating a table fires the initialize event.
      *
      * @return void