Jednoduché API ve frameworku Slim 4 – č. 8 Přidáme si do datbáze produkty
- Jednoduché API ve frameworku Slim 4 – č. 1 Instalace
- Jednoduché API ve frameworku Slim 4 – č. 2 Základní CRUD
- Jednoduché API ve frameworku Slim 4 – č. 3 Struktura API a připojení k databázi
- Jednoduché API ve frameworku Slim 4 – č. 4 Testování funkcionality našeho malého API
- Jednoduché API ve frameworku Slim 4 – č. 5 Vylepšení UserControlleru
- Jednoduché API ve frameworku Slim 4 – č. 6 Přidání Model a Repositories
- Jednoduché API ve frameworku Slim 4 – č. 7 Validace dat
- Jednoduché API ve frameworku Slim 4 – č. 8 Přidáme si do datbáze produkty
Aby naše API nebylo tak chudé doplníme si je o nové endpointy pro produkty. Nejprve si ale ještě provedeme malý refaktoring. Co se mi na API nelíbí je definice roue v hlahním souboru. Routu bude asi přibývat a líbili by se mi, kdyby byl v nějakém samostatném souboru. Takže si vytvoříme v adresáři vytvoříme soubor routes.php a do něj přesume všechny routy z hlavního index.php
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php use App\Controllers\UserController; use Slim\App; return function (App $app) { $app->get('/api/users', [UserController::class, 'getAll']); $app->get('/api/users/{id}', [UserController::class, 'getOne']); $app->post('/api/users', [UserController::class, 'create']); $app->put('/api/users/{id}', [UserController::class, 'update']); $app->delete('/api/users/{id}', [UserController::class, 'delete']); }; |
a v hlavním souboru pak místo původně vložených rout vložíme náš nově vytvořený soubor
1 2 3 |
// Routes $routes = require __DIR__ . '/../config/routes.php'; $routes($app); |
Samozřejmě otestujem zda vše funguje jak má.
Budeme si přidávat další endpointy a když se podívám do UserControlleru máme tam funkci jsonResponse, kterou budeme chtít použít i v jiných controlerech. Takže pojďme ji nějak extrahovat. V naší aplikaci si vytvoříme nový adresář Traits, jak už asi tušíme vyřešíme tento problém použitím traitu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?php namespace App\Traits; use Psr\Http\Message\ResponseInterface as Response; trait JsonResponseTrait { protected function jsonResponse(Response $response, mixed $data, int $status = 200): Response { $response->getBody()->write(json_encode($data, JSON_THROW_ON_ERROR)); return $response ->withHeader('Content-Type', 'application/json') ->withStatus($status); } protected function successResponse(Response $response, mixed $data = null, int $status = 200): Response { return $this->jsonResponse($response, [ 'success' => true, 'data' => $data ], $status); } protected function errorResponse(Response $response, string|array $error, int $status = 400): Response { $errorData = is_string($error) ? ['message' => $error] : $error; return $this->jsonResponse($response, [ 'success' => false, 'error' => $errorData ], $status); } } |
a samozřejmě upravíme UserController takže vypustíme funkci jsonResponse a přidáme náš trait. Takže dalírefaktroing máme za sebou a jdeme na přidání nového controlleru. V naší aplikaci budou mít jednotlivý uživatelé přiřazeny produkty, takže si nejprve do databáze přidáme tabulku pro produkty
1 2 3 4 5 6 7 8 |
CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); |
No a nyní si vytvoříme pro ně vytvoříme endpointy Vytvoříme ProductController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
<?php namespace App\Controllers; use App\Models\Product; use App\Repositories\ProductRepository; use App\Validators\ProductValidator; use App\Traits\JsonResponseTrait; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; class ProductController { use JsonResponseTrait; private ProductRepository $productRepository; private ProductValidator $validator; public function __construct( ProductRepository $productRepository, ProductValidator $validator ) { $this->productRepository = $productRepository; $this->validator = $validator; } public function getAll(Request $request, Response $response): Response { $products = $this->productRepository->findAll(); $productsArray = array_map(fn(Product $product) => $product->toArray(), $products); return $this->jsonResponse($response, $productsArray); } public function getOne(Request $request, Response $response, array $args): Response { $product = $this->productRepository->findById((int)$args['id']); if (!$product) { return $this->errorResponse($response, 'Produkt nenalezen', 404); } return $this->jsonResponse($response, $product->toArray()); } public function create(Request $request, Response $response): Response { $data = $request->getParsedBody(); // Validace $errors = $this->validator->validateCreate($data); if (!empty($errors)) { return $this->errorResponse($response, [ 'message' => 'Neplatná data', 'errors' => $errors ], 400); } try { $product = Product::fromArray($data); $newProduct = $this->productRepository->create($product); return $this->successResponse($response, $newProduct->toArray(), 201); } catch (\Exception $e) { return $this->errorResponse($response, 'Chyba při vytváření produktu', 500); } } public function update(Request $request, Response $response, array $args): Response { $id = (int)$args['id']; if (!$this->productRepository->exists($id)) { return $this->errorResponse($response, 'Produkt nenalezen', 404); } $data = $request->getParsedBody(); if (empty($data)) { return $this->errorResponse($response, 'Žádná data k aktualizaci', 400); } // Validace $errors = $this->validator->validateUpdate($data); if (!empty($errors)) { return $this->errorResponse($response, [ 'message' => 'Neplatná data', 'errors' => $errors ], 400); } try { $updatedProduct = $this->productRepository->update($id, $data); return $this->successResponse($response, $updatedProduct->toArray()); } catch (\Exception $e) { return $this->errorResponse($response, 'Chyba při aktualizaci produktu', 500); } } public function delete(Request $request, Response $response, array $args): Response { $id = (int)$args['id']; if (!$this->productRepository->exists($id)) { return $this->errorResponse($response, 'Produkt nenalezen', 404); } try { $this->productRepository->delete($id); return $this->successResponse($response, ['message' => 'Produkt byl úspěšně smazán']); } catch (\Exception $e) { return $this->errorResponse($response, 'Chyba při mazání produktu', 500); } } } |
a podobně jako u uživatelů ještě musíme přidat repository a model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
<?php namespace App\Repositories; use App\Models\Product; use PDO; class ProductRepository { private PDO $db; public function __construct(PDO $db) { $this->db = $db; } public function findAll(): array { $stmt = $this->db->query('SELECT * FROM products ORDER BY created_at DESC'); $products = $stmt->fetchAll(PDO::FETCH_ASSOC); return array_map(function ($product) { return Product::fromArray($product); }, $products); } public function findById(int $id): ?Product { $stmt = $this->db->prepare('SELECT * FROM products WHERE id = :id'); $stmt->execute(['id' => $id]); $product = $stmt->fetch(PDO::FETCH_ASSOC); return $product ? Product::fromArray($product) : null; } public function create(Product $product): Product { $stmt = $this->db->prepare(' INSERT INTO products (name, description, price, created_at) VALUES (:name, :description, :price, NOW()) '); $stmt->execute([ 'name' => $product->getName(), 'description' => $product->getDescription(), 'price' => $product->getPrice(), ]); return $this->findById((int)$this->db->lastInsertId()); } public function update(int $id, array $data): ?Product { $currentProduct = $this->findById($id); if (!$currentProduct) { return null; } $updates = []; $params = ['id' => $id]; if (isset($data['name'])) { $updates[] = 'name = :name'; $params['name'] = $data['name']; } if (array_key_exists('description', $data)) { $updates[] = 'description = :description'; $params['description'] = $data['description']; } if (isset($data['price'])) { $updates[] = 'price = :price'; $params['price'] = $data['price']; } if (empty($updates)) { return $currentProduct; } $sql = 'UPDATE products SET ' . implode(', ', $updates) . ' WHERE id = :id'; $stmt = $this->db->prepare($sql); $stmt->execute($params); return $this->findById($id); } public function delete(int $id): bool { $stmt = $this->db->prepare('DELETE FROM products WHERE id = :id'); $stmt->execute(['id' => $id]); return $stmt->rowCount() > 0; } public function exists(int $id): bool { $stmt = $this->db->prepare('SELECT 1 FROM products WHERE id = :id'); $stmt->execute(['id' => $id]); return (bool)$stmt->fetch(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
<?php namespace App\Models; class Product { private ?int $id; private string $name; private ?string $description; private float $price; private string $created_at; private ?string $updated_at; public function __construct( ?int $id = null, string $name = '', ?string $description = null, float $price = 0.0, string $created_at = '', ?string $updated_at = null ) { $this->id = $id; $this->name = $name; $this->description = $description; $this->price = $price; $this->created_at = $created_at; $this->updated_at = $updated_at; } // Gettery public function getId(): ?int { return $this->id; } public function getName(): string { return $this->name; } public function getDescription(): ?string { return $this->description; } public function getPrice(): float { return $this->price; } public function getCreatedAt(): string { return $this->created_at; } public function getUpdatedAt(): ?string { return $this->updated_at; } // Settery public function setName(string $name): void { $this->name = $name; } public function setDescription(?string $description): void { $this->description = $description; } public function setPrice(float $price): void { $this->price = $price; } public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, 'price' => $this->price, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at ]; } public static function fromArray(array $data): self { return new self( $data['id'] ?? null, $data['name'] ?? '', $data['description'] ?? null, (float)($data['price'] ?? 0), $data['created_at'] ?? date('Y-m-d H:i:s'), $data['updated_at'] ?? null ); } } |
a ještě přidáme validaci dat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
<?php namespace App\Validators; class ProductValidator { public function validateCreate(array $data): array { $errors = []; if (empty($data['name'])) { $errors['name'] = 'Název produktu je povinný'; } elseif (strlen($data['name']) > 255) { $errors['name'] = 'Název produktu nemůže být delší než 255 znaků'; } if (!isset($data['price'])) { $errors['price'] = 'Cena je povinná'; } elseif (!is_numeric($data['price'])) { $errors['price'] = 'Cena musí být číslo'; } elseif ($data['price'] < 0) { $errors['price'] = 'Cena nemůže být záporná'; } if (isset($data['description']) && strlen($data['description']) > 1000) { $errors['description'] = 'Popis nemůže být delší než 1000 znaků'; } return $errors; } public function validateUpdate(array $data): array { $errors = []; if (isset($data['name'])) { if (empty($data['name'])) { $errors['name'] = 'Název produktu nemůže být prázdný'; } elseif (strlen($data['name']) > 255) { $errors['name'] = 'Název produktu nemůže být delší než 255 znaků'; } } if (isset($data['price'])) { if (!is_numeric($data['price'])) { $errors['price'] = 'Cena musí být číslo'; } elseif ($data['price'] < 0) { $errors['price'] = 'Cena nemůže být záporná'; } } if (isset($data['description']) && strlen($data['description']) > 1000) { $errors['description'] = 'Popis nemůže být delší než 1000 znaků'; } return $errors; } } |
a samozřejmě si pořádně otestujte naše API.