Laravel 5.1 Beauty - Blog Tags

Adding "tagging" to the blog administration.

Posted on April 19, 2015 in L5 Beauty
Note: this is the eighth step in the tutorial.

The basic blog built in the 10 Minute Blog chapter wasn’t very fancy. Most blogging platforms allow blog posts to be categorized or “tagged” in different ways. In this chapter we’ll develop a tagging system for the l5beauty project.

Chapter Contents

Creating the Model and Migrations

First step is to create the Tag model. Do this from within the Homestead VM.

Creating Tag Model

~/Code/l5beauty$ php artisan make:model --migration Tag
Model created successfully.
Created Migration: 2015_04_07_044634_create_tags_table

This creates the model in the file app/Tag.php. Because we used the --migration option the make:model command will automatically create a migration for the database.

There will be a many-to-many relationship between Tags and Posts. Follow the command below to create a migration for the pivot table to store this relationship.

Creating the Post and Tag Pivot Migration

~/Code/l5beauty$ php artisan make:migration --create=post_tag_pivot \
    create_post_tag_pivot
Created Migration: 2015_04_07_044734_create_post_tag_pivot

Editing tags migration

Edit the newly created tags migration file in the database/migrations directory. Make it match what’s below.

The tags table create migration

<?php

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

class CreateTagsTable extends Migration
{
  /**
   * Run the migrations.
   */
  public function up()
  {
    Schema::create('tags', function (Blueprint $table) {
      $table->increments('id');
      $table->string('tag')->unique();
      $table->string('title');
      $table->string('subtitle');
      $table->string('page_image');
      $table->string('meta_description');
      $table->string('layout')->default('blog.layouts.index');
      $table->boolean('reverse_direction');
      $table->timestamps();
    });
  }

  /**
   * Reverse the migrations.
   */
  public function down()
  {
    Schema::drop('tags');
  }
}

Here’s the description of a few of the columns.

page_image
The look and feel of the finished blog will allow a large image at the top of each page. If we’re viewing a page of posts for a particular tag it’ll be nice to show a different image. This column allows the image to be set.
meta_description
A description to embed in a meta tag for search engines.
layout
The blog will eventually use layouts
reverse_directions
Normally blogs list posts in the order of publication with the most recent on top. This flag allows flipping that order.

Editing post tag pivot migration

Edit the post/tag pivot migration created earlier to match what’s below.

The post/tag pivot create migration

<?php

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

class CreatePostTagPivot extends Migration
{
  /**
   * Run the migrations.
   */
  public function up()
  {
    Schema::create('post_tag_pivot', function (Blueprint $table) {
      $table->increments('id');
      $table->integer('post_id')->unsigned()->index();
      $table->integer('tag_id')->unsigned()->index();
    });
  }

  /**
   * Reverse the migrations.
   */
  public function down()
  {
    Schema::drop('post_tag_pivot');
  }
}

Running Migrations

Run the migrations to create the two new tables.

Running the migrations

~/Code/l5beauty$ php artisan migrate
Migrated: 2015_04_07_044634_create_tags_table
Migrated: 2015_04_07_044734_create_post_tag_pivot

Implementing admin.tag.index

Back in Chapter 8 - Starting the Admin Area we created the TagController and routes to it. Implementing the admin.tag.index route will be fairly trivial. We’ll update the index() method in the controller and provide a view.

Implementing TagController index

Update TagController.php in the app/Http/Controllers/Admin directory, making the changes specified below.

Updates to TagController for index

// Add the following use statement at the top
use App\Tag;

// Replace the index() method with what's below
  /**
   * Display a listing of the tags.
   */
  public function index()
  {
    $tags = Tag::all();

    return view('admin.tag.index')
            ->withTags($tags);
  }

Easy, huh? All we’re doing here is returning a view and passing it a variable that will be called $tags containing all the tags from the database. The Tag is a model so we can use the all() method to return a collection of all tags on file.

Adding the admin.tag.index view

Create a new tag directory in the resources/views/admin folder and then create the index.blade.php file with the following contents.

Content of admin.tag.index

@extends('admin.layout')

@section('content')
  <div class="container-fluid">
    <div class="row page-title-row">
      <div class="col-md-6">
        <h3>Tags <small>» Listing</small></h3>
      </div>
      <div class="col-md-6 text-right">
        <a href="/admin/tag/create" class="btn btn-success btn-md">
          <i class="fa fa-plus-circle"></i> New Tag
        </a>
      </div>
    </div>

    <div class="row">
      <div class="col-sm-12">

        @include('admin.partials.errors')
        @include('admin.partials.success')

        <table id="tags-table" class="table table-striped table-bordered">
          <thead>
          <tr>
            <th>Tag</th>
            <th>Title</th>
            <th class="hidden-sm">Subtitle</th>
            <th class="hidden-md">Page Image</th>
            <th class="hidden-md">Meta Description</th>
            <th class="hidden-md">Layout</th>
            <th class="hidden-sm">Direction</th>
            <th data-sortable="false">Actions</th>
          </tr>
          </thead>
          <tbody>
          @foreach ($tags as $tag)
            <tr>
              <td>{{ $tag->tag }}</td>
              <td>{{ $tag->title }}</td>
              <td class="hidden-sm">{{ $tag->subtitle }}</td>
              <td class="hidden-md">{{ $tag->page_image }}</td>
              <td class="hidden-md">{{ $tag->meta_description }}</td>
              <td class="hidden-md">{{ $tag->layout }}</td>
              <td class="hidden-sm">
                @if ($tag->reverse_direction)
                  Reverse
                @else
                  Normal
                @endif
              </td>
              <td>
                <a href="/admin/tag/{{ $tag->id }}/edit"
                   class="btn btn-xs btn-info">
                  <i class="fa fa-edit"></i> Edit
                </a>
              </td>
            </tr>
          @endforeach
          </tbody>
        </table>
      </div>
    </div>
  </div>
@stop

@section('scripts')
  <script>
    $(function() {
      $("#tags-table").DataTable({
      });
    });
  </script>
@stop

This template is easy to follow. We’re using the admin layout. At the top is the title of the page and a button to create new tags. Then a table outputting all the tags we have on file (that $tags variable we passed to the view). And finally, there’s a bit of javascript at the bottom to convert the table to DataTables.

There are two views included within the admin.tag.index: admin.partials.errors and admin.partials.success. The admin.partials.errors view was created back in Chapter 8, but we haven’t created the admin.partials.success view yet.

The success partial

Create the success.blade.php file in the resources/views/admin/partials directory with the following content.

The success feedback partial

@if (Session::has('success'))
  <div class="alert alert-success">
    <button type="button" class="close" data-dismiss="alert">×</button>
    <strong>
      <i class="fa fa-check-circle fa-lg fa-fw"></i> Success.  
    </strong>
    {{ Session::get('success') }}
  </div>
@endif

What we’re doing here is looking for a success value in the session and outputting it if there is one.

We’ll use flash data to store success whenever we want this type of feedback to the user?

What is Flash Data?

Often in web applications there’s a need to store data in the session for only the next request. This is called flash data. Laravel 5.1 makes flashing data to the session easy. Use the Session::flash('name', 'value') method to do it.

Another way to set flash data is to return a redirect response and pass the value using a ->withName('value') to the redirect before returning it.

The empty list

Bring up the l5beauty project in your browser, navigate to the administration page and click the Tags link at the top.

You should see a screen like the one below.

Figure 10.1 - Tags Listing

List of Tags

No data yet. But it sure does look professional.

Implementing admin.tag.create

Next we’ll implement the screen that allows a new tag to be created. This way when you click on the [New Tag] button you’ll have a form to fill out.

Implementing TagController create

Update the TagController class as instructed below. This file is in the app/Http/Controllers/Admin directory.

Updates to TagController for create

// Add the $fields property at the top of the class
class TagController extends Controller
{
  protected $fields = [
    'tag' => '',
    'title' => '',
    'subtitle' => '',
    'meta_description' => '',
    'page_image' => '',
    'layout' => 'blog.layouts.index',
    'reverse_direction' => 0,
  ];

// Replace the create() method with this
  /**
   * Show form for creating new tag
   */
  public function create()
  {
    $data = [];
    foreach ($this->fields as $field => $default) {
      $data[$field] = old($field, $default);
    }

    return view('admin.tag.create', $data);
  }

This method will be executed either when the [New Tag] button is hit, or the form is filled out but there’s an error. In this second case we want to pass any data that was previously input back to the form. Otherwise, any default value for a field will be assigned. This is accomplished with the old() function.

The view is returned with this data. Thus each field will be a variable in the view. For example $layout will exist in the view (and it’ll have a default value of ‘blog.layouts.index’).

Adding the admin.tag.create view

Create the create.blade.php file in the resources/views/admin/tag directory with the content below.

Content of the admin.tag.create view

@extends('admin.layout')

@section('content')
  <div class="container-fluid">
    <div class="row page-title-row">
      <div class="col-md-12">
        <h3>Tags <small>» Create New Tag</small></h3>
      </div>
    </div>

    <div class="row">
      <div class="col-md-8 col-md-offset-2">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">New Tag Form</h3>
          </div>
          <div class="panel-body">

            @include('admin.partials.errors')

            <form class="form-horizontal" role="form" method="POST"
                  action="/admin/tag">
              <input type="hidden" name="_token" value="{{ csrf_token() }}">

              <div class="form-group">
                <label for="tag" class="col-md-3 control-label">Tag</label>
                <div class="col-md-3">
                  <input type="text" class="form-control" name="tag" id="tag"
                         value="{{ $tag }}" autofocus>
                </div>
              </div>

              @include('admin.tag._form')

              <div class="form-group">
                <div class="col-md-7 col-md-offset-3">
                  <button type="submit" class="btn btn-primary btn-md">
                    <i class="fa fa-plus-circle"></i>
                      Add New Tag
                  </button>
                </div>
              </div>

            </form>

          </div>
        </div>
      </div>
    </div>
  </div>

@stop

This is a simple to follow Blade template. There’s no navigation back to the Tags Listing page, but it’s easy enough to use the Tags link in the navigation bar whenever the user needs to get back to the Tags Listing page.

The fields for the form itself will be in the admin.tag._form view because we can use the same form for both creation and editing.

Adding the admin.tag._form partial

The reason I named this view _form, beginning with an underscore is that I’m following a common Ruby convention of prefacing partials that aren’t in their own directory with an underscore.

Create the _form.blade.php file in the resources/views/admin/tag directory with the content below.

Content of admin.tag._form partial

<div class="form-group">
  <label for="title" class="col-md-3 control-label">
    Title
  </label>
  <div class="col-md-8">
    <input type="text" class="form-control" name="title"
           id="title" value="{{ $title }}">
  </div>
</div>

<div class="form-group">
  <label for="subtitle" class="col-md-3 control-label">
    Subtitle
  </label>
  <div class="col-md-8">
    <input type="text" class="form-control" name="subtitle"
           id="subtitle" value="{{ $subtitle }}">
  </div>
</div>

<div class="form-group">
  <label for="meta_description" class="col-md-3 control-label">
    Meta Description
  </label>
  <div class="col-md-8">
    <textarea class="form-control" id="meta_description"
              name="meta_description" rows="3">{{
      $meta_description
    }}</textarea>
  </div>
</div>

<div class="form-group">
  <label for="page_image" class="col-md-3 control-label">
    Page Image
  </label>
  <div class="col-md-8">
    <input type="text" class="form-control" name="page_image"
           id="page_image" value="{{ $page_image }}">
  </div>
</div>

<div class="form-group">
  <label for="layout" class="col-md-3 control-label">
    Layout
  </label>
  <div class="col-md-4">
    <input type="text" class="form-control" name="layout" id="layout"
           value="{{ $layout }}">
  </div>
</div>

<div class="form-group">
  <label for="reverse_direction" class="col-md-3 control-label">
    Direction
  </label>
  <div class="col-md-7">
    <label class="radio-inline">
      <input type="radio" name="reverse_direction"
             id="reverse_direction"
      @if (! $reverse_direction)
        checked="checked"
      @endif
      value="0"> Normal
    </label>
    <label class="radio-inline">
      <input type="radio" name="reverse_direction"
      @if ($reverse_direction)
        checked="checked"
      @endif
      value="1"> Reversed
    </label>
  </div>
</div>

The screen

In your browser navigate to the screen we just created by clicking the [New Tag] button on the Tags Listing screen.

You should see a screen like the one below.

Figure 10.2 - New Tag Form

New Tag Form

Again, we have a beautiful looking form, created with a minimum of effort.

Implementing admin.tag.store

Now that we have a form for creating the tag, we need to build the code that will be executed when this form is filled out and submitted.

Adding TagCreateRequest

One of the great features of Laravel 5.1 are Form Requests. These are classes which contain validation logic for a particular form.

Create the new TagCreateRequest with the artisan command below.

Creating the TagCreateRequest class

vagrant@homestead:~/Code/l5beauty$ php artisan make:request TagCreateRequest
Request created successfully.

This creates a skeleton class in the app/Http/Requests directory. Edit the TagCreateRequest.php file created there to match what’s below.

Content of TagCreateRequest.php

<?php
namespace App\Http\Requests;

class TagCreateRequest extends Request
{

  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'tag' => 'required|unique:tags,tag',
      'title' => 'required',
      'subtitle' => 'required',
      'layout' => 'required',
    ];
  }
}

Up the hierarchy of this class is the Illuminate\Foundation\Http\FormRequest, which will perform the validation when the class is instantiated.

The two methods we must provide are:

  1. authorize()- This method should return whether or not this request is authorized. Since we don’t have any additional authorization (beyond the middleware protecting our route), we simply return true.
  2. rules() - Return an array of validation rules. We want the tag, title, subtitle, and layout all to be required values. Additionally, the tag must be unique in the tags database.

There’s many validation rules available and you can even create your own. See the Laravel Documentation for details.

The Magic of Form Requests

The “magic” of Form Requests is that they validate input during construction and if validation fails, control returns back to the form the user was just on with the appropriate error messages.

This means if a Form Request is an argument of a controller method, the form will be validated before the first line of that method is executed.

Implementing TagController store

Update the TagController.php file to match what’s below.

Updates to TagController for store

<?php
// Edit the "use" statements at the top so there's only the following
use App\Http\Controllers\Controller;
use App\Http\Requests\TagCreateRequest;
use App\Tag;

// Update the store() method to match what's below
  /**
   * Store the newly created tag in the database.
   *
   * @param TagCreateRequest $request
   * @return Response
   */
  public function store(TagCreateRequest $request)
  {
    $tag = new Tag();
    foreach (array_keys($this->fields) as $field) {
      $tag->$field = $request->get($field);
    }
    $tag->save();

    return redirect('/admin/tag')
        ->withSuccess("The tag '$tag->tag' was created.");
  }

Now, through the magic of Dependency Injection, the TagCreateRequest is constructed, the form is validated, and only if it passes validation will the object be passed to the store() method.

The store() method will then create and save the new Tag. Finally, it redirects back to the Tags Listing page, flashing a “success” message as it does.

Try adding a couple tags. You’ll be able to delete these tags later. Just get some data into the system to make sure it’s working.

Implementing admin.tag.edit

Next will edit the route named admin.tag.edit to present the form for editing a tag.

Implementing TagController edit

Update the TagController class, editing the edit() method to match what’s below.

Updates to TagController for edit

  /**
   * Show the form for editing a tag
   *
   * @param  int  $id
   * @return Response
   */
  public function edit($id)
  {
    $tag = Tag::findOrFail($id);
    $data = ['id' => $id];
    foreach (array_keys($this->fields) as $field) {
      $data[$field] = old($field, $tag->$field);
    }

    return view('admin.tag.edit', $data);
  }

Nothing magical is happening here. This function loads a Tag object based on the id, builds the associative array $data with either values from this object or the old input values, and returns a view passing the values to it.

Adding admin.tag.edit view

In the resources/views/admin/tag folder, create the edit.blade.php view with the following content.

Content of admin.tag.edit

@extends('admin.layout')

@section('content')
  <div class="container-fluid">
    <div class="row page-title-row">
      <div class="col-md-12">
        <h3>Tags <small>» Edit Tag</small></h3>
      </div>
    </div>

    <div class="row">
      <div class="col-md-8 col-md-offset-2">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">Tag Edit Form</h3>
          </div>
          <div class="panel-body">

            @include('admin.partials.errors')
            @include('admin.partials.success')

            <form class="form-horizontal" role="form" method="POST"
                  action="/admin/tag/{{ $id }}">
              <input type="hidden" name="_token" value="{{ csrf_token() }}">
              <input type="hidden" name="_method" value="PUT">
              <input type="hidden" name="id" value="{{ $id }}">

              <div class="form-group">
                <label for="tag" class="col-md-3 control-label">Tag</label>
                <div class="col-md-3">
                  <p class="form-control-static">{{ $tag }}</p>
                </div>
              </div>

              @include('admin.tag._form')

              <div class="form-group">
                <div class="col-md-7 col-md-offset-3">
                  <button type="submit" class="btn btn-primary btn-md">
                    <i class="fa fa-save"></i>
                      Save Changes
                  </button>
                  <button type="button" class="btn btn-danger btn-md"
                          data-toggle="modal" data-target="#modal-delete">
                    <i class="fa fa-times-circle"></i>
                    Delete
                  </button>
                </div>
              </div>

            </form>

          </div>
        </div>
      </div>
    </div>
  </div>

  {{-- Confirm Delete --}}
  <div class="modal fade" id="modal-delete" tabIndex="-1">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal">
            ×
          </button>
          <h4 class="modal-title">Please Confirm</h4>
        </div>
        <div class="modal-body">
          <p class="lead">
            <i class="fa fa-question-circle fa-lg"></i>  
            Are you sure you want to delete this tag?
          </p>
        </div>
        <div class="modal-footer">
          <form method="POST" action="/admin/tag/{{ $id }}">
            <input type="hidden" name="_token" value="{{ csrf_token() }}">
            <input type="hidden" name="_method" value="DELETE">
            <button type="button" class="btn btn-default"
                    data-dismiss="modal">Close</button>
            <button type="submit" class="btn btn-danger">
              <i class="fa fa-times-circle"></i> Yes
            </button>
          </form>
        </div>
      </div>
    </div>
  </div>

@stop

There’s a lot going on in this template, but it should be easy to follow.

Line 1
As always, we’re using the admin layout
Lines 19 and 20
And we use the partials to provide error or success feedback to the user.
Lines 22 - 25
Starting a form to save any edits. The $id is used so we’re updating the correct tag. Hidden values are used for the CSRF Token and to fake a PUT method.
Line 34
Using the same basic form as we used for the create.
Lines 42 - 46
In addition to the standard “Save” button, here’s a “Delete” button which will pop up a Modal dialog.
Lines 59 - 87
Here’s the Modal dialog we’re popping up. If the user chooses “Yes” then a POST is made which fakes a DELETE method.

The screen

Now if you click the [edit] button in your browser one one of the tags added to the system (from the Tags Listing) screen, you’ll see the edit form. If you click the [Delete] button from the edit form you’ll see a screen similar to the one below.

Figure 10.3 - Tag Edit Form

Tag Edit Form

Of course, the tag cannot yet be deleted or updated, we’ll do that next.

Implementing admin.tag.update

In the same way the admin.tag.store route processes the result of the admin.tag.create form, the admin.tag.update route will process the result of the admin.tag.edit form.

Adding TagUpdateRequest

To create the skeleton of the Form Request to handle the update, use the artisan command as illustrated below.

Creating the TagUpdateRequest class

~/Code/l5beauty$ php artisan make:request TagUpdateRequest
Request created successfully.

Now edit the TagUpdateRequest.php file to match what’s below. (This file should have been created in the app/Http/Requests directory.)

Content of TagUpdateRequest.php

<?php
namespace App\Http\Requests;

class TagUpdateRequest extends Request
{

  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'title' => 'required',
      'subtitle' => 'required',
      'layout' => 'required',
    ];
  }
}

This request class is almost exactly the same as the TagCreateRequest we created earlier in this chapter.

Implementing TagController update

Update the TagController.php file to match what’s below.

Updates to TagController for update

// Add the new use statement near the top
use App\Http\Requests\TagUpdateRequest;

// Replace the update() method with the following
  /**
   * Update the tag in storage
   *
   * @param TagUpdateRequest $request
   * @param int  $id
   * @return Response
   */
  public function update(TagUpdateRequest $request, $id)
  {
    $tag = Tag::findOrFail($id);

    foreach (array_keys(array_except($this->fields, ['tag'])) as $field) {
      $tag->$field = $request->get($field);
    }
    $tag->save();

    return redirect("/admin/tag/$id/edit")
        ->withSuccess("Changes saved.");
  }

Now you should be able to edit a tag and click [Save Changes] button successfully.

Finishing the Tag System

The administration side of our tagging system is almost finished. (We’ll tie the actual tags to posts in a later chapter when we implement the Post create and edit functionality.)

The two things left are implementing the destroy() method and a bit of cleanup.

Implementing TagController destroy

To add the ability to delete tags from the edit screen, update the destroy() method in TagController.php as illustrated below.

Updates to TagController for destroy()

  /**
   * Delete the tag
   *
   * @param  int  $id
   * @return Response
   */
  public function destroy($id)
  {
    $tag = Tag::findOrFail($id);
    $tag->delete();

    return redirect('/admin/tag')
        ->withSuccess("The '$tag->tag' tag has been deleted.");
  }

No magic here. Loads the tag, deletes it, and returns to the Tags Listing page with a success message.

Removing admin.tag.show

You may have noticed there’s one route that hasn’t been used. The admin.tag.show route was set up to show a tag. Often a show is used when a user doesn’t have editing privileges, but does have viewing privileges.

The admin.tag.show route isn’t needed in our system. So let’s remove it.

First adjust the route in app/Http/routes.php as instructed below.

Updating routes to remove admin.tag.show

// Change the following line
    resource('admin/tag', 'TagController');
// to this
    resource('admin/tag', 'TagController', ['except' => 'show']);

This addition tells the router to set up all the resource routes except for the show route.

Then edit TagController.php and remove the show() method.

Congratulations

Now the l5beauty blog administration has all the functionality in place to create, update, and delete tags from your system.

Recap

This was a fairly long chapter, but in it we fully implemented the administration side of our tagging system. This included everything from setting up the Tag model, creating database migration, finishing the TagController class, using Form Requests, and creating the needed Blade template views.

And, don’t forget, the resulting interface was simple and clean.

In the next chapter we’ll add the “Upload” feature to the administration.

comments powered by Disqus