Building a Laravel Blog App with Fly
Tags: laravel
Published: 2018-10-24 11:08:44

---
title: Building a Laravel Blog App with Fly
published: true
tags: Laravel
canonical_url: https://miguelpiedrafita.com/fly-laravel/
cover_image: https://miguelpiedrafita.com/content/images/2018/10/laravel--1-.png
---
_Originally published on [my blog](https://miguelpiedrafita.com/fly-laravel/)_

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](https://github.com/m1guelpf/fly-blog-example) and you can explore the live demo [here](https://fly-php-blog-example.miguelpiedrafita.com/). If you'd like to play with the PHP Fly API client, you can find it on [GitHub](https://github.com/m1guelpf/php-fly-api).

## **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:

```bash
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:

```bash
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:

```env
// .env

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:

```php
// database/migrations/2014_10_12_000000_create_users_table.php

$table->string('domain')->nullable();
```

Finally, we can run our migration:

```bash
php artisan migrate
```

There’s a bug in Laravel 5 that if you’re running a version of MySQL older than 5.7.7 or MariaDB older than 10.2.2. More info [here](https://github.com/laravel/framework/issues/17508). This can be fixed by replacing the `boot()`method of the `AppServiceProvider` with:

```php
// app/Providers/AppServiceProvider.php

// add this under the namespace line
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:

```bash
php artisan make:model Post -m
```

Open the `Post` model and add the code below to it:

```php
// app/Post.php

/**
 * 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:

```php
// database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php

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:

```bash
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:

```php
// app/User.php

/**
 * 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:

```php
// app/Post.php

/**
 * 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:

```php
// routes/web.php
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:

```php
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
php artisan make:controller PostsController
```

Open the controller we've just created and add the following code to it:

```php
// app/Http/Controllers/PostsController.php

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](https://startbootstrap.com/template-categories/blogs/).

Create a new `resources/views/posts.blade.php` file and paste into it:

```blade
<!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>
    <!-- Page Content -->
    <div class="container">
      <div class="row">
        <!-- Blog Entries Column -->
        <div class="col-md-8">

          <h1 class="my-4">Posts</h1>

          <!-- Blog Post -->
          @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 &rarr;</a>
            </div>
            <div class="card-footer text-muted">
              Posted {{ $post->created_at->diffForHumans() }}
            </div>
          </div>
          @endforeach
        </div>

        <!-- Sidebar Widgets Column -->
        <div class="col-md-4">
          @auth
          <!-- New Post Widget -->
          <div class="card my-4">
            <h5 class="card-header">New Post</h5>
            <div class="card-body">
              <form method="POST" action="{{ route('create') }}">
              {{ csrf_field() }}
                <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>
      <!-- /.row -->

    </div>
    <!-- /.container -->

    <!-- Footer -->
    <footer class="py-5 bg-dark">
      <div class="container">
        <p class="m-0 text-center text-white">Copyright &copy; {{ config('app.name') }} {{ date('Y') }}</p>
      </div>
      <!-- /.container -->
    </footer>

    <!-- Bootstrap core JavaScript -->
    <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:

```blade
<!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>

    <!-- Page Content -->
    <div class="container">

      <div class="row">

        <!-- Post Content Column -->
        <div class="col-lg-12">

          <!-- Title -->
          <h1 class="mt-4">{{ $post->title }}</h1>

          <!-- Author -->
          <p class="lead">
            by {{ $post->user->name }}
          </p>

          <hr>

          <!-- Date/Time -->
          <p>Posted {{ $post->created_at->diffForHumans() }}</p>

          <hr>

          <!-- Post Content -->
          <p>{{ $post->body }}</p>
      </div>
      <!-- /.row -->

    </div>
    <!-- /.container -->

    <!-- Footer -->
    <footer class="py-5 bg-dark">
      <div class="container">
        <p class="m-0 text-center text-white">Copyright &copy; {{ config('app.name') }} {{ date('Y') }}</p>
      </div>
      <!-- /.container -->
    </footer>

    <!-- Bootstrap core JavaScript -->
    <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](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:

```bash
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:

```php
// config/services.php

'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:

```env
// .env

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:

```php
// routes/web.php

// 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** :

```php
// routes/web.php

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** :

```php
// routes/web.php

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:

```blade
@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_field() }}
                          <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:

```bash
php artisan make:controller DomainController
```

Open the controller we've just created and add the following code to it:

```php
// app/Http/Controllers/PostsController.php

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](https://fly-php-blog-example.miguelpiedrafita.com/) 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!

_Liked this article? Consider [supporting me on Patreon](https://miguelpiedrafita.com/patreon). [Learn more](https://miguelpiedrafita.com/patreon)._