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

conedevelopment / root / 20606811462

30 Dec 2025 10:01PM UTC coverage: 76.05% (-0.1%) from 76.188%
20606811462

push

github

iamgergo
async relation fileds

96 of 124 new or added lines in 6 files covered. (77.42%)

23 existing lines in 2 files now uncovered.

3439 of 4522 relevant lines covered (76.05%)

33.72 hits per line

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

55.29
/src/Fields/BelongsToMany.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Root\Fields;
6

7
use Closure;
8
use Cone\Root\Http\Controllers\BelongsToManyController;
9
use Illuminate\Database\Eloquent\Builder;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsTo as BelongsToRelation;
12
use Illuminate\Database\Eloquent\Relations\BelongsToMany as EloquentRelation;
13
use Illuminate\Database\Eloquent\Relations\Pivot;
14
use Illuminate\Http\Request;
15
use Illuminate\Support\Arr;
16
use Illuminate\Support\Str;
17

18
/**
19
 * @template TRelation of \Illuminate\Database\Eloquent\Relations\BelongsToMany
20
 *
21
 * @extends \Cone\Root\Fields\Relation<TRelation>
22
 */
23
class BelongsToMany extends Relation
24
{
25
    /**
26
     * The pivot fields resolver callback.
27
     */
28
    protected ?Closure $pivotFieldsResolver = null;
29

30
    /**
31
     * The default pivot values that should be saved.
32
     */
33
    protected array $pivotValues = [];
34

35
    /**
36
     * Indicates if the field allows duplicate relations.
37
     */
38
    protected bool $allowDuplicateRelations = false;
39

40
    /**
41
     * Create a new relation field instance.
42
     */
43
    public function __construct(string $label, Closure|string|null $modelAttribute = null, Closure|string|null $relation = null)
197✔
44
    {
45
        parent::__construct($label, $modelAttribute, $relation);
197✔
46

47
        $this->setAttribute('multiple', true);
197✔
48
        $this->name(sprintf('%s[]', $this->getAttribute('name')));
197✔
49
    }
50

51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function getRelation(Model $model): EloquentRelation
13✔
55
    {
56
        $relation = parent::getRelation($model);
13✔
57

58
        return $relation->withPivot([
13✔
59
            $relation->newPivot()->getKeyName(),
13✔
60
            $relation->getForeignPivotKeyName(),
13✔
61
            $relation->getRelatedPivotKeyName(),
13✔
62
        ]);
13✔
63
    }
64

65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function fields(Request $request): array
197✔
69
    {
70
        return [
197✔
71
            BelongsTo::make($this->getRelatedName(), 'related', static fn (Pivot $model): BelongsToRelation => $model->belongsTo(
197✔
72
                $model->getRelation('related')::class,
197✔
73
                $model->getRelatedKey(),
197✔
74
                $model->getForeignKey(),
197✔
75
                'related'
197✔
76
            )->withDefault())
197✔
77
                ->withRelatableQuery(fn (Request $request, Builder $query, Pivot $model): Builder => $this->resolveRelatableQuery($request, $model->pivotParent)
197✔
78
                    ->unless($this->allowDuplicateRelations, fn (Builder $query): Builder => $query->whereNotIn(
197✔
79
                        $query->getModel()->getQualifiedKeyName(),
197✔
80
                        $this->getRelation($model->pivotParent)->select($query->getModel()->getQualifiedKeyName())
197✔
81
                    )))->hydrate(function (Request $request, Pivot $model, mixed $value): void {
197✔
82
                        $model->setAttribute(
2✔
83
                            $this->getRelation($model->pivotParent)->getRelatedPivotKeyName(),
2✔
84
                            $value
2✔
85
                        );
2✔
86
                    })->display(fn (Model $model): ?string => $this->resolveDisplay($model)),
197✔
87
        ];
197✔
88
    }
89

90
    /**
91
     * Allow duplicate relations.
92
     */
93
    public function allowDuplicateRelations(bool $value = true): static
×
94
    {
95
        $this->allowDuplicateRelations = $value;
×
96

97
        return $this;
×
98
    }
99

100
    /**
101
     * {@inheritdoc}
102
     */
103
    protected function resolveField(Request $request, Field $field): void
197✔
104
    {
105
        if (! $this->isSubResource()) {
197✔
106
            $field->setModelAttribute(
×
107
                sprintf('%s.*.%s', $this->getModelAttribute(), $field->getModelAttribute())
×
108
            );
×
109
        }
110

111
        if ($field instanceof Relation) {
197✔
112
            $field->resolveRouteKeyNameUsing(function () use ($field): string {
197✔
NEW
113
                return Str::of($field->getRelationName())
×
NEW
114
                    ->singular()
×
NEW
115
                    ->ucfirst()
×
NEW
116
                    ->prepend($this->getRouteKeyName())
×
NEW
117
                    ->value();
×
118
            });
197✔
119
        }
120

121
        parent::resolveField($request, $field);
197✔
122
    }
123

124
    /**
125
     * Set the pivot field resolver.
126
     */
127
    public function withPivotFields(Closure $callback): static
197✔
128
    {
129
        $this->withFields($callback);
197✔
130

131
        $this->pivotFieldsResolver = function (Request $request, Model $model, Model $related) use ($callback): Fields {
197✔
132
            $fields = new Fields;
×
133

134
            $fields->register(Arr::wrap(call_user_func_array($callback, [$request])));
×
135

136
            $fields->each(function (Field $field) use ($model, $related): void {
×
137
                $attribute = sprintf(
×
138
                    '%s.%s.%s',
×
139
                    $this->getModelAttribute(),
×
140
                    $related->getKey(),
×
141
                    $key = $field->getModelAttribute()
×
142
                );
×
143

144
                $field->setModelAttribute($attribute)
×
145
                    ->name($attribute)
×
146
                    ->id($attribute)
×
147
                    ->value(fn (): mixed => $related->getRelation($this->getRelation($model)->getPivotAccessor())->getAttribute($key));
×
148
            });
×
149

150
            return $fields;
×
151
        };
197✔
152

153
        return $this;
197✔
154
    }
155

156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function getValueForHydrate(Request $request): mixed
5✔
160
    {
161
        $value = (array) parent::getValueForHydrate($request);
5✔
162

163
        return $this->mergePivotValues($value);
5✔
164
    }
165

166
    /**
167
     * Merge the pivot values.
168
     */
169
    public function mergePivotValues(array $value): array
6✔
170
    {
171
        $value = array_is_list($value) ? array_fill_keys($value, []) : $value;
6✔
172

173
        return array_map(fn (array $pivot): array => array_merge($this->pivotValues, $pivot), $value);
6✔
174
    }
175

176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function persist(Request $request, Model $model, mixed $value): void
2✔
180
    {
181
        if ($this->isSubResource()) {
2✔
182
            parent::persist($request, $model, $value);
2✔
183
        } else {
184
            $model->saved(function (Model $model) use ($request, $value): void {
×
185
                $this->resolveHydrate($request, $model, $value);
×
186

187
                $this->getRelation($model)->sync($value);
×
188
            });
×
189
        }
190
    }
191

192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function resolveHydrate(Request $request, Model $model, mixed $value): void
4✔
196
    {
197
        if (is_null($this->hydrateResolver)) {
4✔
198
            $this->hydrateResolver = function (Request $request, Model $model, mixed $value): void {
4✔
199
                $relation = $this->getRelation($model);
4✔
200

201
                $results = $this->resolveRelatableQuery($request, $model)
4✔
202
                    ->findMany(array_keys($value))
4✔
203
                    ->each(static function (Model $related) use ($relation, $value): void {
4✔
204
                        $related->setRelation(
1✔
205
                            $relation->getPivotAccessor(),
1✔
206
                            $relation->newPivot($value[$related->getKey()])
1✔
207
                        );
1✔
208
                    });
4✔
209

210
                $model->setRelation($relation->getRelationName(), $results);
4✔
211
            };
4✔
212
        }
213

214
        parent::resolveHydrate($request, $model, $value);
4✔
215
    }
216

217
    /**
218
     * {@inheritdoc}
219
     */
220
    public function resolveRouteBinding(Request $request, string $id): Model
2✔
221
    {
222
        $relation = $this->getRelation($request->route()->parentOfParameter($this->getRouteKeyName()));
2✔
223

224
        $related = $relation->wherePivot($relation->newPivot()->getQualifiedKeyName(), $id)->firstOrFail();
2✔
225

226
        return tap($related, static function (Model $related) use ($relation, $id): void {
2✔
227
            $pivot = $related->getRelation($relation->getPivotAccessor());
2✔
228

229
            $pivot->setRelation('related', $related)->setAttribute($pivot->getKeyName(), $id);
2✔
230
        });
2✔
231
    }
232

233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function mapRelated(Request $request, Model $model, Model $related): array
1✔
237
    {
238
        $relation = $this->getRelation($model);
1✔
239

240
        $pivot = $related->getRelation($relation->getPivotAccessor());
1✔
241

242
        $pivot->setRelation('related', $related);
1✔
243

244
        return [
1✔
245
            'id' => $related->getKey(),
1✔
246
            'model' => $pivot,
1✔
247
            'url' => $this->relatedUrl($pivot),
1✔
248
            'fields' => $this->resolveFields($request)
1✔
249
                ->subResource(false)
1✔
250
                ->authorized($request, $related)
1✔
251
                ->visible('index')
1✔
252
                ->mapToDisplay($request, $pivot),
1✔
253
            'abilities' => $this->mapRelatedAbilities($request, $model, $related),
1✔
254
        ];
1✔
255
    }
256

257
    /**
258
     * Get the related URL.
259
     */
260
    public function relatedUrl(Model $related): string
3✔
261
    {
262
        return match (true) {
263
            $related instanceof Pivot => sprintf('%s/%s', $this->modelUrl($related->pivotParent), $related->getKey()),
3✔
264
            default => parent::relatedUrl($related),
3✔
265
        };
266
    }
267

268
    /**
269
     * Get the relation controller class.
270
     */
271
    public function getRelationController(): string
197✔
272
    {
273
        return BelongsToManyController::class;
197✔
274
    }
275

276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function toOption(Request $request, Model $model, Model $related): array
2✔
280
    {
281
        $relation = $this->getRelation($model);
2✔
282

283
        if (! $related->relationLoaded($relation->getPivotAccessor())) {
2✔
284
            $related->setRelation($relation->getPivotAccessor(), $relation->newPivot());
2✔
285
        }
286

287
        $option = parent::toOption($request, $model, $related);
2✔
288

289
        $option['attrs']['name'] = sprintf(
2✔
290
            '%s[%s][%s]',
2✔
291
            $this->getAttribute('name'),
2✔
292
            $related->getKey(),
2✔
293
            $this->getRelation($model)->getRelatedPivotKeyName()
2✔
294
        );
2✔
295

296
        $option['fields'] = is_null($this->pivotFieldsResolver)
2✔
297
            ? []
2✔
298
            : call_user_func_array($this->pivotFieldsResolver, [$request, $model, $related])->mapToInputs($request, $model);
×
299

300
        return $option;
2✔
301
    }
302

303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function toArray(): array
5✔
307
    {
308
        return array_merge(parent::toArray(), [
5✔
309
            'relatedName' => $this->getRelatedName(),
5✔
310
        ]);
5✔
311
    }
312

313
    /**
314
     * {@inheritdoc}
315
     */
316
    public function toValidate(Request $request, Model $model): array
3✔
317
    {
318
        return array_merge(
3✔
319
            parent::toValidate($request, $model),
3✔
320
            $this->resolveFields($request)->mapToValidate($request, $model)
3✔
321
        );
3✔
322
    }
323

324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function toCreate(Request $request, Model $model): array
×
328
    {
329
        $relation = $this->getRelation($model);
×
330

331
        $pivot = $relation->newPivot();
×
332

333
        $pivot->setRelation('related', $relation->make());
×
334

335
        return array_merge($this->toSubResource($request, $model), [
×
336
            'template' => 'root::resources.form',
×
337
            'title' => __('Attach :model', ['model' => $this->getRelatedName()]),
×
338
            'model' => $pivot,
×
339
            'action' => $this->modelUrl($model),
×
340
            'method' => 'POST',
×
341
            'uploads' => $this->hasFileField($request),
×
342
            'fields' => $this->resolveFields($request)
×
343
                ->subResource(false)
×
344
                ->authorized($request, $pivot)
×
345
                ->visible('create')
×
346
                ->mapToInputs($request, $pivot),
×
347
        ]);
×
348
    }
349

350
    /**
351
     * {@inheritdoc}
352
     */
353
    public function toShow(Request $request, Model $model, Model $related): array
×
354
    {
355
        $relation = $this->getRelation($model);
×
356

357
        $pivot = $related->getRelation($relation->getPivotAccessor());
×
358

359
        $pivot->setRelation('related', $related);
×
360

361
        return array_merge($this->toSubResource($request, $model), [
×
362
            'template' => 'root::resources.show',
×
363
            'title' => $this->resolveDisplay($related),
×
364
            'model' => $pivot,
×
365
            'action' => $this->relatedUrl($pivot),
×
366
            'fields' => $this->resolveFields($request)
×
367
                ->subResource(false)
×
368
                ->authorized($request, $related)
×
369
                ->visible('show')
×
370
                ->mapToDisplay($request, $pivot),
×
371
            'actions' => $this->resolveActions($request)
×
372
                ->authorized($request, $related)
×
373
                ->visible('show')
×
374
                ->standalone(false)
×
375
                ->mapToForms($request, $pivot),
×
376
            'abilities' => array_merge(
×
377
                $this->mapRelationAbilities($request, $model),
×
378
                $this->mapRelatedAbilities($request, $model, $related)
×
379
            ),
×
380
        ]);
×
381
    }
382

383
    /**
384
     * {@inheritdoc}
385
     */
386
    public function toEdit(Request $request, Model $model, Model $related): array
×
387
    {
388
        $relation = $this->getRelation($model);
×
389

390
        $pivot = $related->getRelation($relation->getPivotAccessor());
×
391

392
        $pivot->setRelation('related', $related);
×
393

394
        return array_merge($this->toSubResource($request, $model), [
×
395
            'template' => 'root::resources.form',
×
396
            'title' => __('Edit :model', ['model' => $this->resolveDisplay($related)]),
×
397
            'model' => $pivot,
×
398
            'action' => $this->relatedUrl($pivot),
×
399
            'method' => 'PATCH',
×
400
            'uploads' => $this->hasFileField($request),
×
401
            'fields' => $this->resolveFields($request)
×
402
                ->subResource(false)
×
403
                ->authorized($request, $related)
×
404
                ->visible('update')
×
405
                ->mapToInputs($request, $pivot),
×
406
            'abilities' => array_merge(
×
407
                $this->mapRelationAbilities($request, $model),
×
408
                $this->mapRelatedAbilities($request, $model, $related)
×
409
            ),
×
410
        ]);
×
411
    }
412
}
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