Browse Source

Merge pull request #9143 from burzum/feature/entity-option-builder

Adding an option builder for the save options
José Lorenzo Rodríguez 9 years ago
parent
commit
68d0b4a14c

+ 219 - 0
src/ORM/SaveOptionsBuilder.php

@@ -0,0 +1,219 @@
+<?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.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\ORM;
+
+use ArrayObject;
+use RuntimeException;
+
+/**
+ * OOP style Save Option Builder.
+ *
+ * This allows you to build options to save entities in a OOP style and helps
+ * you to avoid mistakes by validating the options as you build them.
+ *
+ * @see \Cake\Datasource\RulesChecker
+ */
+class SaveOptionsBuilder extends ArrayObject
+{
+
+    use AssociationsNormalizerTrait;
+
+    /**
+     * Options
+     *
+     * @var array
+     */
+    protected $_options = [];
+
+    /**
+     * Table object.
+     *
+     * @var \Cake\ORM\Table;
+     */
+    protected $_table;
+
+    /**
+     * Constructor.
+     *
+     * @param \Cake\ORM\Table $table A table instance.
+     * @param array $options Options to parse when instantiating.
+     */
+    public function __construct(Table $table, array $options = [])
+    {
+        $this->_table = $table;
+        $this->parseArrayOptions($options);
+    }
+
+    /**
+     * Takes an options array and populates the option object with the data.
+     *
+     * This can be used to turn an options array into the object.
+     *
+     * @throws \InvalidArgumentException If a given option key does not exist.
+     * @param array $array Options array.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function parseArrayOptions($array)
+    {
+        foreach ($array as $key => $value) {
+            $this->{$key}($value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set associated options.
+     *
+     * @param string|array $associated String or array of associations.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function associated($associated)
+    {
+        $associated = $this->_normalizeAssociations($associated);
+        $this->_associated($this->_table, $associated);
+        $this->_options['associated'] = $associated;
+
+        return $this;
+    }
+
+    /**
+     * Checks that the associations exists recursively.
+     *
+     * @param \Cake\ORM\Table $table Table object.
+     * @param array $associations An associations array.
+     * @return void
+     */
+    protected function _associated(Table $table, array $associations)
+    {
+        foreach ($associations as $key => $associated) {
+            if (is_int($key)) {
+                $this->_checkAssociation($table, $associated);
+                continue;
+            }
+            $this->_checkAssociation($table, $key);
+            if (isset($associated['associated'])) {
+                $this->_associated($table->association($key)->target(), $associated['associated']);
+                continue;
+            }
+        }
+    }
+
+    /**
+     * Checks if an association exists.
+     *
+     * @throws \RuntimeException If no such association exists for the given table.
+     * @param \Cake\ORM\Table $table Table object.
+     * @param string $association Association name.
+     * @return void
+     */
+    protected function _checkAssociation(Table $table, $association)
+    {
+        if (!$table->associations()->has($association)) {
+            throw new RuntimeException(sprintf('Table `%s` is not associated with `%s`', get_class($table), $association));
+        }
+    }
+
+    /**
+     * Set the guard option.
+     *
+     * @param bool $guard Guard the properties or not.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function guard($guard)
+    {
+        $this->_options['guard'] = (bool)$guard;
+
+        return $this;
+    }
+
+    /**
+     * Set the validation rule set to use.
+     *
+     * @param string $validate Name of the validation rule set to use.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function validate($validate)
+    {
+        $this->_table->validator($validate);
+        $this->_options['validate'] = $validate;
+
+        return $this;
+    }
+
+    /**
+     * Set check existing option.
+     *
+     * @param bool $checkExisting Guard the properties or not.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function checkExisting($checkExisting)
+    {
+        $this->_options['checkExisting'] = (bool)$checkExisting;
+
+        return $this;
+    }
+
+    /**
+     * Option to check the rules.
+     *
+     * @param bool $checkRules Check the rules or not.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function checkRules($checkRules)
+    {
+        $this->_options['checkRules'] = (bool)$checkRules;
+
+        return $this;
+    }
+
+    /**
+     * Sets the atomic option.
+     *
+     * @param bool $atomic Atomic or not.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function atomic($atomic)
+    {
+        $this->_options['atomic'] = (bool)$atomic;
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->_options;
+    }
+
+    /**
+     * Setting custom options.
+     *
+     * @param string $option Option key.
+     * @param mixed $value Option value.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function set($option, $value)
+    {
+        if (method_exists($this, $option)) {
+            return $this->{$option}($value);
+        }
+        $this->_options[$option] = $value;
+
+        return $this;
+    }
+}

+ 15 - 0
src/ORM/Table.php

@@ -1466,6 +1466,10 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      */
     public function save(EntityInterface $entity, $options = [])
     {
+        if ($options instanceof SaveOptionsBuilder) {
+            $options = $options->toArray();
+        }
+
         $options = new ArrayObject($options + [
             'atomic' => true,
             'associated' => true,
@@ -2391,6 +2395,17 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     }
 
     /**
+     * Gets a SaveOptionsBuilder instance.
+     *
+     * @param array $options Options to parse by the builder.
+     * @return \Cake\ORM\SaveOptionsBuilder
+     */
+    public function getSaveOptionsBuilder(array $options = [])
+    {
+        return new SaveOptionsBuilder($this, $options);
+    }
+
+    /**
      * Loads the specified associations in the passed entity or list of entities
      * by executing extra queries in the database and merging the results in the
      * appropriate properties.

+ 227 - 0
tests/TestCase/ORM/SaveOptionsBuilderTest.php

@@ -0,0 +1,227 @@
+<?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.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\ORM;
+
+use Cake\Datasource\ConnectionManager;
+use Cake\ORM\Entity;
+use Cake\ORM\SaveOptionsBuilder;
+use Cake\ORM\Table;
+use Cake\ORM\TableRegistry;
+use Cake\TestSuite\TestCase;
+
+/**
+ * SaveOptionsBuilder test case.
+ */
+class SaveOptionsBuilderTest extends TestCase
+{
+
+    public $fixtures = [
+        'core.articles',
+        'core.authors',
+        'core.comments',
+        'core.users',
+    ];
+
+    /**
+     * setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $this->connection = ConnectionManager::get('test');
+        $this->table = new Table([
+            'table' => 'articles',
+            'connection' => $this->connection,
+        ]);
+
+        $this->table->belongsTo('Authors');
+        $this->table->hasMany('Comments');
+        $this->table->Comments->belongsTo('Users');
+    }
+
+    /**
+     * testAssociatedChecks
+     *
+     * @return void
+     */
+    public function testAssociatedChecks()
+    {
+        $expected = [
+            'associated' => [
+                'Comments' => []
+            ]
+        ];
+        $builder = new SaveOptionsBuilder($this->table);
+        $builder->associated(
+            'Comments'
+        );
+        $result = $builder->toArray();
+        $this->assertEquals($expected, $result);
+
+        $expected = [
+            'associated' => [
+                'Comments' => [
+                    'associated' => [
+                        'Users' => []
+                    ]
+                ]
+            ]
+        ];
+        $builder = new SaveOptionsBuilder($this->table);
+        $builder->associated(
+            'Comments.Users'
+        );
+        $result = $builder->toArray();
+        $this->assertEquals($expected, $result);
+
+        try {
+            $builder = new SaveOptionsBuilder($this->table);
+            $builder->associated(
+                'Comments.DoesNotExist'
+            );
+            $this->fail('No \RuntimeException throw for invalid association!');
+        } catch (\RuntimeException $e) {
+        }
+
+        $expected = [
+            'associated' => [
+                'Comments' => [
+                    'associated' => [
+                        (int)0 => 'Users'
+                    ]
+                ]
+            ]
+        ];
+        $builder = new SaveOptionsBuilder($this->table);
+        $builder->associated([
+            'Comments' => [
+                'associated' => [
+                    'Users'
+                ]
+            ]
+        ]);
+        $result = $builder->toArray();
+        $this->assertEquals($expected, $result);
+
+        $expected = [
+            'associated' => [
+                'Authors' => [],
+                'Comments' => [
+                    'associated' => [
+                        (int)0 => 'Users'
+                    ]
+                ]
+            ]
+        ];
+        $builder = new SaveOptionsBuilder($this->table);
+        $builder->associated([
+            'Authors',
+            'Comments' => [
+                'associated' => [
+                    'Users'
+                ]
+            ]
+        ]);
+        $result = $builder->toArray();
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * testBuilder
+     *
+     * @return void
+     */
+    public function testBuilder()
+    {
+        $expected = [
+            'associated' => [
+                'Authors' => [],
+                'Comments' => [
+                    'associated' => [
+                        (int)0 => 'Users'
+                    ]
+                ]
+            ],
+            'guard' => false,
+            'checkRules' => false,
+            'checkExisting' => true,
+            'atomic' => true,
+            'validate' => 'default'
+        ];
+
+        $builder = new SaveOptionsBuilder($this->table);
+        $builder->associated([
+            'Authors',
+            'Comments' => [
+                'associated' => [
+                    'Users'
+                ]
+            ]
+        ])
+        ->guard(false)
+        ->checkRules(false)
+        ->checkExisting(true)
+        ->atomic(true)
+        ->validate('default');
+
+        $result = $builder->toArray();
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * testParseOptionsArray
+     *
+     * @return void
+     */
+    public function testParseOptionsArray()
+    {
+        $options = [
+            'associated' => [
+                'Authors' => [],
+                'Comments' => [
+                    'associated' => [
+                        (int)0 => 'Users'
+                    ]
+                ]
+            ],
+            'guard' => false,
+            'checkRules' => false,
+            'checkExisting' => true,
+            'atomic' => true,
+            'validate' => 'default'
+        ];
+
+        $builder = new SaveOptionsBuilder($this->table, $options);
+        $this->assertEquals($options, $builder->toArray());
+    }
+
+    /**
+     * testSettingCustomOptions
+     *
+     * @return void
+     */
+    public function testSettingCustomOptions()
+    {
+        $expected = [
+            'myOption' => true,
+        ];
+
+        $builder = new SaveOptionsBuilder($this->table);
+        $builder->set('myOption', true);
+        $this->assertEquals($expected, $builder->toArray());
+    }
+}

+ 66 - 0
tests/TestCase/ORM/TableTest.php

@@ -32,6 +32,7 @@ use Cake\ORM\Association\HasMany;
 use Cake\ORM\Entity;
 use Cake\ORM\Query;
 use Cake\ORM\RulesChecker;
+use Cake\ORM\SaveOptionsBuilder;
 use Cake\ORM\Table;
 use Cake\ORM\TableRegistry;
 use Cake\TestSuite\TestCase;
@@ -3856,6 +3857,59 @@ class TableTest extends TestCase
     }
 
     /**
+     * Test that a save call takes a SaveOptionBuilder object as well.
+     *
+     * @group save
+     * @return void
+     */
+    public function testSaveWithOptionBuilder()
+    {
+        $articles = new Table([
+            'table' => 'articles',
+            'connection' => $this->connection,
+        ]);
+        $articles->belongsTo('Authors');
+
+        $optionBuilder = new SaveOptionsBuilder($articles, [
+            'associated' => [
+                'Authors'
+            ]
+        ]);
+
+        $entity = $articles->newEntity([
+            'title' => 'test save options',
+            'author' => [
+                'name' => 'author name'
+            ]
+        ]);
+
+        $articles->save($entity, $optionBuilder);
+        $this->assertFalse($entity->isNew());
+        $this->assertEquals('test save options', $entity->title);
+        $this->assertNotEmpty($entity->id);
+        $this->assertNotEmpty($entity->author->id);
+        $this->assertEquals('author name', $entity->author->name);
+
+        $entity = $articles->newEntity([
+            'title' => 'test save options 2',
+            'author' => [
+                'name' => 'author name'
+            ]
+        ]);
+
+        $optionBuilder = new SaveOptionsBuilder($articles, [
+            'associated' => []
+        ]);
+
+        $articles->save($entity, $optionBuilder);
+        $this->assertFalse($entity->isNew());
+        $this->assertEquals('test save options 2', $entity->title);
+        $this->assertNotEmpty($entity->id);
+        $this->assertEmpty($entity->author->id);
+        $this->assertTrue($entity->author->isNew());
+    }
+
+    /**
      * Tests that saving a persisted and clean entity will is a no-op
      *
      * @group save
@@ -6054,6 +6108,18 @@ class TableTest extends TestCase
     }
 
     /**
+     * Test getting the save options builder.
+     *
+     * @return void
+     */
+    public function getSaveOptionsBuilder()
+    {
+        $table = TableRegistry::get('Authors');
+        $result = $table->getSaveOptionsBuilder();
+        $this->assertInstanceOf('Cake\ORM\SaveOptionsBuilder', $result);
+    }
+
+    /**
      * Helper method to skip tests when connection is SQLServer.
      *
      * @return void