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 Application|Factory|View */ 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 Application|Factory|View */ 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 RedirectResponse * @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
and
RedirectResponse
class:
use Illuminate\Http\RedirectResponse; 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):
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:
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 method
__call()
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); } if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) { return $resolver($this); } 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.