Building a Laravel project¶
This example will show how to create a simple Laravel project using vagga.
- Creating the project structure
- Setup the database
- Adding some code
- Trying out memcached
- Deploying to a shared server
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 runcomposer 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.