一、效果展示
最终实现效果如下图所示:
支持i18n国际化数据返回,修改不同'locale' => 'zh_CN',
返回中文
'locale' => 'en',
返回英文。
当前使用版本:PHP Version 8.2.9 、Laravel10x。
image.png
- 有数据返回:
public function token(Request $request)
{
$request->validate([
'name' => 'required',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('name', $request->name)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['用户名或密码不正确!'],
]);
}
return $this->success(["token" => $user->createToken($request->device_name)->plainTextToken]);
}
{
"code": 200,
"message": "请求成功",
"data": {
"token": "49|SpUVNfkiPQhd8aY2BffAGNmN8e8ywMtHw9L0k2I97c7bf39a"
}
}
public function show(Request $request)
{
return $this->success(['user' => User::all()]);
}
{
"code": 200,
"message": "请求成功",
"data": {
"user": [
{
"id": 1,
"name": "admin",
"email": "admin@qq.com",
"email_verified_at": null,
"created_at": null,
"updated_at": null
}
]
}
}
- 无需数据返回
public function index()
{
return $this->ok();
}
{
"code": 200,
"message": "请求成功"
}
二、实现参考
在app目录下,新建\Helpers\Api子目录
image.png
代码实现如下,
<?php
namespace App\Helpers\Api;
use Illuminate\Http\Response as FoundationResponse;
trait ApiResponse
{
protected $statusCode = FoundationResponse::HTTP_OK;
/**
* @return mixed
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* @param $statusCode
* @return $this
*/
public function setStatusCode($statusCode)
{
$this->statusCode = $statusCode;
return $this;
}
/**
* 自定义发送数据
* @param $data
* @param array $header
* @return mixed
*/
public function respond($data, $header = [])
{
return Response()->json($data, $this->getStatusCode(), $header);
}
/**
* 生成发送数据
* @param string $message
* @param array $data
* @param $code
* @return mixed
*/
private function makeDataAndRespond(array $data, string $message, $code = 200)
{
$this->setStatusCode($code ?? 200);
$arrData = [
'code' => $this->statusCode,
'message' => $message
];
$ret = array_merge($arrData, ["data" => $data]);
return $this->respond($ret);
}
/**
* 仅发送消息
* @param string $message
* @param $code
* @return mixed
*/
public function message(string $message, $code = 200)
{
$this->setStatusCode($code ?? 200);
$arrData = [
'code' => $this->statusCode,
'message' => $message
];
return $this->respond($arrData);
}
/**
* 返回成功
* http_status始终为200
* code可以修改
* 不带data参数
* @param string|NULL $message
* @param $code
* @return mixed
*/
public function ok(string $message = NULL, $code = 200)
{
$this->setStatusCode(200);
$arrData = [
'code' => $code ?? 200,
'message' => $message ?? __("http-statuses.200")
];
return $this->respond($arrData);
}
/**
* 发送数据
* @param $data
* @param string|NULL $message
* @return mixed
*/
public function success($data = null, string $message = NULL)
{
$code = FoundationResponse::HTTP_OK;
if (!is_array($data)) {
return $this->respond(['code' => $code,
'message' => $message ?? __("http-statuses." . $code),
'data' => $data]
);
}
return $this->makeDataAndRespond($data, $message ?? __("http-statuses." . $code));
}
/**
* 发送失败消息
* @param $message
* @param $code
* @return mixed
*/
public function failed(string $message = NULL, $code = FoundationResponse::HTTP_BAD_REQUEST)
{
return $this->message($message ?? __("http-statuses." . $code), $code);
}
/**
* 已创建
* @param string|NULL $message
* @return mixed
*/
public function created(string $message = NULL)
{
return $this->failed($message, FoundationResponse::HTTP_CREATED);
}
public function locked(string $message = NULL)
{
return $this->failed($message, FoundationResponse::HTTP_LOCKED);
}
/**
* 内部错误
* @param string|NULL $message
* @return mixed
*/
public function internalError(string $message = NULL)
{
return $this->failed($message, FoundationResponse::HTTP_INTERNAL_SERVER_ERROR);
}
/**
* 未找到
* @param string|null $message
* @return mixed
*/
public function notFound(string $message = null)
{
return $this->failed($message, Foundationresponse::HTTP_NOT_FOUND);
}
/**
* 禁止访问
* @param string|NULL $message
* @return mixed
*/
public function forbidden(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_FORBIDDEN);
}
/**
* 无内容
* @param string|NULL $message
* @return mixed
*/
public function noContent(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_NO_CONTENT);
}
/**
* 未认证
* @param string|NULL $message
* @return mixed
*/
public function unauthorized(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_UNAUTHORIZED);
}
/**
* 网关错误
* @param string|NULL $message
* @return mixed
*/
public function badGateway(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_BAD_GATEWAY);
}
/**
* 未知错误
* @param string|NULL $message
* @return mixed
*/
public function unknownError(string $message = NULL)
{
return $this->failed($message, 520);
}
/**
* 版本不支持
* @param string|NULL $message
* @return mixed
*/
public function versionNotSupported(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_VERSION_NOT_SUPPORTED);
}
/**
* 连接超时
* @param string|NULL $message
* @return mixed
*/
public function connectionTimedOut(string $message = NULL)
{
return $this->failed($message, 522);
}
/**
* 已存在
* @param string|NULL $message
* @return mixed
*/
public function found(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_FOUND);
}
/**
* 已冲突
* @param string|NULL $message
* @return mixed
*/
public function conflict(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_CONFLICT);
}
/**
* 已不可用
* @param string|NULL $message
* @return mixed
*/
public function gone(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_GONE);
}
/**
* 已接受
* @param string|NULL $message
* @return mixed
*/
public function accepted(string $message = NULL)
{
return $this->failed($message, Foundationresponse::HTTP_ACCEPTED);
}
}
修改更符合axios.js等移动端请求,建议修改以下函数。实现,http_status始终返回200,而code进行变化。这样统一一下,移动端更容易统一处理。
即:请求所有接口,本身http状态始终为200,接口由code值变化而变化。
/**
* 自定义发送数据
* @param $data
* @param array $header
* @return mixed
*/
public function respond($data, $header = [])
{
return Response()->json($data, $this->getStatusCode(), $header);
}
修改为:
/**
* 自定义发送数据
* @param $data
* @param array $header
* @return mixed
*/
public function respond($data, $header = [])
{
return Response()->json($data, FoundationResponse::HTTP_OK, $header);
}
三、使用
在控制器类中,使用use ApiResponse;
即可。
image.png
【需要安装
laravel-lang
】国际化支持。image.png
<?php
namespace App\Http\Controllers;
use App\Helpers\Api\ApiResponse;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class UserController extends Controller
{
use ApiResponse;
public function index()
{
return $this->ok();
}
public function show(Request $request)
{
return $this->success(['user' => User::all()]);
}
public function token(Request $request)
{
$request->validate([
'name' => 'required',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('name', $request->name)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['用户名或密码不正确!'],
]);
}
return $this->success(["token" => $user->createToken($request->device_name)->plainTextToken]);
}
}
备注【注意事项】:Postman接口设置X-Requested-With=XMLHttpRequest
有利于调试,并且有异常都通过接口统一返回。Lavavel框架,依赖此字段判断是否为ajax的请求,否则会返回页面。Laravel整体使用非常Nice。
image.png
- 这是uniapp中的配置参考
const BASE_URL = 'http://api.demo.test/api'
export const myRequest = (option) => {
return new Promise ((resolve, reject) => {
// 请求头信息
const requestHeader = {
// 获取手机C_ID 用于push动作
// #ifdef APP-PLUS
'C_ID': plus.push.getClientInfo().clientid || '',
// #endif
// 获取用户token
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
// 做多点登录 PC APP 可同时登录两个账号
'Os-Type': 'APP',
"X-Requested-With":"XMLHttpRequest",
'accept':'application/json',
'content-type': 'application/x-www-form-urlencoded'
}
uni.request({
url: BASE_URL + option.url,
method: option.method || 'GET',
data: option.data || {},
header:requestHeader,
dataType: 'json',
success:(res) => {
if(res.statusCode !== 200) {
console.log(res)
uni.showToast({
title:res.data.message
})
return
}
resolve(res.data)
},fail: (err) => {
uni.showToast({
title: '请求接口失败'
})
reject(err)
}
})
})
}
- 这是vue中,axios的配置参考
// 发送 POST 请求
axios({
url: "http://api.demo.test/api/user/token",
method: "POST",
headers: {
"X-Requested-With":"XMLHttpRequest",
'accept':'application/json',
'content-type': 'application/x-www-form-urlencoded',
},
data: {
name: "admin",
password: "123456786",
device_name:"WeX"
},
}).then((res) => {
console.log(JSON.stringify(res));
});
- 错误码解释参考:
http-statuses.php
<?php
declare(strict_types=1);
return [
'0' => '未知错误',
'100' => '继续请求',
'101' => '切换协议',
'102' => '处理中',
'200' => '请求成功',
'201' => '已创建',
'202' => '已接受',
'203' => '非权威信息',
'204' => '无内容',
'205' => '重置内容',
'206' => '部分内容',
'207' => '多状态',
'208' => '已上报',
'226' => 'IM已使用',
'300' => '多种选择',
'301' => '已永久移动',
'302' => '临时移动',
'303' => '见其他',
'304' => '未修改',
'305' => '使用代理',
'307' => '临时重定向',
'308' => '永久重定向',
'400' => '请求错误',
'401' => '未授权',
'402' => '需要付款',
'403' => '禁止',
'404' => '未找到',
'405' => '方法不允许',
'406' => '无法接受',
'407' => '需要代理验证',
'408' => '请求超时',
'409' => '冲突',
'410' => '不可用',
'411' => '长度要求',
'412' => '前提条件未满足',
'413' => '请求实体过大',
'414' => 'URI太长了',
'415' => '不支持的媒体类型',
'416' => '请求范围不符合',
'417' => '期望不满足',
'418' => '我是一个茶壶',
'419' => '会话已过期',
'421' => '错误的请求',
'422' => '不可处理的实体',
'423' => '锁定',
'424' => '失败的依赖',
'425' => '太早了',
'426' => '需要升级',
'428' => '前提要求',
'429' => '请求太多',
'431' => '请求标头字段太大',
'444' => '连接关闭无响应',
'449' => '重试',
'451' => '法律原因不可用',
'499' => '客户端关闭请求',
'500' => '内部服务器错误',
'501' => '未实现',
'502' => '网关错误',
'503' => '服务不可用',
'504' => '网关超时',
'505' => 'HTTP版本不支持',
'506' => '变体协商',
'507' => '存储空间不足',
'508' => '检测到环路',
'509' => '超出带宽限制',
'510' => '未延期',
'511' => '需要网络验证',
'520' => '未知错误',
'521' => 'Web服务器已关闭',
'522' => '连接超时',
'523' => '原点无法到达',
'524' => '发生超时',
'525' => 'SSL握手失败',
'526' => '无效的SSL证书',
'527' => '轨道炮错误',
'598' => '网络读取超时',
'599' => '网络连接超时',
'unknownError' => '未知错误',
];
zh_CN.json
{
"(and :count more error)": "(还有 :count 个错误)",
"(and :count more errors)": "(以及另外 :count 个错误)",
"A Timeout Occurred": "发生超时",
"Accept": "接受",
"Accepted": "已接受",
"Action": "操作",
"Actions": "操作",
"Add": "添加",
"Admin": "行政",
"Agree": "同意",
"All rights reserved.": "版权所有。",
"Already Reported": "已上报",
"Archive": "档案",
"Assign": "分配",
"Attach": "附加",
"Bad Gateway": "网关错误",
"Bad Request": "请求错误",
"Bandwidth Limit Exceeded": "超出带宽限制",
"Browse": "浏览",
"Cancel": "取消",
"Choose": "选择",
"Choose :name": "选择:name",
"Choose File": "选择文件",
"Choose Image": "选择图片",
"Click to copy": "点击复制",
"Client Closed Request": "客户端关闭请求",
"Close": "关闭",
"Collapse": "坍塌",
"Collapse All": "全部收缩",
"Comment": "评论",
"Confirm": "确认",
"Conflict": "冲突",
"Connect": "连接",
"Connection Closed Without Response": "连接关闭无响应",
"Connection Timed Out": "连接超时",
"Continue": "继续请求",
"Create": "创建",
"Created": "已创建",
"Delete": "删除",
"Detach": "分离",
"Details": "详情",
"Disable": "禁用",
"Discard": "丢弃",
"Done": "完毕",
"Down": "向下",
"Duplicate": "复制",
"Duplicate: name": "重复:名称",
"Edit": "编辑",
"Edit :name": "编辑:name",
"Enable": "启用",
"Expand": "扩张",
"Expand All": "展开全部",
"Expectation Failed": "期望不满足",
"Explanation": "解释",
"Export": "出口",
"Failed Dependency": "失败的依赖",
"File": "文件",
"Files": "文件",
"Forbidden": "访问被拒绝",
"Found": "临时移动",
"Gateway Timeout": "网关超时",
"Go Home": "回首页",
"Go to page :page": "前往第 :page 页",
"Gone": "不可用",
"Hello!": "您好!",
"Hide": "隐藏",
"Hide :name": "隐藏 :name",
"Home": "家",
"HTTP Version Not Supported": "HTTP版本不支持",
"I'm a teapot": "我是一个茶壶",
"If you did not create an account, no further action is required.": "如果您未注册帐号,请忽略此邮件。",
"If you did not request a password reset, no further action is required.": "如果您未申请重设密码,请忽略此邮件。",
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "如果您单击「:actionText」按钮时遇到问题,请复制下方链接到浏览器中访问:",
"IM Used": "IM已使用",
"Image": "图像",
"Impersonate": "模拟登录",
"Impersonation": "冒充",
"Import": "进口",
"Import :name": "进口 :name",
"Insufficient Storage": "存储空间不足",
"Internal Server Error": "内部服务器错误",
"Introduction": "介绍",
"Invalid JSON was returned from the route.": "从路由返回无效的 JSON。",
"Invalid SSL Certificate": "无效的SSL证书",
"Length Required": "长度要求",
"Like": "喜欢",
"Load": "加载",
"Localize": "本地化",
"Locked": "锁定",
"Login": "登录",
"Logout": "登出",
"Loop Detected": "检测到环路",
"Maintenance Mode": "服务不可用",
"Method Not Allowed": "方法不允许",
"Misdirected Request": "错误的请求",
"Moved Permanently": "已永久移动",
"Multi-Status": "多状态",
"Multiple Choices": "多种选择",
"Network Authentication Required": "需要网络验证",
"Network Connect Timeout Error": "网络连接超时",
"Network Read Timeout Error": "网络读取超时",
"New": "新建",
"New :name": "新 :name",
"No": "不",
"No Content": "无内容",
"Non-Authoritative Information": "非权威信息",
"Not Acceptable": "无法接受",
"Not Extended": "未延期",
"Not Found": "页面不存在",
"Not Implemented": "未实现",
"Not Modified": "未修改",
"of": "于",
"OK": "请求成功",
"Open": "打开",
"Open in a current window": "在当前窗口中打开",
"Open in a new window": "在新窗口中打开",
"Open in a parent frame": "在父框架中打开",
"Open in the topmost frame": "在最上面的框架中打开",
"Open on the website": "在网站上打开",
"Origin Is Unreachable": "原点无法到达",
"Page Expired": "页面会话已超时",
"Pagination Navigation": "分页导航",
"Partial Content": "部分内容",
"Payload Too Large": "请求实体过大",
"Payment Required": "需要付款",
"Permanent Redirect": "永久重定向",
"Please click the button below to verify your email address.": "请点击下面按钮验证您的 E-mail:",
"Precondition Failed": "前提条件未满足",
"Precondition Required": "前提要求",
"Preview": "预览",
"Price": "价格",
"Processing": "处理中",
"Proxy Authentication Required": "需要代理验证",
"Railgun Error": "轨道炮错误",
"Range Not Satisfiable": "请求范围不符合",
"Regards": "致敬",
"Register": "注册",
"Request Header Fields Too Large": "请求标头字段太大",
"Request Timeout": "请求超时",
"Reset Content": "重置内容",
"Reset Password": "重置密码",
"Reset Password Notification": "重置密码通知",
"Restore": "恢复",
"Restore :name": "恢复:name",
"results": "结果",
"Retry With": "重试",
"Save": "保存",
"Save & Close": "保存并关闭",
"Save & Return": "保存并返回",
"Save :name": "节省 :name",
"Search": "搜索",
"Search :name": "搜索 :name",
"See Other": "见其他",
"Select": "选择",
"Select All": "全选",
"Send": "发送",
"Server Error": "服务器错误",
"Service Unavailable": "服务不可用",
"Session Has Expired": "会话已过期",
"Settings": "设置",
"Show": "展示",
"Show :name": "显示 :name",
"Show All": "显示所有",
"Showing": "显示中",
"Solve": "解决",
"SSL Handshake Failed": "SSL握手失败",
"Submit": "提交",
"Subscribe": "订阅",
"Switch": "转变",
"Switch To Role": "切换角色",
"Switching Protocols": "切换协议",
"Tag": "标签",
"Tags": "标签",
"Temporary Redirect": "临时重定向",
"The given data was invalid.": "给定的数据无效。",
"The response is not a streamed response.": "该响应不是流式响应。",
"The response is not a view.": "响应不是视图。",
"This password reset link will expire in :count minutes.": "这个重设密码链接将会在 :count 分钟后失效。",
"to": "至",
"Toggle navigation": "切换导航",
"Too Early": "太早了",
"Too Many Requests": "请求次数过多。",
"Translate": "翻译",
"Translate It": "翻译它",
"Unauthorized": "未授权",
"Unavailable For Legal Reasons": "法律原因不可用",
"Unknown Error": "未知错误",
"Unpack": "打开包装",
"Unprocessable Entity": "不可处理的实体",
"Unsubscribe": "退订",
"Unsupported Media Type": "不支持的媒体类型",
"Up": "向上",
"Update": "更新",
"Update :name": "更新:name",
"Upgrade Required": "需要升级",
"URI Too Long": "URI太长了",
"Use Proxy": "使用代理",
"User": "用户",
"Variant Also Negotiates": "变体协商",
"Verify Email Address": "验证 E-mail",
"View": "查看",
"View :name": "查看 :name",
"Web Server is Down": "Web服务器已关闭",
"Whoops!": "哎呀!",
"Yes": "是的",
"You are receiving this email because we received a password reset request for your account.": "您收到此电子邮件是因为我们收到了您帐户的密码重设请求。"
}