Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.88% covered (success)
96.88%
31 / 32
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasExternalId
96.88% covered (success)
96.88%
31 / 32
85.71% covered (warning)
85.71%
6 / 7
14
0.00% covered (danger)
0.00%
0 / 1
 bootHasExternalId
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 initializeHasExternalId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 usesExternalId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExternalIdColumn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExternalId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findByExternalId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 findByExternalIdOrFail
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelEloquentPlus\Concerns;
6
7use DevToolbelt\LaravelEloquentPlus\Exceptions\ExternalIdNotEnabledException;
8use DevToolbelt\LaravelEloquentPlus\Exceptions\MissingModelPropertyException;
9use Illuminate\Database\Eloquent\ModelNotFoundException;
10use Illuminate\Support\Str;
11
12/**
13 * Trait for optional external ID (UUID) support on Eloquent models.
14 *
15 * Provides functionality to automatically generate and manage an external UUID
16 * identifier separate from the primary key. This is useful for exposing
17 * public-facing identifiers without revealing internal auto-increment IDs.
18 *
19 * @package DevToolbelt\LaravelEloquentPlus\Concerns
20 */
21trait HasExternalId
22{
23    /**
24     * The name of the external ID column.
25     *
26     * @var string
27     */
28    protected string $externalIdColumn = 'external_id';
29
30    /**
31     * Indicates if the model uses an external ID column.
32     *
33     * When true, a UUID will be automatically generated on model creation.
34     *
35     * @var bool
36     */
37    protected bool $usesExternalId = false;
38
39    /**
40     * Boot the HasExternalId trait.
41     *
42     * Registers a creating event listener to automatically generate
43     * a UUID for the external ID column if enabled.
44     *
45     * @return void
46     * @throws MissingModelPropertyException
47     */
48    protected static function bootHasExternalId(): void
49    {
50        static::creating(static function (self $model): void {
51            if (!$model->usesExternalId()) {
52                return;
53            }
54
55            if (!$model->hasAttribute($model->getExternalIdColumn())) {
56                throw new MissingModelPropertyException($model::class, $model->getExternalIdColumn());
57            }
58
59            if ($model->getAttribute($model->getExternalIdColumn())) {
60                return;
61            }
62
63            $model->setAttribute($model->getExternalIdColumn(), Str::uuid7()->toString());
64        });
65    }
66
67    /**
68     * Initialize the HasExternalId trait.
69     *
70     * Configures hidden attributes to hide the external ID column and
71     * the numeric primary key from serialization output.
72     *
73     * @return void
74     */
75    protected function initializeHasExternalId(): void
76    {
77        if (!$this->usesExternalId) {
78            return;
79        }
80
81        $this->fillable = array_unique([
82            ...$this->fillable,
83            $this->externalIdColumn
84        ]);
85    }
86
87    /**
88     * Determine if the model uses an external ID column.
89     *
90     * @return bool
91     */
92    public function usesExternalId(): bool
93    {
94        return $this->usesExternalId;
95    }
96
97    /**
98     * Get the name of the external ID column.
99     *
100     * @return string
101     */
102    public function getExternalIdColumn(): string
103    {
104        return $this->externalIdColumn;
105    }
106
107    /**
108     * Get the external ID value.
109     *
110     * @return string|null
111     */
112    public function getExternalId(): ?string
113    {
114        if (!$this->usesExternalId()) {
115            return null;
116        }
117
118        return $this->getAttribute($this->externalIdColumn);
119    }
120
121    /**
122     * Find a model by its external ID.
123     *
124     * @param string $externalId The external ID to search for
125     * @return static|null
126     *
127     * @throws ExternalIdNotEnabledException
128     */
129    public static function findByExternalId(string $externalId): ?static
130    {
131        /** @phpstan-ignore new.static */
132        $instance = new static();
133
134        if (!$instance->usesExternalId()) {
135            throw new ExternalIdNotEnabledException();
136        }
137
138        return static::query()
139            ->where($instance->getExternalIdColumn(), $externalId)
140            ->first();
141    }
142
143    /**
144     * Find a model by its external ID or throw an exception.
145     *
146     * @param string $externalId The external ID to search for
147     * @return static
148     *
149     * @throws ExternalIdNotEnabledException
150     * @throws ModelNotFoundException<static>
151     */
152    public static function findByExternalIdOrFail(string $externalId): static
153    {
154        /** @phpstan-ignore new.static */
155        $instance = new static();
156
157        if (!$instance->usesExternalId()) {
158            throw new ExternalIdNotEnabledException();
159        }
160
161        return static::query()
162            ->where($instance->getExternalIdColumn(), $externalId)
163            ->firstOrFail();
164    }
165}