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

conedevelopment / root / 15084010432

17 May 2025 09:46AM UTC coverage: 77.93% (+0.04%) from 77.891%
15084010432

Pull #240

github

web-flow
Merge f6dd44bc7 into abd60b533
Pull Request #240: Modernize back-end.yml

3291 of 4223 relevant lines covered (77.93%)

36.04 hits per line

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

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

3
namespace Cone\Root\Fields;
4

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

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

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

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

35
    /**
36
     * The storage disk.
37
     */
38
    protected string $disk;
39

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

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

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

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

66
        return $this;
2✔
67
    }
68

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

76
        return $this;
×
77
    }
78

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

86
        return $this;
198✔
87
    }
88

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

96
        return $this;
1✔
97
    }
98

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

110
        return parent::resolveDisplay($related);
2✔
111
    }
112

113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function formatRelated(Request $request, Model $model, Model $related): ?string
×
117
    {
118
        $value = $this->resolveDisplay($related);
×
119

120
        if ($related->isImage || ! $this->resolveAbility('view', $request, $model, $related)) {
×
121
            return $value;
×
122
        }
123

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

127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function resolveOptions(Request $request, Model $model): array
2✔
131
    {
132
        return $this->resolveValue($request, $model)
2✔
133
            ->map(function (Medium $medium) use ($request, $model): array {
2✔
134
                $option = $this->toOption($request, $model, $medium);
×
135

136
                return array_merge($option, [
×
137
                    'html' => View::make('root::fields.file-option', $option)->render(),
×
138
                ]);
×
139
            })
2✔
140
            ->all();
2✔
141
    }
142

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

150
        return $this;
×
151
    }
152

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

160
        $disk->putFileAs('', $file, $file->getClientOriginalName());
1✔
161

162
        return $this->stored($request, $model, $disk->path($file->getClientOriginalName()));
1✔
163
    }
164

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

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

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

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

185
        $user->uploads()->save($medium);
2✔
186

187
        MoveFile::withChain($medium->convertible() ? [new PerformConversions($medium)] : [])
2✔
188
            ->dispatch($medium, $path, false);
2✔
189

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

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

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

204
        return $this;
×
205
    }
206

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

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

217
            $value += $this->mergePivotValues($ids);
1✔
218

219
            $this->resolveHydrate($request, $model, $value);
1✔
220

221
            $keys = $this->getRelation($model)->sync($value);
1✔
222

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

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

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

243
                    $deleted[] = $medium->getKey();
1✔
244
                }
245
            });
1✔
246

247
        return $deleted;
1✔
248
    }
249

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

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

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

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

275
        /** @var \Cone\Root\Models\Medium $related */
276

277
        return array_merge($option, [
2✔
278
            'fileName' => $related->file_name,
2✔
279
            'isImage' => $related->isImage,
2✔
280
            'processing' => false,
2✔
281
            'url' => ! is_null($this->displayConversion) && $related->hasConversion($this->displayConversion)
2✔
282
                ? $related->getUrl($this->displayConversion)
×
283
                : $related->getUrl(),
2✔
284
            'uuid' => $related->uuid,
2✔
285
        ]);
2✔
286
    }
287

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