Laravel 8

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">&times;</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.

 

Print Friendly, PDF & Email

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.

  • 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

  • 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

  • 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 !

  • 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.

      • 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

  • 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 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

          • 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…

  • 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

      • 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()

        • 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

          • 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

        ç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') }}' });

  • 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);
    }

  • 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.

    • 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

    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).

        • 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

    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.

Laisser un commentaire