PHP - - By Christopher Pitt

Starting a Business with Laravel Spark

I am really excited about Laravel Spark. By the time you read this, there will probably be a multitude of posts explaining how you can set it up. That’s not as interesting to me as the journey I’m about to take in creating an actual business with Spark!

The idea is simple. I have created a Pagekit module which you can use to back up and restore site data. The module makes it easy to store and download these backups, and restore them on different servers.

The trouble is, getting those backup files to the remote server takes time and a bit of hassle. I have often wanted a way to quickly and painlessly transfer this application state from one server to another, and make automated offsite backups. So I’m going to set that up for myself, and perhaps others will find it useful enough to pay for it.

Spark splash screen

Getting Started

I’m using Stripe, and intend to have a single plan with no trial. The setup for this is quite easy, but I’ve made a note of the plan ID. I’ll need that to set the plan up in Spark…

Stripe welcome screen

Next, I reset my secret and public Stripe keys and update to the latest API (through the same screen, https://dashboard.stripe.com/account/apikeys).

I forgot that the settings in .env do not automatically reload while the Laravel development server is running, so I was getting needlessly frustrated at the keys which wouldn’t seem to update.

Spark has a few expected registration/profile fields, but I want to add a few more. I would like to ask users if they would like automatic backups and I’d also like to collect their billing address, so I can show it on their invoice. First I’ll have to create a migration for the new field:

php artisan make:migration add_should_backup_field

To do this, we can add the column (making sure to remove it if the migrations are rolled back):

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddShouldBackupField extends Migration
{
    public function up()
    {
        Schema::table("users", function (Blueprint $table) {
            $table->boolean("should_backup");
        });
    }

    public function down()
    {
        Schema::table("users", function (Blueprint $table) {
            $table->dropColumn("should_backup");
        });
    }
}

Collecting billing addresses is as easy as a single method call, in app/Providers/SparkServiceProvider.php:

/**
 * @inheritdoc
 */
public function booted()
{
    Spark::useStripe();

    Spark::collectBillingAddress();

    Spark::plan("[Stripe plan name]", "[Stripe plan ID]")
        ->price(4.99)
        ->features([
            "Backups"
        ]);
}

This adds the billing address fields to the registration form:

Billing Address Field Added

Don’t forget to customize the rest of SparkServiceProvider.php to suit your needs, if you’re following along.

Storing Backups

The whole point of this app is to give content authors the ability to upload, list, and download their backups. Next I’ll create a Backup model:

php artisan make:migration create_backups_table
php artisan make:model Backups

The backups table needs a few fields:

  1. The name of each backup (perhaps containing the site name, the date and whether it includes database and upload data).
  2. The uploaded backup file.
  3. The size of each backup.
  4. A field to link the backup to the user that created it.

This is what the customized migration file looks like:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;

class CreateBackupsTable extends Migration
{
    public function up()
    {
        Schema::create("backups", function (Blueprint $table) {
            $table->increments("id");
            $table->string("name");
            $table->string("file");
            $table->integer("size");
            $table->integer("user_id");
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists("backups");
    }
}

Given this database structure, I want to provide a set of JSON endpoints through which users can view and manipulate backups. Again, we can use the make command to get going:

php artisan make:controller BackupsController --resource

Spark is specifically designed to be a good starting-point for authenticated API creation. When you log in with a Spark account, you can go to a section in which you can create API tokens:

With this token, and having created the resource controller, I can make the index method echo some debug text. Then I can make curl requests to it:

curl -XGET http://localhost:8000/api/backup

For this to work, I need to define an API route (in app/Http/api.php):

Route::group([
    "prefix" => "api",
    "middleware" => "auth:api"
], function () {
    Route::resource("backup", "BackupsController");
});

Since I haven’t provided the api_token GET parameter, the request isn’t correctly authenticated, and Spark will try to redirect me to the login page. If I’m logged in at the time, I’m redirected back to the dashboard screen.

If I define the api_token parameter, I see the debug text:

I’ve opted for a simple upload mechanism, which does very little validation on the files (while I get the prototype working). I’ll want to harden this up later with some validation, but for now I have:

public function store(Request $request)
{
    $file = $request->file("file");
    $name = $request->get("name");

    if (!$file || !$name) {
        return [
            "status" => "error",
            "error" => "file or name missing",
        ];
    }

    $fileExtension = "." . $file->getClientOriginalExtension();
    $fileName = str_slug(time() . " " . $name) . $fileExtension;

    try {
        $file->move(storage_path("backups"), $fileName);
    } catch (Exception $exception) {
        return [
            "status" => "error",
            "error" => "could not store file",
        ];
    }

    $backup = Backup::create([
        "name" => $name,
        "file" => $fileName,
        "size" => $file->getClientSize(),
        "user_id" => Auth::user()->id,
    ]);

    return $backup;
}

I can test this by creating an empty backup file and then submitting it to the store action:

touch backup.zip
curl
    -X POST
    -F name="My First Backup"
    -F file=@backup.zip
    "http://localhost:8000/api/backup?api_token=[my token]"

I get back something resembling:

{
    "name": "My First Backup",
    "file": "1461802753-my-first-backup.zip",
    "size": 0,
    "user_id": 1,
    "updated_at": "2016-04-28 00:19:13",
    "created_at": "2016-04-28 00:19:13",
    "id": 8
}

Notice how the file is named differently to avoid collisions? It’s actually named after the backup’s name, not the original file name, and I’ve added the timestamp so it’s possible to upload similarly-named backups without the names clashing.

There’s still a race-condition for when users upload backups with exactly the same name in the same second. The risk of that is less, and I’m prepared to deal with it when it becomes a problem.

Now I can customize the index action to return more useful information:

public function index()
{
    return [
        "status" => "ok",
        "data" => Auth::user()->backups,
    ];
}

I’ve also had to create this relationship between users and backups by adding the backups method to the User model:

public function backups()
{
    return $this->hasMany(Backup::class);
}

Downloading Backups

I’m uploading the backups to a non-public folder for a reason. I don’t want users to be able to guess (no matter how slim the chance that they will guess correctly) how to access the backups of other users.

So I have to create a special downloads endpoint. I can, however, piggyback off the show action:

public function show($id)
{
    $backup = Auth::user()->backups()->findOrFail($id);
    $path = storage_path("backups/". $backup->file);

    return response()->download($path, $backup->file);
}

Because of how the query is scoped, users will only ever be allowed to download backups linked to their account, and the backups are not publicly accessible outside of this one route.

Conclusion

In an incredibly short space of time, I’ve managed to use Spark to create a subscription-based hosting platform, with an authenticated API for storing and downloading backup files.

There’s a lot more work to do, from theming the application to adding more security to implementing storage limits and backup deletion. Still, I’m encouraged by the start I’ve made, and I may revisit this application in future tutorials, as I learn more about Spark!

Do you have any questions about this code, or comments about what you’d do differently? Let us know in the comments!

Sponsors
Login or Create Account to Comment
Login Create Account