<?phpnamespaceApp\Http\Controllers\Admin;useApp\Http\Controllers\Controller;useApp\Models\Post;useIlluminate\Http\Request;classPostControllerextendsController{/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/publicfunctionindex(){returnview('admin.posts.index');}/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/publicfunctioncreate(){returnview('admin.posts.create');}/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/publicfunctionstore(Request$request){//
}/**
* Display the specified resource.
*
* @param App\Models\Post $post
* @return \Illuminate\Http\Response
*/publicfunctionshow(Post$post){returnview('admin.posts.show',compact('post'));}/**
* Show the form for editing the specified resource.
*
* @param App\Models\Post $post
* @return \Illuminate\Http\Response
*/publicfunctionedit(Post$post){returnview('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
*/publicfunctionupdate(Request$request,Post$post){//
}/**
* Remove the specified resource from storage.
*
* @param App\Models\Post $post
* @return \Illuminate\Http\Response
*/publicfunctiondestroy(Post$post){//
}}
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.
/*
|--------------------------------------------------------------------------
| 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,........
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>Listadodeposts</h1>@stop@section('content')@livewire('admin.posts-index')//Aquí incluyo el componente livewire
@stop@section('css')<linkrel="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
useApp\Models\Post;useLivewire\Component;useLivewire\WithPagination;classPostsIndexextendsComponent{useWithPagination;//para usar la paginación con livewire
protected$paginationTheme="bootstrap";//para que livewire use los estilos de bootstrap en vez de tailwind
publicfunctionrender(){$posts=Post::where('user_id',auth()->user()->id)->latest('id')->paginate();returnview('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
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.
useApp\Models\Post;useLivewire\Component;useLivewire\WithPagination;classPostsIndexextendsComponent{useWithPagination;//para usar la paginación con livwwire
protected$paginationTheme="bootstrap";//para que livewire use los estilos de bootstrap en vez de tailwind
public$search;publicfunctionrender(){$posts=Post::where('user_id',auth()->user()->id)->where('name','LIKE','%'.$this->search.'%')->latest('id')->paginate();returnview('livewire.admin.posts-index',compact('posts'));}}
<divclass=" card"><divclass="card-header"> {{-- Sincronizo el input con la propiedad search de PostIndex.php--}}
<inputwire:model ="search"class=" form-control"placeholder="Escriba el nombre de un post"></div> @if ($posts->count())
<divclass="card-body"><tableclass="table table-striped"><thead><tr><th>Id</th><th>Nombre</th><thcolspan="2"></th></tr></thead><tbody> @foreach ($posts as $post)
<tr><th>{{$post->id}}</th><th>{{$post->name}}</th><thwidth="10px"><aclass=" btn btn-primary btn-sm"href="{{route('admin.posts.edit', $post)}}">Editar</a></th><thwidth="10px"><formaction="{{route('admin.posts.destroy', $post)}}"method="POST"> @csrf
@method('delete')
<buttonclass="btn btn-danger btn-sm"type="submit">Eliminar</button></form></th></tr> @endforeach
</tbody></table></div> {{-- Aqui pongo la paginación--}}
<divclass="car-footer"> {{$posts->links()}}
</div> @else
<divclass="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.
useApp\Models\Post;useLivewire\Component;useLivewire\WithPagination;classPostsIndexextendsComponent{useWithPagination;//para usar la paginación con livwwire
protected$paginationTheme="bootstrap";//para que livewire use los estilos de bootstrap en vez de tailwind
public$search;publicfunctionupdatingSearch()// Solo se activa cuando la propiedad search cambia de valor
{$this->resetPage();}publicfunctionrender(){$posts=Post::where('user_id',auth()->user()->id)->where('name','LIKE','%'.$this->search.'%')->latest('id')->paginate();returnview('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.
@extends('adminlte::page')@section('title','Blog con Laravel')@section('content_header')<aclass="btn btn-secondary btn-m float-right"href="{{route('admin.posts.create')}}">Crearnuevopost</a><h1>Listadodeposts</h1>@stop@section('content')@livewire('admin.posts-index')@stop@section('css')<linkrel="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
*/publicfunctioncreate(){$categories=Category::pluck('name','id');//Devuelve un array de parejas clave=>valor ('id'=>'nombre')
$tags=Tag::all();//Devuelve un array de objetos Tag.
returnview('admin.posts.create',compact('categories','tags'));}...................
Creo el formulario en la vista resources/views/admin/posts/create.blade.php.
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
<divid="editor"></div>
Load the classic editor build (here CDN location is used):
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.
<?phpuseIlluminate\Database\Migrations\Migration;useIlluminate\Database\Schema\Blueprint;useIlluminate\Support\Facades\Schema;classCreatePostsTableextendsMigration{/**
* Run the migrations.
*
* @return void
*/publicfunctionup(){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
*/publicfunctiondown(){Schema::dropIfExists('posts');}}
Vuelvo a realizar las migraciones para que tengan efecto los cambios.
<?phpnamespaceApp\Http\Requests;useIlluminate\Foundation\Http\FormRequest;classStorePostRequestextendsFormRequest{/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/publicfunctionauthorize(){returnfalse;}/**
* Get the validation rules that apply to the request.
*
* @return array
*/publicfunctionrules(){return[//
];}}
y lo modifico incluyendo las reglas de validación para los status de borrador y de publicado.
<?phpnamespaceApp\Http\Requests;useIlluminate\Foundation\Http\FormRequest;classStorePostRequestextendsFormRequest{/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/publicfunctionauthorize(){returntrue;}/**
* Get the validation rules that apply to the request.
*
* @return array
*/publicfunctionrules(){$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
*/publicfunctionstore(StorePostRequest$request){//
}
Lo siguiente es agregar los mensajes de error de validación en la vista resources/views/admin/posts/create.blade.php
@extends('adminlte::page')@section('title','Blog con Laravel')@section('content_header')<h1>Creaunpost</h1>@stop@section('content')<divclass="card"><divclass="card-body">{!!Form::open(['route'=>'admin.posts.store'])!!}<divclass="form-group">{!!Form::label('name','Nombre: ')!!}{!!Form::text('name',null,['class'=>'form-control','placeholder'=>'Escribe el nombre del post'])!!}@error('name')<smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group">{!!Form::label('slug','Slug: ')!!}{!!Form::text('slug',null,['class'=>'form-control','readonly','placeholder'=>'Escribe el slug del post'])!!}@error('slug')<smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group">{!!Form::label('category_id','Categoría: ')!!}{!!Form::select('category_id',$categories,null,['class'=>'form-control'])!!}@error('category_id')<br><smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group"><pclass="font-weight-bold">Etiquetas:</p>@foreach($tagsas$tag)<labelclass="mr-2">{!!Form::checkbox('tags[]',$tag->id,null)!!}{{--Seleccionmultipleenarraytags[]--}}{{$tag->name}}</label>@endforeach@error('tags')<br><smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group"><pclass="font-weight-bold">Estado</p><labelclass="mr-2">{!!Form::radio('status',1,true)!!}Borrador</label><labelclass="mr-2">{!!Form::radio('status',2)!!}Publicado</label></div><divclass="form-group">{!!Form::label('extract','Extracto: ')!!}{!!Form::textarea('extract',null,['class'=>'form-control'])!!}@error('extract')<smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group">{!!Form::label('body','Cuerpo del post: ')!!}{!!Form::textarea('body',null,['class'=>'form-control'])!!}@error('body')<smallclass="text-danger">{{$message}}</small>@enderror</div>{!!Form::submit('Crear post',['class'=>'btn btn-primary'])!!}{!!Form::close()!!}</div></div>@stop@section('js')<scriptsrc="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script><scriptsrc="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')
<divclass="card"><divclass="card-body"> {!! Form::open(['route' => 'admin.posts.store']) !!}
{!! Form::hidden('user_id', auth()->user()->id) !!} {{-- Campo oculto --}}
<divclass="form-group"> {!! Form::label('name', 'Nombre: ') !!}
{!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Escribe el nombre del post']) !!}
@error('name')
<smallclass="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
.........publicfunctionauthorize(){// 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){returntrue;}else{returnfalse;}}.........
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.
<?phpnamespaceApp\Models;useIlluminate\Database\Eloquent\Factories\HasFactory;useIlluminate\Database\Eloquent\Model;classPostextendsModel{useHasFactory;protected$guarded=['id','created_at','updated_at'];// Relacion uno a muchos inversa users
publicfunctionuser(){return$this->belongsTo(User::class);}// Relacion uno a muchos inversa categories
publicfunctioncategory(){return$this->belongsTo(Category::class);}// Relacion muchos a muchos etiquetas
publicfunctiontags(){return$this->belongsToMany(Tag::class);}// Relacion uno a uno polimórfica con image
publicfunctionimage(){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
*/publicfunctionstore(StorePostRequest$request){$post=Post::create($request->all());{{--Guardoelpost--}}if($request->tags){{--Sihayetiquetasmarcadasenelformulario--}}{$post->tags()->attach($request->tags);{{--Lasañadoalatalaintermedia--}}}returnredirect()->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.
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
<x-app-layout><divclass=" micontainer py-8"><h1class=" text-4xl font-bold text-gray-600">{{$post->name}}</h1><divclass=" text-lg text-gray-500 mb-2"> {!!$post->extract!!} {{-- Sustituido por {{$post->extract}} para que laravel no escape el html usado con ckeditor--}}
</div><divclass="grid grid-cols-1 lg:grid-cols-3 gap-6"> {{-- Contenido principal --}}
<divclass=" lg:col-span-2"><figure> @if ($post->image)
<imgclass=" w-full h-80 object-cover object-center"src="{{Storage::url($post->image->url)}}"alt=""> @else
<imgclass=" 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><divclass=" 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><h1class=" text-2xl font-bold text-gray-600 mb-4">Más en: {{$post->category->name}}</h1><ul> @foreach ($similares as $similar)
<liclass=" mb-4"><aclass="flex"href="{{Route('posts.show', $similar)}}"><imgclass=" 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=""><spanclass=" 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.
.................
@section('content')
<divclass="card"><divclass="card-body"> {!! Form::open(['route' => 'admin.posts.store', 'autocomplete' => 'off', 'files' => true]) !!}
{!! Form::hidden('user_id', auth()->user()->id) !!}
.................
<divclass="row mb-3"><divclass="col"><divclass="image-wrapper"><imgid="picture"src="https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg"alt=""></div></div><divclass="col"><divclass="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')
<scriptsrc="{{asset('vendor/jQuery-Plugin-stringToSlug-1.3/jquery.stringToSlug.min.js')}}"></script><scriptsrc="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script><script>.........//Cambiar imagen
document.getElementById("file").addEventListener('change',cambiarImagen);functioncambiarImagen(event){varfile=event.target.files[0];varreader=newFileReader();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
............
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
<?phpnamespaceApp\Models;useIlluminate\Database\Eloquent\Factories\HasFactory;useIlluminate\Database\Eloquent\Model;classImageextendsModel{useHasFactory;protected$fillable=['url'];// Habilito la signación masiva.
// Relacion uno a uno polimórfica
publicfunctionimageable(){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.
........./**
* Get the validation rules that apply to the request.
*
* @return array
*/publicfunctionrules(){$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 32drwxrwsr-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.
<divclass="form-group">{!!Form::label('name','Nombre: ')!!}{!!Form::text('name',null,['class'=>'form-control','placeholder'=>'Escribe el nombre del post'])!!}@error('name')<smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group">{!!Form::label('slug','Slug: ')!!}{!!Form::text('slug',null,['class'=>'form-control','readonly','placeholder'=>'Escribe el slug del post'])!!}@error('slug')<smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group">{!!Form::label('category_id','Categoría: ')!!}{!!Form::select('category_id',$categories,null,['class'=>'form-control'])!!}@error('category_id')<br><smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group"><pclass="font-weight-bold">Etiquetas:</p>@foreach($tagsas$tag)<labelclass="mr-2">{!!Form::checkbox('tags[]',$tag->id,null)!!}{{--Seleccionmultipleenarraytags[]--}}{{$tag->name}}</label>@endforeach@error('tags')<br><smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group"><pclass="font-weight-bold">Estado</p><labelclass="mr-2">{!!Form::radio('status',1,true)!!}Borrador</label><labelclass="mr-2">{!!Form::radio('status',2)!!}Publicado</label></div><divclass="row mb-3"><divclass="col"><divclass="image-wrapper">@isset($post->image)<imgid="picture"src="{{Storage::url($post->image->url)}}"alt="">@else<imgid="picture"src="https://cdn.pixabay.com/photo/2016/10/22/17/46/mountains-1761292_960_720.jpg"alt="">@endisset</div></div><divclass="col"><divclass="form-group">{!!Form::label('file','Imagen a mostrar en el post')!!}{!!Form::file('file',['class'=>'form-control-file','accept'=>'image/*'])!!}<p>Loremipsumdolorsitametconsecteturadipisicingelit.Molestiastotamadipisci,reiciendisremsolutacupiditaterepudiandaevoluptateautemfugitasperioresnobisnihilevenietquamaperiamassumendaeiusipsafacilistempora?</p>@error('file')<smallclass="text-danger">{{$message}}</small>@enderror</div></div></div><divclass="form-group">{!!Form::label('extract','Extracto: ')!!}{!!Form::textarea('extract',null,['class'=>'form-control'])!!}@error('extract')<smallclass="text-danger">{{$message}}</small>@enderror</div><divclass="form-group">{!!Form::label('body','Cuerpo del post: ')!!}{!!Form::textarea('body',null,['class'=>'form-control'])!!}@error('body')<smallclass="text-danger">{{$message}}</small>@enderror</div>
.........
/**
* 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
<?phpnamespaceApp\Http\Requests;useIlluminate\Foundation\Http\FormRequest;classPostRequestextendsFormRequest{/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/publicfunctionauthorize(){returntrue;}/**
* Get the validation rules that apply to the request.
*
* @return array
*/publicfunctionrules(){$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
*/publicfunctiondestroy(Post$post){$post->delete();returnredirect()->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
<divclass=" card"><divclass="card-header"> {{-- Sincronizo el input con la propiedad search de PostIndex.php--}}
<inputwire:model ="search"class=" form-control"placeholder="Escriba el nombre de un post"></div> @if ($posts->count())
@if (session('info'))
<divclass=" alert alert-success"><strong>{{session('info')}}</strong></div> @elseif(session('info-del'))
<divclass=" alert alert-danger"><strong>{{session('info-del')}}</strong></div> @endif
<divclass="card-body"><tableclass="table table-striped"><thead><tr><th>Id</th><th>Nombre</th><thcolspan="2"></th></tr></thead><tbody> @foreach ($posts as $post)
<tr><th>{{$post->id}}</th><th>{{$post->name}}</th><thwidth="10px"><aclass=" btn btn-primary btn-sm"href="{{route('admin.posts.edit', $post)}}">Editar</a></th><thwidth="10px"><formaction="{{route('admin.posts.destroy', $post)}}"method="POST"> @csrf
@method('delete')
<buttonclass="btn btn-danger btn-sm"type="submit">Eliminar</button></form></th></tr> @endforeach
</tbody></table></div> {{-- Aqui pongo la paginación--}}
<divclass="car-footer"> {{$posts->links()}}
</div> @else
<divclass="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.)
<?phpnamespaceApp\Observers;useApp\Models\Post;classPostObserver{/**
* Handle the Post "created" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctioncreated(Post$post){//
}/**
* Handle the Post "updated" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctionupdated(Post$post){//
}/**
* Handle the Post "deleted" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctiondeleted(Post$post){//
}/**
* Handle the Post "restored" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctionrestored(Post$post){//
}/**
* Handle the Post "force deleted" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctionforceDeleted(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.
<?phpnamespaceApp\Observers;useApp\Models\Post;classPostObserver{/**
* Handle the Post "created" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctioncreated(Post$post){//
}/**
* Handle the Post "deleted" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctiondeleted(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
*/publicfunctiondeleting(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.
<?phpnamespaceApp\Providers;useIlluminate\Auth\Events\Registered;useIlluminate\Auth\Listeners\SendEmailVerificationNotification;useIlluminate\Foundation\Support\Providers\EventServiceProviderasServiceProvider;useIlluminate\Support\Facades\Event;useApp\Models\Post;useApp\Observers\PostObserver;classEventServiceProviderextendsServiceProvider{/**
* The event listener mappings for the application.
*
* @var array
*/protected$listen=[Registered::class=>[SendEmailVerificationNotification::class,],];/**
* Register any events for your application.
*
* @return void
*/publicfunctionboot(){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)
namespaceApp\Observers;useApp\Models\Post;useIlluminate\Support\Facades\Storage;classPostObserver{/**
* Handle the Post "created" event.
*
* @param \App\Models\Post $post
* @return void
*/publicfunctioncreating(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.
}}..........