Üdvözöllek a modern webfejlesztés izgalmas világában! Ha valaha is azon gondolkodtál, hogyan kommunikálnak az alkalmazások egymással, hogyan biztosítják az adatokhoz való hozzáférést a webes és mobil appok számára, akkor valószínűleg találkoztál már a REST API fogalmával. Ez a cikk egy átfogó, lépésről lépésre útmutatót nyújt ahhoz, hogy hogyan készíthetsz saját REST API-t PHP segítségével. Függetlenül attól, hogy kezdő vagy a témában, vagy szeretnéd felfrissíteni a tudásodat, itt mindent megtalálsz, amire szükséged lehet!
Mi is az a REST API és miért van rá szükségünk?
A REST (Representational State Transfer) API egy architektúra stílus, amely iránymutatásokat ad a webes szolgáltatások fejlesztéséhez. Egyszerűen fogalmazva, ez egy módja annak, hogy két számítógépes rendszer biztonságosan és hatékonyan kommunikáljon egymással az interneten keresztül. Képzeld el úgy, mint egy pincért egy étteremben: te (az ügyfél) adsz egy rendelést (HTTP kérés) neki (az API-nak), és ő továbbítja azt a konyhának (a szerver), majd visszahozza neked az ételt (az adatot).
Miért olyan népszerű a REST? Mert:
- Egyszerűség: Könnyen érthető és használható.
- Skálázhatóság: Képes kezelni a nagy mennyiségű kérést és adatforgalmat.
- Platformfüggetlenség: Bármilyen kliensről (web, mobil, desktop) elérhető, és bármilyen szerver oldali technológiával (PHP, Python, Node.js) implementálható.
- Stateless: Minden kérés független a korábbiaktól, ami javítja a skálázhatóságot és a megbízhatóságot.
Manapság szinte minden modern webes alkalmazás, mobil app és IoT eszköz REST API-n keresztül éri el a szerver oldali adatokat és funkciókat.
Miért PHP a REST API-hoz?
A PHP egy széles körben elterjedt, nyílt forráskódú szkriptnyelv, amelyet elsősorban webszerver oldali fejlesztésre használnak. Számos előnye van, ami ideális választássá teszi REST API-k készítéséhez:
- Könnyű tanulhatóság: A szintaxisa viszonylag egyszerű, így gyorsan elsajátítható.
- Nagy közösségi támogatás: Rengeteg dokumentáció, tutorial és fórum áll rendelkezésre.
- Széleskörű eszköztár: Kiterjedt függvénykönyvtárak és keretrendszerek (Laravel, Symfony) segítik a fejlesztést.
- Jó teljesítmény: A modern PHP verziók (7.x, 8.x) rendkívül gyorsak és hatékonyak.
Ebben a cikkben natív PHP-t fogunk használni, hogy mélyebben megértsd az API működésének alapjait, anélkül, hogy egy keretrendszer absztrakciói eltakarnák a lényeget.
1. Alapok és Előkészületek
A REST alapelvei röviden
Mielőtt belevágnánk, ismételjük át röviden a REST kulcsfogalmait:
- Erőforrások (Resources): Minden, amit az API kezel, egy erőforrás. Például egy felhasználó, egy termék, egy bejegyzés. Minden erőforrásnak van egy egyedi azonosítója (URI).
- URI (Uniform Resource Identifier): Az erőforrásokat azonosító URL-ek. Pl.
/api/termekek
,/api/termekek/123
. - HTTP metódusok: Standard műveleteket definiálnak az erőforrásokon.
- GET: Adatok lekérdezése.
- POST: Új erőforrás létrehozása.
- PUT/PATCH: Erőforrás frissítése.
- DELETE: Erőforrás törlése.
- Stateless: A szerver nem tárolja a kliens állapotát két kérés között. Minden kérésnek tartalmaznia kell minden szükséges információt.
- Reprezentáció: Az erőforrásokat különböző formátumokban (pl. JSON, XML) lehet reprezentálni. A JSON a legelterjedtebb.
Szükséges eszközök
Mielőtt elkezdenénk, győződj meg róla, hogy a következő eszközök telepítve vannak a gépeden:
- PHP 7.4+ vagy újabb: A legfrissebb verzió ajánlott a jobb teljesítmény és biztonság miatt.
- Webszerver: Apache vagy Nginx (XAMPP/WAMP/MAMP csomagok tartalmazzák).
- Adatbázis: MySQL vagy MariaDB (szintén XAMPP/WAMP/MAMP részei).
- Composer (opcionális, de ajánlott): A PHP függőségkezelője.
- Postman vagy Insomnia: API-k teszteléséhez elengedhetetlen eszközök.
Projektstruktúra kialakítása
A rendezett kód a jó kód alapja. Hozzuk létre a következő mappastruktúrát:
rest_api_php/ ├── config/ │ └── database.php ├── models/ │ └── Product.php ├── api/ │ ├── read.php │ ├── read_single.php │ ├── create.php │ ├── update.php │ └── delete.php ├── .htaccess └── index.php
config/database.php
: Adatbázis kapcsolati adatok.models/Product.php
: A termék entitásunk logikája és adatbázis műveletei.api/
: Itt lesznek az API végpontjaink, amelyek feldolgozzák a kéréseket..htaccess
: URL átírásokhoz az „index.php” fájlra.index.php
: A fő belépési pont és a router.
2. Az Adatbázis Felépítése
Kezdjük egy egyszerű adatbázis táblával, amit API-n keresztül kezelni fogunk. Létrehozunk egy products
nevű táblát.
Nyisd meg a phpMyAdmin-t vagy más adatbázis kliensedet, és futtasd le a következő SQL parancsot:
CREATE DATABASE IF NOT EXISTS `rest_api_db`;
USE `rest_api_db`;
CREATE TABLE IF NOT EXISTS `products` (
`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`description` TEXT NOT NULL,
`price` DECIMAL(10, 2) NOT NULL,
`category_id` INT(11) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `products` (`name`, `description`, `price`) VALUES
('Laptop', 'Erős laptop mindennapi használatra.', '1200.00'),
('Egér', 'Ergonomikus vezeték nélküli egér.', '25.00'),
('Billentyűzet', 'Mechanikus gaming billentyűzet RGB világítással.', '80.00');
Ez létrehoz egy rest_api_db
adatbázist és benne egy products
táblát néhány alap adattal. A category_id
-t most csak placeholderként használjuk, nem megyünk bele a kategóriák kezelésébe ebben a cikkben.
3. Adatbázis Kapcsolat Létrehozása
Hozzunk létre egy fájlt az adatbázis kapcsolat kezelésére a config/database.php
útvonalon.
<?php
class Database {
// Adatbázis adatok
private $host = "localhost";
private $db_name = "rest_api_db";
private $username = "root"; // Alapértelmezett XAMPP/WAMP
private $password = ""; // Alapértelmezett XAMPP/WAMP
public $conn;
// Adatbázis kapcsolat
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
$this->conn->exec("set names utf8");
} catch(PDOException $exception) {
echo "Adatbázis kapcsolódási hiba: " . $exception->getMessage();
}
return $this->conn;
}
}
?>
Ebben a kódban egy Database
osztályt definiáltunk, amely a PDO (PHP Data Objects) segítségével hoz létre és tart fenn kapcsolatot a MySQL adatbázissal. Ne felejtsd el módosítani a $username
és $password
változókat, ha nem az alapértelmezett értékeket használod!
4. Modell Osztály Létrehozása (Product.php
)
Most jöhet az erőforrásunk, a Product
osztály. Ez az osztály felelős az adatbázis műveletekért (CRUD: Create, Read, Update, Delete) a products
táblán.
Készítsd el a models/Product.php
fájlt:
<?php
class Product {
// Adatbázis kapcsolat és tábla neve
private $conn;
private $table_name = "products";
// Termék tulajdonságai
public $id;
public $name;
public $description;
public $price;
public $category_id; // Most csak place-holder
public $created_at;
public $updated_at;
// Konstruktor
public function __construct($db) {
$this->conn = $db;
}
// Termékek lekérdezése
public function read() {
$query = "SELECT id, name, description, price, category_id, created_at, updated_at
FROM " . $this->table_name . " ORDER BY created_at DESC";
$stmt = $this->conn->prepare($query);
$stmt->execute();
return $stmt;
}
// Egy termék lekérdezése ID alapján
public function read_single() {
$query = "SELECT id, name, description, price, category_id, created_at, updated_at
FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(1, $this->id);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// Tulajdonságok beállítása
if ($row) {
$this->name = $row['name'];
$this->description = $row['description'];
$this->price = $row['price'];
$this->category_id = $row['category_id'];
$this->created_at = $row['created_at'];
$this->updated_at = $row['updated_at'];
return true;
}
return false;
}
// Új termék létrehozása
public function create() {
$query = "INSERT INTO " . $this->table_name . "
SET name=:name, description=:description, price=:price, category_id=:category_id";
$stmt = $this->conn->prepare($query);
// Tisztítás és validáció
$this->name = htmlspecialchars(strip_tags($this->name));
$this->description = htmlspecialchars(strip_tags($this->description));
$this->price = htmlspecialchars(strip_tags($this->price));
$this->category_id = htmlspecialchars(strip_tags($this->category_id));
// Paraméterek bindolása
$stmt->bindParam(":name", $this->name);
$stmt->bindParam(":description", $this->description);
$stmt->bindParam(":price", $this->price);
$stmt->bindParam(":category_id", $this->category_id);
if ($stmt->execute()) {
return true;
}
printf("Hiba: %s.n", $stmt->error);
return false;
}
// Termék frissítése
public function update() {
$query = "UPDATE " . $this->table_name . "
SET name=:name, description=:description, price=:price, category_id=:category_id
WHERE id = :id";
$stmt = $this->conn->prepare($query);
// Tisztítás és validáció
$this->name = htmlspecialchars(strip_tags($this->name));
$this->description = htmlspecialchars(strip_tags($this->description));
$this->price = htmlspecialchars(strip_tags($this->price));
$this->category_id = htmlspecialchars(strip_tags($this->category_id));
$this->id = htmlspecialchars(strip_tags($this->id));
// Paraméterek bindolása
$stmt->bindParam(':name', $this->name);
$stmt->bindParam(':description', $this->description);
$stmt->bindParam(':price', $this->price);
$stmt->bindParam(':category_id', $this->category_id);
$stmt->bindParam(':id', $this->id);
if ($stmt->execute()) {
return true;
}
printf("Hiba: %s.n", $stmt->error);
return false;
}
// Termék törlése
public function delete() {
$query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
$stmt = $this->conn->prepare($query);
// Tisztítás és validáció
$this->id = htmlspecialchars(strip_tags($this->id));
// Paraméter bindolása
$stmt->bindParam(1, $this->id);
if ($stmt->execute()) {
return true;
}
printf("Hiba: %s.n", $stmt->error);
return false;
}
}
?>
Ez az osztály a CRUD műveletek logikáját foglalja magában. Fontos megjegyezni a htmlspecialchars(strip_tags())
használatát, ami segít megelőzni az XSS (Cross-Site Scripting) támadásokat azáltal, hogy eltávolítja a HTML tageket és speciális karaktereket alakít át.
5. Az API Belépési Pontja és Routing (.htaccess
és index.php
)
.htaccess
fájl
Hogy a URL-ek szépek legyenek (pl. /api/products
ahelyett, hogy /api/read.php
), szükségünk van egy .htaccess
fájlra a gyökérkönyvtárban.
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^api/(.*)$ index.php [QSA,L]
Ez a szabály átírja az összes olyan kérést, amely az /api/
előtaggal kezdődik, és nem létező fájlra vagy mappára mutat, az index.php
fájlra. A QSA
(Query String Append) biztosítja, hogy a GET paraméterek is átadódjanak, az L
(Last) pedig azt jelenti, hogy ez az utolsó szabály, amit alkalmazni kell.
index.php
(Egyszerű Router)
Ez lesz a központi fájl, amely fogadja az összes API kérést, meghatározza az URI-t és a HTTP metódust, majd meghívja a megfelelő logikát.
<?php
// Beállítjuk a Content-Type fejlécet JSON formátumra
header("Access-Control-Allow-Origin: *"); // Engedélyezzük a cross-origin kéréseket
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Szükséges fájlok importálása
include_once 'config/database.php';
include_once 'models/Product.php';
// Adatbázis kapcsolat inicializálása
$database = new Database();
$db = $database->getConnection();
// Termék objektum inicializálása
$product = new Product($db);
// URI és metódus elemzése
$request_uri = $_SERVER['REQUEST_URI'];
$request_method = $_SERVER['REQUEST_METHOD'];
// Kivágjuk az /api/ részt az URI elejéről
$uri_segments = explode('/', trim($request_uri, '/'));
$api_path = array_slice($uri_segments, array_search('api', $uri_segments) + 1);
// Az első szegmens az erőforrás neve (pl. 'products')
$resource = array_shift($api_path);
// A második szegmens az ID lehet (pl. '1')
$id = array_shift($api_path);
// Kezeljük a kéréseket az erőforrás és metódus alapján
switch ($resource) {
case 'products':
if ($request_method === 'GET') {
if ($id) {
// Egy termék lekérdezése ID alapján
$product->id = $id;
if ($product->read_single()) {
$product_arr = array(
"id" => $product->id,
"name" => $product->name,
"description" => $product->description,
"price" => $product->price,
"category_id" => $product->category_id,
"created_at" => $product->created_at,
"updated_at" => $product->updated_at
);
http_response_code(200); // OK
echo json_encode($product_arr);
} else {
http_response_code(404); // Not Found
echo json_encode(array("message" => "A termék nem található."));
}
} else {
// Összes termék lekérdezése
$stmt = $product->read();
$num = $stmt->rowCount();
if ($num > 0) {
$products_arr = array();
$products_arr["data"] = array();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
extract($row); // Kinyerjük a $row tömb elemeit változókká
$product_item = array(
"id" => $id,
"name" => $name,
"description" => $description,
"price" => $price,
"category_id" => $category_id,
"created_at" => $created_at,
"updated_at" => $updated_at
);
array_push($products_arr["data"], $product_item);
}
http_response_code(200); // OK
echo json_encode($products_arr);
} else {
http_response_code(404); // Not Found
echo json_encode(array("message" => "Nincs termék a adatbázisban."));
}
}
} elseif ($request_method === 'POST') {
// Új termék létrehozása
$data = json_decode(file_get_contents("php://input"));
if (!empty($data->name) && !empty($data->price) && !empty($data->description) && !empty($data->category_id)) {
$product->name = $data->name;
$product->price = $data->price;
$product->description = $data->description;
$product->category_id = $data->category_id;
if ($product->create()) {
http_response_code(201); // Created
echo json_encode(array("message" => "A termék sikeresen létrejött."));
} else {
http_response_code(503); // Service Unavailable
echo json_encode(array("message" => "A termék létrehozása sikertelen."));
}
} else {
http_response_code(400); // Bad Request
echo json_encode(array("message" => "Hiányzó adatok. Kérjük, adja meg a termék nevét, leírását, árát és kategória ID-t."));
}
} elseif ($request_method === 'PUT') {
// Termék frissítése
$data = json_decode(file_get_contents("php://input"));
if (!empty($id) && !empty($data->name) && !empty($data->price) && !empty($data->description) && !empty($data->category_id)) {
$product->id = $id;
$product->name = $data->name;
$product->price = $data->price;
$product->description = $data->description;
$product->category_id = $data->category_id;
if ($product->update()) {
http_response_code(200); // OK
echo json_encode(array("message" => "A termék sikeresen frissítve."));
} else {
http_response_code(503); // Service Unavailable
echo json_encode(array("message" => "A termék frissítése sikertelen."));
}
} else {
http_response_code(400); // Bad Request
echo json_encode(array("message" => "Hiányzó adatok vagy termék ID."));
}
} elseif ($request_method === 'DELETE') {
// Termék törlése
if (!empty($id)) {
$product->id = $id;
if ($product->delete()) {
http_response_code(200); // OK
echo json_encode(array("message" => "A termék sikeresen törölve."));
} else {
http_response_code(503); // Service Unavailable
echo json_encode(array("message" => "A termék törlése sikertelen."));
}
} else {
http_response_code(400); // Bad Request
echo json_encode(array("message" => "Hiányzó termék ID."));
}
} else {
http_response_code(405); // Method Not Allowed
echo json_encode(array("message" => "Nem engedélyezett HTTP metódus."));
}
break;
default:
http_response_code(404); // Not Found
echo json_encode(array("message" => "Az erőforrás nem található."));
break;
}
?>
Ez az index.php
fájl az API routere. Felelős a következőkért:
- Beállítja a HTTP fejléceket (
Content-Type
,CORS
engedélyezés). - Inicializálja az adatbázis kapcsolatot és a
Product
modellt. - Kinyeri az URI-ból az erőforrás nevét (pl.
products
) és az opcionális ID-t. - Egy
switch
utasítással elirányítja a kérést a megfelelő logikához a HTTP metódus (GET, POST, PUT, DELETE) és az erőforrás (pl.products
) alapján. - Keletkező adatokról JSON választ ad vissza, megfelelő HTTP státuszkóddal.
6. Tesztelés Postman/Insomnia segítségével
Most, hogy az API elkészült, teszteljük le! Győződj meg róla, hogy a webszervered fut, és a projekt mappája elérhető. (Pl. ha XAMPP-et használsz, a rest_api_php
mappa a htdocs
-ban van, akkor az URL valószínűleg http://localhost/rest_api_php/api/products
lesz).
GET kérések
- Összes termék lekérdezése:
- Metódus:
GET
- URL:
http://localhost/rest_api_php/api/products
- Várható válasz: JSON tömb a termékekkel, státuszkód: 200 OK.
- Metódus:
- Egy adott termék lekérdezése:
- Metódus:
GET
- URL:
http://localhost/rest_api_php/api/products/1
(az 1-es ID-jű termék) - Várható válasz: JSON objektum a termék adataival, státuszkód: 200 OK. Ha nem található, 404 Not Found.
- Metódus:
POST kérés (Új termék létrehozása)
- Metódus:
POST
- URL:
http://localhost/rest_api_php/api/products
- Headers:
Content-Type: application/json
- Body (raw, JSON):
{ "name": "Okostelefon", "description": "A legújabb okostelefon modell.", "price": "799.99", "category_id": "1" }
- Várható válasz:
{"message": "A termék sikeresen létrejött."}
, státuszkód: 201 Created.
PUT kérés (Termék frissítése)
- Metódus:
PUT
- URL:
http://localhost/rest_api_php/api/products/4
(a 4-es ID-jű termék frissítése) - Headers:
Content-Type: application/json
- Body (raw, JSON):
{ "name": "Okostelefon (Frissítve)", "description": "A legújabb okostelefon modell, javított akkumulátorral.", "price": "850.00", "category_id": "1" }
- Várható válasz:
{"message": "A termék sikeresen frissítve."}
, státuszkód: 200 OK.
DELETE kérés (Termék törlése)
- Metódus:
DELETE
- URL:
http://localhost/rest_api_php/api/products/4
(a 4-es ID-jű termék törlése) - Várható válasz:
{"message": "A termék sikeresen törölve."}
, státuszkód: 200 OK.
7. Hibakezelés és HTTP Státuszkódok
Az API válaszainak nem csupán az adatokat kell tartalmazniuk, hanem információt is arról, hogy a kérés sikeres volt-e, vagy valamilyen hiba történt. Erre szolgálnak a HTTP státuszkódok:
- 200 OK: A kérés sikeres volt.
- 201 Created: Sikeres erőforrás létrehozás.
- 400 Bad Request: A kérés szintaktikailag hibás volt, vagy hiányzó paramétereket tartalmazott.
- 401 Unauthorized: A kéréshez hitelesítés szükséges.
- 403 Forbidden: A kliens nem jogosult a kérés végrehajtására.
- 404 Not Found: Az erőforrás nem található.
- 405 Method Not Allowed: A használt HTTP metódus nem engedélyezett az adott erőforráson.
- 500 Internal Server Error: Váratlan szerver oldali hiba történt.
- 503 Service Unavailable: A szerver ideiglenesen nem érhető el (pl. túlterheltség miatt).
A fenti példakód már használja ezeket a kódokat, és egységes JSON hibaüzeneteket küld vissza.
8. Autentikáció és Autorizáció (Rövid áttekintés)
Egy valós API-ban elengedhetetlen a biztonság. Jelenlegi API-nk bárki számára hozzáférhető. Néhány elterjedt módszer az autentikációra:
- API Kulcsok: A kliensek egy egyedi kulcsot küldenek minden kérésnél (pl. a HTTP fejlécben vagy URL paraméterként), amit a szerver ellenőriz. Egyszerű, de kevésbé biztonságos.
- OAuth 2.0: Egy iparági szabvány az autorizációhoz, amely lehetővé teszi a kliensek számára, hogy delegált hozzáférést kapjanak a felhasználó adataihoz anélkül, hogy a felhasználónév-jelszó párost megosztanák.
- JWT (JSON Web Tokens): A kliens bejelentkezés után kap egy titkosított tokent, amit minden további kérésnél elküld. A token tartalmazza a felhasználó adatait és aláírását, így a szerver ellenőrizni tudja annak érvényességét. Ez az egyik legnépszerűbb módszer modern REST API-kban.
Egy egyszerű API kulcs ellenőrzés hozzáadása például a index.php
elejére:
// ...
// Ellenőrizzük az API kulcsot
$api_key = $_SERVER['HTTP_X_API_KEY'] ?? ''; // Feltételezve, hogy 'X-API-KEY' fejlécben küldik
$valid_api_key = "my_super_secret_api_key"; // Ez valós esetben egy adatbázisból jönne
if ($api_key !== $valid_api_key) {
http_response_code(401);
echo json_encode(array("message" => "Érvénytelen API kulcs. Hozzáférés megtagadva."));
exit();
}
// ... további API logika
Ez egy nagyon alapvető példa, valós környezetben sokkal robusztusabb megoldásra van szükség!
Összefoglalás és További Lépések
Gratulálok! Sikeresen elkészítetted az első REST API-dat PHP segítségével, ami képes CRUD műveletek végrehajtására egy adatbázison. Megtanultad az alapvető REST elveket, hogyan strukturáld a projektedet, hogyan kezeld az adatbázis kapcsolatot, modelleket, routingot, és hogyan adj vissza JSON válaszokat megfelelő HTTP státuszkódokkal.
Mire figyelj még?
- Validáció: A bejövő adatok (pl. POST/PUT kérések) alaposabb validálása (pl. szám-e az ár, érvényes email cím-e).
- Rate Limiting: Korlátozd, hogy egy kliens mennyi kérést küldhet egy adott időn belül, hogy megelőzd a DoS támadásokat és a szerver túlterhelését.
- Hiba naplózás: Részletes hibanaplózás segíti a problémák felderítését.
- Dokumentáció: Egy jó API-nak mindig van dokumentációja (pl. Swagger/OpenAPI), ami leírja a végpontokat, paramétereket és válaszokat.
- Tesztelés: Unit és integrációs tesztek írása a kód megbízhatóságának növelése érdekében.
- Keretrendszerek: Valós projektekben érdemes megfontolni egy PHP keretrendszer (pl. Laravel Lumen, Slim Framework, Symfony) használatát, mivel ezek rengeteg funkcionalitást (routing, ORM, autentikáció, validáció) biztosítanak „dobozból”, felgyorsítva a fejlesztést és növelve a kód minőségét.
Remélem, ez a részletes útmutató segített megérteni a REST API-k működését és elindított a PHP API fejlesztés útján. Sok sikert a további projektekhez!
Leave a Reply