第4章:核心构建:创建你的第一个PHP RESTful API
章节介绍
章节学习目标:
在本章中,你将首次将前三章所学的知识进行整合,在不依赖任何框架的情况下,从零开始构建一个完整的RESTful API.你将学会如何处理不同的HTTP请求、安全地操作数据库、接收和发送标准化的JSON数据,并返回恰当的HTTP状态码.通过本章的学习,你将具备构建基本数据接口的核心能力.
在教程中的作用:
本章是连接理论知识与实战应用的桥梁,是教程的核心实践环节.之前你学习了PHP语法、数据库操作和HTTP/RESTful理论,本章将把这些知识点串联起来,形成完整的API开发技能链.掌握本章内容,意味着你已经从"知道概念"进阶到"能够创造".
与前面章节的衔接:
- 第1章:使用PHP基础语法编写控制逻辑和函数.
- 第2章:使用PDO扩展安全地连接MySQL数据库并执行CRUD操作.
- 第3章:遵循RESTful风格设计API的URI、HTTP方法和状态码.
- 第4章:整合以上所有技能,构建可工作的API接口.
本章主要内容概览:
- 学习如何通过PHP原生方式处理HTTP请求,包括方法判断和数据获取.
- 将数据库CRUD操作与HTTP请求进行映射(如POST对应CREATE,GET对应READ).
- 学习如何设置HTTP响应头和输出标准化的JSON数据.
- 完成一个"学生信息管理"的完整API项目.
- 使用Postman工具进行专业的API接口测试.
核心概念讲解
1. 路由分发:API的"交通警察"
在Web应用中,路由(Routing)是指根据用户请求的URL和HTTP方法,将其引导至相应的处理代码的过程.在没有框架的情况下,我们可以通过一个入口文件(如api.php)和PHP的超全局变量$_SERVER来实现简单的路由.
关键变量:
$_SERVER['REQUEST_METHOD']:获取当前请求的HTTP方法(如GET、POST).$_SERVER['PATH_INFO']或$_SERVER['REQUEST_URI']:获取请求的路径信息,用于区分不同的资源端点(如/students与/students/1).
应用场景:
一个API入口文件需要像交通警察一样,根据请求的"目的地"(URI)和"方式"(HTTP方法),决定调用哪段处理逻辑.
注意事项:
- 对
$_SERVER['REQUEST_METHOD']的判断通常不区分大小写,但统一使用大写进行比较是良好实践. - 需要妥善处理不支持的HTTP方法或无效的URI,返回
405 Method Not Allowed或404 Not Found状态码.
2. 接收请求数据:API的"信息接收员"
客户端(如前端页面、移动App)通过请求向API发送数据.PHP提供了多种方式接收这些数据,选择哪种方式取决于数据的发送方式(编码格式)和HTTP方法.
数据来源:
- 查询参数(Query String):附加在URL
?之后,如GET /api/students?class=1.通过$_GET超全局数组获取. - 请求体(Request Body):
- 表单编码数据:当HTML表单提交或Content-Type为
application/x-www-form-urlencoded时,数据通过$_POST获取. - JSON数据:现代API最常用的格式,Content-Type为
application/json.PHP不能直接通过$_POST获取,需要从php:// input流中读取并解析. - 文件上传:通过
$_FILES处理(本章暂不涉及).
- 表单编码数据:当HTML表单提交或Content-Type为
处理JSON请求体:
// 读取原始的POST数据(JSON格式)$jsonInput=file_get_contents('php:// input');// 将JSON字符串解码为PHP数组或对象$data=json_decode($jsonInput,true);// true参数返回关联数组if(json_last_error()!==JSON_ERROR_NONE){// JSON解析失败,返回错误}最佳实践:
- 始终验证接收到的数据是否存在且格式正确.
- 对于关键操作,不要完全依赖客户端发送的数据,服务端应进行二次验证.
3. 数据库操作集成:API的"数据仓库管理员"
本章将第2章学习的数据库CRUD操作,封装成函数,并根据HTTP请求动态调用.
映射关系:
GET /students-> 调用函数getAllStudents()(SELECT *)GET /students/{id}-> 调用函数getStudentById($id)(SELECT … WHERE id = ?)POST /students-> 调用函数createStudent($data)(INSERT INTO …)PUT /students/{id}-> 调用函数updateStudent($id, $data)(UPDATE … WHERE id = ?)DELETE /students/{id}-> 调用函数deleteStudent($id)(DELETE FROM … WHERE id = ?)
安全核心:
必须继续使用PDO预处理语句(Prepared Statements)来防止SQL注入攻击.所有来自客户端、用于构建SQL语句的参数都必须通过预处理语句的占位符来传递.
4. 构建JSON响应:API的"信息发送员"
RESTful API通常使用JSON作为数据交换格式.服务端需要做两件事:
- 设置正确的HTTP响应头:告知客户端返回的内容类型是JSON.
- 输出JSON格式的字符串:将操作结果(数据或错误信息)编码成JSON.
关键技术:
header('Content-Type: application/json; charset=utf-8'):设置响应头.json_encode($data, JSON_UNESCAPED_UNICODE):将PHP数组或对象转换为JSON字符串.JSON_UNESCAPED_UNICODE选项确保中文字符正常显示,不被转义为\uXXXX格式.http_response_code(404):设置HTTP状态码(PHP 5.4+).
响应结构标准化:
建议为API响应设计一个统一的格式,便于前端处理.例如:
{"code":200,"message":"成功","data":{...}// 或 []}{"code":400,"message":"请求参数无效","data":null}5. HTTP状态码的应用:API的"状态指示灯"
状态码是HTTP响应中至关重要的部分,它用三位数字快速告知客户端请求的结果概况.
- 2xx 成功:
200 OK(通用成功),201 Created(资源创建成功). - 4xx 客户端错误:
400 Bad Request(请求语法错误),401 Unauthorized(未认证),403 Forbidden(无权限),404 Not Found(资源不存在). - 5xx 服务器错误:
500 Internal Server Error(服务器内部错误).
在API中恰当使用状态码,是开发专业、易用接口的标志.
代码示例
示例1:基础路由分发结构
<?php// api.php - API单一入口文件示例header('Content-Type: application/json; charset=utf-8');// 获取请求方法和路径$method=$_SERVER['REQUEST_METHOD'];// 简单解析路径,这里假设我们的API都放在 /api 路径下$requestUri=$_SERVER['REQUEST_URI'];$path=parse_url($requestUri,PHP_URL_PATH);// 获取路径部分,去掉查询参数// 移除可能存在的 '/api' 前缀,得到资源路径$resourcePath=str_replace('/api','',$path);// 根据请求方法和资源路径进行路由分发switch("$method$resourcePath"){case'GET /students':handleGetStudents();break;case'GET /students/':// 处理带ID的详情请求,需要进一步解析ID$studentId=extractIdFromPath($resourcePath);if($studentId){handleGetStudent($studentId);}else{sendErrorResponse(400,'无效的学生ID');}break;case'POST /students':handleCreateStudent();break;case'PUT /students/':$studentId=extractIdFromPath($resourcePath);if($studentId){handleUpdateStudent($studentId);}else{sendErrorResponse(400,'无效的学生ID');}break;case'DELETE /students/':$studentId=extractIdFromPath($resourcePath);if($studentId){handleDeleteStudent($studentId);}else{sendErrorResponse(400,'无效的学生ID');}break;default:// 没有匹配到任何路由sendErrorResponse(404,'接口不存在');break;}// 辅助函数:从路径中提取ID(例如从 "/students/123" 中提取 123)functionextractIdFromPath($path){$parts=explode('/',trim($path,'/'));// 假设路径格式为 /resource/{id}if(count($parts)>=2&&is_numeric($parts[1])){return(int)$parts[1];}returnnull;}// 辅助函数:发送错误响应functionsendErrorResponse($code,$message){http_response_code($code);echojson_encode(['code'=>$code,'message'=>$message,'data'=>null],JSON_UNESCAPED_UNICODE);exit;}// 以下为各个路由处理函数的声明(具体实现在后面)functionhandleGetStudents(){/* ... */}functionhandleGetStudent($id){/* ... */}functionhandleCreateStudent(){/* ... */}functionhandleUpdateStudent($id){/* ... */}functionhandleDeleteStudent($id){/* ... */}?>示例2:接收和处理不同类型的请求数据
<?php// 示例:演示如何接收不同来源的数据// 1. 接收查询参数(GET请求)// 假设请求:GET /api/search?name=张三&age=20if($_SERVER['REQUEST_METHOD']==='GET'){$name=$_GET['name']??null;// 使用空合并运算符提供默认值$age=$_GET['age']??null;// 注意:来自用户输入的数据必须验证和过滤if($name&&$age){echo"搜索条件:姓名包含 '{$name}',年龄{$age}岁";}}// 2. 接收表单编码的POST数据// 假设请求:POST /api/form,Content-Type: application/x-www-form-urlencodedif($_SERVER['REQUEST_METHOD']==='POST'&&empty($_POST)){// 注意:只有当Content-Type是表单编码时,$_POST才会自动填充// 对于JSON格式的POST请求,$_POST是空的}if(isset($_POST['username'])){$username=trim($_POST['username']);// trim去除首尾空格$email=filter_var($_POST['email']??'',FILTER_VALIDATE_EMAIL);// 使用filter_var验证邮箱if(!$email){sendErrorResponse(400,'邮箱格式无效');}}// 3. 接收JSON格式的请求体数据(推荐用于API)// 假设请求:POST /api/students,Content-Type: application/json,Body: {"name":"李四","age":22}functiongetJsonRequestBody(){// 读取原始输入流$jsonInput=file_get_contents('php:// input');// 如果输入流为空,返回空数组if(empty($jsonInput)){return[];}// 解码JSON$data=json_decode($jsonInput,true);// 检查JSON解码是否成功if(json_last_error()!==JSON_ERROR_NONE){sendErrorResponse(400,'JSON格式错误:'.json_last_error_msg());}return$data;}// 使用示例$requestData=getJsonRequestBody();if(isset($requestData['name'])){$name=htmlspecialchars($requestData['name'],ENT_QUOTES,'UTF-8');// 防止XSS,虽然JSON响应通常不需要,但安全起见$age=(int)($requestData['age']??0);// 转换为整数,提供默认值0echo"接收到的JSON数据:姓名={$name}, 年龄={$age}";}// 辅助函数(同上例)functionsendErrorResponse($code,$message){http_response_code($code);echojson_encode(['error'=>$message],JSON_UNESCAPED_UNICODE);exit;}?>预期输出(对于JSON请求):
接收到的JSON数据:姓名=李四, 年龄=22示例3:集成数据库CRUD操作
<?php// db.php - 数据库连接和基础CRUD函数header('Content-Type: application/json; charset=utf-8');// 数据库配置(应存储在配置文件中,此处为示例)define('DB_HOST','localhost');define('DB_NAME','school_api');define('DB_USER','root');define('DB_PASS','password');// 生产环境务必使用强密码// 创建数据库连接functiongetDbConnection(){try{$dsn='mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4';$options=[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,// 抛出异常PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,// 默认获取关联数组PDO::ATTR_EMULATE_PREPARES=>false,// 禁用预处理模拟,某些情况更安全];$pdo=newPDO($dsn,DB_USER,DB_PASS,$options);return$pdo;}catch(PDOException$e){// 记录日志,并返回500错误error_log('数据库连接失败: '.$e->getMessage());sendErrorResponse(500,'数据库服务暂时不可用');returnnull;// 实际上sendErrorResponse会exit,这里不会执行到}}// 1. 获取所有学生functiongetAllStudents(){$pdo=getDbConnection();$stmt=$pdo->query('SELECT id, name, age, class FROM students WHERE is_deleted = 0 ORDER BY id DESC');$students=$stmt->fetchAll();sendSuccessResponse('获取学生列表成功',$students);}// 2. 获取单个学生详情functiongetStudentById($id){$pdo=getDbConnection();// 使用预处理语句防止SQL注入$stmt=$pdo->prepare('SELECT id, name, age, class, created_at FROM students WHERE id = ? AND is_deleted = 0');$stmt->execute([$id]);$student=$stmt->fetch();if($student){sendSuccessResponse('获取学生详情成功',$student);}else{sendErrorResponse(404,'学生不存在或已被删除');}}// 3. 创建新学生functioncreateStudent($data){// 数据验证$name=trim($data['name']??'');$age=(int)($data['age']??0);$class=trim($data['class']??'');if(empty($name)||$age<=0||empty($class)){sendErrorResponse(400,'参数无效:姓名、年龄、班级均为必填项,且年龄需大于0');}$pdo=getDbConnection();$stmt=$pdo->prepare('INSERT INTO students (name, age, class) VALUES (?, ?, ?)');try{$stmt->execute([$name,$age,$class]);$newId=$pdo->lastInsertId();// 获取新插入记录的IDhttp_response_code(201);// 201 CreatedsendSuccessResponse('学生创建成功',['id'=>$newId]);}catch(PDOException$e){// 可能是唯一约束冲突等sendErrorResponse(500,'创建失败: '.$e->getMessage());}}// 4. 更新学生信息functionupdateStudent($id,$data){// 检查学生是否存在$pdo=getDbConnection();$checkStmt=$pdo->prepare('SELECT id FROM students WHERE id = ? AND is_deleted = 0');$checkStmt->execute([$id]);if(!$checkStmt->fetch()){sendErrorResponse(404,'学生不存在,无法更新');}// 构建更新字段(只更新提供的字段)$updateFields=[];$params=[];if(isset($data['name'])){$name=trim($data['name']);if(!empty($name)){$updateFields[]='name = ?';$params[]=$name;}}if(isset($data['age'])){$age=(int)$data['age'];if($age>0){$updateFields[]='age = ?';$params[]=$age;}}if(isset($data['class'])){$class=trim($data['class']);if(!empty($class)){$updateFields[]='class = ?';$params[]=$class;}}if(empty($updateFields)){sendErrorResponse(400,'未提供任何要更新的有效字段');}// 添加ID参数到末尾(WHERE条件)$params[]=$id;$sql='UPDATE students SET '.implode(', ',$updateFields).', updated_at = NOW() WHERE id = ?';$stmt=$pdo->prepare($sql);if($stmt->execute($params)){sendSuccessResponse('学生信息更新成功');}else{sendErrorResponse(500,'更新失败');}}// 5. 删除学生(软删除)functiondeleteStudent($id){$pdo=getDbConnection();// 使用软删除(将is_deleted标记为1),而非物理删除,保留数据$stmt=$pdo->prepare('UPDATE students SET is_deleted = 1, deleted_at = NOW() WHERE id = ? AND is_deleted = 0');if($stmt->execute([$id])){if($stmt->rowCount()>0){sendSuccessResponse('学生删除成功');}else{// 可能学生不存在或已被删除sendErrorResponse(404,'学生不存在或已被删除');}}else{sendErrorResponse(500,'删除失败');}}// 统一的成功响应函数functionsendSuccessResponse($message,$data=null){$response=['code'=>http_response_code(),// 获取当前设置的状态码,默认为200'message'=>$message,'data'=>$data];echojson_encode($response,JSON_UNESCAPED_UNICODE);exit;}// 统一的错误响应函数functionsendErrorResponse($code,$message){http_response_code($code);$response=['code'=>$code,'message'=>$message,'data'=>null];echojson_encode($response,JSON_UNESCAPED_UNICODE);exit;}// 注意:此文件不能直接执行,它只包含函数定义,需要被api.php包含调用?>示例4:构建完整的JSON响应
<?php// response_demo.php - 演示各种API响应header('Content-Type: application/json; charset=utf-8');// 模拟一些数据$students=[['id'=>1,'name'=>'张三','age'=>20,'class'=>'计算机1班'],['id'=>2,'name'=>'李四','age'=>21,'class'=>'计算机2班'],['id'=>3,'name'=>'王五','age'=>19,'class'=>'软件工程1班'],];// 1. 成功响应 - 列表数据functionsuccessListResponse(){global$students;http_response_code(200);// 明确设置,虽然默认是200echojson_encode(['code'=>200,'message'=>'获取学生列表成功','data'=>$students,'meta'=>[// 可选的元数据,如分页信息'total'=>count($students),'page'=>1,'per_page'=>10]],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);// PRETTY_PRINT使JSON格式化,便于阅读,生产环境可去掉}// 2. 成功响应 - 单个资源functionsuccessItemResponse(){global$students;http_response_code(200);echojson_encode(['code'=>200,'message'=>'获取学生详情成功','data'=>$students[0]// 假设是第一个学生],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);}// 3. 创建成功响应 (201 Created)functioncreatedResponse(){http_response_code(201);// 资源创建成功echojson_encode(['code'=>201,'message'=>'学生创建成功','data'=>['id'=>1001,// 新创建的ID'name'=>'新学生','age'=>22],'links'=>[// HATEOAS风格,提供相关链接(可选)'self'=>'/api/students/1001','collection'=>'/api/students']],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);}// 4. 错误响应 - 客户端错误functionclientErrorResponse(){http_response_code(400);echojson_encode(['code'=>400,'message'=>'请求参数无效:年龄必须大于0','data'=>null,'errors'=>[// 可以提供更详细的错误信息'age'=>['年龄必须大于0']]],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);}// 5. 错误响应 - 资源未找到functionnotFoundResponse(){http_response_code(404);echojson_encode(['code'=>404,'message'=>'请求的学生资源不存在','data'=>null],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);}// 6. 错误响应 - 服务器错误functionserverErrorResponse(){http_response_code(500);echojson_encode(['code'=>500,'message'=>'服务器内部错误,请稍后重试','data'=>null],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);}// 测试调用(实际使用时根据条件调用)$testType=$_GET['test']??'list';switch($testType){case'item':successItemResponse();break;case'created':createdResponse();break;case'client_error':clientErrorResponse();break;case'not_found':notFoundResponse();break;case'server_error':serverErrorResponse();break;default:successListResponse();break;}?>运行结果(在浏览器中访问 response_demo.php?test=item):
{"code":200,"message":"获取学生详情成功","data":{"id":1,"name":"张三","age":20,"class":"计算机1班"}}示例5:整合路由与处理逻辑
<?php// complete_api.php - 完整的API示例(简化版)header('Content-Type: application/json; charset=utf-8');// 引入数据库函数(假设db.php在同一目录)require_once'db.php';// 配置允许的请求来源(CORS),简单示例,生产环境需精确配置header('Access-Control-Allow-Origin: *');// 允许所有域名,仅限开发环境header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');header('Access-Control-Allow-Headers: Content-Type, Authorization');// 处理预检请求(Preflight Request)if($_SERVER['REQUEST_METHOD']==='OPTIONS'){http_response_code(200);exit;}// 解析请求$method=$_SERVER['REQUEST_METHOD'];$requestUri=$_SERVER['REQUEST_URI'];$path=parse_url($requestUri,PHP_URL_PATH);// 简单路由匹配(假设API路径为 /api/students 或 /api/students/{id})if(preg_match('#^/api/students/(\d+)$#',$path,$matches)){// 匹配 /api/students/{id}$studentId=(int)$matches[1];switch($method){case'GET':getStudentById($studentId);// 调用db.php中的函数break;case'PUT':$data=getJsonRequestBody();updateStudent($studentId,$data);break;case'DELETE':deleteStudent($studentId);break;default:sendErrorResponse(405,'方法不允许');// 405 Method Not Allowedbreak;}}elseif($path==='/api/students'){// 匹配 /api/studentsswitch($method){case'GET':getAllStudents();break;case'POST':$data=getJsonRequestBody();createStudent($data);break;default:sendErrorResponse(405,'方法不允许');break;}}else{sendErrorResponse(404,'接口不存在');}// 注意:getJsonRequestBody, sendErrorResponse 等函数在db.php中已定义?>实战项目:学生信息管理API
项目需求分析
我们将构建一个完整的学生信息管理API,支持标准的CRUD操作,并使用RESTful设计原则.
功能需求:
- 获取学生列表:GET
/api/students,支持分页和筛选(可选扩展). - 获取学生详情:GET
/api/students/{id}. - 创建新学生:POST
/api/students,接收JSON格式的学生信息. - 更新学生信息:PUT
/api/students/{id},更新指定学生的全部或部分信息. - 删除学生:DELETE
/api/students/{id},执行软删除.
非功能需求:
- 所有数据交互使用JSON格式.
- 返回恰当的HTTP状态码.
- 数据库操作使用PDO预处理语句,防止SQL注入.
- 对输入数据进行基础验证.
- 提供统一的响应格式.
技术方案
数据库设计:
- 表名:
students - 字段:
idINT PRIMARY KEY AUTO_INCREMENTnameVARCHAR(100) NOT NULLageTINYINT UNSIGNED NOT NULL (年龄范围0-255)classVARCHAR(50) NOT NULLcreated_atTIMESTAMP DEFAULT CURRENT_TIMESTAMPupdated_atTIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPis_deletedTINYINT(1) DEFAULT 0 (0-未删除,1-已删除,用于软删除)deleted_atTIMESTAMP NULL
- 表名:
API端点设计:
GET /api/students- 获取学生列表
POST /api/students- 创建新学生GET /api/students/{id}- 获取学生详情PUT /api/students/{id}- 更新学生信息DELETE /api/students/{id}- 删除学生
- 响应格式:
- 成功:
{"code": 2xx, "message": "...", "data": ...} - 错误:
{"code": 4xx/5xx, "message": "...", "data": null}
- 成功:
分步骤实现
步骤1:初始化数据库
创建数据库和表结构:
-- 创建数据库CREATEDATABASEIFNOTEXISTSschool_apiCHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ci;USEschool_api;-- 创建学生表CREATETABLEstudents(idINTPRIMARYKEYAUTO_INCREMENT,nameVARCHAR(100)NOTNULL,ageTINYINTUNSIGNEDNOTNULL,classVARCHAR(50)NOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,is_deletedTINYINT(1)DEFAULT0,deleted_atTIMESTAMPNULL,INDEXidx_is_deleted(is_deleted),INDEXidx_class(class))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;-- 插入一些测试数据INSERTINTOstudents(name,age,class)VALUES('张三',20,'计算机科学与技术1班'),('李四',21,'软件工程2班'),('王五',19,'网络工程1班'),('赵六',22,'计算机科学与技术2班'),('钱七',20,'软件工程1班');步骤2:创建配置文件
<?php// config.php - 配置文件// 数据库配置define('DB_HOST','localhost');define('DB_NAME','school_api');define('DB_USER','root');define('DB_PASS','your_password_here');// 请更改为你的密码// API配置define('API_BASE_PATH','/api');// 错误报告设置(开发环境)error_reporting(E_ALL);ini_set('display_errors',1);// 时区设置date_default_timezone_set('Asia/Shanghai');?>步骤3:创建数据库连接和辅助函数
<?php// database.php - 数据库连接和基础函数require_once'config.php';classDatabase{privatestatic$pdo=null;publicstaticfunctiongetConnection(){if(self::$pdo===null){try{$dsn='mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4';$options=[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES=>false,];self::$pdo=newPDO($dsn,DB_USER,DB_PASS,$options);}catch(PDOException$e){// 记录到日志文件error_log('['.date('Y-m-d H:i:s').'] 数据库连接错误: '.$e->getMessage());Response::sendError(500,'数据库连接失败');}}returnself::$pdo;}}// 响应处理类classResponse{publicstaticfunctionsendSuccess($data=null,$message='成功',$code=200){http_response_code($code);echojson_encode(['code'=>$code,'message'=>$message,'data'=>$data],JSON_UNESCAPED_UNICODE);exit;}publicstaticfunctionsendError($code,$message,$details=null){http_response_code($code);$response=['code'=>$code,'message'=>$message,'data'=>null];if($details!==null){$response['details']=$details;}echojson_encode($response,JSON_UNESCAPED_UNICODE);exit;}}// 请求处理类classRequest{publicstaticfunctiongetJsonBody(){$jsonInput=file_get_contents('php:// input');if(empty($jsonInput)){return[];}$data=json_decode($jsonInput,true);if(json_last_error()!==JSON_ERROR_NONE){Response::sendError(400,'JSON格式错误: '.json_last_error_msg());}return$data;}publicstaticfunctiongetMethod(){return$_SERVER['REQUEST_METHOD'];}publicstaticfunctiongetPath(){$path=parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH);// 移除API基础路径if(strpos($path,API_BASE_PATH)===0){$path=substr($path,strlen(API_BASE_PATH));}returnrtrim($path,'/');}publicstaticfunctiongetQueryParams(){return$_GET;}}?>步骤4:创建学生模型
<?php// models/Student.php - 学生数据模型require_once'database.php';classStudent{// 获取所有学生(带分页)publicstaticfunctiongetAll($page=1,$perPage=10,$filters=[]){$pdo=Database::getConnection();// 构建WHERE条件$whereClauses=['is_deleted = 0'];$params=[];if(isset($filters['class'])&&!empty($filters['class'])){$whereClauses[]='class LIKE ?';$params[]='%'.$filters['class'].'%';}if(isset($filters['min_age'])&&is_numeric($filters['min_age'])){$whereClauses[]='age >= ?';$params[]=(int)$filters['min_age'];}if(isset($filters['max_age'])&&is_numeric($filters['max_age'])){$whereClauses[]='age <= ?';$params[]=(int)$filters['max_age'];}$whereSql=implode(' AND ',$whereClauses);// 计算偏移量$offset=($page-1)*$perPage;// 获取总数$countStmt=$pdo->prepare("SELECT COUNT(*) as total FROM students WHERE$whereSql");$countStmt->execute($params);$total=$countStmt->fetch()['total'];$totalPages=ceil($total/$perPage);// 获取数据$sql="SELECT id, name, age, class, created_at FROM students WHERE$whereSqlORDER BY id DESC LIMIT ? OFFSET ?";$stmt=$pdo->prepare($sql);// 注意:LIMIT和OFFSET参数必须是整数$params[]=(int)$perPage;$params[]=(int)$offset;$stmt->execute($params);$students=$stmt->fetchAll();return['data'=>$students,'pagination'=>['total'=>(int)$total,'total_pages'=>$totalPages,'current_page'=>$page,'per_page'=>$perPage]];}// 获取单个学生publicstaticfunctiongetById($id){$pdo=Database::getConnection();$stmt=$pdo->prepare('SELECT id, name, age, class, created_at, updated_at FROM students WHERE id = ? AND is_deleted = 0');$stmt->execute([$id]);$student=$stmt->fetch();if(!$student){returnnull;}return$student;}// 创建学生publicstaticfunctioncreate($data){// 验证数据$errors=self::validateStudentData($data);if(!empty($errors)){Response::sendError(400,'数据验证失败',$errors);}$pdo=Database::getConnection();$stmt=$pdo->prepare('INSERT INTO students (name, age, class) VALUES (?, ?, ?)');try{$stmt->execute([trim($data['name']),(int)$data['age'],trim($data['class'])]);return$pdo->lastInsertId();}catch(PDOException$e){error_log('创建学生失败: '.$e->getMessage());returnfalse;}}// 更新学生publicstaticfunctionupdate($id,$data){// 检查学生是否存在$existing=self::getById($id);if(!$existing){returnfalse;// 学生不存在}// 验证数据$errors=self::validateStudentData($data,true);// true表示更新模式,允许部分字段if(!empty($errors)){Response::sendError(400,'数据验证失败',$errors);}$pdo=Database::getConnection();// 构建动态更新语句$updateFields=[];$params=[];if(isset($data['name'])){$updateFields[]='name = ?';$params[]=trim($data['name']);}if(isset($data['age'])){$updateFields[]='age = ?';$params[]=(int)$data['age'];}if(isset($data['class'])){$updateFields[]='class = ?';$params[]=trim($data['class']);}if(empty($updateFields)){Response::sendError(400,'未提供任何要更新的字段');}$params[]=$id;// WHERE条件$sql='UPDATE students SET '.implode(', ',$updateFields).', updated_at = NOW() WHERE id = ? AND is_deleted = 0';$stmt=$pdo->prepare($sql);return$stmt->execute($params);}// 删除学生(软删除)publicstaticfunctiondelete($id){$pdo=Database::getConnection();$stmt=$pdo->prepare('UPDATE students SET is_deleted = 1, deleted_at = NOW() WHERE id = ? AND is_deleted = 0');return$stmt->execute([$id]);}// 数据验证privatestaticfunctionvalidateStudentData($data,$isUpdate=false){$errors=[];// 姓名验证if(!$isUpdate||isset($data['name'])){$name=trim($data['name']??'');if(empty($name)){$errors['name']='姓名不能为空';}elseif(mb_strlen($name,'UTF-8')>100){$errors['name']='姓名长度不能超过100个字符';}}// 年龄验证if(!$isUpdate||isset($data['age'])){$age=$data['age']??null;if($age===null&&!$isUpdate){$errors['age']='年龄不能为空';}elseif($age!==null){$age=(int)$age;if($age<=0||$age>150){$errors['age']='年龄必须在1-150之间';}}}// 班级验证if(!$isUpdate||isset($data['class'])){$class=trim($data['class']??'');if(empty($class)){$errors['class']='班级不能为空';}elseif(mb_strlen($class,'UTF-8')>50){$errors['class']='班级长度不能超过50个字符';}}return$errors;}}?>步骤5:创建API入口文件
<?php// api/index.php - API主入口文件require_once'../config.php';require_once'../database.php';require_once'../models/Student.php';// 设置响应头header('Content-Type: application/json; charset=utf-8');// CORS设置(开发环境)header('Access-Control-Allow-Origin: *');header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');// 处理预检请求if(Request::getMethod()==='OPTIONS'){http_response_code(200);exit;}// 获取请求路径和方法$path=Request::getPath();$method=Request::getMethod();// 路由分发try{// 学生资源路由if(preg_match('#^/students/(\d+)$#',$path,$matches)){$studentId=(int)$matches[1];switch($method){case'GET':// 获取学生详情$student=Student::getById($studentId);if($student){Response::sendSuccess($student,'获取学生详情成功');}else{Response::sendError(404,'学生不存在');}break;case'PUT':// 更新学生信息$data=Request::getJsonBody();if(empty($data)){Response::sendError(400,'请求体不能为空');}$result=Student::update($studentId,$data);if($result){Response::sendSuccess(null,'学生信息更新成功');}else{Response::sendError(404,'学生不存在或更新失败');}break;case'DELETE':// 删除学生$result=Student::delete($studentId);if($result){Response::sendSuccess(null,'学生删除成功');}else{Response::sendError(404,'学生不存在或已被删除');}break;default:Response::sendError(405,'不允许的请求方法');break;}}elseif($path==='/students'){switch($method){case'GET':// 获取学生列表(支持分页和筛选)$queryParams=Request::getQueryParams();$page=max(1,(int)($queryParams['page']??1));$perPage=max(1,min(100,(int)($queryParams['per_page']??10)));// 限制每页最多100条// 筛选条件$filters=[];if(isset($queryParams['class'])){$filters['class']=$queryParams['class'];}if(isset($queryParams['min_age'])){$filters['min_age']=$queryParams['min_age'];}if(isset($queryParams['max_age'])){$filters['max_age']=$queryParams['max_age'];}$result=Student::getAll($page,$perPage,$filters);Response::sendSuccess($result,'获取学生列表成功');break;case'POST':// 创建新学生$data=Request::getJsonBody();if(empty($data)){Response::sendError(400,'请求体不能为空');}$newId=Student::create($data);if($newId){http_response_code(201);Response::sendSuccess(['id'=>$newId],'学生创建成功',201);}else{Response::sendError(500,'创建学生失败');}break;default:Response::sendError(405,'不允许的请求方法');break;}}else{// 路径不匹配Response::sendError(404,'接口不存在');}}catch(Exception$e){// 捕获未处理的异常error_log('API异常: '.$e->getMessage());Response::sendError(500,'服务器内部错误');}?>步骤6:创建 .htaccess 文件(用于Apache服务器)
# .htaccess - Apache URL重写规则 RewriteEngine On # 如果请求的不是真实文件或目录,则重写到api/index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^api/(.*)$ api/index.php [QSA,L] # 设置默认字符集 AddDefaultCharset UTF-8项目测试指南
现在我们已经完成了学生信息管理API的开发,接下来使用Postman进行测试.
测试1:获取学生列表
请求:
- 方法:GET
- URL:
http:// localhost/your_project/api/students - 可选的查询参数:
?page=1&per_page=5&class=计算机
预期响应(状态码:200):
{"code":200,"message":"获取学生列表成功","data":{"data":[{"id":5,"name":"钱七","age":20,"class":"软件工程1班","created_at":"2023-10-01 10:30:00"},{"id":4,"name":"赵六","age":22,"class":"计算机科学与技术2班","created_at":"2023-10-01 10:25:00"}],"pagination":{"total":5,"total_pages":1,"current_page":1,"per_page":10}}}测试2:创建新学生
请求:
- 方法:POST
- URL:
http:// localhost/your_project/api/students - Headers:
Content-Type: application/json - Body(原始JSON):
{"name":"测试学生","age":18,"class":"测试班级"}预期响应(状态码:201):
{"code":201,"message":"学生创建成功","data":{"id":6}}测试3:获取学生详情
请求:
- 方法:GET
- URL:
http:// localhost/your_project/api/students/1
预期响应(状态码:200):
{"code":200,"message":"获取学生详情成功","data":{"id":1,"name":"张三","age":20,"class":"计算机科学与技术1班","created_at":"2023-10-01 10:00:00","updated_at":"2023-10-01 10:00:00"}}测试4:更新学生信息
请求:
- 方法:PUT
- URL:
http:// localhost/your_project/api/students/6 - Headers:
Content-Type: application/json - Body:
{"name":"更新的名字","age":19}预期响应(状态码:200):
{"code":200,"message":"学生信息更新成功","data":null}测试5:删除学生
请求:
- 方法:DELETE
- URL:
http:// localhost/your_project/api/students/6
预期响应(状态码:200):
{"code":200,"message":"学生删除成功","data":null}测试6:错误场景测试
- 获取不存在的学生:GET
/api/students/999-> 应返回404 - 创建学生时缺少必要字段:POST
/api/studentsBody:{"name": "测试"}-> 应返回400,并提示年龄和班级为必填 - JSON格式错误:POST
/api/studentsBody:{invalid json-> 应返回400,提示JSON格式错误 - 不支持的HTTP方法:PATCH
/api/students/1-> 应返回405
项目扩展建议
- 添加搜索功能:在获取列表接口中增加
?search=关键词参数,支持按姓名搜索. - 添加排序功能:允许客户端指定排序字段和方向,如
?sort=age&order=desc. - 数据导出:添加
GET /api/students/export接口,支持导出为CSV或Excel格式. - 数据备份与恢复:实现简单的数据备份接口.
- 接口限流:防止恶意请求,限制每个IP的请求频率.
- API文档:使用OpenAPI(Swagger)规范自动生成API文档.
- 单元测试:为模型类和API接口编写PHPUnit测试用例.
最佳实践
1. 路由设计最佳实践
更清晰的路由解析方法:
<?php// 更健壮的路由解析$path=Request::getPath();$segments=explode('/',trim($path,'/'));$resource=$segments[0]??'';$resourceId=$segments[1]??null;// 验证资源ID是否为数字if($resourceId!==null&&!ctype_digit($resourceId)){Response::sendError(400,'资源ID必须是数字');}// 路由映射表$routes=['students'=>['GET'=>$resourceId?'getStudent':'listStudents','POST'=>$resourceId?null:'createStudent','PUT'=>$resourceId?'updateStudent':null,'DELETE'=>$resourceId?'deleteStudent':null,],// 可以添加其他资源,如'teachers', 'courses'等];if(!isset($routes[$resource])){Response::sendError(404,'资源不存在');}$method=Request::getMethod();$handler=$routes[$resource][$method]??null;if(!$handler){Response::sendError(405,'不允许的请求方法');}// 调用对应的处理函数call_user_func($handler,$resourceId);?>2. 输入验证与数据过滤
全面的输入验证示例:
<?phpclassValidator{// 验证并过滤学生数据publicstaticfunctionvalidateStudent($data,$isUpdate=false){$rules=['name'=>['required'=>!$isUpdate,'type'=>'string','max_length'=>100,'trim'=>true,'filter'=>FILTER_SANITIZE_FULL_SPECIAL_CHARS],'age'=>['required'=>!$isUpdate,'type'=>'int','min'=>1,'max'=>150,'filter'=>FILTER_VALIDATE_INT],'class'=>['required'=>!$isUpdate,'type'=>'string','max_length'=>50,'trim'=>true,'filter'=>FILTER_SANITIZE_FULL_SPECIAL_CHARS]];$validated=[];$errors=[];foreach($rulesas$field=>$rule){$value=$data[$field]??null;$isRequired=$rule['required']&&!$isUpdate;// 检查必填字段if($isRequired&&($value===null||$value==='')){$errors[$field]="{$field}是必填字段";continue;}// 如果更新模式且字段未提供,跳过if(!$isRequired&&($value===null||$value==='')){continue;}// 类型转换和过滤switch($rule['type']){case'int':$value=filter_var($value,$rule['filter'],['options'=>['min_range'=>$rule['min'],'max_range'=>$rule['max']]]);if($value===false){$errors[$field]="{$field}必须是{$rule['min']}到{$rule['max']}之间的整数";}break;case'string':if($rule['trim']){$value=trim($value);}$value=filter_var($value,$rule['filter']);if(mb_strlen($value,'UTF-8')>$rule['max_length']){$errors[$field]="{$field}长度不能超过{$rule['max_length']}个字符";}break;}if(!isset($errors[$field])){$validated[$field]=$value;}}if(!empty($errors)){Response::sendError(400,'数据验证失败',$errors);}return$validated;}// 验证分页参数publicstaticfunctionvalidatePagination($page,$perPage){$page=max(1,(int)$page);$perPage=max(1,min(100,(int)$perPage));// 限制每页最多100条return[$page,$perPage];}}?>3. 错误处理与日志记录
统一的异常处理:
<?php// 自定义异常类classApiExceptionextendsException{protected$statusCode;protected$details;publicfunction__construct($message,$statusCode=500,$details=null,$code=0,Throwable$previous=null){$this->statusCode=$statusCode;$this->details=$details;parent::__construct($message,$code,$previous);}publicfunctiongetStatusCode(){return$this->statusCode;}publicfunctiongetDetails(){return$this->details;}}// 全局异常处理set_exception_handler(function($exception){// 记录异常日志$logMessage=sprintf("[%s] 异常: %s\n文件: %s(%d)\n堆栈: %s\n",date('Y-m-d H:i:s'),$exception->getMessage(),$exception->getFile(),$exception->getLine(),$exception->getTraceAsString());error_log($logMessage,3,__DIR__.'/logs/error.log');// 发送错误响应if($exceptioninstanceofApiException){Response::sendError($exception->getStatusCode(),$exception->getMessage(),$exception->getDetails());}else{// 生产环境隐藏具体错误信息$message='生产环境'?'服务器内部错误':$exception->getMessage();Response::sendError(500,$message);}});// 使用示例functiongetStudent($id){if(!is_numeric($id)){thrownewApiException('无效的学生ID',400);}$student=Student::getById($id);if(!$student){thrownewApiException('学生不存在',404);}return$student;}?>4. 安全性最佳实践
SQL注入防护案例
攻击示例(危险代码):
<?php// 危险:直接拼接用户输入到SQL语句中$search=$_GET['search']??'';$sql="SELECT * FROM students WHERE name LIKE '%$search%'";$result=$pdo->query($sql);// 如果$search是 "'; DROP TABLE students; --",将导致灾难?>防护方案:
<?php// 安全:使用预处理语句$search=$_GET['search']??'';$sql="SELECT * FROM students WHERE name LIKE ? AND is_deleted = 0";$stmt=$pdo->prepare($sql);$searchParam="%{$search}%";$stmt->execute([$searchParam]);// 参数会被安全处理,无法注入SQL// 或者使用命名参数$sql="SELECT * FROM students WHERE name LIKE :search AND is_deleted = 0";$stmt=$pdo->prepare($sql);$stmt->execute([':search'=>"%{$search}%"]);?>其他安全建议
- HTTPS:生产环境必须使用HTTPS,防止中间人攻击和数据窃听.
- CORS配置:生产环境不要使用
Access-Control-Allow-Origin: *,应指定具体域名.
$allowedOrigins=['https:// www.yourfrontend.com','https://app.yourfrontend.com'];$origin=$_SERVER['HTTP_ORIGIN']??'';if(in_array($origin,$allowedOrigins)){header("Access-Control-Allow-Origin:$origin");}- 限速与防刷:对敏感接口(如登录)实施请求频率限制.
- 敏感数据脱敏:API返回的数据中不要包含密码、身份证号等敏感信息的完整内容.
- API密钥管理:如果提供公开API,应使用API密钥和签名机制.
5. 性能优化建议
- 数据库连接复用:使用单例模式或连接池管理数据库连接.
- 查询优化:
// 只选择需要的字段,而不是SELECT *$sql="SELECT id, name, age FROM students";// 好$sql="SELECT * FROM students";// 不好// 添加合适的索引// ALTER TABLE students ADD INDEX idx_name_age (name, age);- 分页优化:对于大数据集,使用基于游标的分页(Cursor-based Pagination)而非偏移量分页.
- 响应缓存:对不经常变化的数据(如城市列表)添加缓存头.
header('Cache-Control: public, max-age=3600');// 缓存1小时- 输出压缩:启用Gzip压缩减少传输数据量.
if(substr_count($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip')){ob_start('ob_gzhandler');}练习题与挑战
基础练习题
练习1:HTTP方法映射(难度:★☆☆☆☆)
题目:写出RESTful API中HTTP方法与CRUD操作的对应关系,并为"书籍"资源设计对应的API端点.
要求:
- GET对应什么操作?端点是什么?
- POST对应什么操作?端点是什么?
- PUT对应什么操作?端点是什么?
- DELETE对应什么操作?端点是什么?
提示:参考第3章RESTful设计原则和第4章的学生API示例.
参考思路:
- GET /books - 获取书籍列表(READ)
- GET /books/{id} - 获取特定书籍详情(READ)
- POST /books - 创建新书籍(CREATE)
- PUT /books/{id} - 更新书籍信息(UPDATE)
- DELETE /books/{id} - 删除书籍(DELETE)
练习2:路由判断(难度:★★☆☆☆)
题目:分析以下请求,指出在API入口文件中如何用代码判断应该调用哪个处理函数?
GET http:// api.example.com/studentsPOST http:// api.example.com/studentsGET http:// api.example.com/students/123PUT http:// api.example.com/students/123
要求:写出PHP代码逻辑,使用$_SERVER['REQUEST_METHOD']和$_SERVER['REQUEST_URI']进行判断.
参考思路:
$method=$_SERVER['REQUEST_METHOD'];$path=parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH);if(preg_match('#^/students/(\d+)$#',$path,$matches)){$id=$matches[1];switch($method){case'GET':handleGetStudent($id);break;case'PUT':handleUpdateStudent($id);break;// ... 其他方法}}elseif($path==='/students'){switch($method){case'GET':handleGetStudents();break;case'POST':handleCreateStudent();break;// ... 其他方法}}练习3:JSON数据处理(难度:★★☆☆☆)
题目:编写一个PHP函数,接收JSON格式的POST请求,解析其中的数据,验证必须包含title和author字段,且title不能为空.如果验证通过,返回解析后的数据;如果验证失败,返回错误信息.
要求:函数需处理JSON解析错误和字段验证.
参考思路:
functionhandleBookCreation(){$jsonInput=file_get_contents('php:// input');if(empty($jsonInput)){return['error'=>'请求体为空'];}$data=json_decode($jsonInput,true);if(json_last_error()!==JSON_ERROR_NONE){return['error'=>'JSON格式错误: '.json_last_error_msg()];}$title=trim($data['title']??'');$author=trim($data['author']??'');if(empty($title)){return['error'=>'标题不能为空'];}if(empty($author)){return['error'=>'作者不能为空'];}return['success'=>true,'data'=>['title'=>$title,'author'=>$author]];}进阶练习题
练习4:分页实现(难度:★★★☆☆)
题目:在获取学生列表的API中实现分页功能.客户端通过page和per_page参数指定页码和每页数量.你需要:
- 从查询参数中获取
page和per_page(提供默认值) - 计算数据库查询的
LIMIT和OFFSET - 查询总记录数,计算总页数
- 在响应中返回分页元数据
要求:确保参数是有效的正整数,per_page有最大值限制(如100).
参考思路:
functiongetStudentsWithPagination(){$page=max(1,(int)($_GET['page']??1));$perPage=max(1,min(100,(int)($_GET['per_page']??10)));$offset=($page-1)*$perPage;$pdo=getDbConnection();// 获取总数$countStmt=$pdo->query('SELECT COUNT(*) as total FROM students WHERE is_deleted = 0');$total=$countStmt->fetch()['total'];$totalPages=ceil($total/$perPage);// 获取当前页数据$stmt=$pdo->prepare('SELECT * FROM students WHERE is_deleted = 0 LIMIT ? OFFSET ?');$stmt->bindValue(1,$perPage,PDO::PARAM_INT);$stmt->bindValue(2,$offset,PDO::PARAM_INT);$stmt->execute();$students=$stmt->fetchAll();return['data'=>$students,'pagination'=>['total'=>$total,'total_pages'=>$totalPages,'current_page'=>$page,'per_page'=>$perPage]];}练习5:状态码应用(难度:★★★☆☆)
题目:为以下场景选择合适的HTTP状态码,并简要说明理由:
- 成功创建新资源
- 客户端请求语法错误(如JSON格式无效)
- 请求的资源不存在
- 客户端未提供认证信息访问受保护资源
- 客户端认证信息有效但权限不足
- 服务器数据库连接失败
要求:列出状态码和对应的PHP设置代码.
参考思路:
- 201 Created -
http_response_code(201) - 400 Bad Request -
http_response_code(400) - 404 Not Found -
http_response_code(404) - 401 Unauthorized -
http_response_code(401) - 403 Forbidden -
http_response_code(403) - 500 Internal Server Error -
http_response_code(500)
综合挑战题
挑战1:完整API实现(难度:★★★★☆)
题目:实现一个完整的"任务管理"API(Todo API),包含以下功能:
- 任务有id、title、description、completed(是否完成)、created_at、updated_at字段
- 实现所有CRUD操作:列表、详情、创建、更新、删除
- 支持按completed状态筛选任务(
GET /todos?completed=true) - 更新任务时,如果只更新部分字段(如只标记完成),应使用PATCH方法
- 删除使用软删除,添加is_deleted字段
- 为所有接口添加合适的状态码和错误处理
要求:提供完整的代码,包括数据库表结构、PHP模型类和API入口文件.
参考实现思路:
- 创建数据库表:
CREATETABLEtodos(idINTPRIMARYKEYAUTO_INCREMENT,titleVARCHAR(200)NOTNULL,descriptionTEXT,completedTINYINT(1)DEFAULT0,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,is_deletedTINYINT(1)DEFAULT0,deleted_atTIMESTAMPNULL);- 创建Todo模型类(参考Student模型).
- 在API入口文件中添加对
/todos和/todos/{id}的路由处理. - 特别注意PATCH请求的处理,应只更新提供的字段.
- 使用Postman进行全面测试.
挑战2:API安全加固(难度:★★★★★)
题目:在学生信息管理API的基础上,添加以下安全措施:
- SQL注入防护:确保所有数据库查询都使用预处理语句.
- 输入验证:为所有输入字段添加严格的验证规则(类型、长度、格式等).
- XSS防护:虽然API返回JSON,但确保存储到数据库的数据经过适当的过滤.
- 速率限制:为
POST /api/students接口添加速率限制(如每个IP每分钟最多5次). - 请求日志:记录所有请求的IP、方法、路径、时间和状态码,便于审计.
要求:提供具体实现代码,特别是速率限制的逻辑.
参考实现思路(速率限制示例):
classRateLimiter{privatestatic$limits=['POST:/api/students'=>['limit'=>5,'window'=>60],// 5次/分钟];publicstaticfunctioncheck($key,$ip){$limitConfig=self::$limits[$key]??null;if(!$limitConfig){returntrue;// 无限制}$cacheKey="rate_limit:{$key}:{$ip}";$redis=newRedis();// 使用Redis存储计数,也可以使用数据库或文件$redis->connect('127.0.0.1',6379);$current=$redis->get($cacheKey);if(!$current){$redis->setex($cacheKey,$limitConfig['window'],1);returntrue;}if($current>=$limitConfig['limit']){returnfalse;// 超过限制}$redis->incr($cacheKey);returntrue;}}// 在API入口中使用$clientIp=$_SERVER['REMOTE_ADDR']??'unknown';$endpoint=Request::getMethod().':'.Request::getPath();if(!RateLimiter::check($endpoint,$clientIp)){Response::sendError(429,'请求过于频繁,请稍后再试');}章节总结
本章重点知识回顾
- 路由分发:学会了如何根据HTTP方法和请求路径,将请求分发到相应的处理函数.这是构建多端点API的基础.
- 请求数据处理:掌握了接收和处理不同格式的客户端数据,特别是JSON格式请求体的处理方法.
- 数据库操作集成:成功将第2章学习的PDO数据库操作与HTTP请求结合,实现了完整的CRUD功能映射.
- JSON响应构建:学会了设置正确的HTTP响应头,并将数据编码为标准JSON格式返回给客户端.
- HTTP状态码应用:理解了常用HTTP状态码的含义,并能在代码中恰当使用,提供清晰的请求结果指示.
- 安全基础:通过预处理语句的应用,掌握了防止SQL注入的基本方法.
- 项目实战:完成了一个完整的学生信息管理API,涵盖了设计、实现、测试的全过程.
技能掌握要求
完成本章学习后,你应该能够:
- 在不依赖框架的情况下,构建一个基本的RESTful API.
- 正确处理GET、POST、PUT、DELETE等HTTP请求.
- 安全地接收、验证和处理客户端提交的数据.
- 将数据库操作封装为函数,并与API端点对应.
- 返回格式统一、状态码恰当的JSON响应.
- 使用Postman等工具对API进行全面的测试.
- 理解并应用基本的API安全防护措施.
进一步学习建议
- 深入学习框架:尝试使用Laravel、Symfony或Slim等PHP框架重构本章项目,体会框架带来的便利.
- 探索高级特性:学习API版本控制、文档自动生成(Swagger/OpenAPI)、GraphQL等高级主题.
- 安全深度研究:深入了解OWASP Top 10中的其他安全漏洞,如身份验证失效、敏感数据泄露等.
- 性能优化:学习API缓存策略、数据库查询优化、异步处理等技术.
- 部署与运维:了解如何将API部署到生产环境,配置Nginx/Apache、设置SSL证书、监控API健康状态.
- 微服务架构:如果你对大规模系统感兴趣,可以了解微服务架构和API网关的概念.
本章是成为后端开发者的重要里程碑.你不仅学会了技术,更重要的是理解了前后端分离架构中,API作为"数据服务提供者"的核心角色.在下一章中,我们将为API添加至关重要的安全层——用户认证与授权,让你的API从"能用"变得"安全可靠".