Inicio
Artículos
Categorias
Etiquetas
Creando el crud de posts - IX
publicado el: 2021-2-3   actualizado el: 2021-2-3   incluido en: Proyecto blog laravel
palabras totales: 7003   tiempo de lectura: 33 mins  

Creando un crud para los post del blog

Creo el controlador con los siete métodos

1
2
enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:controller Admin/PostController -r
Controller created successfully.

Lo modifico para recibir objetos post y las vistas a los métodos correspondientes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php

namespace App\Http\Controllers\Admin;

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

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return view('admin.posts.index');
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('admin.posts.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function show(Post $post)
    {
        return view('admin.posts.show', compact('post'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function edit(Post $post)
    {
        return view('admin.posts.edit', compact('post'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Post $post)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function destroy(Post $post)
    {
        //
    }
}

Creo la ruta dentro del archivo routes/admin.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use App\Http\Controllers\Admin\CategoryController;
use App\Http\Controllers\Admin\HomeController;
use App\Http\Controllers\Admin\PostController;
use App\Http\Controllers\Admin\TagController;
use Illuminate\Support\Facades\Route;

Route::get('', [HomeController::class, 'index'])->name('admin.home');
Route::resource('categories', CategoryController::class)->names('admin.categories');
Route::resource('tags', TagController::class)->names('admin.tags');
Route::resource('posts', PostController::class)->names('admin.posts');

Creo las vistas index.blade.php, create.blade.php, show.blade.php y edit.blade.php en la carpeta resources/views/admin/posts y les pego la plantilla de adminLTE3.

Abro el archivo config/adminlte.php, quiero modificar el menú de navegación de la barra lateral de la izquierda. Para ello dentro de adminlte.php modifico la sección Menu Items para que aparezca un enlace a la vista index de los posts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
   /*
    |--------------------------------------------------------------------------
    | Menu Items
    |--------------------------------------------------------------------------
    |
    | Here we can modify the sidebar/top navigation of the admin panel.
    |
    | For more detailed instructions you can look here:
    | https://github.com/jeroennoten/Laravel-AdminLTE/#8-menu-configuration
    |
    */

    'menu' => [
        [
            'text' => 'search',
            'search' => true,
            'topnav' => true,
        ],
        [
            'text' => 'blog',
            'url'  => 'admin/blog',
            'can'  => 'manage-blog',
        ],
        [
            'text'        => 'Dashboard',
            'route'       => 'admin.home',
            'icon'        => 'fas fa-tachometer-alt fa-fw',
        ],
        ['header' => 'ADMINISTRADOR'],
        [
            'text' => 'Categorías',
            'route'  => 'admin.categories.index',
            'icon' => 'fab fa-fw fa-buffer',
            'active' => ['admin/categories*'],
        ],
        [
            'text' => 'Etiquetas',
            'route'  => 'admin.tags.index',
            'icon' => 'fas fa-fw fa-lock',
            'active' => ['admin/tags*'],
        ],
        ['header' => 'OPCIONES DEL BLOG'],
        [
            'text'       => 'Listado de posts',
            'icon' => 'fas fa-fw fa-clipboard',
            'route'        => 'admin.posts.index',
        ],
        [
            'text'       => 'Crear un post',
            'icon' => 'fas fa-fw fa-file',
            'route'        => 'admin.posts.create',
        ],
    ],

Para mostrar el listado de posts quiero utilizar livewire en la plantilla admiLTE para así poder interactuar con el listado sin refrescar la pantalla y dotar lo de un campo de búsqueda que nos filtre los registros por nombre. Para poder usar livewire junto con la plantilla adminLTE debo molificar a true el campo livewire valor de en el archivo de configuración de adminlte.

adminLTE.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
........
    /*
    |--------------------------------------------------------------------------
    | Livewire
    |--------------------------------------------------------------------------
    |
    | Here we can enable the Livewire support.
    |
    | For more detailed instructions you can look here:
    | https://github.com/jeroennoten/Laravel-AdminLTE/#93-livewire
    */

    'livewire' => true,
........

Ahora ya puedo crear un componente de livewire

1
2
3
4
5
enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:livewire Admin/PostsIndex
 COMPONENT CREATED  🤙

CLASS: app/Http/Livewire/Admin/PostsIndex.php
VIEW:  resources/views/livewire/admin/posts-index.blade.php

Me ha creado una clase app/Http/Livewire/Admin/PostsIndex.php y una vista resources/views/livewire/admin/posts-index.blade.php en las rutas arriba indicadas.

Incluyo el componente @livewire('admin.posts-index') dentro del archivo resources/views/admin/posts/index.blade.php

index.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@extends('adminlte::page')

@section('title', 'Blog con Laravel')

@section('content_header')
    <h1>Listado de posts</h1>
@stop

@section('content')
    @livewire('admin.posts-index')  //Aquí incluyo el componente livewire
@stop

@section('css')
    <link rel="stylesheet" href="/css/admin_custom.css">
@stop

@section('js')
    <script> console.log('Hi!'); </script>
@stop

Escribo un texto de prueba en resources/views/livewire/admin/posts-index.blade.php

posts-index.blade.php

1
2
3
<div>
    Componente de prueba.
</div>

El resultado es:

Abro el archivo de la clase de livewire app/Http/Livewire/Admin/PostsIndex.php y agrego la logica que pasará a la vista resources/views/livewire/admin/posts-index.blade.php. En el almaceno en la variable $posts aquellos post que pertenecen al usuario actualmente logeado ordenados por id de mayor a menor y paginados. Para que la paginación use estilos de bootstrap defino la propiedad $paginationTheme y pido que use WithPagination.

PostIndex.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use App\Models\Post;
use Livewire\Component;
use Livewire\WithPagination;


class PostsIndex extends Component
{
    use WithPagination;  //para usar la paginación con livewire

    protected $paginationTheme = "bootstrap"; //para que livewire use los estilos de bootstrap en vez de tailwind
    
    public function render()
    {
        $posts = Post::where('user_id', auth()->user()->id)->latest('id')->paginate();
        
        return view('livewire.admin.posts-index', compact('posts'));
    }
}

En la vista creo una tabla que lista los posts dentro de una tarjeta y le añado la paginación

posts-indes.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div class=" card">
    <div class="card-body">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Id</th>
                    <th>Nombre</th>
                    <th colspan="2"></th>
                </tr>
            </thead>
            <tbody>
                @foreach ($posts as $post)
                    <tr>
                        <th>{{$post->id}}</th>
                        <th>{{$post->name}}</th>
                        <th width="10px"><a class=" btn btn-primary btn-sm" href="{{route('admin.posts.edit', $post)}}">Editar</a></th>
                        <th width="10px">
                            <form action="{{route('admin.posts.destroy', $post)}}" method="POST">
                                @csrf
                                @method('delete')
                                <button class="btn btn-danger btn-sm" type="submit">Eliminar</button>
                            </form>
                        </th>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
    {{-- Aqui pongo la paginación--}}
    <div class="car-footer">
        {{$posts->links()}}
    </div>
</div>

En la parte superior de la lista de post voy a agregar un buscador en el que al escribir una palabra me seleccione todos los posts que contengan el texto escrito dentro del campo nombre.

Para ello en app/Http/Livewire/Admin/PostsIndex.php defino una propiedad llamada search que sincronizo con un input que pongo en la vista index.blade.php gracias al componente wire:model = "search" de livewire. La propiedad search la uso dentro de una clausula where para filtrar los post que cumplan esa condición.

PostIndex.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use App\Models\Post;
use Livewire\Component;
use Livewire\WithPagination;


class PostsIndex extends Component
{
    use WithPagination;  //para usar la paginación con livwwire

    protected $paginationTheme = "bootstrap"; //para que livewire use los estilos de bootstrap en vez de tailwind
    
    public $search;

    public function render()
    {
        $posts = Post::where('user_id', auth()->user()->id)
                        ->where('name', 'LIKE', '%' . $this->search . '%')
                        ->latest('id')
                        ->paginate();
        
        return view('livewire.admin.posts-index', compact('posts'));
    }
}

posts-indes.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<div class=" card">
    <div class="card-header">
        {{-- Sincronizo el input con la propiedad search de PostIndex.php--}}
        <input wire:model = "search" class=" form-control" placeholder="Escriba el nombre de un post">
    </div>
    @if ($posts->count())
        <div class="card-body">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Nombre</th>
                        <th colspan="2"></th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($posts as $post)
                        <tr>
                            <th>{{$post->id}}</th>
                            <th>{{$post->name}}</th>
                            <th width="10px"><a class=" btn btn-primary btn-sm" href="{{route('admin.posts.edit', $post)}}">Editar</a></th>
                            <th width="10px">
                                <form action="{{route('admin.posts.destroy', $post)}}" method="POST">
                                    @csrf
                                    @method('delete')
                                    <button class="btn btn-danger btn-sm" type="submit">Eliminar</button>
                                </form>
                            </th>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
        {{-- Aqui pongo la paginación--}}
        <div class="car-footer">
            {{$posts->links()}}
        </div>
    @else
        <div class="card-body">
            <strong>No hay ninguna coincidencia.</strong>
        </div>
    @endif
</div>

Para que me elimine la página de la paginación de la barra de direcciones del navegador web, defino el método updatingSearch que se activa cuando la propiedad search cambia de valor. De esta manera la búsqueda siempre se realiza sobre el listado completo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use App\Models\Post;
use Livewire\Component;
use Livewire\WithPagination;


class PostsIndex extends Component
{
    use WithPagination;  //para usar la paginación con livwwire

    protected $paginationTheme = "bootstrap"; //para que livewire use los estilos de bootstrap en vez de tailwind
    
    public $search;

    public function updatingSearch() // Solo se activa cuando la propiedad search cambia de valor
    {
        $this->resetPage();
    }

    public function render()
    {
        $posts = Post::where('user_id', auth()->user()->id)
                        ->where('name', 'LIKE', '%' . $this->search . '%')
                        ->latest('id')
                        ->paginate();
        
        return view('livewire.admin.posts-index', compact('posts'));
    }
}

Creo el formulario para crear un post

En la vista resources/views/admin/posts/index.blade.php agrego un botón que me dirige a la ruta admin.posts.create administrada por el método create del controlador app/Http/controllers/Admin/PostController.php.

index.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@extends('adminlte::page')

@section('title', 'Blog con Laravel')

@section('content_header')
    <a class="btn btn-secondary btn-m float-right" href="{{route('admin.posts.create')}}">Crear nuevo post</a>
    <h1>Listado de posts</h1>
@stop

@section('content')
    @livewire('admin.posts-index')
@stop

@section('css')
    <link rel="stylesheet" href="/css/admin_custom.css">
@stop

@section('js')
    <script> console.log('Hi!'); </script>
@stop

Dentro del método create almaceno en dos variables dos arrays, uno con parejas clave, valor de nombre e id de las categorías y otro de objetos tag que se los paso a la vista donde escribiré el código del formulario para crear un post.

Método create de PostController.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...................
/**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        $categories = Category::pluck('name', 'id'); //Devuelve un array de parejas clave=>valor ('id'=>'nombre')
        $tags = Tag::all();//Devuelve un array de objetos Tag.
        
        return view('admin.posts.create', compact('categories', 'tags'));
    }
...................

Creo el formulario en la vista resources/views/admin/posts/create.blade.php.

create.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@extends('adminlte::page')

@section('title', 'Blog con Laravel')

@section('content_header')
    <h1>Crea un post</h1>
@stop

@section('content')
    <div class="card">
        <div class="card-body">
            {!! Form::open(['route' => 'admin.posts.store']) !!}
                <div class="form-group">
                    {!! Form::label('name', 'Nombre: ') !!}
                    {!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Escribe el nombre del post']) !!}
                </div>
                <div class="form-group">
                    {!! Form::label('slug', 'Slug: ') !!}
                    {!! Form::text('slug', null, ['class' => 'form-control', 'readonly', 'placeholder' => 'Escribe el slug del post']) !!}
                </div>
                <div class="form-group">
                    {!! Form::label('category_id', 'Categoría: ') !!}
                    {!! Form::select('category_id', $categories, null, ['class' => 'form-control']) !!}
                </div>
                <div class="form-group">
                    <p class="font-weight-bold">Etiquetas: </p>
                    @foreach ($tags as $tag)
                        <label class="mr-2">
                            {!! Form::checkbox('tags[]', $tag->id, null) !!} {{--Seleccion multiple en array tags[]--}}
                            {{$tag->name}}
                        </label>
                    @endforeach
                </div>
                <div class="form-group">
                    {!! Form::label('extract', 'Extracto: ') !!}
                    {!! Form::textarea('extract', null, ['class' => 'form-control']) !!}
                </div>
                <div class="form-group">
                    {!! Form::label('body', 'Cuerpo del post: ') !!}
                    {!! Form::textarea('body', null, ['class' => 'form-control']) !!}
                </div>
                {!! Form::submit('Crear post', ['class' => 'btn btn-primary']) !!}
            {!! Form::close() !!}
        </div>
    </div>
@stop

@section('js')
    <script src="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script>
    <script>
        $(document).ready( function() {
            $("#name").stringToSlug({
                setEvents: 'keyup keydown blur',
                getPut: '#slug',
                space: '-'
            });
        });
    </script>
@endsection

Para permitir texto enriquecido en los dos textarea voy a utilizar ckeditor. Me dirijo a su página de descargas y copio el CDN <script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>que inserto en la sección @section('js') de la vista create.blade.php. Localizo el elemento en el que quiero usarlo y pongo el valor de su id dentro del método ClassicEditro.create(). Como indica sus instrucciones de la página.

Classic editor

In your HTML page add an element that CKEditor should replace:

1
<div id="editor"></div>

Load the classic editor build (here CDN location is used):

1
<script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>

Call the ClassicEditor.create() method.

1
2
3
4
5
6
7
<script>
    ClassicEditor
        .create( document.querySelector( '#editor' ) )
        .catch( error => {
            console.error( error );
        } );
</script>

La sección @section('js') de la vista create.blade.php queda así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@section('js')
    <script src="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script>
    <script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>
    <script>
        $(document).ready( function() {
            $("#name").stringToSlug({
                setEvents: 'keyup keydown blur',
                getPut: '#slug',
                space: '-'
            });
        });

        ClassicEditor
        .create( document.querySelector( '#extract' ) )
        .catch( error => {
            console.error( error );
        } );

        ClassicEditor
        .create( document.querySelector( '#body' ) )
        .catch( error => {
            console.error( error );
        } );
    </script>
@endsection

y el formulario se ve así:

Se me olvidó añadir un radio button para el estado

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
                <div class="form-group">
                    <p class="font-weight-bold">Estado</p>
                    <label class="mr-2">
                        {!! Form::radio('status', 1, true) !!}
                        Borrador
                    </label>
                    <label class="mr-2">
                        {!! Form::radio('status', 2) !!}
                        Publicado
                    </label>
                </div>

Voy a manejar dos validaciones con este formulario, una cuando quiera guardar el formulario como borrador y otra cuando quiera que sea como publicado. Como los campos extract y body de la tabla posts quiero que puedan ser vacíos en el caso de borrador, debo modificarlo en el factory correspondiente database/migrations/2020_12_10_152625_create_posts_table.php.

2020_12_10_152625_create_posts_table.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?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('name');
            $table->string('slug');
            $table->text('extract')->nullable();  //Modificado para que sea nulable
            $table->longText('body')->nullable(); //Modificado para que sea nulable
            $table->enum('status', [1,2])->default(1); //1 es borrador y 2 publicado
            $table->unsignedBigInteger('category_id');
            $table->unsignedBigInteger('user_id');

            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

            $table->timestamps();
        });
    }

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

Vuelvo a realizar las migraciones para que tengan efecto los cambios.

1
enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan migrate:fresh --seed

Ahora debo crear las reglas de validación que no voy a incluir dentro del método store del controlador Admin/PostController.php sino

mediante un formRequest

1
2
enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:request StorePostRequest
Request created successfully.

app/Http/Requests/StorePostRequest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

y lo modifico incluyendo las reglas de validación para los status de borrador y de publicado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        $rules = [
            'name' => 'required',
            'slug' => 'required|unique:posts',
            'status' => 'required|in:1,2'
        ];

        if($this->status == 2){
            $rules = array_merge($rules, [
                'category_id' => 'required',
                'tags' => 'required',
                'extract' => 'required',
                'body' => 'required'
            ]);
        }
        return $rules;
    }
}

Cambio la clase Request por StorePostRequest en el método store del controlador para que utilice la clase del formRequest que acabo de crear con las validaciones, los objetos que pasamos por parámetro serán de la clase formRequest.

Método store en Admin/PostController.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    /**
     * Store a newly created resource in storage.
     *
     * @param  App\Http\Requests\StorePostRequest   $request
     * @return \Illuminate\Http\Response
     */
    public function store(StorePostRequest $request)
    {
        //
    }

Lo siguiente es agregar los mensajes de error de validación en la vista resources/views/admin/posts/create.blade.php

create.blade.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
@extends('adminlte::page')

@section('title', 'Blog con Laravel')

@section('content_header')
    <h1>Crea un post</h1>
@stop

@section('content')
    <div class="card">
        <div class="card-body">
            {!! Form::open(['route' => 'admin.posts.store']) !!}
                <div class="form-group">
                    {!! Form::label('name', 'Nombre: ') !!}
                    {!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Escribe el nombre del post']) !!}
                    @error('name')
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
                <div class="form-group">
                    {!! Form::label('slug', 'Slug: ') !!}
                    {!! Form::text('slug', null, ['class' => 'form-control', 'readonly', 'placeholder' => 'Escribe el slug del post']) !!}
                    @error('slug')
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
                <div class="form-group">
                    {!! Form::label('category_id', 'Categoría: ') !!}
                    {!! Form::select('category_id', $categories, null, ['class' => 'form-control']) !!}
                    @error('category_id')
                        <br>
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
                <div class="form-group">
                    <p class="font-weight-bold">Etiquetas: </p>
                    @foreach ($tags as $tag)
                        <label class="mr-2">
                            {!! Form::checkbox('tags[]', $tag->id, null) !!} {{--Seleccion multiple en array tags[]--}}
                            {{$tag->name}}
                        </label>
                    @endforeach
                    @error('tags')
                        <br>
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
                <div class="form-group">
                    <p class="font-weight-bold">Estado</p>
                    <label class="mr-2">
                        {!! Form::radio('status', 1, true) !!}
                        Borrador
                    </label>
                    <label class="mr-2">
                        {!! Form::radio('status', 2) !!}
                        Publicado
                    </label>
                </div>
                <div class="form-group">
                    {!! Form::label('extract', 'Extracto: ') !!}
                    {!! Form::textarea('extract', null, ['class' => 'form-control']) !!}
                    @error('extract')
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
                <div class="form-group">
                    {!! Form::label('body', 'Cuerpo del post: ') !!}
                    {!! Form::textarea('body', null, ['class' => 'form-control']) !!}
                    @error('body')
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
                {!! Form::submit('Crear post', ['class' => 'btn btn-primary']) !!}
            {!! Form::close() !!}
        </div>
    </div>
@stop

@section('js')
    <script src="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script>
    <script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>
    <script>
        $(document).ready( function() {
            $("#name").stringToSlug({
                setEvents: 'keyup keydown blur',
                getPut: '#slug',
                space: '-'
            });
        });

        ClassicEditor
        .create( document.querySelector( '#extract' ) )
        .catch( error => {
            console.error( error );
        } );

        ClassicEditor
        .create( document.querySelector( '#body' ) )
        .catch( error => {
            console.error( error );
        } );
    </script>
@endsection

Compruebo que funciona la validación.

Dentro del formulario debo incluir de forma oculta dentro de un input el id del usuario logeado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
........
@section('content')
    <div class="card">
        <div class="card-body">
            {!! Form::open(['route' => 'admin.posts.store']) !!}
                {!! Form::hidden('user_id', auth()->user()->id) !!} {{-- Campo oculto --}}
                <div class="form-group">
                    {!! Form::label('name', 'Nombre: ') !!}
                    {!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Escribe el nombre del post']) !!}
                    @error('name')
                        <small class="text-danger">{{$message}}</small>
                    @enderror
                </div>
........

Para garantizar que no se modifica este id al enviar el formulario voy a modificar el método authorize de app/Http/Requests/StorePostRequest.php

StorePostRequest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.........
    public function authorize()
    {
        // Compruebo si cualquier user_id que se pasa por el formulario coincide con el del usuario logeado
        // Entonces e permito la verificacion de las reglas y enviar el formulario
        if($this->user_id == auth()->user()->id)
        {
            return true;
        }else{
            return false;
        }
    }
.........

Para poder mandar el formulario utilizando el método store del controlador Admin/PostController.php debo activa la asignación masiva en el modeloPost.php. En este caso utilizaré la propiedad $guarded.

app/Models/Post.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory;

    protected $guarded = ['id', 'created_at', 'updated_at'];

    // Relacion uno a muchos inversa users
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // Relacion uno a muchos inversa categories
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    // Relacion muchos a muchos etiquetas
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }

    // Relacion uno a uno polimórfica con image
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Método store en Admin/PostController.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.......
    /**
     * Store a newly created resource in storage.
     *
     * @param  App\Http\Requests\StorePostRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(StorePostRequest $request)
    {
        $post = Post::create($request->all()); {{-- Guardo el post--}}

        if($request->tags) {{-- Si hay etiquetas marcadas en el formulario--}}
        {
            $post->tags()->attach($request->tags); {{-- Las añado a la tala intermedia--}}
        }

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

Si ahora creo un post y lo publico, al listarlo en la página principal del sitio, o al intentar mostrar su contenido, o filtrar los posts por categorías o etiquetas, laravel me lanzará un mensaje de error porque le estoy pidiendo en la vista correspondiente que muestre una imagen que no he subido. Para corregir este fallo de diseño debo hacer los siguientes cambios de código en las vistas.

resources/views/posts/index.blade.php

1
2
3
4
5
6
7
8
<x-app-layout>

    <div class="micontainer py-8">
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            @foreach ($posts as $post)
                <article class=" w-full h-80 bg-cover bg-center @if($loop->first) md:col-span-2 @endif" style="background-image: url(@if($post->image){{Storage::url($post->image->url)}} @else https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg @endif)">
                    <div class=" w-full h-full px-8 flex flex-col justify-center">
.....................

En este bloque de código le digo con un if a la propiedad backgroud-image que me muestre una imagen por defecto cuando no he subido yo una al crear el post

resources/views/components/card-post.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@props(['post'])

<article class="mb-8 bg-white shadow-lg rounded-lg overflow-hidden">
    @if ($post->image)
        <img class=" w-full h-72 object-cover object-center" src="{{Storage::url($post->image->url)}}" alt="">
    @else
        <img class=" w-full h-72 object-cover object-center" src="https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg " alt="">
    @endif
    <div class="px-6 py-4">
        <h1 class=" font-bold text-xl mb-2">
            <a href="{{route('posts.show', $post)}}">{{$post->name}}</a>
        </h1>
        <div class=" text-gray-700 text-base">
            {!!$post->extract!!} {{-- Sustituido por {{$post->extract}} para que laravel no escape el html usado con ckeditor--}}
        </div>
    </div>
    <div class="px-6 pt-4 pb-2">
        @foreach ($post->tags as $tag)
            <a href="{{route('posts.tag', $tag)}}" class=" inline-block bg-gray-200 rounded-full px-3 py-1 text-sm text-gray-700 mr-2">{{$tag->name}}</a>
        @endforeach
    </div>
</article> 

Filtrado de categorías o etiquetas corregido.

resources/views/posts/show.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<x-app-layout>
    <div class=" micontainer py-8">
        <h1 class=" text-4xl font-bold text-gray-600">{{$post->name}}</h1>
        <div class=" text-lg text-gray-500 mb-2">
            {!!$post->extract!!} {{-- Sustituido por {{$post->extract}} para que laravel no escape el html usado con ckeditor--}}
        </div>
        <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
            {{-- Contenido principal --}}
            <div class=" lg:col-span-2">
                <figure>
                   @if ($post->image)
                        <img class=" w-full h-80 object-cover object-center" src="{{Storage::url($post->image->url)}}" alt="">
                   @else
                        <img class=" w-full h-80 object-cover object-center" src="https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg" alt="">
                   @endif
                </figure>
                <div class=" text-base text-gray-500 mt-4">
                    {!!$post->body!!} {{-- Sustituido por {{$post->body}} para que laravel no escape el html usado con ckeditor--}}
                </div>
            </div>
            {{-- Contenido relacionado--}}
            <aside>
                <h1 class=" text-2xl font-bold text-gray-600 mb-4">Más en: {{$post->category->name}}</h1>
                <ul>
                    @foreach ($similares as $similar)
                        <li class=" mb-4">
                            <a class="flex" href="{{Route('posts.show', $similar)}}">
                                <img class=" w-40 h-20 object-cover object-center " src="@if($similar->image) {{Storage::url($similar->image->url)}} @else https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg @endif" alt="">
                                <span class=" ml-2 text-gray-600">{{$similar->name}}</span>
                            </a>
                        </li>
                    @endforeach
                </ul>
            </aside>
        </div>
    </div>
</x-app-layout>

Subiendo una imagen al servidor y asociandola a un post

Modifico la vista create.blade.php en la que añado un div que me contiene una imagen de muestra y un input de tipo file para poder subir imágenes. Además necesito añadir al formulario la propiedad 'files' => true para permitir subir imágenes y un script js que controle el cambio de imagen en la vista previa antes de mandar el formulario.

resources/views/admin/posts/create.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.................
@section('content')
    <div class="card">
        <div class="card-body">
            {!! Form::open(['route' => 'admin.posts.store', 'autocomplete' => 'off', 'files' => true]) !!}
                {!! Form::hidden('user_id', auth()->user()->id) !!}
.................
<div class="row mb-3">
    <div class="col">
        <div class="image-wrapper">
            <img id="picture" src="https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg" alt="">
        </div>                        
    </div>
    <div class="col">
        <div class="form-group">
            {!! Form::label('file', 'Imagen a mostrar en el post') !!}
            {!! Form::file('file', ['class' => 'form-control-file', 'accept' => 'image/*']) !!}
            <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias totam adipisci, reiciendis rem soluta cupiditate repudiandae voluptate autem fugit asperiores nobis nihil eveniet quam aperiam assumenda eius ipsa facilis tempora?</p>
        </div>
    </div>
</div>
........................


@section('js')
    <script src="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script>
    <script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>
    <script>
     .........

        //Cambiar imagen
        document.getElementById("file").addEventListener('change', cambiarImagen);

        function cambiarImagen(event){
            var file = event.target.files[0];

            var reader = new FileReader();
            reader.onload = (event) => {
                document.getElementById("picture").setAttribute('src', event.target.result); 
            };

            reader.readAsDataURL(file);
        }
    </script>
@endsection

Al enviar este formulario a la ruta admin.posts.store que gestiona el controlador Admin/PostsController.php a través de su método store debo modificar este último para que pueda recoger la imagen si es enviada y almacenarla en la tabla images.

use Illuminate\Support\Facades\Storage; Añado el Facades Storage para que laravel me permita almacenar imágenes en la carpeta public del servidor.

Método store del controlador Admin/PostsController.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
............
use Illuminate\Support\Facades\Storage
............
	/**
     * Store a newly created resource in storage.
     *
     * @param  App\Http\Requests\StorePostRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(StorePostRequest $request)
    {
        $post = Post::create($request->all());
        
        if ($request->file('file')) { // Si se está enviando una imagen
            $url = Storage::put('posts', $request->file('file'));  //guardo la imagen en la carpeta post y copio su url en la variable $url

            $post->image()->create([ // al usar create para almacenar la imagen en la tabla img debo habilitar asignación masiva en Image.php
                'url' => $url
            ]);
        }         

        if($request->tags)
        {
            $post->tags()->attach($request->tags);
        }

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

Habilito la asignación masiva en app/Models/Image.php.

Modelo Image.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Models;

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

class Image extends Model
{
    use HasFactory;

    protected $fillable = ['url']; // Habilito la signación masiva.

    // Relacion uno a uno polimórfica
    public function imageable()
    {
        return $this->morphTo();
    }
}

Valido desde el servidor que solo pueda subir imágenes añadiendo la regla 'file' => 'image' en app/Http/Requests/StorePostRequest.php.

StorePostRequest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.........
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        $rules = [
            'name' => 'required',
            'slug' => 'required|unique:posts',
            'status' => 'required|in:1,2',
            'file' => 'image'
        ];

        if($this->status == 2){
            $rules = array_merge($rules, [
                'category_id' => 'required',
                'tags' => 'required',
                'extract' => 'required',
                'body' => 'required'
            ]);
        }
        return $rules;
    }
.........

Y 'accept' => 'image/*' en el input tipo file de la vista.

1
2
3
4
5
6
7
8
9
enrique@enrique-server:/var/www/laravel/blog_laravel$ sudo chmod 777 storage/app/public/posts/
[sudo] password for enrique: 
enrique@enrique-server:/var/www/laravel/blog_laravel$ ls -la storage/app/public/
total 32
drwxrwsr-x 3 enrique www-data  4096 Jan 24 15:48 .
drwxrwsr-x 3 enrique www-data  4096 Dec  8 15:51 ..
-rwxrwxr-x 1 enrique www-data    14 Dec  8 15:51 .gitignore
drwxrwsrwx 2 enrique www-data 20480 Jan 24 15:58 posts
enrique@enrique-server:/var/www/laravel/blog_laravel$ 

Actualizando un post

Para editar un post voy a reutilizar los input del formulario de la vista views/admin/posts/create.blade.php por lo que lo refactorizo para poder utilizarlos en ambas vistas. Creo el partial views/admin/posts/partials/form.blade.php y pego en el los campos comunes a los dos formularios.

views/admin/posts/partials/form.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<div class="form-group">
    {!! Form::label('name', 'Nombre: ') !!}
    {!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Escribe el nombre del post']) !!}
    @error('name')
        <small class="text-danger">{{$message}}</small>
    @enderror
</div>
<div class="form-group">
    {!! Form::label('slug', 'Slug: ') !!}
    {!! Form::text('slug', null, ['class' => 'form-control', 'readonly', 'placeholder' => 'Escribe el slug del post']) !!}
    @error('slug')
        <small class="text-danger">{{$message}}</small>
    @enderror
</div>
<div class="form-group">
    {!! Form::label('category_id', 'Categoría: ') !!}
    {!! Form::select('category_id', $categories, null, ['class' => 'form-control']) !!}
    @error('category_id')
        <br>
        <small class="text-danger">{{$message}}</small>
    @enderror
</div>
<div class="form-group">
    <p class="font-weight-bold">Etiquetas: </p>
    @foreach ($tags as $tag)
        <label class="mr-2">
            {!! Form::checkbox('tags[]', $tag->id, null) !!} {{--Seleccion multiple en array tags[]--}}
            {{$tag->name}}
        </label>
    @endforeach
    @error('tags')
        <br>
        <small class="text-danger">{{$message}}</small>
    @enderror
</div>
<div class="form-group">
    <p class="font-weight-bold">Estado</p>
    <label class="mr-2">
        {!! Form::radio('status', 1, true) !!}
        Borrador
    </label>
    <label class="mr-2">
        {!! Form::radio('status', 2) !!}
        Publicado
    </label>
</div>
<div class="row mb-3">
    <div class="col">
        <div class="image-wrapper">
            @isset($post->image)
                <img id="picture" src="{{Storage::url($post->image->url)}}" alt="">
            @else
                <img id="picture" src="https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg" alt="">
            @endisset            
        </div>                        
    </div>
    <div class="col">
        <div class="form-group">
            {!! Form::label('file', 'Imagen a mostrar en el post') !!}
            {!! Form::file('file', ['class' => 'form-control-file', 'accept' => 'image/*']) !!}
            <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias totam adipisci, reiciendis rem soluta cupiditate repudiandae voluptate autem fugit asperiores nobis nihil eveniet quam aperiam assumenda eius ipsa facilis tempora?</p>
            @error('file')
                <small class="text-danger">{{$message}}</small>
            @enderror
        </div>
    </div>
</div>
<div class="form-group">
    {!! Form::label('extract', 'Extracto: ') !!}
    {!! Form::textarea('extract', null, ['class' => 'form-control']) !!}
    @error('extract')
        <small class="text-danger">{{$message}}</small>
    @enderror
</div>
<div class="form-group">
    {!! Form::label('body', 'Cuerpo del post: ') !!}
    {!! Form::textarea('body', null, ['class' => 'form-control']) !!}
    @error('body')
        <small class="text-danger">{{$message}}</small>
    @enderror
</div>

Actualizo la vista create:

views/admin/posts/create.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@extends('adminlte::page')

@section('title', 'Blog con Laravel')

@section('content_header')
    <h1>Crea un post</h1>
@stop

@section('content')
    <div class="card">
        <div class="card-body">
            {!! Form::open(['route' => 'admin.posts.store', 'autocomplete' => 'off', 'files' => true]) !!}
                {!! Form::hidden('user_id', auth()->user()->id) !!}
                @include('admin.posts.partials.form')
                {!! Form::submit('Crear post', ['class' => 'btn btn-primary']) !!}
            {!! Form::close() !!}
        </div>
    </div>
@stop

@section('css')
    <style>
        .image-wrapper{
            position: relative;
            padding-bottom: 56.25%
        }
        .image-wrapper img{
            position: absolute;
            object-fit: cover;
            width: 100%;
            height: 100%;
        }
    </style>   
@endsection

@section('js')
    <script src="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script>
    <script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>
    <script>
        $(document).ready( function() {
            $("#name").stringToSlug({
                setEvents: 'keyup keydown blur',
                getPut: '#slug',
                space: '-'
            });
        });

        ClassicEditor
        .create( document.querySelector( '#extract' ) )
        .catch( error => {
            console.error( error );
        } );

        ClassicEditor
        .create( document.querySelector( '#body' ) )
        .catch( error => {
            console.error( error );
        } );

        //Cambiar imagen
        document.getElementById("file").addEventListener('change', cambiarImagen);

        function cambiarImagen(event){
            var file = event.target.files[0];

            var reader = new FileReader();
            reader.onload = (event) => {
                document.getElementById("picture").setAttribute('src', event.target.result); 
            };

            reader.readAsDataURL(file);
        }
    </script>
@endsection

Y la vista edit:

views/admin/posts/edit.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@extends('adminlte::page')

@section('title', 'Blog con Laravel')

@section('content_header')
    <h1>Editar un post</h1>
@stop

@section('content')
    @if (session('info'))
        <div class=" alert alert-success">
            <strong>{{session('info')}}</strong>
        </div>
    @endif
    <div class="card">
        <div class="card-body">
            {!! Form::model($post,['route' => ['admin.posts.update', $post], 'autocomplete' => 'off', 'files' => true, 'method' => 'put']) !!}
               {{-- {!! Form::hidden('user_id', auth()->user()->id) !!} Lo validare--}}
                @include('admin.posts.partials.form')
                {!! Form::submit('Actualizar post', ['class' => 'btn btn-primary']) !!}
            {!! Form::close() !!}
        </div>
    </div>
@stop

@section('css')
    <style>
        .image-wrapper{
            position: relative;
            padding-bottom: 56.25%
        }
        .image-wrapper img{
            position: absolute;
            object-fit: cover;
            width: 100%;
            height: 100%;
        }
    </style>   
@endsection

@section('js')
    <script src="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script>
    <script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>
    <script>
        $(document).ready( function() {
            $("#name").stringToSlug({
                setEvents: 'keyup keydown blur',
                getPut: '#slug',
                space: '-'
            });
        });

        ClassicEditor
        .create( document.querySelector( '#extract' ) )
        .catch( error => {
            console.error( error );
        } );

        ClassicEditor
        .create( document.querySelector( '#body' ) )
        .catch( error => {
            console.error( error );
        } );

        //Cambiar imagen
        document.getElementById("file").addEventListener('change', cambiarImagen);

        function cambiarImagen(event){
            var file = event.target.files[0];

            var reader = new FileReader();
            reader.onload = (event) => {
                document.getElementById("picture").setAttribute('src', event.target.result); 
            };

            reader.readAsDataURL(file);
        }
    </script>
@endsection

Actualizo los métodos edit y update del controlador PostController.php

PostController.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
.........
    /**
     * Show the form for editing the specified resource.
     *
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function edit(Post $post)
    {
        $categories = Category::pluck('name', 'id'); //Devuelve un array de parejas clave=>valor ('id'=>'nombre')
        $tags = Tag::all();//Devuelve un array de objetos Tag.

        return view('admin.posts.edit', compact('post', 'categories', 'tags'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\PostRequest  $request
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function update(PostRequest $request, Post $post)
    {
        $post->update($request->all()); // actualizo los datos del post

        if($request->file('file')){ //Si se envia una imagen en el formulario
            $url = Storage::put('posts', $request->file('file'));  //la almaceno en la carpeta posts
            if($post->image){ //si el registro del post tiene una imagen previa asociada
                Storage::delete([$post->image->url]); //borro la imagen de la carpeta posts
                $post->image->update([  // Y cambio la url por la nueva
                    'url' => $url
                ]);
            }else{ // Si no tiene imagen previa le asigno la url de la nueva
                $post->image()->create([
                    'url' => $url
                ]);
            }
        }

        if($request->tags){            
            $post->tags()->sync($request->tags); // El método sync sincroniza las etiquetas, eliminando las desseleccionadas y agregando las nuevas.
        }

        return redirect()->route('admin.posts.edit', $post)->with('info', 'El post se actualizó con éxito.');
    }
.........

Tanto en el método update como en el store he utilizado las mismas reglas de validación por lo que modifico el formRequest creado para validar los campos del método store y cambio también su nombre a PostRequest.php

app/Http/Request/PostRequest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
       return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        $post = $this->route()->parameter('post');

        $rules = [
            'name' => 'required',
            'slug' => 'required|unique:posts',
            'status' => 'required|in:1,2',
            'file' => 'image|max:3000'
        ];

        if($post){
            $rules['slug'] = 'required|unique:posts,slug, '. $post->id;
        }

        if($this->status == 2){
            $rules = array_merge($rules, [
                'category_id' => 'required',
                'tags' => 'required',
                'extract' => 'required',
                'body' => 'required'
            ]);
        }
        return $rules;
    }
}

Eliminando un post

Al hacer clic en el botón eliminar del listado de post de la vista resources/views/livewire/admin/posts-index.blade.php el formulario en el que está encerrado me dirige a la ruta admin.posts.destroy gestionada por el método destroy del controlador Admin/PostCopntroller.php. En el escribo la logica para eliminar el post y una redireccion a la ruta admin.posts.index a la que le paso un mensaje de éxito.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    /**
     * Remove the specified resource from storage.
     *
     * @param  App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function destroy(Post $post)
    {
        $post->delete();

        return redirect()->route('admin.posts.index')->with('info-del', 'El post se eliminó con éxito');
    }

Recojo las variables de sesión dentro de resources/views/livewire/admin/posts-index.blade.php

resources/views/livewire/admin/posts-index.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<div class=" card">
    <div class="card-header">
        {{-- Sincronizo el input con la propiedad search de PostIndex.php--}}
        <input wire:model = "search" class=" form-control" placeholder="Escriba el nombre de un post">
    </div>
    @if ($posts->count())
        @if (session('info'))
            <div class=" alert alert-success">
                <strong>{{session('info')}}</strong>
            </div>
        @elseif(session('info-del'))
            <div class=" alert alert-danger">
                <strong>{{session('info-del')}}</strong>
            </div>
        @endif
        <div class="card-body">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Nombre</th>
                        <th colspan="2"></th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($posts as $post)
                        <tr>
                            <th>{{$post->id}}</th>
                            <th>{{$post->name}}</th>
                            <th width="10px"><a class=" btn btn-primary btn-sm" href="{{route('admin.posts.edit', $post)}}">Editar</a></th>
                            <th width="10px">
                                <form action="{{route('admin.posts.destroy', $post)}}" method="POST">
                                    @csrf
                                    @method('delete')
                                    <button class="btn btn-danger btn-sm" type="submit">Eliminar</button>
                                </form>
                            </th>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
        {{-- Aqui pongo la paginación--}}
        <div class="car-footer">
            {{$posts->links()}}
        </div>
    @else
        <div class="card-body">
            <strong>No hay ninguna coincidencia.</strong>
        </div>
    @endif
</div>

No he incluido el código para eliminar la imagen asociada al post dentro del método destroy como si hice en el método update. Para eliminar las imágenes asociadas a los post voy a utilizar una clase observer (son clases que tienen métodos que reflejan los eventos de un modelo en especifico: created: evento que se ejecuta cuando se crea un registro en el modelo.)

1
2
enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:observer PostObserver --model=Post
Observer created successfully.

La clase creada queda así:

app/Observers/PostObserver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php

namespace App\Observers;

use App\Models\Post;

class PostObserver
{
    /**
     * Handle the Post "created" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function created(Post $post)
    {
        //
    }

    /**
     * Handle the Post "updated" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function updated(Post $post)
    {
        //
    }

    /**
     * Handle the Post "deleted" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function deleted(Post $post)
    {
        //
    }

    /**
     * Handle the Post "restored" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function restored(Post $post)
    {
        //
    }

    /**
     * Handle the Post "force deleted" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function forceDeleted(Post $post)
    {
        //
    }
}

Cada una de esas funciones se activan cada vez que realicemos una acción con respecto al modelo Post. cada vez que cree un nuevo post se activara el evento public function created(Post $post).

De momento solo voy a usar los métodos created y updated por lo que elimino los demás.

app/Observers/PostObserver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace App\Observers;

use App\Models\Post;

class PostObserver
{
    /**
     * Handle the Post "created" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function created(Post $post)
    {
        //
    }

    /**
     * Handle the Post "deleted" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function deleted(Post $post)
    {
        //
    }

  
}

Ambos métodos se activan después de borrar o crear un post. Pero necesito que el evento se ejecute antes de eliminar o crear el post. Por lo que voy a

usar el evento deleting en vez del deleted.

Método deleting en app/Observers/PostObserver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
........
	/**
     * Handle the Post "deleted" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function deleting(Post $post) //cada vez que se elimina un post si este tiene imagen la borramos tambien.
    {
        if($post->image){
            Storage::delete($post->image->url);
        }
    }
........

Registro PostObserver en app/Providers/EventServiceProvider.php. Importo tanto el modelo como el observador con: use App\Models\Post; y use App\Observers\PostObserver; y lo registro en el método boot del mismo.

app/Providers/EventServiceProvider.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace App\Providers;

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

use App\Models\Post;
use App\Observers\PostObserver;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        Post::observe(PostObserver::class);
    }
}

Aprovecho este observer para pasar desde el backend el id del usuario logeado como creador del post en vez de enviarlo desde el frontend (vista views/admin/posts/create.blade.php)

resources/views/admin/posts/create.blade.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
........
@section('content')
    <div class="card">
        <div class="card-body">
            {!! Form::open(['route' => 'admin.posts.store', 'autocomplete' => 'off', 'files' => true]) !!}
               {{-- {!! Form::hidden('user_id', auth()->user()->id) !!}--}}
                @include('admin.posts.partials.form')
                {!! Form::submit('Crear post', ['class' => 'btn btn-primary']) !!}
            {!! Form::close() !!}
        </div>
    </div>
@stop
.....

app/Observers/PostObserver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
..........
class PostObserver
{
    /**
     * Handle the Post "created" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function creating(Post $post)
    {
        $post->user_id = auth()->user()->id;
    }
.........

Para evitar errores durante las migraciones y seeders modifico el método creating.

Método creating en app/Observers/PostObserver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Facades\Storage;

class PostObserver
{
    /**
     * Handle the Post "created" event.
     *
     * @param  \App\Models\Post  $post
     * @return void
     */
    public function creating(Post $post)
    {
        if(! \App::runningInConsole()){ // Para que no se ejecute cuando se esten ejecutando los seeders
            $post->user_id = auth()->user()->id; //cada vez que se cree un post se asigna l campo user_id el valor del usuario autentiado.
        }
    }
..........
ESC
«No se puede enseñar nada a un hombre; sólo se le puede ayudar a encontrar la respuesta dentro de sí mismo». Galileo Galilei (1564 - 1642)
Tabla de contenidos
Artículos relacionados
Creando el crud de etiquetas - VIII
Creando un crud para las etiquetas del blog Sigo los mismos pasos que para las categorias. Desde la consola de comandos y situado dentro del proyecto creo el controlador con sus 7 métodos para administrar las etiquetas desde el backend. 1 2 enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:controler Admin/TagController -r Controller created successfully.
2021-1-31
Creando el crud de categorías - VII
Creando un crud para las categorías del blog Genero en admin.php una ruta de tipo resources que me genere las rutas para el crud de las categorías. Antes creo un controlador para categorías que me genere ya los 7 métodos. 1 2 enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:controller Admin/CategoryController -r Controller created successfully.
2021-1-30
Creando el backend del blog -VI
Integrando AdminLTE en el proyecto Para la gestión de la parte administrativa voy a usar el paquete AdminLTE 3 Voy a generar una url llamada admin que utilizaré para desarrollar la parte de administración del blog. Esta url no la voy a declarar en el fichero web.php sino que creo uno nuevo llamado routes/admin.
2021-1-29
Mostrando y filtrando posts - V
Mostrando el detalle de un post Creo una ruta en web.php para mostrar el detalle de un post. web.php 1 Route::get('posts/{post}', [PostController::class, 'show'])->name('posts.show'); Creo en método show en el controllador PostController.php al que le paso como parámetro un objeto post. Dentro de el recupero los post que pertenecen a la misma categoría que el en la variable $similares y se los paso junto con la variable $post a la vista posts.
2021-1-28
Crear un listado de post en la página principal - IV
Listado de posts Voy a mostrar un listado con los pos almacenados en la base de datos, para ello debo empezar creando un controlador en el que recogeré una variable con los post y desde donde llamar a una vista para mostrarlos. Creo un controlador llamado PostController. 1 enrique@enrique-server:/var/www/laravel/blog_laravel$ php artisan make:controller PostController entro en la clase app/Http/Controllers/PostController.
2021-1-28