← Назад к вопросам

Мини-блог на 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 и миграции.