
Créer un blog – créer un article
Nous avons dans le précédent article créé le tableau pour afficher la liste des articles et des principaux renseignements les concernant. Par la même occasion ce que nous avons mis en place servira pour les autres entités. A présent nous allons coder le formulaire de création d’un article et les méthodes concernées du contrôleur pour le gérer. Ce formulaire sera assez chargé parce qu’il y a de nombreux champs à renseigner pour un article. Nous allons traiter simultanément le cas du clonage.
Cet article va être assez chargé mais je préfère traiter complètement la création en une seule fois.
Vous pouvez télécharger le code final de cet article ici.
Edit au 22/02/2021 : j’ai modifié la méthode create du contrôleur empêcher la duplication d’un article dont on n’est pas l’auteur.
Les routes
A priori nous avons créé toutes les routes dans le précédent article. Mais on doit traiter le cas particulier du clonage. Pour la route posts.create on n’a pas prévu de paramètre. Pour le clonage le plus évident est d’utiliser la même route mais avec un paramètre qui sera l’identifiant de l’article à cloner. On va donc sortir la route de la ressource et la coder séparément pour ajouter le paramètre :
Route::prefix('admin')->group(function () { Route::middleware('redac')->group(function () { ... // Posts Route::resource('posts', BackPostController::class)->except(['show', 'create']); Route::name('posts.create')->get('posts/create/{id?}', [BackPostController::class, 'create']); });
Le tableau
Pour le bouton de clonage dans PostsDataTable on n’avait pas renseigné l’url alors maintenant on va le faire puisqu’on a décidé comment on va s’y prendre :
return $buttons ... ). $this->button( 'posts.create', $post->id, 'info', __('Clone'), 'clone'
La validation
Pour la validation on crée un form request :
php artisan make:request Back\PostRequest
On a deux champs particuliers au niveau de la validation :
- le slug
- les étiquettes et les mots-clés (META)
Pour cette validation il n’existe pas de règle dans Laravel et on doit les créer nous-même. Pour le slug on crée une classe parce que la règle sera utilisée plusieurs fois :
php artisan make:rule Slug
Dans cette classe on utilise une expression régulière :
<?php namespace App\Rules; use Illuminate\Contracts\Validation\Rule; class Slug implements Rule { public function __construct() { // } public function passes($attribute, $value) { return preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value); } public function message() { return trans('validation.slug'); } }
J’ai déjà prévu le message dans les fichiers de langue que j’avais intégrés dans les précédents fichiers à télécharger, on n’a donc pas à nous en inquiéter ici.
Pour les étiquettes et les mots-clés (séparés par des virgules, pas d’espace et 50 caractères maximum) je vais mettre l’expression régulière directement dans la form request :
<?php namespace App\Http\Requests\Back; use Illuminate\Foundation\Http\FormRequest; use App\Rules\Slug; class PostRequest extends FormRequest { public function authorize() { return true; } public function rules() { $regex = '/^[A-Za-z0-9-éèàù]{1,50}?(,[A-Za-z0-9-éèàù]{1,50})*$/'; $id = $this->post ? ',' . $this->post->id : ''; return $rules = [ 'title' => 'required|max:255', 'body' => 'required|max:65000', 'slug' => ['required', 'max:255', new Slug, 'unique:posts,slug' . $id], 'excerpt' => 'required|max:500', 'meta_description' => 'required|max:160', 'meta_keywords' => 'required|regex:' . $regex, 'seo_title' => 'required|max:60', 'image' => 'required|max:255', 'categories' => 'required', 'tags' => 'nullable|regex:' . $regex, ]; } }
On utilisera la même classe pour la modification d’un article. C’est pour cette raison que je détecte la présence de données d’un article pour bien renseigner la champ unique slug. En effet dans le cas d’une modification la valeur peut évidemment être déjà présente.
Le repository
Dans le repository PostRepository on crée une méthode store :
public function store($request) { $request->merge([ 'active' => $request->has('active'), 'image' => basename($request->image), ]); $post = $request->user()->posts()->create($request->all()); $this->saveCategoriesAndTags($post, $request); }
Pour le champ active, qui indique si l’article doit être immédiatement publié, on aura une case à cocher. Le souci avec les cases à cocher dans un formulaire c’est qu’on ne trouve la valeur dans la requête que si la case est cochée, sinon on ne trouve rien. C’est pour cette raison qu’il faut faire un test d’existence d’active dans la requête.
Pour l’image illustrative de l’article on va trouver l’url complète dans la requête mais on n’a pas besoin de mémoriser la totalité, on se contente du nom parce qu’on sait générer l’url complète.
Pour les catégories et les étiquettes c’est un peu particulier alors j’ai préféré créer une fonction distincte, surtout qu’elle sera aussi utilisée pour la modification d’un article :
use App\Models\ { Post, Tag }; use Illuminate\Support\Str; ... protected function saveCategoriesAndTags($post, $request) { // Categorie $post->categories()->sync($request->categories); // Tags $tags_id = []; if($request->tags) { $tags = explode(',', $request->tags); foreach ($tags as $tag) { $tag_ref = Tag::firstOrCreate([ 'tag' => ucfirst($tag), 'slug' => Str::slug($tag), ]); $tags_id[] = $tag_ref->id; } } $post->tags()->sync($tags_id); }
Pour les catégories c’est vite réglé avec la méthode sync puisqu’on a une relation de type n:n.
Pour les étiquettes il faut décomposer les données pour ensuite les mémoriser en tenant compte du fait qu’une étiquette peut déjà exister.
Le contrôleur
Ce sont les méthodes create et store du contrôleur Back/PostController qui sont concernées pour cette création.
create
Habituellement la fonction create se contente de renvoyer la vue qui comporte le formulaire en ajoutant parfois des données en relations. Dans notre cas on doit tenir compte aussi du clonage :
use App\Http\{ Controllers\Controller, Requests\Back\PostRequest }; use App\Repositories\PostRepository; use App\Models\{ Post, Category }; use App\DataTables\PostsDataTable; ... public function create($id = null) { $post = null; if($id) { $post = Post::findOrFail($id); if($post->user_id === auth()->id()) { $post->title .= ' (2)'; $post->slug .='-2'; $post->active = false; } else { $post = null; } } $categories = Category::all()->pluck('title', 'id'); return view('back.posts.form', compact('post', 'categories')); }
Le paramètre est nul par défaut, ce qui correspond à une nouvelle création et un formulaire vierge. Si le paramètre est présent on va chercher l’article dans la base, on vérifie au passage que c’est bien l’auteur de l’article, et on envoie les informations à la vue. D’autre part on envoie toutes les catégories pour renseigner une liste déroulante dans le formulaire.
store
La méthode store est celle qui traite l’enregistrement dans la base du nouvel article :
public function store(PostRequest $request, PostRepository $repository) { $repository->store($request); return back()->with('ok', __('The post has been successfully created')); }
Là pour le contrôleur c’est simple parce que tout le travail se fait dans d’autres classes qu’on a déjà codées.
Des composants
Pour les vues de l’administration on aura du code commun. On règle ça en créant des composants qui vont bien alléger le code des formulaires.
alert
On aura pas mal d’alertes alors un composant :
@props([ 'type', 'icon' => 'check', 'title' => '', ]) <div class="alert alert-{{ $type }} alert-dismissible"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <h5><span class="icon fa fa-{{ $icon }}"></span>{{ $title }}</h5> {{ $slot }} </div>
card
On va tout organiser en fiches (cards), alors un autre composant :
@props([ 'outline' => true, 'type', 'title', ]) <div class="card @if($outline) card-outline @endif card-{{ $type }}"> @if($title) <div class="card-header"> <h3 class="card-title">{{ __($title) }}</h3> <div class="card-tools pull-right"> <button type="button" class="btn btn-tool" data-card-widget="collapse"> <i class="fas fa-minus"></i> </button> </div> </div> @endif <div class="card-body"> {{ $slot }} </div> </div>
input
On aura de multiples champs de saisie, alors j’ai créé un composant universel :
@props([ 'input', 'name', 'required' => false, 'title', 'rows' => 3, 'title', 'label', 'options', 'value' => '', 'Values', 'multiple' => false, ]) <div class="form-group"> @isset($title) <label for="{{ $name }}">@lang($title)</label> @endisset @if ($input === 'textarea') <textarea class="form-control{{ $errors->has($name) ? ' is-invalid' : '' }}" rows="{{ $rows }}" id="{{ $name }}" name="{{ $name }}" @if ($required) required @endif>{{ old($name, $value) }}</textarea> @elseif ($input === 'checkbox') <div class="custom-control custom-checkbox"> <input class="custom-control-input" id="{{ $name }}" name="{{ $name }}" type="checkbox" {{ $value ? 'checked' : '' }}> <label class="custom-control-label" for="{{ $name }}"> {{ __($label) }} </label> </div> @elseif ($input === 'select') <select @if($required) required @endif class="form-control{{ $errors->has($name) ? ' is-invalid' : '' }}" name="{{ $name }}" id="{{ $name }}"> @foreach($options as $option) <option value="{{ $option }}" {{ old($name) ? (old($name) == $option ? 'selected' : '') : ($option == $value ? 'selected' : '') }}> {{ $option }} </option> @endforeach </select> @elseif ($input === 'selectMultiple') <select multiple @if($required) required @endif class="form-control{{ $errors->has($name) ? ' is-invalid' : '' }}" name="{{ $name }}[]" id="{{ $name }}"> @foreach($options as $id => $title) <option value="{{ $id }}" {{ old($name) ? (in_array($id, old($name)) ? 'selected' : '') : ($values->contains('id', $id) ? 'selected' : '') }}> {{ $title }} </option> @endforeach </select> @else <input type="text" class="form-control{{ $errors->has($name) ? ' is-invalid' : '' }}" id="{{ $name }}" name="{{ $name }}" value="{{ old($name, $value) }}" @if($required) required @endif> @endif @if ($errors->has($name)) <div class="invalid-feedback"> {{ $errors->first($name) }} </div> @endif </div>
validation-errors
Enfin il faudra systématiquement afficher les erreurs de validation :
@props(['errors']) @if($errors->any()) <x-back.alert type='danger' icon='ban' title="{{ __('Whoops! Something went wrong.') }}"> <ul> @foreach($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </x-back.alert> @endif
La vue
On crée la vue :
Le code utilise de façon systématique les composants qu’on a créés :
@extends('back.layout') @section('css') <style> #holder img { height: 100%; width: 100%; } </style> @endsection @section('main') <form method="post" action="{{ Route::currentRouteName() === 'posts.edit' ? route('posts.update', $post->id) : route('posts.store') }}"> @if(Route::currentRouteName() === 'posts.edit') @method('PUT') @endif @csrf <div class="row"> <div class="col-md-8"> <x-back.validation-errors :errors="$errors" /> @if(session('ok')) <x-back.alert type='success' title="{!! session('ok') !!}"> </x-back.alert> @endif <x-back.card type='primary' title='Title'> <x-back.input name='title' :value="isset($post) ? $post->title : ''" input='text' :required="true"> </x-back.input> </x-back.card> <x-back.card type='primary' title='Excerpt'> <x-back.input name='excerpt' :value="isset($post) ? $post->excerpt : ''" input='textarea' :required="true"> </x-back.input> </x-back.card> <x-back.card type='primary' title='Body'> <x-back.input name='body' :value="isset($post) ? $post->body : ''" input='textarea' rows=10 :required="true"> </x-back.input> </x-back.card> <button type="submit" class="btn btn-primary">@lang('Submit')</button> </div> <div class="col-md-4"> <x-back.card type='primary' :outline="false" title='Publication'> <x-back.input name='active' :value="isset($post) ? $post->active : false" input='checkbox' label="Active"> </x-back.input> </x-back.card> <x-back.card type='warning' :outline="false" title='Categories' :required="true"> <x-back.input name='categories' :values="isset($post) ? $post->categories : collect()" input='selectMultiple' :options="$categories"> </x-back.input> </x-back.card> <x-back.card type='danger' :outline="false" title='Tags'> <x-back.input name='tags' :value="isset($post) ? implode(',', $post->tags->pluck('tag')->toArray()) : ''" input='text'> </x-back.input> </x-back.card> <x-back.card type='success' :outline="false" title='Slug'> <x-back.input name='slug' :value="isset($post) ? $post->slug : ''" input='text' :required="true"> </x-back.input> </x-back.card> <x-back.card type='primary' :outline="false" title='Image'> <div id="holder" class="text-center" style="margin-bottom:15px;"> @isset($post) <img style="width:100%;" src="{{ getImage($post, true) }}" alt=""> @endisset </div> <div class="input-group mb-3"> <div class="input-group-prepend"> <a id="lfm" data-input="image" data-preview="holder" class="btn btn-primary text-white" class="btn btn-outline-secondary" type="button">Button</a> </div> <input id="image" class="form-control {{ $errors->has('image') ? 'is-invalid' : '' }}" type="text" name="image" value="{{ old('image', isset($post) ? getImage($post) : '') }}" required> @if ($errors->has('image')) <div class="invalid-feedback"> {{ $errors->first('image') }} </div> @endif </div> </x-back.card> <x-back.card type='info' :outline="false" title='SEO'> <x-back.input title='META Description' name='meta_description' :value="isset($post) ? $post->meta_description : ''" input='textarea' :required="true"> </x-back.input> <x-back.input title='META Keywords' name='meta_keywords' :value="isset($post) ? $post->meta_keywords : ''" input='textarea' :required="true"> </x-back.input> <x-back.input title='SEO Title' name='seo_title' :value="isset($post) ? $post->seo_title : ''" input='text' :required="true"> </x-back.input> </x-back.card> </div> </div> </form> @endsection @section('js') @include('back.shared.editorScript') @endsection
Cette vue sera utilisée également pour la modification d’un article. Il y a en effet peu de différences.
Comme le Javascript sera utilisé pour d’autres vues je l’ai placé dans un fichier partagé :
<script src="https://cdn.ckeditor.com/4.15.1/standard/ckeditor.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/speakingurl/14.0.1/speakingurl.min.js"></script> <script> $(function() { $.fn.filemanager = function(type, options) { type = type || 'file'; this.on('click', function(e) { var route_prefix = (options && options.prefix) ? options.prefix : '/filemanager'; var target_input = $('#' + $(this).data('input')); var target_preview = $('#' + $(this).data('preview')); window.open(route_prefix + '?type=' + type, 'FileManager', 'width=900,height=600'); window.SetUrl = function (items) { var file_path = items.map(function (item) { return item.url; }).join(','); // set the value of the desired input to image url target_input.val('').val(file_path).trigger('change'); // clear previous preview target_preview.html(''); // set or change the preview image src items.forEach(function (item) { target_preview.append( $('<img>').attr('src', item.thumb_url) ); }); // trigger change event target_preview.trigger('change'); }; return false; }); } $('#lfm').filemanager('image'); $('#slug').keyup(function () { $(this).val(getSlug($(this).val())) }) $('#title').keyup(function () { $('#slug').val(getSlug($(this).val())) }) }); CKEDITOR.replace('body', { customConfig: '{{ asset('js/ckeditor.js') }}' }); </script>
Il nous faut un éditeur pour la rédaction du résumé et du contenu de l’article. Dans ce domaine on a du choix : Summernote, TinyMCE, CKEditor… J’ai opté pour ce dernier. Il est juste dommage que la page d’intégration de Laravel File Manager n’ait pas encore prévu la version 5 de cet excellent éditeur. Je n’ai pas eu le courage de faire cette intégration moi-même et me suis donc contenté de la version 4 qui comporte déjà tout ce dont nous avons besoin.
Pour la configuration de CKEditor j’ai préféré mettre ça dans un fichier séparé :
CKEDITOR.editorConfig = function(config) { config.height = 400 config.filebrowserImageBrowseUrl = '/laravel-filemanager?type=Images', config.filebrowserImageUploadUrl = '/laravel-filemanager/upload?type=Images&_token=', config.filebrowserBrowseUrl = '/laravel-filemanager?type=Files', config.filebrowserUploadUrl = '/laravel-filemanager/upload?type=Files&_token=' config.toolbarGroups = [ { name: 'clipboard', groups: [ 'clipboard', 'undo' ] }, { name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] }, { name: 'links' }, { name: 'insert' }, { name: 'forms' }, { name: 'tools' }, { name: 'document', groups: [ 'mode', 'document', 'doctools' ] }, { name: 'others' }, { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }, { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] }, { name: 'styles' }, { name: 'colors' } ] }
Normalement en cliquant dans le menu sur Ajouter dans la rubrique Articles vous obtenez le formulaire :
Le formulaire est séparé verticalement en deux parties. A gauche les éléments les plus importants (titre, résumé et contenu) ainsi que le bouton de soumission :
Dans la partie droite on a tout le reste :
Remarque importante : pour que Laravel File Manager génère des url correctes il faut bien renseigner la valeur de APP_URL dans le fichier .env :
APP_URL=http://monblog.oo
Fonctionnement
Pour la génération automatique du slug quand on tape le titre, mais aussi pour contrôler la saisie directement quand on tape le slug, j’ai ajouté la librairie Javascript speakingurl qui est parfaite pour ça. Ainsi quand on entre ce titre :
Ce slug est automatiquement généré :
Vous pouvez vérifier que la validation fonctionne bien :
En faisant l’essai de l’enregistrement d’un article je me rends compte que le champ $fillable du modèle Tag est incomplet, il manque slug, alors on l’ajoute :
protected $fillable = ['tag', 'slug'];
Pour les catégories on peut en sélectionner plusieurs dans la liste puisqu’on a une relation de type n:n et qu’un article peut ainsi appartenir à plusieurs catégories :
Pour l’image illustrative on utilise une intégration de Laravel File Manager un peu adaptée :
L’url générée est complète mais on a vu que dans le repository on récupère juste le nom :
$request->merge([ ... 'image' => basename($request->image), ]);
Par contre les images ajoutées dans le contenu de l’article auront l’url complète :
Ca peut présenter un inconvénient dans le cas où on veut changer de domaine parce que les urls ne seront plus correctes. Il serait plus judicieux de prévoir des urls relatives. Rien n’empêche de faire ce traitement lors de l’enregistrement de l’article mais je ne l’ai pas prévu.
Quand on a renseigné tous les champs et qu’il n’y a aucune erreur de validation on délivre une alerte rassurante :
Il ne reste plus qu’à vérifier si le clonage fonctionne. Dans le tableau des articles au niveau de la colonne des actions on a un bouton pour a duplication :
On se retrouve avec le formulaire complété des données de l’article dupliqué mais on change le titre en ajoutant (2) :
Et on modifie aussi le slug en accord :
D’autre part la case de publication est systématiquement décochée.
Conclusion
Un long article mais la création est la partie la plus complexe à traiter, ça sera plus simple pour la suite. Nous complèterons avec la modification et la suppression d’un article. Pour la modification les routes existent déjà, de même que la validation, le formulaire et une bonne partie du traitement. Pour la suppression il faudra utiliser un peu de Javascript pour éviter de faire une suppression directe mais plutôt de mander une confirmation.


97 commentaires
Antho
Bonsoir, j’ai fini depuis un moment le projet et je voudrais ajouter ça : « Date et heure de publication :
»
c’est dans quel fichier de vues que je mets ça ??
Je préfère te demander pour éviter de faire une bêtise.
Antho
Bon je ne sais pas pourquoi ça s’est effacé dans le commentaire précédent mais c’est un label et un input que je veux mettre.
bestmomo
Salut,
Tout ajout doit être dans la vue « form » qui regroupe tout le formulaire. Mais je ne comprends pas la démarche, quand on crée un article c’est forcément à une certaine date et une certaine heure et Eloquent se charge automatiquement de renseigner la colonne created_at. Tu veux donc modifier ce comportement et mettre une date différente…
Antho
C’est parce que j’aimerai planifier la publication en choissant tel date à tel heure et que ça soit visible automatiquement.
bestmomo
Dans ce cas, il faut empêcher Laravel de renseigner le created_at avec cette propriété dans le modèle :
public $timestamps = false;
Ensuite ajouter les inputs dans le formulaire pour renseigner la date et l’heure.
DIM
Bonjour Bestmomo j’ai un problème avec l’éditeur ckeditor impossible de téléverser une image depuis mon ordinateur j’ai une erreur 419 impossible de téléverser l’image
bestmomo
Salut, ça ressemble à une erreur CSRF, tu as bien mis le token ?
DIM
id) : route(‘posts.store’) }} »>
@if(Route::currentRouteName() === ‘posts.edit’)
@method(‘PUT’)
@endif
@csrf
………….
il y a bien le token dans back/ posts/form.blade.php
DIM
Bonjour Bestmomo, en fait il n’y avait pas de problème juste une mauvaise compréhension du package maintenant que j’ai bien compris le fonctionnement ça marche
softcode
Bonjour best momo, j’ai rencontré un problème avec ckeditor, après avoir rediger l’article, suis allé le voir au niveau front, je constate que la colonne body affiche les text encadré avec la balise html comme ceci :
Seminaire sur l'administration bdSeminaire sur l'administration bdSeminaire sur l'administration bd
cela est dut à quoi? Merci beaucoup
bestmomo
Salut. Difficile à dire comme ça, il faudrait faire un peu de debug pour voir où ça coince. Déjà j’ai vu que maintenant la version est pasée à 4.20.2, des fois ça résoud bien des choses.
softcode
d’accord bestmomo je vais essayer de mettre à jour mes versions pour voir si ca va marcher
DIM
Bonjour bestmomo j’ai le meme problème et difficile de le résoudre pour le moment
besoin de ton aide
softcode
Bonjour best momo, merci pour ce travail qui nous aide beaucoup que j’avais dejà terminé jusqu’à sa fin. Mais je me suis lancé dans ce même projet, cette fois ci j’avais ajouter la table souscategory et category_souscategory. avec des connaissances que j’ai maintenant là tout marche bien.
mais ma préocupation est celle ci : je voulais avoir une logique de votre part de comment mettre à jour la table category_souscategory lors de la creation d’un post car dans votre part, vous avez utilisez le code ci dessous pour mettre à jour la table category_post
// Categorie
$post->categories()->sync($request->categories);
vouus avez effectué cette enregistrement au même moment de la cretion dun post auquel vous avez recuperé l’id du post créer et l’id de la category depuis le formulaire
de ma part, pour mettre à jour la table category_souscategory j’ai ecris ce bout de code que voici:
$category = new Category();
$category->souscategories()->sync($request->categories, $request-> souscategories);
vous remarquerez que j’envois les deux identifiants qui proviennen du formulaire post, au moment de la validation j’ai cette erreur :
SQLSTATE[23000]: Integrity constraint violation: 1048 Column ‘category_id’ cannot be null
INSERT INTO `category_souscategory` (`category_id`, `souscategory_id`) VALUES (?, 2)
ce pourquoi je demande votre aide si vous pouvez me donner une autre logique pour bien receuillir ces deux identifiants et les inserer dans la table category_souscategory,
Merci beaucoup pour votre comprehension.
bestmomo
Bonjour,
Comment est composée cette table category_souscategory? Je ne comprends pas trop le modèle de données que tu as mis en place.
Pour avoir des sous-catégories, il me semble qu’il faudrait créer une table subcategories en liaison n:n avec les posts. Donc avec une table pivot entre les deux subcategory_post. Et avoir une simple relation 1:n entre categories et subcategories. Lors de la mise à jour ça serait pratiquement le même code que j’ai établi avec la nouvelle table pivot.
tgenougan
Bonjour ?.
Déjà super tout ce travail que tu as fait.
Je suis confronté à une situation. Je souhaite utiliser laravel file manager et le watermark mais je suis confronté à l’erreur image source is not readable. Merci de m’orienter
Cordialement
bestmomo
Salut,
Je pense que tu as une bonne piste ici.
gp
Le résultat est superbe, mais c’est une usine à gaz en comparaison de ce que propose Symfony. il ya des composants des parametres pour des titres qui viennent en config, des helpers, des datatables, des notifications ;des composeurs. J’ai du mal à croire que ce soit pour de simples CRUD. Quelle complication délirante !
bestmomo
Bonjour,
Je n’utilise pas Symfony et je ne peux donc avoir aucun point de vue comparatif entre les deux.
softcode
Bonjour bestmomo,
Je viens de finir cette partie avec succès, Merci beaucoup, mais je voulais aussi insérer de morceaux de code source PHP , HTML et autre toujours à l’intérieur de ckeditor vraiment je n’ai pas pu trouver cela,
Svp je peux trouver une solution de votre part quant à ce me permettant de présenter des morceaux de code lors de l’édition d’un post merci!
bestmomo
Salut,
Apparemment, c’est prévu dans CKEditor 5.
softcode
Est ce que la migration au Ckeditor 5 ne va pas perturber l’implémentation précédente ? Ou faut juste installer un packagé?
bestmomo
Le souci, c’est que Laravel File Manager n’a pas encore prévu l’intégration de la version 5.
Antho
Bonjour j’ai un petit souci, j’ai une erreur 404 quand je clique sur le bouton pour upload une image et dans l’URL je n’ai pas la bonne URL et j’ai beau tenté plusieurs app URL toujours la même chose et le lien symbolique est bien établi. Merci d’avance de ton aide.
bestmomo
Salut, tu as bien renseigné le fichier .env ?
APP_URL=http://monblog.oo
Antho
Hello, oui mais je suppose que c’était à cause du module https everywhere que j’avais activé sur mon navigateur donc en le désactivant et en ayant redémarré le serveur apache sur Laragon ça a enfin fonctionné.
softcode
Bonjour best momo, merci beaucoup pour vos cours qui m’aide beaucoup sur mes développements des applications sur laravel,
Ma préoccupation se trouve au niveau de l’édition d’un article je voulais intégrer summernot editor de admin template au lieux d’utiliser texterrea simple
Quand je rédige l’article à l’intérieur de summernot Editor avec quelques photos à l’intérieur
C’est après click droit sur le bouton enregistrer ça me génère l’erreur donc la colonne body de la table post ne prend pas en charge l’enregistrement de la photo provenant de l’éditeur summernot
Je vous en prie de m’aider à comment mettre en place summernot editor pourque ça prenne en charge et les écrits et la photo merci beaucoup
bestmomo
Salut,
Le souci est toujours avec les images, il n’y a pas beaucoup d’intégration avec Laravel Filemanager. C’est pour cette raison que j’avais choisi ici CKEditor. Pour Summernote il faut prendre en charge ça et ce n’est pas si simple. Je l’avais choisi pour la partie CMS de mon exemple de boutique en ligne mais sans la gestion des images.
softcode
D’accord merci beaucoup je vais essayer d’utiliser CKeditor merci
MatthiasScreed
j’ai un petit probleme tout fonctionne normalement mais je n’arrive pas a activer les articles les image s’enregistre mais l’activation reste toujours a zero je comprends pas pourquoi
riftone07
Bonjour J’aimerais savoir est ce qu’il y’a une securité prevue pour le CKeditor vu que les données sont affiché avec la syntaxe non securisé de blade
bestmomo
Bonjour,
Il n’y a rien de prévu au niveau sécurité. On part juste du principe que les auteurs d’articles sont fiables. Si on veut vraiment sécuriser on peut utiliser un package comme purify côté serveur.
bastoes
Bonjour BESTMOMO.
S’il vous plait j’ai besoin de votre aide.
Quand je clique sur submit pour ajouter ou bien modifier un nouveau article je tombe sur une erreur 403.
bestmomo
Bonjour,
Est-ce qu’il y a les droits d’écriture pour les dossiers storage et bootstrap/cache ?
bastoes
Bonjour BESTMOMO je suis désolé pour le retard au niveau de ma réponse. j’ai reglé le problème. PostRequest->authorize() return false je l’ai changé à true.
Thibaut
voici l’url que j’ai lors de upload de l’image
http://blog-tutoriel.local/laravel-filemanager?type=Images&CKEditor=body&CKEditorFuncNum=1&langCode=fr
bestmomo
Salut,
Tu as bien utilisé la commande php artisan storage:link ?
Thibaut
oui oui j’ai bien utilisé la commande php artisan storage;link
bestmomo
Bon si tu obtiens une erreur 404 c’est que la route n’existe pas, alors vérifie la route de filemanager.
Thibaut
voici la route que j’ai :
Route::group([‘prefix’ => ‘laravel-filemanager’, ‘middleware’ => ‘auth’], function () {
Lfm::routes();
});
et lorsque je fais un php artisan route;list jai tt les route y compris celle de filemanager
bestmomo
Tu as essayé en téléchargeant le code du Zip que j’ai mis pour voir si ça fonctionne ?
Thibaut
bjr Bestmomo, j’ai une 404 lors de l’upload de l’image dans le contenu de l’article, pour l’image mise en avant tout est ok, je ne sais vraiment pas où regardé, tout le fichierq sont bien en place: le ckeditor.js est bien dans le dossier public et bien chargé, et j’ai bien le editorScript
bensa
Salut,
svp dans editorScript.blade.php à quel niveau ce script identifie le chemin du dossier ../photos/1 ou bien …/photos/2 ou bien …/photos/3 ?
Bonne journée.
bestmomo
Salut,
C’est pas là que ça se passe mais dans les scripts du package, je n’ai pas fouillé.
bensa
Bonjour,
oui je vois l’idée c’est est ce que je peux me placer dans un dossier photos/2 sachant que je suis connécté avec un id:1 car laravel file manager pointe toujours sur le dossier dont le nom vaut la valeur de l’id du user connecté.
Salutations
bestmomo
Le système est conçu ainsi, on peut à la rigueur choisir une autre colonne que l’id mais pas plus. Par contre on peut définir un dossier partagé.
bensa
Salut BESTMOMO you are really the best 🙂
j’ai resolu le probleme de laravel file manager, reste seulement un petit souci, je veux télécharger un fichier pdf dans le dossier de l’utilisateur, comment puisse faire cette action sachant que je recois le message:
you can’t upload file of this type
merci
bestmomo
Salut,
Ca avance alors 🙂
Pour le type de fichier ça se passe dans le fichier de configuration config/lfm.php. Logiquement pour le dossier images on n’autorise que les images, le reste devant aller dans le dossier photos. Mais rien ne t’empêche d’autoriser les pdf dans le dossier photos, je crains juste qu’il y ait un souci lors de la création du thumb 🙂
bensa
Bonjour,
En fait le souci que j’ai c’est que en faisant upload d’une photo je la trouve au niveau du chemin:
monblog\storage\app\public\photos au lieu de la trouver au chemin : monblog\public\storage\photos
NB: j’ai dèja fait la commande: php artisan storage:link mais ca marche pas le nouveau poste s’affiche sans image.
Have a nice day
bestmomo
Salut,
Si le lien symbolique ne fonctionne pas ça ira pas. A la limite tu t’en passes en restant strictement dans public, en redéfinissant le root du disque public de Laravel dans config.filesystems.
bensa
Salut,
en fait c’est résolu en configurant un autre disk dans config.filessystems avec:
‘root’ => public_path(‘storage’),
Merci
bestmomo
Parfait. Cette histoire de lien symbolique franchement c’est surtout fait pour nous compliquer la vie…
lafia
Salut, je demande si vous avez pu telecharger un pdf avec le package ?
bensa
Salut
svp un support pour Filemanager ca fonctionne pas chez moi,
en cliquant sur le bouton je recois un message:
The requested URL was not found on this server.
Apache/2.4.46 (Win64) PHP/7.3.21 Server at localhost Port 80
Merci
bestmomo
Salut,
Vérifie bien que les routes du file manager sont présentes en faisant un php artisan route:list.
bensa
quand j’exécute la commande php artisan route:list je recois l’exception:
C:\wamp64\www\monblog>php artisan route:list
Symfony\Component\HttpKernel\Exception\HttpException
at C:\wamp64\www\monblog\vendor\laravel\framework\src\Illuminate\Foundation\Application.php:1116
1112▕ if ($code == 404) {
1113▕ throw new NotFoundHttpException($message);
1114▕ }
1115▕
➜ 1116▕ throw new HttpException($code, $message, null, $headers);
1117▕ }
1118▕
1119▕ /**
1120▕ * Register a terminating callback with the application.
1 C:\wamp64\www\monblog\vendor\laravel\framework\src\Illuminate\Foundation\helpers.php:44
Illuminate\Foundation\Application::abort(« », [])
2 C:\wamp64\www\monblog\app\Http\Controllers\Front\CommentController.php:13
abort()
bensa
oui maintenant je peux voir les routes de laravelmanager mais j’ai toujours le même problème
bestmomo
Dans le constructeur de CommentController j’avais ajouté après coup la vérification pour la console :
if(!app()->runningInConsole() && !request()->ajax()) {
Mais appremment tu las résolu ça puisque tu peux voir tes routes.
Il faudrait voir quelle route manque en regardant l’activité réseau avec les outils développeur du navigateur.
bensa
Bonjour,
En fait j’ai essayé plusieurs configurations mais j’ai toujours ce problème de 404 Url was not found avec laravel file manager, en fait en cliquant sur le button image le navigateur s’ouvre avec url: http://localhost/filemanager?type=image, quand j’utilise url:
http://localhost/mon blog/public/filemanager?type=image ca fonctionne.
sur la table routage je trouve pas mal de route de Laravel manager, ci-après la liste des noms:
unisharp.lfm.show |unisharp.lfm.getCrop | unisharp.lfm.getCropimage| unisharp.lfm.getCropnewimage | unisharp.lfm.getDelete |unisharp.lfm |unisharp.lfm.domove |unisharp.lfm.performResize | unisharp.lfm.getDownload |unisharp.lfm.getDownload | unisharp.lfm.getErrors | unisharp.lfm.getFolders | unisharp.lfm.getItems | unisharp.lfm.move | unisharp.lfm.getAddfolder | unisharp.lfm.getAddfolder | unisharp.lfm.getRename | unisharp.lfm.getResize | unisharp.lfm.upload
Merci beaucoup pour ton support
bestmomo
Est-ce que tu as bien renseigné l’url dans le fichier .env ?
APP_URL=http://monblog.ext
bensa
salut
voici la config de mon .env
APP_NAME= »monblog »
APP_ENV=local
APP_KEY=base64:K/tQYF4/l1wmtQyVP82MbeVWICPRkmEqiS79VTPVTeo=
APP_DEBUG=true
APP_URL=http://monblog
bensa
salut
en changeant la variable var route_prefix dans editorScript.blade php:
var route_prefix = (options && options.prefix) ? options.prefix : ‘/monblog/public/filemanager’;
la fenêtre s’ouvre de laravelmanger mais ca me permet pas d’ajouter une photo
webwatson
Salut,
Moi mon soucis est au niveau du bouton, lorsque je clique, j’ai pas de réaction et l’ajout de CKeditor n’apparaît pas bien chez moi.
webwatson
Et Lorsque j’ajoute un article, il n’est pas recupérer dans le tableau bien qu’il ait bien sauvegrardé en BDD
bestmomo
Il faudrait faire du débogage pour cerner l’erreur, dans le Javascript pour voir ce que donne l’action du bouton, et sur le servuer si ça arrive jusque là…
webwatson
ça ne va toujours pas les amis venez m’aider !
*PostController*
render('back.shared.index');
}
public function create($id = null)
{
$post = null;
if($id) {
$post = Post::findOrFail($id);
if($post->user_id === auth()->id()) {
$post->title .= ' (2)';
$post->slug .='-2';
$post->active = false;
} else {
$post = null;
}
}
$categories = Category::all()->pluck('title', 'id');
return view('back.posts.form', compact('post', 'categories'));
}
public function store(PostRequest $request, PostRepository $repository)
{
$repository->store($request);
return back()->with('ok', __('The post has been successfully created'));
}
public function show(Post $post)
{
//
}
public function edit(Post $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)
{
//
}
}
*web.php*
Route::resource('posts', BackPostController::class)->except('show');
Route::name('posts.create')->get('posts/create/{id?}', [BackPostController::class, 'create']);
*editoScript*
https://cdn.ckeditor.com/4.15.1/standard/ckeditor.js
https://cdnjs.cloudflare.com/ajax/libs/speakingurl/14.0.1/speakingurl.min.js
$(function() {
$.fn.filemanager = function(type, options) {
type = type || 'file';
this.on('click', function(e) {
var route_prefix = (options && options.prefix) ? options.prefix : '/filemanager';
var target_input = $('#' + $(this).data('input'));
var target_preview = $('#' + $(this).data('preview'));
window.open(route_prefix + '?type=' + type, 'FileManager', 'width=900,height=600');
window.SetUrl = function (items) {
var file_path = items.map(function (item) {
return item.url;
}).join(',');
// set the value of the desired input to image url
target_input.val('').val(file_path).trigger('change');
// clear previous preview
target_preview.html('');
// set or change the preview image src
items.forEach(function (item) {
target_preview.append(
$('').attr('src', item.thumb_url)
);
});
// trigger change event
target_preview.trigger('change');
};
return false;
});
}
$('#lfm').filemanager('image');
$('#slug').keyup(function () {
$(this).val(getSlug($(this).val()))
})
$('#title').keyup(function () {
$('#slug').val(getSlug($(this).val()))
})
});
CKEDITOR.replace('body', { customConfig: '{{ asset('js/ckeditor.js') }}' });
https://cdn.ckeditor.com/4.15.1/standard/ckeditor.js
https://cdnjs.cloudflare.com/ajax/libs/speakingurl/14.0.1/speakingurl.min.js
$(function() {
$.fn.filemanager = function(type, options) {
type = type || 'file';
this.on('click', function(e) {
var route_prefix = (options && options.prefix) ? options.prefix : '/filemanager';
var target_input = $('#' + $(this).data('input'));
var target_preview = $('#' + $(this).data('preview'));
window.open(route_prefix + '?type=' + type, 'FileManager', 'width=900,height=600');
window.SetUrl = function (items) {
var file_path = items.map(function (item) {
return item.url;
}).join(',');
// set the value of the desired input to image url
target_input.val('').val(file_path).trigger('change');
// clear previous preview
target_preview.html('');
// set or change the preview image src
items.forEach(function (item) {
target_preview.append(
$('').attr('src', item.thumb_url)
);
});
// trigger change event
target_preview.trigger('change');
};
return false;
});
}
$('#lfm').filemanager('image');
$('#slug').keyup(function () {
$(this).val(getSlug($(this).val()))
})
$('#title').keyup(function () {
$('#slug').val(getSlug($(this).val()))
})
});
CKEDITOR.replace('body', { customConfig: '{{ asset('js/ckeditor.js') }}' });
bestmomo
Salut,
Il faudrait vraiment passer par du débogage pour traquer les erreurs, en voyant le code comme ça c’est impossible sauf erreur évidente…
webwatson
Au chargement de la page add post, voici l’erreur dans la console https://ibb.co/sCDXdHk
Quand je clique sur le bouton, j’ai vraiment pas de réaction ni sur le front, ni dans la console.
Cependant, j’ai une erreur dans la partie source sur le JS https://ibb.co/jWJxdnd
bestmomo
Apparemment JQuery n’est pas chargé.
webwatson
Est-ce normal qu’un dd($datatable) nous retourne cette capture https://ibb.co/S57NnZ7
cette erreur me dérange, je galère depuis deux jours maintenant!
bestmomo
Je viens de faire un essai et j’ai le même rendu que toi. Mais d’après tes commentaires précédents il semblerait que tu ne charges pas JQuery.
Yagrasdemonde
Petit rectificatif à faire dans les méthodes getPreviousPost et getNextPost du repository PostRepository.
En effet après avoir créé un nouvel article en ne cochant pas la case publié puis en dupliquant ce dernier, lorsque je suis sur la page de l’article dupliqué, en bas de page, j’ai le lien vers l’article précédent de l’article non publié.
J’ai donc modifié :
protected function getPreviousPost($id)
{
return Post::select(‘title’, ‘slug’)->whereActive(true)->latest(‘id’)->firstWhere(‘id’, ‘whereActive(true)->oldest(‘id’)->firstWhere(‘id’, ‘>’, $id);
}
bestmomo
Salut,
Oui effectivement je n’ai pas filtré les articles actifs, merci, je vais corriger ça…
webwatson
Au chargement de la page de la liste des articles:
jquery.dataTables.min.js:21 Uncaught ReferenceError: jQuery is not defined
at jquery.dataTables.min.js:21
at jquery.dataTables.min.js:21
(anonymous) @ jquery.dataTables.min.js:21
(anonymous) @ jquery.dataTables.min.js:21
dataTables.bootstrap4.min.js:11 Uncaught ReferenceError: jQuery is not defined
at dataTables.bootstrap4.min.js:11
at dataTables.bootstrap4.min.js:11
(anonymous) @ dataTables.bootstrap4.min.js:11
(anonymous) @ dataTables.bootstrap4.min.js:11
posts:194 Uncaught ReferenceError: jQuery is not defined
at posts:194
(anonymous) @ posts:194
posts:197 Uncaught ReferenceError: $ is not defined
at posts:197
(anonymous) @ posts:197
ckeditor.js:1 Uncaught ReferenceError: CKEDITOR is not defined
at ckeditor.js:1
(anonymous) @ ckeditor.js:1
posts:1 [Intervention] Slow network is detected. See https://www.chromestatus.com/feature/5636954674692096 for more details. Fallback font will be used while loading: http://localhost:8000/assetsb/vendors/iconfonts/mdi/fonts/materialdesignicons-webfont.woff2?v=3.3.92
ronald169
Salut a tous j’éspere que vous allez tous bien, juste une remarque a faire sur le probleme en commun que nous avons eu avec l’image. Je vous conseillerez de revoir le code source sinon cloner le projet a partir de ce niveau et avancer, il y’a telement de detail dans cet aventure que personnellement j’ai oublié de mettre ‘image’ dans le tableau de $fillable.
oksam
je suis sous windows 10
bestmomo
Sous Windows pour se rendre la vie plus facile le mieux est d’utiliser Laragon. Il faut juste changer la version de PHP qui est par défaut la 7.2 mais c’est facile à faire. Le gros avantage c’est que les hôtes locaux sont automatiquement créés, sinon il faut aller les écrire manuellement dans le fichier hosts de Windows.
oksam
D’accord mais moi j’utilise xamp et comme tu l’as suggéré j’ai ajouté le domaine dans .env mais le problème persiste
bestmomo
Je n’ai jamais utilisé xamp, c’est sous quel système ? Linux, Windows, Mac ?
oksam
Bonjour Best apparement le probème avec l’affiche des images est commun:
est-ce normal que lorsque je clic sur boutton, l’onglet gestionnaire de fichiers le nom de toutes les images mais impssible de voir les images: url:http://127.0.0.1:8000/filemanager?type=image.
puis quand je sellectionne quand même une image au niveau du gestionnaire et que je confirme j’ai cette erreur dans l’inspecteur: GET http://monblog.oo/storage/photos/1/thumbs/img07.jpg net::ERR_NAME_NOT_RESOLVED
mais quand je vais dupliquer un article l’image correspondant s’affiche dans le champ image!
bestmomo
Bonjour,
Apparemment c’est le domaine qui n’est pas reconnu. Comment le domaine est configuré en local ? Personnellement je n’utilise pas le serveur proposé par Laravel. Comme j’utilise Laragon j’ai tout sur un plateau, en particulier les domaines, sinon il faudrait expliquer à Windows que je veux des domaines.
Quand on a bien configuré un domaine local sur sa machine il faut aussi penser à renseigner la valeur dans le fichier .env (APP_URL).
Michel
Bonjour,
En ce qui me concerne, https://monblog.test/storage/photos/1/thumbs/img07.jpg ==> j’obtiens une erreur 404 « cette page n’existe pas »
Mon fichier .env :
APP_NAME= »Mon Blog »
APP_ENV=local
APP_KEY=base64:ZCGjzVuIuorqGQVE5c/l1zHzEA+i4Uwx4FfW6ZuUGzo=
APP_DEBUG=true
APP_URL=http://monblog.test
– J’ai dupliqué un article mais je n’ai pas eu d’image pour autant.
bestmomo
Je vois que l’appel est fait en https, ça peut coincer là. Sinon est-ce que le lien symbolique vers le storage a été mis en place ?
Michel
Merci pour votre retour.
Le blog fonctionne bien cependant aucune image ne s’affiche.
Je n’arrive pas à comprendre pourquoi.
bestmomo
Salut,
Il faudrait voir les urls générées pour les images pour comprendre le problème.
Il y a bien le lien symbolique vers le storage ?
Michel
Je n’ai aucun lien qui s’affiche;
Dans une réponse précédente vous faisiez allusion à cette ligne pour résoudre le problème des images
?
CKEDITOR.replace(‘body’, { customConfig: ‘{{ asset(‘js/ckeditor.js’) }}’ });
bestmomo
Les causes possibles sont multiples. Est-ce que Laravel File Manager est bien installé ? Dans le frontend on utilise l’helper « getImage », est-ce que ça fonctionne à ce niveau ? Pour le backend est-ce que la page pour aller chercher une image s’ouvre bien, dans CKEditor et pour le bouton pour l’image d’illustration ? En fait il faudrait préciser ce qui coince et à quel niveau ça se passe.
Michel
Bonjour,
J’ai suivi vos conseils. réinstall File manager, contrôle helper, etc.
Désormais j’ai les images mais j’ai une page blanche quand je clique sur modifier un article alors que cela fonctionnait avant.
https://monblog.test/admin/posts/6/edit == > page blanche.
bestmomo
Salut,
S’il y a une page blanche c’est sans doute que la méthode edit du contrôleur n’est pas codée.
ronald169
Salut best best j’ai un probleme au niveau de de l’image de description qui ne genere pas la bonne url, j’ai essayer d’examiner le code et voila url qu’il génére: http://blog.test/storage/photos/1
Par contre pour l’image du contenu du texte est me genere une bonne url : http://blog.test/storage/photos/1/shutterbug.jpg
Comment remédier a ce probleme ?
bestmomo
Salut,
Ca se passe dans le Javascript (editorScript.blade.php) qui se retrouve en fin de page, il faudrait déboguer à ce niveau avec les outils développement du navigateur.
ronald169
Salut best juste pour le feedback, j’ai pu gérer le probleme de lien, en fait au niveau du model j’avais omis de mettre ‘image’ en fillable.
bestmomo
Je pense que c’est l’erreur la plus répandue 🙂
Michel
Non; aucun lien ne s’affiche.
Michel
Aucun lien ne s’affiche.
Michel
Tout marche à merveille cependant dans le téléchargement du code de cet article on retrouve le même que pour le tuto précédent monblog10 alors que ce devrait être monblog11.
Est ce voulu pour nous faire travailler un peu ?
Pour vous offir un bon café, je voudrai savoir si il est possible de le faire sans avoir à créer un compte paypal et si oui comment ?
Votre tuto est vraiment super. Encore Merci.
bestmomo
Bonjour,
J’ai corrigé pour le ZIP, ça devrait être bon.
Pour le café je n’ai rien trouvé d’aussi simple que Paypal.