June 01, 2015

Simple Image Gallery CRUD with Laravel 5

After writing my previous blog post (Codeigniter Image Gallery CRUD) I thought it would be fun to write on the same subject for Laravel. I am imagining that the image gallery application would be faster to implement, and the code would be more readable. This tutorial was written with Laravel 5, and will be updated as soon as the 5.1 LTS version is released.

Why Image Gallery?

There are so many ways to learn a PHP framework. Some start from reading documentation pages, and some other prefer reading tutorial on subject such as the famously CRUD. If you are of the second stereotype, read along. The benefit for you implementing an image gallery CRUD are:
  1. You will learn how to implement a CRUD application, obviously ^_^ !
  2. In the meanwhile you also learn how to upload a file.
  3. Another important subject, you will learn how to validate file inputs as well as text inputs.
  4. Implementing an edit form is tricky because sometimes you just want to edit a caption without changing the image, you will also learn how to do this.
Based on my experience when I first time learning Laravel, these four fundamental lessons are good to know to make amazing application in the future. But before we start, let's take a look on what the image gallery looks like after implementation.
Final Appearance of the Image Gallery Application

Database Migration

Assuming that you already know how to install Laravel and how to setup your database connection credentials, Laravel's migration provides an easy way to create, drop, alter, and even rolling back table schemas during application development process. Even though our image gallery is a small application, it is good to have a practice on database migration.

First up, open your command prompt (if you are on Windows), or shell (if you are on Mac or Linux). Enter your laravel project folder, and type in:
C:\project\laravel> php artisan make:model Image
This command will automatically create an Eloquent model class on a file named Image.php under the app/ folder, and automatically creates a migration file for the images table called yyyy_mm_dd_xxxxxx_create_images_table.php under the database/migration/ folder. The yyyy_mm_dd_xxxxxx is varied according to the creation date of our migration file. Now, open up the migration file, and write our up() and down() functions such as below:
public function up()
   {
      Schema::create('images', function(Blueprint $table)
      {
         $table->increments('id');
         $table->string('file');
         $table->string('caption');
         $table->text('description');
         $table->timestamps();
      });
   }

public function down()
   {
      Schema::dropIfExists('images');
   }
Don't forget to run the php artisan migrate on your command prompt to execute the migration.
C:\project\laravel> php artisan migrate
At this point, you will have a new images table in your database.
Database Migration Result

Now, Here Comes the Controller

A controller is the part of an MVC application which takes heaviest responsibility. It is holding the key role for the entire application to run. A controller's job is to:
  1. Communicate to a model to get some data, manipulate them if necessary, and then present them for a view to be displayed.
  2. Vice versa, controllers take request variables from a submitted form, validate them, manipulate them if necessary, and then store them as a new record (or replace existing record data) in the database.
  3. Preparing and destroying session variables for success-or-fail notification bars. 
  4. And maybe some other amazing stuff ^_^.
To get a pre-written controller class for our image gallery an artisan command is here to help us. Let's get back to the command prompt and type these in:
C:\project\laravel> php artisan make:controller ImageController
This command will automatically generate the ImageController class as a file named ImageController.php located under app/Http/Controllers/. Let's open up the ImageController.php, and start code like a pro ^_^.

In this controller, we are going to be using the Image model and the Validator class, therefore we should load the model's and the validator's namespace before the class definition (one line above class ImageController extends Controller code).
use App\Image;
use Validator;

class ImageController extends Controller {
Next, if we take a look at the class, there are some pre-built empty methods (functions) inside the ImageController class. We begin by writing methods which are view-related. Visually, these methods will have a return view() line inside. These are the view-related methods:
/* 1. This method relates to the "images list" view */
      public function index()
      {
         $images = Image::paginate(10);
         return view('images-list')->with('images', $images);
      }

/* 2. This method relates to the "add new image" view */
      public function create()
      {
         return view('add-new-image');
      }

/* 3. This method relates to the "image detail" view */
      public function show($id)
      {
         $image = Image::find($id);
         return view('image-detail')->with('image', $image);
      }

/* 4. This method relates to the "edit image" view */
      public function edit($id)
      {
         $image = Image::find($id);
         return view('edit-image')->with('image', $image);
      }

Time to Decorate Some View Files

Laravel has a templating engine called blade. We are going to learn about blade as we type. There are six view files that we are going to create under the resources/views/ folder, each of which are:
  1. The global-layout.blade.php view file, is the master template that will rendered on every page of our application as an extension of individual view files called directly from the Imagecontroller.
  2. The images-list.blade.php view file, displays all images in the database with a pagination bar if the number of images are greater than 10 (you can change this amount in the controller). This file, as well as the next three files below, are being called from the controller and is extending the global-layout.blade.php file.
  3. The add-new-image.blade.php view file, display a form for user to add a new image.
  4. The image-detail.blade.php view file, displays one individual image with an edit button.
  5. The edit-image.blade.php view file, displays a form for user to edit an existing image.
  6. The error-notification.blade.php view file, is a widget to display error messages or success notifications if there is any.
The global layout will be like below. Notice the @yield tag. This is where the dynamic content will be placed, depends on which page is being visited. The @yield has one string parameter. This parameter, is a mark that corresponds to a @section tag inside any file which extends the global layout. The @section tag then should have exactly the same parameter as the @yield's.
<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="utf-8">
      <title>Laravel 5 Image Gallery</title>
      <link href='http://fonts.googleapis.com/css?family=Oxygen' rel='stylesheet' type='text/css'>
      <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
      <style type="text/css">::selection{background-color:#E13300;color:#fff}::-moz-selection{background-color:#E13300;color:#fff}body{background-color:#fff;margin:40px;font:16px/24px normal Oxygen,sans-serif;color:#4F5155}a{color:#039;background-color:transparent;font-weight:400}h1{color:#444;background-color:transparent;border-bottom:1px solid #D0D0D0;font-size:19px;font-weight:400;margin:0 0 14px;padding:14px 15px 10px}code{font-family:Consolas,Monaco,Courier New,Courier,monospace;font-size:12px;background-color:#f9f9f9;border:1px solid #D0D0D0;color:#002166;display:block;margin:14px 0;padding:12px 10px}#body{margin:0 15px}p.footer{text-align:right;font-size:11px;border-top:1px solid #D0D0D0;line-height:32px;padding:0 10px;margin:20px 0 0}#container{margin:10px;border:1px solid #D0D0D0;box-shadow:0 0 8px #D0D0D0}</style>
   </head>
   <body>

      <div id="container">
         <h1>Laravel 5 Image Gallery</h1>

         <div id="body">
            @yield('body')
         </div>

         <p class="footer">
            <a href="http://The-Amazing-PHP.blogspot.com">
               The-Amazing-PHP.blogspot.com
            </a>
         </p>
      </div>

   </body>
</html>
The images list view will be like below. Notice that blade has several ways to render a php string. In this file we found the {{ and }} pair, and also the {!! and !!} pair. Both will render any string or any function returning a string. The {!! and !!} pair will render escaped string, while the {{ and }} pair will not. A <form> tag is something that you would want to render as unescaped string.
@extends('global-layout')

@section('body')
   <div class="row">
      @if(count($images) > 0)
         <div class="col-md-12 text-center" >
            <a href="{{ url('/image/create') }}" class="btn btn-primary" role="button">
               Add New Image
            </a>
            <hr />
            @include('error-notification')
         </div>
      @endif
      @forelse($images as $image)
         <div class="col-md-3">
            <div class="thumbnail">
               <img src="{{asset($image->file)}}" />
               <div class="caption">
                  <h3>{{$image->caption}}</h3>
                  <p>{!! substr($image->description, 0,100) !!}</p>
                  <p>
                     <div class="row text-center" style="padding-left:1em;">
                     <a href="{{ url('/image/'.$image->id.'/edit') }}" class="btn btn-warning pull-left">Edit</a>
                     <span class="pull-left">&nbsp;</span>
                     {!! Form::open(['url'=>'/image/'.$image->id, 'class'=>'pull-left']) !!}
                        {!! Form::hidden('_method', 'DELETE') !!}
                        {!! Form::submit('Delete', ['class' => 'btn btn-danger', 'onclick'=>'return confirm(\'Are you sure?\')']) !!}
                     {!! Form::close() !!}
                     </div>
                  </p>
               </div>
            </div>
         </div>
      @empty
         <p>No images yet, <a href="{{ url('/image/create') }}">add a new one</a>?</p>
      @endforelse
   </div>
   <div align="center">{!! $images->render() !!}</div>
@stop
The add new image view will be like below.
@extends('global-layout')

@section('body')
   @include('error-notification')
   {!! Form::open(['url'=>'/image', 'method'=>'POST', 'files'=>'true']) !!}

      <div class="form-group">
         <label for="userfile">Image File</label>
         <input type="file" class="form-control" name="userfile">
      </div>

      <div class="form-group">
         <label for="caption">Caption</label>
         <input type="text" class="form-control" name="caption" value="">
      </div>

      <div class="form-group">
         <label for="description">Description</label>
         <textarea class="form-control" name="description"></textarea>
      </div>

      <button type="submit" class="btn btn-primary">Upload</button>
      <a href="{{ url('/image') }}" class="btn btn-warning">Cancel</a>

   {!! Form::close() !!}
@stop
The show image detail view will be like below.
@extends('global-layout')

@section('body')
   <form class="form-horizontal">

     <img src="{{ asset($image->file) }}" height="150" />
     <div class="form-group">
       <label class="col-sm-2 control-label">Caption</label>
       <div class="col-sm-10">
         <p class="form-control-static">{{ $image->caption }}</p>
       </div>
     </div>

     <div class="form-group">
       <label class="col-sm-2 control-label">Description</label>
       <div class="col-sm-10">
         <p class="form-control-static">{{ $image->description }}</p>
       </div>
     </div>

      <a href="{{ url('/image/'.$image->id.'/edit') }}" class="btn btn-warning">Edit</a>
      <a href="{{ url('/image') }}" class="btn btn-warning">&lt;Back</a>
      
   </form>
@stop
The edit image view will be like below.
@extends('global-layout')

@section('body')
   @include('error-notification')
   {!! Form::model($image,['url' => '/image/'.$image->id, 'method' => 'PUT', 'files'=>true]) !!}

      <img src="{{ asset($image->file) }}" height="150" />
      <div class="form-group">
         <label for="userfile">Image File</label>
         {!! Form::file('userfile',null,['class'=>'form-control']) !!}
      </div>

      <div class="form-group">
         <label for="caption">Caption</label>
         {!! Form::text('caption',null,['class'=>'form-control']) !!}
      </div>

      <div class="form-group">
         <label for="description">Description</label>
         {!! Form::textarea('description',null,['class'=>'form-control']) !!}
      </div>

      <button type="submit" class="btn btn-primary">Save</button>
      <a href="{{ url('/image') }}" class="btn btn-warning">Cancel</a>

   {!! Form::close() !!}
@stop
The error notification widget will be like below. Since errors and success messages are two different thing and not dependent on each other, therefore we separate them. Each will have their own @if tag.
@if( Session::has('errors') )
  <div class="alert alert-danger" role="alert" align="center">
  <ul>
     @foreach($errors->all() as $error) 
        <li>{{$error}}</li>
     @endforeach
  </ul>
  </div>
@endif
@if( Session::has('message') )
  <div class="alert alert-success" role="alert" align="center">
  {{ Session::get('message') }}
  </div>
@endif

Two Lines of Routing File?!

Yeah, you got it! In Laravel, we can write a single route to a resourceful controller. This means that we can have a single route to map the whole CRUD url to a controller, that is: the ImageController class. The routes.php file should be located under the app/Http/ folder. Open it up, and write these two lines of code:
<?php Route::get('/', function(){ return redirect('/image'); });
Route::resource('/image', 'ImageController'); 

Request Handler Methods

As you can guess from our view files, there are three forms occur in our image gallery application.
  1. The Add New Image has a POST method,
  2. The Edit Image has a PUT method, and
  3. The Delete Image has a DELETE method.
Therefore we will implement three methods for handling each of those forms. First let's implement the "add new image form" handler.
public function store(Request $request)
   {
      // Validation //
      $validation = Validator::make($request->all(), [
         'caption'     => 'required|regex:/^[A-Za-z ]+$/',
         'description' => 'required',
         'userfile'     => 'required|image|mimes:jpeg,png|min:1|max:250'
      ]);

      // Check if it fails //
      if( $validation->fails() ){
         return redirect()->back()->withInput()
                          ->with('errors', $validation->errors() );
      }

      $image = new Image;

      // upload the image //
      $file = $request->file('userfile');
      $destination_path = 'uploads/';
      $filename = str_random(6).'_'.$file->getClientOriginalName();
      $file->move($destination_path, $filename);
      
      // save image data into database //
      $image->file = $destination_path . $filename;
      $image->caption = $request->input('caption');
      $image->description = $request->input('description');
      $image->save();

      return redirect('/')->with('message','You just uploaded an image!');
   }
Next, let's do the "edit image form" handler implementation.
public function update(Request $request, $id)
   {
      // Validation //
      $validation = Validator::make($request->all(), [
            'caption'     => 'required|regex:/^[A-Za-z ]+$/',
            'description' => 'required',
            'userfile'    => 'sometimes|image|mimes:jpeg,png|min:1|max:250'
      ]);

      // Check if it fails //
      if( $validation->fails() ){
            return redirect()->back()->withInput()
                             ->with('errors', $validation->errors() );
      }

      // Process valid data & go to success page //
      $image = Image::find($id);

      // if user choose a file, replace the old one //
      if( $request->hasFile('userfile') ){
           $file = $request->file('userfile');
           $destination_path = 'uploads/';
           $filename = str_random(6).'_'.$file->getClientOriginalName();
           $file->move($destination_path, $filename);
           $image->file = $destination_path . $filename;
      }
        
      // replace old data with new data from the submitted form //
      $image->caption = $request->input('caption');
      $image->description = $request->input('description');
      $image->save();

      return redirect('/')->with('message','You just updated an image!');
   }
Finally, the "delete image form" handler would be implemented such as:
public function destroy($id)
   {
      $image = Image::find($id);
      $image->delete();
      return redirect('/')->with('message','You just uploaded an image!');
   }
That's it ! I hope you enjoy reading this tutorial, and please give a comment down below if you are having trouble anywhere in the tutorial. Happy coding ^_^

Download the full source code here.

One other small tweak: since Laravel 5 excluded the HtmlServiceProvider, we can not directly use the Form::open, Form::close, and Form::model methods on a fresh install. Open up the composer.json file, and add inside the require brackets such as this:
"require": {
        "laravel/framework": "5.0.*",
        "illuminate/html": "5.*"
},
Save your composer.json file and run this on your command prompt (or Linux / Mac shell):
C:\project\laravel> composer update
After the update is finished, open up the config/app.php file, and add 'Illuminate\Html\HtmlServiceProvider', line inside the 'providers' array. Also add 'Form'=> 'Illuminate\Html\FormFacade', line inside the 'aliases' array.

Last thing: do not forget to create the uploads/ folder under the public/ folder.

13 comments:

  1. I'm only half way through your tutorial but have spotted an error. "images-list.blade.php".

    Line 36

    <p>No images yet, <a href="{{ url('/create') }}">add a new one</a>?</p>

    Should be

    <p>No images yet, <a href="{{ url('/image/create') }}">add a new one</a>?</p>

    As referenced on line 8. Using the supplied code gives you a NotFoundHttpException as the route does not exist.

    ReplyDelete
  2. Thanks for the info Neil, I already fixed it thanks to you (y)

    ReplyDelete
  3. Thx for share dude ...
    Its help me so much

    ReplyDelete
  4. thank this code very help. thaks you my friend

    ReplyDelete
  5. I was trying to manipulate images in a code that I have for several days, and it was not working until I read this tutorial. Now I've learned enough to make it work. Thank you very much!

    ReplyDelete

  6. When i run the source codenya even get an error like this
    Function mcrypt_get_iv_size() is deprecated

    ReplyDelete
    Replies
    1. Not sure why you got that message, but I'll check later. Could you please show me the content of your composer.json file?

      Delete
  7. This comment has been removed by the author.

    ReplyDelete
  8. First, thanks for the post! But the show image detail should be @section('body') not @section('content')

    ReplyDelete