PHP
Article

Starting a Business with Laravel Spark

By Christopher Pitt

Working with PHP 7.1? Download our FREE PHP 7.1 Cheat Sheet!

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!

  • M S i N Lund

    What is it?
    It doesn’t say anywhere on their site, what their product actually is, only that its GREAT!!!, apparently.

    “The starting-point for your next great idea!”
    …doesn’t really tell you jack shit,

    Seems like an important thing for a company selling a product, that you should be able to just look at their homepage and know what they are selling, without having to google is, or dive into documentation for ..what ever the thing is.

    You see this on a lot of corporate sites these days.
    Like they aren’t really interested in you if you don’t already know them.

    My reaction when i land on such a site is usually “Well fuck you too, then”.
    And I’m out the door.

    So what is it?

    Some kind of geocities, sitez4dummies,
    would be my first guess

    But wait, it has a download-button…
    So its software then?

    But wait, the download-button takes you to a login-form…
    So its a facebook-aroony, then?

    • Chris

      I encourage you to read this post and/or watch the Laracasts videos linked on the Spark home page. It’s a white-label system for starting a subscription based web app.

  • Why would you even buy spark? -.-

    • Chris

      Spark has a lot of white-label commerce functionality built already. It would cost significantly more to develop the features it ships with than to buy a license.

      • You know sparks beta is still available in github. You can copy paste whatever you need.

        • Chris

          It’s not available in an officially-supported version. Taylor rewrote it to include many new features and support for Braintree. I spent a ton of time researching the old version and I’m quite happy to pay for the new version.

        • Nicklas Kevin Frank

          Not to mention that if you work with certain software and actively participate in its community, then its a very small price to pay for keeping good things going.

          The amount of money in time I save by not having to develop basic core functionality for each new SaaS project, not to mention updating them, is vastly larger then the price of Spark.

          I bought it and I will buy it again when the next client comes asking for a cheap SaaS project.

  • M S i N Lund

    Just link to a page that describes the product in text.
    Just a description, not a course in how to install it, no sales waffle.

    “The starting-point for your next great idea!”
    That could just as well be the site for a toilet-manufacturer.

    • M S i N Lund

      I just revisited this, to see if it really was as broken as i thought.
      And yes, yes it is.

      I poked a round until i found a video called “What is Spark?”

      And it was FREE to watch!!! Yayyy…

      I just sat trough about half, without ever hearing anything that resembles a description of what the fuck “spark” is.

      Just the same brain-dead mantra again “A starting-point for your next great idea!”

      At least now i know it falls in the category “a piece of software”, because they told me to “first install spark”.

      First?

      No.

      Nope!

      FIRST you fucking tell me what the fuck sparkle is,

      THEN I decide if I want to install it, base on that INFORMATION.

      Then maybe, MAYBE I install it.

      THAT’S the order.

      JFC, these people are idiots.

      • Chris

        You seem unreasonably upset about this.

        • M S i N Lund

          Im just fed up with the growing trend of obfuscation from people who push stuff like this.

          There is a clear correlation between how little the creator of a product or service wants to talk about what it is, and how useless it is.

          If its just “great” and nothing else, then you can be pretty sure its also not great.

  • Ashley

    Nice post ! I agree – great value, even if just to read through the code to learn how it was put together – I look forward to additional articles on your progress.

  • Željko Sredojević

    “…By the time you read this, there will probably be a multitude of posts explaining how you can set it up.”
    Well, it seems that loudest part was initial release. I don’t see much going on around Spark anymore… I guess that majority was simply disappointed, like myself. Too bad.

  • M S i N Lund

    So its an “application”?
    That … does … WHAT?

    Neither you nor those people, seem to grasp what it means to actually DESCRIBE something.
    And that’s not a great thing for someone who writes articles.

    Imagine that you walk into a grocery store, there you see a lot of containers only labeled “GREAT FOR DRINKING!!”

    And when you ask someone what it its, they tell you:
    -Oh it a type of liquid, which focuses on the ingestion-aspect of life, bla bla, here watch these 10 videos of someone telling you how great it is.

    …instead of just labeling it with a clear description right on the box.
    Because that’s how it works in the rest of the real world.

    And that’s it.
    You just don’t get it.
    So I wont waste any more time here trying to explain.

    • Chris

      “authenticated services through subscription billing”

      I don’t know how to state it any simpler. I get your frustration in trying to understand what it is but I do not get why you are so angry or why you consider it ok to be rude to a complete stranger. If you don’t like the post, or the polite replies I have taken the time to give you, you’re welcome to just not read any of it and go about your merry way. Nobody is forcing you to like or try to understand anything.

Recommended
Sponsors
Get the latest in PHP, once a week, for free.