How to build a localized app with Laravel – Part 2

The setup and application backend

In the last chapter, we looked at what an international application should be like. We covered some fundamental issues to consider when building an international application. We differentiated between what a multilingual application is and what a multi-regional application is. We also addressed what to do when building any of such applications.

In this chapter, we will build our tour guide application. We will not do anything complicated so we can rapidly get to adding multilingual support. We will build the base application and make it work without any additional language or region support. In chapters following, we will look at adding all of that support.

Prerequisites

  1. Knowledge of PHP (version >= 7.1.3)
  2. Knowledge of Laravel (version 5.6.*)
  3. Composer is installed on your computer (version >= 1.3.2)
  4. Laravel installer is installed on your computer

What we are going to build

We are going to build a simple tour guide that will help users book our services. This service rendered will help users take a tour of our city. It will allow them set a time and date they want the tour and how many persons they will come with.

The application will have a simple dashboard that serves as mission control for the administrator. It will let the admin see who booked a tour, when they want the tour and where they want to tour.

Finally, it will have a page that lists all the destinations users can tour. Users will also be able to click a link to see more information about their desired destination.

Getting started

To begin, we need to create a Laravel application. We will use the Laravel installer to make it easy and fast. Run the following command:

$ laravel new tourGuide
$ cd tourGuide

It will create the Laravel application, make the .env file and set the application. If you run php artisan serve, your application will come up right away.

We will use SQLite as our database. Create the SQLite database with the following command:

$ touch database/database.sqlite

Then, update your .env file to use SQLite

// replace
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

// with
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite

Creating the models and migration files

Next thing we want to do is make the models and accompanying migrations for the database our application will use. It is important to plan this ahead of time to reduce the amount of changes we will make to our application later.

For the tour guide, we want the following:

Booking information

  • Destination to visit
  • Date and time for visit
  • Number of people to visit
  • User who booked the visit

Destination information

  • Name
  • Image
  • Description
  • Location

User information

  • Name
  • Email
  • Phone
  • Country of residence

Now that we are clear on what we want to make, let us create the model, controller and migrations for them:

$ php artisan make:model Destination -mr
$ php artisan make:model Booking -mr

There is a User model that ships with Laravel, so we will use it. It also comes with a migration file as well. Since we only register users, we will not need a User controller.

Next, we edit the migration files. Open the users migration file in ./database/migrations and replace with this:

// database/migrations/<timestamp>_create_users_table.php
<?php

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

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('country');
            $table->string('phone');
            $table->boolean('is_admin')->default(false);
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Open the destinations migration file and replace with this:

// database/migrations/<timestamp>_create_destinations_table.php
<?php

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

class CreateDestinationsTable extends Migration
{
    public function up()
    {
        Schema::create('destinations', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('image');
            $table->json('description');
            $table->string('location');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('destinations');
    }
}

Open the bookings migration file and replace with this:

// database/migrations/<timestamp>_create_bookings_table.php
<?php

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

class CreateBookingsTable extends Migration
{
    public function up()
    {
        Schema::create('bookings', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('destination_id');
            $table->unsignedInteger('user_id');
            $table->unsignedInteger('number_of_tourists');
            $table->datetime('visit_date');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('bookings');
    }
}

Our migration files are now ready. We made them mirror the information we want to store. Next, we will edit the models to make them access our database correctly.

Open the User model in ./app and replace with the following:

// app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'country', 'phone', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    public function bookings()
    {
        return $this->hasMany(Booking::class);
    }
}

Open the Destination model and edit:

// app/Destination.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Destination extends Model
{
    protected $fillable = [
        'name', 'image', 'location', 'description'
    ];

    public function bookings()
    {
        return $this->hasMany(Booking::class);
    }
}

Open the Booking model and edit:

// app/Booking.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Booking extends Model
{
    protected $fillable = [
        'user_id', 'destination_id', 'number_of_tourists', 'visit_date'
    ];

    protected $dates = [
        'created_at',
        'updated_at',
        'visit_date'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function destination()
    {
        return $this->belongsTo(Destination::class);
    }
}

And that is all the edits needed for our models now, we will update them as we progress.

Run the following command to create the database tables:

$ php artisan migrate

The controllers

For the controllers, we need to keep it very simple. In subsequent chapters, we will extend the controllers to serve content based on the language a user requests. All the controllers can be found in ./app/Http/Controllers directory.

The first thing we want to do is edit the RegisterController to include the phone and country fields. Open Auth/RegisterController.php and edit:

<?php

namespace App\Http\Controllers\Auth;
[...]

class RegisterController extends Controller
{
    [...]

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'phone' => 'required|string|unique:users',
            'country' => 'required|string|max:255',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'phone' => $data['phone'],
            'country' => $data['country'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

Storing countries as a string means each user will type in their country. This is not very efficient because it will leave our database with a lot of redundant data. For this tutorial, we will ignore that since we will not be going live with it. If you intend to go live with it, please consider creating a table with countries and link them using their id. Learn about database normalization.

Destination controller

In our destination controller, we want to list all the destinations we tour. We also want to give users the ability to click on a destination and see more information on it. So, we are going to create a few methods to return the data we want.

Open the DestinationController and replace with the following:

// app/Http/Controllers/DestinationController.php
<?php

namespace App\Http\Controllers;

use App\Destination;
use Illuminate\Http\Request;

class DestinationController extends Controller
{
    public function index()
    {
        $destinations = Destination::all();
        return view('destination.index', compact('destinations'));
    }

    public function show(Destination $destination)
    {
        return view('destination.show', compact('destination'));
    }
}

We will seed our destination table, so there will be no need to create methods for creating, updating or deleting destination records. This is to keep it simple.

Booking controller

The booking controller should show us what users booked and also help users book tours. Open the controller and replace with the following:

// app/Http/Controllers/BookingController.php
<?php

namespace App\Http\Controllers;

use App\Booking;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
use Auth;

class BookingController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('admin')->only(['index']);
    }

    public function index()
    {
        $bookings = Booking::with(['destination','user'])->get();
        return view('booking.index',compact('bookings'));
    }

    public function create(\App\Destination $destination)
    {
        return view('booking.create',compact('destination'));
    }

    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [ 
            'destination_id' => 'required|integer',  
            'number_of_tourists' => 'required|integer', 
            'visit_date' => 'required'
        ]);
        $validator->validate();
        $input = $request->only(['destination_id', 'number_of_tourists', 'visit_date']);
        $input['user_id'] = Auth::id();
        $input['visit_date'] = Carbon::createFromFormat('m/d/Y',$input['visit_date'])->toDateTimeString();

        $booking = Booking::create($input);
        return redirect('/home');
    }

    public function userPage()
    {
        $bookings = Auth::user()->bookings()->with('destination')->get();
        return view('booking.userpage', compact('bookings'));
    }
}

Our booking controller has a constructor that defines a middleware for checking each request to it. We defined the auth middleware to ensure that only logged in users can access the pages linked to this controller.

We then defined an admin middleware to be sure ONLY admin users can see the page tied to the index method. We will define the admin middleware below.

Middlewares are called in Laravel in the order which they are placed like first, second, … If the execution of a middleware is dependent on say auth, you have to place the auth middleware before it. This also applies to using route groups and adding middlewares to it.

In our index method, we are fetching all bookings and eager loading the users and destinations tied to them. This is a good option since we intend to use the data. It saves us some time with the number of queries we have to run and will make our application faster.

The create method returns the form for making a booking with information on the destination the user had clicked on from the previous page.

The store method stores the booking a user made. Because we set bootstrap datepicker to return the date in a format that is easy for users to read, we are doing a second conversion to a format that our application stores.

The userPage method returns the bookings the logged in user made with the destination information eager loaded.

Create authentication

We will use Laravel’s authentication scaffolding. To generate it, run the following command:

$ php artisan make:auth

This will publish the authentication routes in routes/web.php and also create the view files for different authentication actions like registration, login and more. They are all connected to the respective controllers handling them, so we will not worry about setting that up anymore.

When we use the scaffolding, it makes the /home route, which is where logged in users are redirected to. We can change this to what we want in the LoginController. For this app, we are going to set the route as the default for regular users and redirect admin users.

First, open .app/Http/Controllers/Auth/LoginController.php and add the following:

[...]
use Illuminate\Http\Request;

class LoginController extends Controller
{        
    [...]
    protected function authenticated(Request $request, $user)
    {
        if ($user->is_admin) {
            return redirect('/dashboard');
        }
    }
}

Now, when an admin user logs in, they get redirected to dashboard. We are going to make a middleware to restrict access to the dashboard page.

Making the middleware for the admin check

Middleware in Laravel provide a convenient mechanism for filtering HTTP requests entering our application. This middleware is going to check if a logged in user has an administrator account or not. This is important since we use a single users table for both regular users and administrators.

Run the following command to create the middleware:

$ php artisan make:middleware IsAdmin

Then open the IsAdmin middleware file in ./app/Http/Middleware and add the following:

<?php

namespace App\Http\Middleware;

use Closure;

class IsAdmin
{
    public function handle($request, Closure $next)
    {
        if (!$request->user()->is_admin) {
            abort(404);
        }
        return $next($request);
    }
}

Next, we register the route in ./app/Http/Kernel.php:

protected $routeMiddleware = [
        'admin' => \App\Http\Middleware\IsAdmin::class,
        [...]
    ];

We used this middleware in our booking controller to protect the index method. This middleware will only allow admin users see the page containing all bookings while other non admin users are directed to a lovely 404 page.

Conclusion

We have looked at what an international application is and different things to consider when making the application. We also got started on a simple application for tourists to book a tour guide service.

In the next chapter, we will make the views for all of our pages. We will make the styles for multi-language support and make the pages as well. Then, we can proceed to adding content in multiple languages and see how to show content based on the users language.

The source code to the application in this article is available on GitHub.

This post was originally posted on Pusher.