This commit is contained in:
Philippe Torrel
2026-08-31 12:03:08 +02:00
parent bc4d5a41fd
commit 0bdbf245ee
24 changed files with 2099 additions and 17 deletions

View File

@@ -4,7 +4,19 @@ import RowProduct from './content/RowProduct';
// Reducer (bei größerer Applikaitonsstuktur ausgelagert) // Reducer (bei größerer Applikaitonsstuktur ausgelagert)
const initialState = (props) => { const initialState = (props) => {
// Validierung und Error Checks // Validierung und Error Checks
return { items: props.items || [] }; items.filter((item) => !Object.hasOwn(item, _id));
return {
items: props.items || [
{
_id: '1',
title: 'Example Product 1',
sku: 'ABC1234',
stock: 2,
price: 15.2,
},
],
};
}; };
const reducer = (state, action) => { const reducer = (state, action) => {
@@ -65,9 +77,9 @@ const TableProducts = (props) => {
const handleAmount = useCallback((amount, id) => { const handleAmount = useCallback((amount, id) => {
dispatch({ type: 'UPDATE_AMOUNT', payload: { amount, id } }); dispatch({ type: 'UPDATE_AMOUNT', payload: { amount, id } });
// setItems((prevItems) => { setItems((prevItems) => {
// return prevItems.map((item) => (item._id === id ? { ...item, stock: amount } : item)); return prevItems.map((item) => (item._id === id ? { ...item, stock: amount } : item));
// }); });
}, []); }, []);
const handlePrice = useCallback((price, id) => { const handlePrice = useCallback((price, id) => {

View File

@@ -14,7 +14,7 @@
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.2" "react-router-dom": "^7.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
@@ -1963,9 +1963,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.2", "version": "7.18.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz",
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -1985,12 +1985,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.18.2", "version": "7.18.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz",
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.18.2" "react-router": "7.18.3"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

View File

@@ -16,7 +16,7 @@
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.2" "react-router-dom": "^7.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.18", "@types/react": "^19.2.18",

View File

@@ -3,8 +3,9 @@ import RowProduct from './content/RowProduct';
import { ProductsProvider, useProductsState } from '../../stores/products'; import { ProductsProvider, useProductsState } from '../../stores/products';
const TableProductsView = (props) => { const TableProductsView = (props) => {
const { items } = useProductsState(); const { items } = useProductsState(); // const {items} = useContext(ProductsStateContext);
// const dispatch = useProductsDispatch()
// const dispatch = useProductsDispatch() // const dispatch = useContext(ProductsDispatchContext);
//const [{ items }, dispatch] = useReducer(reducer, initialState(props)); //const [{ items }, dispatch] = useReducer(reducer, initialState(props));

View File

@@ -1,7 +1,8 @@
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProductsActions } from '../../../stores/products'; // /index.js muss nicht angegeben werden (wg. Bundler) import { useProductsActions } from '../../../stores/products'; // /index.js muss nicht angegeben werden (wg. Bundler)
// import { useProductsDispatch } from '../../../stores/products'; // import { useProductsDispatch } from '../../../stores/products';
// import { PRODUCTS_ACTIONS } from '../../../stores/products'; //import { PRODUCTS_ACTIONS } from '../../../stores/products';
import InputAmount from '../../inputs/InputAmount'; import InputAmount from '../../inputs/InputAmount';
import InputPrice from '../../inputs/InputPrice'; import InputPrice from '../../inputs/InputPrice';
@@ -11,7 +12,7 @@ const RowProduct = (props) => {
//const { sku = '', title = '' } = props; //const { sku = '', title = '' } = props;
const { updateAmount, updatePrice } = useProductsActions(); const { updateAmount, updatePrice } = useProductsActions();
// const dispatch = useProductsDispatch(); // const dispatch = useProductsDispatch(); // const dispatch = useContext(ProductsDispatchContext)
// Eventhandler // Eventhandler
// const handleAmount = useCallback((val) => { // const handleAmount = useCallback((val) => {
@@ -23,6 +24,7 @@ const RowProduct = (props) => {
const handleAmount = useCallback( const handleAmount = useCallback(
(val) => { (val) => {
updateAmount(val, _id); updateAmount(val, _id);
// dispatch({type: PRODUCTS_ACTIONS.UPDATE_AMOUNT, payload: {amount: val, id: _id}})
}, },
[updateAmount, _id], [updateAmount, _id],
); );

View File

@@ -44,6 +44,7 @@ export const useProductsDispatch = () => {
return dispatch; return dispatch;
}; };
// Factory Function
export const useProductsActions = () => { export const useProductsActions = () => {
const dispatch = useProductsDispatch(); const dispatch = useProductsDispatch();

View File

@@ -0,0 +1,6 @@
DB_HOST=127.0.0.1
DB_PORT=8889
DB_NAME=nerdshop
DB_USER=root
DB_PASS=root
CORS_ORIGIN=http://localhost:5173

View File

@@ -0,0 +1,2 @@
/vendor/
.env

View File

@@ -0,0 +1,100 @@
## MacOS Installation
Um die PHP-Version und Composer aus MAMP standardmäßig in deinem Terminal über die `~/.bashrc` zu nutzen, fügst du den Pfad zur gewünschten PHP-Version deinem `$PATH` voran.
**1. MAMP PHP-Version ermitteln**
Prüfe im Finder unter `/Applications/MAMP/bin/php/`, welche Versionen installiert sind (z. B. `php8.2.0`).
**2. `.bashrc` bearbeiten**
Öffne die Datei im Editor:
```bash
nano ~/.bashrc
```
Füge am Ende folgende Zeilen ein (ersetze `php8.2.0` durch deine tatsächliche Version):
```bash
# MAMP PHP zum PATH hinzufügen
export PATH="/Applications/MAMP/bin/php/php8.2.0/bin:$PATH"
# Falls Composer separat als .phar oder Skript liegt, optional ein Alias setzen:
# alias composer="/Applications/MAMP/bin/php/php8.2.0/bin/php /usr/local/bin/composer"
```
**3. Änderungen anwenden**
Lade die Konfiguration neu:
```bash
source ~/.bashrc
```
**4. Überprüfen**
Teste, ob die Pfade korrekt geladen werden:
```bash
which php
php -v
composer -v
```
> **Hinweis für macOS:** Standardmäßig nutzt das Terminal unter macOS seit macOS Catalina `zsh` statt `bash`. Falls deine Änderungen nach einem Terminal-Neustart nicht greifen, trage die gleichen Zeilen stattdessen in deine `~/.zshrc` ein.
## Windows Installation
Unter Windows nutzt du für Bash in der Regel **Git Bash** oder **WSL**. In Git Bash entspricht `~/.bashrc` der Datei `C:\Users\<DeinBenutzer>\.bashrc`.
**1. MAMP PHP-Version ermitteln**
Prüfe unter `C:\MAMP\bin\php\`, welche PHP-Version installiert ist (z. B. `php8.2.0`).
**2. `.bashrc` in Git Bash anpassen**
Öffne die Datei in Git Bash:
```bash
code ~/.bashrc
```****
Füge folgende Zeilen ein (Pfad an deine PHP-Version anpassen):
```bash
# MAMP PHP zum PATH hinzufügen (Windows-Pfad im POSIX-Format)
export PATH="/c/MAMP/bin/php/php8.2.0:$PATH"
# Composer Alias (falls composer.phar im MAMP-Verzeichnis oder Benutzerverzeichnis liegt)
alias composer="/c/MAMP/bin/php/php8.2.0/php.exe /c/ProgramData/ComposerSetup/bin/composer.phar"
```
> Falls Composer als Windows-Batch-Datei (`composer.bat`) installiert ist, reicht bereits der PHP-Eintrag im `PATH`, da Git Bash installierte Windows-Programme direkt aufrufen kann.
**3. Änderungen anwenden**
```bash
source ~/.bashrc
```
**4. Überprüfen**
```bash
which php
php -v
composer --version
```
---
**Alternative: Systemweit für CMD / PowerShell setzen**
Falls du PHP auch in der Windows Eingabeaufforderung oder PowerShell nutzen möchtest:
1. `Win + R` drücken, `sysdm.cpl` eingeben und Enter drücken.
2. Reiter **Erweitert****Umgebungsvariablen**.
3. Unter **Systemvariablen** (oder **Benutzervariablen**) die Variable `Path` auswählen und auf **Bearbeiten** klicken.
4. Neuen Eintrag hinzufügen: `C:\MAMP\bin\php\php8.2.0` und ganz nach oben schieben.

View File

@@ -0,0 +1,28 @@
Die MySQL-Datenbank und die Slim-REST-API sind eingerichtet und getestet.
**Datenbank `nerdshop`** (MAMP, Port 8889) enthält die Tabelle `products` mit den 13 Einträgen aus der JSON-Datei. Die Mongo-IDs bleiben als `id` erhalten; die API liefert sie als `_id`, damit die React-App kompatibel bleibt.
**API läuft auf** [http://localhost:8080](http://localhost:8080)
| Methode | URL | Status |
| -------- | ---------------- | ------------- |
| `GET` | `/products` | alle Produkte |
| `GET` | `/products/{id}` | ein Produkt |
| `POST` | `/products` | anlegen |
| `PUT` | `/products/{id}` | aktualisieren |
| `DELETE` | `/products/{id}` | löschen |
CORS ist für `http://localhost:5173` (Vite) gesetzt. In der React-App reicht später:
```js
fetch('http://localhost:8080/products');
```
Server neu starten:
```bash
cd slim-php-framework
php -S localhost:8080 -t public public/router.php
```
Falls MAMP andere Zugangsdaten nutzt, stehen sie in `slim-php-framework/.env`.

View File

@@ -0,0 +1,22 @@
{
"name": "nerdshop/slim-api",
"version": "1.0.0",
"description": "Nerdshop REST API",
"require": {
"php": "^8.1",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"php-di/php-di": "^7.0",
"slim/psr7": "^1.7",
"slim/slim": "^4.14",
"vlucas/phpdotenv": "^5.6"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"sort-packages": true
}
}

1354
webseite-react-php/slim-php/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use App\Config;
use App\Middleware\CorsMiddleware;
use App\Repository\ProductRepository;
use DI\Container;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
$dotenv->safeLoad();
$container = new Container();
$container->set(PDO::class, static fn () => Config::pdo());
$container->set(
ProductRepository::class,
static fn ($c) => new ProductRepository($c->get(PDO::class))
);
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);
$app->add(new CorsMiddleware());
(require __DIR__ . '/../src/Routes/productRoutes.php')($app);
$app->run();

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
$uri = urldecode(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/');
if ($uri !== '/' && file_exists(__DIR__ . $uri)) {
return false;
}
require __DIR__ . '/index.php';

View File

@@ -32,3 +32,26 @@ INSERT INTO products (id, title, sku, stock, price, description, tagline) VALUES
('675bee96bb39023b77e6cda3', 'Automatic Plant Waterer with Compliment Dispenser', 'GRD0334', 17, 69.99, 'Keep your plants happy and hydrated with automatic watering and daily compliments.', 'Nurture your plants, boost their self-esteem.'), ('675bee96bb39023b77e6cda3', 'Automatic Plant Waterer with Compliment Dispenser', 'GRD0334', 17, 69.99, 'Keep your plants happy and hydrated with automatic watering and daily compliments.', 'Nurture your plants, boost their self-esteem.'),
('675bee96bb39023b77e6cda4', 'Self-Stirring Cereal Bowl', 'KIT0445', 55, 24.95, 'Enjoy perfectly crunchy cereal every time with this self-stirring bowl.', 'No more soggy surprises.'), ('675bee96bb39023b77e6cda4', 'Self-Stirring Cereal Bowl', 'KIT0445', 55, 24.95, 'Enjoy perfectly crunchy cereal every time with this self-stirring bowl.', 'No more soggy surprises.'),
('675bee96bb39023b77e6cda5', 'Polygon Planet Lamp', 'LMP0556', 4, 39.90, 'Transform your space with the enchanting glow of a planetarium-inspired lamp.', 'Bring the cosmos into your home.'); ('675bee96bb39023b77e6cda5', 'Polygon Planet Lamp', 'LMP0556', 4, 39.90, 'Transform your space with the enchanting glow of a planetarium-inspired lamp.', 'Bring the cosmos into your home.');
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
token VARCHAR(64) NULL DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_users_username (username),
UNIQUE KEY uq_users_email (email),
KEY idx_users_token (token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO users (username, email, password_hash)
VALUES (
'megaman',
'order@gkontra.de',
'$2y$12$CdOg.e08jP968kQoA298Ne92r9pyEYBfSya4gFWQCh.X8gJhmDaVW'
)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
password_hash = VALUES(password_hash);

View File

@@ -0,0 +1,24 @@
USE nerdshop;
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
token VARCHAR(64) NULL DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_users_username (username),
UNIQUE KEY uq_users_email (email),
KEY idx_users_token (token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO users (username, email, password_hash)
VALUES (
'megaman',
'order@gkontra.de',
'$2y$12$CdOg.e08jP968kQoA298Ne92r9pyEYBfSya4gFWQCh.X8gJhmDaVW'
)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
password_hash = VALUES(password_hash);

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App;
use PDO;
final class Config
{
public static function pdo(): PDO
{
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
$_ENV['DB_HOST'] ?? '127.0.0.1',
$_ENV['DB_PORT'] ?? '3306',
$_ENV['DB_NAME'] ?? 'nerdshop'
);
return new PDO(
$dsn,
$_ENV['DB_USER'] ?? 'root',
$_ENV['DB_PASS'] ?? '',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Domain;
final class Product
{
public function __construct(
public string $id,
public string $title,
public string $sku,
public int $stock,
public float $price,
public string $description,
public string $tagline,
) {
}
public static function fromRow(array $row): self
{
return new self(
id: $row['id'],
title: $row['title'],
sku: $row['sku'],
stock: (int) $row['stock'],
price: (float) $row['price'],
description: $row['description'],
tagline: $row['tagline'],
);
}
/** Kompatibel mit der React-App (_id statt id). */
public function toArray(): array
{
return [
'_id' => $this->id,
'title' => $this->title,
'sku' => $this->sku,
'stock' => $this->stock,
'price' => $this->price,
'description' => $this->description,
'tagline' => $this->tagline,
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Domain;
final class User
{
public function __construct(
public int $id,
public string $username,
public string $email,
public string $passwordHash,
public ?string $token,
public string $createdAt,
) {
}
public static function fromRow(array $row): self
{
return new self(
id: (int) $row['id'],
username: $row['username'],
email: $row['email'],
passwordHash: $row['password_hash'],
token: $row['token'] ?: null,
createdAt: $row['created_at'],
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'username' => $this->username,
'email' => $this->email,
'created_at' => $this->createdAt,
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Slim\Psr7\Response as SlimResponse;
final class CorsMiddleware implements MiddlewareInterface
{
public function process(Request $request, Handler $handler): Response
{
$origin = $_ENV['CORS_ORIGIN'] ?? 'http://localhost:5173';
if ($request->getMethod() === 'OPTIONS') {
$response = new SlimResponse(204);
} else {
$response = $handler->handle($request);
}
return $response
->withHeader('Access-Control-Allow-Origin', $origin)
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Domain\Product;
use PDO;
final class ProductRepository
{
public function __construct(private PDO $pdo)
{
}
/** @return Product[] */
public function findAll(): array
{
$stmt = $this->pdo->query('SELECT * FROM products ORDER BY sku');
return array_map(Product::fromRow(...), $stmt->fetchAll());
}
public function findById(string $id): ?Product
{
$stmt = $this->pdo->prepare('SELECT * FROM products WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch();
return $row ? Product::fromRow($row) : null;
}
public function create(array $data): Product
{
$id = $data['_id'] ?? $this->generateId();
$stmt = $this->pdo->prepare(
'INSERT INTO products (id, title, sku, stock, price, description, tagline)
VALUES (:id, :title, :sku, :stock, :price, :description, :tagline)'
);
$stmt->execute([
'id' => $id,
'title' => $data['title'],
'sku' => $data['sku'],
'stock' => (int) $data['stock'],
'price' => (float) $data['price'],
'description' => $data['description'],
'tagline' => $data['tagline'],
]);
$product = $this->findById($id);
if ($product === null) {
throw new \RuntimeException('Product could not be created');
}
return $product;
}
public function update(string $id, array $data): ?Product
{
if ($this->findById($id) === null) {
return null;
}
$stmt = $this->pdo->prepare(
'UPDATE products
SET title = :title, sku = :sku, stock = :stock,
price = :price, description = :description, tagline = :tagline
WHERE id = :id'
);
$stmt->execute([
'id' => $id,
'title' => $data['title'],
'sku' => $data['sku'],
'stock' => (int) $data['stock'],
'price' => (float) $data['price'],
'description' => $data['description'],
'tagline' => $data['tagline'],
]);
return $this->findById($id);
}
public function patch(string $id, array $data): ?Product
{
$current = $this->findById($id);
if ($current === null) {
return null;
}
return $this->update($id, [
'title' => $data['title'] ?? $current->title,
'sku' => $data['sku'] ?? $current->sku,
'stock' => array_key_exists('stock', $data) ? (int) $data['stock'] : $current->stock,
'price' => array_key_exists('price', $data) ? (float) $data['price'] : $current->price,
'description' => $data['description'] ?? $current->description,
'tagline' => $data['tagline'] ?? $current->tagline,
]);
}
public function delete(string $id): bool
{
$stmt = $this->pdo->prepare('DELETE FROM products WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->rowCount() > 0;
}
private function generateId(): string
{
return bin2hex(random_bytes(12));
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Domain\User;
use PDO;
final class UserRepository
{
public function __construct(private PDO $pdo)
{
}
/** @return User[] */
public function findAll(): array
{
$stmt = $this->pdo->query('SELECT * FROM users ORDER BY username');
return array_map(User::fromRow(...), $stmt->fetchAll());
}
public function findById(int $id): ?User
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch();
return $row ? User::fromRow($row) : null;
}
public function findByUsernameOrEmail(string $login): ?User
{
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE username = :login OR email = :login LIMIT 1'
);
$stmt->execute(['login' => $login]);
$row = $stmt->fetch();
return $row ? User::fromRow($row) : null;
}
}

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
use App\Repository\ProductRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;
return function (App $app): void {
$container = $app->getContainer();
if ($container === null) {
throw new RuntimeException('DI container is not configured');
}
$json = static function (Response $response, mixed $data, int $status = 200): Response {
$response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($status);
};
$requiredFields = ['title', 'sku', 'stock', 'price', 'description', 'tagline'];
$validate = static function (array $data) use ($requiredFields): ?string {
foreach ($requiredFields as $field) {
if (!isset($data[$field]) || $data[$field] === '') {
return "Missing field: {$field}";
}
}
return null;
};
$app->get('/', function (Request $request, Response $response) use ($json): Response {
return $json($response, [
'name' => 'Nerdshop API',
'endpoints' => [
'GET /products',
'GET /products/{id}',
'POST /products',
'PUT /products/{id}',
'PATCH /products/{id}',
'DELETE /products/{id}',
],
]);
});
// app.get('/products', (req,res) => {
// const products = productManager.getAll();
// res.send(products);
// })
$app->get('/products', function (Request $request, Response $response) use ($container, $json): Response {
$repo = $container->get(ProductRepository::class);
$items = array_map(static fn($product) => $product->toArray(), $repo->findAll());
return $json($response, $items);
});
$app->get('/products/{id}', function (Request $request, Response $response, array $args) use ($container, $json): Response {
$product = $container->get(ProductRepository::class)->findById($args['id']);
if ($product === null) {
return $json($response, ['error' => 'Product not found'], 404);
}
return $json($response, $product->toArray());
});
$app->post('/products', function (Request $request, Response $response) use ($container, $json, $validate): Response {
$data = (array) $request->getParsedBody();
$error = $validate($data);
if ($error !== null) {
return $json($response, ['error' => $error], 400);
}
try {
$product = $container->get(ProductRepository::class)->create($data);
} catch (\PDOException $exception) {
if ((int) $exception->getCode() === 23000) {
return $json($response, ['error' => 'SKU already exists'], 409);
}
throw $exception;
}
return $json($response, $product->toArray(), 201);
});
$app->put('/products/{id}', function (Request $request, Response $response, array $args) use ($container, $json, $validate): Response {
$data = (array) $request->getParsedBody();
$error = $validate($data);
if ($error !== null) {
return $json($response, ['error' => $error], 400);
}
try {
$product = $container->get(ProductRepository::class)->update($args['id'], $data);
} catch (\PDOException $exception) {
if ((int) $exception->getCode() === 23000) {
return $json($response, ['error' => 'SKU already exists'], 409);
}
throw $exception;
}
if ($product === null) {
return $json($response, ['error' => 'Product not found'], 404);
}
return $json($response, $product->toArray());
});
$app->patch('/products/{id}', function (Request $request, Response $response, array $args) use ($container, $json): Response {
$data = (array) $request->getParsedBody();
$allowed = ['title', 'sku', 'stock', 'price', 'description', 'tagline'];
$patch = array_intersect_key($data, array_flip($allowed));
if ($patch === []) {
return $json($response, ['error' => 'No fields to update'], 400);
}
try {
$product = $container->get(ProductRepository::class)->patch($args['id'], $patch);
} catch (\PDOException $exception) {
if ((int) $exception->getCode() === 23000) {
return $json($response, ['error' => 'SKU already exists'], 409);
}
throw $exception;
}
if ($product === null) {
return $json($response, ['error' => 'Product not found'], 404);
}
return $json($response, $product->toArray());
});
$app->delete('/products/{id}', function (Request $request, Response $response, array $args) use ($container, $json): Response {
$deleted = $container->get(ProductRepository::class)->delete($args['id']);
if (!$deleted) {
return $json($response, ['error' => 'Product not found'], 404);
}
return $response->withStatus(204);
});
};