Files
web_work/app/Controllers/ReviewController.php
kirill.khorkov a4092adf2e feat: Add complete reviews system with star ratings
 New Features:
- Reviews system with 1-5 star ratings
- User can add, edit, and delete their own reviews
- One review per product per user (DB constraint)
- Automatic average rating calculation
- Review count tracking
- Interactive star selection UI
- AJAX-powered review submission
- Responsive design for all devices

🗄️ Database:
- New 'reviews' table with full structure
- Added 'rating' and 'review_count' fields to products
- PostgreSQL triggers for automatic rating updates
- Database functions for rating calculations
- Indexes for performance optimization

📦 Backend (PHP):
- Review model with 15+ methods
- ReviewController with 5 actions
- Updated Product model to include ratings
- Updated ProductController to load reviews
- 5 new API endpoints

🎨 Frontend:
- Reviews list component (_reviews_list.php)
- Review form component (_review_form.php)
- Reviews sechow page
- Star ratings in catalog view
- Interactive JavaScript (200+ lines)
- Adaptive styles (400+ lines)

🔒 Security:
- Server-side authorization checks
- XSS protection (htmlspecialchars)
- SQL injection protection (PDO prepared)
- Input validation (client + server)
- Access control for review editing

📝 Modified Files:
- app/Models/Product.php - added rating fields to queries
- app/Controllers/ProductController.php - loads reviews
- app/Views/products/show.php - reviews section
- app/Views/products/catalog.php - star ratings
- config/routes.php - review endpoints
- public/style_for_cite.less - rating styles

🆕 New Files:
- app/Models/Review.php
- app/Controllers/ReviewController.php
- app/Views/products/_reviews_list.php
- app/Views/products/_review_form.php
2026-01-06 17:04:09 +03:00

299 lines
8.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Review;
use App\Models\Product;
class ReviewController extends Controller
{
private Review $reviewModel;
private Product $productModel;
public function __construct()
{
$this->reviewModel = new Review();
$this->productModel = new Product();
}
/**
* Create a new review (POST /reviews)
*/
public function create(): void
{
// Require authentication
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
], 401);
return;
}
// Admins cannot leave reviews
if ($this->isAdmin()) {
$this->json([
'success' => false,
'message' => 'Администраторы не могут оставлять отзывы'
], 403);
return;
}
$productId = (int) $this->getPost('product_id', 0);
$rating = (int) $this->getPost('rating', 0);
$comment = trim($this->getPost('comment', ''));
$user = $this->getCurrentUser();
// Validate product ID
if ($productId <= 0) {
$this->json([
'success' => false,
'message' => 'Неверный ID товара'
], 400);
return;
}
// Check if product exists
$product = $this->productModel->find($productId);
if (!$product) {
$this->json([
'success' => false,
'message' => 'Товар не найден'
], 404);
return;
}
// Validate rating
if ($rating < 1 || $rating > 5) {
$this->json([
'success' => false,
'message' => 'Рейтинг должен быть от 1 до 5'
], 400);
return;
}
// Check if user can review
if (!$this->reviewModel->userCanReview($user['id'], $productId)) {
$this->json([
'success' => false,
'message' => 'Вы уже оставили отзыв на этот товар'
], 400);
return;
}
// Create review
$reviewId = $this->reviewModel->createReview([
'product_id' => $productId,
'user_id' => $user['id'],
'rating' => $rating,
'comment' => $comment,
'is_approved' => true
]);
if ($reviewId) {
// Get updated product info
$updatedProduct = $this->productModel->find($productId);
$this->json([
'success' => true,
'message' => 'Спасибо за ваш отзыв!',
'review_id' => $reviewId,
'product_rating' => $updatedProduct['rating'] ?? 0,
'product_review_count' => $updatedProduct['review_count'] ?? 0
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при создании отзыва'
], 500);
}
}
/**
* Update a review (POST /reviews/{id})
*/
public function update(int $id): void
{
// Require authentication
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
], 401);
return;
}
$user = $this->getCurrentUser();
$review = $this->reviewModel->find($id);
if (!$review) {
$this->json([
'success' => false,
'message' => 'Отзыв не найден'
], 404);
return;
}
// Check ownership (only owner or admin can update)
if ($review['user_id'] !== $user['id'] && !$this->isAdmin()) {
$this->json([
'success' => false,
'message' => 'У вас нет прав для редактирования этого отзыва'
], 403);
return;
}
$rating = (int) $this->getPost('rating', 0);
$comment = trim($this->getPost('comment', ''));
// Validate rating
if ($rating < 1 || $rating > 5) {
$this->json([
'success' => false,
'message' => 'Рейтинг должен быть от 1 до 5'
], 400);
return;
}
// Update review
$success = $this->reviewModel->updateReview($id, [
'rating' => $rating,
'comment' => $comment
]);
if ($success) {
// Get updated product info
$updatedProduct = $this->productModel->find($review['product_id']);
$this->json([
'success' => true,
'message' => 'Отзыв обновлен',
'product_rating' => $updatedProduct['rating'] ?? 0,
'product_review_count' => $updatedProduct['review_count'] ?? 0
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при обновлении отзыва'
], 500);
}
}
/**
* Delete a review (POST /reviews/{id}/delete)
*/
public function delete(int $id): void
{
// Require authentication
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
], 401);
return;
}
$user = $this->getCurrentUser();
$review = $this->reviewModel->find($id);
if (!$review) {
$this->json([
'success' => false,
'message' => 'Отзыв не найден'
], 404);
return;
}
// Check ownership (only owner or admin can delete)
if ($review['user_id'] !== $user['id'] && !$this->isAdmin()) {
$this->json([
'success' => false,
'message' => 'У вас нет прав для удаления этого отзыва'
], 403);
return;
}
$productId = $review['product_id'];
$success = $this->reviewModel->deleteReview($id);
if ($success) {
// Get updated product info
$updatedProduct = $this->productModel->find($productId);
$this->json([
'success' => true,
'message' => 'Отзыв удален',
'product_rating' => $updatedProduct['rating'] ?? 0,
'product_review_count' => $updatedProduct['review_count'] ?? 0
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при удалении отзыва'
], 500);
}
}
/**
* Get reviews for a product (GET /reviews/product/{id})
*/
public function getByProduct(int $productId): void
{
$product = $this->productModel->find($productId);
if (!$product) {
$this->json([
'success' => false,
'message' => 'Товар не найден'
], 404);
return;
}
$reviews = $this->reviewModel->getByProduct($productId, true);
$distribution = $this->reviewModel->getRatingDistribution($productId);
$this->json([
'success' => true,
'reviews' => $reviews,
'rating_distribution' => $distribution,
'average_rating' => $product['rating'] ?? 0,
'total_reviews' => $product['review_count'] ?? 0
]);
}
/**
* Toggle review approval (admin only) (POST /reviews/{id}/toggle-approval)
*/
public function toggleApproval(int $id): void
{
$this->requireAdmin();
$review = $this->reviewModel->find($id);
if (!$review) {
$this->json([
'success' => false,
'message' => 'Отзыв не найден'
], 404);
return;
}
$success = $this->reviewModel->toggleApproval($id);
if ($success) {
$updatedReview = $this->reviewModel->find($id);
$this->json([
'success' => true,
'message' => 'Статус отзыва изменен',
'is_approved' => $updatedReview['is_approved']
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при изменении статуса'
], 500);
}
}
}