Recently I've been working with a friend on a project in which users have profile pictures.

We used Jetstream to get this functionality out of the box. Although it provided us with almost everything that we needed in that aspect, one thing was missing: uploading pictures from URLs.

Long Story Short

I created the laravel-url-uploaded-file package that extends Laravel's UploadedFile class to allow its instantiation from a URL instead of an actual file that was uploaded from the user's computer.

The package can be installed using Composer:

composer require naxon/laravel-url-uploaded-file

The usage is pretty simple and straightforward:

use Naxon\UrlUploadedFile\UrlUploadedFile;

$file = UrlUploadedFile::createFromUrl('https://naxon.dev/assets/img/portrait.jpg');

Now you have an UrlUploadedFile instance that can use all of the functionality that's in the base UploadedFile class. For example, you can store the file:

$file->storeAs('pics', 'profile-pic.' . $file->extension());

Using with Jetstream's Profile Photos

Jetsteam uses the HasProfilePhoto trait that exposes three methods: updateProfilePhoto, deleteProfilePhoto and getProfilePhoto. No need to explain, pretty straightforward, right?

I wanted to have another updateProfilePictureFromUrl that, well, updates the user's profile picture from a url instead of an uploaded file. In order to achieve this, I needed to override the base HasProfilePhoto and add my new method to it:

<?php

namespace App\Models\Concerns;

use Naxon\UrlUploadedFile\UrlUploadedFile;
use Laravel\Jetstream\HasProfilePhoto as BaseTrait;

trait HasProfilePhoto
{
    use BaseTrait;

    public function updateProfilePhotoFromUrl(string $url)
    {
        $this->updateProfilePhoto(UrlUploadedFile::createFromUrl($url));
    }
}

The updateProfilePhoto method receives an UploadedFile instance as its first argument, and because UrlUploadedFile extends UploadedFile, I can simply pass it to the method and have Jetstream take care of the process without overriding / duplicating any of its functionality.

Pretty simple, right?

Under The Hoods

As mentioned before, UrlUploadedFile extends Laravel's UploadedFile class file, which extends a class with the same name from Symfony and adds Laravel-specific functionality to it, such as storing and mocking.

Due to our rule of sticking with Laravel's standards and style, we looked for a quick way to instantiate UploadedFile from a URL by mocking a file upload. At this point I remembered that I actually used something similar in the past while working with Spatie's great package Laravel Media Library. This package allows you to associate media to a model from URLs. After reviewing the package's source code, I came across this Downloader.

So let's take a look on the UrlUploadedFile class and break down its functionality:

<?php

namespace Naxon\UrlUploadedFile;

use Illuminate\Http\UploadedFile;
use Naxon\UrlUploadedFile\Exceptions\CantOpenFileFromUrlException;

class UrlUploadedFile extends UploadedFile
{
    public static function createFromUrl(string $url, string $originalName = '', string $mimeType = null, int $error = null, bool $test = false): self
    {
        if (! $stream = @fopen($url, 'r')) {
            throw new CantOpenFileFromUrlException($url);
        }

        $tempFile = tempnam(sys_get_temp_dir(), 'url-file-');

        file_put_contents($tempFile, $stream);

        return new static($tempFile, $originalName, $mimeType, $error, $test);
    }
}

If you expected to see some very long code, then I'm sorry to disappoint you. The code is pretty straightforward, and here's what is does:

  • Open the file using fopen.
  • Create a file with tempnam in the system directory for temporary files (sys_get_temp_dir). url-file- is just a prefix that's used to distinguish files created by the package.
  • Fill the temporary file with the content of the file that was opened at the beginning of the method, using file_put_contents.
  • Return a new instance of UrlUploadedFile with our temporary file. This is possible because Symfony's UploadedFile is instantiated with a file path.

I created this package to answer a specific need in one of my projects. I didn't test it thoroughly with other file types. In order to extend its functionality, other downloaders may be needed. My focus for the next version will be to support custom downloaders, like Spatie are doing in their Media Library package.