Parte 5: Vamos a crear un CRUD con Laravel 8 para administrar un sistema de posts
Bueno bueno, lo primero feliz año nuevo a tod@s. Seguimos ultimando detalles en la creación de nuestro blog con Laravel 8 y para el tutorial de hoy, entraremos de lleno en la gestión de posts mediante la creación de un CRUD. Podremos listar nuestros posts, crear nuevos, editarlos y borrarlos, además podremos aprovechar las herramientas que nos brinda Laravel para hacerlo más rápido y sencillo 💪.
Eeeeeeeempezamos 🚀.
Enrutamiento
Para la gestión de los posts, vamos a necesitar varias rutas, una para listar, otra para el panel de creación, el guardado, etc... Esto podría ser un poco rollo, por eso, Laravel nos provee del método resource que genera todas esas rutas por nosotros con una sola línea. Para realizar esta acción, vamos al archivo routes/web.php y añadimos la siguiente línea:
Route::resource('/admin/posts', \App\Http\Controllers\PostController::class);
Esta línea nos creará todas las rutas para administrar nuestros posts. Si queréis ver todas las rutas que tenemos actualmente en nuestro proyecto, podemos usar el comando php artisan route:list:
php artisan route:list
+--------+-----------+-------------------------+------------------+------------------------------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+-------------------------+------------------+------------------------------------------------------------------------+------------+
| | GET|HEAD | / | home | App\Http\Controllers\PostController@home | web |
| | POST | admin/posts | posts.store | App\Http\Controllers\PostController@store | web |
| | GET|HEAD | admin/posts | posts.index | App\Http\Controllers\PostController@index | web |
| | GET|HEAD | admin/posts/create | posts.create | App\Http\Controllers\PostController@create | web |
| | DELETE | admin/posts/{post} | posts.destroy | App\Http\Controllers\PostController@destroy | web |
| | PUT|PATCH | admin/posts/{post} | posts.update | App\Http\Controllers\PostController@update | web |
| | GET|HEAD | admin/posts/{post} | posts.show | App\Http\Controllers\PostController@show | web |
| | GET|HEAD | admin/posts/{post}/edit | posts.edit | App\Http\Controllers\PostController@edit | web |
| | GET|HEAD | api/user | | Closure | api |
| | | | | | auth:api |
| | POST | comment | comments.store | App\Http\Controllers\CommentController@store | web |
| | POST | login | | App\Http\Controllers\Auth\LoginController@login | web |
| | | | | | guest |
| | GET|HEAD | login | login | App\Http\Controllers\Auth\LoginController@showLoginForm | web |
| | | | | | guest |
| | POST | logout | logout | App\Http\Controllers\Auth\LoginController@logout | web |
| | GET|HEAD | password/confirm | password.confirm | App\Http\Controllers\Auth\ConfirmPasswordController@showConfirmForm | web |
| | | | | | auth |
| | POST | password/confirm | | App\Http\Controllers\Auth\ConfirmPasswordController@confirm | web |
| | | | | | auth |
| | POST | password/email | password.email | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail | web |
| | GET|HEAD | password/reset | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web |
| | POST | password/reset | password.update | App\Http\Controllers\Auth\ResetPasswordController@reset | web |
| | GET|HEAD | password/reset/{token} | password.reset | App\Http\Controllers\Auth\ResetPasswordController@showResetForm | web |
| | GET|HEAD | posts/{slug} | posts.detail | App\Http\Controllers\PostController@detail | web |
| | POST | register | | App\Http\Controllers\Auth\RegisterController@register | web |
| | | | | | guest |
| | GET|HEAD | register | register | App\Http\Controllers\Auth\RegisterController@showRegistrationForm | web |
| | | | | | guest |
+--------+-----------+-------------------------+------------------+------------------------------------------------------------------------+------------+
Además, gracias a que añadimos el parámetro --resource cuando creamos el controlador PostController.php, también tenemos los métodos relacionados con estas rutas por lo que solo tendremos que definir su funcionamiento 🎉.
Mostrar listado de posts
Ahora que ya tenemos las rutas configuradas, es hora de ir al archivo PostController.php y crear la primera acción que será la de mostrar el listado de posts. Esta corresponderá a la ruta admin/posts y será una petición de tipo GET. Si nos fijamos en el listado anterior, esta acción corresponde al método index() de PostController.php así que lo que vamos a hacer es modificar este método para que contenga el siguiente código:
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
abort_unless(Auth::check(), 404);
$user = $request->user();
if ($user->isAdmin()) {
$posts = Post::orderBy('created_at', 'desc')->get();
} elseif ($user->isStaff()) {
$posts = Post::where('user_id', $user->id)->orderBy('created_at', 'desc')->get();
} else {
abort_unless(Auth::check(), 404);
}
return view('posts/list', [
'posts' => $posts
]);
}
Nota: También vamos a importar el siguiente espacio de nombres porque lo usaremos varias veces en esta clase:
use Illuminate\Support\Facades\Auth;
Lo que hace el método index(), básicamente es comprobar si el usuario está autenticado, si no es así, le envía a la página de error 404. Si pasa ese filtro comprueba si es un usuario con privilegios de administración. De ser así, mostrará todo el listado de posts. Por el contrario, si el usuario solo tiene permisos de staff, solo podrá ver los posts que él ha creado. Por último, si el usuario está autenticado, pero no tiene permisos, lo enviaremos a la página de error 404.
Si tiene los privilegios adecuados, retornamos la template posts/list con el listado de posts.
Ahora que ya tenemos la parte del controlador, vamos a crear la template. Para ello, crearemos el siguiente archivo resources/views/posts/list.blade.php y añadiremos el código que muestro a continuación:
@extends('..layouts.app')
@section('content')
<section class="w-full bg-gray-200 py-4 flex-row justify-center text-center">
<div class="flex justify-center">
<div class="max-w-4xl">
<h1 class="px-4 text-6xl break-words">List Post</h1>
</div>
</div>
</section>
<article class="w-full py-8">
<div class="flex justify-center">
<div class="max-w-7xl text-justify">@if($errors->any())
<div class="w-full bg-red-500 p-2 text-center my-2 text-white">
{{$errors->first()}}
</div>
@endif
@if (session('status'))
<div class="w-full bg-green-500 p-2 text-center my-2 text-white">
{{ session('status') }}
</div>
@endif
<div class="text-right py-2">
<a class="inline-block px-4 py-1 bg-orange-500 text-white rounded mr-2 hover:bg-orange-800" href="{{ route('posts.create') }}" title="Edit">Create new post</a>
</div>
<table class="table-auto">
<thead>
<tr>
<th class="px-2">Title</th>
<th class="px-2">Creation</th>
<th class="px-2">Author</th>
<th class="px-2">Status</th>
<th class="px-2">Actions</th>
</tr>
</thead>
<tbody>
@foreach($posts as $post)
<tr>
<td class="px-2">{{ $post->title }}</td>
<td class="px-2">{{ $post->created_at->format('j F, Y') }}</td>
<td class="px-2">{{ $post->user->name }}</td>
<td class="px-2">
@if ($post->is_draft)
<div class="text-red-500">In draft</div>
@else
<div class="text-green-500">Published</div>
@endif
</td>
<td class="px-2">
<a class="inline-block px-4 py-1 bg-blue-500 text-white rounded mr-2 hover:bg-blue-800" href="{{ route('posts.edit', $post) }}" title="Edit">Edit</a>
<a class="inline-block px-4 py-1 bg-red-500 text-white rounded mr-2 hover:bg-red-800 delete-post" href="{{ route('posts.destroy', $post) }}" title="Delete" data-id="{{$post->id}}">Delete</a>
<form id="posts.destroy-form-{{$post->id}}" action="{{ route('posts.destroy', $post) }}" method="POST" class="hidden">
{{ csrf_field() }}
@method('DELETE')
</form>
</td>
@endforeach
</tbody>
</table>
</div>
</div>
</article>
<script>
var delete_post_action = document.getElementsByClassName("delete-post");
var deleteAction = function(e) {
event.preventDefault();
var id = this.dataset.id;
if(confirm('Are you sure?')) {
document.getElementById('posts.destroy-form-' + id).submit();
}
return false;
}
for (var i = 0; i < delete_post_action.length; i++) {
delete_post_action[i].addEventListener('click', deleteAction, false);
}
</script>
@endsection
Si accedéis a http://127.0.0.1:8000/admin/posts podréis ver el resultado. En esta plantilla tendremos un botón que nos llevará a un panel para añadir nuevos posts y además listaremos todos los posts (o solo los de un usuario en concreto dependiendo de los permisos) de forma descendente en el que se verá la información más relevante como el título, la fecha de creación, el autor y si está publicado o en borradores. Además, tendremos la columna de acciones en el que podremos editar el post o eliminarlo.
Antes de continuar, vamos a añadir un nuevo enlace en la barra de navegación. De esta forma, cuando un usuario tenga privilegios de administración o sea staff, le aparecerá el enlace para ir al panel de administración de posts. Para ello, vamos al archivo resources/views/layouts/app.blade.php y añadimos este código entre las líneas 42 y 43:
@if( Auth::user()->isAdmin() or Auth::user()->isStaff() )
<li>
<a class="hover:text-blue-600" href="{{ route('posts.store') }}" title="Admin">
<i class="fas fa-user-shield"></i>
</a>
</li>
@endif
Crear posts
Seguimos con el panel de administración y ahora vamos a crear la vista para la creación de posts. Para ello, volvemos al archivo PostController.php y modificamos el método create() para que ahora contenga el siguiente código:
/**
* Show the form for creating a new resource.
* @param \Illuminate\Http\Request
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
abort_unless(Auth::check(), 404);
$request->user()->authorizeRoles(['is_staff', 'is_admin']);
return view('posts/create');
}
Verificamos que el usuario tenga permisos de acceso y si es así, mostramos la template posts/create que crearemos en el siguiente paso.
Ahora creamos el archivo resources/views/posts/create.blade.php y añadimos el siguiente código:
@extends('..layouts.app')
@section('content')
<section class="w-full bg-gray-200 py-4 flex-row justify-center text-center">
<div class="flex justify-center">
<div class="max-w-4xl">
<h1 class="px-4 text-6xl break-words">Create Post</h1>
</div>
</div>
</section>
<article class="w-full py-8">
<div class="flex justify-center">
<div class="max-w-7xl text-justify">
<form action="{{ route('posts.store') }}" method="post">
@csrf
<input class="w-full border rounded focus:outline-none focus:shadow-outline p-2 mb-4" type="text" name="title" value="{{ old('title') }}" placeholder="Write the title of the post">
<textarea class="w-full h-72 resize-none border rounded focus:outline-none focus:shadow-outline p-2 mb-4" name="body" placeholder="Write your post here" required>{{ old('body') }}</textarea>
<div class="mb-4">
<input type="hidden" name="is_draft" value="0">
<input type="checkbox" name="is_draft" value="1"> Is draft?
</div>
<input type="submit" value="SEND" class="px-4 py-2 bg-orange-300 cursor-pointer hover:bg-orange-500 font-bold w-full border rounded border-orange-300 hover:border-orange-500 text-white">
@if (session('status'))
<div class="w-full bg-green-500 p-2 text-center my-2 text-white">
{{ session('status') }}
</div>
@endif
@if($errors->any())
<div class="w-full bg-red-500 p-2 text-center my-2 text-white">
{{$errors->first()}}
</div>
@endif
</form>
</div>
</div>
</article>
@endsection
Esta template, contiene un formulario con los datos que necesitaremos para crear un post, además, si tenemos algún error a la hora de crear un post, podremos mostrárselos al usuario al igual que hicimos con los comentarios
Para este caso, también necesitaremos crear una request, para ello, lanzamos el siguiente comando:
php artisan make:request PostRequest
Una vez creado, lo abrimos y añadimos el siguiente código:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class PostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|max:255',
'body' => 'required',
'is_draft' => 'required',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array
*/
public function messages()
{
return [
'title.required' => 'A title is required',
'title.max' => 'A title cannot exceed 255 characters',
'body.required' => 'You must sent a body',
'is_draft.required' => 'You must sent if is draft or not',
];
}
}
Al igual que al crear un comentario, en este archivo, validamos los campos que nos interesen y creamos nuestros mensajes customizados.
Por último, volvemos a PostController.php y añadimos el siguiente código dentro del método store():
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\PostRequest $request
* @return \Illuminate\Http\Response
*/
public function store(PostRequest $request)
{
$request->validated();
$user = Auth::user();
$request->user()->authorizeRoles(['is_staff', 'is_admin']);
$post = new Post;
$post->title = $request->input('title');
$post->body = $request->input('body');
$post->is_draft = $request->input('is_draft');
$post->user()->associate($user);
$res = $post->save();
if ($res) {
return back()->with('status', 'Post has been created sucessfully');
}
return back()->withErrors(['msg', 'There was an error saving the post, please try again later']);
}
También necesitaremos añadir el espacio de nombres para PostRequest:
use App\Http\Requests\PostRequest;
Ahora, en nuestro navegador, vamos a la página con el listado de posts y al pulsar en el botón create new posts, nos llevará a la página donde podremos crear nuestros posts 🤘.
Actualizar posts
Nuestro siguiente paso será, la acción de poder editar posts. Para ello, volvemos al archivo PostController.php y modificamos el método edit() para que contenga el siguiente código:
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @param \Illuminate\Http\Request
* @return \Illuminate\Http\Response
*/
public function edit(Request $request, $id)
{
abort_unless(Auth::check(), 404);
$request->user()->authorizeRoles(['is_staff', 'is_admin']);
$post = Post::find($id);
if (($post->user->id != $request->user()->id) && !$request->user()->isAdmin()) {
abort_unless(false, 401);
}
return view('posts/edit', [
'post' => $post
]);
}
Este se encargará de comprobar que estamos autorizados. Si es así, verificará también que si el usuario no tiene permisos de administrador, sea propietario del post. Si no es así, le mostrará un error avisándolo de que no está autorizado para realizar tal acción.
Ahora creamos la plantilla en la ruta resources/views/posts/edit.blade.php y añadimos el siguiente código:
@extends('..layouts.app')
@section('content')
<section class="w-full bg-gray-200 py-4 flex-row justify-center text-center">
<div class="flex justify-center">
<div class="max-w-4xl">
<h1 class="px-4 text-6xl break-words">Edit Post</h1>
</div>
</div>
</section>
<article class="w-full py-8">
<div class="flex justify-center">
<div class="max-w-7xl text-justify">
<form action="{{ route('posts.update', $post) }}" method="post">
@csrf
@method('PUT')
<input class="w-full border rounded focus:outline-none focus:shadow-outline p-2 mb-4" type="text" name="title" value="{{ $post->title }}" placeholder="Write the title of the post">
<textarea class="w-full h-72 resize-none border rounded focus:outline-none focus:shadow-outline p-2 mb-4" name="body" placeholder="Write your post here" required>{{ $post->body }}</textarea>
<div class="mb-4">
<input type="hidden" name="is_draft" value="0">
@if (!$post->is_draft)
<input type="checkbox" name="is_draft" value="1">
@else
<input type="checkbox" name="is_draft" value="1" checked>
@endif
Is draft?
</div>
<input type="submit" value="SEND" class="px-4 py-2 bg-orange-300 cursor-pointer hover:bg-orange-500 font-bold w-full border rounded border-orange-300 hover:border-orange-500 text-white">
@if (session('status'))
<div class="w-full bg-green-500 p-2 text-center my-2 text-white">
{{ session('status') }}
</div>
@endif
@if($errors->any())
<div class="w-full bg-red-500 p-2 text-center my-2 text-white">
{{$errors->first()}}
</div>
@endif
</form>
</div>
</div>
</article>
@endsection
Una vez hecho esto, solo nos falta crear la función para realizar la acción de actualizar un post. Volvemos al archivo PostController.php y modificamos el método udpate() para que ahora contenga el siguiente código:
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\PostRequest $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(PostRequest $request, $id)
{
$request->validated();
$request->user()->authorizeRoles(['is_staff', 'is_admin']);
$post = Post::find($id);
if (($post->user->id != $request->user()->id) && !$request->user()->isAdmin()) {
abort_unless(false, 401);
}
$post->title = $request->input('title');
$post->body = $request->input('body');
$post->is_draft = $request->input('is_draft');
$res = $post->save();
if ($res) {
return back()->with('status', 'Post has been updated sucessfully');
}
return back()->withErrors(['msg', 'There was an error updating the post, please try again later']);
}
En este método, validamos que exista el post, también verificamos que el usuario tenga permisos para realizar la acción y si es así lo actualizamos.
Ahora ya podéis volver al listado de posts y al pulsar el botón para editar un post, deberíais poder realizar la acción sin problema 💪.
Borrar posts
Por último realizaremos la acción de borrar un post. En la template que muestra el listado de posts, ya tenemos configurada la acción de borrar un post. Si vamos al archivo list.blade.php, podremos ver el formulario a partir de la línea 53 en la que además de pasar el campo csrf, añadimos @method('DELETE') para que Laravel sepa que tipo de acción estamos realizando.
Ahora solo necesitamos volver al archivo PostController.php y modificar el método destroy() para que contenga el siguiente código:
/**
* Remove the specified resource from storage.
*
* @param \Illuminate\Http\Request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, $id)
{
abort_unless(Auth::check(), 404);
$request->user()->authorizeRoles(['is_staff', 'is_admin']);
$post = Post::where('id', $id)->first();
if (($post->user->id != $request->user()->id) && !$request->user()->isAdmin()) {
abort_unless(false, 401);
}
$post->delete();
return back()->with('status', 'Post has been deleted sucessfully');
}
Este método comprobará que el usuario tenga los permisos adecuados, además si no es usuario administrador, necesitará ser propietario del post para poder borrarlo.
Uff y eso es todo por hoy, menudo palizón me he dado con este post 🥱. Espero que os ayude y nos vemos en el último post de esta serie donde veremos como implementar los test unitarios en nuestro proyecto (disponible el 21 de enero de 2021).
Como siempre os dejo el enlace al proyecto.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.