• 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

84.96
/src/Fields/File.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Root\Fields;
6

7
use Closure;
8
use Cone\Root\Jobs\MoveFile;
9
use Cone\Root\Jobs\PerformConversions;
10
use Cone\Root\Models\Medium;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Http\Request;
13
use Illuminate\Http\UploadedFile;
14
use Illuminate\Support\Arr;
15
use Illuminate\Support\Facades\Config;
16
use Illuminate\Support\Facades\Storage;
17
use Illuminate\Support\Facades\URL;
18
use Illuminate\Support\Facades\View;
19

20
class File extends MorphToMany
21
{
22
    /**
23
     * The Blade template.
24
     */
25
    protected string $template = 'root::fields.file';
26

27
    /**
28
     * The storage resolver callback.
29
     */
30
    protected ?Closure $storageResolver = null;
31

32
    /**
33
     * Indicates whether the file input is prunable.
34
     */
35
    protected bool $prunable = false;
36

37
    /**
38
     * The storage disk.
39
     */
40
    protected string $disk;
41

42
    /**
43
     * The displayable conversion name.
44
     */
45
    protected ?string $displayConversion = 'original';
46

47
    /**
48
     * Create a new field instance.
49
     */
50
    public function __construct(string $label, Closure|string|null $modelAttribute = null, Closure|string|null $relation = null)
197✔
51
    {
52
        parent::__construct($label, $modelAttribute, $relation);
197✔
53

54
        $this->name($this->modelAttribute);
197✔
55
        $this->type('file');
197✔
56
        $this->multiple(false);
197✔
57
        $this->class(['form-file']);
197✔
58
        $this->disk(Config::get('root.media.disk', 'public'));
197✔
59
    }
60

61
    /**
62
     * Set the "multiple" HTML attribute.
63
     */
64
    public function multiple(bool $value = true): static
2✔
65
    {
66
        $this->setAttribute('multiple', $value);
2✔
67

68
        return $this;
2✔
69
    }
70

71
    /**
72
     * Set the "accept" HTML attribute.
73
     */
74
    public function accept(string $value): static
×
75
    {
76
        $this->setAttribute('accept', $value);
×
77

78
        return $this;
×
79
    }
80

81
    /**
82
     * Set the disk attribute.
83
     */
84
    public function disk(string $value): static
197✔
85
    {
86
        $this->disk = $value;
197✔
87

88
        return $this;
197✔
89
    }
90

91
    /**
92
     * Set the collection pivot value.
93
     */
94
    public function collection(string $value): static
1✔
95
    {
96
        $this->pivotValues['collection'] = $value;
1✔
97

98
        return $this;
1✔
99
    }
100

101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function resolveDisplay(Model $related): ?string
2✔
105
    {
106
        if (is_null($this->displayResolver)) {
2✔
107
            $this->display(function (Medium $related): string {
2✔
108
                return $related->isImage
2✔
109
                    ? sprintf(
2✔
110
                        '<img src="%s" width="40" height="40" alt="%s">',
2✔
111
                        $related->getUrl($this->displayConversion), $related->name
2✔
112
                    )
2✔
113
                    : $related->file_name;
2✔
114
            });
2✔
115
        }
116

117
        return parent::resolveDisplay($related);
2✔
118
    }
119

120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function formatRelated(Request $request, Model $model, Model $related): ?string
×
124
    {
125
        $value = $this->resolveDisplay($related);
×
126

127
        if (! $this->resolveAbility('view', $request, $model, $related)) {
×
128
            return $value;
×
129
        }
130

131
        return sprintf('<a href="%s" download>%s</a>', URL::signedRoute('root.download', $related), $value);
×
132
    }
133

134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function resolveOptions(Request $request, Model $model): array
2✔
138
    {
139
        return $this->resolveValue($request, $model)
2✔
140
            ->map(function (Medium $medium) use ($request, $model): array {
2✔
NEW
141
                return $this->toOption($request, $model, $medium);
×
142
            })
2✔
143
            ->all();
2✔
144
    }
145

146
    /**
147
     * Set the storage resolver callback.
148
     */
149
    public function storeUsing(Closure $callback): static
×
150
    {
151
        $this->storageResolver = $callback;
×
152

153
        return $this;
×
154
    }
155

156
    /**
157
     * Store the uploaded file.
158
     */
159
    public function store(Request $request, Model $model, UploadedFile $file): array
1✔
160
    {
161
        $disk = Storage::build(Config::get('root.media.tmp_dir'));
1✔
162

163
        $disk->putFileAs('', $file, $file->getClientOriginalName());
1✔
164

165
        return $this->stored($request, $model, $disk->path($file->getClientOriginalName()));
1✔
166
    }
167

168
    /**
169
     * Handle the stored event.
170
     */
171
    protected function stored(Request $request, Model $model, string $path): array
2✔
172
    {
173
        $target = str_replace($request->header('X-Chunk-Hash', ''), '', $path);
2✔
174

175
        $medium = (Medium::proxy())::fromPath($path, [
2✔
176
            'disk' => $this->disk,
2✔
177
            'file_name' => $name = basename($target),
2✔
178
            'name' => pathinfo($name, PATHINFO_FILENAME),
2✔
179
        ]);
2✔
180

181
        if (! is_null($this->storageResolver)) {
2✔
182
            call_user_func_array($this->storageResolver, [$request, $medium, $path]);
×
183
        }
184

185
        /** @var \Illuminate\Foundation\Auth\User&\Cone\Root\Interfaces\Models\User $user */
186
        $user = $request->user();
2✔
187

188
        $user->uploads()->save($medium);
2✔
189

190
        MoveFile::withChain($medium->convertible() ? [new PerformConversions($medium)] : [])
2✔
191
            ->dispatch($medium, $path, false);
2✔
192

193
        $option = $this->toOption($request, $model, $medium);
2✔
194

195
        return array_merge($option, [
2✔
196
            'html' => View::make('root::fields.file-option', $option)->render(),
2✔
197
        ]);
2✔
198
    }
199

200
    /**
201
     * Set the prunable attribute.
202
     */
203
    public function prunable(bool $value = true): static
×
204
    {
205
        $this->prunable = $value;
×
206

207
        return $this;
×
208
    }
209

210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function persist(Request $request, Model $model, mixed $value): void
1✔
214
    {
215
        $model->saved(function (Model $model) use ($request, $value): void {
1✔
216
            $files = Arr::wrap($request->file($this->getRequestKey(), []));
1✔
217

218
            $ids = array_map(fn (UploadedFile $file): int => $this->store($request, $model, $file)['value'], $files);
1✔
219

220
            $value += $this->mergePivotValues($ids);
1✔
221

222
            $this->resolveHydrate($request, $model, $value);
1✔
223

224
            $keys = $this->getRelation($model)->sync($value);
1✔
225

226
            if ($this->prunable && ! empty($keys['detached'])) {
1✔
227
                $this->prune($request, $model, $keys['detached']);
×
228
            }
229
        });
1✔
230
    }
231

232
    /**
233
     * Prune the related models.
234
     */
235
    public function prune(Request $request, Model $model, array $keys): array
1✔
236
    {
237
        $deleted = [];
1✔
238

239
        $this->resolveRelatableQuery($request, $model)
1✔
240
            ->whereIn('id', $keys)
1✔
241
            ->cursor()
1✔
242
            ->each(static function (Medium $medium) use ($request, &$deleted): void {
1✔
243
                if ($request->user()->can('delete', $medium)) {
1✔
244
                    $medium->delete();
1✔
245

246
                    $deleted[] = $medium->getKey();
1✔
247
                }
248
            });
1✔
249

250
        return $deleted;
1✔
251
    }
252

253
    /**
254
     * Determine if the relation is a subresource.
255
     */
256
    public function isSubResource(): bool
10✔
257
    {
258
        return false;
10✔
259
    }
260

261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function toOption(Request $request, Model $model, Model $related): array
2✔
265
    {
266
        /** @var \Cone\Root\Models\Medium $related */
267
        $option = parent::toOption($request, $model, $related);
2✔
268

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

276
        $option['attrs']->merge(['name' => $name]);
2✔
277

278
        /** @var \Cone\Root\Models\Medium $related */
279
        $option = array_merge($option, [
2✔
280
            'fileName' => $related->file_name,
2✔
281
            'isImage' => $related->isImage,
2✔
282
            'processing' => false,
2✔
283
            'url' => $related->getUrl($this->displayConversion),
2✔
284
            'uuid' => $related->uuid,
2✔
285
        ]);
2✔
286

287
        $option['html'] = View::make('root::fields.file-option', $option)->render();
2✔
288

289
        return $option;
2✔
290
    }
291

292
    /**
293
     * {@inheritdoc}
294
     */
295
    public function toInput(Request $request, Model $model): array
2✔
296
    {
297
        return array_merge(parent::toInput($request, $model), [
2✔
298
            'options' => $this->resolveOptions($request, $model),
2✔
299
        ]);
2✔
300
    }
301
}
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