├── .htaccess ├── database.sql ├── README.md ├── src ├── Database.php ├── ErrorHandler.php ├── ProductGateway.php └── ProductController.php ├── index.php └── LICENSE /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule . index.php -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE product_db; 2 | 3 | CREATE TABLE product ( 4 | id INT NOT NULL AUTO_INCREMENT, 5 | name VARCHAR(128) NOT NULL, 6 | size INT NOT NULL DEFAULT 0, 7 | is_available BOOLEAN NOT NULL DEFAULT FALSE, 8 | PRIMARY KEY (id) 9 | ); 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create a PHP REST API 2 | 3 | Source code to accompany this video: https://youtu.be/X51KOJKrofU 4 | 5 | [![Create a PHP REST API : Write a RESTful API from Scratch using Plain, Object-Oriented PHP and MySQL](https://img.youtube.com/vi/X51KOJKrofU/0.jpg)](https://youtu.be/X51KOJKrofU) 6 | 7 | ## Topics covered in the video 8 | * Basic REST API routing and URLs 9 | * List, show, create, update and delete database records using a RESTful API 10 | * Best-practice code organisation 11 | * Controllers and table gateways 12 | * Relevant HTTP status codes 13 | * Data validation 14 | * JSON decoding and encoding 15 | 16 | Complete API course, including authentication: https://davehollingworth.net/phpapisy 17 | -------------------------------------------------------------------------------- /src/Database.php: -------------------------------------------------------------------------------- 1 | host};dbname={$this->name};charset=utf8"; 14 | 15 | return new PDO($dsn, $this->user, $this->password, [ 16 | PDO::ATTR_EMULATE_PREPARES => false, 17 | PDO::ATTR_STRINGIFY_FETCHES => false 18 | ]); 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | $exception->getCode(), 11 | "message" => $exception->getMessage(), 12 | "file" => $exception->getFile(), 13 | "line" => $exception->getLine() 14 | ]); 15 | } 16 | 17 | public static function handleError( 18 | int $errno, 19 | string $errstr, 20 | string $errfile, 21 | int $errline 22 | ): bool 23 | { 24 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 25 | } 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | processRequest($_SERVER["REQUEST_METHOD"], $id); 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dave Hollingworth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ProductGateway.php: -------------------------------------------------------------------------------- 1 | conn = $database->getConnection(); 10 | } 11 | 12 | public function getAll(): array 13 | { 14 | $sql = "SELECT * 15 | FROM product"; 16 | 17 | $stmt = $this->conn->query($sql); 18 | 19 | $data = []; 20 | 21 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 22 | 23 | $row["is_available"] = (bool) $row["is_available"]; 24 | 25 | $data[] = $row; 26 | } 27 | 28 | return $data; 29 | } 30 | 31 | public function create(array $data): string 32 | { 33 | $sql = "INSERT INTO product (name, size, is_available) 34 | VALUES (:name, :size, :is_available)"; 35 | 36 | $stmt = $this->conn->prepare($sql); 37 | 38 | $stmt->bindValue(":name", $data["name"], PDO::PARAM_STR); 39 | $stmt->bindValue(":size", $data["size"] ?? 0, PDO::PARAM_INT); 40 | $stmt->bindValue(":is_available", (bool) ($data["is_available"] ?? false), PDO::PARAM_BOOL); 41 | 42 | $stmt->execute(); 43 | 44 | return $this->conn->lastInsertId(); 45 | } 46 | 47 | public function get(string $id): array | false 48 | { 49 | $sql = "SELECT * 50 | FROM product 51 | WHERE id = :id"; 52 | 53 | $stmt = $this->conn->prepare($sql); 54 | 55 | $stmt->bindValue(":id", $id, PDO::PARAM_INT); 56 | 57 | $stmt->execute(); 58 | 59 | $data = $stmt->fetch(PDO::FETCH_ASSOC); 60 | 61 | if ($data !== false) { 62 | $data["is_available"] = (bool) $data["is_available"]; 63 | } 64 | 65 | return $data; 66 | } 67 | 68 | public function update(array $current, array $new): int 69 | { 70 | $sql = "UPDATE product 71 | SET name = :name, size = :size, is_available = :is_available 72 | WHERE id = :id"; 73 | 74 | $stmt = $this->conn->prepare($sql); 75 | 76 | $stmt->bindValue(":name", $new["name"] ?? $current["name"], PDO::PARAM_STR); 77 | $stmt->bindValue(":size", $new["size"] ?? $current["size"], PDO::PARAM_INT); 78 | $stmt->bindValue(":is_available", $new["is_available"] ?? $current["is_available"], PDO::PARAM_BOOL); 79 | 80 | $stmt->bindValue(":id", $current["id"], PDO::PARAM_INT); 81 | 82 | $stmt->execute(); 83 | 84 | return $stmt->rowCount(); 85 | } 86 | 87 | public function delete(string $id): int 88 | { 89 | $sql = "DELETE FROM product 90 | WHERE id = :id"; 91 | 92 | $stmt = $this->conn->prepare($sql); 93 | 94 | $stmt->bindValue(":id", $id, PDO::PARAM_INT); 95 | 96 | $stmt->execute(); 97 | 98 | return $stmt->rowCount(); 99 | } 100 | } 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/ProductController.php: -------------------------------------------------------------------------------- 1 | processResourceRequest($method, $id); 14 | 15 | } else { 16 | 17 | $this->processCollectionRequest($method); 18 | 19 | } 20 | } 21 | 22 | private function processResourceRequest(string $method, string $id): void 23 | { 24 | $product = $this->gateway->get($id); 25 | 26 | if ( ! $product) { 27 | http_response_code(404); 28 | echo json_encode(["message" => "Product not found"]); 29 | return; 30 | } 31 | 32 | switch ($method) { 33 | case "GET": 34 | echo json_encode($product); 35 | break; 36 | 37 | case "PATCH": 38 | $data = (array) json_decode(file_get_contents("php://input"), true); 39 | 40 | $errors = $this->getValidationErrors($data, false); 41 | 42 | if ( ! empty($errors)) { 43 | http_response_code(422); 44 | echo json_encode(["errors" => $errors]); 45 | break; 46 | } 47 | 48 | $rows = $this->gateway->update($product, $data); 49 | 50 | echo json_encode([ 51 | "message" => "Product $id updated", 52 | "rows" => $rows 53 | ]); 54 | break; 55 | 56 | case "DELETE": 57 | $rows = $this->gateway->delete($id); 58 | 59 | echo json_encode([ 60 | "message" => "Product $id deleted", 61 | "rows" => $rows 62 | ]); 63 | break; 64 | 65 | default: 66 | http_response_code(405); 67 | header("Allow: GET, PATCH, DELETE"); 68 | } 69 | } 70 | 71 | private function processCollectionRequest(string $method): void 72 | { 73 | switch ($method) { 74 | case "GET": 75 | echo json_encode($this->gateway->getAll()); 76 | break; 77 | 78 | case "POST": 79 | $data = (array) json_decode(file_get_contents("php://input"), true); 80 | 81 | $errors = $this->getValidationErrors($data); 82 | 83 | if ( ! empty($errors)) { 84 | http_response_code(422); 85 | echo json_encode(["errors" => $errors]); 86 | break; 87 | } 88 | 89 | $id = $this->gateway->create($data); 90 | 91 | http_response_code(201); 92 | echo json_encode([ 93 | "message" => "Product created", 94 | "id" => $id 95 | ]); 96 | break; 97 | 98 | default: 99 | http_response_code(405); 100 | header("Allow: GET, POST"); 101 | } 102 | } 103 | 104 | private function getValidationErrors(array $data, bool $is_new = true): array 105 | { 106 | $errors = []; 107 | 108 | if ($is_new && empty($data["name"])) { 109 | $errors[] = "name is required"; 110 | } 111 | 112 | if (array_key_exists("size", $data)) { 113 | if (filter_var($data["size"], FILTER_VALIDATE_INT) === false) { 114 | $errors[] = "size must be an integer"; 115 | } 116 | } 117 | 118 | return $errors; 119 | } 120 | } 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | --------------------------------------------------------------------------------