基于PHP无框架构建简易REST API技术问询
Hey, totally get where you're coming from—sometimes ditching frameworks and building an API from scratch gives you way more control over every detail, plus you really learn the ins and outs of how things work under the hood. Ditching a full MVC setup was a smart call too; for a lightweight REST API, a layered architecture is way more flexible and avoids unnecessary bloat. Let's walk through a clean, OOP-based implementation that checks all your boxes:
We'll use a modular, layered structure (no unnecessary view layer since we're returning JSON):
- Database Layer: Singleton PDO connection to avoid duplicate connections and prevent SQL injection
- Model Layer: Maps directly to database tables, handles all CRUD operations
- Validation Layer: Reusable class for input validation logic
- Controller Layer: Handles request logic, coordinates models/validators, and sends responses
- Router Layer: Parses HTTP methods/URLs and routes requests to the right controller action
1. Database Connection (Singleton PDO)
Create Database.php—this ensures we only have one active DB connection at a time, and uses PDO for secure database interactions:
<?php class Database { private static $instance = null; private $connection; // Private constructor to enforce singleton pattern private function __construct() { $config = [ 'host' => 'localhost', 'dbname' => 'your_database', 'username' => 'db_user', 'password' => 'db_password' ]; try { $this->connection = new PDO( "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4", $config['username'], $config['password'] ); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); } catch(PDOException $e) { $this->sendErrorResponse(500, "Database connection failed: {$e->getMessage()}"); } } public static function getInstance() { if (!self::$instance) { self::$instance = new Database(); } return self::$instance->connection; } private function sendErrorResponse($statusCode, $message) { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode(['error' => $message]); exit; } } ?>
2. Model Layer (Example: User Model)
Create UserModel.php—this handles all data operations for the users table, with secure practices like password hashing:
<?php class UserModel { private $db; public function __construct() { $this->db = Database::getInstance(); } public function getAllUsers() { $stmt = $this->db->query("SELECT id, name, email FROM users"); return $stmt->fetchAll(); } public function getUserById($id) { $stmt = $this->db->prepare("SELECT id, name, email FROM users WHERE id = :id"); $stmt->bindParam(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(); } public function createUser($data) { $stmt = $this->db->prepare("INSERT INTO users (name, email, password) VALUES (:name, :email, :password)"); $hashedPassword = password_hash($data['password'], PASSWORD_DEFAULT); $stmt->bindParam(':name', $data['name']); $stmt->bindParam(':email', $data['email']); $stmt->bindParam(':password', $hashedPassword); return $stmt->execute(); } } ?>
3. Validation Layer
Create Validator.php—a reusable class to keep validation logic separate from controllers:
<?php class Validator { public static function validateUserInput($data) { $errors = []; if (empty($data['name'])) { $errors[] = "Name is required"; } elseif (strlen($data['name']) < 2) { $errors[] = "Name must be at least 2 characters"; } if (empty($data['email'])) { $errors[] = "Email is required"; } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors[] = "Invalid email format"; } if (empty($data['password'])) { $errors[] = "Password is required"; } elseif (strlen($data['password']) < 6) { $errors[] = "Password must be at least 6 characters"; } return $errors; } } ?>
4. Controller Layer (Example: User Controller)
Create UserController.php—this handles request logic, uses the validator and model, and sends consistent JSON responses:
<?php class UserController { private $userModel; public function __construct() { $this->userModel = new UserModel(); } public function index() { $users = $this->userModel->getAllUsers(); $this->sendSuccessResponse(200, $users); } public function show($id) { $user = $this->userModel->getUserById($id); if (!$user) { $this->sendErrorResponse(404, "User not found"); return; } $this->sendSuccessResponse(200, $user); } public function store() { // Parse JSON input $input = json_decode(file_get_contents('php://input'), true); if (!$input) { $this->sendErrorResponse(400, "Invalid JSON input"); return; } // Validate input $errors = Validator::validateUserInput($input); if (!empty($errors)) { $this->sendErrorResponse(400, ['errors' => $errors]); return; } // Create user $success = $this->userModel->createUser($input); if ($success) { $this->sendSuccessResponse(201, [ 'message' => "User created successfully", 'user_id' => $this->userModel->db->lastInsertId() ]); } else { $this->sendErrorResponse(500, "Failed to create user"); } } private function sendSuccessResponse($statusCode, $data) { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode(['data' => $data]); } private function sendErrorResponse($statusCode, $message) { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode(['error' => $message]); } } ?>
5. Router Layer
Create Router.php—this parses incoming requests and routes them to the correct controller action:
<?php class Router { private $routes = []; public function addRoute($method, $path, $controller, $action) { $this->routes[] = [ 'method' => strtoupper($method), 'path' => $this->normalizePath($path), 'controller' => $controller, 'action' => $action ]; } private function normalizePath($path) { return rtrim($path, '/'); } public function dispatch() { $requestMethod = strtoupper($_SERVER['REQUEST_METHOD']); $requestPath = $this->normalizePath($_SERVER['REQUEST_URI']); foreach ($this->routes as $route) { // Convert route parameters (e.g., /users/{id}) to regex $routePattern = preg_replace('/\{(\w+)\}/', '(\d+)', $route['path']); if (preg_match("#^$routePattern$#", $requestPath, $matches) && $route['method'] === $requestMethod) { array_shift($matches); // Remove full path match $controller = new $route['controller'](); call_user_func_array([$controller, $route['action']], $matches); return; } } // 404 if no route matches $this->sendNotFoundResponse(); } private function sendNotFoundResponse() { http_response_code(404); header('Content-Type: application/json'); echo json_encode(['error' => "Route not found"]); } } ?>
6. Entry Point (index.php)
This is the main file that boots up the API:
<?php // Simple autoloader for our classes spl_autoload_register(function ($className) { $file = __DIR__ . "/{$className}.php"; if (file_exists($file)) { require_once $file; } }); // Add CORS headers if you need cross-origin support header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE"); header("Access-Control-Allow-Headers: Content-Type"); // Initialize router and define routes $router = new Router(); $router->addRoute('GET', '/users', 'UserController', 'index'); $router->addRoute('GET', '/users/{id}', 'UserController', 'show'); $router->addRoute('POST', '/users', 'UserController', 'store'); // Dispatch the request $router->dispatch(); ?>
- SQL Injection Protection: We use PDO prepared statements everywhere—never concatenate user input into SQL queries
- Password Security: Passwords are hashed with
password_hash()(never store plain text) - Input Validation: All user input is validated before hitting the database
- Error Handling: Consistent JSON error responses with appropriate HTTP status codes
- Extensibility: Adding new endpoints only requires creating a new model/controller and registering a route—no core code changes needed
This setup is clean, maintainable, and fully aligned with your requirements. If you need to add features like authentication, pagination, or update/delete endpoints, you can extend this structure easily.
内容的提问来源于stack exchange,提问作者Todor Atanasov




