laravel / framework

The Laravel Framework.
https://laravel.com
MIT License
32.18k stars 10.89k forks source link

Laravel 10.x Model Attribute Mutators support for both snake_case and camelCase Columns. #48580

Closed jcrawford closed 11 months ago

jcrawford commented 11 months ago

Laravel Version

10.8

PHP Version

8.2.10

Database Driver & Version

MySQL on AWS

Description

I am trying to use Laravel 10.8 mutators using the Attribute return type. The issue I am having is that some of our database columns are snake_case and some are camelCase. Having both column types is something that we currently have to live with as modifying the table with millions of records is not an option at this time. What I am finding is that when I call $model->toArray() the mutators are being executed for our snake_case columns but not for our camelCase columns.

It appears to be failing in the HasAttributes trait on the following lines. It seems to think the camelCase attribute is snake_case and since it does not find the snake case attribute it continues.

I have found that for some reason in the HasAttributes.php file our column of camelCaseName is listed in the static::$snakeAttributes array on the model as snake_case_name, therefore it thinks there is a column snake_case_name on our model so I assume that the mutator is trying to mutate that attribute which does not exist. Currently all of our camelCase attributes are ignored when the mutator is executed when the model is converted to an Array.

The camelCase mutators only seem to work if we manually call $model->camelCaseName. In this case the mutator is executed. I have tried setting the models $snakeAttributes property to false as it is true by default, when I do this it causes issues with our mutators for snake_case columns. Is there a way to make Laravel support both snake and camel case attributes when using mutators?

I have found a work-around but it is tedious and requires all developers to remember they have to do this, we would have to set $snakeAttributes = false which will allow the snakeCase column and mutator to run, however if we then add mutators for our snake_case columns we have to define two methods for it to work. If we define only the snake_case mutator then an error is thrown that it cannot find the camelCaseName() method. If we don't define the camelCaseName mutator method then the mutation is not completed on the snake_case attribute.

In order to make this work for both camelCase and snake_case columns on a model where both will have Attribute mutators you need to set $snakeAttributes = false and then for all snake_case columns that have mutators you will need to do the following

use Illuminate\Database\Eloquent\Model;

class MyModel extends Model {

  public static $snakeAttributes = false;

  /**
   * This is the mutator for the camelCase column
   */
  public function camelCaseColumnName() : Attribute
  {
    return Attribute::make(
      get: fn (float $value) => intval($value * 100),
      set: fn (int $value) => $value / 100.0
    );
  }

 /**
  * This has to be here otherwise the mutation does not happen.
  */
  public function snake_case_column_name(): Attribute
  {
    return $this->snakeCaseColumnName();
  }

  /**
   * This is the camel case mutator for the snake_case_column_name column
   */
  public function snakeCaseColumnName(): Attribute
  {
    return Attribute::make(
      get: fn (float $value) => intval($value * 100),
      set: fn (int $value) => $value / 100.0
    );
  }
}

I have been trying to trace the issue through HasAttributes but was unable to determine why this is happening and ran out of time at the moment.

Steps To Reproduce

Create a database with a table having both snake_case and camelCase columns of type Decimal. Create a model for the table using the code in the description naming your mutators to match your column names. Add the following model property public static $snakeAttributes = false; Start Tinker Find a record and assign to a variable $variable = MyModel::find(1); Call $variable->toArray() and notice the outcome is as explained in the description.

Remove the snake_case mutator from your model Restart Tinker for code changes. Find a record and assign to a varable $variable = MyModel::find(1); Call $variable->toArray() notice there are no errors but the snake_case mutator did not mutate the data.

Remove the camelCase mutator for the snake_case column from your model Restart Tinker for code changes. Find a Find a record and assign to a varable $variable = MyModel::find(1); Call $variable->toArray() notice the erorr that camelCaseName method was not defined.

Remove the $snakeAttributes = false from the model and then Restart Tinker for code changes. Find a record and assign to a variabel $variable = MyModel::find(1); call $variable->toArray() and notice the camelCase column value is not mutated but the snake_case value was.

Eforen commented 11 months ago

Nice catch