Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

dmytroshved's avatar

Problem with image preview in Livewire 3

I have a problem with image preview, it works for png, jpeg and webp, but not working with for example svg. It makes sense, because docs said that ->temporaryUrl() works only for those 3 types.

When Im providing svg image I see an error message.

Is there any options to fix that problem?

<form wire:submit="save">
    @if ($photo) 
        <img src="{{ $photo->temporaryUrl() }}">
    @endif
 
    <input type="file" wire:model="photo">
 
    @error('photo') <span class="error">{{ $message }}</span> @enderror
 
    <button type="submit">Save photo</button>
</form>
0 likes
5 replies
bvfi-dev's avatar

If you want completely secure uploads, always as a rule of thumb do Client and Server -side file upload validation. You can use AlpineJS to handle client side file uploads. If you need more code, I can give you something, otherwise this will be just wording. You want when a file is uploaded to be checked with Javascript first, preferably alpineJS and validate it. If its valid (Size, type, etc), then send it to the backend and double check it there as well. There are a lot of exploits you should and could handle both in the frontend and then some specific ones for the PHP backend. Heres some reference. Do not trust the accept attribute in the input, nor any frontend input attribute.

If the file is valid in both the front and back end, then store it to a variable and display the preview.

1 like
dmytroshved's avatar

@bvfi-dev Yes, show me the code If you can

Additionally I was using this code below (but switched to livewire 3 because this thumbnail disappears when you change step in the wizard form or change page)

<!-- Select Image -->
<label>Recipe image</label>
<div x-data="{ files: null }">
    <label for="recipe_image">
        <input 
            name="recipe_image" type="file"
            x-on:change="files = Object.values($event.target.files)"
            accept="image/jpeg, image/png, image/webp">

        <span x-text="files ? files.map(file => file.name).join(', ') : 'Select photo...'"></span>

    </label>

    <!-- mini photo -->
    <template x-if="files && files.length > 0">
        <div>
            <img :src="URL.createObjectURL(files[0])" alt="Thumbnail"/>
        </div>
    </template>

    <!-- reset button -->
    <button type="reset" @click="files = null">Reset</button>
</div>
1 like
bvfi-dev's avatar
bvfi-dev
Best Answer
Level 3

@Dmytro_Shved Heres some code, but its not tested, you should make sure it works with console.logs, because Im blindly writing it.

<div><!-- Extra wrapping div, because alpine sometimes doesnt play nice with livewire -->
<div x-data="fileUploader($wire)">
    <div>
        <input type="file" id="upload-file" accept="{{ $allowedMimes }}" x-ref="uploadingFile"
            class="hidden" x-on:change="uploadFile(event)">
        <x-button x-on:click.prevent="$refs.uploadingFile.click();">Upload File</x-button>
    </div>
</div>
</div>

Some explanation: No one wants the default upload button, so what this does is it hides the default upload button and replaces it with a <x-button> (Blade component, or you can use <button> html). Its made so that when this button is clicked, it acts as if the hidden input has been clicked. So, its just frontend manipulation. The x-data initializes AlpineJS, and I pass the $wire component in it. Im not sure if I wrap alpineJS with @script tags it would work, so this is a workaround to have a $wire ( Read more about $wire ) without @script tags wrapping the <script> tags. Read More about the @script

Then also in the frontend you would have to put this script somewhere:

<script>
function fileUploader($wire){
    return {
        uploading: false,
        allowedTypes: {!! json_encode(explode(',', $allowedMimes)) !!},
        maxSizePerType: {
            'application/pdf':  /* file limit in bytes, like 8 * 1024 would be 8MBs */,
            'image/jpg':  /* image limit in bytes */,
            'image/jpeg': /* image limit in bytes */,
            'image/png': {{ $uploadFileLimit * 1024 }}  /* Example to use the backend variable, ITS IN BYTES, remember, not KBs, not MBs, bytes */
         },
         uploadFile(event) {
             let files = Array.from(event.target.files || []);
             if (!files.length) return;
             const file = files[0];
             let mimeType = file.type;
             let maxSize = this.maxSizePerType[mimeType];
             let isFileValid = this.allowedTypes.includes(mimeType) && maxSize && file.size <= maxSize;
             if(isFileValid) {
                 $wire.$upload('uploadedFile', file); //Sets the public $uploadedFile that is in the backend
             else {
                 $wire.$set('uploadedFile', null);
                 files = [];
             }
         }
    }
}
</script>

Note again, that you should test every step of the way, with console logs, to check if its exactly what you want. I know some variables should be const instead of let, but doesnt really matter that much to me. These are just the basic checks for the frontend.

Then in the backend you would have:

#[Locked] //Make sure Locked is here, to prevent manipulation
public $allowedMimes = 'application/pdf,image/jpg,image/jpeg,image/png' //Or whatever value you need
#[Locked]
public $uploadFileLimit = 8 * 1024;
public $uploadedFile = null; //Instance of TemporaryUplaodedFile
$this->previewImageUrl = null;

Somewhere in the function have this, which is a livewire cycle hook:

public function updatedUploadedFile()
{
if($this->uploadedFile instanceof TemporaryUploadedFile) { 
    $uploadedFilePath = $uploadedFile->getRealPath();
    if(file_exists($uploadedFilePath)) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $fileType = finfo_file($finfo, $uploadedFilePath);
        finfo_close($finfo);
        $fileSize = filesize($uploadedFilePath) / 1024; // KBs MAKE SURE TO TEST THIS, I might be mixing up KBs, MBs and or bytes. You might have to do some extra */ 1024 depending on what you get, Im just trying to write this code fast
        $imgUploadLimit = $imgUploadLimitMB;
        if ($imgSize > $imgUploadLimit) {
            $this->previewImageUrl = null; //Make sure to use this appropriately on the right places, it depends on this what is shown in the frontend.
            // File exceeds limit, so set uploadedFile to null and throw error
        }
        if (($fileType === 'image/jpeg' && @imagecreatefromjpeg($tmpImgPath) === false) ||
            ($fileType === 'image/png' && @imagecreatefrompng($tmpImgPath) === false)) {
            // File is invalid, $this->uplaodedFile to null and throw error. This checks just for IMAGES, you should add more cases here handling more files if you have more files. I understand this is just for images and preview images, but I want to explain that you should check each file type separately. Check at the end for code for checking if PDF documents are spoofed for example
        }
    // If all checks pass and the uploadedFile is not null, set the previewImageUrl:
    if(isset($this->uploadedFile)) $this->previewImageUrl = $this->uploadedFile->temporaryUrl();
    }
}

}

With this, everytime you pass a file from the frontend to the backend, this function gets called and it processes the file.

So at the end you jsut need a check for the preiew:

@if ($previewImageUrl)
    <img src="{{ $previewImageUrl }}" alt="Preview Image" style="max-width: 100%; height: auto;">
@endif

Also, add in <style> tags:

[x-cloak] { display: none !important; }

To make Alpine's x-cloak work

Example to check if a PDF is spoofed:

//Spoofer Doofer
$stream = fopen($tmpDocPath, 'rb');
if ($stream) {
    $firstBytes = fread($stream, 5);
    fclose($stream);
    if (!str_starts_with($firstBytes, '%PDF-')) {
        //Return invalid file type error message
    }

I believe I have given you more than enough to have code to work with, again, remember, this is just REFERENCE code, its not tested and you should build it step by step and test each step accordingly.

1 like
dmytroshved's avatar

I've played around with a few examples of image preview logic and with the @bvfi code

I've achieved a nice logic using

RecipeWizard.php:

#[Validate] // real-time validation
    #[Rule(['nullable','mimes:jpeg,png,webp'])]
    public $recipe_image;

It doesn't have a js logic and it works well

Very grateful for help!

2 likes

Please or to participate in this conversation.