This is the 4th part of the tutorial, and the source code will be available on Github, under the 0.4.0 tag.

Also check out the rest of the tutorial here:

Images

One thing missing in our articles are images. We’ll now take a look at implementing image uploading and manipulation into our app.

You may have noticed in the beginning that we’re including the package imagine/Imagine. This package will handle our image manipulation. I chose this package since it supports both GD and Imagemagick. There are other packages available, and you’re free to use any other if you prefer.

Ok, so first let’s add a file upload element into our article form. Since we’re keeping things simple, I decided to use a Twitter Bootstrap fork that handles this element, and also offers a nice preview.

So go ahead and include [http://jasny.github.io/bootstrap/] into your backend layout.

One more thing I added is a service for the Imagine library. So I created the file app/services/Image.php. This is just a simple helper class that I like to use in my projects that resizes the images and caches them.

I won’t go into the complete functionality here, that’s why I wrote a separate post on my blog.

For now just include the helper into your app/services directory. I stripped out the comments, if you want to see everything checkout the code on Github.

app/services/Image.php

namespace App\Services; use File, Log; class Image { protected $library = 'imagick'; protected $imagine; public $overwrite = false; public $quality = 85; public function __construct($library = null) { if ( ! $this->imagine) { $this->library = $library ? $library : null; if ( ! $this->library and class_exists('Imagick')) $this->library = 'imagick'; else $this->library = 'gd'; if ($this->library == 'imagick') $this->imagine = new \Imagine\Imagick\Imagine(); elseif ($this->library == 'gmagick') $this->imagine = new \Imagine\Gmagick\Imagine(); elseif ($this->library == 'gd') $this->imagine = new \Imagine\Gd\Imagine(); else $this->imagine = new \Imagine\Gd\Imagine(); } } public function resize($url, $width = 100, $height = null, $crop = false, $quality = null) { if ($url) { $info = pathinfo($url); if ( ! $height) $height = $width; $quality = ($quality) ? $quality : $this-&gt;quality; $fileName = $info['basename']; $sourceDirPath = public_path() . $info['dirname']; $sourceFilePath = $sourceDirPath . '/' . $fileName; $targetDirName = $width . 'x' . $height . ($crop ? '_crop' : ''); $targetDirPath = $sourceDirPath . '/' . $targetDirName . '/'; $targetFilePath = $targetDirPath . $fileName; $targetUrl = asset($info['dirname'] . '/' . $targetDirName . '/' . $fileName); try { if ( ! File::isDirectory($targetDirPath) and $targetDirPath) @File::makeDirectory($targetDirPath); $size = new \Imagine\Image\Box($width, $height); $mode = $crop ? \Imagine\Image\ImageInterface::THUMBNAIL_OUTBOUND : \Imagine\Image\ImageInterface::THUMBNAIL_INSET; if ($this->overwrite or ! File::exists($targetFilePath) or (File::lastModified($targetFilePath) < File::lastModified($sourceFilePath))) { $this->imagine->open($sourceFilePath) ->thumbnail($size, $mode) ->save($targetFilePath, array('quality' =&gt; $quality)); } } catch (\Exception $e) { Log::error('[IMAGE SERVICE] Failed to resize image &quot;' . $url . '&quot; [' . $e->getMessage() . ']'); } return $targetUrl; } } public function thumb($url, $width, $height = null) { return $this->resize($url, $width, $height, true); } public function upload($file, $dir = null) { if ($file) { // Generate random dir if ( ! $dir) $dir = str_random(8); // Get file info and try to move $destination = public_path() . '/uploads/' . $dir; $filename = $file->getClientOriginalName(); $path = '/uploads/' . $dir . '/' . $filename; $uploaded = $file->move($destination, $filename); if ($uploaded) return $path; } } }

To use the class in a “Laravel” way (Image::resize()) we’ll also need a facade.

app/facades/ImageFacade.php

namespace App\Facades; use Illuminate\Support\Facades\Facade; class ImageFacade extends Facade { protected static function getFacadeAccessor() { return new \App\Services\Image; } }

Now we are ready to use out image helper. To autoload the facades we need to tell composer to do that, so make sure you have our custom directories in the autoloader classmap inside composer.json.

"app/services", "app/facades", "public/site"

And don’t forget to run composer dump-autoload.

One last thing we’ll do is add an alias for the helper. And while we’re here, you can also add aliases for some other classes if you want.

app/config/app.php

'Article' => 'App\Models\Article', 'Page' => 'App\Models\Page', 'Image' => 'App\Facades\ImageFacade',

Our image helper is ready now and after that we’ll add the input fields to our create and edit forms:

app/views/admin/articles/create.blade.php

... <div class="control-group"> {{ Form::label('image', 'Image') }} <div class="fileupload fileupload-new" data-provides="fileupload"> <div class="fileupload-preview thumbnail" style="width: 200px; height: 150px;"> <img src="http://www.placehold.it/200x150/EFEFEF/AAAAAA&amp;text=no+image"> </div> <div> <span class="btn btn-file"><span class="fileupload-new">Select image</span><span class="fileupload-exists">Change</span>{{ Form::file('image') }}</span> </div> </div> </div> ...

app/views/admin/articles/edit.blade.php

... <div class="control-group"> {{ Form::label('image', 'Image') }} <div class="fileupload fileupload-new" data-provides="fileupload"> <div class="fileupload-preview thumbnail" style="width: 200px; height: 150px;"> @if ($article->image) <a href="<?php echo $article->image; ?>"><img src="<?php echo Image::resize($article->image, 200, 150); ?>" alt=""></a> @else <img src="http://www.placehold.it/200x150/EFEFEF/AAAAAA&amp;text=no+image"> @endif </div> <div> <span class="btn btn-file"><span class="fileupload-new">Select image</span><span class="fileupload-exists">Change</span>{{ Form::file('image') }}</span> <a href="#" class="btn fileupload-exists" data-dismiss="fileupload">Remove</a> </div> </div> </div> ...

Don’t worry much about the markup, this is the way the file input works with the Bootstrap fork. If you don’t like it, you’re free to use the native file input element, it should work also.

And not to forget the show view.

app/views/admin/articles/show.blade.php

@extends('admin._layouts.default') @section('main') <h2>Display article</h2> <hr> <h3>{{ $article->title }}</h3> <h5>@{{ $article->created_at }}</h5> {{ $article->body }} @if ($article->image) <hr> <figure><img src="{{ Image::resize($article->image, 800, 600) }}" alt=""></figure> @endif @stop

Our forms are ready, but they don’t do anything for now. We need to handle the upload inside our controller.

app/controllers/admin/ArticleController.php

// Your controller beginning... public function store() { $validation = new ArticleValidator; if ($validation->passes()) { $article = new Article; $article->title = Input::get('title'); $article->slug = Str::slug(Input::get('title')); $article->body = Input::get('body'); $article->user_id = Sentry::getUser()->id; $article->save(); // Now that we have the article ID we need to move the image if (Input::hasFile('image')) { $article->image = Image::upload(Input::file('image'), 'articles/' . $article->id); $article->save(); } Notification::success('The article was saved.'); return Redirect::route('admin.articles.edit', $article->id); } return Redirect::back()->withInput()->withErrors($validation->errors); } public function update($id) { $validation = new ArticleValidator; if ($validation->passes()) { $article = Article::find($id); $article->title = Input::get('title'); $article->slug = Str::slug(Input::get('title')); $article->body = Input::get('body'); $article->user_id = Sentry::getUser()->id; if (Input::hasFile('image')) $article->image = Image::upload(Input::file('image'), 'articles/' . $article->id); $article->save(); Notification::success('The article was saved.'); return Redirect::route('admin.articles.edit', $article->id); } return Redirect::back()->withInput()->withErrors($validation->errors); } // Rest of the controller code...

As you can see we’re leveraging the upload() method from our Image helper. The images are uploaded to the public/uploads directory, so create it and make it writable. I know, this path is hard-coded into our helper. The reason for that is to speed up the tutorial, you can of course make a config file for the helper, and put all the paths there.

Now go ahead and try the image uploading, it should all work now if you followed everything correctly. If you have problems, take a look at the code on github. That code works 100%.

The theme

There’s really not much to this, so I wont go into the whole front-end stuff. We already have some code in place from the last part of the tutorial. So first just copy the entire public/site/assets directory from Github. After that include the assets in your views.

app/public/_partials/header.blade.php

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Laravel 4 Tutorial</title> <link rel="stylesheet" href="{{ asset('site/assets/css/main.css') }}"> </head> <body> <div id="layout"> <header id="header"> @include('site::_partials.navigation') <h1><a href="{{ route('home') }}">Laravel 4 Tutorial</a></h1> </header>

app/public/_partials/navigation.blade.php

<nav id="nav"> <ul> <li class="{{ (Route::is('home')) ? 'active' : null }}"><a href="{{ route('home') }}">Home</a></li> <li class="{{ (Route::is('page') and Request::segment(1) == 'about-us') ? 'active' : null }}"><a href="{{ route('page', 'about-us') }}">About us</a></li> <li class="{{ (Route::is('article.list') or Route::is('article')) ? 'active' : null }}"><a href="{{ route('article.list') }}">Blog</a></li> <li class="{{ (Route::is('page') and Request::segment(1) == 'contact') ? 'active' : null }}"><a href="{{ route('page', 'contact') }}">Contact</a></li> </ul> </nav>

app/public/_partials/footer.blade.php

<footer id="footer"> <p>&copy; 2013 &bull; <a href="http://creolab.hr">Creo, Boris Strahija</a></p> </footer> </div><!--/#layout--> </body> </html>

As you see I already prepared out site navigation and the active item should be marked. The theme itself is mod of the Modernist theme, but extremely simplified 🙂

The markup changed a bit since we also need to display images in our views.

app/public/site/views/index.blade.php

@include('site::_partials/header') {{ $entry->title }} {{ $entry->body }} @include('site::_partials/footer')

app/public/site/views/articles.blade.php

@include('site::_partials/header') <h2>Articles</h2> <hr> <ul class="articles"> @foreach ($entries as $entry) <li> <article> @if ($entry->image) <figure><a href="{{ route('article', $entry->slug) }}"><img src="{{ Image::thumb($entry->image, 150) }}" alt=""></a></figure> @endif <h3><a href="{{ route('article', $entry->slug) }}">{{ $entry->title }}</a></h3> <h5>Created at {{ $entry->created_at }} &bull; by {{ $entry->author->email }}</h5> <p>{{ Str::limit($entry->body, 100) }}</p> <p><a href="{{ route('article', $entry->slug) }}" class="more">Read more</a></p> </article> </li> @endforeach </ul> @include('site::_partials/footer')

app/public/site/views/article.blade.php

@include('site::_partials/header') <article> <h3>{{ $entry->title }}</h3> <h5>Published at {{ $entry->created_at }} &bull; by {{ $entry->author->email }}</h5> {{ $entry->body }} <hr> @if ($entry->image) <figure><img src="{{ Image::resize($entry->image, 800, 600) }}" alt=""></figure> <hr> @endif <a href="{{ route('article.list') }}">&laquo; Back to articles</a> </article> @include('site::_partials/footer')

app/public/site/views/page.blade.php

@include('site::_partials/header') <article> <h2>{{ $entry->title }}</h2> {{ $entry->body }} </article> @include('site::_partials/footer'

Error pages

There’s one thing missing, a nice 404 page. This is really easy to do in Laravel. We’ll need to modify our site routes.

public/site/routes.php

<?php // Home page Route::get('/', array('as' => 'home', function() { return View::make('site::index')->with('entry', Page::where('slug', 'welcome')->first()); })); // Article list Route::get('blog', array('as' => 'article.list', function() { return View::make('site::articles')->with('entries', Article::orderBy('created_at', 'desc')->get()); })); // Single article Route::get('blog/{slug}', array('as' => 'article', function($slug) { $article = Article::where('slug', $slug)->first(); if ( ! $article) App::abort(404, 'Article not found'); return View::make('site::article')->with('entry', $article); })); // Single page Route::get('{slug}', array('as' => 'page', function($slug) { $page = Page::where('slug', $slug)->first(); if ( ! $page) App::abort(404, 'Page not found'); return View::make('site::page')->with('entry', $page); }))->where('slug', '^((?!admin).)*$'); // 404 Page App::missing(function($exception) { return Response::view('site::404', array(), 404); });

As you can we added App::abort() to our routes if no entries are found. And at the end of our routes file we added a 404 handler with App::missing() that catches those errors and displays the custom 404 page. It doesn’t get any easier than that.

What’s next?

This time I’ll let you decide what topics I should cover next. So let me know in the comments…