Fotos, Laravel y VueJS

Para dar la opción al usuario de poder subir fotos a la aplicación Web he utilizado el componente vue-media-upload de Saimow.

Instalo mediante npm

npm install vue-media-upload

El componente funciona subiendo las imágenes con Axios a una carpeta temporal cada vez que añadimos una. Luego si queremos guardar las imágenes hay que pasarlas a la carpeta final.

Definimos la ruta para subir las carpetas a la ruta temporal:

Route::post('/media/upload', [MediaController::class, 'uploadMedia'])-> name('media.upload')->;

En la página Vue:

import Uploader from 'vue-media-upload';

// los datos de las imágenes obtenidas las guardaré a través del componente useForm de Inertia
const form = useForm({
    _method: 'POST',
    ...
    media: {},
});

// Definimos los eventos necesarios del Uploader para actualizar los datos de las imágenes añadidas o eliminadas
const addMedia = (addedImage, addedMedia) => {
    form.media.added = addedMedia
}
const removeMedia = (removedImage, removedMedia) => {
    form.media.removed = removedMedia
}

// Al montar el componente Vue vamos a obtener las imagenes
onMounted(() => {
    getMedia();

});
const getMedia = () => {
    axios.get(route('ruta.get.media', form.id))
        .then(response => {
            form.media = {saved: [], added:[], removed:[]};
            form.media.saved = response.data.media;
            form.defaults('media', form.media);  // Para quitar el aviso de que hay cambios pendientes de grabar.

            hasMediaResponse.value = true;  // Aquí es donde vamos a dar la señal para generar el componente Uploader
        })
}

...

<Uploader
    v-if="hasMediaResponse"
    :server="route('media.upload')"
    :media="form.media.saved"
    location="/media"
    @add="addMedia"
    @remove="removeMedia"
/>

Es importante el v-if para que solo se genere el componente cuando ya ha obtenido las imágenes del servidor, sino, no se actualizará.

A continuación vamos a completar el controlador encargado de subir y grabar las fotos:

public function getMedia(Request $request, int $work_id)
{
    $media = Media::
        select('type', 'file_name as name')->
        whereModelId($work_id)->get();

    return response()->json(['media' => $media]);
}

// Se llama cuando al componente se le asigna una imagen, la sube a una carpeta temporal
public function uploadMedia(Request $request)
{
    if ($request->hasFile('image'))
    {
        // Guardo en carpeta temporal
        $file = $request->file('image');
        $name = uniqid() . '_' . $file->getClientOriginalName();

        $file->storeAs('tmp', $name);

        return ['name' => $name];            
    }
}

// Grabar el modelo con las imágenes
public function update(Request $request)
{
    ...
    $model->save();

    $this->saveTemporaryFilesToMedia($model->id, $request->media['added']);
    $this->removeFilesFromMedia($model->id, $request->media['removed']);

    return redirect(route('...'));
}

// Guarda las imágenes temporales que finalmente se hayan grabado
private function saveTemporaryFilesToMedia(int $model_id, array $media)
{
    foreach($media as $image){
        $from = storage_path('app/tmp/' . $image['name']);
        $to = storage_path('app/media/' . $image['name']);

        File::move($from, $to);

        $media = Media::create([
            'model_name' => Work::class,
            'model_id' => $model_id,
            'type' => '',
            'file_name' => $image['name'],
        ]);
    }
}

// Elimina las imagenes guardadas que se han borrado (no las temporales)
private function removeFilesFromMedia(int $model_id, array $media)
{
    foreach($media as $image) {
        File::delete('app/media/' . $image['name']);

        Media::
            whereModelName(Work::class)->
            whereModelId($model_id)->
            whereFileName($image['name'])->
            delete();
    }
}

Las imágenes las guardo en la carpeta storage la cual no es pública, por lo que debo crear una función en el controlador para obtenerlas. Primero defino la ruta:

Route::get('/media/{file_name}', [MediaController::class, 'getFile'])->name('media.get');

Y su definición en el controlador:

public function getFile(Request $request, string $fileName)
{
    return response()->file(storage_path('app/media/') . $fileName);
}

Por último nos faltaría generar un cron para que todas las noches se borraran todas las imágenes de la carpeta temporal, eso para más adelante.