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

conedevelopment / root / 13086943877

01 Feb 2025 08:35AM UTC coverage: 79.285% (+1.2%) from 78.037%
13086943877

push

github

web-flow
Merge pull request #234 from xHeaven/patch-1

Codebase health checkup

195 of 239 new or added lines in 45 files covered. (81.59%)

2 existing lines in 2 files now uncovered.

2572 of 3244 relevant lines covered (79.28%)

35.8 hits per line

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

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

3
namespace Cone\Root\Fields;
4

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

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

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

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

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

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

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

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

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

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

96
        return $this;
×
97
    }
98

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

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

116
        parent::resolveField($request, $field);
198✔
117
    }
118

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

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

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

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

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

145
            return $fields;
×
146
        };
198✔
147

148
        return $this;
198✔
149
    }
150

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

158
        return $this->mergePivotValues($value);
5✔
159
    }
160

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

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

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

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

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

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

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

209
        parent::resolveHydrate($request, $model, $value);
4✔
210
    }
211

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

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

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

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

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

235
        $pivot = $related->getRelation($relation->getPivotAccessor());
1✔
236

237
        $pivot->setRelation('related', $related);
1✔
238

239
        return parent::mapRelated($request, $model, $pivot);
1✔
240
    }
241

242
    /**
243
     * {@inheritdoc}
244
     */
245
    public function routes(Router $router): void
246
    {
247
        if ($this->isSubResource()) {
198✔
248
            $router->get('/', [BelongsToManyController::class, 'index']);
198✔
249
            $router->get('/create', [BelongsToManyController::class, 'create']);
198✔
250
            $router->get("/{{$this->getRouteKeyName()}}", [BelongsToManyController::class, 'show']);
198✔
251
            $router->post('/', [BelongsToManyController::class, 'store']);
198✔
252
            $router->get("/{{$this->getRouteKeyName()}}/edit", [BelongsToManyController::class, 'edit']);
198✔
253
            $router->patch("/{{$this->getRouteKeyName()}}", [BelongsToManyController::class, 'update']);
198✔
254
            $router->delete("/{{$this->getRouteKeyName()}}", [BelongsToManyController::class, 'destroy']);
198✔
255
        }
256
    }
257

258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function toOption(Request $request, Model $model, Model $related): array
262
    {
263
        $relation = $this->getRelation($model);
2✔
264

265
        if (! $related->relationLoaded($relation->getPivotAccessor())) {
2✔
266
            $related->setRelation($relation->getPivotAccessor(), $relation->newPivot());
2✔
267
        }
268

269
        $option = parent::toOption($request, $model, $related);
2✔
270

271
        $option['attrs']['name'] = sprintf(
2✔
272
            '%s[%s][%s]',
2✔
273
            $this->getAttribute('name'),
2✔
274
            $related->getKey(),
2✔
275
            $this->getRelation($model)->getRelatedPivotKeyName()
2✔
276
        );
2✔
277

278
        $option['fields'] = is_null($this->pivotFieldsResolver)
2✔
279
            ? []
2✔
280
            : call_user_func_array($this->pivotFieldsResolver, [$request, $model, $related])->mapToInputs($request, $model);
×
281

282
        return $option;
2✔
283
    }
284

285
    /**
286
     * {@inheritdoc}
287
     */
288
    public function toArray(): array
289
    {
290
        return array_merge(parent::toArray(), [
5✔
291
            'relatedName' => $this->getRelatedName(),
5✔
292
        ]);
5✔
293
    }
294

295
    /**
296
     * {@inheritdoc}
297
     */
298
    public function toValidate(Request $request, Model $model): array
299
    {
300
        return array_merge(
3✔
301
            parent::toValidate($request, $model),
3✔
302
            $this->resolveFields($request)->mapToValidate($request, $model)
3✔
303
        );
3✔
304
    }
305

306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function toCreate(Request $request, Model $model): array
310
    {
311
        $relation = $this->getRelation($model);
×
312

313
        $pivot = $relation->newPivot();
×
314

315
        $pivot->setRelation('related', $relation->make());
×
316

317
        return array_merge($this->toSubResource($request, $model), [
×
318
            'title' => __('Attach :model', ['model' => $this->getRelatedName()]),
×
319
            'model' => $pivot,
×
320
            'action' => $this->modelUrl($model),
×
321
            'method' => 'POST',
×
322
            'fields' => $this->resolveFields($request)
×
323
                ->subResource(false)
×
324
                ->authorized($request, $pivot)
×
325
                ->visible('relation.create')
×
326
                ->mapToInputs($request, $pivot),
×
327
        ]);
×
328
    }
329

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

337
        $pivot = $related->getRelation($relation->getPivotAccessor());
×
338

339
        $pivot->setRelation('related', $related);
×
340

341
        return parent::toShow($request, $model, $pivot);
×
342
    }
343

344
    /**
345
     * {@inheritdoc}
346
     */
347
    public function toEdit(Request $request, Model $model, Model $related): array
348
    {
349
        $relation = $this->getRelation($model);
×
350

351
        $pivot = $related->getRelation($relation->getPivotAccessor());
×
352

353
        $pivot->setRelation('related', $related);
×
354

355
        return parent::toEdit($request, $model, $pivot);
×
356
    }
357
}
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