上手开发篇
laravel-fast-api-youhujun 上手开发篇
注意
建议在实际开发中可以参照laravel-fast-api-youhujun,组件化开发自己的业务逻辑.这样便于后续维护以及扩展
组件包命令
自定义创建DTO
php artisan make:dto 路径/名称自定义门面代理和门面服务命令
生成门面代理以及门面服务(Generate a facade agent and a facade service)
php artisan call:facade 路径/名称示例(for example):
php artisan call:facade Test/TestFacade这条命令其实是同时调用了自定义创建门面命令
php artisan make:facade 路径/名称和自定义创建门面服务命令
php artisan facade:service 路径/名称执行完成以后会提示门面代理和和门面服务创建成功
自定义契约和契约服务
- 创建契约
php artisan make:contract 路径/名称- 创建契约服务
php artisan contract:service 路径/名称自定义事件和事件服务
- 创建事件和事件监听
php artisan call:event 路径/名称助手函数
助手函数分为两部分,一部分伴随组件包发布安装位于config\help.php中,可以自行查看,并且拓展维护,
另一部分是组件包内置的
config\help.php助手函数
| 函数名称 | 参数说明 | 返回值说明 | 函数作用说明 |
|---|---|---|---|
p($param) | $param: 要打印的参数(任意类型) | 无返回值 | 格式化输出参数便于调试 |
init_number_code() | 无参数 | 4位数字字符串 | 生成4位数字短信验证码 |
get_shard_config_key() | 无参数 | 微服务配置键字符串 | 获取当前微服务配置键 |
get_machine_id() | 无参数 | 机器ID字符串 | 获取雪花算法机器ID |
get_snow_flake_id() | 无参数 | 雪花算法ID整型 | 生成全局唯一雪花ID |
get_shard_table_count() | 无参数 | 分表数量整型 | 获取分库分表分表数量 |
get_shard_cache_db() | 无参数 | 缓存库号整型 | 获取分库分表缓存数据库号 |
get_shard_cache_prefix() | 无参数 | 缓存前缀字符串 | 获取分库分表缓存前缀 |
get_shard_cache_enable() | 无参数 | 布尔值 | 获取分库分表缓存启用状态 |
get_system_user_account_name() | 无参数 | 账号名称数组 | 获取系统内置账号名称数组 |
get_cover_album_picture_uid() | 无参数 | 封面图片UID字符串 | 获取系统默认封面相册图片UID |
get_avatar_album_picture_uid() | 无参数 | 头像图片UID字符串 | 获取系统默认头像相册图片UID |
get_system_album_uid() | 无参数 | 系统相册UID字符串 | 获取系统默认相册UID |
get_admin_album_uid($admin_uid) | $admin_uid: 管理员UID | 管理员相册UID字符串 | 获取管理员默认相册UID |
get_user_album_uid($user_uid) | $user_uid: 用户UID | 用户相册UID字符串 | 获取用户默认相册UID |
get_user_openid($user_uid) | $user_uid: 用户UID | 微信openid字符串 | 获取用户微信openid |
get_user_unionid($user_uid) | $user_uid: 用户UID | 微信unionid字符串 | 获取用户微信unionid |
get_user_level($user_uid) | $user_uid: 用户UID | 用户级别名称字符串 | 获取用户等级名称 |
get_es_user_roles($user_uid, $type = 0) | $user_uid: 用户UID$type: 角色类型(默认0) | 角色名称数组 | 获取ES中用户关联角色名称 |
内置授权相关助手函数
auth_helper.php
| 函数名称 | 参数说明 | 返回值说明 | 函数作用说明 |
|---|---|---|---|
get_admin_roles(Admin $adminObject) | $adminObject: 管理员模型对象 | 角色逻辑名称数组 | 获取管理员角色列表(Redis+ES缓存) |
get_user_roles(User $userObject) | $userObject: 用户模型对象 | 角色逻辑名称数组 | 获取用户角色列表(Redis+ES缓存) |
is_develop(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为开发者角色 |
is_super(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为超级管理员 |
is_admin(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为管理员 |
is_config_admin(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为配置管理员 |
is_album_admin(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为相册管理员 |
is_order_admin(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为订单管理员 |
is_article_admin(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否为文章管理员 |
is_user(Admin $adminObject) | $adminObject: 管理员模型对象 | true/false | 判断是否包含用户角色 |
is_phone_user(User $userObject) | $userObject: 用户模型对象 | true/false | 判断是否为手机端普通用户 |
container_helper.php
| 函数名称 | 参数说明 | 返回值说明 | 函数作用说明 |
|---|---|---|---|
resolve_with_validation(Application $app, ?string $concrete, string $interface, ?string $default = null) | $app: Laravel 应用实例$concrete: 要解析的类$interface: 必须实现的接口$default: 默认类(可选) | 解析后的实例对象 | 从容器解析类并验证接口实现 |
php-tool-youhujun工具包的curl_helper.php
| 函数名称 | 参数说明 | 返回值说明 | 函数作用说明 |
|---|---|---|---|
http_get($url, $headers = []) | $url: 请求地址$headers: 请求头数组 | 响应字符串/失败false | CURL 发送 GET 请求 |
http_post($url, $headers = [], $data = null) | $url: 请求地址$headers: 请求头$data: 提交数据 | 响应字符串/失败false | CURL 发送 POST 请求 |
http_put($url, $headers = [], $data = null) | $url: 请求地址$headers: 请求头$data: 提交数据 | 响应字符串/失败false | CURL 发送 PUT 请求 |
http_delete($url, $headers = [], $data = null) | $url: 请求地址$headers: 请求头$data: 提交数据 | 响应字符串/失败false | CURL 发送 DELETE 请求 |
http_head($url, $headers = []) | $url: 请求地址$headers: 请求头 | 响应头字符串 | CURL 发送 HEAD 请求 |
helper.php
| 函数名称 | 参数说明 | 返回值说明 | 函数作用说明 |
|---|---|---|---|
p(mixed $param) | $param: 任意类型数据 | 无返回值 | 格式化打印数据调试 |
plog(mixed $data, string $typeDir = 'common', string $logFileName = 'common') | $data: 日志数据$typeDir: 日志目录$logFileName: 日志文件名 | 写入字节数/false | 自定义日志写入文件 |
f(mixed $param, int $type = 0) | $param: 字符串/数组$type: 0转义/1去标签 | 过滤后内容 | 过滤HTML标签/转义实体 |
code(?array $code = null, ?array $add = null) | $code: 返回码数组$add: 附加数据 | 合并后数组 | 接口返回数据合并 |
get_cascader_array(array $cascader_id_array = []) | $cascader_id_array: 级联数组 | 一维ID数组 | 级联数组转一维去重数组 |
convert_to_string(mixed $data) | $data: 数组/模型/对象 | 字符串 | 数据转字符串(序列化/JSON) |
is_serialized(mixed $data) | $data: 待检测字符串 | true/false | 判断是否为序列化数据 |
array_level(array $arr) | $arr: 数组 | 维度数字 | 计算数组维度 |
total($arr, &$levels, $level = 0) | $arr: 数据$levels: 层级数组$level: 当前层级 | 无返回值 | 递归计算数组维度(辅助) |
to_array($array) | $array: 数组 | 处理后数组 | 数组元素转为单元素数组 |
check_id($id) | $id: 待验证ID | 有效ID/0 | 验证ID是否为合法数字 |
system_helper.php
| 函数名称 | 参数说明 | 返回值说明 | 函数作用说明 |
|---|---|---|---|
make_system_config() | 无参数 | 无返回值 | 初始化系统配置缓存(Redis+ES) |
clean_system_config() | 无参数 | 无返回值 | 清理系统配置缓存标记 |
em(\Throwable $e, $log = false, $notification = false) | $e: 异常对象$log: 是否记录日志$notification: 是否发送通知 | 异常信息字符串 | 统一异常处理、日志、通知 |
接口文档
提示
接口文档使用apipost生成,可以直接调试.
文档地址-https://docs.apipost.net/docs/64bfef32ac88000?locale=zh-cn
组件包发布以后在Documents目录下有一个laravel-fast-api-youhujun.json的文件,可以直接在apipost新建项目导入
ApiPost官网-https://www.apipost.cn
自定义模版
注意
除了有准备好的stub模版,在项目不同模块目录下也分别准备了*.php.example的模版,目地就是为了方便开发过程中参考和复制修改
元数据注解
DocNote用于简单单行注解
DocParams用于带参数,返回值的复杂注解,第一个参数是说明,实际参数和返回值用二维数组注释说明和类型,便于长期维护
- 示例1:
<?php
/*
* @Description: 更新商品规格名称 DTO
* @version: v1
* @Author: youhujun youhu8888@163.com & xueer
* @Date: 2026-06-16 00:42:17
* @LastEditors: youhujun youhu8888@163.com & xueer
* @LastEditTime: 2026-06-16 12:40:15
* @FilePath: \youhu-laravel-api-13\app\DTOs\YouHuShop\V1\Admin\Goods\Spec\UpdateGoodsSpecNameDTO.php
* Copyright (C) 2026 youhujun & xueer . All rights reserved.
*/
namespace App\DTOs\YouHuShop\V1\Admin\Goods\Spec;
use App\Attributes\Common\DocNote;
use App\Attributes\Common\DocParams;
use App\DTOs\Traits\BaseDTOTrait;
use Illuminate\Support\Facades\Validator;
use App\Exceptions\Common\RuleException;
use App\Rules\Pub\Required;
use App\Rules\Pub\Numeric;
use App\Rules\Pub\CheckUnique;
use App\Rules\Pub\CheckString;
use App\Rules\Pub\CheckBetween;
/**
* @see \App\Http\Controllers\YouHuShop\V1\Admin\Goods\Spec\GoodsSpecController::updateGoodsSpecName()
*/
#[DocNote('更新商品规格名称 DTO')]
class UpdateGoodsSpecNameDTO
{
use BaseDTOTrait;
#[DocNote('雪花id')]
public string $spec_category_uid = '';
#[DocNote('是否默认选项')]
public int $is_default_option = 0;
#[DocNote('规格名称')]
public string $name = '';
#[DocNote('规格编码')]
public ?string $code = null;
#[DocNote('备注')]
public ?string $note = null;
#[DocNote('排序')]
public int $sort = 0;
#[DocParams('字段映射,核心约定必须实现')]
public function getFieldMap(): array
{
return ['spec_category_uid', 'is_default_option', 'name', 'code', 'note', 'sort'];
}
#[DocParams('校验规则')]
public function rules(): array
{
$esIndexName = config('...');
return [
'spec_category_uid' => ['bail', new Required(), new Numeric()],
'is_default_option' => ['bail', new Required(), new Numeric()],
'name' => ['bail', new Required(), new CheckString(), new CheckBetween(1, 30),new CheckUnique($esIndexName, 'name','spec_category_uid',$this->spec_category_uid)],
'code' => ['bail', 'nullable', new CheckString(), new CheckBetween(1, 30),new CheckUnique($esIndexName, 'code','spec_category_uid',$this->spec_category_uid)],
'note' => ['bail', 'nullable', new CheckString(), new CheckBetween(1, 50)],
'sort' => ['bail', new Required(), new Numeric()],
];
}
#[DocParams('验证字段')]
public function validate(array $data): self
{
// 更新唯一校验需要提前拿到主键uid,供rules内CheckUnique忽略自身
$this->spec_category_uid = isset($data['spec_category_uid']) ? $data['spec_category_uid'] : '';
$validator = Validator::make($data, $this->rules(), []);
$validated = $validator->validated();
$requiredFields = ['spec_category_uid', 'is_default_option', 'name', 'sort'];
foreach ($requiredFields as $field) {
if (!isset($validated[$field])) {
throw new RuleException('RuleRequiredError', $field);
}
}
$this->fill($validated);
$this->formatFields();
return $this;
}
#[DocParams('格式化|过滤|处理字段')]
public function formatFields(): self
{
foreach ($this->getFieldMap() as $field) {
$this->$field = f($this->$field);
}
return $this;
}
}- 示例2:
<?php
namespace App\Rules\Pub;
use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use App\Exceptions\Common\RuleException;
use App\Facades\Common\V1\Es\EsQueryFacade;
use App\Attributes\Common\DocNote;
use App\Attributes\Common\DocParams;
#[DocNote('检查唯一性规则,自定义用于es索引')]
class CheckUnique implements DataAwareRule, ValidationRule
{
#[DocNote('设置验证数据')]
protected array $dataArray = [];
#[DocNote('es索引名')]
protected string $eIndexName;
#[DocNote('字段名')]
protected string $field;
#[DocNote('忽略的uid')]
protected ?string $ignore_uid;
#[DocNote('忽略的uid字段名,默认user_uid')]
protected string $ignore_uid_field;
public function __construct(string $eIndexName, string $field, string $ignore_uid_field = 'user_uid', ?string $ignore_uid = null)
{
$this->eIndexName = $eIndexName;
$this->field = $field;
$this->ignore_uid_field = $ignore_uid_field;
$this->ignore_uid = $ignore_uid;
}
#[DocParams('设置正在被验证的数据',['dataArray'=>['type'=>'array<string, mixed>','note'=>'正在被验证的数据'],'return'=>['type'=>'static','note'=>'当前对象']])]
public function setData(array $dataArray): static
{
$this->dataArray = $dataArray;
return $this;
}
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
#[DocParams('返回验证规则',['attribute'=>['type'=>'string','note'=>'字段名'],'value'=>['type'=>'string','note'=>'字段值'],'fail'=>['type'=>'\Illuminate\Translation\PotentiallyTranslatedString $fail','note'=>'失败资源'],'return'=>['type'=>'void']])]
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$result = true;
//为了可读性,将变量赋值到局部变量,不要修改删除
$field = $this->field;
$ignore_uid_field = $this->ignore_uid_field;
$ignore_uid = $this->ignore_uid;
$eIndexName = $this->eIndexName;
if(!$eIndexName){
throw new RuleException('EsIndexEmptyError', $attribute);
}
//如果有忽略的UID,则是更新检查
if ($ignore_uid) {
// 精确匹配搜索
$esTotalNumber = $this->getEsSelectTotalNumber($eIndexName, $field, $value);
//超过1个肯定不对
if ($esTotalNumber > 1) {
$result = false;
}
//有一个的情况下,需要判断是否是当前的UID
if ($esTotalNumber == 1) {
$esObject = EsQueryFacade::index($eIndexName)->whereNull('deleted_at')->where($field, $value)->get()->first();
if (isset($esObject->{$ignore_uid_field})) {
if ($esObject->{$ignore_uid_field} !== $ignore_uid) {
$result = false;
}
}
}
} else {
$esTotalNumber = $this->getEsSelectTotalNumber($eIndexName, $field, $value);
if ($esTotalNumber) {
$result = false;
}
}
if (!$result) {
throw new RuleException('RuleCheckUniqueError', $attribute);
}
}
/**
* 获取es查询总数
*
* @param string $indexName
* @return integer
*/
#[DocParams('获取es查询总数',['indexName'=>['type'=>'string','note'=>'es索引名'],'field'=>['type'=>'string','note'=>'字段名'],'value'=>['type'=>'string','note'=>'字段值'],'return'=>['type'=>'int','note'=>'查询总数']])]
private function getEsSelectTotalNumber(string $indexName, string $field, string $value): int
{
$totalNumber = 0;
$esCollection = EsQueryFacade::index($indexName)->whereNull('deleted_at')->where($field, $value)->limit(1000)->get();
$totalNumber = $esCollection->count();
return $totalNumber;
}
}路由
- 路由
注意
建议参考laravel-fast-api-youhujun的路由进行开发,按照功能和模块划分出不同的路由文件,开发阶段可以使用Route::any定义,等到项目开发完成以后,再根据需要区分get和post
控制器
开发思路:
控制器用来过滤参数和权限验证,总体来说控制器只负责接收和处理参数,真正的业务逻辑放在门面代理的服务层实现.如果业务逻辑复杂有两种实现方案
- 第一种:
门面代理的服务层负责主要业务逻辑实现,其他因此产生的关联业务逻辑放在相关事件中去实现
- 第二种:
用不同的门面去处理不同模块的业务逻
