logo cosasdedevs

Parte 3: Crear una página de inicio y de detalle de posts con Laravel 8

Parte 3: Crear una página de inicio y de detalle de posts con Laravel 8

My Profile
Dic 24, 2020

¡Hola! Una semana más seguimos avanzando en nuestro blog con Laravel 8. El tutorial de hoy va a ser muy movidito, ya que esta vez veremos varias cosas. Crearemos controladores, rutas y plantillas para mostrar los datos de nuestro proyecto..

Sin más dilación os dejo con los pasos que vamos a seguir para seguir avanzando en el blog 💪.

Crear el controlador para los posts

Lo primero que realizaremos será crear un controlador para los posts. Para el que no lo tenga claro, un controlador es una clase que hace de intermediario entre la petición del usuario y la respuesta que le vamos a mostrar. En Laravel 8 se almacenan dentro de la carpeta app/Http/Controllers/. Actualmente solo deberíamos tener el HomeController.php, Controller.php y la carpeta Auth que contiene todos los controladores del sistema de registro y autenticación.

Para crear el controlador que administre nuestros posts, lanzaremos el siguiente comando:

php artisan make:controller PostController --resource

Nota: Como convención el nombre del controlador es el nombre del modelo en singular y con la primera letra en mayúscula junto con la palabra Controller. Si además añadimos el parámetro --resource nos generará los métodos para un sistema CRUD. Esto lo utilizaremos en el siguiente tutorial en el que veremos todo el proceso de administración de nuestros posts.

Una vez creado nuestro controlador, vamos al archivo app/Http/Controllers/PostController.php y añadimos al principio de la clase estas dos nuevas funciones llamadas home() y detail() que se encargarán de mostrar la página principal y el detalle de un post respectivamente. También importaremos el modelo Post para realizar las consultas a la base de datos:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function home()
    {
        return view('posts/home', [
            'posts' => Post::where('is_draft', 0)->orderBy('created_at', 'desc')->get()->take(6)
        ]);
    }

    /**
     * Display the specified resource.
     *
     * @param  string  $slug
     * @return \Illuminate\Http\Response
     */
    public function detail($slug)
    {
        $post = Post::where('slug', $slug)->where('is_draft', false)->first();
        abort_unless($post, 404);
        return view('posts/post', [
            'post' => $post
        ]);
    }
.
.
.

El método home() retornará una vista o view. El primer parámetro que estamos pasando es una plantilla que crearemos posteriormente. Esta plantilla se encontrará dentro del directorio resources/views/posts y el archivo se llamará home.blade.php aunque para referenciarlo solo necesitamos escribir posts/home. Para dejarlo más claro, si por ejemplo tuviéramos una plantilla en resources/views/home/detail/user.blade.php, como parámetro dentro del método view, deberíamos pasar "home/detail/user".

El siguiente parámetro que enviamos es un array con la clave posts. Podremos enviar los parámetros que queramos y luego podremos utilizarlos dentro de la plantilla como veremos más adelante.

En la clave posts, queremos enviar todos los posts que no sean borradores y ordenados de forma descendente por la fecha de creación, además queremos limitar el resultado a los 6 últimos. Para ello utilizamos el método take().

En este caso, utilizamos el modelo Post que creamos en el tutorial anterior y que extiende del ORM de eloquent. Un ORM es un modelo de programación que nos permite mapear las estructuras de una base de datos relacional. De esta forma, nos ahorramos de tener que realizar la consulta a la base de datos directamente y utilizamos las funciones del ORM para simularlo:

Post::where('is_draft', 0)->orderBy('created_at', 'desc')->get()->take(6)

Como veis en este caso, utilizamos el método where para añadir la condición de que el post no sea un borrador (is_draft = 0), luego utilizamos la función orderBy para decirle que los ordene por la columna created_at y como segundo parámetro le decimos que lo ordene de forma descendiente. Para obtener los datos usamos get() y por último usamos el método take() y le pasamos por parámetro el número de posts (en este caso 6) que queremos obtener para limitar la consulta. La traducción de la consulta que acabamos de realizar con Eloquent a SQL sería algo así:

SELECT * FROM posts WHERE is_draft = 0 ORDER BY created_at DESC LIMIT 6

Si queréis explorar más acerca de todo lo que podéis hacer con Eloquent, os dejo el enlace a la doc.

Esta consulta nos devolverá un array con objetos posts que cumplan las condiciones dadas y por el que podremos acceder a los datos de un post como veremos cuando entremos de lleno a trabajar con las plantillas.

El método detail() recibirá un parámetro llamado slug. Este será el título en formato slug que guardamos en el tutorial anterior al crear los posts. Posteriormente buscará en la base de datos si existe un match entre una columna slug con ese dato en concreto y que además no sea un borrador. Además, usamos el método first() para que nos retorne solo un objeto Post en vez de un array con el post. El campo slug al ser de tipo único, solo devolverá un resultado si lo encuentra, por lo que podemos limitar el resultado sin problema.

Una vez realizada la búsqueda del post, lanzamos la función abort_unless. Si post es nulo (no ha encontrado el post), enviará al usuario a la página de error 404, si lo encuentra, retornará la vista y el post relacionado con el parámetro slug.

Plantillas

Al igual que otros frameworks, Laravel utiliza un sistema de plantillas llamado Blade. Los sistemas de plantillas nos ayudan a separar las vistas de otras partes del proyecto como puede ser el controlador y nos permite tener más organizado nuestro código. Al final sería algo así como tener código PHP dentro un HTML para darle dinamismo a la plantilla pero de una forma más organizada.

Si vamos a la carpeta resources/views, veremos que contiene varios archivos y carpetas. Estos son las plantillas de autenticación, que en este caso se han generado configuradas para Tailwind CSS y se guardan en la carpeta auth.

También tenemos otra carpeta llamada layouts que contiene un archivo llamado app.blade.php. Este archivo es muy interesante, ya que se puede utilizar de base para las plantillas y nos permitirá reutilizar código de forma sencilla.

Por último, tenemos un archivo llamado home.blade.php que será una pantalla de bienvenida cuando hacemos login y el archivo welcome.blade.php que es la página de inicio por defecto que se muestra al iniciar el servidor de Laravel.

Una vez explicado el sistema de plantillas, vamos a ponernos manos a la obra, lo primero que vamos a hacer, es modificar el archivo resources/views/layouts/app.blade.php para configurar nuestro archivo base. Lo abrimos y sustituimos su código por el código siguiente:

<!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">
    <title>My Laravel Blog</title>
    <!-- Styles -->
    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog==" crossorigin="anonymous" />

</head>
<body>
    <header class="w-full">
        <nav class="w-full bg-orange-300 p-1 text-white flex justify-center">
            <div class="w-full flex justify-between px-4">
                @guest
                <ul class="flex justify-between" style="width:130px">
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('home') }}">
                            HOME
                        </a>
                    </li>
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('login') }}">
                            <i class="fas fa-sign-in-alt"></i>
                        </a>
                    </li>
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('register') }}">
                            <i class="fas fa-user-plus"></i>
                        </a>
                    </li>
                </ul>
                @else
                <ul class="flex justify-between" style="width:140px">
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('home') }}">
                            HOME
                        </a>
                    </li>
                    <li>{{ Auth::user()->name }}</li>
                    @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
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('logout') }}" title="logout" class="no-underline hover:underline" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"><i class="fas fa-sign-out-alt"></i></a>
                        <form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
                            {{ csrf_field() }}
                        </form>
                    </li>
                </ul>
                @endguest
                <ul class="flex justify-between" style="width:99px">
                    <li>
                        <a class="hover:text-blue-600" href="http://">
                            <i class="fab fa-twitter"></i>
                        </a>
                    </li>
                    <li>
                        <a class="hover:text-blue-600" href="http://">
                            <i class="fab fa-facebook-f"></i>
                        </a>
                    </li>
                    <li>
                        <a class="hover:text-blue-600" href="http://">
                            <i class="fas fa-rss"></i>
                        </a>
                    </li>
                </ul>
            </div>
        </nav>
        <div class="text-center py-8 text-4xl font-bold">
            <h1>My Laravel Blog</h1>
        </div>
    </header>
    @yield('content')
    <footer class="mt-12">
        <div class="max-w-full bg-orange-300 p-4"></div>
        <div class="max-w-full text-center bg-gray-700 text-white p-4">
            <div class="text-lg font-bold">@MyLaravelBlog By <a class="hover:underline" href="https://cosasdedevs.com/" target="_blank">Alberto Ramírez</a></div>
        </div>
    </footer>
</body>
</html>

Esta plantilla se encargará de mostrar la cabecera de la web, la barra de navegación y el footer y la podremos utilizar siempre que queramos para que otras plantillas extiendan de esta. También contiene unos estilos generales para todo el proyecto en el que vamos a comentar las líneas más importantes:

<link href="{{ mix('css/app.css') }}" rel="stylesheet">

Para acceder a funciones y variables dentro de la plantilla, utilizamos {{}} y dentro asignamos el nombre de la variable o función. En este caso, usamos la función mix para que nos devuelva el css ya compilado que le enviamos por la ruta y que ya contiene los estilos de Tailwind CSS.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog==" crossorigin="anonymous" />

Aquí estamos importando la fuente de iconos llamada Font Awesome y que utilizaremos en este proyecto.

                @guest
                <ul class="flex justify-between" style="width:130px">
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('home') }}">
                            HOME
                        </a>
                    </li>
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('login') }}">
                            <i class="fas fa-sign-in-alt"></i>
                        </a>
                    </li>
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('register') }}">
                            <i class="fas fa-user-plus"></i>
                        </a>
                    </li>
                </ul>
                @else
                <ul class="flex justify-between" style="width:140px">
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('home') }}">
                            HOME
                        </a>
                    </li>
                    <li>{{ Auth::user()->name }}</li>
                    <li>
                        <a class="hover:text-blue-600" href="{{ route('logout') }}" title="logout" class="no-underline hover:underline" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"><i class="fas fa-sign-out-alt"></i></a>
                        <form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
                            {{ csrf_field() }}
                        </form>
                    </li>
                </ul>
                @endguest

En Blade, para declarar condiciones y ciclos, primero escribimos la @ seguido del tipo, por ejemplo, para realizar un if, lo escribiríamos así @if (condición) ....

Avanzamos hasta la línea 17 donde encontramos la palabra @guest, esto sería igual a @if(!is_authenticated). Si el usuario no está autenticado, queremos mostrar los enlaces de registro y login, si no, mostraremos el nombre del usuario.

En la línea 20 nos encontramos con {{ route('home') }}. La función route generará una ruta sobre el texto pasado por parámetro.  Este nombre deberá coincidir con la ruta que generemos más adelante.

En la línea 35, tenemos el @else, o sea si un usuario está autenticado.

En la línea 42, mostraremos el  nombre del usuario.

De la línea 43 a 48, tenemos un formulario para realizar el logout de un usuario. Como podéis observar, dentro del formulario se añade el parámetro {{ csrf_field() }}. Esto genera un token para validar nuestra petición. Si no añadimos el token en nuestros formularios con Laravel, el proceso no funcionará así que es muy importante tener siempre esto en cuenta.

En la línea 50, cerramos la condición @endguest.

Por último, en la línea 74, añadimos la función @yield('content'). Esta función la veremos en uso más adelante cuando extendamos la plantilla actual en otras plantillas. La utilizaremos para reemplazar @yield('content') por el contenido que queramos. Como nombre le he dado la palabra content pero podéis poner el nombre que queráis. El nombre lo usaremos para asignarlo a una sección en una plantilla que extienda de app.blade.php.

Lo siguiente que vamos a hacer, es crear el directorio posts dentro de resources/views/ y dentro de posts, crearemos un archivo llamado home.blade.php con el siguiente contenido:

@extends('..layouts.app')

@section('content')
<section class="w-full bg-gray-200 py-4 flex-row justify-center text-center">
    <h2 class="py-4 text-3xl">About me</h2>
    <div class="flex text-justify justify-center">
        <div class="max-w-5xl px-2">
            Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptate necessitatibus ullam commodi perferendis accusamus sint error sequi, dolorem nam, vel praesentium dignissimos nostrum quod fuga corporis asperiores laudantium, possimus veniam!
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur iure cumque qui impedit quod earum dolores nisi nemo totam vero natus aperiam, libero consequuntur nesciunt atque officia exercitationem rerum. Veritatis!
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptas in hic ratione recusandae nostrum, saepe aliquam alias ipsum? Asperiores rerum numquam officia harum atque, impedit perspiciatis facilis nobis tempora est!
        </div>
    </div>
</section>
<section class="w-full">
    <div class="flex justify-center">
        <div class="max-w-6xl text-center">
            <h2 class="py-4 text-3xl border-solid border-gray-300 border-b-2">Lasts posts</h2>
            <div class="flex flex-wrap justify-between">
                @foreach($posts as $post)
                <article style="width:300px" class="text-left p-2">
                    <h3 class="py-4 text-xl">{{$post->title}}</h3>
                    <p>{{$post->get_limit_body}} <a class="font-bold text-blue-600 no-underline hover:underline" href="{{ route('posts.detail', $post->slug) }}">Read more</a></p>
                </article>
                @endforeach
            </div>
        </div>
    </div>
</section>
@endsection

Esta página será el home page del nuestro blog y mostrará una cabecera con un about me y un párrafo, además los últimos 6 posts escritos en nuestro blog.

Como se puede ver en la primera línea, usamos la función @extends('..layouts.app') añadiendo la ruta del archivo del que queremos extender, en este caso el archivo app.blade.php que es el que hemos explicado anteriormente.

Lo siguiente que hacemos es abrir una sección con @section('content'), esta función sustituirá el código que contiene por la función @yield('content') alojada en el archivo app.blade.php. Como aclaración, podríamos tener varios @yield en el archivo app.blade.php y poder sustituirlos por secciones en la plantilla que extiende esta. Solo necesitaríamos darle un nombre distinto para que blade diferencie unas de otras.

Si nos vamos a la línea 19, veremos un ciclo foreach recorriendo los posts que enviamos desde el controlador PostController en el método index().

En la línea 21, estamos mostrando el título. Como veis, para acceder a los atributos del objeto desde Blade, lo hacemos de la siguiente forma: {{$post->title}}.

En la línea 22, estamos usando {{$post->get_limit_body}} que no se corresponde a ningún atributo de nuestro post. Este atributo corresponde a la función que creamos en el modelo Post llamada getGetLimitBodyAttribute() y que retorna los primeros 140 caracteres de un post. 

Nota: Para generar atributos customizados en un modelo y poder utilizarlos después en nuestras plantillas, debemos crearlos con el siguiente formato en el modelo:

get<NombreAtributo>Attribute():

En el que sustituimos NombreAtributo por el nombre que le queramos dar en formato CamelCase. Para acceder a él desde blade, podremos hacerlo llamando al nombre que le dimos al atributo en formato snake_case.

También, en esa misma línea, podéis ver que generamos la ruta al detalle de un post y además pasamos el parámetro slug. Esto lo hacemos para construir la url. Cuando tengamos las rutas preparadas, debería verse algo así:

http://127.0.0.1:8000/posts/id-eligendi-qui-ipsam-voluptates

Por último, cerramos el foreach con @endforeach y la sección con @endsection y ya podríamos dar la home page por finalizada.

Ahora vamos a crear la página para ver los detalles de los posts. Para ello vamos a crear un archivo llamado post.blade.php dentro de la carpeta resources/views/posts/ y añadiremos 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">{{$post->title}}</h1>
        </div>
    </div>
</section>
<article class="w-full py-8">
    <div class="flex justify-center">
        <div class="max-w-4xl text-justify">
            {{$post->body}}
        </div>
    </div>
</article>
<section class="w-full py-8">
    <div class="max-w-4xl flex-row justify-start p-3 text-left ml-auto mr-auto border rounded shadow-sm bg-gray-50">
        <h3 class="py-4 text-2xl">Comments</h3>
        <div>
            @foreach($post->comments as $comment)
            <div class="w-full bg-white p-2 my-2 border">
                <div class="header flex justify-between mb-4 text-sm text-gray-500">
                    <div>
                        By {{$comment->user->name}}
                    </div>
                    <div>
                        {{$comment->created_at->format('j F, Y')}}
                    </div>
                </div>
                <div class="text-lg">{{$comment->comment}}</div>
            </div>
            @endforeach
        </div>
    </div>
</section>
@endsection

Esta será la página del detalle de un post y mostrará información de este como el título y el cuerpo además de los comentarios escritos por los usuarios pero vamos explicar algunas cosas más a fondo:

En la línea 22, vemos como accedemos a los comentarios desde el post, esto lo podemos hacer gracias a que en el modelo de Post, creamos la función comments() que utiliza el método hasMany que se encarga de traer los comentarios relacionados con ese post.

En la línea 26, accedemos al nombre del usuario también gracias a la función user() que creamos en el modelo Comment y que genera la relación con el usuario que ha creado el post.

En la línea 29, mostramos la fecha de creación del post y además utilizamos la función format para darle el formato que queramos a la fecha.

Ahora ya solo nos falta conectar todo. Para ello, vamos al archivo routes/web.php y añadimos las siguientes líneas:

Route::get('/', [\App\Http\Controllers\PostController::class, 'home'])->name('home');
Route::get('/posts/{slug}', [\App\Http\Controllers\PostController::class, 'detail'])->name('posts.detail');

Las demás rutas las podremos eliminar a excepción de la ruta Auth::routes() que son las que se encargan del login, logout y registro.

Debería quedar de esta forma el archivo:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', [\App\Http\Controllers\PostController::class, 'home'])->name('home');
Route::get('/posts/{slug}', [\App\Http\Controllers\PostController::class, 'detail'])->name('posts.detail');
Auth::routes();

La primera ruta que tenemos es la ruta hacia la home page. Le decimos que será una petición de tipo get utilizando el método get()  y como primer parámetro, le pasamos la ruta a la que se corresponde, en este caso al index del blog. Como segundo parámetro, enviamos un array con el controlador en este caso PostController y el nombre del método al que está relacionado, en este caso el método home. Por último le damos un nombre con el método name() que es el nombre que luego utilizamos en las funciones route dentro de las plantillas del proyecto.

La siguiente ruta es la que nos lleva al detalle de un post. También será una petición de tipo get y la ruta será '/post/{slug}'. Todo lo que introduzcamos dentro de corchetes una url será tomado como una variable. En este caso, cuando queramos acceder al detalle de un post, enviaremos la url que guardamos como slug en nuestros posts. Si vais al PostController y al método detail, veréis que recibe un parámetro llamado $slug. Este corresponde al que pasamos en la url y que utilizamos para realizar la consulta en la base de datos y posteriormente mostrarlo en la vista.

Al igual que en el caso anterior, pasamos el controlador y el método al que corresponde y le damos un nombre para utilizarlo en nuestras rutas.

Si lanzáis el servidor con php artisan serve, podréis ver ya la home page en funcionamiento y podréis acceder al detalle de un post pinchando el read more.

También estarán habilitados los paneles de login y registro que ya los generó automáticamente laravel/ui en el primer tutorial de esta serie así que también podréis hacer login con el usuario que creamos o crear uno nuevo.

Y por fin podemos decir que hemos terminado por hoy 😅. Espero que no se haya hecho muy pesado y nos vemos en el próximo donde habilitaremos un formulario para que nuestros usuarios logueados puedan escribir comentarios en el blog (Disponible el 31 de diciembre de 2020).

Como siempre os dejo el enlace al proyecto y os recomiendo seguirme en Twitter.

Nos leemos 👋

241 vistas

Nos tomamos en serio tu privacidad

Utilizamos cookies propias y de terceros para mejorar la experiencia del usuario a través de su navegación. Si pulsas entendido aceptas su uso. Ver política de cookies.

🐍 Sígueme en Twitter

Si te gusta el contenido que subo y no quieres perderte nada, sígueme en Twitter y te avisaré cada vez que cree contenido nuevo 💪
Luego ¡Te sigo!