Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
19 / 20
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
UuidToIdCast
95.00% covered (success)
95.00%
19 / 20
75.00% covered (warning)
75.00%
3 / 4
8
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
 findRelatedRecord
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelEloquentPlus\Casts;
6
7use DevToolbelt\LaravelEloquentPlus\Exceptions\ValidationException;
8use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
9use Illuminate\Database\Eloquent\Model;
10use Illuminate\Support\Facades\DB;
11use stdClass;
12
13/**
14 * Cast that converts external UUID to internal numeric ID for foreign keys.
15 *
16 * This cast allows the application to receive UUIDs from external sources
17 * (like API requests) while storing numeric IDs in the database for performance.
18 *
19 * Usage in model:
20 * ```php
21 * protected $casts = [
22 *     'product_id' => UuidToIdCast::class . ':products',
23 *     'category_id' => UuidToIdCast::class . ':categories,id',
24 *     'user_id' => UuidToIdCast::class . ':users,id,uuid',
25 * ];
26 * ```
27 *
28 * @package DevToolbelt\LaravelEloquentPlus\Casts
29 */
30final readonly class UuidToIdCast implements CastsAttributes
31{
32    /**
33     * The default column name for external ID lookups.
34     */
35    private const string DEFAULT_EXTERNAL_ID_COLUMN = 'external_id';
36
37    /**
38     * @param string $relatedTableName The name of the related table to lookup
39     * @param string|null $relatedPkName The primary key column name (defaults to the attribute key)
40     * @param string $externalIdColumn The external ID column name for lookup
41     */
42    public function __construct(
43        private string $relatedTableName,
44        private ?string $relatedPkName = null,
45        private string $externalIdColumn = self::DEFAULT_EXTERNAL_ID_COLUMN,
46    ) {
47    }
48
49    /**
50     * Transform the attribute from the underlying model values.
51     *
52     * Returns the numeric ID as stored in the database.
53     *
54     * @param Model $model
55     * @param string $key
56     * @param mixed $value
57     * @param array<string, mixed> $attributes
58     * @return mixed
59     */
60    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
61    {
62        return $value;
63    }
64
65    /**
66     * Transform the attribute to its underlying model values.
67     *
68     * Converts UUID to numeric ID by looking up the related table.
69     *
70     * @param Model $model
71     * @param string $key
72     * @param mixed $value
73     * @param array<string, mixed> $attributes
74     * @return mixed
75     * @throws ValidationException
76     */
77    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
78    {
79        if ($value === null) {
80            return null;
81        }
82
83        if (is_numeric($value)) {
84            return (int) $value;
85        }
86
87        if (!is_string($value)) {
88            return $value;
89        }
90
91        $record = $this->findRelatedRecord($value);
92        $pkColumn = $this->relatedPkName ?? 'id';
93
94        if (!$record) {
95            $message = "The selected '{$key}' is invalid. Record not found in '{$this->relatedTableName}'.";
96
97            throw new ValidationException(
98                [['field' => $key, 'error' => 'relationRecordNotFound', 'value' => $value, 'message' => $message]],
99                $message
100            );
101        }
102
103        return $record->{$pkColumn};
104    }
105
106    /**
107     * Find the related record by external ID.
108     *
109     * @param string $externalId The external UUID to search for
110     * @return stdClass|null The found record or null
111     */
112    private function findRelatedRecord(string $externalId): ?stdClass
113    {
114        return DB::table($this->relatedTableName)
115            ->where($this->externalIdColumn, $externalId)
116            ->first();
117    }
118}