目录结构
app/routes/config 分层。
目录结构、配置与请求生命周期
这一课你将能做到
- 说出 Laravel 11 每个顶层目录的职责,新增代码时知道该放在哪里
- 读懂
.env 与 config/*.php 的关系,并能自己加一个自定义配置项
- 画出从
public/index.php 到响应发出的完整生命周期,并在 bootstrap/app.php 中注册中间件、异常和调度任务
为什么要学
初学者常见的困惑不是「某段代码看不懂」,而是「文件太多,不知道东西放哪」。Spring Boot 项目里你写的类会按 controller / service / mapper / entity / config / common 分包,Laravel 也有等价的目录约定。理解目录约定后,读任何一个 Laravel 项目都像读自家代码——控制器在 app/Http/Controllers,业务模型在 app/Models,配置在 config,路由在 routes。
另一个必须现在解决的问题是请求生命周期。它是回答「中间件什么时候执行」「服务容器什么时候构建」「异常被谁接住」这些问题的底座,也是面试高频题。本课我们用一份可运行的最小项目把这条链路走一遍。
先建立图像
Laravel 11 项目的目录地图:
shop-api/
├── app/
│ ├── Http/
│ │ ├── Controllers/ # 控制器(等价 Spring 的 @RestController)
│ │ ├── Middleware/ # 自定义中间件(等价 Filter/Interceptor)
│ │ └── Requests/ # 表单请求(含验证规则,第 8 课)
│ ├── Models/ # Eloquent 模型(等价 @Entity + DAO)
│ ├── Providers/ # 服务提供者(启动/绑定逻辑,等价 @Configuration)
│ ├── Services/ # 业务服务类(约定目录,需自建)
│ ├── Events/ Listeners/ # 事件与监听器(第 9 课)
│ ├── Jobs/ # 队列任务(第 9 课)
│ └── Console/Commands/ # 自定义 artisan 命令
├── bootstrap/
│ ├── app.php # ★ 11 的应用引导:中间件/异常/调度配置
│ ├── providers.php # 包级服务提供者清单
│ └── cache/ # 缓存后的配置/路由/服务(需可写)
├── config/ # 所有配置文件(database/cache/queue...)
├── database/
│ ├── migrations/ # 数据库版本化脚本
│ ├── factories/ seeders/ # 测试数据工厂与填充
│ └── database.sqlite # 学习期默认数据库
├── public/
│ └── index.php # ★ 所有 HTTP 请求的唯一入口
├── routes/
│ ├── web.php # 网页路由(带 Session、CSRF)
│ ├── api.php # API 路由(默认 /api 前缀,无状态)
│ ├── console.php # 闭包形式的命令行任务
│ └── channels.php # 广播频道(第 9 课 Reverb)
├── resources/views/ # Blade 模板(第 5 课)
├── storage/ # 日志、编译后的 Blade、上传文件(需可写)
└── tests/ # Pest/PHPUnit 测试(第 11 课)
与 Spring Boot 的目录心智对照:app ≈ src/main/java,config ≈ application.yml + @Configuration 类,resources/views ≈ templates/, ≈ 日志目录, ≈ 。
storage/logs
tests
src/test/java
跟着做
步骤 1:观察入口文件
打开 public/index.php(Laravel 11 版本非常短):
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// 维护模式(php artisan down 生成的文件)
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Composer 自动加载
require __DIR__.'/../vendor/autoload.php';
// 引导应用并处理请求
(require_once __DIR__.'/../bootstrap/app.php')
->handleRequest(Request::capture());
三行核心动作:加载自动加载器 → 拿到应用实例 → 用它处理当前请求。这与 Spring Boot 中 SpringApplication.run() 启动容器、再由 DispatcherServlet 处理请求是同构的,只不过 PHP 每个请求都走一遍引导。
步骤 2:读懂 Laravel 11 的引导文件
Laravel 10 以前的 app/Http/Kernel.php 在 11 中消失了,配置全部集中在 bootstrap/app.php:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
// 全局中间件、别名、分组都在这里配
// $middleware->append(\App\Http\Middleware\TrackRequest::class);
// $middleware->alias(['role' => \App\Http\Middleware\EnsureRole::class]);
})
->withExceptions(function (Exceptions $exceptions): void {
// 自定义异常渲染,等价 Spring 的 @ControllerAdvice
})
->create();
health: '/up' 会自动注册一个健康检查路由,供负载均衡探活,相当于 Spring Boot Actuator 的 /actuator/health 简版。
步骤 3:加一个自定义配置项
<?php
return [
// 单笔订单最大金额(分),防止误操作
'max_order_amount' => (int) env('SHOP_MAX_ORDER_AMOUNT', 5_000_000),
// 免运费门槛(分)
'free_shipping_threshold' => 9_900,
// 支持的支付渠道
'payment_channels' => ['alipay', 'wechat', 'mock'],
// 订单号前缀
'order_no_prefix' => env('SHOP_ORDER_NO_PREFIX', 'NO'),
];
SHOP_MAX_ORDER_AMOUNT=8000000
SHOP_ORDER_NO_PREFIX=BETA
在路由里读取(注意:代码里应读 config(),不要直接读 env(),因为 config:cache 后 env() 在模块外会返回 null):
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/config-demo', function () {
return response()->json([
'max_amount' => config('shop.max_order_amount'),
'free_shipping' => config('shop.free_shipping_threshold'),
'channels' => config('shop.payment_channels'),
'prefix' => config('shop.order_no_prefix'),
]);
});
访问 http://127.0.0.1:8000/config-demo 验证。
步骤 4:注册一个全局中间件观察生命周期
php artisan make:middleware TrackRequest
编写 app/Http/Middleware/TrackRequest.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class TrackRequest
{
public function handle(Request $request, Closure $next): Response
{
// 前置逻辑:请求进入时执行
$start = microtime(true);
Log::info('request in', [
'method' => $request->method(),
'path' => $request->path(),
]);
$response = $next($request); // 把请求交给下一层
// 后置逻辑:响应返回前执行
$response->headers->set('X-Request-Duration',
number_format((microtime(true) - $start) * 1000, 2).'ms');
return $response;
}
}
在 bootstrap/app.php 的 withMiddleware 中注册为全局中间件:
->withMiddleware(function (Middleware $middleware): void {
$middleware->append(\App\Http\Middleware\TrackRequest::class);
})
# 查看日志,确认前置逻辑执行
# Windows PowerShell:
Get-Content storage\logs\laravel.log -Tail 5
# 确认响应头
curl -I http://127.0.0.1:8000/
# X-Request-Duration: 12.34ms
步骤 5:看清中间件的「洋葱模型」
请求 ──► TrackRequest 前置
└─► EncryptCookies 前置
└─► StartSession 前置
└─► 路由/控制器处理
◄─ 响应
◄─ StartSession 后置
◄─ EncryptCookies 后置
请求 ◄─ TrackRequest 后置
这与 Spring 的 FilterChain 完全一致:chain.doFilter() 之前是前置,之后是后置。理解这张图,你就理解了为什么认证中间件能在控制器之前拦截未登录请求。
讲清楚要点
app/ 放你写的代码,vendor/ 放别人写的代码:业务代码永远不要落到 vendor,config、routes、database 则是框架协作面。
bootstrap/app.php 是 11 版的神经中枢:路由文件挂载、中间件、异常渲染、命令调度(withSchedule)都在这里声明。
- 配置分两层:
config/*.php 是结构化配置,.env 提供环境差异值。代码只读 config(),env() 只出现在 config/ 文件中。
- 服务提供者是启动扩展点:
AppServiceProvider 的 register() 只做容器绑定,boot() 在所有提供者注册完后执行,适合做视图 Composer、事件绑定等。它等价于 Spring 的 @Configuration + @PostConstruct。
storage/ 必须可写:storage/framework 放 Session、缓存、编译视图,storage/logs 放日志,storage/app 放用户上传。部署时权限错误几乎都出在这里。
- web 路由与 api 路由的区别是中间件不同:web 组带 Session、CSRF 验证;api 组默认无状态、挂
/api 前缀。给前端 App 或第三方调用的接口写 api.php。
你可能会懵的地方
-
Laravel 11 文件怎么这么少?Kernel、Exception Handler 都去哪了?
被框架内部化了。需要定制时通过 bootstrap/app.php 的回调注入,属于「默认零配置、需要时再显式声明」的设计,目的是让新项目噪音更少。
-
Service 层要不要建?为什么官方没有 app/Services?
框架不强制。控制器很薄、模型方法够用时可以不建;一旦业务逻辑跨多个模型或被多个入口(HTTP + 命令 + 队列)复用,就应当建 app/Services,这点和 Spring 项目抽 @Service 的判断标准一样。
-
config:cache 之后为什么 .env 改了没效果?
缓存会把 config 与当时的 env 值合并固化为一个 PHP 文件。改完配置需要 php artisan config:clear(或重新 config:cache)。这也解释了为什么不能在 config/ 外面调用 env()。
-
Facade 和助手函数(如 config()、cache())该用哪个?
两者解析的是同一个容器服务。Laravel 官方两种都认可;团队内统一风格即可。控制器构造函数注入最利于测试,Facade 最简洁。
-
维护模式怎么用?
php artisan down(可带 --secret=xxx 设访问暗号、--retry=60 设 Retry-After),部署时先 down 再更新代码最后 php artisan up。负载均衡探活 /up 在维护模式会返回 503。
错法对照
| 错法 | 问题 | 正解 |
|---|
在 app/Http/Controllers 之外散落业务代码 | 入口多了无法复用 | 抽到 app/Services 或模型作用域 |
控制器里直接 env('DB_HOST') | 配置缓存后返回 null | 一律 config('database...') |
把上传文件存到 public/uploads 手写路径 | 权限与可见性失控 | 用 Storage::disk('public') + storage:link |
在 ServiceProvider register() 里查数据库 | 此时依赖可能尚未就绪 | 查库类逻辑放 boot() |
修改 bootstrap/cache/*.php 想改配置 | 这是生成物,会被覆盖 | 改源文件后重新生成缓存 |
| 给 API 路由挂 web 中间件拿 Session | 无状态接口被污染 | API 用 token/sanctum,保持无状态 |
卡点急救
- 新增的配置项读出来是 null:先确认文件名为
config/xxx.php 且返回数组;若跑过缓存,执行 php artisan config:clear;再用 php artisan tinker 输入 config('shop') 直接检查。
- 日志写不进去 / 视图编译报错:给
storage 与 bootstrap/cache 写权限。Linux 下常见命令 chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache。
- 改了
bootstrap/app.php 后整个站白屏:多半是闭包内引用了不存在的类或漏写 use。打开 storage/logs/laravel.log 看第一行错误;临时无法定位可先 git diff 逐段回退。
面试常问
-
请描述 Laravel 的请求生命周期。
请求进入 public/index.php,引入 Composer 自动加载;bootstrap/app.php 创建 Application,注册基础服务提供者并 Boot;Request 穿过全局与路由组中间件(洋葱模型);路由器匹配后由容器调用路由对应的闭包或控制器方法(自动解析构造与方法依赖);返回 Response,逆序穿过中间件后置逻辑,发送给浏览器;应用触发终止回调(如 Session 写入)。按「入口 → 引导 → 中间件 → 路由分发 → 响应 → 终止」六段讲即可。
-
Service Container、Service Provider、Facade 的关系?
Container 是绑定与解析对象的容器(IoC);Provider 在启动阶段向容器做绑定与初始化;Facade 提供一个静态代理,从容器里解析服务。对应 Spring:IoC Container、@Configuration/@Bean、以及一个自动注入后暴露的静态句柄。
-
Laravel 为什么推荐配置缓存?有什么坑?
config:cache 把数十个配置文件合并为一个数组文件,省去每个请求重复加载与闭包;坑是 env() 只应在 config 内调用,每次部署后必须重新缓存,否则新环境变量不生效。
自测三问
- 用户登录状态相关的逻辑(Session、CSRF)在哪一组路由上?对外提供给移动端的 API 应该写哪个文件?为什么?
- 一个请求到达控制器之前,至少经过哪三类组件?请按顺序说出来。
- 要新增「全站统一的灰度开关」,配置文件、
.env、读取代码分别写在哪里?部署时要执行什么缓存命令?
小结一句话
public/index.php 点火、bootstrap/app.php 装配、中间件洋葱包裹路由分发——看懂目录约定和这条生命周期,你就拿到了在 Laravel 项目里自由导航的地图。
延伸