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

conedevelopment / root / 20644444576

01 Jan 2026 07:39PM UTC coverage: 75.399% (-0.1%) from 75.542%
20644444576

push

github

iamgergo
wip

2 of 11 new or added lines in 2 files covered. (18.18%)

3451 of 4577 relevant lines covered (75.4%)

33.07 hits per line

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

82.76
/src/Fields/Repeater.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\RepeaterController;
9
use Cone\Root\Traits\RegistersRoutes;
10
use Cone\Root\Traits\ResolvesFields;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Http\Request;
13
use Illuminate\Routing\Router;
14
use Illuminate\Support\Facades\View;
15
use Illuminate\Support\Str;
16

17
class Repeater extends Field
18
{
19
    use RegistersRoutes {
20
        RegistersRoutes::registerRoutes as __registerRoutes;
21
    }
22
    use ResolvesFields {
23
        ResolvesFields::withFields as __withFields;
24
    }
25

26
    /**
27
     * The Blade template.
28
     */
29
    protected string $template = 'root::fields.repeater';
30

31
    /**
32
     * The option fields resolver.
33
     */
34
    protected ?Closure $optionFieldsResolver = null;
35

36
    /**
37
     * The maximum number of options.
38
     */
39
    protected ?int $max = null;
40

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

48
        $this->hiddenOn(['index']);
196✔
49
    }
50

51
    /**
52
     * Get the URI key.
53
     */
54
    public function getUriKey(): string
196✔
55
    {
56
        return str_replace('.', '-', $this->getRequestKey());
196✔
57
    }
58

59
    /**
60
     * Get the route parameter name.
61
     */
62
    public function getRouteParameterName(): string
1✔
63
    {
64
        return 'field';
1✔
65
    }
66

67
    /**
68
     * Set the maximum number of options.
69
     */
70
    public function max(int $value): static
×
71
    {
72
        $this->max = $value;
×
73

74
        return $this;
×
75
    }
76

77
    /**
78
     * Get the option name.
79
     */
80
    public function getOptionName(): string
3✔
81
    {
82
        return __(Str::singular($this->label));
3✔
83
    }
84

85
    /**
86
     * Get the add new option label.
87
     */
88
    public function getAddNewOptionLabel(): string
2✔
89
    {
90
        return __('Add :name', ['name' => $this->getOptionName()]);
2✔
91
    }
92

93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function getValueForHydrate(Request $request): array
3✔
97
    {
98
        return array_values((array) parent::getValueForHydrate($request));
3✔
99
    }
100

101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function getOldValue(Request $request): array
×
105
    {
106
        return array_values((array) parent::getOldValue($request));
×
107
    }
108

109
    /**
110
     * Handle the callback for the field resolution.
111
     */
112
    protected function resolveField(Request $request, Field $field): void
196✔
113
    {
114
        $field->setModelAttribute(
196✔
115
            sprintf('%s.*.%s', $this->getModelAttribute(), $field->getModelAttribute())
196✔
116
        );
196✔
117

118
        if ($field instanceof Relation) {
196✔
119
            $field->resolveRouteKeyNameUsing(function () use ($field): string {
×
120
                return Str::of($field->getRelationName())
×
121
                    ->singular()
×
122
                    ->ucfirst()
×
123
                    ->prepend($this->getModelAttribute())
×
124
                    ->value();
×
125
            });
×
126
        }
127
    }
128

129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function withFields(Closure $callback): static
196✔
133
    {
134
        $this->optionFieldsResolver = function (Request $request, Model $model, Model $tmpModel) use ($callback): Fields {
196✔
135
            $fields = new Fields([
1✔
136
                Hidden::make(__('Key'), '_key'),
1✔
137
            ]);
1✔
138

139
            $fields->register(call_user_func_array($callback, [$request, $model, $tmpModel]));
1✔
140

141
            $fields->each(function (Field $field) use ($tmpModel): void {
1✔
142
                $attribute = sprintf(
1✔
143
                    '%s.%s.%s',
1✔
144
                    $this->getModelAttribute(),
1✔
145
                    $tmpModel->getAttribute('_key'),
1✔
146
                    $key = $field->getModelAttribute()
1✔
147
                );
1✔
148

149
                $field->setModelAttribute($attribute)
1✔
150
                    ->name($attribute)
1✔
151
                    ->id($attribute)
1✔
152
                    ->value(fn (): mixed => $tmpModel->getAttribute($key));
1✔
153
            });
1✔
154

155
            return $fields;
1✔
156
        };
196✔
157

158
        return $this->__withFields($callback);
196✔
159
    }
160

161
    /**
162
     * Resolve the option fields.
163
     */
164
    public function resolveOptionFields(Request $request, Model $model, Model $tmpModel): Fields
1✔
165
    {
166
        return is_null($this->optionFieldsResolver)
1✔
167
            ? new Fields
×
168
            : call_user_func_array($this->optionFieldsResolver, [$request, $model, $tmpModel]);
1✔
169
    }
170

171
    /**
172
     * Make a new temporary model for the option.
173
     */
174
    public function newTemporaryModel(array $attributes = []): Model
1✔
175
    {
176
        $model = new class extends Model
1✔
177
        {
1✔
178
            //
179
        };
1✔
180

181
        return $model->forceFill(array_replace(
1✔
182
            ['_key' => Str::uuid()],
1✔
183
            $attributes
1✔
184
        ));
1✔
185
    }
186

187
    /**
188
     * Resolve the repeater options.
189
     */
190
    public function resolveOptions(Request $request, Model $model): array
2✔
191
    {
192
        $value = (array) $this->resolveValue($request, $model);
2✔
193

194
        return array_map(function (array $option) use ($request, $model): array {
2✔
NEW
195
            return $this->toOption($request, $model, $this->newTemporaryModel($option));
×
196
        }, $value);
2✔
197
    }
198

199
    /**
200
     * Build a new option.
201
     */
202
    public function buildOption(Request $request, Model $model): array
1✔
203
    {
204
        $option = $this->toOption($request, $model, $this->newTemporaryModel([]));
1✔
205

206
        $option['fields'] = $option['fields']->mapToInputs($request, $model);
1✔
207

208
        $option['html'] = View::make('root::fields.repeater-option', $option)->render();
1✔
209

210
        return $option;
1✔
211
    }
212

213
    /**
214
     * {@inheritdoc}
215
     */
216
    public function resolveFormat(Request $request, Model $model): ?string
3✔
217
    {
218
        if (is_null($this->formatResolver)) {
3✔
219
            $this->formatResolver = function (Request $request, Model $model, ?array $value = null): string {
3✔
220
                $values = array_map(fn (array $value): array => $this->resolveOptionFields($request, $model, $this->newTemporaryModel($value))
3✔
221
                    ->authorized($request, $model)
3✔
222
                    ->visible('show')
3✔
223
                    ->mapToDisplay($request, $model), (array) $value);
3✔
224

225
                return View::make('root::fields.repeater-table', ['values' => $values])->render();
3✔
226
            };
3✔
227
        }
228

229
        return parent::resolveFormat($request, $model);
3✔
230
    }
231

232
    /**
233
     * Register the routes using the given router.
234
     */
235
    public function registerRoutes(Request $request, Router $router): void
196✔
236
    {
237
        $this->__registerRoutes($request, $router);
196✔
238

239
        $router->prefix($this->getUriKey())->group(function (Router $router) use ($request): void {
196✔
240
            $this->resolveFields($request)->registerRoutes($request, $router);
196✔
241
        });
196✔
242
    }
243

244
    /**
245
     * The routes that should be registered.
246
     */
247
    public function routes(Router $router): void
196✔
248
    {
249
        $router->post('/', RepeaterController::class);
196✔
250
    }
251

252
    /**
253
     * Get the option representation of the model and the temporary model.
254
     */
255
    public function toOption(Request $request, Model $model, Model $tmpModel): array
1✔
256
    {
257
        return [
1✔
258
            'open' => true,
1✔
259
            'value' => $tmpModel->getAttribute('_key'),
1✔
260
            'label' => $this->getOptionName(),
1✔
261
            'fields' => $this->resolveOptionFields($request, $model, $tmpModel),
1✔
262
        ];
1✔
263
    }
264

265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function toInput(Request $request, Model $model): array
2✔
269
    {
270
        return array_merge(parent::toInput($request, $model), [
2✔
271
            'addNewLabel' => $this->getAddNewOptionLabel(),
2✔
272
            'max' => $this->max,
2✔
273
            'options' => array_map(static function (array $option) use ($request, $model): array {
2✔
274
                $option['fields'] = $option['fields']->mapToInputs($request, $model);
×
275

276
                return array_merge($option, [
×
277
                    'html' => View::make('root::fields.repeater-option', $option)->render(),
×
278
                ]);
×
279
            }, $this->resolveOptions($request, $model)),
2✔
280
            'url' => $this->replaceRoutePlaceholders($request->route()),
2✔
281
        ]);
2✔
282
    }
283

284
    /**
285
     * {@inheritdoc}
286
     */
287
    public function toValidate(Request $request, Model $model): array
3✔
288
    {
289
        return array_merge(
3✔
290
            parent::toValidate($request, $model),
3✔
291
            $this->resolveFields($request)->mapToValidate($request, $model)
3✔
292
        );
3✔
293
    }
294

295
    /**
296
     * Clone the field.
297
     */
298
    public function __clone(): void
×
299
    {
300
        $this->fields = null;
×
301
    }
302
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc