diff --git a/src/controllers/API.php b/src/controllers/API.php index 0ab4e29..d5ea393 100644 --- a/src/controllers/API.php +++ b/src/controllers/API.php @@ -4,6 +4,7 @@ namespace Steamy\Controller; +use Exception; use Steamy\Core\Controller; use Steamy\Core\Utility; @@ -26,7 +27,7 @@ public function __construct() } /** - * Checks if root relative url starts with /api/v1 + * Checks if root relative url starts with api/v1 * @return bool */ private function validateURLFormat(): bool @@ -37,17 +38,25 @@ private function validateURLFormat(): bool public function index(): void { if (!$this->validateURLFormat()) { - echo "Invalid API URL: " . $_GET["url"]; + http_response_code(400); die(); } // call appropriate controller to handle resource $controllerClassName = 'Steamy\\Controller\\API\\' . ucfirst($this->resource); + try { + if (class_exists($controllerClassName)) { + (new $controllerClassName())->index(); + } else { + http_response_code(404); + die(); + } + } catch (Exception $e) { + http_response_code(500); + + // Uncomment line below only when testing API + echo $e->getMessage(); - if (class_exists($controllerClassName)) { - (new $controllerClassName())->index(); - } else { - echo "Invalid API resource: " . $this->resource; die(); } } diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index 1b1f78d..978005d 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -4,54 +4,255 @@ namespace Steamy\Controller\API; +use Steamy\Core\Utility; use Steamy\Model\Product; +use Steamy\Core\Model; -/** - * Handles /products route of api - */ class Products { - private function getProducts(): void + use Model; + + /** + * Get the list of all products available in the store. + */ + private function getAllProducts(): void { - $all_products = Product::getAll(); + // Retrieve all products from the database + $allProducts = Product::getAll(); + + // Convert products to array format $result = []; - foreach ($all_products as $product) { + foreach ($allProducts as $product) { $result[] = $product->toArray(); } + + // Return JSON response echo json_encode($result); } - private function addProduct(): void + /** + * Get the details of a specific product by its ID. + */ + private function getProductById(): void + { + $productId = (int)Utility::splitURL()[3]; + + // Retrieve product details from the database + $product = Product::getByID($productId); + + // Check if product exists + if ($product === null) { + // Product not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Product not found']); + return; + } + + // Return JSON response + echo json_encode($product->toArray()); + } + + /** + * Get the list of product categories. + */ + private function getProductCategories(): void + { + // Retrieve all product categories from the database + $categories = Product::getCategories(); + + // Return JSON response + echo json_encode($categories); + } + + /** + * Create a new product entry in the database. + */ + private function createProduct(): void { + // Retrieve POST data + $postData = $_POST; + + // TODO : Use json schema validation here + // Check if required fields are present + $requiredFields = [ + 'name', + 'calories', + 'img_url', + 'img_alt_text', + 'category', + 'price', + 'description' + ]; + + 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 Product object + $newProduct = new Product( + $postData['name'], + (int)$postData['calories'], + $postData['img_url'], + $postData['img_alt_text'], + $postData['category'], + (float)$postData['price'], + $postData['description'] + ); + + // Save the new product to the database + if ($newProduct->save()) { + // Product created successfully, return 201 Created + http_response_code(201); + echo json_encode(['message' => 'Product created successfully', 'product_id' => $newProduct->getProductID()] + ); + } else { + // Failed to create product, return 500 Internal Server Error + http_response_code(500); + echo json_encode(['error' => 'Failed to create product']); + } } + /** + * Delete a product with the specified ID. + */ private function deleteProduct(): void { + $productId = (int)Utility::splitURL()[3]; + + // Retrieve the product by ID + $product = Product::getByID($productId); + + // Check if product exists + if ($product === null) { + // Product not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Product not found']); + return; + } + + // Attempt to delete the product + if ($product->deleteProduct()) { + // Product successfully deleted + http_response_code(204); // No Content + } else { + // Failed to delete the product + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to delete product']); + } } + /** + * Update the details of a product with the specified ID. + */ private function updateProduct(): void { + $productId = (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 product + $product = Product::getByID($productId); + + // Check if product exists + if ($product === null) { + // Product not found + http_response_code(404); // Not Found + echo json_encode(['error' => 'Product not found']); + return; + } + + // Update product in the database + $success = $product->updateProduct($putData); + + if ($success) { + // Product updated successfully + http_response_code(200); // OK + echo json_encode(['message' => 'Product updated successfully']); + } else { + // Failed to update product + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to update product']); + } } + 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. + */ public function index(): void { - switch ($_SERVER['REQUEST_METHOD']) { - case 'GET': - $this->getProducts(); - break; - case 'POST': - $this->addProduct(); - break; - case 'DELETE': - $this->deleteProduct(); - break; - case 'PUT': - $this->updateProduct(); - break; - default: - http_response_code(400); + $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', + ] + ]; + + // 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(); } } -} \ No newline at end of file +} diff --git a/src/core/App.php b/src/core/App.php index 1f50d01..22523d5 100644 --- a/src/core/App.php +++ b/src/core/App.php @@ -18,7 +18,9 @@ class App */ public function exception_handler(Throwable $exception): void { -// echo "Uncaught exception: ", $exception->getMessage(), "\n"; + // Uncomment line below only during development + echo "Uncaught exception: ", $exception->getMessage(), "\n"; + (new Error())->handleUnknownError(); } diff --git a/src/core/Model.php b/src/core/Model.php index 361b366..1d1e2fc 100644 --- a/src/core/Model.php +++ b/src/core/Model.php @@ -116,35 +116,35 @@ protected function insert(array $data, string $table_name = ""): ?int return empty($lastInsertID) ? null : (int)$lastInsertID; } + /** - * Update a record in the table based on the provided ID. - * - * @param int|string $id The value of the primary key (ID) identifying the record to be updated. - * @param array $data An associative array representing the columns and their new values to be updated. - * @param string $table_name Name of table without backticks. Defaults to $this->table. - * @param string $id_column The name of the ID column. Default is 'id'. - * - * @return int Number of rows affected + * @param array $new_data Associative array for SET part of query. + * @param array $condition Associative array representing WHERE condition of query. + * @param string $table_name Defaults to $this->table. + * @return bool True on success. */ - protected function update(int|string $id, array $data, string $table_name, string $id_column = 'id'): int + protected function update(array $new_data, array $condition, string $table_name = ""): bool { $table_name = empty($table_name) ? $this->table : $table_name; - $keys = array_keys($data); $query = "UPDATE `$table_name` SET "; // add placeholders to query - foreach ($keys as $key) { + foreach (array_keys($new_data) as $key) { $query .= $key . " = :" . $key . ","; } + $query = trim($query, ", "); // remove extra comma at the end of query - // remove extra comma at the end of query - $query = trim($query, ", "); - - // add where condition - $query .= " WHERE $id_column = $id;"; + // add conditions + $query .= " WHERE "; + foreach (array_keys($condition) as $key) { + $query .= $key . " = :" . $key . ","; + } + $query = trim($query, ", "); // remove extra comma at the end of query $conn = self::connect(); - return $conn->exec($query); + $stm = $conn->prepare($query); + + return $stm->execute([...$new_data, ...$condition]); } /** @@ -152,12 +152,18 @@ protected function update(int|string $id, array $data, string $table_name, strin * @param mixed $id value of column name in WHERE clause. * @param string $table_name Name of table without backticks. Defaults to $this->table. * @param string $id_column_name primary key of table or name of column in WHERE clause. - * @return void + * @return bool Success or not */ - protected function delete(mixed $id, string $table_name, string $id_column_name = 'id'): void + protected function delete(mixed $id, string $table_name, string $id_column_name = 'id'): bool { $table_name = empty($table_name) ? $this->table : $table_name; $query = "DELETE FROM `$table_name` WHERE $id_column_name = :id"; - self::query($query, array('id' => $id)); + + $con = self::connect(); + $stm = $con->prepare($query); + $success = $stm->execute(['id' => $id]); + $con = null; + + return $success; } } diff --git a/src/models/Product.php b/src/models/Product.php index 365048a..1035c4b 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -260,6 +260,7 @@ public function save(): bool } } + public function getAverageRating(): float { // Query the database to calculate the average rating excluding unverified reviews @@ -275,7 +276,7 @@ public function getAverageRating(): float WHERE op.product_id = r.product_id ) EOL; - + $params = ['product_id' => $this->product_id]; $result = $this->query($query, $params); @@ -289,6 +290,11 @@ public function getAverageRating(): float return 0; // No reviews, return 0 as the average rating } + public function deleteProduct(): bool + { + return $this->delete($this->product_id, $this->table, 'product_id'); + } + public function validate(): array { $errors = []; @@ -410,4 +416,17 @@ public function getRatingDistribution(): array return $distribution; } + /** + * Updates product record in database but does not update the object itself. + * @param array $newProductData Associative array indexed by attribute name. + * The values are the new product data. + * @return bool Success or not + */ + public function updateProduct(array $newProductData): bool + { + // remove product_id (if present) from user data + unset($newProductData['product_id']); + + return $this->update($newProductData, ['product_id' => $this->product_id], $this->table); + } } \ No newline at end of file