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

conedevelopment / root / 18216690222

03 Oct 2025 08:07AM UTC coverage: 76.331% (-0.7%) from 77.022%
18216690222

push

github

iamgergo
fixes

14 of 61 new or added lines in 2 files covered. (22.95%)

1 existing line in 1 file now uncovered.

3354 of 4394 relevant lines covered (76.33%)

34.53 hits per line

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

58.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\Routing\Router;
16
use Illuminate\Support\Arr;
17
use Illuminate\Support\Str;
18

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

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

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

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

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

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

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

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

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

98
        return $this;
×
99
    }
100

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

112
        if ($field instanceof Relation) {
197✔
113
            $field->resolveRouteKeyNameUsing(
197✔
114
                fn (): string => Str::of($field->getRelationName())->singular()->ucfirst()->prepend($this->getRouteKeyName())->value()
197✔
115
            );
197✔
116
        }
117

118
        parent::resolveField($request, $field);
197✔
119
    }
120

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

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

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

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

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

147
            return $fields;
×
148
        };
197✔
149

150
        return $this;
197✔
151
    }
152

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

160
        return $this->mergePivotValues($value);
5✔
161
    }
162

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

170
        return array_map(fn (array $pivot): array => array_merge($this->pivotValues, $pivot), $value);
6✔
171
    }
172

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

184
                $this->getRelation($model)->sync($value);
×
185
            });
×
186
        }
187
    }
188

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

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

207
                $model->setRelation($relation->getRelationName(), $results);
4✔
208
            };
4✔
209
        }
210

211
        parent::resolveHydrate($request, $model, $value);
4✔
212
    }
213

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

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

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

226
            $pivot->setRelation('related', $related)->setAttribute($pivot->getKeyName(), $id);
2✔
227
        });
2✔
228
    }
229

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

237
        $pivot = $related->getRelation($relation->getPivotAccessor());
1✔
238

239
        $pivot->setRelation('related', $related);
1✔
240

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

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

265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function routes(Router $router): void
197✔
269
    {
270
        if ($this->isSubResource()) {
197✔
271
            $router->get('/', [BelongsToManyController::class, 'index']);
197✔
272
            $router->get('/create', [BelongsToManyController::class, 'create']);
197✔
273
            $router->get("/{{$this->getRouteKeyName()}}", [BelongsToManyController::class, 'show']);
197✔
274
            $router->post('/', [BelongsToManyController::class, 'store']);
197✔
275
            $router->get("/{{$this->getRouteKeyName()}}/edit", [BelongsToManyController::class, 'edit']);
197✔
276
            $router->patch("/{{$this->getRouteKeyName()}}", [BelongsToManyController::class, 'update']);
197✔
277
            $router->delete("/{{$this->getRouteKeyName()}}", [BelongsToManyController::class, 'destroy']);
197✔
278
        }
279
    }
280

281
    /**
282
     * {@inheritdoc}
283
     */
284
    public function toOption(Request $request, Model $model, Model $related): array
2✔
285
    {
286
        $relation = $this->getRelation($model);
2✔
287

288
        if (! $related->relationLoaded($relation->getPivotAccessor())) {
2✔
289
            $related->setRelation($relation->getPivotAccessor(), $relation->newPivot());
2✔
290
        }
291

292
        $option = parent::toOption($request, $model, $related);
2✔
293

294
        $option['attrs']['name'] = sprintf(
2✔
295
            '%s[%s][%s]',
2✔
296
            $this->getAttribute('name'),
2✔
297
            $related->getKey(),
2✔
298
            $this->getRelation($model)->getRelatedPivotKeyName()
2✔
299
        );
2✔
300

301
        $option['fields'] = is_null($this->pivotFieldsResolver)
2✔
302
            ? []
2✔
303
            : call_user_func_array($this->pivotFieldsResolver, [$request, $model, $related])->mapToInputs($request, $model);
×
304

305
        return $option;
2✔
306
    }
307

308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function toArray(): array
5✔
312
    {
313
        return array_merge(parent::toArray(), [
5✔
314
            'relatedName' => $this->getRelatedName(),
5✔
315
        ]);
5✔
316
    }
317

318
    /**
319
     * {@inheritdoc}
320
     */
321
    public function toValidate(Request $request, Model $model): array
3✔
322
    {
323
        return array_merge(
3✔
324
            parent::toValidate($request, $model),
3✔
325
            $this->resolveFields($request)->mapToValidate($request, $model)
3✔
326
        );
3✔
327
    }
328

329
    /**
330
     * {@inheritdoc}
331
     */
332
    public function toCreate(Request $request, Model $model): array
×
333
    {
334
        $relation = $this->getRelation($model);
×
335

336
        $pivot = $relation->newPivot();
×
337

338
        $pivot->setRelation('related', $relation->make());
×
339

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

355
    /**
356
     * {@inheritdoc}
357
     */
358
    public function toShow(Request $request, Model $model, Model $related): array
×
359
    {
360
        $relation = $this->getRelation($model);
×
361

362
        $pivot = $related->getRelation($relation->getPivotAccessor());
×
363

364
        $pivot->setRelation('related', $related);
×
365

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

388
    /**
389
     * {@inheritdoc}
390
     */
391
    public function toEdit(Request $request, Model $model, Model $related): array
×
392
    {
393
        $relation = $this->getRelation($model);
×
394

395
        $pivot = $related->getRelation($relation->getPivotAccessor());
×
396

397
        $pivot->setRelation('related', $related);
×
398

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