pmall's avatar
Level 56

Form request like uploading

I came up with a cool way of uploading files inspired of form request mechanism. Tell me what you think about it :

An interface for uploaded files :

<?php namespace App\Http\Files;

interface UploadWhenResolvedInterface {

    public function upload();

}

Call the upload method when it gets resolved :

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Http\Files\UploadWhenResolvedInterface;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->resolving(function(UploadWhenResolvedInterface $instance, $app)
        {
            $instance->upload();
        });
    }
}

The abstract class implementing base functionality of UploadWhenResolvedInterface :

<?php namespace App\Http\Files;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Contracts\Filesystem\Factory as Filesystem;

class UploadedFile implements UploadWhenResolvedInterface {

    private $request;
    private $filesystem;
    public $path = '';

    protected $disk = 'local';
    protected $name = '';

    public function __construct (Request $request, Filesystem $filesystem) {

        $this->request = $request;
        $this->filesystem = $filesystem;

    }

    public function upload () {

        // Check existence of disk and file
        if (! $disk = $this->getDisk()) throw new Exception("No disk");
        if (! $file = $this->getFile()) throw new Exception("No file");

        // Check valididty of the upload
        if (! $file->isValid()) throw new Exception("File not valid");

        // Get the uploaded file real path
        $real_path = $file->getRealPath();

        // Get a relative path from the disk's root and set it
        $this->path = $this->getRelativePath(
            $file->getClientOriginalName(),
            $file->getClientOriginalExtension()
        );

        // Put the uploaded file content to the path
        $disk->put($this->path, file_get_contents($real_path));

        // Remove the uploaded file
        unset($real_path);

    }

    public function getPath () {

        return $this->path;

    }

    protected function getDisk () {

        return $this->filesystem->disk($this->disk);

    }

    protected function getFile () {

        // If a name is specified get the file named like this
        if ($this->name) return $this->request->file($this->name);

        // Otherwise get the first uploaded file
        $keys = $this->request->files->keys();

        return $this->request->file($keys[0]);

    }

    protected function getRelativePath ($file_name, $file_ext) {

        // Generate a random file name and get some subdirs
        $random = md5(str_random() . $file_name);

        $parts = str_split($random, 2);

        $subdirs = array_slice($parts, 0, 6);

        // Get the relative path
        $path = implode($subdirs, '/') . '/' . $random . '.' . $file_ext;

        // Ensure a file with the same path doesn't already exists
        if ($this->getDisk()->exists($path)) {

            return $this->getRelativePath($file_name, $file_ext);

        }

        return $path;

    }

}

A concrete uploaded file :

  • filesystem disk name and file input name can be specified with the $disk and $name attributes. By default local disk and first uploaded file are used (see abstract class)
  • getRelativePath can be overrided if you want to make a custom file path from the file name and ext. By default it md5 the filename + random string and append subdirectories (see abstract class)
  • This file is not needed if you use local disk and your form only upload one file
<?php namespace App\Http\Files;

class UploadedPicture extends UploadedFile {

    public $disk = 'pictures';
    public $name = 'my_picture';

}

Then no more headaches at controller level :

Route::post('files', function (UploadedPicture $uploaded_picture) {

    // The file from input 'my_picture' is uploaded in the 'pictures' disk with an unique path
    echo $uploaded_picture->getPath();

});

I would gladly read your reviews and suggestions :)

0 likes
6 replies
phildawson's avatar

Very nice!

I've done something very similar on one of the projects and seemed to work well. I like your solution.

Another recent was an AttachmentTrait that has the attachments() morphMany relationship and hooks into the save so in the model you would list protected $attachments = ['profile','featured','home']; etc. All models implemented a simple AttachmentInterface with eg AttachmentName could be used in a FileManager to display the item it attached against. Unfortunately I can't go into specifics, pretty simple but a great time/code saver.

HRcc's avatar

This really looks like a neat solution :) Generally I tend to use queued jobs for this task, since there is often some additional processing or S3 upload involved. However firing an event at the end of upload method could solve this.

bobbybouwmann's avatar

Nice work man, really nice from you to share your solution with us :D

pmall's avatar
Level 56

@phildawson thanks for the feedback. I was doing it like this before but i wanted a solution less tied to models. Just have a file already set up in the controller without worrying about upload logic. Also there is finally a place and a class where it makes sense to me to have upload logic.

@bobbybouwmann It striked me than logic can be fired when a class gets resolved. This is an unusual and very helpful feature, I wanted to share :D

@HRcc thanks for pointing me this I didn't through about it. Upload method can be overriden but i think it would be cool to end with something like allowing to fire a job for the upload process.

Please or to participate in this conversation.