├── .env.dist ├── .htaccess ├── composer.json ├── example-web-app-REST-API-architecture.svg ├── index.php ├── license.md ├── readme.md └── src ├── AllowCors.php ├── Dal ├── FoodItemDal.php ├── TokenKeyDal.php └── UserDal.php ├── Entity ├── Entitable.php ├── Item.php └── User.php ├── Route ├── Exception │ └── NotFoundException.php ├── Http.php ├── food-item.routes.php ├── not-found.routes.php ├── routes.php └── user.routes.php ├── Service ├── Exception │ ├── CannotLoginUserException.php │ ├── CredentialsInvalidException.php │ └── EmailExistsException.php ├── FoodItem.php ├── SecretKey.php └── User.php ├── Validation ├── Exception │ └── InvalidValidationException.php └── UserValidation.php ├── config ├── config.inc.php └── database.inc.php └── helpers ├── headers.inc.php └── misc.inc.php /.env.dist: -------------------------------------------------------------------------------- 1 | # API URL 2 | APP_URL="http://localhost:8080" 3 | 4 | # JSON Web Token 5 | JWT_TOKEN_EXPIRATION="86400" # in seconds 6 | JWT_ALGO_ENCRYPTION="HS512" 7 | 8 | # Database details 9 | DB_HOST="" 10 | DB_NAME="" 11 | DB_USER="" 12 | DB_PASS="" 13 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # DirectoryIndex index.php 2 | 3 | 4 | 5 | # Disable Directory Browsing 6 | Options -MultiViews -Indexes 7 | 8 | Options +FollowSymLinks 9 | 10 | # Indicate to Apache to enable URLs rewrite 11 | RewriteEngine On 12 | 13 | # Uncomment below "#RewriteBase /"if installed in a folder, and then, add the folder name after the slash "/" 14 | # RewriteBase / 15 | 16 | # This is where the magic happens 17 | # Rewrite the requests to index.php with correct GET params 18 | RewriteRule ^([^/]+)/?([^/]+)?/?([^/]+)?/?$ index.php?resource=$1&action=$2&id=$3 [QSA,L] 19 | 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pierrehenry/api-simple-menu", 3 | "description": "A simple RESTful API for a Udemy course", 4 | "homepage": "https://www.udemy.com/course/build-modern-php-api/", 5 | "autoload": { 6 | "psr-4": { 7 | "PH7\\ApiSimpleMenu\\": "src/" 8 | }, 9 | "files": [ 10 | "src/helpers/headers.inc.php", 11 | "src/helpers/misc.inc.php", 12 | "src/config/config.inc.php", 13 | "src/config/database.inc.php", 14 | "src/Route/routes.php" 15 | ] 16 | }, 17 | "authors": [ 18 | { 19 | "name": "Pierre-Henry Soria", 20 | "email": "hi@ph7.me" 21 | } 22 | ], 23 | 24 | "config": { 25 | "platform": { 26 | "php": "8.2.0" 27 | } 28 | }, 29 | "require": { 30 | "php": ">=8.2.0", 31 | "firebase/php-jwt": "^6.4", 32 | "respect/validation": "^2.2", 33 | "ph-7/php-http-response-header": "^1.0.2", 34 | "ph-7/just-http-status-codes": "^1.1", 35 | "ramsey/uuid": "^4.7", 36 | "gabordemooij/redbean": "^5.7", 37 | "vlucas/phpdotenv": "^5.5", 38 | "filp/whoops": "^2.15" 39 | }, 40 | "require-dev": { 41 | "phpunit/phpunit": "^10.2" 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/phpunit" 45 | }, 46 | "license": "MIT" 47 | } 48 | -------------------------------------------------------------------------------- /example-web-app-REST-API-architecture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | The end-user app +website + devices (smart watches, IoT, etc)PHP REST APIUsersMySQL DatabaseSource Codestored in GitHubDevicePHPMySQLJSON -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | pushHandler(new WhoopsJsonResponseHandler); 12 | $whoops->register(); 13 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | **Copyright (c) 2023 Pierre-Henry Soria** 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 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Build a PHP RESTful API 2 | 3 | This is the source code of my Udemy course **[Build a Modern REST API with PHP 8.2](https://www.udemy.com/course/build-modern-php-api/)** 🚀 4 | 5 | ![Diagram showing example of REST API architecture](example-web-app-REST-API-architecture.svg) 6 | 7 | 8 | ## The course 9 | 10 | It is indispensable to follow [Build from Scratch a Modern REST API](https://www.udemy.com/course/build-modern-php-api/) in order to understand and setup properly this project. 11 | 12 | 13 | ## Requirements 14 | 15 | 1. Enroll [Build from Scratch a Modern REST API with PHP 8](https://www.udemy.com/course/build-modern-php-api/) course. 16 | 2. [PHP v8.2](https://www.php.net/releases/8.2/en.php) or newer. 17 | 18 | 19 | ## Quick Setup 20 | 21 | * Ensure [Composer](https://getcomposer.org) is setup on your machine. Then, run `composer install` 22 | * Run the in-built PHP server `php -S localhost:8080` 23 | 24 | 25 | ## Author 26 | 27 | [![Pierre-Henry Soria](https://avatars0.githubusercontent.com/u/1325411?s=200)](https://pierrehenry.be "Pierre-Henry Soria, Software AI Engineer") 28 | 29 | **[Pierre-Henry Soria](https://ph7.me)**. A truly super passionate and enthusiastic software engineer! 😊 Also, a **true cheese** 🧀, **dark chocolate**, and **espresso lover**! ☕️ 30 | 31 | 32 | ## License 33 | 34 | This source code is distributed under the open-source [MIT license](https://opensource.org/licenses/MIT). 35 | -------------------------------------------------------------------------------- /src/AllowCors.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu; 11 | 12 | class AllowCors 13 | { 14 | private const ALLOW_CORS_ORIGIN_KEY = 'Access-Control-Allow-Origin'; 15 | private const ALLOW_CORS_METHOD_KEY = 'Access-Control-Allow-Methods'; 16 | 17 | private const ALLOW_CORS_ORIGIN_VALUE = '*'; 18 | private const ALLOW_CORS_METHODS_VALUE = 'GET, POST, PUT, DELETE, PATCH, OPTIONS'; 19 | 20 | /** 21 | * Initialize the Cross-Origin Resource Sharing (CORS) headers. 22 | */ 23 | public function init(): void 24 | { 25 | $this->set(self::ALLOW_CORS_ORIGIN_KEY, self::ALLOW_CORS_ORIGIN_VALUE); 26 | $this->set(self::ALLOW_CORS_METHOD_KEY, self::ALLOW_CORS_METHODS_VALUE); 27 | } 28 | 29 | private function set(string $key, string $value): void 30 | { 31 | header($key . ':' . $value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dal/FoodItemDal.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Dal; 11 | 12 | use PH7\ApiSimpleMenu\Entity\Item as ItemEntity; 13 | use RedBeanPHP\R; 14 | 15 | final class FoodItemDal 16 | { 17 | public const TABLE_NAME = 'fooditems'; // Cannot have underscore. Use one word 18 | 19 | public static function get(string $itemUuid): ItemEntity 20 | { 21 | $bindings = ['itemUuid' => $itemUuid]; 22 | $itemBean = R::findOne(self::TABLE_NAME, 'item_uuid = :itemUuid', $bindings); 23 | 24 | return (new ItemEntity())->unserialize($itemBean?->export()); 25 | } 26 | 27 | public static function getAll(): array 28 | { 29 | $itemsBean = R::findAll(self::TABLE_NAME); 30 | 31 | $areAnyItems = $itemsBean && count($itemsBean); 32 | 33 | if (!$areAnyItems) { 34 | // if no items found, return empty array 35 | return []; 36 | } 37 | 38 | return array_map( 39 | function (object $itemBean): array { 40 | $itemEntity = (new ItemEntity())->unserialize($itemBean?->export()); 41 | 42 | // Select the fields we want to export and give back to the client 43 | return [ 44 | 'foodUuid' => $itemEntity->getItemUuid(), 45 | 'name' => $itemEntity->getName(), 46 | 'price' => $itemEntity->getPrice(), 47 | 'available' => $itemEntity->getAvailable() 48 | ]; 49 | }, $itemsBean); 50 | } 51 | 52 | public static function insertDefaultItem(ItemEntity $itemEntity): int|string 53 | { 54 | $itemBan = R::dispense(self::TABLE_NAME); 55 | 56 | $itemBan->item_uuid = $itemEntity->getItemUuid(); 57 | $itemBan->name = $itemEntity->getName(); 58 | $itemBan->price = $itemEntity->getPrice(); 59 | $itemBan->available = $itemEntity->getAvailable(); 60 | 61 | // return the increment entry ID 62 | return R::store($itemBan); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Dal/TokenKeyDal.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Dal; 11 | 12 | use RedBeanPHP\R; 13 | 14 | final class TokenKeyDal 15 | { 16 | public const TABLE_NAME = 'secretkeys'; 17 | 18 | public static function saveSecretKey(string $jwtKey): void 19 | { 20 | $tokenBean = R::dispense(self::TABLE_NAME); 21 | $tokenBean->secretKey = $jwtKey; 22 | 23 | R::store($tokenBean); 24 | 25 | // close connection with database 26 | R::close(); 27 | } 28 | 29 | public static function getSecretKey(): ?string 30 | { 31 | $tokenKeyBean = R::load(self::TABLE_NAME, 1); 32 | 33 | return $tokenKeyBean?->secretKey; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Dal/UserDal.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Dal; 11 | 12 | use PH7\ApiSimpleMenu\Entity\User as UserEntity; 13 | use RedBeanPHP\R; 14 | use RedBeanPHP\RedException\SQL; 15 | 16 | final class UserDal 17 | { 18 | public const TABLE_NAME = 'users'; 19 | 20 | public static function create(UserEntity $userEntity): string|false 21 | { 22 | $userBean = R::dispense(self::TABLE_NAME); 23 | $userBean->user_uuid = $userEntity->getUserUuid(); 24 | $userBean->first_name = $userEntity->getFirstName(); 25 | $userBean->last_name = $userEntity->getLastName(); 26 | $userBean->email = $userEntity->getEmail(); 27 | $userBean->phone = $userEntity->getPhone(); 28 | $userBean->password = $userEntity->getPassword(); 29 | $userBean->created_date = $userEntity->getCreationDate(); 30 | 31 | try { 32 | $redBeanIncrementId = R::store($userBean); 33 | } catch (SQL) { // since PHP 8, we can omit the caught variable (e.g. SQL $e) 34 | return false; 35 | } finally { 36 | R::close(); 37 | } 38 | 39 | // Retrieve the user we just created for accessing to `user_uuid` column 40 | $userBean = R::load(self::TABLE_NAME, $redBeanIncrementId); 41 | 42 | // Return user UUID (UUID is a string datatype) 43 | return $userBean->user_uuid; 44 | } 45 | 46 | public static function update(string $userUuid, UserEntity $userEntity): int|string|false 47 | { 48 | $userBean = R::findOne(self::TABLE_NAME, 'user_uuid = :userUuid', ['userUuid' => $userUuid]); 49 | 50 | // If the user exists, update it 51 | if ($userBean) { 52 | $firstName = $userEntity->getFirstName(); 53 | $lastName = $userEntity->getLastName(); 54 | $phone = $userEntity->getPhone(); 55 | 56 | if ($firstName) { 57 | $userBean->firstName = $firstName; 58 | } 59 | 60 | if ($lastName) { 61 | $userBean->lastName = $lastName; 62 | } 63 | 64 | if ($phone) { 65 | $userBean->phone = $phone; 66 | } 67 | 68 | // attempt to save the user 69 | try { 70 | return R::store($userBean); // returns the user ID 71 | } catch (SQL) { // PHP >=8.0 allows to omit the caught variable (e.g. SQL $e) 72 | return false; 73 | } finally { 74 | R::close(); 75 | } 76 | } 77 | 78 | // Return false when the requested user isn't found 79 | return false; 80 | } 81 | 82 | public static function getById(string $userUuid): UserEntity 83 | { 84 | $bindings = ['userUuid' => $userUuid]; 85 | $userBean = R::findOne(self::TABLE_NAME, 'user_uuid = :userUuid ', $bindings); 86 | 87 | return (new UserEntity())->unserialize($userBean?->export()); 88 | } 89 | 90 | public static function getByEmail(string $email): UserEntity 91 | { 92 | $bindings = ['email' => $email]; 93 | 94 | $userBean = R::findOne(self::TABLE_NAME, 'email = :email', $bindings); 95 | 96 | return (new UserEntity())->unserialize($userBean?->export()); 97 | } 98 | 99 | /** 100 | * @throws \RedBeanPHP\RedException\SQL 101 | */ 102 | public static function setToken(string $jwtToken, string $userUuid): void 103 | { 104 | $bindings = ['userUuid' => $userUuid]; 105 | $userBean = R::findOne(self::TABLE_NAME, 'user_uuid = :userUuid', $bindings); 106 | 107 | $userBean->session_token = $jwtToken; 108 | $userBean->last_session_time = time(); 109 | 110 | R::store($userBean); 111 | 112 | R::close(); 113 | } 114 | 115 | public static function getAll(): ?array 116 | { 117 | $usersBean = R::findAll(self::TABLE_NAME); 118 | $areAnyUsers = $usersBean && count($usersBean); 119 | 120 | if (!$areAnyUsers) { 121 | return []; // guard clause approach 122 | } 123 | 124 | return array_map(function (object $userBean): array 125 | { 126 | $userEntity = (new UserEntity())->unserialize($userBean?->export()); 127 | // Retrieve the User entity fields we want to expose to the client 128 | return [ 129 | 'userUuid' => $userEntity->getUserUuid(), 130 | 'first' => $userEntity->getFirstName(), 131 | 'last' => $userEntity->getLastName(), 132 | 'email' => $userEntity->getEmail(), 133 | 'phone' => $userEntity->getPhone(), 134 | 'creationDate' => $userEntity->getCreationDate() 135 | ]; 136 | }, $usersBean); 137 | } 138 | 139 | public static function remove(string $userUuid): bool 140 | { 141 | $userBean = R::findOne(self::TABLE_NAME, 'user_uuid = :userUuid', ['userUuid' => $userUuid]); 142 | 143 | if ($userBean) { 144 | return (bool)R::trash($userBean); 145 | } 146 | 147 | return false; 148 | } 149 | 150 | public static function doesEmailExist(string $email): bool 151 | { 152 | // If R::findOne doesn't find any rows, it returns NULL (meaning, the email address doesn't exist) 153 | return R::findOne(self::TABLE_NAME, 'email = :email', ['email' => $email]) !== null; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Entity/Entitable.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Entity; 11 | 12 | interface Entitable 13 | { 14 | public function unserialize(?array $data): self; 15 | 16 | public function setSequentialId(int $sequentialId): self; 17 | 18 | public function getSequentialId(): int; 19 | } 20 | -------------------------------------------------------------------------------- /src/Entity/Item.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Entity; 11 | 12 | class Item implements Entitable 13 | { 14 | private int $sequentialId; 15 | 16 | private ?string $itemUuid = null; 17 | 18 | private string $name; 19 | 20 | private float $price; 21 | 22 | private bool $available; 23 | 24 | public function setSequentialId(int $sequentialId): self 25 | { 26 | $this->sequentialId = $sequentialId; 27 | 28 | return $this; 29 | } 30 | 31 | public function getSequentialId(): int 32 | { 33 | return $this->sequentialId; 34 | } 35 | 36 | public function setItemUuid(string $itemUuid): self 37 | { 38 | $this->itemUuid = $itemUuid; 39 | 40 | return $this; 41 | } 42 | 43 | public function getItemUuid(): ?string 44 | { 45 | return $this->itemUuid; 46 | } 47 | 48 | public function setName(string $name): self 49 | { 50 | $this->name = $name; 51 | 52 | return $this; 53 | } 54 | 55 | public function getName(): string 56 | { 57 | return $this->name; 58 | } 59 | 60 | public function setPrice(float $price): self 61 | { 62 | $this->price = $price; 63 | 64 | return $this; 65 | } 66 | 67 | public function getPrice(): float 68 | { 69 | return $this->price; 70 | } 71 | 72 | public function setAvailable(bool $available): self 73 | { 74 | $this->available = $available; 75 | 76 | return $this; 77 | } 78 | 79 | public function getAvailable(): bool 80 | { 81 | return $this->available; 82 | } 83 | 84 | public function unserialize(?array $data): self 85 | { 86 | if (!empty($data['id'])) { 87 | $sequentialId = (int)$data['id']; 88 | $this->setSequentialId($sequentialId); 89 | } 90 | 91 | if (!empty($data['item_uuid'])) { 92 | $this->setItemUuid($data['item_uuid']); 93 | } 94 | 95 | if (!empty($data['name'])) { 96 | $this->setName($data['name']); 97 | } 98 | 99 | if (!empty($data['price'])) { 100 | $price = (float)$data['price']; 101 | $this->setPrice($price); 102 | } 103 | 104 | if (!empty($data['available'])) { 105 | $isAvailable = (bool)$data['available']; 106 | $this->setAvailable($isAvailable); 107 | } 108 | 109 | return $this; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Entity; 11 | 12 | class User implements Entitable 13 | { 14 | private int $sequentialId; 15 | private ?string $userUuid = null; 16 | 17 | private ?string $firstName = null; 18 | 19 | private ?string $lastName = null; 20 | 21 | private ?string $email = null; 22 | 23 | private ?string $phone = null; 24 | 25 | private string $password; 26 | 27 | private ?string $creationDate = null; 28 | 29 | public function setSequentialId(int $sequentialId): self 30 | { 31 | $this->sequentialId = $sequentialId; 32 | 33 | return $this; 34 | } 35 | 36 | public function getSequentialId(): int 37 | { 38 | return $this->sequentialId; 39 | } 40 | 41 | public function setUserUuid(string $userUuid) :self 42 | { 43 | $this->userUuid = $userUuid; 44 | 45 | return $this; 46 | } 47 | 48 | public function getUserUuid(): ?string 49 | { 50 | return $this->userUuid; 51 | } 52 | 53 | public function setFirstName(string $firstName): self 54 | { 55 | $this->firstName = $firstName; 56 | 57 | return $this; 58 | } 59 | 60 | public function getFirstName(): ?string 61 | { 62 | return $this->firstName; 63 | } 64 | 65 | public function setLastName(string $lastName): self 66 | { 67 | $this->lastName = $lastName; 68 | 69 | return $this; 70 | } 71 | 72 | public function getLastName(): ?string 73 | { 74 | return $this->lastName; 75 | } 76 | 77 | public function setEmail(string $email): self 78 | { 79 | $this->email = $email; 80 | 81 | return $this; 82 | } 83 | 84 | public function getEmail(): ?string 85 | { 86 | return $this->email; 87 | } 88 | 89 | public function setPhone(string $phone): self 90 | { 91 | $this->phone = $phone; 92 | 93 | return $this; 94 | } 95 | 96 | public function getPhone(): ?string 97 | { 98 | return $this->phone; 99 | } 100 | 101 | public function setPassword(string $password): self 102 | { 103 | $this->password = $password; 104 | 105 | return $this; 106 | } 107 | 108 | public function getPassword(): string 109 | { 110 | return $this->password; 111 | } 112 | 113 | public function setCreationDate(string $creationDate): self 114 | { 115 | $this->creationDate = $creationDate; 116 | 117 | return $this; 118 | } 119 | 120 | public function getCreationDate(): ?string 121 | { 122 | return $this->creationDate; 123 | } 124 | 125 | public function unserialize(?array $data): self 126 | { 127 | if (!empty($data['id'])) { 128 | $sequentialId = (int)$data['id']; 129 | $this->setSequentialId($sequentialId); 130 | } 131 | 132 | if (!empty($data['user_uuid'])) { 133 | $this->setUserUuid($data['user_uuid']); 134 | } 135 | 136 | if (!empty($data['first_name'])) { 137 | $this->setFirstName($data['first_name']); 138 | } 139 | 140 | if (!empty($data['last_name'])) { 141 | $this->setLastName($data['last_name']); 142 | } 143 | 144 | if (!empty($data['email'])) { 145 | $this->setEmail($data['email']); 146 | } 147 | 148 | if (!empty($data['phone'])) { 149 | $this->setPhone($data['phone']); 150 | } 151 | 152 | if (!empty($data['password'])) { 153 | $this->setPassword($data['password']); 154 | } 155 | 156 | if (!empty($data['created_date'])) { 157 | $this->setCreationDate($data['created_date']); 158 | } 159 | 160 | return $this; 161 | } 162 | 163 | // public function serialize(): array 164 | // { 165 | // // optional. we could also only return the properties we want to make it safe 166 | // // (not all properties should indeed be returned) 167 | // return get_object_vars($this); 168 | // } 169 | } 170 | -------------------------------------------------------------------------------- /src/Route/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Route\Exception; 9 | 10 | use RuntimeException; 11 | 12 | class NotFoundException extends RuntimeException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Route/Http.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Route; 11 | 12 | final class Http 13 | { 14 | public const POST_METHOD = 'POST'; 15 | public const GET_METHOD = 'GET'; 16 | public const PUT_METHOD = 'PUT'; 17 | public const DELETE_METHOD = 'DELETE'; 18 | 19 | public static function doesHttpMethodMatch(string $httpMethod): bool 20 | { 21 | return strtolower($_SERVER['REQUEST_METHOD']) === strtolower($httpMethod); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Route/food-item.routes.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Route; 9 | 10 | use PH7\ApiSimpleMenu\Service\FoodItem; 11 | 12 | enum FoodItemAction: string 13 | { 14 | case RETRIEVE_ALL = 'retrieveall'; 15 | case RETRIEVE = 'retrieve'; 16 | 17 | public function getResponse(): string 18 | { 19 | $postBody = file_get_contents('php://input'); 20 | $postBody = json_decode($postBody); // unused for now 21 | 22 | // Ternary conditional operator operator 23 | $itemId = $_REQUEST['id'] ?? ''; // using the null coalescing operator 24 | 25 | $item = new FoodItem(); 26 | $response = match ($this) { 27 | self::RETRIEVE_ALL => $item->retrieveAll(), 28 | self::RETRIEVE => $item->retrieve($itemId), 29 | }; 30 | 31 | return json_encode($response); 32 | } 33 | } 34 | 35 | $action = $_REQUEST['action'] ?? null; 36 | 37 | $itemAction = FoodItemAction::tryFrom($action); 38 | if ($itemAction) { 39 | echo $itemAction->getResponse(); 40 | } else { 41 | require_once 'not-found.routes.php'; 42 | } 43 | -------------------------------------------------------------------------------- /src/Route/not-found.routes.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Route; 9 | 10 | use PH7\JustHttp\StatusCode; 11 | use PH7\PhpHttpResponseHeader\Http; 12 | 13 | // PHP 7.4 anonymous arrow function 14 | $getResponse = fn(): string => json_encode(['error' => 'Request not found']); 15 | 16 | // Send HTTP 404 Not Found 17 | Http::setHeadersByCode(StatusCode::NOT_FOUND); 18 | echo $getResponse(); 19 | -------------------------------------------------------------------------------- /src/Route/routes.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Route; 9 | 10 | use PH7\ApiSimpleMenu\Route\Exception\NotFoundException; 11 | use PH7\ApiSimpleMenu\Service\Exception\CredentialsInvalidException; 12 | use PH7\ApiSimpleMenu\Validation\Exception\InvalidValidationException; 13 | use PH7\JustHttp\StatusCode; 14 | use PH7\PhpHttpResponseHeader\Http as HttpResponse; 15 | 16 | $resource = $_REQUEST['resource'] ?? null; 17 | 18 | try { 19 | return match ($resource) { 20 | 'user' => require_once 'user.routes.php', 21 | 'item' => require_once 'food-item.routes.php', 22 | default => require_once 'not-found.routes.php', 23 | }; 24 | } catch (CredentialsInvalidException $e) { 25 | response([ 26 | 'errors' => [ 27 | 'message' => $e->getMessage() 28 | ] 29 | ]); 30 | } catch (InvalidValidationException $e) { 31 | // Send 400 http status code 32 | HttpResponse::setHeadersByCode(StatusCode::BAD_REQUEST); 33 | 34 | response([ 35 | 'errors' => [ 36 | 'message' => $e->getMessage(), 37 | 'code' => $e->getCode() 38 | ] 39 | ]); 40 | } catch (NotFoundException $e) { 41 | // FYI, not-found.Route already sends a 404 Not Found HTTP code 42 | return require_once 'not-found.routes.php'; 43 | } 44 | -------------------------------------------------------------------------------- /src/Route/user.routes.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Route; 9 | 10 | use PH7\ApiSimpleMenu\Route\Exception\NotFoundException; 11 | use PH7\ApiSimpleMenu\Service\Exception\CannotLoginUserException; 12 | use PH7\ApiSimpleMenu\Service\Exception\EmailExistsException; 13 | use PH7\ApiSimpleMenu\Service\SecretKey; 14 | use PH7\ApiSimpleMenu\Service\User; 15 | 16 | use PH7\JustHttp\StatusCode; 17 | use PH7\PhpHttpResponseHeader\Http as HttpResponse; 18 | 19 | enum UserAction: string 20 | { 21 | case LOGIN = 'login'; 22 | case CREATE = 'create'; 23 | case RETRIEVE_ALL = 'retrieveall'; 24 | case RETRIEVE = 'retrieve'; 25 | case REMOVE = 'remove'; 26 | case UPDATE = 'update'; 27 | 28 | /** 29 | * @throws Exception\NotFoundException 30 | */ 31 | public function getResponse(): string 32 | { 33 | $postBody = file_get_contents('php://input'); 34 | $postBody = json_decode($postBody); 35 | 36 | // Ternary conditional operator operator 37 | $userId = $_REQUEST['id'] ?? ''; // using the null coalescing operator 38 | 39 | // retrieve JWT secret key, and pass it to User Service' constructor 40 | $jwtToken = SecretKey::getJwtSecretKey(); 41 | $user = new User($jwtToken); 42 | 43 | try { 44 | // first, let's check if HTTP method for the requested endpoint is valid 45 | $expectHttpMethod = match ($this) { 46 | self::LOGIN => Http::POST_METHOD, 47 | self::CREATE => Http::POST_METHOD, 48 | self::UPDATE => Http::PUT_METHOD, 49 | self::RETRIEVE_ALL => Http::GET_METHOD, 50 | self::RETRIEVE => Http::GET_METHOD, 51 | self::REMOVE => Http::DELETE_METHOD 52 | }; 53 | 54 | if (Http::doesHttpMethodMatch($expectHttpMethod) === false) { 55 | throw new NotFoundException('HTTP method is incorrect. Request not found'); 56 | } 57 | 58 | $response = match ($this) { 59 | self::LOGIN => $user->login($postBody), 60 | self::CREATE => $user->create($postBody), 61 | self::UPDATE => $user->update($postBody), 62 | self::RETRIEVE_ALL => $user->retrieveAll(), 63 | self::RETRIEVE => $user->retrieve($userId), 64 | self::REMOVE => $user->remove($postBody), 65 | }; 66 | } catch (CannotLoginUserException $e) { 67 | // Send 400 http status code 68 | HttpResponse::setHeadersByCode(StatusCode::BAD_REQUEST); 69 | 70 | $response = [ 71 | 'errors' => [ 72 | 'message' => $e->getMessage(), 73 | ] 74 | ]; 75 | } catch (EmailExistsException $e) { 76 | HttpResponse::setHeadersByCode(StatusCode::BAD_REQUEST); 77 | 78 | $response = [ 79 | 'errors' => [ 80 | 'message' => $e->getMessage() 81 | ] 82 | ]; 83 | } 84 | 85 | return json_encode($response); 86 | } 87 | } 88 | 89 | $action = $_REQUEST['action'] ?? null; 90 | 91 | $userAction = UserAction::tryFrom($action); 92 | if ($userAction) { 93 | echo $userAction->getResponse(); 94 | } else { 95 | require_once 'not-found.routes.php'; 96 | } 97 | -------------------------------------------------------------------------------- /src/Service/Exception/CannotLoginUserException.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Service\Exception; 9 | 10 | use RuntimeException; 11 | 12 | class CannotLoginUserException extends RuntimeException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Service/Exception/CredentialsInvalidException.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Service\Exception; 9 | 10 | use RuntimeException; 11 | 12 | class CredentialsInvalidException extends RuntimeException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Service/Exception/EmailExistsException.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu\Service\Exception; 9 | 10 | use RuntimeException; 11 | 12 | class EmailExistsException extends RuntimeException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Service/FoodItem.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Service; 11 | 12 | use PH7\ApiSimpleMenu\Dal\FoodItemDal; 13 | use PH7\ApiSimpleMenu\Entity\Item as ItemEntity; 14 | use PH7\ApiSimpleMenu\Validation\Exception\InvalidValidationException; 15 | use Ramsey\Uuid\Uuid; 16 | use Respect\Validation\Validator as v; 17 | 18 | class FoodItem 19 | { 20 | public function retrieve(string $itemUuid): array 21 | { 22 | if (v::uuid()->validate($itemUuid)) { 23 | if ($item = FoodItemDal::get($itemUuid)) { 24 | if ($item->getItemUuid()) { 25 | return [ 26 | 'itemUuid' => $item->getItemUuid(), 27 | 'name' => $item->getName(), 28 | 'price' => $item->getPrice(), 29 | 'available' => $item->getAvailable() 30 | ]; 31 | } 32 | } 33 | 34 | return []; 35 | } 36 | 37 | throw new InvalidValidationException("Invalid user UUID"); 38 | } 39 | 40 | public function retrieveAll(): array 41 | { 42 | $items = FoodItemDal::getAll(); 43 | 44 | if (count($items) === 0) { 45 | $this->createDefaultItem(); 46 | 47 | // then, get again all items 48 | // to retrieve the new one that just got added 49 | $items = FoodItemDal::getAll(); 50 | } 51 | 52 | return $items; 53 | } 54 | 55 | private function createDefaultItem(): void 56 | { 57 | // default item values 58 | $defaultPrice = 19.99; 59 | $isEnabled = true; 60 | 61 | 62 | // if no items have been added yet, create the first one 63 | $itemUuid = Uuid::uuid4()->toString(); 64 | $itemEntity = new ItemEntity(); 65 | 66 | // chaining each method with the arrow -> 67 | $itemEntity 68 | ->setItemUuid($itemUuid) 69 | ->setName('Burrito Cheese with French Fries') 70 | ->setPrice($defaultPrice) 71 | ->setAvailable($isEnabled); 72 | 73 | FoodItemDal::insertDefaultItem($itemEntity); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Service/SecretKey.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Service; 11 | 12 | use PH7\ApiSimpleMenu\Dal\TokenKeyDal; 13 | 14 | class SecretKey 15 | { 16 | public static function getJwtSecretKey(): string 17 | { 18 | $jwtKey = TokenKeyDal::getSecretKey(); 19 | 20 | if (!$jwtKey) { 21 | $uniqueSecretKey = hash('sha512', strval(time())); 22 | TokenKeyDal::saveSecretKey($uniqueSecretKey); 23 | 24 | return $uniqueSecretKey; 25 | } 26 | 27 | return $jwtKey; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Service/User.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace PH7\ApiSimpleMenu\Service; 11 | 12 | use Exception; 13 | use Firebase\JWT\JWT; 14 | use PH7\ApiSimpleMenu\Dal\UserDal; 15 | use PH7\ApiSimpleMenu\Service\Exception\CannotLoginUserException; 16 | use PH7\ApiSimpleMenu\Service\Exception\EmailExistsException; 17 | use PH7\ApiSimpleMenu\Service\Exception\CredentialsInvalidException; 18 | use PH7\ApiSimpleMenu\Validation\Exception\InvalidValidationException; 19 | use PH7\ApiSimpleMenu\Validation\UserValidation; 20 | use PH7\JustHttp\StatusCode; 21 | use PH7\PhpHttpResponseHeader\Http as HttpResponse; 22 | use Ramsey\Uuid\Uuid; 23 | use Respect\Validation\Validator as v; 24 | use PH7\ApiSimpleMenu\Entity\User as UserEntity; 25 | 26 | class User 27 | { 28 | public const DATE_TIME_FORMAT = 'Y-m-d H:i:s'; 29 | 30 | public function __construct(protected string $jwtSecretKey) 31 | { 32 | } 33 | 34 | public function login(mixed $data): array 35 | { 36 | $userValidation = new UserValidation($data); 37 | if ($userValidation->isLoginSchemaValid()) { 38 | if (UserDal::doesEmailExist($data->email)) { 39 | $user = UserDal::getByEmail($data->email); 40 | 41 | $areCredentialsValid = $user->getEmail() && password_verify($data->password, $user->getPassword()); 42 | if ($areCredentialsValid) { 43 | $userName = "{$user->getFirstName()} {$user->getLastName()}"; 44 | 45 | $currentTime = time(); 46 | $jwtToken = JWT::encode( 47 | [ 48 | 'iss' => $_ENV['APP_URL'], 49 | 'iat' => $currentTime, 50 | 'exp' => $currentTime + $_ENV['JWT_TOKEN_EXPIRATION'], 51 | 'data' => [ 52 | 'email' => $data->email, 53 | 'name' => $userName 54 | ] 55 | ], 56 | $this->jwtSecretKey, 57 | $_ENV['JWT_ALGO_ENCRYPTION'] 58 | ); 59 | 60 | try { 61 | UserDal::setToken($jwtToken, $user->getUserUuid()); 62 | } catch (Exception) { // since PHP 8.0, we can omit the caught variable name (e.g. Exception $e) 63 | throw new CannotLoginUserException('Cannot set token to user'); 64 | } 65 | 66 | return [ 67 | 'message' => sprintf('%s successfully logged in', $userName), 68 | 'token' => $jwtToken 69 | ]; 70 | } 71 | } 72 | throw new CredentialsInvalidException('Credentials invalid'); 73 | } 74 | throw new InvalidValidationException('Payload invalid'); 75 | } 76 | 77 | public function create(mixed $data): array|object 78 | { 79 | $userValidation = new UserValidation($data); 80 | if ($userValidation->isCreationSchemaValid()) { 81 | $userUuid = Uuid::uuid4()->toString(); // assigning a UUID to the user 82 | 83 | $userEntity = new UserEntity(); 84 | $userEntity 85 | ->setUserUuid($userUuid) 86 | ->setFirstName($data->first) 87 | ->setLastName($data->last) 88 | ->setEmail($data->email) 89 | ->setPhone($data->phone) 90 | ->setPassword(hashPassword($data->password)) 91 | ->setCreationDate(date(self::DATE_TIME_FORMAT)); 92 | 93 | $email = $userEntity->getEmail(); 94 | if (UserDal::doesEmailExist($email)) { 95 | throw new EmailExistsException( 96 | sprintf('Email address %s already exists', $email) 97 | ); 98 | } 99 | 100 | if (!$userUuid = UserDal::create($userEntity)) { 101 | // If we receive an error while creating a user to the database, give a 400 to client 102 | HttpResponse::setHeadersByCode(StatusCode::BAD_REQUEST); 103 | 104 | // Set to empty result, because an issue happened. The client has to handle this properly 105 | $data = []; 106 | } 107 | 108 | // Send a 201 when the user has been successfully added to DB 109 | HttpResponse::setHeadersByCode(StatusCode::CREATED); 110 | 111 | // Add user UUID to the object to give back the user's UUID to the client 112 | $data->userUuid = $userUuid; 113 | 114 | return $data; 115 | } 116 | 117 | throw new InvalidValidationException("Invalid user payload"); 118 | } 119 | 120 | public function update(mixed $postBody): array|object 121 | { 122 | $userValidation = new UserValidation($postBody); 123 | if ($userValidation->isUpdateSchemaValid()) { 124 | $userUuid = $postBody->userUuid; 125 | 126 | $userEntity = new UserEntity(); 127 | if (!empty($postBody->first)) { 128 | $userEntity->setFirstName($postBody->first); 129 | } 130 | 131 | if (!empty($postBody->last)) { 132 | $userEntity->setLastName($postBody->last); 133 | } 134 | 135 | if (!empty($postBody->phone)) { 136 | $userEntity->setPhone($postBody->phone); 137 | } 138 | 139 | if (UserDal::update($userUuid, $userEntity) === false) { 140 | // Most likely, the user isn't found, set a 404 to the client 141 | HttpResponse::setHeadersByCode(StatusCode::NOT_FOUND); 142 | 143 | // If invalid or got an error, give back an empty response 144 | return []; 145 | } 146 | 147 | return $postBody; 148 | } 149 | 150 | throw new InvalidValidationException("Invalid user payload"); 151 | } 152 | 153 | public function retrieveAll(): array 154 | { 155 | return UserDal::getAll(); 156 | } 157 | 158 | public function retrieve(string $userUuid): array 159 | { 160 | if (v::uuid()->validate($userUuid)) { 161 | if ($user = UserDal::getById($userUuid)) { 162 | if ($user->getUserUuid()) { 163 | // Retrieve the needed properties we want to expose for the user 164 | return [ 165 | 'userUuid' => $user->getUserUuid(), 166 | 'first' => $user->getFirstName(), 167 | 'last' => $user->getLastName(), 168 | 'email' => $user->getEmail(), 169 | 'phone' => $user->getPhone(), 170 | 'creationDate' => $user->getCreationDate() 171 | ]; 172 | } 173 | } 174 | 175 | return []; 176 | } 177 | 178 | throw new InvalidValidationException("Invalid user UUID"); 179 | } 180 | 181 | /** 182 | * @internal Set `mixed` type, because if we get an incorrect payload with syntax errors, `json_decode` gives NULL, 183 | * and `object` wouldn't be a valid datatype here. 184 | */ 185 | public function remove(mixed $data): bool 186 | { 187 | $userValidation = new UserValidation($data); 188 | if ($userValidation->isRemoveSchemaValid()) { 189 | // Send a 204 if the user got removed 190 | //HttpResponse::setHeadersByCode(StatusCode::NO_CONTENT); 191 | return UserDal::remove($data->userUuid); 192 | } 193 | 194 | throw new InvalidValidationException("Invalid user UUID"); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Validation/Exception/InvalidValidationException.php: -------------------------------------------------------------------------------- 1 | length(self::MINIMUM_NAME_LENGTH, self::MAXIMUM_NAME_LENGTH)) 34 | ->attribute('last', v::stringType()->length(self::MINIMUM_NAME_LENGTH, self::MAXIMUM_NAME_LENGTH)) 35 | ->attribute('password', v::stringType()->length(self::MINIMUM_PASSWORD_LENGTH)) 36 | ->attribute('email', v::email()) 37 | ->attribute('phone', v::phone()); 38 | 39 | return $schemaValidation->validate($this->data); 40 | } 41 | 42 | public function isRemoveSchemaValid(): bool 43 | { 44 | return v::attribute('userUuid', v::uuid())->validate($this->data); 45 | } 46 | 47 | public function isUpdateSchemaValid(): bool 48 | { 49 | $schemaValidation = 50 | v::attribute('userUuid', v::uuid()) 51 | ->attribute('first', v::stringType()->length(self::MINIMUM_NAME_LENGTH, self::MAXIMUM_NAME_LENGTH), mandatory: false) 52 | ->attribute('last', v::stringType()->length(self::MINIMUM_NAME_LENGTH, self::MAXIMUM_NAME_LENGTH), mandatory: false) 53 | ->attribute('phone', v::phone(), mandatory: false); 54 | 55 | return $schemaValidation->validate($this->data); 56 | } 57 | 58 | public function isLoginSchemaValid(): bool 59 | { 60 | $schemaValidation = 61 | v::attribute('email', v::stringType()) 62 | ->attribute('password', v::stringType()); 63 | 64 | return $schemaValidation->validate($this->data); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/config/config.inc.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu; 9 | 10 | use Dotenv\Dotenv; 11 | 12 | enum Environment : string 13 | { 14 | case DEVELOPMENT = 'development'; 15 | case PRODUCTION = 'production'; 16 | 17 | public function environmentName(): string 18 | { 19 | return match($this) { 20 | self::DEVELOPMENT => 'development', 21 | self::PRODUCTION => 'production' 22 | }; 23 | } 24 | } 25 | 26 | $path = dirname(__DIR__, 2); 27 | $dotenv = Dotenv::createImmutable($path); 28 | $dotenv->load(); 29 | 30 | // optional: check if the necessary values are in the .env file 31 | $dotenv->required(['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS']); 32 | -------------------------------------------------------------------------------- /src/config/database.inc.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu; 9 | 10 | use RedBeanPHP\R; 11 | 12 | // setup RedBean 13 | $dsn = sprintf('mysql:host=%s;dbname=%s', $_ENV['DB_HOST'], $_ENV['DB_NAME']); 14 | R::setup($dsn, $_ENV['DB_USER'], $_ENV['DB_PASS']); 15 | 16 | // Freeze RedBean on production 17 | $currentEnvironment = Environment::tryFrom($_ENV['ENVIRONMENT']); 18 | if ($currentEnvironment?->environmentName() !== Environment::DEVELOPMENT->value) { 19 | echo 'RedBean Frozen'; 20 | R::freeze(); 21 | } 22 | -------------------------------------------------------------------------------- /src/helpers/headers.inc.php: -------------------------------------------------------------------------------- 1 | 4 | * @website https://ph7.me 5 | * @license MIT License 6 | */ 7 | 8 | namespace PH7\ApiSimpleMenu; 9 | 10 | use PH7\PhpHttpResponseHeader\Http; 11 | 12 | (new AllowCors)->init(); 13 | 14 | Http::setContentType('application/json'); 15 | -------------------------------------------------------------------------------- /src/helpers/misc.inc.php: -------------------------------------------------------------------------------- 1 | 6 | * @license MIT License 7 | */ 8 | 9 | declare(strict_types=1); 10 | 11 | const PASSWORD_COST_FACTOR = 12; 12 | 13 | function response(mixed $data): void { 14 | echo json_encode($data); 15 | } 16 | 17 | function hashPassword(string $password): false|null|string { 18 | return password_hash($password, PASSWORD_ARGON2I, ['cost' => PASSWORD_COST_FACTOR]); 19 | } 20 | --------------------------------------------------------------------------------