Building a Laravel project

This example will show how to create a simple Laravel project using vagga.

Creating the project structure

In order to create the initial project structure, we will need a container with the Laravel installer. First, let’s create a directory for our project:

$ mkdir -p ~/projects/vagga-laravel-tutorial && cd ~/projects/vagga-laravel-tutorial

Create the vagga.yaml file and add the following to it:

containers:
  laravel:
    setup:
    - !Ubuntu xenial
    - !ComposerInstall
    - !ComposerConfig
      keep-composer: true
    environ:
      HOME: /tmp

Here we are building a container from Ubuntu and telling it to install PHP and setup Composer. Now create our new project:

$ vagga _run laravel composer create-project \
    --prefer-dist --no-install --no-scripts \
    laravel/laravel src 5.3.*
$ mv src/* src/.* .
$ rmdir src

The first command is quite big! It tells composer to create a new project from laravel/laravel version 5.3 and place it into the src directory. The three flags tell composer to:

  • --prefer-dist install packages from distribution source when available;
  • --no-install do not run composer install after creating the project;
  • --no-scripts do not run scripts defined in the root package.

We want our project’s files to be in the current directory (the one containing vagga.yaml) but Composer only accepts an empty directory, so we tell it to create the project into src, move its contents into the current directory and remove src.

Now that we have our project created, change our container as follows:

containers:
  laravel:
    environ: &env
      ENV_CONTAINER: 1 ❶
      APP_ENV: development ❷
      APP_DEBUG: true ❸
      APP_KEY: YourRandomGeneratedEncryptionKey ❹
    setup:
    - !Ubuntu xenial
    - !UbuntuUniverse
    - !Install
      - php-dom ❺
      - php-mbstring ❺
    - !Env { <<: *env } 
    - !ComposerDependencies 
  • ❶ – tell our application we are running on a container.
  • ❷ – the “environment” our application will run (development, testing, production).
  • ❸ – enable debug mode.
  • ❹ – a random, 32 character string used by encryption service.
  • ❺ – php modules needed by laravel
  • ❻ – inherit environment during build.
  • ❼ – install dependencies from composer.json.

Laravel uses dotenv to load configuration into environment automatically from a .env file, but we won’t use that. Instead, we tell vagga to set the environment for us.

See that environment variable ENV_CONTAINER? With that, our application will be able to tell whether it’s running in a container or not. We will need this to require the right autoload.php generated by Composer.

Warning

Your composer dependencies will not be installed at the ./vendor directory. Instead, they are installed globally at /usr/local/lib/composer/vendor, so be sure to follow this section to see how to require autoload.php from the right location.

THIS IS VERY IMPORTANT!

Now open bootstrap/autoload.php and change the line require __DIR__.'/../vendor/autoload.php'; as follows:

<?php
// ...
if (getenv('ENV_CONTAINER')) {
    require '/usr/local/lib/composer/vendor/autoload.php';
} else {
    require __DIR__.'/../vendor/autoload.php';
}
// ...

This will enable our project to run either from a container (as we are doing here with vagga) or from a shared server.

Note

If you are deploying your project to production using a container, you can just require '/usr/local/lib/composer/vendor/autoload.php'; and ignore the environment variable we just set.

To test if everything is ok, let’s add a command to run our project:

containers:
  # ...
commands:
  run: !Command
    container: laravel
    description: run the laravel development server
    run: |
        php artisan cache:clear ❶
        php artisan config:clear ❶
        php artisan serve
  • ❶ – clear application cache to prevent previous runs from intefering on subsequent runs.

Now run our project:

$ vagga run

And visit localhost:8000. If everithing is OK, you will see Laravel default page saying “Laravel 5”.

Setup the database

Every PHP project needs a database, and ours is not different, so let’s create a container for our database:

containers:
  # ...
  postgres:
    setup:
    - !Ubuntu xenial
    - !EnsureDir /data
    - !Sh |
        addgroup --system --gid 200 postgres ❶
        adduser --uid 200 --system --home /data --no-create-home \
            --shell /bin/bash --group --gecos "PostgreSQL administrator" \
            postgres
    - !Install [postgresql-9.5]
    environ: &db_config 
      PGDATA: /data
      DB_PORT: 5433
      DB_DATABASE: vagga
      DB_USERNAME: vagga
      DB_PASSWORD: vagga
      PG_BIN: /usr/lib/postgresql/9.5/bin
      DB_CONNECTION: pgsql
      DB_HOST: 127.0.0.1
    volumes:
      /data: !Persistent
        name: postgres
        owner-uid: 200
        owner-gid: 200
        init-command: _pg-init ❸
      /run: !Tmpfs
        subdirs:
          postgresql: { mode: 0o777 }
  • ❶ – Use fixed user id and group id for postgres
  • ❷ – Put an anchor at the database environment so we can reference it later
  • ❸ – Vagga command to initialize the volume

Note

The database will be persisted in .vagga/.volumes/postgres.

Add the command to initialize the database:

commands:
  # ...
  _pg-init: !Command
    description: Init postgres database
    container: postgres
    user-id: 200
    group-id: 200
    run: |
      set -ex
      ls -la /data
      $PG_BIN/pg_ctl initdb
      $PG_BIN/pg_ctl -w -o '-F --port=$DB_PORT -k /tmp' start
      $PG_BIN/createuser -h 127.0.0.1 -p $DB_PORT $PG_USER
      $PG_BIN/createdb -h 127.0.0.1 -p $DB_PORT $DB_DATABASE -O $DB_USERNAME
      $PG_BIN/psql -h 127.0.0.1 -p $DB_PORT -c "ALTER ROLE $DB_USERNAME WITH ENCRYPTED PASSWORD '$DB_PASSWORD';"
      $PG_BIN/pg_ctl stop

Now change our run command to start the database alongside our project:

commands:
  run: !Supervise
    description: run the laravel development server
    children:
      app: !Command
        container: laravel
        environ: *db_config 
        run: |
            php artisan cache:clear
            php artisan config:clear
            php artisan serve
      db: !Command
        container: postgres
        user-id: 200
        group-id: 200
        run: exec $PG_BIN/postgres -F --port=$DB_PORT
  • ❶ – Reference the database environment

And run our project:

$ vagga run

Inspecting the database

Now that we have a working database, we can inspect it using a small php utility called adminer. Let’s create a container for it:

containers:
  # ...
  adminer:
    setup:
    - !Alpine v3.4
    - !Install
      - php5-cli
      - php5-pdo_pgsql
    - !EnsureDir /opt/adminer
    - !Download 
      url: https://www.adminer.org/static/download/4.2.5/adminer-4.2.5.php
      path: /opt/adminer/index.php
    - !Download 
      url: https://raw.githubusercontent.com/vrana/adminer/master/designs/nette/adminer.css
      path: /opt/adminer/adminer.css
  • ❶ – download the adminer script.
  • ❷ – use a better style (optional).

Change our run command to start the adminer container:

commands:
  run: !Supervise
    description: run the laravel development server
    children:
      app: !Command
        # ...
      db: !Command
        # ...
      adminer: !Command
        container: adminer
        run: php -S 127.0.0.1:8800 -t /opt/adminer

This command will simply start the php embedded development server with its root pointing to the directory containing the adminer files.

To access adminer, visit localhost:8800, fill in the server field with 127.0.0.1:5433 and the other fields with “vagga” (the username and password we defined).

Adding some code

Now that we have our project working and our database is ready, let’s add some.

Let’s add a shortcut command for running artisan

commands:
  # ...
  artisan: !Command
    description: Shortcut for running php artisan
    container: laravel
    run: [php, artisan]

Now, we need a layout. Fortunately, Laravel can give us one, we just have to scaffold authentication:

$ vagga artisan make:auth

This will give us a nice layout at resources/views/layouts/app.blade.php.

Now create a model:

$ vagga artisan make:model --migration Article

This will create a new model at app/Article.php and its respective migration at database/migrations/2016_03_24_172211_create_articles_table.php (yours will have a slightly different name).

Open the migration file and tell it to add two fields, title and body, to the database table for our Article model:

<?php

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

class CreateArticlesTable extends Migration
{
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title', 100);
            $table->text('body');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('articles');
    }
}

Open routes/web.php and setup routing:

<?php
Route::get('/', 'ArticleController@index');
Route::resource('/article', 'ArticleController');

Auth::routes();

Route::get('/home', 'HomeController@index');

Create our controller:

$ vagga artisan make:controller --resource ArticleController

This will create a controller at app/Http/Controllers/ArticleController.php populated with some CRUD method stubs.

Now change the controller to actually do something:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Article;

class ArticleController extends Controller
{
    public function index()
    {
        $articles = Article::orderBy('created_at', 'asc')->get();
        return view('article.index', [
           'articles' => $articles
        ]);
    }

    public function create()
    {
        return view('article.create');
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'title' => 'required|max:100',
            'body' => 'required'
        ]);

        $article = new Article;
        $article->title = $request->title;
        $article->body = $request->body;
        $article->save();

        return redirect('/');
    }

    public function show(Article $article)
    {
        return view('article.show', [
            'article' => $article
        ]);
    }

    public function edit(Article $article)
    {
        return view('article.edit', [
            'article' => $article
        ]);
    }

    public function update(Request $request, Article $article)
    {
        $article->title = $request->title;
        $article->body = $request->body;
        $article->save();

        return redirect('/');
    }

    public function destroy(Article $article)
    {
        $article->delete();
        return redirect('/');
    }
}

Create the views for our controller:

<!-- resources/views/article/show.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>{{ $article->title }}</h2>
            <p>{{ $article->body }}</p>
        </div>
    </div>
</div>
@endsection
<!-- resources/views/article/index.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>Article List</h2>
            <a href="{{ url('article/create') }}" class="btn">
                <i class="fa fa-btn fa-plus"></i>New Article
            </a>
            @if (count($articles) > 0)
            <table class="table table-bordered table-striped">
                <thead>
                    <th>id</th>
                    <th>title</a></th>
                    <th>actions</th>
                </thead>
                <tbody>
                    @foreach($articles as $article)
                    <tr>
                        <td>{{ $article->id }}</td>
                        <td>{{ $article->title }}</td>
                        <td>
                            <a href="{{ url('article/'.$article->id) }}" class="btn btn-success">
                                <i class="fa fa-btn fa-eye"></i>View
                            </a>
                            <a href="{{ url('article/'.$article->id.'/edit') }}" class="btn btn-primary">
                                <i class="fa fa-btn fa-pencil"></i>Edit
                            </a>
                            <form action="{{ url('article/'.$article->id) }}"
                                    method="post" style="display: inline-block">
                                {!! csrf_field() !!}
                                {!! method_field('DELETE') !!}
                                <button type="submit" class="btn btn-danger"
                                        onclick="if (!window.confirm('Are you sure?')) { return false; }">
                                    <i class="fa fa-btn fa-trash"></i>Delete
                                </button>
                            </form>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
            @endif
        </div>
    </div>
</div>
@endsection
<!-- resources/views/article/create.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>Create Article</h2>
            @include('common.errors')
            <form action="{{ url('article') }}" method="post">
                {!! csrf_field() !!}
                <div class="form-group">
                    <label for="id-title">Title:</label>
                    <input id="id-title" class="form-control" type="text" name="title" />
                </div>
                <div class="form-group">
                    <label for="id-body">Title:</label>
                    <textarea id="id-body" class="form-control" name="body"></textarea>
                </div>
                <button type="submit" class="btn btn-primary">Save</button>
            </form>
        </div>
    </div>
</div>
@endsection
<!-- resources/views/article/edit.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>Edit Article</h2>
            @include('common.errors')
            <form action="{{ url('article/'.$article->id) }}" method="post">
                {!! csrf_field() !!}
                {!! method_field('PUT') !!}
                <div class="form-group">
                    <label for="id-title">Title:</label>
                    <input id="id-title" class="form-control"
                           type="text" name="title" value="{{ $article->title }}" />
                </div>
                <div class="form-group">
                    <label for="id-body">Title:</label>
                    <textarea id="id-body" class="form-control" name="body">{{ $article->body }}</textarea>
                </div>
                <button type="submit" class="btn btn-primary">Save</button>
            </form>
        </div>
    </div>
</div>
@endsection

And the view for the common errors:

<!-- resources/views/common/errors.blade.php -->
@if (count($errors) > 0)
<div class="alert alert-danger">
    <ul>
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
</div>
@endif

Create a seeder to prepopulate our database:

$ vagga artisan make:seeder ArticleSeeder

This will create a seeder class at database/seeds/ArticleSeeder.php. Open it and change it as follows:

<?php

use Illuminate\Database\Seeder;

use App\Article;

class ArticleSeeder extends Seeder
{
    private $articles = [
        ['title' => 'Article 1', 'body' => 'Lorem ipsum dolor sit amet'],
        ['title' => 'Article 2', 'body' => 'Lorem ipsum dolor sit amet'],
        ['title' => 'Article 3', 'body' => 'Lorem ipsum dolor sit amet'],
        ['title' => 'Article 4', 'body' => 'Lorem ipsum dolor sit amet'],
        ['title' => 'Article 5', 'body' => 'Lorem ipsum dolor sit amet']
    ];

    public function run()
    {
        if (Article::all()->count() > 0) {
            return;
        }

        foreach ($this->articles as $article) {
            $new = new Article;
            $new->title = $article['title'];
            $new->body = $article['body'];
            $new->save();
        }
    }
}

Change database/seeds/DatabaseSeeder.php to include ArticleSeeder:

<?php
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(ArticleSeeder::class);
    }
}

Add a the php postgresql module to our container:

containers:
  laravel:
    environ: &env
      ENV_CONTAINER: 1
      APP_ENV: development
      APP_DEBUG: true
      APP_KEY: YourRandomGeneratedEncryptionKey
    setup:
    - !Ubuntu xenial
    - !UbuntuUniverse
    - !Install
      - php-dom
      - php-mbstring
      - php-pgsql
    - !Env { <<: *env }
    - !ComposerDependencies

Change the run command to execute the migrations and seed our database:

commands:
  run: !Supervise
    description: run the laravel development server
    children:
      app: !Command
        container: laravel
        environ: *db_config
        run: |
            php artisan cache:clear
            php artisan config:clear
            php artisan migrate
            php artisan db:seed
            php artisan serve
      db: !Command
        # ...
      adminer: !Command
        # ...

If you run our project, you will see the articles we defined in the seeder class. Try adding some articles, then access adminer at localhost:8800 to inspect the database.

Trying out memcached

Many projects use memcached to speed up things, so let’s try it out.

Activate Universe repository and add php-memcached, to our container:

containers:
  laravel:
    environ: &env
      ENV_CONTAINER: 1
      APP_ENV: development
      APP_DEBUG: true
      APP_KEY: YourRandomGeneratedEncryptionKey
    setup:
    - !Ubuntu xenial
    - !UbuntuUniverse
    - !Install
      - php-dom
      - php-mbstring
      - php-pgsql
      - php-memcached
    - !Env { <<: *env }
    - !ComposerDependencies

Create a container for memcached:

containers:
  # ...
  memcached:
    setup:
    - !Alpine v3.4
    - !Install [memcached]

Add some yaml anchors on the run command so we can avoid repetition:

commands:
  run: !Supervise
    description: run the laravel development server
    children:
      app: !Command
        container: laravel
        environ: *db_config
        run: &run_app | # ❶
            # ...
      db: &db_cmd !Command 
        # ...
      adminer: &adminer_cmd !Command 
        # ...
  • ❶ – set an anchor at the app child command
  • ❷ – set an anchor at the db child command
  • ❸ – set an anchor at the adminer child command

Create the command to run with caching:

commands:
  # ...
  run-cached: !Supervise
    description: Start the laravel development server alongside memcached
    children:
      cache: !Command
        container: memcached
        run: memcached -u memcached -vv ❶
      app: !Command
        container: laravel
        environ:
          <<: *db_config
          CACHE_DRIVER: memcached
          MEMCACHED_HOST: 127.0.0.1
          MEMCACHED_PORT: 11211
        run: *run_app
      db: *db_cmd
      adminer: *adminer_cmd
  • ❶ – run memcached as verbose so we see can see the cache working

Now let’s change our controller to use caching:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Article;

use Cache;

class ArticleController extends Controller
{
    public function index()
    {
        $articles = Cache::rememberForever('article:all', function() {
            return Article::orderBy('created_at', 'asc')->get();
        });
        return view('article.index', [
           'articles' => $articles
        ]);
    }

    public function create()
    {
        return view('article.create');
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'title' => 'required|max:100',
            'body' => 'required'
        ]);

        $article = new Article;
        $article->title = $request->title;
        $article->body = $request->body;
        $article->save();

        Cache::forget('article:all');

        return redirect('/');
    }

    public function show($id)
    {
        $article = Cache::rememberForever('article:'.$id, function() use ($id) {
            return Article::find($id);
        });
        return view('article.show', [
            'article' => $article
        ]);
    }

    public function edit($id)
    {
        return view('article.edit', [
            'article' => $article
        ]);
    }

    public function update(Request $request, Article $article)
    {
        $article->title = $request->title;
        $article->body = $request->body;
        $article->save();

        Cache::forget('article:'.$article->id);
        Cache::forget('article:all');

        return redirect('/');
    }

    public function destroy(Article $article)
    {
        $article->delete();
        Cache::forget('article:'.$article->id);
        Cache::forget('article:all');
        return redirect('/');
    }
}

Now run our project with caching:

$ vagga run-cached

Keep an eye on the console to see Laravel talking to memcached.

Deploying to a shared server

It’s still common to deploy a php application to a shared server running a LAMP stack (Linux, Apache, MySQL and PHP), but our container in its current state isn’t compatible with that approach. To solve this, we will create a command to export our project almost ready to be deployed.

Before going to the command part, we will need a new container for this task:

containers:
  # ...
  exporter:
    setup:
    - !Ubuntu xenial
    - !UbuntuUniverse
    - !Install [php-mbstring, php-dom]
    - !Depends composer.json ❶
    - !Depends composer.lock ❶
    - !EnsureDir /usr/local/src/
    - !Copy 
      source: /work
      path: /usr/local/src/work
    - !ComposerInstall 
    - !Env
      COMPOSER_VENDOR_DIR: /usr/local/src/work/vendor ❹
    - !Sh |
        cd /usr/local/src/work
        rm -f export.tar.gz
        composer install \ ❺
          --no-dev --prefer-dist --optimize-autoloader
    volumes:
      /usr/local/src/work: !Snapshot 
  • ❶ – rebuild the container if dependencies change.
  • ❷ – copy our project into a directory inside the container.
  • ❸ – require Composer to be available.
  • ❹ – install composer dependencies into the directory we just copied.
  • ❺ – call composer binary directly, because using !ComposerDependencies would make vagga try to find composer.json before starting the build.
  • ❻ – create a volume so we can manipulate the files in the copied directory.

Now let’s create the command to export our container:

commands:
  # ...
  export: !Command
    container: exporter
    description: export project into tarball
    run: |
        cd /usr/local/src/work
        rm -f .env
        rm -f database/database.sqlite
        php artisan cache:clear
        php artisan config:clear
        php artisan route:clear
        php artisan view:clear
        rm storage/framework/sessions/*
        rm -rf tests
        echo APP_ENV=production >> .env
        echo APP_KEY=random >> .env
        php artisan key:generate
        php artisan optimize
        php artisan route:cache
        php artisan config:cache
        php artisan vendor:publish
        tar -czf export.tar.gz .env *
        cp -f export.tar.gz /work/

Note

Take this command as a mere example, hence you are encouraged to change it in order to better suit your needs.

The shell in the export command will make some cleanup, remove tests (we don’t need them in production) and create a minimal .env file with an APP_KEY generated. Then it will compress everything into a file called export.tar.gz and copy it to our project directory.

Since the export command is quite long, it is a good candidate to be moved to a separate file, for example:

commands:
  # ...
  export: !Command
    container: exporter
    description: export project into tarball
    run: [/bin/sh, export.sh]