Laravel makes it easy to build modern applications with custom domains by providing a powerful routing system which allows developers to build logic around multiple domains in your application.
Fly, on the other hand, is an easy and reliable application delivery network that provides useful services like routing different sources to one domain, securing your application or providing the logic for accepting custom domains in a secure, easy and fast way.
In this post, I will be showing you how to build a Laravel blog application with Fly. I will be using a custom client to interact with the Fly API but you can also use Guzzle, cURL or any other HTTP client of your preference.
The code of the completed demo is available on GitHub and you can explore the live demo here. If you'd like to play with the PHP Fly API client, you can find it on GitHub.
Part I: Blogging app
Setting Up Laravel
We'll start by creating a new Laravel project. While there are different ways of creating a new Laravel project, I prefer using the Laravel installer. Open your terminal and run the code below:
laravel new laravel-blog
This will create a laravel-blog
project within the directory where you ran the command above.
Authenticating Users
Our app will require users to be logged in before they can make a post or add a domain. So, we need an authentication system which with Laravel is as simple as running an artisan command in the terminal:
php artisan make:auth
This will create the necessary routes, views and controllers needed for an authentication system.
Before we go on to create users, we need to run the users migration that comes with a fresh installation of Laravel. But to do this, we first need to setup our database. Open the .env
file and enter your database details:
DB_CONNECTION=mysql
DB_HOST=[YOUR_DATABASE_HOST]
DB_PORT=3306
DB_DATABASE=[YOUR_DATABASE_NAME]
DB_USERNAME=[YOUR_DATABASE_USER]
DB_PASSWORD=[YOUR_USER_PASSWORD]
The last thing to do before we run our migration is to make a change to allow an user to have custom domains. To do so, open the users
migration in the database/migrations
directory and add the following code before the timestamps:
$table->string('domain')->nullable();
Finally, we can run our migration:
php artisan migrate
There's a bug in Laravel 5 if you're running a version of MySQL older than 5.7.7 or MariaDB older than 10.2.2. More info here. This can be fixed by replacing the boot()
method of the AppServiceProvider
with:
// add this under the namespace line
use Illuminate\Support\Facades\Schema;
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Schema::defaultStringLength(191);
}
Post Model and Migration
Create a Post model along with the migration file by running the command:
php artisan make:model Post -m
Open the Post
model and add the code below to it:
/**
* Fields that are mass assignable
*
* @var array
*/
protected $guarded = [];
Within the databases/migrations
directory, open the posts
table migration that was created when we ran the command above and update the up()
method with:
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->string('title')->default('Untitled');
$table->text('body');
$table->timestamps();
});
The post will have six columns: an auto-incrementing id
, user_id
, title
, body
, created_at
and updated_at
.
The user_id
column will hold the ID of the user that sent a message, the title
column will hold the title of the post and the body
column will hold the content of the post.
Run the migration:
php artisan migrate
User To Post Relationship
We need to setup the relationship between a user and a post. A user can have many posts while a particular posts was created by a user. So, the relationship between the user and message is a one to many
relationship.
To define this relationship, add the code below to User
model:
/**
* A user can have many posts
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany(Post::class);
}
Next, we need to define the inverse relationship by adding the code below to Post
model:
/**
* A post belongs to a user
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
Defining App Routes
Let's create the routes our app will need. Open routes/web.php and replace the routes with the code below to define three simple routes:
Route::get('/', 'PostsController@index')->name('index');
Auth::routes();
Route::get('posts/{post}', 'PostsController@show')->name('post');
Route::post('posts', 'PostsController@create')->name('create');
The homepage will display the user's posts and an input field to add a new post. A GET
post route will show a specific post and a POST
posts route will be used for creating new posts.
NOTE: Since we have removed the /home
route, you might want to update the redirectTo
property of both app/Http/Controllers/Auth/LoginController.php
and app/Http/Controllers/Auth/RegisterController.php
to:
protected $redirectTo = '/';
PostsController
Now let's create the controller which will handle the logic of our chat app. Create a PostsController
with the command below:
php artisan make:controller PostsController
Open the controller we've just created and add the following code to it:
use App\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
public function __construct()
{
$this->middleware('auth')->only('create');
}
/**
* Show posts.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('posts', ['posts' => Post::all()]);
}
/**
* Show a specific post
*
* @return \Illuminate\Http\Response
*/
public function show(Post $post)
{
return view('post')->withPost($post);
}
/**
* Persist post to database
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
$post = Auth::user()->posts()->create($request->validate([
'title' => 'required|string',
'body' => 'required|string',
]));
return redirect()->route('post', $post);
}
Using the auth
middleware in ChatsController's __construct()
indicates that all the methods with the controller will only be accessible to authorized users.
The index()
method will simply return a view file which we will create shortly.
The show()
method returns a view file with a post
attached to it.
Lastly, the create()
method will persist the post
to the database and return a redirect to the post
page.
Creating the Views
To keep everything simple, we'll be using a modified version of the StartBootstrap blog templates.
Create a new resources/views/posts.blade.php
file and paste into it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Posts {{ config('app.name') }}</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8">
<h1 class="my-4">Posts</h1>
@foreach($posts as $post)
<div class="card mb-4">
<div class="card-body">
<h2 class="card-title">{{ $post->title }}</h2>
<p class="card-text">{{ str_limit($post->body, 200) }}</p>
<a href="{{ route('post', $post) }}" class="btn btn-primary">Read More →</a>
</div>
<div class="card-footer text-muted">
Posted {{ $post->created_at->diffForHumans() }}
</div>
</div>
@endforeach
</div>
<div class="col-md-4">
@auth
<div class="card my-4">
<h5 class="card-header">New Post</h5>
<div class="card-body">
<form method="POST" action="{{ route('create') }}">
@csrf
<div class="form-group">
<label for="title">Title:</label>
<input type="text" class="form-control" id="title" name="title" />
</div>
<div class="form-group">
<label for="body">Content:</label>
<textarea class="form-control" rows="5" id="body" name="body"></textarea>
</div>
<button class="btn btn-primary" type="submit">Post</button>
</form>
</div>
</div>
@else
<div class="card my-4">
<p class="text-center"><a href="{{ route('login') }}">Login</a> to make a post</p>
</div>
@endauth
</div>
</div>
</div>
<footer class="py-5 bg-dark">
<div class="container">
<p class="m-0 text-center text-white">Copyright © {{ config('app.name') }} {{ date('Y') }}</p>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>
We also need a view for displaying a single post, so let's create a new resources/views/post.blade.php
file and paste the following into it:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="{{ $post->user->name }}">
<title>{{ $post->title }} - {{ config('app.name') }}</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="mt-4">{{ $post->title }}</h1>
<p class="lead">
by {{ $post->user->name }}
</p>
<hr>
<p>Posted {{ $post->created_at->diffForHumans() }}</p>
<hr>
<p>{{ $post->body }}</p>
</div>
</div>
<footer class="py-5 bg-dark">
<div class="container">
<p class="m-0 text-center text-white">Copyright © {{ config('app.name') }} {{ date('Y') }}</p>
</div>
</footer>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>
Now, we have a simple blogging platform. Let's add custom domains.
Part II: Custom Domains
Setting Up Fly.io
If you don't have one already, create a free Fly account at https://fly.io/app/sign-up then login to your dashboard and create a site.
First, let's install a package that will help us interact with the Fly API. To do so, simply open your terminal and run the following code:
composer require m1guelpf/fly-api
Now, let's fill in our Fly app credentials. Open the config/services.php
file and add the following before the closing square bracket:
'fly' => [
'token' => env('FLY_TOKEN'),
'site' => env('FLY_SITE')
],
You probably noticed that we're pulling data from the .env file, so let's update the .env file to contain it:
FLY_TOKEN=[YOUR_FLY_TOKEN]
FLY_SITE=[YOUR_FLY_SITE_SLUG]
If you don't know where to get your Token, go to your Fly dashboard, click the account button on the top navigation bar, open the settings menu, click the personal access tokens item on the navigation bar and create a new one.
Setting up routing
We need to setup two new routes, one for the page where users can add custom domains and the other for the page where users will see when accessing a custom domain. To do so, we'll first add our settings route like so:
// the routes we defined before
Route::view('domain', 'domain-setup')->middleware('auth')->name('domain-setup');
Route::post('domain', 'DomainController@create')->middleware('auth');
Finally, we'll add a route group at the very begining of the file:
Route::group(['domain' => '{domain}'], function() {
Route::get('/', 'DomainController@index');
});
// the rest of the routes
Also, to make the index page load when we're not using a custom domain, we'll need to move the index route to a route group before the one we've just defined:
Route::group(['domain' => '[YOUR_APP_DOMAIN_HERE]'], function() {
Route::get('/', 'PostsController@index')->name('index');
});
// the route group we defined before
// the rest of the routes, minus the index one
Creating the views
We'll need a page where users can add a custom domain, so we are going to create a standard Bootstrap page with a form. Create a resources/views/domain-setup.blade.php
file and paste the following into it:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Custom Domain</div>
<div class="panel-body">
@if (session('status'))
<div class="alert alert-success">
{!! session('status') !!}
</div>
@endif
@if (count($errors->all()) > 0)
<div class="alert alert-danger">
{{ $errors->first() }}
</div>
@endif
@if(is_null(Auth::user()->domain))
<form class="text-center" method="POST">
@csrf
<input class="form-control" name="domain" type="text" placeholder="yourdomain.com" value="{{ old('domain') }}" required />
<br />
<button type="submit" class="btn btn-primary">Setup Custom Domain</button>
</form>
@else
<p>You have setup <b>{{ Auth::user()->domain }}</b> as your custom domain.</p>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
DomainController
Now let's create the controller which will handle custom domains. Create a DomainController
with the command below:
php artisan make:controller DomainController
Open the controller we've just created and add the following code to it:
use App\User;
use Facades\M1guelpf\FlyAPI\Fly;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* Render the index page for custom domains.
*
* @return \Illuminate\Http\Response
*/
public function index($domain)
{
$user = User::where('domain', $domain)->findOrFail();
return view('posts', ['posts' => $user->posts]);
}
/**
* Persist a custom domain to database
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
Auth::user()->update($request->validate([
'domain' => 'required|string|unique:users',
]));
$domain = Fly::connect(config('services.fly.token'))->createHostname(config('services.fly.site'), $request->input('domain'));
return redirect()->back()->withStatus("Success! To finish the setup, you need to point your domain to <b>{$domain['data']['attributes']['preview_hostname']}</b>. After that, everything's good to go.");
}
Part III: How to improve it?
In this post, we've created a blog application that lets users connect custom domains. Well, I have created the app, you're just reading about it! So, to fix this, here is a list of things that you can improve:
- Letting users remove custom domains
- Supporting more than one custom domain
- Improving the interface
- Allowing private posts
- Supporting Markdown for posts
- That thing I missed but you realized and want to implement
Keep playing with Fly, use it in some side projects and maybe in your next awesome project!
And checkout the PHP Fly API module on GitHub!