Discount week - July
Save up to 80 % on our Swift e-learning courses. Only this week!
Get up to 50 % extra points for free! More info

Lesson 8 - Simple CMS in Laravel - Article creation

In the last lesson, Simple CMS in Laravel - Article listing, we viewed our first article, which we had previously prepared.

In today's lesson we will create an administration and we'll start by directly modifying the generated controller methods and creating views, as we already have the model layer and routes ready.

Articles list

First we will create a view for the articles list.

Controller action

As we already know, we will use the index() method for this. It contains nothing more than a view to which it passes all the articles in alphabetical order:

/**
 * Displays a list of articles in alphabetical order.
 *
 * @return Response
 */
public function index()
{
    return view('article.index', ['articles' => Article::orderBy('title')->get()]);
}

If you are surprised by the IDE's warning of the non-existent orderBy() method, see the end of this lesson for the Magic chapter hidden in the __call () method with a detailed explanation of this functionality.

View

Now let's create a new view in the resources/views/article/ folder and name it index.blade.php. It will be a simple listing of articles in a table:

@extends('base')

@section('title', 'List of articles')
@section('description', 'Listing of all articles in the administration.')

@section('content')
    <table class="table table-striped table-bordered table-responsive-md">
        <thead>
            <tr>
                <th>Title</th>
                <th>Description</th>
                <th>Creation date</th>
                <th>Date of last change</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @forelse ($articles as $article)
                <tr>
                    <td>
                        <a href="{{ route('article.show', ['article' => $article]) }}">
                            {{ $article->title }}
                        </a>
                    </td>
                    <td>{{ $article->description }}</td>
                    <td>{{ $article->created_at }}</td>
                    <td>{{ $article->updated_at }}</td>
                    <td>
                        <a href="{{ route('article.edit', ['article' => $article]) }}">Edit</a>
                        <a href="#" onclick="event.preventDefault(); $('#article-delete-{{ $article->id }}').submit();">Remove</a>

                        <form action="{{ route('article.destroy', ['article' => $article]) }}" method="POST" id="article-delete-{{ $article->id }}" class="d-none">
                            @csrf
                            @method('DELETE')
                        </form>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="5" class="text-center">
                        No one has created an article yet.
                    </td>
                </tr>
            @endforelse
        </tbody>
    </table>

    <a href="{{ route('article.create') }}" class="btn btn-primary">
        Create a new article
    </a>
@endsection

The first interesting thing about this view is the use of the Blade directive @forelse ... @empty ... @endforelse, which prints records using the PHP foreach() loop only if some exist. Otherwise, the user will see text about the non-existing articles.

Run the DELETE method

It is also worth noting the reference to editing the article. As the second parameter of the helper function route() we pass the field with parameters for the given route. The key to each value, which is either the record identifier (usually an id, in our case the url) or an instance of the model, is the name of the parameter.

However, we cannot use simple reference to delete an article, because HTTP is done using the DELETE method. For security reasons, data deletion should not rely on GET or POST methods. DELETE is actually a POST extension. Instead, we will create a hidden form, which will be sent after clicking on the link (via the onclick event). The HTTP declaration of the DELETE method in the form is done through the Blade directive @method.

The Blade directive @method inserts a hidden box on the form just like the Blade directive @csrf. If we look at the hidden form of one of the articles via the "Inspect element" (F12 key in the browser), we will see only two hidden fields, whose names begin with the prefix _, so that they do not confuse with the fields defined by us, see below.

<form action="http://localhost:8000/article/introduction" method="POST" id="article-delete-1" class="d-none">
    <input type="hidden" name="_token" value="g7K5Lt8LRE1pzVlrWfVhCwNy78UgP6f8fPIwHXnb">
    <input type="hidden" name="_method" value="DELETE">
</form>

Link to the articles list

Finally, we must not forget to refer to the newly functioning page in our menu, which is located in the main template resources/views/base.blade.php:

<nav class="my-2 my-md-0 mr-md-3">
    <a class="p-2 text-dark" href="#">Main page</a>
    <a class="p-2 text-dark" href="{{ route('article.index') }}">Articles</a>
    <a class="p-2 text-dark" href="#">Contact</a>
</nav>

Creating new articles

Next we will look at new article creation.

create() and store() actions

We will display the form for the new article creation in the create() action:

/**
 * Display the form for new article creation.
 *
 * @return Response
 */
public function create()
{
    return view('article.create');
}

Form validation and article save will take place in the store() action:

/**
 * Validate the submitted data via the form and create a new article.
 *
 * @param  Request $request
 * @return Response
 * @throws ValidationException
 */
public function store(Request $request)
{
    $this->validate($request, [
        'title' => ['required', 'min:3', 'max:80'],
        'url' => ['required', 'min:3', 'max:80', 'unique:articles,url'],
        'description' => ['required', 'min:25', 'max:255'],
        'content' => ['required', 'min:50'],
    ]);

    $article = new Article();
    $article->title = $request->input('title');
    $article->url = $request->input('url');
    $article->description = $request->input('description');
    $article->content = $request->input('content');
    $article->save();

    return redirect()->route('article.index');
}

As you can see, the store() method contains the $request parameter, even if it is not defined in the routing table. We get the parameter again using dependency injection, because we define what type of object it is. Although we can work with the helper function request() as in previous lessons (in this case the method would have no parameter), in the next lesson we will show why in some cases it's good to use this object-oriented approach.

Also, don't forget to import the ValidationException class:

use Illuminate\Validation\ValidationException;

View

We will create the view create.blade.php in the folder resources/views/article/. A novelty of this view is the use of the helper function old(), which contains old form data, for example in the case when the data for a new article does not pass through the validation rules:

@extends('base')

@section('title', 'Article creation')
@section('description', 'Editor to create a new article.')

@section('content')
    <h1>Article creation</h1>

    <form action="{{ route('article.store') }}" method="POST">
        @csrf

        <div class="form-group">
            <label for="title">Title</label>
            <input type="text" name="title" id="title" class="form-control" value="{{ old('title') }}" required minlength="3" maxlength="80" />
        </div>

        <div class="form-group">
            <label for="url">URL</label>
            <input type="text" name="url" id="url" class="form-control" value="{{ old('url') }}" required minlength="3" maxlength="80" />
        </div>

        <div class="form-group">
            <label for="description">Article description</label>
            <textarea name="description" id="description" rows="4" class="form-control" required minlength="25" maxlength="255">{{ old('description') }}</textarea>
        </div>

        <div class="form-group">
            <label for="content">Article content</label>
            <textarea name="content" id="content" class="form-control" rows="8">{{ old('content') }}</textarea>
        </div>

        <button type="submit" class="btn btn-primary">Create an article</button>
    </form>
@endsection

@push('scripts')
    <script type="text/javascript" src="{{ asset('//cdn.tinymce.com/4/tinymce.min.js') }}"></script>
    <script type="text/javascript">
        tinymce.init({
            selector: '#content',
            plugins: [
                'advlist autolink lists link image charmap print preview anchor',
                'searchreplace visualblocks code fullscreen',
                'insertdatetime media table contextmenu paste'
            ],
            toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image',
            entities: '160,nbsp',
            entity_encoding: 'raw'
        });
    </script>
@endpush

For the editor of the content of the article, I decided to use the external tool TinyMCE, a useful WYSIWYG HTML editor resembling, for example, MS Word.

Now we can create new articles. We can try it on the page /article/create (simply click on it from the articles list):

Editor for creating new articles in the content management system in Laravel

However, the code in the store() method for create a new article seems repetitive and can just lead to unnecessary typographical errors, as we need to define a value for each attribute:

$article = new Article();
$article->title = $request->input('title');
$article->url = $request->input('url');
$article->description = $request->input('description');
$article->content = $request->input('content');
$article->save();

So let's improve this method a bit.

Mass assignment

Instead of setting values one by one, we can use the Eloquent method of create(), where we pass only the data field from the form, where the keys are the column names:

Article::create($request->all());

We now have only one line instead of six, while maintaining the same application logic. Unfortunately, as you may already know, this method could also get unwanted data into the article. In our case, it wouldn't matter, after all, there is nothing to abuse on the articles. However, for more important database tables, such as users, without our knowledge, a different value could be passed than we would expect, for example for administrator rights. This attack is called mass assignment.

Laravel automatically protects us from this attack. If we try to create a new article now, we get the following error:

Mass assignment error in the content management system in Laravel

As the error message tells us, in our Article model, we should define an array of properties that can be passed. The $fillable variable is used for this, so now we'll add all the properties of our articles like that:

/**
 * An array of properties that are not protected from a mass assignment attack.
 *
 * @var array
 */
protected $fillable = [
    'title', 'url', 'description', 'content',
];

If we try to create an article now, everything will work as it should. However, the IDE still warns us that the create() and orderBy() methods of the Article model are not defined. Why is that so?

Magic hidden in the __call() method

If you've ever been more interested in PHP, you've probably come across the term magic method. If not, you probably know at least one of them - __construct(). As you know, this is not exactly a method that we would call on an object in the code. Even so, it contains countless classes.

Magic methods are called automatically at some point. The moment when certain criteria are met. For the just mentioned constructor, it is the creation of an object. And for __call(), it is a method call that is not defined in the scope of the class. As you probably already know, this is one of the magical methods that is rewritten by the Model class and inherited by our Article model. Its content is as follows:

/**
 * Handle dynamic method calls into the model.
 *
 * @param  string  $method
 * @param  array  $parameters
 * @return mixed
 */
public function __call($method, $parameters)
{
    if (in_array($method, ['increment', 'decrement'])) {
        return $this->$method(...$parameters);
    }

    return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}

If we wanted to look even deeper, we would also have to open the forwardCallTo() method. This is how we would go on and on. However, this context is enough for us. Note that all methods that do not exist and are not increment() or decrement() are automatically passed to the builder of the object that is obtained from the newQuery() method. This object provides us with the famous Eloquent ORM through the well-known Builder class.

If we wanted to be specific and avoid all the warnings in our IDE, creating an article would look like this:

Article::query()->create($request->all());

In fact, a mere static call to the create() method at runtime is converted to such a format.

Although we can find definitions of the increase() and decrease() methods in the Model class, they are not static methods. However, we use __call(), which also includes unknown static methods, to create their static form :)

We will also use the option of passing values in a field from a form to a model method when editing an article.

In the next lesson, Simple CMS in Laravel - Article management, we'll talk about HTTP request classes and look at article management.


 

Previous article
Simple CMS in Laravel - Article listing
All articles in this section
Laravel Framework for PHP
Article has been written for you by Lishaak
Avatar
Do you like this article?
No one has rated this quite yet, be the first one!
Author is interested in programming mostly at web development, sometimes he does funny video edits from his vacations. He also loves memes and a lot of TV series :)
Activities (1)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!