diff --git a/src/controllers/API.php b/src/controllers/API.php index d5ea393..84f387f 100644 --- a/src/controllers/API.php +++ b/src/controllers/API.php @@ -21,8 +21,12 @@ class API public function __construct() { + // Set the Content-Type header to application/json header("Content-Type:application/json"); + // Allow access from any origin (CORS) + header('Access-Control-Allow-Origin: *'); + $this->resource = Utility::splitURL()[2] ?? ""; } @@ -32,32 +36,83 @@ public function __construct() */ private function validateURLFormat(): bool { - return preg_match("/^api\/v1/", $_GET["url"]) > 0; + return preg_match("/^api\/v1/", Utility::getURL()) > 0; + } + + + /** + * Returns the name of function responsible for handling the current request, as defined by the $routes variable. + * @param string $controllerName class name of controller + * @return string|null + */ + private function getHandler(string $controllerName): ?string + { + $all_routes = $controllerName::$routes; + + // check if there are handlers defined for current request method + $my_routes = $all_routes[$_SERVER['REQUEST_METHOD']] ?? ""; + if (empty($my_routes)) { + return null; + } + + foreach ($my_routes as $route => $handler) { + $pattern = str_replace('/', '\/', $route); // Convert to regex pattern + $pattern = preg_replace( + '/\{([a-zA-Z0-9_]+)\}/', + '(?P<$1>[^\/]+)', + $pattern + ); // Replace placeholders with regex capture groups + $pattern = '/^' . $pattern . '$/'; + + if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { + return $handler; + } + } + return null; } public function index(): void { if (!$this->validateURLFormat()) { http_response_code(400); - die(); + return; } - // call appropriate controller to handle resource + // check if there is a controller to handle resource $controllerClassName = 'Steamy\\Controller\\API\\' . ucfirst($this->resource); + if (!class_exists($controllerClassName)) { + // no controller available + http_response_code(404); + echo 'Invalid resource: ' . $this->resource; // comment this line for production + return; + } + + // determine which function to call in the controller to handle route + $functionName = $this->getHandler($controllerClassName); + if ($functionName === null) { + // Controller does not have any method defined for route + http_response_code(404); + echo "Request has not been defined in \$routes for " . $controllerClassName; + return; + } + + $controller = new $controllerClassName(); + + if (!method_exists($controller, $functionName)) { + // handle function not found in controller + http_response_code(500); + echo $controllerClassName . ' does not have a public method ' . $functionName; + return; + } + + // call function in controller for handling request try { - if (class_exists($controllerClassName)) { - (new $controllerClassName())->index(); - } else { - http_response_code(404); - die(); - } + call_user_func(array($controller, $functionName)); } catch (Exception $e) { http_response_code(500); // Uncomment line below only when testing API echo $e->getMessage(); - - die(); } } } diff --git a/src/controllers/api/Districts.php b/src/controllers/api/Districts.php index 7d3ed3a..ea0f610 100644 --- a/src/controllers/api/Districts.php +++ b/src/controllers/api/Districts.php @@ -12,10 +12,17 @@ class Districts { use Model; + public static array $routes = [ + 'GET' => [ + '/api/v1/districts' => 'getAllDistricts', + '/api/v1/districts/{id}' => 'getDistrictById', + ] + ]; + /** * Get the list of all districts available. */ - private function getAllDistricts(): void + public function getAllDistricts(): void { // Retrieve all districts from the database $allDistricts = District::getAll(); @@ -36,7 +43,7 @@ private function getAllDistricts(): void /** * Get the details of a specific district by its ID. */ - private function getDistrictById(): void + public function getDistrictById(): void { $districtId = (int)Utility::splitURL()[3]; @@ -57,55 +64,4 @@ private function getDistrictById(): void 'name' => $district->getName() ]); } - - private function getHandler($routes): ?string - { - foreach ($routes[$_SERVER['REQUEST_METHOD']] as $route => $handler) { - $pattern = str_replace('/', '\/', $route); // Convert to regex pattern - $pattern = preg_replace( - '/\{([a-zA-Z0-9_]+)\}/', - '(?P<$1>[^\/]+)', - $pattern - ); // Replace placeholders with regex capture groups - $pattern = '/^' . $pattern . '$/'; - - if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { - return $handler; - } - } - return null; - } - - /** - * Main entry point for the Districts API. - */ - public function index(): void - { - $routes = [ - 'GET' => [ - '/api/v1/districts' => 'getAllDistricts', - '/api/v1/districts/{id}' => 'getDistrictById', - ] - ]; - - // Handle the request - $handler = $this->getHandler($routes); - - if ($handler !== null) { - $functionName = $handler; - if (method_exists($this, $functionName)) { - call_user_func(array($this, $functionName)); - } else { - // Handle function not found - http_response_code(404); - echo "Function Not Found"; - die(); - } - } else { - // Handle route not found - http_response_code(404); - echo "Route Not Found"; - die(); - } - } } diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index 978005d..c973359 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -12,10 +12,28 @@ class Products { use Model; + public static array $routes = [ + 'GET' => [ + '/api/v1/products' => 'getAllProducts', + '/api/v1/products/categories' => 'getProductCategories', + '/api/v1/products/{id}' => 'getProductById', + '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', + ], + 'POST' => [ + '/api/v1/products' => 'createProduct', + ], + 'PUT' => [ + '/api/v1/products/{id}' => 'updateProduct', + ], + 'DELETE' => [ + '/api/v1/products/{id}' => 'deleteProduct', + ] + ]; + /** * Get the list of all products available in the store. */ - private function getAllProducts(): void + public function getAllProducts(): void { // Retrieve all products from the database $allProducts = Product::getAll(); @@ -33,7 +51,7 @@ private function getAllProducts(): void /** * Get the details of a specific product by its ID. */ - private function getProductById(): void + public function getProductById(): void { $productId = (int)Utility::splitURL()[3]; @@ -55,7 +73,7 @@ private function getProductById(): void /** * Get the list of product categories. */ - private function getProductCategories(): void + public function getProductCategories(): void { // Retrieve all product categories from the database $categories = Product::getCategories(); @@ -67,7 +85,7 @@ private function getProductCategories(): void /** * Create a new product entry in the database. */ - private function createProduct(): void + public function createProduct(): void { // Retrieve POST data $postData = $_POST; @@ -126,7 +144,7 @@ private function createProduct(): void /** * Delete a product with the specified ID. */ - private function deleteProduct(): void + public function deleteProduct(): void { $productId = (int)Utility::splitURL()[3]; @@ -155,7 +173,7 @@ private function deleteProduct(): void /** * Update the details of a product with the specified ID. */ - private function updateProduct(): void + public function updateProduct(): void { $productId = (int)Utility::splitURL()[3]; @@ -195,64 +213,22 @@ private function updateProduct(): void } } - private function getHandler($routes): ?string - { - foreach ($routes[$_SERVER['REQUEST_METHOD']] as $route => $handler) { - $pattern = str_replace('/', '\/', $route); // Convert to regex pattern - $pattern = preg_replace( - '/\{([a-zA-Z0-9_]+)\}/', - '(?P<$1>[^\/]+)', - $pattern - ); // Replace placeholders with regex capture groups - $pattern = '/^' . $pattern . '$/'; - - if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { - return $handler; - } - } - return null; - } - /** - * Main entry point for the Products API. + * Get all reviews for a particular product by its ID. */ - public function index(): void + public function getAllReviewsForProduct(): void { - $routes = [ - 'GET' => [ - '/api/v1/products' => 'getAllProducts', - '/api/v1/products/categories' => 'getProductCategories', - '/api/v1/products/{id}' => 'getProductById', - ], - 'POST' => [ - '/api/v1/products' => 'createProduct', - ], - 'PUT' => [ - '/api/v1/products/{id}' => 'updateProduct', - ], - 'DELETE' => [ - '/api/v1/products/{id}' => 'deleteProduct', - ] - ]; + // Get product ID from URL + $productId = (int)Utility::splitURL()[3]; - // Handle the request - $handler = $this->getHandler($routes); - - if ($handler !== null) { - $functionName = $handler; - if (method_exists($this, $functionName)) { - call_user_func(array($this, $functionName)); - } else { - // Handle function not found - http_response_code(404); - echo "Function Not Found"; - die(); - } - } else { - // Handle route not found - http_response_code(404); - echo "Route Not Found"; - die(); - } + // Instantiate the Reviews controller + $reviewsController = new Reviews(); + + // Call the method to get all reviews for the specified product + // Since the Reviews controller method expects the ID to be in the URL, we'll set it directly + $_SERVER['REQUEST_URI'] = "/api/v1/products/$productId/reviews"; + + // Call the method from the Reviews controller + $reviewsController->getAllReviewsForProduct(); } } diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php new file mode 100644 index 0000000..892735f --- /dev/null +++ b/src/controllers/api/Reviews.php @@ -0,0 +1,221 @@ + [ + '/api/v1/reviews' => 'getAllReviews', + '/api/v1/reviews/{id}' => 'getReviewByID', + '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', + ], + 'POST' => [ + '/api/v1/reviews' => 'createReview', + ], + 'PUT' => [ + '/api/v1/reviews/{id}' => 'updateReview', + ], + 'DELETE' => [ + '/api/v1/reviews/{id}' => 'deleteReview', + ] + ]; + + /** + * Get the list of all reviews available. + */ + public function getAllReviews(): void + { + // Retrieve all reviews from the database + $allReviews = Review::getAll(); + + // Convert reviews to array format + $result = []; + foreach ($allReviews as $Review) { + $result[] = $Review->toArray(); + } + + // Return JSON response + echo json_encode($result); + } + + public function getReviewByID(): void + { + $id = (int)Utility::splitURL()[3]; + + // Retrieve all reviews from the database + $review = Review::getByID($id); + + // Check if product exists + if ($review === null) { + // review not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Review not found']); + return; + } + + // Return JSON response + echo json_encode($review->toArray()); + } + + + /** + * Get all reviews for a particular product by its ID. + */ + public function getAllReviewsForProduct(): void + { + $productId = (int)Utility::splitURL()[3]; + // Check if product exists + if (ProductModel::getById($productId) === null) { + // product not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Product not found']); + return; + } + + // Retrieve all reviews for the specified product from the database + $reviews = Review::getAllReviewsForProduct($productId); + + // Return JSON response + echo json_encode($reviews); + } + + /** + * Create a new review for a product. + */ + public function createReview(): void + { + // Retrieve POST data + $postData = $_POST; + + // TODO: Implement validation for required fields and data types + // Check if required fields are present + $requiredFields = [ + 'product_id', + 'client_id', + 'text', + 'rating', + ]; + + if (empty($postData)) { + http_response_code(400); + echo json_encode(['error' => "Missing fields: " . implode(', ', $requiredFields)]); + return; + } + + foreach ($requiredFields as $field) { + if (empty($postData[$field])) { + // Required field is missing, return 400 Bad Request + http_response_code(400); + echo json_encode(['error' => "Missing required field: $field"]); + return; + } + } + // Create a new Review object + $newReview = new Review( + null, // review_id will be auto-generated + (int)$postData['product_id'], + (int)$postData['client_id'], + $postData['text'], + (int)$postData['rating'] + ); + + $errors = $newReview->validate(); + + if (!empty($errors)) { + http_response_code(400); + echo json_encode(['error' => ($errors)]); + return; + } + + // Save the new review to the database + if ($newReview->save()) { + // Review created successfully, return 201 Created + http_response_code(201); + echo json_encode(['message' => 'Review created successfully', 'review_id' => $newReview->getReviewID()]); + } else { + // Failed to create review, return 500 Internal Server Error + http_response_code(500); + echo json_encode(['error' => 'Failed to create review']); + } + } + + /** + * Update the details of a review with the specified ID. + */ + public function updateReview(): void + { + $reviewId = (int)Utility::splitURL()[3]; + + // Retrieve PUT request data + $putData = json_decode(file_get_contents("php://input"), true); + + // Check if PUT data is valid + if (empty($putData)) { + // Invalid JSON data + http_response_code(400); // Bad Request + echo json_encode(['error' => 'Invalid JSON data']); + return; + } + + // Retrieve existing review + $review = Review::getByID($reviewId); + + // Check if review exists + if ($review === null) { + // Review not found + http_response_code(404); // Not Found + echo json_encode(['error' => 'Review not found']); + return; + } + + // Update review in the database + $success = $review->updateReview($putData); + + if ($success) { + // Review updated successfully + http_response_code(200); // OK + echo json_encode(['message' => 'Review updated successfully']); + } else { + // Failed to update review + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to update review']); + } + } + + /** + * Delete a review with the specified ID. + */ + public function deleteReview(): void + { + $reviewId = (int)Utility::splitURL()[3]; + + // Retrieve the review by ID + $review = Review::getByID($reviewId); + + // Check if review exists + if ($review === null) { + // Review not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Review not found']); + return; + } + + // Attempt to delete the review + if ($review->deleteReview($reviewId)) { + // Review successfully deleted + http_response_code(204); // No Content + } else { + // Failed to delete the review + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to delete review']); + } + } +} diff --git a/src/controllers/api/Sessions.php b/src/controllers/api/Sessions.php index 48d559c..537c7c3 100644 --- a/src/controllers/api/Sessions.php +++ b/src/controllers/api/Sessions.php @@ -11,7 +11,14 @@ */ class Sessions { - private function handleLogin(): void + + public static array $routes = [ + 'POST' => [ + '/api/v1/products' => 'handleLogin', + ] + ]; + + public function handleLogin(): void { $email = trim($_POST['email'] ?? ""); $password = trim($_POST['password'] ?? ""); @@ -39,16 +46,4 @@ private function handleLogin(): void $_SESSION['admin_email'] = $email; session_regenerate_id(); } - - public function index(): void - { - switch ($_SERVER['REQUEST_METHOD']) { - case 'POST': - $this->handleLogin(); - break; - default: - http_response_code(400); - die(); - } - } } \ No newline at end of file diff --git a/src/models/Review.php b/src/models/Review.php index fb74c64..f45d3e9 100644 --- a/src/models/Review.php +++ b/src/models/Review.php @@ -13,6 +13,7 @@ class Review { use Model; + protected string $table = 'review'; private int $review_id; private int $product_id; @@ -88,6 +89,79 @@ public static function getByID(int $review_id): ?Review ); } + /** + * Retrieve all reviews from the database. + * + * @return array Array of Review objects representing all reviews in the database. + */ + public static function getAll(): array + { + // Prepare and execute SQL query to retrieve all reviews + $query = "SELECT * FROM review"; + $results = self::query($query); + + if (!$results) { + return []; + } + + // Fetch all reviews as Review objects + $reviews = []; + foreach ($results as $result) { + $obj = new Review( + product_id: $result->product_id, + client_id: $result->client_id, + text: $result->text, + rating: $result->rating, + created_date: Utility::stringToDate($result->created_date) + ); + $obj->setReviewID($result->review_id); + $reviews[] = $obj; + } + + return $reviews; + } + + /** + * Retrieves all reviews for a particular product from the database. + * + * @param int $productId The ID of the product. + * @return array An array containing all reviews for the specified product. + */ + public static function getAllReviewsForProduct(int $productId): array + { + $query = "SELECT * FROM review WHERE product_id = :product_id"; + $params = ['product_id' => $productId]; + $result = self::query($query, $params); + return $result ?: []; + } + + /** + * Updates review record in database but does not update the object itself. + * @param array $newReviewData Associative array indexed by attribute name. + * The values are the new review data. + * @return bool Success or not + */ + public function updateReview(array $newReviewData): bool + { + // remove review_id (if present) from user data + unset($newReviewData['review_id']); + + return $this->update($newReviewData, ['review_id' => $this->review_id], $this->table); + } + + /** + * Deletes a review with the specified ID from the database. + * + * @param int $reviewId The ID of the review to delete. + * @return bool True if the deletion was successful, false otherwise. + */ + public static function deleteReview(int $reviewId): bool + { + $query = "DELETE FROM review WHERE review_id = :review_id"; + $params = ['review_id' => $reviewId]; + return self::query($query, $params); + } + public function getReviewID(): int { return $this->review_id; @@ -272,7 +346,7 @@ public function getNestedComments(): array // Order the children array of each comment by created_date foreach ($commentMap as $comment) { - usort($comment->children, function($a, $b) { + usort($comment->children, function ($a, $b) { return strtotime($a->created_date) - strtotime($b->created_date); }); }