5 Commits

Author SHA1 Message Date
Clément
cfb482ddf5 comment form 2025-05-27 14:41:22 +02:00
fe87272b43 errors handled in the correct log file 2025-05-27 14:41:22 +02:00
Clément
f4aa84a82d handled errors properly 2025-05-12 16:42:41 +02:00
Clément
338e803c5f Categories (NN) 2025-04-17 15:04:13 +02:00
Clément
dad50e035f Comments + 1N relations 2025-04-17 14:30:56 +02:00
44 changed files with 905 additions and 32 deletions

View File

@@ -10,3 +10,31 @@
- `storage` => logs, cache - `storage` => logs, cache
- `tests` => tests de l'app (automatisés) - `tests` => tests de l'app (automatisés)
- `vendor` => le dossier de composer (on n'y touche pas) - `vendor` => le dossier de composer (on n'y touche pas)
# Commandes PHP Artisan (Docker)
- Créer une migration => `php artisan make:migration create_nom_table`
- migration => `php artisan migrate:fresh`
- seeder => `php artisan db:seed --class=NomSeeder`
php artisan make:model Categorie --migration -a
php artisan make:model billet_categorie -m
```sql
use laravel_db;
INSERT INTO billet_categorie (id, billet_id, categorie_id, created_at, updated_at)
VALUES
(1, 1, 1, NULL, NULL),
(2, 1, 2, NULL, NULL),
(3, 1, 10, NULL, NULL),
(4, 2, 3, NULL, NULL),
(5, 3, 9, NULL, NULL),
(6, 4, 1, NULL, NULL),
(7, 4, 4, NULL, NULL),
(8, 5, 5, NULL, NULL),
(9, 6, 7, NULL, NULL),
(10, 7, 1, NULL, NULL),
(11, 8, 10, NULL, NULL),
(12, 9, 6, NULL, NULL),
(13, 10, 8, NULL, NULL);
```

View File

@@ -6,6 +6,9 @@ use App\Http\Requests\StoreBilletRequest;
use App\Http\Requests\UpdateBilletRequest; use App\Http\Requests\UpdateBilletRequest;
use App\Models\Billet; use App\Models\Billet;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\QueryException;
class BilletController extends Controller class BilletController extends Controller
{ {
/** /**
@@ -13,7 +16,12 @@ class BilletController extends Controller
*/ */
public function index() public function index()
{ {
$billets = Billet::all(); try {
$billets = Billet::all();
} catch (QueryException $e) {
Log::channel('projectError')->error('Erreur d\'accès à la base de données');
return view('errors.dberror');
}
return view('index', compact('billets')); return view('index', compact('billets'));
} }
@@ -39,6 +47,12 @@ class BilletController extends Controller
public function show(Billet $billet) public function show(Billet $billet)
{ {
// //
try {
$commentaires = $billet->commentaires;
} catch (QueryException $e) {
return view('errors.dberror');
}
return view('vBillet', compact('billet', 'commentaires'));
} }
/** /**

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCategorieRequest;
use App\Http\Requests\UpdateCategorieRequest;
use App\Models\Categorie;
class CategorieController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreCategorieRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Categorie $categorie)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Categorie $categorie)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateCategorieRequest $request, Categorie $categorie)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Categorie $categorie)
{
//
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCommentaireRequest;
use App\Http\Requests\UpdateCommentaireRequest;
use App\Models\Commentaire;
use App\Models\Billet;
use Illuminate\Support\Facades\Log;
class CommentaireController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create($idBillet)
{
try {
$billet = Billet::findOrFail($idBillet);
}
catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
Log::channel('projectError')->error('Commentaire : Billet non trouvé');
return view('errors.unavailable');
}
catch (\Illuminate\Database\QueryException $e) {
Log::channel('projectError')->error('Erreur accès base de données');
return view('errors.dberror');
}
return view('vCommenter', compact('idBillet', 'billet'));
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreCommentaireRequest $request)
{
try {
Commentaire::create($request->all());
}
catch (\Illuminate\Database\QueryException $e) {
Log::channel('projectError')->error('Insertion en base de données impossible');
return view('errors.dberror');
}
Log::channel('projectInfo')->info('Commentaire ajouté par : '.$request->ip());
return view('vConfirmStore');
}
/**
* Display the specified resource.
*/
public function show(Commentaire $commentaire)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Commentaire $commentaire)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateCommentaireRequest $request, Commentaire $commentaire)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Commentaire $commentaire)
{
//
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreCategorieRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreCommentaireRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'COM_AUTEUR' => ['required', 'alpha', 'max:100'],
'COM_CONTENU' => ['required', 'string', 'max:200'],
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array
*/
public function messages() {
return [
'COM_AUTEUR.required' => 'Le nom de l\'auteur est requis.',
'COM_AUTEUR.alpha' => 'Le nom de l\'auteur ne doit contenir que des lettres.',
'COM_AUTEUR.max' => 'Le nom de l\'auteur ne doit pas dépasser 100 caractères.',
'COM_CONTENU.required' => 'Le contenu du commentaire est requis.',
'COM_CONTENU.string' => 'Le contenu du commentaire doit être une chaîne de caractères.',
'COM_CONTENU.max' => 'Le contenu du commentaire ne doit pas dépasser 200 caractères.',
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCategorieRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCommentaireRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@@ -9,4 +9,22 @@ class Billet extends Model
{ {
/** @use HasFactory<\Database\Factories\BilletFactory> */ /** @use HasFactory<\Database\Factories\BilletFactory> */
use HasFactory; use HasFactory;
protected $fillable = [
'BIL_DATE',
'BIL_TITRE',
'BIL_CONTENU',
'created_at',
'updated_at',
];
public function commentaires()
{
return $this->hasMany(Commentaire::class);
}
public function categories()
{
return $this->belongsToMany(Categorie::class);
}
} }

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Categorie extends Model
{
/** @use HasFactory<\Database\Factories\CategorieFactory> */
use HasFactory;
public function billets()
{
return $this->belongsToMany(Billet::class);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Commentaire extends Model
{
/** @use HasFactory<\Database\Factories\CommentaireFactory> */
use HasFactory;
protected $fillable = [
'COM_DATE',
'COM_AUTEUR',
'COM_CONTENU',
'billet_id',
'created_at',
'updated_at',
];
public function billet()
{
return $this->belongsTo(Billet::class, 'billet_id');
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class billet_categorie extends Model
{
/** @use HasFactory<\Database\Factories\BilletCategorieFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Categorie;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class CategoriePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Categorie $categorie): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Categorie $categorie): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Categorie $categorie): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Categorie $categorie): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Categorie $categorie): bool
{
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Commentaire;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class CommentairePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Commentaire $commentaire): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Commentaire $commentaire): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Commentaire $commentaire): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Commentaire $commentaire): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Commentaire $commentaire): bool
{
return false;
}
}

View File

@@ -127,6 +127,18 @@ return [
'path' => storage_path('logs/laravel.log'), 'path' => storage_path('logs/laravel.log'),
], ],
"projectError" => [
'driver' => 'single',
'path' => storage_path('logs/project.log'),
'level' => 'error',
],
"projectInfo" => [
'driver' => 'single',
'path' => storage_path('logs/project.log'),
'level' => 'info',
],
], ],
]; ];

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\billet_categorie>
*/
class BilletCategorieFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Categorie>
*/
class CategorieFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
'CAT_NOM' => fake()->text(10),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Commentaire>
*/
class CommentaireFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
'COM_DATE' => now(),
'COM_AUTEUR' => fake()->lastName(),
'COM_CONTENU' => fake()->text(200),
'billet_id' => fake()->numberBetween(1,10),
'created_at' => now(),
'updated_at' => now(),
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('commentaires', function (Blueprint $table) {
$table->id();
$table->date('COM_DATE');
$table->text('COM_AUTEUR');
$table->text('COM_CONTENU');
$table->unsignedBigInteger('billet_id');
$table->foreign('billet_id')
->references('id')
->on('billets')
->onDelete('cascade')
->onUpdate('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('commentaires');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->text('CAT_NOM');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('categories');
}
};

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('billet_categorie', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('billet_id');
$table->foreign('billet_id')
->references('id')
->on('billets')
->onDelete('cascade')
->onUpdate('cascade');
$table->unsignedBigInteger('categorie_id');
$table->foreign('categorie_id')
->references('id')
->on('categories')
->onDelete('cascade')
->onUpdate('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('billet_categories');
}
};

View File

@@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Models\Categorie;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class CategorieSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
Categorie::factory(10)->create();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Commentaire;
class CommentaireSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
// Create 10 comments
Commentaire::factory(10)->create();
}
}

View File

@@ -39,4 +39,12 @@ h1 {
#txtCommentaire { #txtCommentaire {
width: 50%; width: 50%;
}
#titreReponses {
font-size: 100%;
}
#txtCommentaire {
width: 50%;
} }

View File

@@ -0,0 +1,4 @@
@extends('layout')
@section('contenu')
<h2>La page demandée n'est pas disponible</h2>
@endsection

View File

@@ -0,0 +1,5 @@
@extends('layout')
@section('contenu')
<h2>Une erreur est survenue. Veuillez réessayer ultérieurement</h2>
@endsection

View File

@@ -0,0 +1,4 @@
@extends('layout')
@section('contenu')
<h2>La ressource demandée n'est pas disponible</h2>
@endsection

View File

@@ -1,31 +1,28 @@
<!doctype html> @extends('layout')
<html lang="fr"> @section('contenu')
<head> @foreach($billets as $billet)
<meta charset="UTF-8" /> <article>
<link rel="stylesheet" href="style.css" />
<title>Mon Blog</title>
</head>
<body>
<div id="global">
<header> <header>
<a href="index.php"><h1 id="titreBlog">Mon Blog</h1></a> <a href="{{ route('billets.show', $billet->id) }}"><h1 class="titreBillet">{{ $billet->BIL_TITRE }}</h1></a>
<p>Je vous souhaite la bienvenue sur ce modeste blog.</p> <time>{{ $billet->BIL_DATE }}</time>
</header> </header>
<div id="contenu"> <p>
@foreach($billets as $billet) Dans :
<article> @php
<header> $counter = 0;
<h1 class="titreBillet">{{ $billet->BIL_TITRE }}</h1> @endphp
<time>{{ $billet->BIL_DATE }}</time> @foreach($billet->categories as $categorie)
</header> {{ $categorie->CAT_NOM }}
<p>{{ $billet->BIL_CONTENU }}</p> @php
</article> $counter++;
<hr /> @endphp
@if(count($billet->categories) > $counter)
/
@endif
@endforeach @endforeach
</div> <!-- #contenu --> </p>
<footer id="piedBlog"> <p>{{ $billet->BIL_CONTENU }}</p>
Blog réalisé avec PHP, HTML5 et CSS. <a href="{{ route('commenter', ['id'=>$billet->id]) }}"><h3 class="titre-billet">Ecrire un commentaire</h3></a>
</footer> </article>
</div> <!-- #global --> @endforeach
</body> @endsection
</html>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<!--link rel="stylesheet" href="style.css" /-->
<link rel="stylesheet" href="{{ URL::asset('style.css') }}" />
<title>Mon Blog</title>
</head>
<body>
<div id="global">
<header>
<a href="{{ route('billets.index') }}"><h1 id="titreBlog">Mon Blog</h1></a>
<p>Je vous souhaite la bienvenue sur ce modeste blog.</p>
</header>
<div id="contenu">
@yield('contenu')
</div> <!-- #contenu -->
<footer id="piedBlog">
Blog réalisé avec PHP, HTML5 et CSS.
</footer>
</div> <!-- #global -->
</body>
</html>

View File

@@ -0,0 +1,24 @@
@extends('layout')
@section('contenu')
<article>
<header>
<h1 class="titreBillet">{{ $billet->BIL_TITRE }}</h1>
<time>{{ $billet->BIL_DATE }}</time>
</header>
<p>{{ $billet->BIL_CONTENU }}</p>
</article>
<hr />
@if (count($commentaires) > 0)
<header>
<h1 id="titreReponses">Réponses à {{ $billet->BIL_TITRE }}</h1>
</header>
@foreach($commentaires as $commentaire)
<p>{{ $commentaire->COM_AUTEUR }} dit :</p>
<p>{{ $commentaire->COM_CONTENU }}</p>
@endforeach
@else
<header>
<h1 id="titreReponses">Il n'y a pas de réponse à ce billet.</h1>
</header>
@endif
@endsection

View File

@@ -0,0 +1,40 @@
@extends('layout')
@section('contenu')
@php
$today = date('Y-m-d');
@endphp
<h2>Commenter le billet : {{ $billet->BIL_TITRE }}</h2>
<form action="{{ route('commentaires.store') }}" method="POST">
@csrf
<p><i>Complétez le formulaire. Les champs marqué par </i><em>*</em> sont <em>obligatoires</em></p>
<fieldset>
<legend>Entrez votre commentaire :</legend>
<div>
<label for="COM_AUTEUR">Nom : <em>*</em></label>
<input type="text" @error('COM_AUTEUR') is-invalid @enderror name="COM_AUTEUR" placeholder="Entrez votre nom"></input>
@error('COM_AUTEUR')
<div><em>{{ $message }}</em></div>
@enderror
</div>
<br>
<div>
<label for="COM_CONTENU">Message : <em>*</em></label>
<textarea name="COM_CONTENU" @error('COM_CONTENU') is-invalid @enderror name="COM_CONTENU" placeholder="Votre commentaire :"></textarea>
@error('COM_CONTENU')
<div><em>{{ $message }}</em></div>
@enderror
</div>
<div>
<input type="hidden" name="COM_DATE" value="{{ $today }}"></input>
</div>
<div>
<input type="hidden" name="created_at" value="{{ $today }}"></input>
</div>
<div>
<input type="hidden" name="billet_id" value="{{ $idBillet }}"></input>
</div>
<br>
<button type="submit">Envoyer !</button>
</fieldset>
</form>
@endsection

View File

@@ -0,0 +1,5 @@
@extends('layout')
@section('contenu')
<h2>Merci ! Nous avons bien enregistré votre commentaire.</h2>
@endsection

View File

@@ -2,9 +2,13 @@
use App\Http\Controllers\BilletController; use App\Http\Controllers\BilletController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CommentaireController;
Route::get('/', function () { Route::get('/', fn() => view('welcome'));
return view('welcome');
});
Route::resource('billets', BilletController::class); Route::resource('billets', BilletController::class);
Route::resource('commentaires', CommentaireController::class);
// Route to create a comment
Route::get('commenter/{id}', [CommentaireController::class, 'create'])->name('commenter');

0
laravel/storage/app/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/app/private/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/app/public/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/framework/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/framework/cache/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/framework/cache/data/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/framework/sessions/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/framework/testing/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/framework/views/.gitignore vendored Normal file → Executable file
View File

0
laravel/storage/logs/.gitignore vendored Normal file → Executable file
View File