
1. 为什么 2012 年我们要反复谈起 Laravel 3.X 的设计如果你现在打开 Laravel 的文档看到的已经是几十个服务提供器、Pipeline、队列和事件系统组成的庞然大物。而想真正理解 Laravel 为什么会长成今天这样最好的方式是退回去看它的“初代经典特性”——Laravel 3.X。2012 年 2 月发布的这个版本第一次在 PHP 5.3 时代把 IoC 容器、Eloquent ORM、Blade 模板、Artisan 命令行、Bundles 包管理这五样东西同时塞进一个框架里。任何一个词放到今天的框架目录里都不过时而且每一件都不是什么锦上添花的小功能而是后来 Laravel 整个体系的地基。这篇文章适合两类人一类是手里恰好接到 2012 到 2013 年之间的 Laravel 3 老项目需要读懂、维护甚至迁移另一类只是想理解现代 Laravel 设计思想的人。3.X 是那个“起点”它提出的很多约定直到 Laravel 11 仍然能看到影子。我会尽量按当年实际使用的方式去讲也会把后来踩过的坑写清楚因为老框架最坑人的地方往往不在功能本身而在当时看起来很自然、今天却很反直觉的某些习惯。1.1 设计理念把“表达力”放在“省代码”之前Laravel 3 出现之前PHP 框架的主流玩法是 Model-View-Controller 三个目录配一堆配置文件很多框架甚至提供图形化界面帮你生成 CRUD。这在当时确实提高效率但问题也明显代码写出来像填表格读起来不像人话。Laravel 3 的思路是反过来的——路由、SQL 查询、权限判断都应该像一句自然语言一样能被直接读出来。举一个最简单的例子。别的框架里你可能要在配置目录里写一个 XML 或者数组描述“用户访问 posts 列表时调用哪些控制器”而 Laravel 3 直接写Route::get(posts, postsindex); Route::post(posts, array(before auth, uses postsstore));这种写法不需要额外解释看一眼就知道“GET 请求去 posts 列表POST 请求要先过 auth 过滤器再去保存”。这背后是 Laravel 3 的一个核心选择所有配置都尽量用代码表达用 PHP 本身当配置语言。当时很多人觉得这是偷懒但实际上它把“写框架流程”和“写业务逻辑”之间的隔阂打穿了。你今天在 Laravel 里看到的路由闭包、中间件链都是这套思路的延续。还有一点容易被人忽略Laravel 3 很早就意识到“单一入口 一个类”的自动加载方式比 CI 那种按目录扫文件更可靠。整个框架目录本身就体现了 PSR-0 风格你可以直接 namespace 扩展不需要像老 CI 那样在 config 里挂一个 library。这个决策是后来 Laravel 能顺畅走向 Composer 生态的重要前提。1.2 和同期框架对比一次很克制的新旧技术折中回顾 2012 年PHP 市场主流是 CodeIgniter 2、Yii 1.1、CakePHP 2都在 PHP 5.3 时代摸索怎么让开发更顺手。Laravel 3 选择了一个巧妙的位置既有老框架的“入门快”又有新框架的“可测试、可扩展”。我整理过一个简单的对照正好能看出差异维度CodeIgniter 2Yii 1.1Laravel 3.X路由URL 段映射为主配置规则闭包 RESTful 控制器ORM自带 Active RecordCActiveRecord relationsEloquent等下划线命名模板原生 PHP HelperCViewBlade 语法CLI无自带 shell 工具Artisan 命令依赖管理Spark 插件PECL / 扩展Bundles Composer尝试配置风格config 数组config 数组PHP 数组但可返回闭包当时大多数框架把重心放在“怎么把增删改查做得更快”上Laravel 3 则把重心放在“怎么让开发者在组织代码时更舒服”。它没有发明新的数据库抽象而是在 SQL 查询外面包一层流畅接口它没有发明新的模板引擎理念而是把 PHP 标签做了一层语法糖它没有发明依赖注入而是用 IoC 容器把它包装成一个人人能上手的工具。这种“拿来主义”在技术史上其实比发明更有效因为它的学习成本被大大降低了。所以如果你想找一个词概括 Laravel 3 的整体设计我会选“克制”。它知道 PHP 5.3 能做什么也知道当时 PHP 社区普遍缺什么于是没有贪心去搞全功能框架而是挑最影响日常开发体验的部分做深。这种贴近真实需求的取舍是今天很多框架值得借鉴的。2. 核心经典特性逐项回顾与实战解读2.1 路由、过滤器与 RESTful 控制器的表达力很多人直到今天写 Laravel 3 还是只把路由当“URL 到 Controller 的映射”这等于把最值钱的部分浪费掉了。Laravel 3 最让我惊艳的设计不是路由本身而是路由过滤器。它本质上就是现代中间件的前身但语法更轻直接挂在路由上Route::filter(auth, function() { if (Auth::guest()) return Redirect::to(user/login); }); Route::get(admin, array(before auth, function() { return 管理后台; }));这里的before会在控制器执行之前运行。如果过滤器返回了 Response后续流程直接终止相当于给路由加了一道“安检门”。我当时用这个特性写权限判断体验非常舒服——不需要在每个控制器方法里重复检查登录只要给特定路由挂before auth就行了。换成今天的语言这就是中间件只是当时没有中间件这个名字。真正能体现表达力的是 RESTful 控制器。在application/controllers里写一个类把 HTTP 动词直接变成方法名class User_Controller extends Base_Controller { public $restful true; public function get_index() { return View::make(user.index); } public function post_login() { $credentials array( username Input::get(username), password Input::get(password), ); if (Auth::attempt($credentials)) { return Redirect::to(dashboard); } return Redirect::back()-with(login_errors, true); } }注意两个细节类名要写成User_Controller不是class UserController方法名是get_xxx/post_xxx而不是现代 Laravel 的index()/store()。$restful true这个开关在 Laravel 3 里是分水岭开了之后一个post_login方法只响应 POST 请求代码里再也没有if ($_POST)这种分支。Laravel 3 还有Route::controller(user)让你不必一个一个注册路由但我建议实际业务里还是显式写路由更清晰至少出了权限问题一眼能看到。2.2 Eloquent ORM 与查询构建器的奠基作用Laravel 3 的 Eloquent 还处于非常青涩的开花期但它已经确立了“关系定义”的基本姿势。你只需要继承Eloquent设置$table然后在模型里定义关系。比如用户和文章的一对多关系class User extends Eloquent { public static $table users; public function posts() { return $this-has_many(Post); } } class Post extends Eloquent { public static $table posts; public function user() { return $this-belongs_to(User); } }这里有个很关键的历史细节在 Laravel 3 里关系方法一律是下划线风格比如has_many、belongs_to、has_one。到了 Laravel 4官方才统一改成驼峰式的hasMany、belongsTo。所以如果你今天看到一句$user-posts(),也不要奇怪旧项目里就是这种写法。另外一个容易踩坑的约定是默认外键名Laravel 3 默认会找user_id这样格式的字段。如果你的表字段不叫这个必须手动传参数比如$this-has_many(Post, author_id)。我在一个老项目里见过很多次“关系返回空数组”的排查记录最后都是外键默认值惹的祸。除了 EloquentLaravel 3 还自带一个非常顺滑的 Fluent Query Builder。它和 ORM 的区别是更贴近 SQL适合处理复杂查询$posts DB::table(posts) -where(user_id, , 1) -order_by(created_at, desc) -take(10) -get(); $count Post::where(title, like, %Laravel%)-count();当年接触到这套接口第一感觉是“终于不用再拼字符串 SQL 了”。它把 for、where、order_by 这种东西变成方法链读起来几乎和英文句子一一对应。这个设计直接影响了后来的查询构建器到现在大部分 ORM 都采用了类似风格。如果想做输入校验Laravel 3 也有一套很轻的 Validator$rules array( email required|email, password required|min:6, ); $validation Validator::make(Input::all(), $rules); if ($validation-fails()) { return Redirect::back()-with_errors($validation); }这套校验规则虽然简单但胜在零学习成本而且可以挂在路由过滤器里使用。在 3.X 时代它已经足够覆盖绝大多数表单场景后来的许多 PHP 框架在整体体验上也并没有超越它太多。2.3 Blade 模板、IoC 容器与捆绑包被低估的“组合拳”Blade 在 Laravel 3 里第一次出现它为后来整个模板语法定了调用{{ }}输出变量用if/foreach做控制流用layout和section做布局继承。一个典型的 3.X 视图长这样layout(layout.main) section(content) h1{{ $post-title }}/h1 p{{ $post-body }}/p endforeach foreach ($post-comments as $comment) p{{ $comment-content }}/p endforeach endsection熟悉现代 Laravel 的你应该已经发现layout后来改成了extendsendsection和stop也经历了多次变化。但核心逻辑没变子视图声明自己挂在哪个布局上用section填充内容布局里用yield(content)占位。Blade 最大的价值是把“视图中的 PHP 语法”压缩到最短写起来像是专门为 HTML 模板设计的新语言。当时很多团队还在用原生 PHP 模板或者 Smarty迁移到 Blade 后的代码量几乎是肉眼可见地减少。IoC 容器在 3.0 版本就已内置是另一个被低估的经典设计。它的传统用法是“绑定一个服务、解析一个服务”IoC::bind(mailer, function() { return new Mailer(Config::get(mail.host)); }); $mailer IoC::resolve(mailer);我觉得把它理解成“一个记了很多外卖电话的本子”更合适你要吃什么不用自己动手做查一下本子打个电话就行。绑定的时候可以传闭包也可以传类名、实例甚至用IoC::singleton注册单例。这个容器虽然不如 Laravel 4 之后的服务容器那么庞大但已经很明确地表达了“不再让开发者自己到处 new 对象”的思路。这为单元测试和依赖替换开了口子也让代码的模块边界变得更加清晰。捆绑包Bundle算是 3.X 时代的“包管理器”只是它还比较粗糙。你可以把它理解成老式 Plugins一个捆绑包可以同时包含控制器、模型、视图、迁移、路由、CSS 和 JS注册之后整体挂进应用里。注册方式常用配置文件// application/bundles.php return array( admin array(handles admin), );handles表示这个包处理以/admin开头的所有路由。也可以直接调用Bundle::register(admin, array(handles admin))。捆绑包让“功能复用”第一次在 PHP 框架里变得像搬模块一样简单但它后来还是被 Composer 取代了因为捆绑包版本管理和依赖传递做得不够好。回头看Laravel 3 的这个过渡设计帮整个生态完成了一次从“插件思维”到“包管理思维”的训练功不可没。3. 亲手跑一个 Laravel 3.X 项目3.1 环境准备与技术栈选择如果你真想体验 Laravel 3 的老味道第一步不是装最新 PHP而是会踩很多坑。Laravel 3 的设计目标是 PHP 5.3但现代 PHP 8 以上跑它会有不少兼容问题。这里我强烈建议用 PHP 5.6 或 PHP 7.0 的环境尤其是 Docker 里拉一个php:5.6-apache的镜像比自己编译旧版本省心得多。当年生产环境常见组合是 PHP 5.4 Apache MySQLNginx PHP-FPM 也有但配置里要注意重写规则。安装 Laravel 3 本身就是把代码放到服务器上如果你从官方仓库或者 GitHub 拿到 3.2.x 源码包把整个目录放到站点根目录然后把 Web 根目录指向public访问就能看到欢迎页。用 Composer 的人也可以在 Laravel 3.1 以后直接拉laravel/framework:3.2.*不过我当时更喜欢下载完整包因为不用处理 Composer 早期镜像不稳定问题。配置文件在application/config目录里最重要的两件事第一打开application/config/application.php把url改成你的域名key填一段 32 位随机字符串这关系到 Session 和加密功能是否正常运行。第二编辑database.php填好 MySQL 的主机、库名、用户名和密码。第三看下session.php的driver配置如果选的是database你得先准备好sessions表。刚开始为了省事可以先选cookie驱动调试通再换。伪静态配置比较关键Apache 环境在public目录加.htaccess就能生效Nginx 则需要在 server 块里写location / { try_files $uri $uri/ /index.php?$uri$args; }注意这是面向 Laravel 3 的时代做法现在新版一般不这么写但老项目里这套 rewrite 非常常见。3.2 用迁移建表顺手写完 SeederLaravel 3 的迁移文件放在application/migrations里文件名建议带日期前缀类名要写成下划线风格例如create_posts.phpclass Create_Posts { public function up() { Schema::create(posts, function($table) { $table-increments(id); $table-integer(user_id)-unsigned(); $table-string(title); $table-text(body); $table-timestamps(); }); } public function down() { Schema::drop(posts); } }这里有个小习惯外键字段user_id一定要用integer()关联才有意义。timestamps()会自动补充created_at和updated_at两个 datetime 字段这是从早期就保留到今天的约定后端存储字段名基本没变过。迁移写完先执行php artisan migrate:install这个命令会在数据库里创建一个迁移记录表然后再执行php artisan migrate把所有未执行的迁移跑掉。如果后续要改字段老项目经常手写Schema::table但 3.X 的SchemaAPI 能力有限有时候不如直接重新迁移来得高效。适合第一次上手的方式是“开发环境直接 drop 库再 migrate”因为版控清晰且不会污染数据。如果要做数据填充Laravel 3 里的 Seeder 可以理解为一个迁移之后运行的“任务类”。最简单的做法是在application/seeders里放一个Post_Seeder然后在命令行执行php artisan db:seed那句db:seed会扫描 seeder 目录执行所有run()方法。注意这里不需要像 Laravel 8 之后那样写DatabaseSeeder::class老版本只有一整套自动发现逻辑类名和文件名对应关系一定要准确。3.3 用 Eloquent 模型关联并写一个 RESTful 控制器有了users表和posts表定义好User和Post模型后在控制器里就可以直接操作。我习惯把业务查询尽量收敛到模型而不是在控制器里堆大段链式调用。控制器则使用$restful true的风格让 HTTP 动词直接映射到方法。比如我们要做“用户详情页和发文章”class Home_Controller extends Base_Controller { public $restful true; public function get_index() { $user User::with(posts)-first(); return View::make(home.index) -with(user, $user); } public function post_publish() { $rules array( title required|max:80, body required, ); $validation Validator::make(Input::all(), $rules); if ($validation-fails()) { return Redirect::back()-with_errors($validation); } $post new Post(array( user_id Auth::user()-id, title Input::get(title), body Input::get(body), )); $post-save(); return Redirect::to(posts/ . $post-id); } }这里重点说一下with(posts)它在 Laravel 3 里就是预加载关联数据了。如果不加这个在视图里循环$user-posts时每取一条文章都会触发额外 SQL也就是常说的 N1 问题。我在老项目里巡检时第一件事就是看控制器里有没有with()因为很少看到新手会主动用它。还有一个小细节Auth::user()-id这种取值方式在老版本里很常用但如果登录状态判断不及时会报空对象错误所以控制器里最好先用Auth::check()做一次判断。3.4 用 Blade 输出页面用 Artisan 跑一个内部任务视图文件放在application/views/home/index.blade.php布局放application/views/layout/main.blade.php。布局文件里只需要一个内容坑位!DOCTYPE html html headtitleLaravel 3 回顾/title/head body yield(content) /body /html子视图则利用layout(layout.main)声明挂在哪个布局上再用section填内容layout(layout.main) section(content) h1{{ $user-username }} 的文章/h1 ul foreach ($user-posts as $post) li{{ $post-title }} - {{ $post-created_at }}/li endforeach /ul endsectionyield(content)和section(content)之间的对应关系不难理解但要注意如果子视图里没用section(content)布局里的yield就渲染成空字符串。老版本不会报错所以排查“页面白了一片”时先去看看 block 名称是否拼对了。Artisan 在 Laravel 3 里的用法更像一个“任务执行器”而不是今天这种路由式的命令系统。你可以直接在application/tasks目录下建一个类文件// application/tasks/count.php class Count_Task { public function run() { echo 用户数 . User::count(); } public function active_users($limit 10) { echo 活跃用户 . User::where(last_login,, 0)-take($limit)-count(); } }执行方式也很简单php artisan count php artisan count:active_users 20在 L3 里任务类名是xxx_Task方法就是具体命令参数直接跟在后面。这套设计虽然简陋但已经足够程序员写一些内部脚本来完成报表统计、数据修复之类的脏活。很多老项目里都埋着这种一个小类解决一个大问题的 Artisan Task我后来迁移时还经常需要把它们的逻辑翻译成新版命令。4. 从 Laravel 3.X 活到今天的踩坑记录4.1 常见故障排查速查表老框架跑起来之后问题通常集中在环境兼容、自动加载、路由重写三块。下面是我整理的最常遇到的几类几乎是 Laravel 3 项目标配现象排查方向处理建议首页白屏但文件存在application/storage/logs有没有日志key是否设置session 驱动先看日志再检查配置或关掉 display_errorsPHP 7.4 及以上直接报错旧语法、魔术方法、each()等函数已废弃用 Docker 跑 PHP 5.6 或 7.0 避免挣扎php artisan报 Class not found任务类名和文件名命名不匹配确认是Count_Task/count.php大小写要准路由能进欢迎页但业务 URL 404rewrite 规则没生效检查 Nginxtry_files或 Apache.htaccessEloquent 关联返回空数组外键名不是user_id手动指定外键或调整表结构表单提交后不执行对应方法控制器没有$restful true确认 GET/POST 方法名与 HTTP 动词匹配这里有个我今天特别想强调的坑千万不要把老 Laravel 3 应用直接暴露到公网上。它早就停止维护了很多安全公告的修复并没有进入 3.X 分支。如果真的还要长期运行正确的做法是放到隔离内网、固定 IP 访问或者规划迁移。历史代码可以拿来学习但拿一个停服框架做生产系统风险不是“缓存和速度”而是安全问题。4.2 新老差异映射迁移时最值得注意的 8 处变化从 Laravel 3 迁到 Laravel 4 再迁到现代版本是一个漫长的过程。我见过很多项目直接文本替换结果跑出各种诡异异常。先认清这几组核心差异会省掉大量排查时间Laravel 3 写法现代 Laravel 写法备注class User_Controller extends Base_Controllerclass UserController extends Controller命名空间与 PSR-4 变了必须改自动加载路径Route::get(posts, postsindex)Route::get(posts, [PostController::class, index])控制器字符串转数组顺带支持 RDIpublic $restful true; public function get_index()public function index()配合路由中分开 GET/POSTRESTful 控制器被路由替代layout(layout.main)extends(layout.main)Blade 指令升级IoC::bind(foo, function(){...})App::bind(foo, function(){...})服务容器统一还多了 bind 单例语法function posts() { return $this-has_many(Post); }function posts() { return $this-hasMany(Post::class); }下划线变驼峰模型写全限定类名Bundles 注册Composer package ServiceProvider老捆绑包要拆成可安装组件一个个处理application/controllersapp/Http/Controllers或app/Modules/...目录结构完全变化表格里最后那一行尤其关键Laravel 3 的整个application目录结构几乎都在当代 Laravel 里被拆散或者改名了。所以如果你要做一次完整迁移我的建议是“不要贪快”。先把旧项目功能清单列出来然后在新版里按模块重建一次性把所有控制器方法、模型关系、Blade 语法都改成现代风格。文本替换看似省事但容易把老命名习惯留在代码里最后变成新壳旧里维护时两头都不讨好。真要做增量迁移有个更稳的过渡方案在旧 Laravel 3 项目前加一层反向代理把新功能逐渐放到新框架路由里老功能继续走旧应用。等新应用覆盖全部功能后再把旧服务下线。我踩过最大的坑就是想一次性把 6 年老项目的所有has_many改成hasMany结果 20 个模型改完页面反而全崩了最后查出来是几个关系参数写错。所以最终还是回到一条经验重构永远比替换更值得我们付出耐心。我个人在实际操作中的体会是老框架代码再晦涩也是一份宝贵的“系统设计历史”。你读它不只是在读语法而是在读当时的开发团队是怎么组织代码的。今天是php artisan make:model能一键生成一堆文件但在 Laravel 3 时代每份控制器和模型的命名都很认真因为它们必须足够自解释才能让别人接手。也许那个年代留下的最大遗产不是某个具体类名而是这种“把代码当作品来对待”的态度。