Published on

Build Simple Laravel 8 CRUD Application

Which is better, the Laravel framework or another framework? Yes, this is one of the questions we often find, and we sometimes ask ourselves. And to find the answer to this question, I decided to learn Laravel 8 and build a simple CRUD application. Of course, to learn seriously, there is a learning roadmap that you can follow to learn the Laravel 8 framework. This article was created to document learning when Laravel 8 leaks into gross material.

Table of Contents

CRUD App Overview

Compared to the framework we built in the previous post, the simple crud application we will create in this tutorial is a simple blog. As you build this application, you will learn a few things related to interacting with the database, such as creating data, reading data, updating data, and deleting data. To handle interacting with the database, we will use Eloquent, the Laravel framework's object-relational mapper (ORM).

Step 1: Setup Laravel 8 Project

First we create new laravel 8 project using composer. Open terminal, and run below command to create laravel 8 project.

composer create-project --prefer-dist laravel/laravel blog

After we run the command above, we can see the laravel 8 installation process. After the installation process is complete we can see a new project directory called blog. We go to the project directory by running the command.

cd blog

Then we can try first whether the installation process of our Laravel 8 project is successful. We tested it by running the local development server using the Artisan CLI command serve.

php artisan serve

We can open http://127.0.0.1:8000 in our browser for a test installation. When we open the url in the browser, we can see the start page of laravel 8, a sign that the laravel 8 installation was successful.

Step 2: Setup Database

The next step is to set the database configuration of our project. The story is in our project this time, the name of the database that we will use is db_blog, then the mysql credential on our laptop, we have the username admin and the password is password.

Now, we create a database with the name db_blog, either via phpmyadmin or through the terminal.

After creating the database, to connect to the db_blog database, we try to open the .env file in a text editor, then adjust the database configuration, as below.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=db_blog
DB_USERNAME=admin
DB_PASSWORD=password

When finished, save the .env file again before continuing to the next step.

Step 3: Create Model dan Migration Files

In this step we will try to create the model and migration with one artisan command. Back to the terminal or cmd, we run the artisan command below.

php artisan make:model Post -m

We can see that there are two files that have been successfully generated using the command above, the first one is the model file app/Models/Post.php and the second one is the migration file database/migrations/2021_08_18_043743_create_posts_table.php. Now for the record, the name of the migration file is adjusted to the date when the migration file was created.

For example, friends ask, if without the migration file, what is the artisan command that we use? We can directly type the command php artisan make:model Post without any sign or -m option. To generate the migration file when we generate the model, we can add the --migration or -m option as shown above.

Next, we try to set or define what model attributes we will add during the mass assignment or insert new data into the database. Open the app/Models/Post.php file, then add the $fillable properties in the Post class.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'content', 'slug', 'status'
    ];
}

We can see in the above line of code, in the $fillable properties, we allow model attributes, namely title, content, slug and status to be inserted into the database when we use the create method. later. Another option we can use the $guarded attribute.

After setting the mass assignable in the model, then we will create a table using the migration file that we generated earlier. Open the migration file 2021_08_18_043743_create_posts_table.php (don't forget the name matches the date the migration file was generated, so it's definitely a different name on the date). Next we define what columns are in the posts table that we will create.

<?php

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

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('slug');
            $table->smallInteger('status');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}


Once done, we run the migration using the following artisan command.

php artisan migrate

We can see that there are several new tables in our project database, including the posts table we just defined in the migration file.

Step 4: Create View Post Data

Alright, our project preparation is complete, then we can start to create the first feature, which is a feature to display data. Of course, because our case study is about a blog, the data we will display is post data from our blog.

To handle the data to be displayed, we try to create a new controller, namely PostController using the following artisan command.

php artisan make:controller PostController

After running the above command successfully, you will see a new controller file appear in the app/Http/Controllers directory called PostController.php file. Open the file app/Http/Controllers/PostController.php in a text editor. Open the app/Http/Controllers/PostController.php file and add the index() method to the PostController class.

<?php

namespace App\Http\Controllers;

use App\Models\Post; // load Post model
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::latest()->get();
        return view('posts.index', compact('posts'));
    }
}


As you can see in the above line of code, in the index() method we take the post data, then we pass the post data to the index.blade.php view via the view() method as the second parameter. . Because we are using a model, don't forget to import the class first using the use statement.

<?php

namespace App\Http\Controllers;

use App\Models\Post; // add use statement
use Illuminate\Http\Request;

class PostController extends Controller
{
	// PostController class content
}


OK, let's continue.

In the view() method, there is a posts.index parameter which means we will create a new folder, called posts in resources/views. After that we create a new file resources/views/posts/index.blade.php. Then we type the following code.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="csrf-token" content="{{ csrf_token() }}" />
    <title>Post List - Tutorial CRUD Laravel 8 @ gungunpriatna.com</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
    />
  </head>

  <body>
    <div class="container mt-5">
      <div class="row">
        <div class="col-md-12">
          <!-- Notif using flash session data -->
          @if (session('success'))
          <div class="alert alert-success">{{ session('success') }}</div>
          @endif @if (session('error'))
          <div class="alert alert-error">{{ session('error') }}</div>
          @endif

          <div class="card border-0 shadow rounded">
            <div class="card-body">
              <a href="{{ route('post.create') }}" class="btn btn-md btn-success mb-3 float-right"
                >New Post</a
              >

              <table class="table table-bordered mt-1">
                <thead>
                  <tr>
                    <th scope="col">Title</th>
                    <th scope="col">Status</th>
                    <th scope="col">Create At</th>
                    <th scope="col">Action</th>
                  </tr>
                </thead>
                <tbody>
                  @forelse ($posts as $post)
                  <tr>
                    <td>{{ $post->title }}</td>
                    <td>{{ $post->status == 0 ? 'Draft':'Publish' }}</td>
                    <td>{{ $post->created_at->format('d-m-Y') }}</td>
                    <td class="text-center">
                      <form
                        onsubmit="return confirm('Are you sure ?');"
                        action="{{ route('post.destroy', $post->id) }}"
                        method="POST"
                      >
                        <a href="{{ route('post.edit', $post->id) }}" class="btn btn-sm btn-primary"
                          >EDIT</a
                        >
                        @csrf @method('DELETE')
                        <button type="submit" class="btn btn-sm btn-danger">DELETE</button>
                      </form>
                    </td>
                  </tr>
                  @empty
                  <tr>
                    <td class="text-center text-mute" colspan="4">Data not found</td>
                  </tr>
                  @endforelse
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
  </body>
</html>

Yes, in our view there are several blade directives, namely @forelse which is used to display data, and @if to display notifications. In addition, there is also a 'route()' method to go to the create, edit and delete data pages.

Next we try to register a new route, we open routes/web.php file. We add a new route to open our post data page in the file.

<?php

use App\Http\Controllers\PostController; //load controller post

// another line of code

Route::resource('post', PostController::class);

One route above declares several routes which can handle multiple actions for resource such as create, show, update, delete and others. You can check existing routes using the artisan command.

php artisan route:list

The command displays the route contained in our laravel application.

+--------+-----------+---------------------+--------------+------------------------------------------------------------+------------------------------------------+
| Domain | Method    | URI                 | Name         | Action                                                     | Middleware                               |
+--------+-----------+---------------------+--------------+------------------------------------------------------------+------------------------------------------+
|        | GET|HEAD  | post                | post.index   | App\Http\Controllers\PostController@index                  | web                                      |
|        | POST      | post                | post.store   | App\Http\Controllers\PostController@store                  | web                                      |
|        | GET|HEAD  | post/create         | post.create  | App\Http\Controllers\PostController@create                 | web                                      |
|        | GET|HEAD  | post/{post}         | post.show    | App\Http\Controllers\PostController@show                   | web                                      |
|        | PUT|PATCH | post/{post}         | post.update  | App\Http\Controllers\PostController@update                 | web                                      |
|        | DELETE    | post/{post}         | post.destroy | App\Http\Controllers\PostController@destroy                | web                                      |
|        | GET|HEAD  | post/{post}/edit    | post.edit    | App\Http\Controllers\PostController@edit                   | web                                      |
+--------+-----------+---------------------+--------------+------------------------------------------------------------+------------------------------------------+

From the list of routes above, we can also see what methods should be in the PostController class.

Step 5: Create a New Post

We have created a page with a list of posts, but we can't add new data yet. Now, let's add the ability to create a new post. To add this functionality, open the "PostController" controller again and then create a new post by adding a new method to render the form page.

<?php
// ... previous line of code

class PostController extends Controller
{
    // ... previous line of code

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

}

Then create a new view according to the view(posts.create)method in the above line of code. Create a new view fileresources/views/posts/create.blade.php` and try the following line of code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="csrf-token" content="{{ csrf_token() }}" />
    <title>Create New Post - Tutorial CRUD Laravel 8 @ gungunpriatna.com</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
    />
    <!-- include summernote css -->
    <link
      href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css"
      rel="stylesheet"
    />
  </head>

  <body>
    <div class="container mt-5 mb-5">
      <div class="row">
        <div class="col-md-12">
          <!-- notif using flash session data -->
          @if (session('success'))
          <div class="alert alert-success">{{ session('success') }}</div>
          @endif @if (session('error'))
          <div class="alert alert-error">{{ session('error') }}</div>
          @endif

          <div class="card border-0 shadow rounded">
            <div class="card-body">
              <form action="{{ route('post.store') }}" method="POST">
                @csrf

                <div class="form-group">
                  <label for="title">Title</label>
                  <input
                    type="text"
                    class="form-control @error('title') is-invalid @enderror"
                    name="title"
                    value="{{ old('title') }}"
                    required
                  />

                  <!-- error message for title -->
                  @error('title')
                  <div class="invalid-feedback">{{ $message }}</div>
                  @enderror
                </div>

                <div class="form-group">
                  <label for="status">Publish Status</label>
                  <select name="status" class="form-control" required>
                    <option value="1" selected>Publish</option>
                    <option value="0">Draft</option>
                  </select>
                </div>

                <div class="form-group">
                  <label for="content">Content</label>
                  <textarea
                    name="content"
                    id="content"
                    class="form-control @error('content') is-invalid @enderror"
                    rows="5"
                    required
                  >
{{ old('content') }}</textarea
                  >

                  <!-- error message for content -->
                  @error('content')
                  <div class="invalid-feedback">{{ $message }}</div>
                  @enderror
                </div>

                <button type="submit" class="btn btn-md btn-primary">Save</button>
                <a href="{{ route('post.index') }}" class="btn btn-md btn-secondary">back</a>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script
      src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
      integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
      crossorigin="anonymous"
    ></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

    <!-- include summernote js -->
    <script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>
    <script>
      $(document).ready(function () {
        $('#content').summernote({
          height: 250, //set editable area's height
        })
      })
    </script>
  </body>
</html>

In the above form, the form action points to the post.store path, and when checked with the artisan command, this path is connected to the store() method in the PostController class. Then add a store() method to the PostController which will handle the process of saving the data.

<?php

// ... previous line of code

use Illuminate\Support\Str; // tambahkan kode ini

class PostController extends Controller
{

    // ... previous line of code

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

        $post = Post::create([
            'title' => $request->title,
            'content' => $request->content,
            'status' => $request->status,
            'slug' => Str::slug($request->title)
        ]);

        if ($post) {
            return redirect()
                ->route('post.index')
                ->with([
                    'success' => 'New post has been created successfully'
                ]);
        } else {
            return redirect()
                ->back()
                ->withInput()
                ->with([
                    'error' => 'Some problem occurred, please try again'
                ]);
        }
    }
}


The algorithm for adding data to the store() method consists of three parts: the validation process, the data storage process, and the redirection to the page after the store process completes.

Validation process uses validate() method with $request as first parameter and validation rules as second parameter. For example, this tutorial uses only 'required', 'string' for string datatype, and 'max:value' for field field. If you do not fill out the form according to the validation rules, the web returns to the Add Data form page and displays an error message in each field. We can see the create.blade.php file which contains the following code.

<!-- error message for title -->
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror

Use the @error directive to check for validation error messages for a particular attribute. The @ error directive allows you to display error messages using the echo variable $message.

If the validation process is appropriate, there is a data insertion process. The code section of the data insertion process uses the create() method in this tutorial. This method takes an array of attributes and performs an operation that inserts data into the database.

$post = Post::create([
    'title' => $request->title,
    'content' => $request->content,
    'status' => $request->status,
    'slug' => Str::slug($request->title)
]);

Oh yes, for the slug attribute, we use the str helper to generate a URL friendly slug. Therefore there is an additional use statement before the PostController class declaration.

use Illuminate\Support\Str;

And the last part is "redirected" to the page determined according to the conditions for successful data insertion. If the insert operation is successful, we will be taken to the post list page, and if an error occurs, we will be returned to the form page.

Step 6: Update post data

Once we have added the data, you'll see that each row of data in the post list table has an Edit button. We can check again. We can find the following line of code in the index.blade.php file.

<a href="{{ route('post.edit', $post->id) }}" class="btn btn-sm btn-info shadow">Edit</a>

The edit button leads to the post.edit route with $post->id as the parameter, where this route points to the edit() method in the PostController class. Now we reopen PostController, then we add the edit() method to display the edit data page.

<?php

// ... previous line of code

class PostController extends Controller
{

    // ... previous line of code

    public function edit($id)
    {
        $post = Post::findOrFail($id);
        return view('posts.edit', compact('post'));
    }
}


In the edit() method above, we try to pass the $post data which contains post data based on the $id parameter to the edit.blade.php view by writing in the second parameter the view() method.

Next we create a new view file calledresources/views/posts/edit.blade.php, then we type the following line of code.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="csrf-token" content="{{ csrf_token() }}" />
    <title>Edit Post - Tutorial CRUD Laravel 8 @ gungunpriatna.com</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
    />
    <!-- include summernote css -->
    <link
      href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css"
      rel="stylesheet"
    />
  </head>

  <body>
    <div class="container mt-5 mb-5">
      <div class="row">
        <div class="col-md-12">
          <!-- notif using flash session data -->
          @if (session('success'))
          <div class="alert alert-success">{{ session('success') }}</div>
          @endif @if (session('error'))
          <div class="alert alert-error">{{ session('error') }}</div>
          @endif

          <div class="card border-0 shadow rounded">
            <div class="card-body">
              <form action="{{ route('post.update', $post->id) }}" method="POST">
                @csrf @method('PUT')

                <div class="form-group">
                  <label for="title">Title</label>
                  <input
                    type="text"
                    class="form-control @error('title') is-invalid @enderror"
                    name="title"
                    value="{{ old('title', $post->title) }}"
                    required
                  />

                  <!-- error message for title -->
                  @error('title')
                  <div class="invalid-feedback">{{ $message }}</div>
                  @enderror
                </div>

                <div class="form-group">
                  <label for="status">Publish Status</label>
                  <select name="status" class="form-control" required>
                    <option value="1" {{ $post->status == 1 ? 'selected':'' }}>Publish</option>
                    <option value="0" {{ $post->status == 0 ? 'selected':'' }}>Draft</option>
                  </select>
                </div>

                <div class="form-group">
                  <label for="content">Content</label>
                  <textarea
                    name="content"
                    id="content"
                    class="form-control @error('content') is-invalid @enderror"
                    name="content"
                    id="content"
                    rows="5"
                    required
                  >
{{ old('content', $post->content) }}</textarea
                  >

                  <!-- error message for content -->
                  @error('content')
                  <div class="invalid-feedback">{{ $message }}</div>
                  @enderror
                </div>

                <button type="submit" class="btn btn-md btn-primary">Update</button>
                <a href="{{ route('post.index') }}" class="btn btn-md btn-secondary">back</a>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script
      src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
      integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
      crossorigin="anonymous"
    ></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
    <!-- include summernote js -->
    <script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>
    <script>
      $(document).ready(function () {
        $('#content').summernote({
          height: 250, //set editable area's height
        })
      })
    </script>
  </body>
</html>

The view you create for the post data edit page is not much different from the post data add page, except for the following sections:

<form action="{{ route('post.update', $post->id) }}" method="POST">@csrf @method('PUT')</form>

You can see it in the Forms section. Since HTML forms cannot make PUT requests, we add a PUT method first for the update process. Second, the form action points to the post.update path with an ID as a parameter.

Now create a new method to handle data update process in PostController according to form action.

<?php

// ... previous line of code

class PostController extends Controller
{

    // ... previous line of code

    public function update(Request $request, $id)
    {
        $this->validate($request, [
            'title' => 'required|string|max:155',
            'content' => 'required',
            'status' => 'required'
        ]);

        $post = Post::findOrFail($id);

        $post->update([
            'title' => $request->title,
            'content' => $request->content,
            'status' => $request->status,
            'slug' => Str::slug($request->title)
        ]);

        if ($post) {
            return redirect()
                ->route('post.index')
                ->with([
                    'success' => 'Post has been updated successfully'
                ]);
        } else {
            return redirect()
                ->back()
                ->withInput()
                ->with([
                    'error' => 'Some problem has occured, please try again'
                ]);
        }
    }
}

The algorithm of the update() method is not very different from the store() method, which inserts data, except for the data update process. In the data update process, you use the findOrFail() method to get the post data based on the $id parameter, and then use the update() method to update the data with an array containing the properties as parameters.

        $post = Post::findOrFail($id);

        $post->update([
            'title' => $request->title,
            'content' => $request->content,
            'status' => $request->status,
            'slug' => Str::slug($request->title)
        ]);

Step 7: Delete Post Data

The last feature of the CRUD to add to the project is the post-data deletion feature. Open the "PostController.php" file and add the "destroy()" method to the "PostController" class. This method handles the process of deleting data that has an ID as a parameter.

<?php

// ... previous line of code

class PostController extends Controller
{
    // ... previous line of code

    public function destroy($id)
    {
        $post = Post::findOrFail($id);
        $post->delete();

        if ($post) {
            return redirect()
                ->route('post.index')
                ->with([
                    'success' => 'Post has been deleted successfully'
                ]);
        } else {
            return redirect()
                ->route('post.index')
                ->with([
                    'error' => 'Some problem has occurred, please try again'
                ]);
        }
    }
}

The algorithm for deleting this data can be divided into three parts: confirming the data by ID, deleting the data, and redirecting to the post list page. In the data validation section, this $id is based on the post ID we wrote to the post list page or theindex.blade.php view. The index.blade.php view has the following line of code:

<form
  onsubmit="return confirm('Are you sure ?');"
  action="{{ route('post.destroy', $post->id) }}"
  method="POST"
>
  <a href="{{ route('post.edit', $post->id) }}" class="btn btn-sm btn-info shadow">Edit</a>
  @csrf @method('DELETE')
  <button type="submit" class="btn btn-sm btn-danger shadow">Delete</button>
</form>

The data is retrieved by ID using the findOrFail() method. If the data is found, it will be stored in the $post variable, otherwise the page will display an error. Once we find the data, use the delete() method to delete it.

$post->delete();

After that we switch back to the post list page, then display a message according to the conditions of success or failure of the delete process.

Finally, all steps in this Laravel 8 CRUD tutorial are complete. Now we can immediately relaunch the app with the artisan command.

php artisan serve

Then open url http://127.0.0.1:8000/post in browser. After that, we can try to add data, edit data and also delete data.

Conclusion

Which one is better the Laravel framework or another framework? That's a question that we often find in programming forums and may be a question that we often to ask. After studying laravel 8 to make this simple crud app, there are a few things I found. Yes, more or less I've found the answer. How about you? Have you found the answer?