Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: onlyChanged is currently true for INSERT
**Describe the bug**

CI4 doesn't respect the default values of entity attributes on INSERT. Currently only the values that have changed are passed to the INSERT query. This means that if the attributes default value is 500, and I try to insert an entity after setting the attributes value to 500, no value is written to the database at all (the default value from the table would be used, as CI would not include the column in the INSERT). 

So [the following statement](https://codeigniter4.github.io/userguide/models/entities.html#working-with-the-entity-class) is not currently accurate:
![image](https://user-images.githubusercontent.com/17505444/107150701-0b71a300-6957-11eb-8a45-2da76767c47e.png)

**CodeIgniter 4 version**

v4.1.1

**Expected behavior, and steps to reproduce if appropriate**
INSERT of entity should include all values, changed and unchanged. INSERT currently only includes changed values. 

**To reproduce:**

- Create an entity class
- Define an attribute with a default value:
```php
namespace App\Entities;

use CodeIgniter\Entity;

class BugExample extends Entity
{
    protected $attributes = [
        'the_attribute' => 500,
    ];

    public function setTheAttribute(int $value)
    {
        $this->attributes['the_attribute'] = 0;
        return $this;
    }

    public function getTheAttribute(): int
    {
        return (int) $this->attributes['the_attribute'];
    }
}
```
- Create an entity, set the attrib value to match the default, insert:
```php
$bugExample = new BugExample();
$bugExample->setTheAttribute(500);
$this->insert($bugExample);
```
- The insert query will not include `the_attribute`

**The root cause:**

- Model `insert()` calls `model->transformDataToArray($data, 'insert');`
- `transformDataToArray()` converts the entity object to an array using `baseModel->objectToArray($data, true, true);`. The 2nd parameter `true` refers to `bool $onlyChanged` - which means object to array conversion will only include changed properties.

**The solution:**

- Change `$data = $this->objectToArray($data, true, true);` to `$data = $this->objectToArray($data, ($type == 'update'), true);` in BaseModel.php:1690. This will mean that all attributes will be included for INSERT and only changed attributes will be included for UPDATE.
- I have tested this, INSERT and UPDATE, and received the expected outcome.

```php
protected function transformDataToArray($data, string $type): array
{
    if (! in_array($type, ['insert', 'update'], true))
    {
        throw new InvalidArgumentException(sprintf('Invalid type "%s" used upon transforming data to array.', $type));
    }

    if (empty($data))
    {
        throw DataException::forEmptyDataset($type);
    }

    // If $data is using a custom class with public or protected
    // properties representing the collection elements, we need to grab
    // them as an array.
    if (is_object($data) && ! $data instanceof stdClass)
    {
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // THE LINE BELOW.... 
            $data = $this->objectToArray($data, true, true);
        // NEEDS TO BE CHANGED TO: 
            $data = $this->objectToArray($data, ($type == 'update'), true);
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    }

    // If it's still a stdClass, go ahead and convert to
    // an array so doProtectFields and other model methods
    // don't have to do special checks.
    if (is_object($data))
    {
        $data = (array) $data;
    }

    // If it's still empty here, means $data is no change or is empty object
    if (empty($data))
    {
        throw DataException::forEmptyDataset($type);
    }

    return $data;
}
```

**Workaround options for those affected:**
1. Set all attribute default values to `null` and set the default values in the database table, or:
2. Set all attribute default values to `null` and create a constructor in the entity class to set your default values:
```php
    public function __construct()
    {
        parent::__construct();
        $this->setTheAttribute(500);
    }
```



Discussion:
Can you send a PR for this?
I can confirm that this is still and issue with 4.1.3 and it is also in the 4.2 branch.

The bug will also show if you read a row from the database, update a couple of fields and then write back a new row. Only the changed values are written. This is for INSERT only.

I would create a PR, but I don't know how to do it.


	/**
	 * Transform data to array
	 *
	 * @param array|object|null $data Data
	 * @param string            $type Type of data (insert|update)
	 *
	 * @return array
	 *
	 * @throws DataException
	 * @throws InvalidArgumentException
	 * @throws ReflectionException
	 */
	protected function transformDataToArray($data, string $type): array
	{
		if (!in_array($type, ['insert', 'update'], true))
		{
			throw new InvalidArgumentException(sprintf('Invalid type "%s" used upon transforming data to array.', $type));
		}

		if (empty($data))
		{
			throw DataException::forEmptyDataset($type);
		}

		// If $data is using a custom class with public or protected
		// properties representing the collection elements, we need to grab
		// them as an array.
		if (is_object($data) && !$data instanceof stdClass)
		{
			$data = $this->objectToArray($data, ($type == 'update'), true);
		}

		// If it's still a stdClass, go ahead and convert to
		// an array so doProtectFields and other model methods
		// don't have to do special checks.
		if (is_object($data))
		{
			$data = (array)$data;
		}

		// If it's still empty here, means $data is no change or is empty object
		if (empty($data))
		{
			throw DataException::forEmptyDataset($type);
		}

		return $data;
	}


Which is the change proposed by @arbitmcdonald, I have tried it and it seems to work correctly. So thanks for that.

Getting this into the next release would be appreciated.


I don't know if this pull request is done correctly as I am not sure of the instructions and I am also not sure if I followed them correctly.

Please let me know if I need to do more to make this work.
Please change your base branch to direct here. In the edit, there's an option to compare between forks. You should select that instead.
Hi @paulbalandan,

I don't really know what I am doing and I don't know how to do what you suggest.

I have not seen an option that obviously says use a fork as a base.

if I am in the right place, the screen shot below shows all the options I have to change the base. The list of branches does not scroll.

![Screenshot 2021-07-30 at 08 58 47](https://user-images.githubusercontent.com/4849251/127621118-0daeccc2-37d2-41a9-b9a5-99c871cc217f.png)

If that's the case, then just open a new pull request.

![image](https://user-images.githubusercontent.com/51850998/127630141-89779f92-935b-4e60-b26a-8c97630b3185.png)

You'll see the `compare across forks` option at the top. Click that. It will change to this:

![image](https://user-images.githubusercontent.com/51850998/127630252-371c39da-2e65-4712-90b8-ba4f26e89742.png)

For the base repo, chose this repo, and select the `4.2` branch. In the head repo, select your fork and PR branch.
Thanks for your help.

OK, I have created a new PR and I think this is what you need.

As before, if it is not, please let me know and I will try to fix it.
OK, a new PR has been created, fingers crossed this time.
Is that all I need to do and can consider this done?
As i said earlier in your PR, you need to write some tests for that change. Please take a look at `tests/system/Models` folder.



Patch:
diff --git a/system/BaseModel.php b/system/BaseModel.php
index 6b9ef3687817..971303664a8e 100644
--- a/system/BaseModel.php
+++ b/system/BaseModel.php
@@ -1578,7 +1578,7 @@ protected function transformDataToArray($data, string $type): array
         // properties representing the collection elements, we need to grab
         // them as an array.
         if (is_object($data) && ! $data instanceof stdClass) {
-            $data = $this->objectToArray($data, true, true);
+            $data = $this->objectToArray($data, ($type === 'update'), true);
         }
 
         // If it's still a stdClass, go ahead and convert to


Test patch:
diff --git a/tests/_support/Entity/User.php b/tests/_support/Entity/User.php
new file mode 100644
index 000000000000..dc8a3954341d
--- /dev/null
+++ b/tests/_support/Entity/User.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * This file is part of CodeIgniter 4 framework.
+ *
+ * (c) CodeIgniter Foundation <admin@codeigniter.com>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Support\Entity;
+
+use CodeIgniter\Entity\Entity;
+
+class User extends Entity
+{
+    protected $attributes = [
+        'country' => 'India',
+    ];
+}
diff --git a/tests/_support/Models/UserObjModel.php b/tests/_support/Models/UserObjModel.php
new file mode 100644
index 000000000000..ea461ec64fb3
--- /dev/null
+++ b/tests/_support/Models/UserObjModel.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * This file is part of CodeIgniter 4 framework.
+ *
+ * (c) CodeIgniter Foundation <admin@codeigniter.com>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Support\Models;
+
+use CodeIgniter\Model;
+
+class UserObjModel extends Model
+{
+    protected $table         = 'user';
+    protected $allowedFields = [
+        'name',
+        'email',
+        'country',
+        'deleted_at',
+    ];
+    protected $returnType     = \Tests\Support\Entity\User::class;
+    protected $useSoftDeletes = true;
+    protected $dateFormat     = 'datetime';
+}
diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php
index 521dec8fbadd..abb246dba424 100644
--- a/tests/system/Models/InsertModelTest.php
+++ b/tests/system/Models/InsertModelTest.php
@@ -15,8 +15,10 @@
 use CodeIgniter\Entity\Entity;
 use CodeIgniter\I18n\Time;
 use stdClass;
+use Tests\Support\Entity\User;
 use Tests\Support\Models\JobModel;
 use Tests\Support\Models\UserModel;
+use Tests\Support\Models\UserObjModel;
 use Tests\Support\Models\WithoutAutoIncrementModel;
 
 /**
@@ -286,4 +288,24 @@ public function testInsertWithSetAndEscape(): void
 
         $this->assertCloseEnough(time(), strtotime($result->created_at));
     }
+
+    /**
+     * @see https://github.com/codeigniter4/CodeIgniter4/issues/4247
+     */
+    public function testInsertWithDefaultValue(): void
+    {
+        $this->createModel(UserObjModel::class);
+
+        $entity             = new User();
+        $entity->name       = 'Mark';
+        $entity->email      = 'mark@example.com';
+        $entity->country    = 'India';  // same as the default
+        $entity->deleted    = 0;
+        $entity->created_at = new Time('now');
+
+        $this->model->insert($entity);
+
+        $id = $this->model->getInsertID();
+        $this->assertSame($entity->country, $this->model->find($id)->country);
+    }
 }