byrcsc/laravel-comments · 1.x
Attachments.
Record metadata about files your application stored, attach processed images in one call, and clean up disks from the removal event.
An attachment is a row of metadata about a file your application stored: a disk name, a path on it, and what the application said the file is called, is, and weighs.
The package never opens the file, never checks that it is there, and never deletes it. Serving it, authorizing the download, and cleaning it up stay on your side of the line.
Record a file
$path = $request->file('receipt')->store('receipts', 'uploads');
$attachment = $comment->attach(
path: $path,
disk: 'uploads',
name: $request->file('receipt')->getClientOriginalName(),
mimeType: $request->file('receipt')->getMimeType(),
size: $request->file('receipt')->getSize(),
);Only path is required:
| Argument | Falls back to |
|---|---|
disk | comments.attachments.disk, then the application's default disk |
name | The path's basename |
mimeType | Stays null |
size | Stays null |
Size and MIME type stay null when you did not measure them. A guess recorded as fact is worse than an honest absence.
The metadata is recorded as given and never verified against the disk. Attaching a path that holds nothing succeeds, because the package has no business reading the file to find out.
Read them
$comment->attachments; // oldest first
foreach ($comment->attachments as $attachment) {
$attachment->disk;
$attachment->path;
$attachment->name;
$attachment->mime_type;
$attachment->size;
}Rendering them is ordinary Eloquent, and so is eager-loading a whole thread's
with with('attachments').
Remove one
$comment->detach($attachment); // returns whether there was one to removeThe file on disk is untouched, here as everywhere. Passing an attachment that
belongs to a different comment throws an InvalidAttachmentException rather
than removing it.
Delete the file
Deleting bytes belongs in a listener on AttachmentRemoved, where the row is
still in hand and its disk and path are still readable:
use ByRcsc\LaravelComments\Events\AttachmentRemoved;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
Event::listen(AttachmentRemoved::class, function (AttachmentRemoved $event): void {
Storage::disk($event->attachment->disk)->delete($event->attachment->path);
});That listener covers both removal paths. detach() fires it once, and a force
delete fires it once per attachment across the whole subtree the cascade takes
— replies and their tombstones included, because a listener that never heard
about a reply's files would leave them on the disk forever.
A soft delete fires nothing, because nothing was removed.
Attach an image
attachImage() processes an uploaded image, stores it, and records it in one
call:
use Illuminate\Support\Facades\Image;
$comment->attachImage(Image::fromUpload($request->file('screenshot')));Everything about the processing is the framework's — hand it whatever
Image::fromUpload() and friends give you, configured however you like:
$comment->attachImage(
Image::fromUpload($request->file('screenshot'))->resize(1600, 900),
name: 'Screenshot.png',
disk: 'uploads',
directory: 'comments/screenshots',
);The disk and directory fall back to comments.attachments.disk and
comments.attachments.directory. The name falls back to the uploaded file's
own name carrying the extension of what was actually stored — a screenshot.png
optimized to WebP is recorded as screenshot.webp, because a row whose name
disagrees with its own bytes is metadata that lies.
This is the one path where the package writes bytes to a disk, and it writes only the ones it was handed. Nothing is read back afterwards.
The optimize flag
$optimize defaults to true and applies the framework's own optimize step —
WebP at its default quality — which is what makes the common screenshot case one
line.
Pass optimize: false when the pipeline you handed over already says what the
output should be:
$comment->attachImage(
Image::fromUpload($file)->toPng(),
optimize: false,
);Image keeps its pipeline private, so the package cannot ask whether you
configured one and would otherwise overwrite your format. The flag is that
question, asked of the caller.
Requirements
attachImage() needs the framework's Image facade, which arrived in Laravel
13, and intervention/image, which the package suggests rather than requires:
composer require intervention/imageWithout it the call throws an ImageSupportMissingException naming the missing
dependency, rather than a driver error three frames away. Nothing else in the
package needs it — attach() works without an image library at all.
Validation
The package checks presence and types, and nothing further:
- A blank path or a blank name throws an
InvalidAttachmentException. - A negative size throws the same. Zero is accepted — an empty file is a real file.
- Attaching to an unsaved comment throws a
LogicException.
Whether the bytes are really on that disk is your application's to know: it is what put them there.
Deleted comments
A tombstone keeps the attachments it already had and takes no new ones, for the
same reason its reactions are frozen: a moderator reading what happened needs
the record to have stopped changing. Attaching to or detaching from a
soft-deleted comment throws a CommentTrashedException. Restoring it makes both
work again.
Force deleting removes the rows through the cascade, for the comment and its
whole subtree — and fires AttachmentRemoved for each one first.
Reading is never gated.
Events
| Event | Fires when |
|---|---|
AttachmentAdded | attach() or attachImage() records a row |
AttachmentRemoved | detach() removes one, or a force delete takes one |
Both extend CommentAttachmentChanged and carry the comment and the attachment
model. On the force-delete sweep, the comment the event carries is the one that
held the attachment — not necessarily the one the caller deleted.
Authorization
attach is an ability on the shipped CommentPolicy and allows any
authenticated actor by default, including on somebody else's comment: a
moderator adding evidence to a reported comment is as ordinary as an author
adding a screenshot to their own. Narrow it to authors by overriding. See
authorization.