• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orchestral / sidekick / 15059475242

16 May 2025 02:43AM UTC coverage: 95.833%. Remained the same
15059475242

Pull #41

github

web-flow
Merge 75310a457 into 38cf30aea
Pull Request #41: Improves `model_diff()` logic for create and update state.

2 of 2 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

138 of 144 relevant lines covered (95.83%)

5.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.4
/src/Eloquent/functions.php
1
<?php
2

3
namespace Orchestra\Sidekick\Eloquent;
4

5
use BackedEnum;
6
use Carbon\CarbonInterface;
7
use Illuminate\Database\Eloquent\Concerns\HasUlids;
8
use Illuminate\Database\Eloquent\Concerns\HasUuids;
9
use Illuminate\Database\Eloquent\Model;
10
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
11
use Illuminate\Database\Eloquent\Relations\Pivot;
12
use Illuminate\Support\Arr;
13
use InvalidArgumentException;
14
use JsonSerializable;
15
use Orchestra\Sidekick\SensitiveValue;
16
use Stringable;
17
use Throwable;
18

19
if (! \function_exists('Orchestra\Sidekick\Eloquent\column_name')) {
20
    /**
21
     * Get qualify column name from Eloquent model.
22
     *
23
     * @api
24
     *
25
     * @param  \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>  $model
26
     *
27
     * @throws \InvalidArgumentException
28
     */
29
    function column_name(Model|string $model, string $attribute): string
30
    {
31
        if (\is_string($model)) {
3✔
32
            $model = new $model;
2✔
33
        }
34

35
        if (! $model instanceof Model) {
3✔
36
            throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s].', Model::class));
1✔
37
        }
38

39
        return $model->qualifyColumn($attribute);
2✔
40
    }
41
}
42

43
if (! \function_exists('Orchestra\Sidekick\Eloquent\is_pivot_model')) {
44
    /**
45
     * Determine if the given model is a pivot model.
46
     *
47
     * @api
48
     *
49
     * @template TPivotModel of (\Illuminate\Database\Eloquent\Model&\Illuminate\Database\Eloquent\Relations\Concerns\AsPivot)|\Illuminate\Database\Eloquent\Relations\Pivot
50
     *
51
     * @param  TPivotModel|class-string<TPivotModel>  $model
52
     *
53
     * @throws \InvalidArgumentException
54
     */
55
    function is_pivot_model(Pivot|Model|string $model): bool
56
    {
57
        if (\is_string($model)) {
4✔
58
            $model = new $model;
1✔
59
        }
60

61
        if (! $model instanceof Model) {
4✔
62
            throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s|%s].', Model::class, Pivot::class));
1✔
63
        }
64

65
        if ($model instanceof Pivot) {
3✔
66
            return true;
1✔
67
        }
68

69
        return \in_array(AsPivot::class, class_uses_recursive($model), true);
2✔
70
    }
71
}
72

73
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_exists')) {
74
    /**
75
     * Check whether given $model exists.
76
     *
77
     * @api
78
     *
79
     * @param  \Illuminate\Database\Eloquent\Model|mixed  $model
80
     */
81
    function model_exists(mixed $model): bool
82
    {
83
        return $model instanceof Model && $model->exists === true;
16✔
84
    }
85
}
86

87
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_key_type')) {
88
    /**
89
     * Check whether given $model key type.
90
     *
91
     * @api
92
     *
93
     * @param  \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>  $model
94
     *
95
     * @throws \InvalidArgumentException
96
     */
97
    function model_key_type(Model|string $model): string
98
    {
99
        if (\is_string($model)) {
6✔
100
            $model = new $model;
2✔
101
        }
102

103
        if (! $model instanceof Model) {
6✔
104
            throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s].', Model::class));
1✔
105
        }
106

107
        $uses = class_uses_recursive($model);
5✔
108

109
        if (\in_array(HasUlids::class, $uses, true)) {
5✔
110
            return 'ulid';
1✔
111
        } elseif (\in_array(HasUuids::class, $uses, true)) {
4✔
112
            return 'uuid';
1✔
113
        }
114

115
        return $model->getKeyType();
3✔
116
    }
117
}
118

119
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_diff')) {
120
    /**
121
     * Get attributes diff state from a model.
122
     *
123
     * @api
124
     *
125
     * @param  array<int, string>  $excludes
126
     * @return array<string, mixed>
127
     */
128
    function model_diff(Model $model, array $excludes = [], bool $withTimestamps = true): array
129
    {
130
        $hiddens = $model->getHidden();
14✔
131

132
        $timestamps = [$model->getCreatedAtColumn(), $model->getUpdatedAtColumn()];
14✔
133

134
        if (! model_exists($model) || $model->wasRecentlyCreated == true) {
14✔
135
            $copy = clone $model;
7✔
136
            $copy->setHidden($excludes);
7✔
137

138
            return Arr::except(
7✔
139
                summarize_changes($copy->attributesToArray(), hiddens: $hiddens),
7✔
140
                $withTimestamps === false ? $timestamps : [$model->getUpdatedAtColumn()]
7✔
141
            );
7✔
142
        }
143

144
        $rawChanges = $model->isDirty() ? $model->getDirty() : $model->getChanges();
7✔
145

146
        $changes = array_intersect_key(
7✔
147
            $model->newInstance()->setHidden($excludes)->setRawAttributes($rawChanges)->attributesToArray(),
7✔
148
            $rawChanges
7✔
149
        );
7✔
150

151
        return Arr::except(
7✔
152
            summarize_changes($changes, hiddens: $hiddens),
7✔
153
            $withTimestamps === false ? $timestamps : []
7✔
154
        );
7✔
155
    }
156
}
157

158
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_snapshot')) {
159
    /**
160
     * Store a snapshot for a model and return the original attributes.
161
     *
162
     * @api
163
     *
164
     * @return array<string, mixed>|null
165
     */
166
    function model_snapshot(Model $model): ?array
167
    {
168
        return Watcher::snapshot($model);
1✔
169
    }
170
}
171

172
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_state')) {
173
    /**
174
     * Get attributes original and changed state from a model.
175
     *
176
     * @api
177
     *
178
     * @param  array<int, string>  $excludes
179
     * @return array{0: array<string, mixed>|null, 1: array<string, mixed>}
180
     */
181
    function model_state(Model $model, array $excludes = [], bool $withTimestamps = true): array
182
    {
183
        $changes = model_diff($model, $excludes, $withTimestamps);
7✔
184

185
        if (! model_exists($model) || $model->wasRecentlyCreated == true) {
7✔
186
            return [null, $changes];
3✔
187
        }
188

189
        $previous = method_exists($model, 'getPrevious') ? $model->getPrevious() : null;
4✔
190

191
        if (empty($previous)) {
4✔
192
            $previous = Watcher::snapshot($model);
4✔
193
        }
194

195
        $original = summarize_changes(
4✔
196
            array_intersect_key($model->newInstance()->setRawAttributes($previous ?? [])->attributesToArray(), $changes),
4✔
197
            hiddens: $model->getHidden(),
4✔
198
        );
4✔
199

200
        return [$original, $changes];
4✔
201
    }
202
}
203

204
if (! \function_exists('Orchestra\Sidekick\Eloquent\normalize_value')) {
205
    /**
206
     * Normalize the given value to be store to database as scalar.
207
     *
208
     * @api
209
     *
210
     * @return scalar
211
     */
212
    function normalize_value(mixed $value): mixed
213
    {
214
        $value = match (true) {
24✔
215
            $value instanceof CarbonInterface => $value->toISOString(),
24✔
216
            $value instanceof JsonSerializable => $value->jsonSerialize(),
24✔
217
            $value instanceof BackedEnum => $value->value,
24✔
218
            default => $value,
24✔
219
        };
24✔
220

221
        if (\is_object($value) && $value instanceof Stringable) {
24✔
222
            return (string) $value;
1✔
223
        } elseif (\is_object($value) || \is_array($value)) {
23✔
224
            try {
225
                return json_encode($value);
4✔
UNCOV
226
            } catch (Throwable $e) { // @phpstan-ignore catch.neverThrown
×
UNCOV
227
                return $value;
×
228
            }
229
        }
230

231
        return $value;
19✔
232
    }
233
}
234

235
if (! \function_exists('Orchestra\Sidekick\Eloquent\summarize_changes')) {
236
    /**
237
     * Get table name from Eloquent model.
238
     *
239
     * @api
240
     *
241
     * @param  array<string, mixed>  $changes
242
     * @param  array<int, string>  $hiddens
243
     * @return array<string, \Orchestra\Sidekick\SensitiveValue|scalar>
244
     */
245
    function summarize_changes(array $changes, array $hiddens = []): array
246
    {
247
        $summaries = [];
16✔
248

249
        foreach ($changes as $attribute => $value) {
16✔
250
            $summaries[$attribute] = \in_array($attribute, $hiddens, true)
16✔
251
                ? new SensitiveValue($value)
14✔
252
                : normalize_value($value);
16✔
253
        }
254

255
        return $summaries;
16✔
256
    }
257
}
258

259
if (! \function_exists('Orchestra\Sidekick\Eloquent\table_name')) {
260
    /**
261
     * Get table name from Eloquent model.
262
     *
263
     * @api
264
     *
265
     * @param  \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>  $model
266
     *
267
     * @throws \InvalidArgumentException
268
     */
269
    function table_name(Model|string $model): string
270
    {
271
        if (\is_string($model)) {
3✔
272
            $model = new $model;
2✔
273
        }
274

275
        if (! $model instanceof Model) {
3✔
276
            throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s].', Model::class));
1✔
277
        }
278

279
        return $model->getTable();
2✔
280
    }
281
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc