Browse Source

Add createEntity() to make newEntity() validation save.

mscherer 7 years ago
parent
commit
80772ec48f

+ 13 - 2
src/Datasource/RepositoryInterface.php

@@ -162,6 +162,17 @@ interface RepositoryInterface
     public function delete(EntityInterface $entity, $options = []): bool;
 
     /**
+     * This creates a new entity object.
+     *
+     * Careful: This does not trigger any field validation.
+     * This entity can be persisted without validation error as empty record.
+     * Always patch in required fields before saving.
+     *
+     * @return \Cake\Datasource\EntityInterface
+     */
+    public function createEntity(): EntityInterface;
+
+    /**
      * Create a new entity + associated entities from an array.
      *
      * This is most useful when hydrating request data back into entities.
@@ -175,11 +186,11 @@ interface RepositoryInterface
      * on the primary key data existing in the database when the entity
      * is saved. Until the entity is saved, it will be a detached record.
      *
-     * @param array|null $data The data to build an entity with.
+     * @param array $data The data to build an entity with.
      * @param array $options A list of options for the object hydration.
      * @return \Cake\Datasource\EntityInterface
      */
-    public function newEntity(?array $data = null, array $options = []): EntityInterface;
+    public function newEntity(array $data, array $options = []): EntityInterface;
 
     /**
      * Create a list of entities + associated entities from an array.

+ 5 - 3
src/ORM/Behavior/Translate/TranslateStrategyTrait.php

@@ -161,11 +161,13 @@ trait TranslateStrategyTrait
                     }
                     foreach ($value as $language => $fields) {
                         if (!isset($translations[$language])) {
-                            $translations[$language] = $this->table->newEntity();
+                            $translations[$language] = $this->table->createEntity();
                         }
                         $marshaller->merge($translations[$language], $fields, $options);
-                        if ((bool)$translations[$language]->getErrors()) {
-                            $errors[$language] = $translations[$language]->getErrors();
+                        /** @var \Cake\Datasource\EntityInterface $entity */
+                        $entity = $translations[$language];
+                        if ((bool)$entity->getErrors()) {
+                            $errors[$language] = $entity->getErrors();
                         }
                     }
                     // Set errors into the root entity, so validation errors

+ 16 - 7
src/ORM/Table.php

@@ -1528,6 +1528,8 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      *   is persisted.
      * @param array $options The options to use when saving.
      * @return \Cake\Datasource\EntityInterface|array An entity.
+     * @throws \Cake\ORM\Exception\PersistenceFailedException
+     * @throws \InvalidArgumentException
      */
     protected function _processFindOrCreate($search, ?callable $callback = null, $options = [])
     {
@@ -1548,7 +1550,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
         if ($row !== null) {
             return $row;
         }
-        $entity = $this->newEntity();
+        $entity = $this->createEntity();
         if ($options['defaults'] && is_array($search)) {
             $accessibleFields = array_combine(array_keys($search), array_fill(0, count($search), true));
             $entity = $this->patchEntity($entity, $search, ['accessibleFields' => $accessibleFields]);
@@ -2396,6 +2398,18 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * {@inheritDoc}
      *
+     * @return \Cake\Datasource\EntityInterface
+     */
+    public function createEntity(): EntityInterface
+    {
+        $class = $this->getEntityClass();
+
+        return new $class([], ['source' => $this->getRegistryAlias()]);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
      * By default all the associations on this table will be hydrated. You can
      * limit which associations are built, or include deeper associations
      * using the options parameter:
@@ -2447,13 +2461,8 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * You can use the `Model.beforeMarshal` event to modify request data
      * before it is converted into entities.
      */
-    public function newEntity(?array $data = null, array $options = []): EntityInterface
+    public function newEntity(array $data, array $options = []): EntityInterface
     {
-        if ($data === null) {
-            $class = $this->getEntityClass();
-
-            return new $class([], ['source' => $this->getRegistryAlias()]);
-        }
         if (!isset($options['associated'])) {
             $options['associated'] = $this->_associations->keys();
         }

+ 1 - 1
src/View/Form/EntityContext.php

@@ -356,7 +356,7 @@ class EntityContext implements ContextInterface
             if (!$isLast && $next === null && $prop !== '_ids') {
                 $table = $this->_getTable($path);
 
-                return $table->newEntity();
+                return $table->createEntity();
             }
 
             $isTraversable = (

+ 32 - 17
tests/TestCase/ORM/TableTest.php

@@ -1805,6 +1805,7 @@ class TableTest extends TestCase
      */
     public function testImplementedEvents()
     {
+        /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
         $table = $this->getMockBuilder(Table::class)
             ->setMethods([
                 'buildValidator',
@@ -1849,7 +1850,7 @@ class TableTest extends TestCase
         $this->assertSame($entity, $table->save($entity));
         $this->assertEquals($entity->id, self::$nextUserId);
 
-        $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
+        $row = $table->find()->where(['id' => self::$nextUserId])->first();
         $this->assertEquals($entity->toArray(), $row->toArray());
     }
 
@@ -1874,6 +1875,7 @@ class TableTest extends TestCase
      */
     public function testSaveNewEntityNoExists()
     {
+        /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
         $table = $this->getMockBuilder(Table::class)
             ->setMethods(['exists'])
             ->setConstructorArgs([[
@@ -1899,6 +1901,7 @@ class TableTest extends TestCase
     public function testSavePrimaryKeyEntityExists()
     {
         $this->skipIfSqlServer();
+        /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
         $table = $this->getMockBuilder(Table::class)
             ->setMethods(['exists'])
             ->setConstructorArgs([[
@@ -1923,6 +1926,7 @@ class TableTest extends TestCase
     public function testSavePrimaryKeyEntityNoExists()
     {
         $this->skipIfSqlServer();
+        /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
         $table = $this->getMockBuilder(Table::class)
             ->setMethods(['exists'])
             ->setConstructorArgs([[
@@ -1977,7 +1981,7 @@ class TableTest extends TestCase
             'created' => new Time('2013-10-10 00:00'),
             'updated' => new Time('2013-10-10 00:00'),
         ]);
-        $listener = function ($e, $entity, $options) use ($data) {
+        $listener = function ($event, EntityInterface $entity, $options) use ($data) {
             $this->assertSame($data, $entity);
             $entity->set('password', 'foo');
         };
@@ -2003,10 +2007,10 @@ class TableTest extends TestCase
             'created' => new Time('2013-10-10 00:00'),
             'updated' => new Time('2013-10-10 00:00'),
         ]);
-        $listener1 = function ($e, $entity, $options) {
+        $listener1 = function ($event, $entity, $options) {
             $options['crazy'] = true;
         };
-        $listener2 = function ($e, $entity, $options) {
+        $listener2 = function ($event, $entity, $options) {
             $this->assertTrue($options['crazy']);
         };
         $table->getEventManager()->on('Model.beforeSave', $listener1);
@@ -2033,8 +2037,8 @@ class TableTest extends TestCase
             'created' => new Time('2013-10-10 00:00'),
             'updated' => new Time('2013-10-10 00:00'),
         ]);
-        $listener = function ($e, $entity) {
-            $e->stopPropagation();
+        $listener = function (EventInterface $event, $entity) {
+            $event->stopPropagation();
 
             return $entity;
         };
@@ -3218,27 +3222,38 @@ class TableTest extends TestCase
         $table = $this->getTableLocator()->get('TestPlugin.Authors');
 
         $existingAuthor = $table->find()->first();
-        $newAuthor = $table->newEntity();
+        $newAuthor = $table->createEntity();
 
         $this->assertEquals('TestPlugin.Authors', $existingAuthor->getSource());
         $this->assertEquals('TestPlugin.Authors', $newAuthor->getSource());
     }
 
     /**
-     * Tests that calling an entity with an empty array will run validation
-     * whereas calling it with no parameters will not run any validation.
+     * Tests that calling an entity with an empty array will run validation.
      *
      * @return void
      */
     public function testNewEntityAndValidation()
     {
         $table = $this->getTableLocator()->get('Articles');
-        $validator = $table->getValidator()->requirePresence('title');
+        $table->getValidator()->requirePresence('title');
+
         $entity = $table->newEntity([]);
         $errors = $entity->getErrors();
         $this->assertNotEmpty($errors['title']);
+    }
+
+    /**
+     * Tests that creating an entity will not run any validation.
+     *
+     * @return void
+     */
+    public function testCreateEntityAndValidation()
+    {
+        $table = $this->getTableLocator()->get('Articles');
+        $table->getValidator()->requirePresence('title');
 
-        $entity = $table->newEntity();
+        $entity = $table->createEntity();
         $this->assertEmpty($entity->getErrors());
     }
 
@@ -5710,12 +5725,12 @@ class TableTest extends TestCase
     {
         $articles = $this->getTableLocator()->get('Articles');
 
-        $article = $articles->findOrCreate(function ($query) {
-            $this->assertInstanceOf('Cake\ORM\Query', $query);
+        $article = $articles->findOrCreate(function (Query $query) {
+            $this->assertInstanceOf(Query::class, $query);
             $query->where(['title' => 'Find Something New']);
             $this->assertFalse($this->connection->inTransaction());
         }, function ($article) {
-            $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
+            $this->assertInstanceOf(EntityInterface::class, $article);
             $this->assertFalse($this->connection->inTransaction());
             $article->title = 'Success';
         }, ['atomic' => false]);
@@ -6024,18 +6039,18 @@ class TableTest extends TestCase
     }
 
     /**
-     * Tests that calling newEntity() on a table sets the right source alias
+     * Tests that calling createEntity() on a table sets the right source alias.
      *
      * @return void
      */
     public function testSetEntitySource()
     {
         $table = $this->getTableLocator()->get('Articles');
-        $this->assertEquals('Articles', $table->newEntity()->getSource());
+        $this->assertEquals('Articles', $table->createEntity()->getSource());
 
         $this->loadPlugins(['TestPlugin']);
         $table = $this->getTableLocator()->get('TestPlugin.Comments');
-        $this->assertEquals('TestPlugin.Comments', $table->newEntity()->getSource());
+        $this->assertEquals('TestPlugin.Comments', $table->createEntity()->getSource());
     }
 
     /**