Overview
Attachments in WireChat provide a flexible, polymorphic system for handling file uploads. They can be attached to messages or used as group cover images, with support for both local and cloud storage.
Attachment Model
Attachments use a polymorphic relationship to attach files to various models:
use Wirechat\Wirechat\Models\ Attachment ;
/**
* @property int $id
* @property string $attachable_type
* @property int $attachable_id
* @property string $file_path
* @property string $file_name
* @property string $original_name
* @property string $url
* @property string $mime_type
* @property \Illuminate\Support\ Carbon | null $created_at
* @property \Illuminate\Support\ Carbon | null $updated_at
*/
Polymorphic Attachable
Attachments can be attached to different models:
// Attach to a message
$attachment = Attachment :: create ([
'attachable_type' => Message :: class ,
'attachable_id' => $message -> id ,
'file_path' => 'uploads/files/document.pdf' ,
'file_name' => 'document.pdf' ,
'original_name' => 'My Document.pdf' ,
'mime_type' => 'application/pdf' ,
]);
// Attach to a group (cover image)
$attachment = Attachment :: create ([
'attachable_type' => Group :: class ,
'attachable_id' => $group -> id ,
'file_path' => 'uploads/covers/image.jpg' ,
'file_name' => 'image.jpg' ,
'original_name' => 'Group Cover.jpg' ,
'mime_type' => 'image/jpeg' ,
]);
Relationships
Attachable
The polymorphic relationship to the parent model:
public function attachable () : MorphTo
{
return $this -> morphTo ();
}
// Usage
$owner = $attachment -> attachable ; // Returns Message or Group
Message Attachments
Messages have a one-to-one morphOne relationship:
// From Message model
public function attachment () : MorphOne
{
return $this -> morphOne ( Attachment :: class , 'attachable' );
}
// Check if message has attachment
if ( $message -> hasAttachment ()) {
$attachment = $message -> attachment ;
}
Group Cover Images
Groups also use the attachment system:
// From Group model
public function cover () : MorphOne
{
return $this -> morphOne ( Attachment :: class , 'attachable' );
}
// Access cover
if ( $group -> cover ) {
echo $group -> cover_url ;
}
Storage Configuration
Attachments use the configured storage disk from WireChat:
use Wirechat\Wirechat\Facades\ Wirechat ;
$disk = Wirechat :: storage () -> disk (); // 'public', 's3', etc.
$visibility = Wirechat :: storage () -> visibility (); // 'public' or 'private'
URL Generation
Attachment URLs are generated dynamically based on storage configuration:
protected function url () : Attribute
{
return Attribute :: make (
get : fn ( $value , array $attributes ) =>
$this -> generateUrl ( $attributes [ 'file_path' ] ?? null )
);
}
protected function generateUrl ( ? string $path ) : ? string
{
if ( ! $path ) {
return null ;
}
$diskVisibility = Wirechat :: storage () -> visibility ();
$storageDisk = Wirechat :: storage () -> disk ();
$disk = Storage :: disk ( $storageDisk );
// If the disk is set to private, generate a temporary URL
if ( $diskVisibility === 'private' ) {
return $disk -> temporaryUrl ( $path , now () -> addMinutes ( 5 ));
}
return $disk -> url ( $path );
}
For private storage, WireChat automatically generates temporary URLs that expire after 5 minutes.
Accessing URLs
Get attachment URLs easily:
// Direct URL access
echo $attachment -> url ;
// For messages
if ( $message -> hasAttachment ()) {
echo $message -> attachment -> url ;
}
// For groups
echo $group -> cover_url ; // Uses cover->url internally
MIME Types
Attachments store the full MIME type and provide a clean accessor:
// Full MIME type
echo $attachment -> mime_type ; // "image/jpeg"
// Clean type (extension only)
echo $attachment -> clean_mime_type ; // "jpeg"
From the source code:
public function getCleanMimeTypeAttribute () : string
{
return explode ( '/' , $this -> mime_type )[ 1 ] ?? 'unknown' ;
}
File Names
Attachments track both the stored filename and original filename:
// Stored filename (sanitized)
echo $attachment -> file_name ; // "abc123-document.pdf"
// Original filename (user's original)
echo $attachment -> original_name ; // "My Important Document.pdf"
Always sanitize filenames before storing them to prevent security issues and filesystem conflicts.
Creating Attachments
Complete example of creating an attachment:
use Illuminate\Http\ UploadedFile ;
use Illuminate\Support\Facades\ Storage ;
use Wirechat\Wirechat\Facades\ Wirechat ;
function uploadAttachment ( UploadedFile $file , Message $message ) : Attachment
{
$disk = Wirechat :: storage () -> disk ();
// Store the file
$path = $file -> store ( 'uploads/messages' , $disk );
// Create attachment record
return Attachment :: create ([
'attachable_type' => Message :: class ,
'attachable_id' => $message -> id ,
'file_path' => $path ,
'file_name' => basename ( $path ),
'original_name' => $file -> getClientOriginalName (),
'mime_type' => $file -> getMimeType (),
]);
}
Automatic Deletion
Attachments are automatically deleted from storage when the model is deleted:
protected static function boot () : void
{
parent :: boot ();
static :: deleted ( function ( Attachment $media ) {
$disk = Wirechat :: storage () -> disk ();
if ( Storage :: disk ( $disk ) -> exists ( $media -> file_path )) {
Storage :: disk ( $disk ) -> delete ( $media -> file_path );
}
});
}
When an attachment record is deleted, the physical file is automatically removed from storage. This is irreversible.
Message Attachments
Sending a message with an attachment:
use Wirechat\Wirechat\Enums\ MessageType ;
// Create message
$message = Message :: create ([
'body' => 'Check out this file!' ,
'type' => MessageType :: ATTACHMENT ,
'participant_id' => $participant -> id ,
'conversation_id' => $conversation -> id ,
]);
// Upload and attach file
$attachment = uploadAttachment ( $file , $message );
// Check attachment
if ( $message -> isAttachment ()) {
echo $message -> attachment -> url ;
}
Group Cover Images
Uploading a group cover image:
function updateGroupCover ( UploadedFile $file , Group $group ) : Attachment
{
$disk = Wirechat :: storage () -> disk ();
// Delete existing cover
if ( $group -> cover ) {
$group -> cover -> delete ();
}
// Store new cover
$path = $file -> store ( 'uploads/covers' , $disk );
return Attachment :: create ([
'attachable_type' => Group :: class ,
'attachable_id' => $group -> id ,
'file_path' => $path ,
'file_name' => basename ( $path ),
'original_name' => $file -> getClientOriginalName (),
'mime_type' => $file -> getMimeType (),
]);
}
File Validation
Validate uploads before creating attachments:
use Illuminate\Support\Facades\ Validator ;
function validateAttachment ( UploadedFile $file ) : bool
{
$validator = Validator :: make (
[ 'file' => $file ],
[
'file' => [
'required' ,
'file' ,
'max:10240' , // 10MB
'mimes:jpg,jpeg,png,pdf,doc,docx,txt' ,
],
]
);
return $validator -> passes ();
}
Always validate file size and type before storing to prevent security issues and storage bloat.
Display Attachments
Render attachments based on MIME type:
@ if ( $message -> hasAttachment ())
@ php
$attachment = $message -> attachment ;
$mimeType = $attachment -> clean_mime_type ;
@ endphp
@ if ( str_starts_with ( $attachment -> mime_type , 'image/' ))
< img src = "{{ $attachment -> url }}" alt = "{{ $attachment -> original_name }}" />
@ elseif ( str_starts_with ( $attachment -> mime_type , 'video/' ))
< video controls src = "{{ $attachment -> url }}" ></ video >
@ elseif ( str_starts_with ( $attachment -> mime_type , 'audio/' ))
< audio controls src = "{{ $attachment -> url }}" ></ audio >
@ else
< a href = "{{ $attachment -> url }}" download >
{{ $attachment -> original_name }} ( . {{ $mimeType }})
</ a >
@ endif
@ endif
Best Practices
Always validate file uploads before processing
Sanitize filenames to prevent directory traversal attacks
Set appropriate file size limits based on your needs
Use private storage for sensitive files
Delete old attachments when updating (especially for covers)
Consider image optimization for cover images
Use chunked uploads for large files
Implement virus scanning for production applications
Security Considerations
Never trust user-supplied MIME types alone - validate file contents
Restrict allowed file types based on your application needs
Use private storage for sensitive documents
Implement proper access controls for attachment URLs
Consider rate limiting file uploads
Scan uploaded files for malware in production
Common Patterns
Complete File Upload Flow
use Illuminate\Http\ Request ;
use Illuminate\Support\Facades\ DB ;
function sendMessageWithAttachment ( Request $request , Conversation $conversation )
{
$request -> validate ([
'message' => 'nullable|string|max:1000' ,
'file' => 'required|file|max:10240|mimes:jpg,jpeg,png,pdf,doc,docx' ,
]);
return DB :: transaction ( function () use ( $request , $conversation ) {
// Create message
$message = Message :: create ([
'body' => $request -> message ,
'type' => MessageType :: ATTACHMENT ,
'participant_id' => $conversation -> participant ( auth () -> user ()) -> id ,
'conversation_id' => $conversation -> id ,
]);
// Upload file
$disk = Wirechat :: storage () -> disk ();
$path = $request -> file ( 'file' ) -> store ( 'uploads/messages' , $disk );
// Create attachment
Attachment :: create ([
'attachable_type' => Message :: class ,
'attachable_id' => $message -> id ,
'file_path' => $path ,
'file_name' => basename ( $path ),
'original_name' => $request -> file ( 'file' ) -> getClientOriginalName (),
'mime_type' => $request -> file ( 'file' ) -> getMimeType (),
]);
return $message -> load ( 'attachment' );
});
}
Check File Type
function isImage ( Attachment $attachment ) : bool
{
return str_starts_with ( $attachment -> mime_type , 'image/' );
}
function isVideo ( Attachment $attachment ) : bool
{
return str_starts_with ( $attachment -> mime_type , 'video/' );
}
function isDocument ( Attachment $attachment ) : bool
{
return in_array ( $attachment -> clean_mime_type , [ 'pdf' , 'doc' , 'docx' , 'txt' ]);
}
Next Steps
Messages Learn how attachments work with messages
Groups Understand group cover images