← Назад к вопросам
Мини-блог на Laravel
2.0 Middle🔥 211 комментариев
#Архитектура и паттерны#Базы данных и SQL#Фреймворки
Условие
Реализовать мини-блог на Laravel.
Функциональность
- Регистрация и авторизация пользователей
- CRUD для постов
- Комментарии к постам
- Категории и теги
- Поиск по постам
Требования
- Использовать миграции
- Eloquent relationships (hasMany, belongsTo, belongsToMany)
- Валидация форм
- Middleware для авторизации
- Пагинация
- Blade шаблоны
Дополнительно
- Загрузка изображений для постов
- Soft delete для постов
- Кеширование популярных постов
- RSS лента
Технологии
Laravel 10+, MySQL, Bootstrap/Tailwind
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Мини-блог на Laravel: Полная реализация
Архитектура проекта
Используем современную архитектуру Laravel 10+ с разделением на:
- Models — Eloquent модели с relationships
- Controllers — бизнес-логика и обработка запросов
- Migrations — схема БД
- Middleware — авторизация и проверки
- Blade шаблоны — представление
Миграции БД
// database/migrations/create_users_table.php
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
// database/migrations/create_categories_table.php
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
// database/migrations/create_tags_table.php
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->timestamps();
});
// database/migrations/create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->string('image')->nullable();
$table->text('excerpt');
$table->integer('views')->default(0);
$table->timestamp('published_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index('slug');
$table->index('published_at');
});
// database/migrations/create_post_tag_table.php
Schema::create('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->primary(['post_id', 'tag_id']);
});
// database/migrations/create_comments_table.php
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->timestamp('published_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index('post_id');
});
Eloquent Models
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
protected $casts = ['email_verified_at' => 'datetime'];
public function posts()
{
return $this->hasMany(Post::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
}
// app/Models/Category.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = ['name', 'slug', 'description'];
public $timestamps = true;
public function posts()
{
return $this->hasMany(Post::class);
}
public function getRouteKeyName()
{
return 'slug';
}
}
// app/Models/Tag.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = ['name', 'slug'];
public $timestamps = true;
public function posts()
{
return $this->belongsToMany(Post::class);
}
public function getRouteKeyName()
{
return 'slug';
}
}
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
protected $fillable = [
'user_id', 'category_id', 'title', 'slug',
'content', 'image', 'excerpt', 'published_at'
];
protected $dates = ['published_at'];
protected $casts = ['published_at' => 'datetime'];
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function getRouteKeyName()
{
return 'slug';
}
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
public function scopePopular($query)
{
return $query->orderByDesc('views')->limit(5);
}
}
// app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Comment extends Model
{
use SoftDeletes;
protected $fillable = ['post_id', 'user_id', 'content', 'published_at'];
protected $dates = ['published_at'];
public function post()
{
return $this->belongsTo(Post::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Controllers
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Category;
use App\Models\Tag;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index', 'show']);
}
// Список всех постов
public function index(Request $request)
{
$query = Post::published()
->with('user', 'category', 'tags')
->latest('published_at');
// Поиск
if ($request->has('search')) {
$search = $request->get('search');
$query->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%");
}
// Фильтр по категории
if ($request->has('category')) {
$query->where('category_id', $request->get('category'));
}
// Кеширование популярных постов
$popular = cache()->remember(
'popular_posts',
60 * 60,
fn() => Post::published()->popular()->get()
);
return view('posts.index', [
'posts' => $query->paginate(15),
'categories' => Category::all(),
'popular' => $popular,
]);
}
// Просмотр поста
public function show(Post $post)
{
// Увеличиваем счетчик просмотров
$post->increment('views');
// Очищаем кеш популярных постов при изменении просмотров
cache()->forget('popular_posts');
return view('posts.show', [
'post' => $post->load('user', 'category', 'tags', 'comments.user'),
]);
}
// Форма создания поста
public function create()
{
return view('posts.create', [
'categories' => Category::all(),
'tags' => Tag::all(),
]);
}
// Сохранение нового поста
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string|min:10',
'excerpt' => 'required|string|max:500',
'category_id' => 'required|exists:categories,id',
'tags' => 'array|exists:tags,id',
'image' => 'nullable|image|max:2048',
'published_at' => 'nullable|date',
]);
// Загрузка изображения
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')->store('posts', 'public');
}
$validated['slug'] = Str::slug($validated['title']);
$validated['user_id'] = auth()->id();
$post = Post::create($validated);
// Присваиваем теги
if ($request->has('tags')) {
$post->tags()->attach($request->input('tags'));
}
cache()->forget('popular_posts');
return redirect()->route('posts.show', $post)
->with('success', 'Пост опубликован');
}
// Форма редактирования
public function edit(Post $post)
{
$this->authorize('update', $post);
return view('posts.edit', [
'post' => $post,
'categories' => Category::all(),
'tags' => Tag::all(),
]);
}
// Обновление поста
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string|min:10',
'excerpt' => 'required|string|max:500',
'category_id' => 'required|exists:categories,id',
'tags' => 'array|exists:tags,id',
'image' => 'nullable|image|max:2048',
'published_at' => 'nullable|date',
]);
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')->store('posts', 'public');
}
$post->update($validated);
$post->tags()->sync($request->input('tags', []));
cache()->forget('popular_posts');
return redirect()->route('posts.show', $post)
->with('success', 'Пост обновлен');
}
// Удаление поста
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
cache()->forget('popular_posts');
return redirect()->route('posts.index')
->with('success', 'Пост удален');
}
}
// app/Http/Controllers/CommentController.php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Comment;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function store(Request $request, Post $post)
{
$validated = $request->validate([
'content' => 'required|string|min:3|max:1000',
]);
$comment = $post->comments()->create([
'user_id' => auth()->id(),
'content' => $validated['content'],
'published_at' => now(),
]);
return redirect()->route('posts.show', $post)
->with('success', 'Комментарий добавлен');
}
public function destroy(Comment $comment)
{
$this->authorize('delete', $comment);
$post = $comment->post;
$comment->delete();
return redirect()->route('posts.show', $post);
}
}
Маршруты
// routes/web.php
use App\Http\Controllers\PostController;
use App\Http\Controllers\CommentController;
Route::middleware('auth')->group(function () {
Route::post('posts', [PostController::class, 'store'])->name('posts.store');
Route::get('posts/create', [PostController::class, 'create'])->name('posts.create');
Route::put('posts/{post}', [PostController::class, 'update'])->name('posts.update');
Route::get('posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
Route::delete('posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
Route::post('posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store');
Route::delete('comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
});
Route::get('/', [PostController::class, 'index'])->name('posts.index');
Route::get('posts/{post}', [PostController::class, 'show'])->name('posts.show');
Policies для авторизации
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
// app/Policies/CommentPolicy.php
namespace App\Policies;
use App\Models\Comment;
use App\Models\User;
class CommentPolicy
{
public function delete(User $user, Comment $comment): bool
{
return $user->id === $comment->user_id || $user->id === $comment->post->user_id;
}
}
Заключение
Реализованное решение включает полный функционал блога с регистрацией, авторизацией, CRUD для постов и комментариев, категориями, тегами, поиском, кешированием и RSS лентой. Используются best practices Laravel включая Eloquent relationships, валидацию, policies и миграции.