Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply casts when serializing embedded relations #2762

Open
florianJacques opened this issue Mar 12, 2024 · 3 comments
Open

Apply casts when serializing embedded relations #2762

florianJacques opened this issue Mar 12, 2024 · 3 comments

Comments

@florianJacques
Copy link
Contributor

florianJacques commented Mar 12, 2024

  • Laravel-mongodb Version: 4.2-dev
  • PHP Version: 8.3.3

Description:

Hello,
would it be possible to automatically cast embedded relations when calling the database?

Steps to reproduce

Add EmbedsMany relation on parent model.
Add ObjectIdCast on Child Model BSON property?

Expected behaviour

When calling toArray() or toJson() on the parent model, return all casted embedded properties.

Actual behaviour

All BSON types are incorrectly serialized

{
  "createdAt": {
    "$date": {
      "$numberLong": "1709908362348"
    }
  },
  "_id": {
    "$oid": "65eb218a42ad885e53013b04"
  }
}
Logs: Insert log.txt here (if necessary)
@florianJacques
Copy link
Contributor Author

florianJacques commented Mar 12, 2024

I have created two cast

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;

use MongoDB\Laravel\Eloquent\Model;

class EmbedManyCast implements Castable
{
    public static function castUsing(array $arguments): CastsAttributes
    {
        return new class($arguments) implements CastsAttributes
        {
            protected array $arguments;

            public function __construct(array $arguments)
            {
                $this->arguments = $arguments;
            }

            public function get($model, $key, $value, $attributes): ?Collection
            {
                if (! is_array($value)) {
                    return null;
                }

                $modelClass = $this->arguments[0];

                if (! is_a($modelClass, Model::class, true)) {
                    throw new \InvalidArgumentException('The provided class must extend ['.Model::class.'].');
                }

                return (new Collection($value))->map(function ($item) use ($modelClass) {
                    return (new $modelClass)->setRawAttributes($item);
                });
            }

            public function set($model, $key, $value, $attributes)
            {
                return $value;
            }
        };
    }

    /**
     * Specify the collection for the cast.
     *
     * @param  string $class
     * @return string
     */
    public static function using(string $class): string
    {
        return static::class.':'.$class;
    }
}
<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

use MongoDB\Laravel\Eloquent\Model;

class EmbedOneCast implements Castable
{
    public static function castUsing(array $arguments): CastsAttributes
    {
        return new class($arguments) implements CastsAttributes
        {
            protected array $arguments;

            public function __construct(array $arguments)
            {
                $this->arguments = $arguments;
            }

            public function get($model, $key, $value, $attributes): ?Model
            {
                if (! $value || ! is_array($value)) {
                    return null;
                }

                $modelClass = $this->arguments[0];

                if (! is_a($modelClass, Model::class, true)) {
                    throw new \InvalidArgumentException('The provided class must extend ['.Model::class.'].');
                }

                return (new $modelClass)->setRawAttributes($value);
            }

            public function set($model, $key, $value, $attributes)
            {
                return $value;
            }
        };
    }

    /**
     * Specify the collection for the cast.
     *
     * @param  string  $class
     * @return string
     */
    public static function using(string $class): string
    {
        return static::class.':'.$class;
    }
}

Use like this

<?php

namespace App\Models;

use MongoDB\Laravel\Auth\User as Authenticatable;
use App\Casts\EmbedManyCast;
use App\Casts\EmbedOneCast;
use MongoDB\Laravel\Relations\EmbedsMany;
use MongoDB\Laravel\Relations\EmbedsOne;

class User extends Authenticatable
{
    protected $connection = 'mongodb';
    protected $collection = 'users';

    protected $guarded = [];

    protected function casts(): array
    {
        return [
            'createdAt' => 'immutable_datetime',
            'updatedAt' => 'immutable_datetime',
            'books' => EmbedManyCast::using(Book::class),
            'info' => EmbedOneCast::using(Info::class),
        ];
    }

    public function embedBooks(): EmbedsMany
    {
        return $this->embedsMany(Book::class, 'books');
    }

    public function embedInfo(): EmbedsOne
    {
        return $this->embedsOne(Info::class, 'info');
    }
}

Without the cast, here's the result of the following code:

<?php

use App\Models\User;

/** @var User $user */
$user = User::query()->find('65f0c181843f4fa3560c9302');
dd($user->toArray());

Before =>

{
  "_id": "65f0c181843f4fa3560c9302",
  "email": "[email protected]",
  "updated_at": "2024-03-12T20:56:33.074000Z",
  "created_at": "2024-03-12T20:56:33.074000Z",
  "books": [
    {
      "title": "mongodb",
      "author": "florian",
      "updated_at": {
        "$date": {
          "$numberLong": "1710277189764"
        }
      },
      "created_at": {
        "$date": {
          "$numberLong": "1710277189764"
        }
      },
      "_id": {
        "$oid": "65f0c245530e10a4da0be372"
      }
    }
  ],
  "info": {
    "gender": "M",
    "birthDate": {
      "$date": {
        "$numberLong": "598579200000"
      }
    },
    "updated_at": {
      "$date": {
        "$numberLong": "1710277463652"
      }
    },
    "created_at": {
      "$date": {
        "$numberLong": "1710277463652"
      }
    },
    "_id": {
      "$oid": "65f0c357e43b5e39160b2f52"
    }
  }
}

After (with cast)

{
  "_id": "65f0c181843f4fa3560c9302",
  "email": "[email protected]",
  "updated_at": "2024-03-12T20:56:33.074000Z",
  "created_at": "2024-03-12T20:56:33.074000Z",
  "books": [
    {
      "title": "mongodb",
      "author": "florian",
      "updated_at": "2024-03-12T20:59:49.764000Z",
      "created_at": "2024-03-12T20:59:49.764000Z",
      "_id": "65f0c245530e10a4da0be372"
    }
  ],
  "info": {
    "gender": "M",
    "birthDate": "1988-12-20T00:00:00.000000Z",
    "updated_at": "2024-03-12T21:04:23.652000Z",
    "created_at": "2024-03-12T21:04:23.652000Z",
    "_id": "65f0c357e43b5e39160b2f52"
  }
}

Do you think it's appropriate?
Is it dangerous?
It might be a good idea to integrate both casts into the library.

@florianJacques florianJacques changed the title Cast embbeded relationship Apply casts on serialize embeded relationships Mar 12, 2024
@florianJacques florianJacques changed the title Apply casts on serialize embeded relationships Apply casts when serializing embedded relations Mar 12, 2024
@florianJacques
Copy link
Contributor Author

Finally a think the better solution is this

 public function toArray(): array
    {
        $array = parent::toArray();
        $array['books'] = $this-> embedBooks->toArray();
        $array['info'] = $this-> embedInfo->toArray();

        return $array;
    }

@FloTelemaque
Copy link

Very interesting approach, both solutions might be a good workaround

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants