
如果你是从Laravel 4.x甚至5.x才开始接触Laravel的开发者第一次看到Laravel 3.X的代码会觉得眼前一花——没有命名空间控制器叫Home_Controller路由参数写成(:num)数据库构建用DB::table(users)-order_by(...)这些写法在今天的新项目中已经完全绝迹。但正是这个相貌古朴的版本在2012年2月把Laravel这个名字带进了PHP主流框架的视野也奠定了后面十几年Laravel一路迭代的设计基调。这篇回顾不是让你放弃新版本回去用老古董而是把“初代经典”摊开来看。我们会拆解Laravel 3.X的路由、Eloquent ORM、Artisan命令行、迁移机制、认证和Bundle扩展搞清楚那些今天看起来理所当然的能力最初是怎么设计的再对比3.X到4.0重写时哪些东西被留下了、哪些被淘汰了。无论你是维护过老项目的工程师在准备框架演进相关的面试还是单纯对“一个框架为什么长成今天这样”感兴趣这篇内容应该都能给你一些有参考价值的答案。1. 为什么值得回头看Laravel 3.X1.1 2012年前后的PHP战场2012年这个时间点很有意思。PHP 5.3是市场主流PHP 5.4刚推出不久Composer虽然已经在2012年4月发布但绝大多数PHP开发者还没有养成“用依赖管理器装包”的习惯更多人的工作流还是下载一个框架压缩包解压到服务器目录里直接改。框架层面CodeIgniter在国内国外都有庞大的用户群CakePHP占着老牌全栈的位置Yii靠性能表现拉走一批人Symfony2则在企业级项目里立住了脚。Laravel 3.0在这种格局下发布打的牌很明确更简洁的API、更现代的开发体验、外加一个叫Artisan的命令行工具。Taylor Otwell在Laravel 3里把“为Web工匠设计的PHP框架”这句口号落地了——它没有试图做成一个什么都能干的企业级全家桶而是先把路由、ORM、迁移、认证这些每个Web项目都绕不开的部分做到顺手。也正因为这种聚焦Laravel 3才能在巨头林立的年代快速圈住一批被CodeIgniter和原生PHP折腾得头疼的开发者。1.2 Laravel 3到底解决了什么痛点当时PHP开发者最常见的状态是写原生PHP每次新建页面都要手写include、手写URL分发SQL和HTML混在一起改一个公共逻辑可能要动十几个文件。CodeIgniter比原生PHP规范了不少MVC分层、路由、ActiveRecord数据库类都有但它的路由不够灵活、ORM能力偏弱、没有迁移机制更别说命令行脚手架。如果项目里想给数据表加个字段基本靠登录phpMyAdmin手工执行SQL多环境同步全靠“复制数据”。Laravel 3把这些问题打包处理了。路由层支持普通路由、RESTful路由、带正则约束的参数占位符还提供before/after过滤器这个“中间件雏形”数据库层有查询构造器和Eloquent ORM大部分SQL不再需要手写迁移机制解决表结构版本化让团队里每个人都能通过php artisan migrate保持同样的数据库状态。Artisan命令行的存在更是让“框架自带工具链”这个概念变得具体这在2012年的PHP社区里算得上新鲜事。1.3 今天你还在用的设计基因回看Laravel 3.X的价值不在于它的代码还能不能用而在于它的设计基因一直延续到了今天。你在Laravel 11里写Route::middleware(auth)时其实对应的是3.X里Route::filter(auth, ...)的过滤思想你使用php artisan make:migration时命令的名字和流程都能追溯到3.X的迁移体系你调用User::where(age, , 18)-get()时这套Eloquent的活动记录模式早在3.X就已经成型。另一个被很多人忽略的基因是“简洁门的式API设计”。Laravel 3全站没有命名空间类名直接平铺调用Route::、DB::、View::、Auth::这种静态门面风格在当时的PHP圈里其实是非常大胆的设计——它牺牲了一些理论上的严谨换来了写业务代码时极低的认知负担。现代Laravel里的Facade机制虽然用到了命名空间和IoC容器但给开发者留下的“极简入口”体验和3.X的时代是一脉相承的。2. 初代经典特性逐一拆解2.1 路由一套规则包揽HTTP动作与前置处理Laravel 3.X的路由系统放在今天看依然不过时。最基本的路由就是一行匿名函数Route::get(/, function() { return View::make(home.index); });带参数的写法用的是正则占位符风格和后来{id}的形态完全不同Route::get(user/(:num), function($id) { return 这里显示用户ID为{$id}的页面; }); Route::get(post/(:any), function($slug) { return $slug; });:num只匹配数字:any匹配任意非空片段:all可以匹配多段。这个设计在当年很实用等于把URL参数的类型约束直接写进了路由定义里路由匹配失败时框架会直接返回404不用在控制器里再二次校验。RESTful支持也是3.X的卖点。一个路由可以按HTTP动词拆成多个动作Route::get(user/(:num), usershow); Route::post(user, usercreate); Route::put(user/(:num), userupdate); Route::delete(user/(:num), userdelete);你还会经常看到过滤器Filter。它做的事情跟现在的中间件几乎一样只是名字和用法更朴素Route::filter(auth, function() { if ( ! Auth::check()) { return Redirect::to(login); } }); Route::get(dashboard, array(before auth, function() { return View::make(dashboard); }));before过滤器会在路由执行前跑返回Response对象则短路不会进入真正的业务逻辑对应的还有after过滤器适合做统一的响应后处理。这个结构虽然没有中间件那么精细但思路已经非常清晰了。2.2 Eloquent ORM把ActiveRecord模式带进了LaravelEloquent应该是Laravel 3里最有辨识度的部分。它采用ActiveRecord模式让一个模型类直接映射一张数据表实例既能查数据也能改数据。在3.X里定义一个模型非常轻量class User extends Eloquent { public static $table users; public static $timestamps true; }注意这里用的还是$table users静态属性不是现代Laravel的protected $table。开启$timestamps后表里如果有created_at和updated_at字段框架会自动帮你维护。基础增删改查的写法现在看起来会有点“老派”但逻辑已经完整$user User::find(1); $users User::where(age, , 18)-get(); $user-name 张三; $user-save(); $newUser new User; $newUser-username jane; $newUser-password Hash::make(secret); $newUser-save(); $user-delete();关联关系同样是下划线风格比如has_many、belongs_toclass Post extends Eloquent { public function author() { return $this-belongs_to(User); } }Eloquent选择ActiveRecord而不是复杂的DataMapper模式我认为是刻意为之。PHP本身是脚本型语言开发者追求的是“快、够用、直观”ActiveRecord让你不用先理解一套数据映射哲学就能立刻写出业务代码。这个选择在3.X时代被验证是有效的今天Laravel的Eloquent虽然引入了一大堆接口和宏但核心心智模型还是当年那套。2.3 查询构造器写SQL的门槛被砍了一半如果不想用完整的ORM只想手写一条查询Laravel 3的查询构造器给了比原生SQL更舒服的中间形态$users DB::table(users) -where(age, , 18) -order_by(created_at, desc) -take(10) -get(); $user DB::table(users)-where(username, , jane)-first(); $count DB::table(users)-where(active, , 1)-count();链式调用是当时PHP圈里很高频的名词Laravel 3把Fluent Interface用得让新手几乎无感——每调一个方法返回的还是查询对象可以一直点下去。order_by、take、get、first、count这些方法在3.2里就已经非常齐全。真遇到复杂查询你仍然可以用DB::query($sql)直接执行原生SQL框架并不会拦着你。这个设计的好处是明显的团队里初级开发可以不写SQL完成80%的数据库操作SQL注入风险也被框架的绑定参数机制化解了大半。放到2012年的环境里这已经是很成熟的工程化思路了。2.4 迁移与Schema生成数据库结构也能做版本管理在没有迁移的年代多人协作时改数据库结构是一次灾难你在本地加了字段同事的本地库没有测试环境库里又是另一种状态最后只能靠互相发SQL文件靠嘴对齐。Laravel 3把迁移机制带进来后这一切开始有了标准答案。创建一张用户表只需要定义一个迁移类class Create_Users_Table { public function up() { Schema::create(users, function($table) { $table-increments(id); $table-string(username); $table-string(email); $table-text(bio); $table-timestamps(); }); } public function down() { Schema::drop(users); } }up方法负责正向变更down方法负责回滚这两个方法的命名被后世所有主流框架沿用。配合Artisan命令php artisan migrate:install php artisan migrate php artisan migrate:rollbackmigrate:install会创建一张记录迁移状态的表之后的每次migrate只会执行还没跑过的迁移文件。把数据库结构像代码一样纳入版本管理这在当时是革命性的也是后来Laravel能撑起大型团队协作的根基之一。2.5 Artisan与Bundle早期的“脚手架包管理”Artisan命令行是Laravel 3的另一个标志。当时很多PHP框架发布的只有代码库工作流是靠浏览器访问安装向导Artisan让人意识到“PHP框架也可以像Rails那样提供专业的命令行体验”。3.X的Artisan还远没有今天的make:controller全家桶那么丰富但方向已经明确php artisan migrate php artisan migrate:make create_users_table php artisan task php artisan route:listBundle系统则是3.X时代的模块化尝试。它的定位相当于“带自动加载的插件包”可以理解为Composer生态成熟前的过渡方案php artisan bundle:install auth php artisan bundle:install hybrid安装后需要在application/bundles.php里注册然后就可以通过Bundle定义的别名调用里面的类和路由。在2012年没有Packagist、没有成熟自动加载标准的环境里Bundle算是一种聪明的集成方式虽然它后来被Composer ServiceProvider的组合彻底取代但“功能包一键安装”的体验让Laravel早期的生态积累快了不少。2.6 认证、表单辅助与IoC雏形被忽略的边角料除了上面几个大特性Laravel 3.X还有几块没那么显眼但很常用的拼图。认证系统已经在3.X里相当完整if (Auth::check()) { // 用户已登录 } $credentials array(username jane, password secret); if (Auth::attempt($credentials)) { return Redirect::to(dashboard); } Auth::logout();它支持认证驱动的可配置方式比如数据库驱动和文件驱动概念延续到了今天。表单辅助函数是那个年代写页面最省力的工具之一echo Form::open(user/profile); echo Form::label(email, 邮箱); echo Form::text(email); echo Form::submit(提交); echo Form::close();它会自动输出带正确method和action的form标签省去很多重复HTML。这类辅助函数到了Laravel 4演化为HTML包后来逐渐被Blade组件替代。事件系统也已经存在比如你可以监听应用错误或自定义业务事件Event::listen(500, function($exception) { // 记录异常 }); Event::listen(auth.login, function($user) { // 用户登录后的逻辑 });还有IoC容器的雏形。Laravel 3里能通过IoC::register注册一个依赖之后用IoC::resolve取出来这为4.0真正成型的依赖注入容器预留了土壤。这些“边角料”单看都不稀奇但它们组合在一起让Laravel 3第一次呈现出“现代PHP框架”的完整轮廓。3. 回到现场用Laravel 3.X搭一个用户面板说了这么多特性不如直接回到当时的现场完整走一遍。我以Laravel 3.2为例做一个带登录保护的简单用户面板。3.1 安装与目录结构Laravel 3的安装非常原生态没有Composer create-project那套流程。去官网下载完整包解压到Web根目录然后把Web服务器的DocumentRoot指向public目录因为真正的入口和资源都在这里。项目根目录的结构大概是laravel/ application/ config/ controllers/ hooks.php language/ libraries/ migrations/ models/ routes.php start.php tasks/ views/ public/ index.php css/laravel/目录是框架核心源码application/是应用代码公共目录和框架目录彻底分离。这个结构比CodeIgniter那种入口文件放在根目录的做法更安全也是后来Laravel一直坚持的形态。入口文件public/index.php的内容很简单它加载框架核心文件后框架会自行完成路由解析、请求分发和响应输出。整个启动过程在今天看起来神秘在3.X里其实就是require加一堆类自动加载。3.2 配置数据库和启动入口数据库配置在application/config/database.php返回一个数组return array( driver mysql, host localhost, database user_panel, username root, password your_password, charset utf8, prefix , );3.X的配置很多用“返回数组”的方式这个习惯一直保留到了Laravel 11只是配置项组织方式有了很大变化。应用级配置在application/config/application.php里可以设置时区、默认语言、自动加载列表等。3.3 路由、控制器与视图的完整链路在application/routes.php里定义一套用户面板的路由Route::get(/, homeindex); Route::get(users, usersindex); Route::get(user/(:num), usersshow); Route::post(user/(:num), usersupdate);对应的控制器放在application/controllers/里class Users_Controller extends Base_Controller { public $restful true; public function get_index() { $users User::all(); return View::make(users.index) -with(users, $users); } public function get_show($id) { $user User::find($id); return View::make(users.show) -with(user, $user); } public function post_update($id) { $user User::find($id); $user-email Input::get(email); $user-save(); return Redirect::to(user/.$id); } }注意几个时代特征控制器继承Base_Controller$restful true表示开启RESTful风格方法名用get_、post_区分HTTP动词视图通过View::make加载名字里的点号对应用户目录下的视图文件和现代Laravel的view(users.index)几乎一致。视图文件是普通的PHP模板在application/views/users/index.php里h1用户列表/h1 ?php foreach ($users as $user): ? p a href?php echo URL::to(user/.$user-id); ? ?php echo $user-username; ? /a /p ?php endforeach; ?当时还没有Blade模板语法就是PHP原生标签URL::to()负责生成站内链接。这套旧模板写法放在今天会觉得啰嗦但可读性并不差过渡到4.X时官方给的方案是继续支持PHP视图所以老项目迁移的阵痛主要不在视图层。3.4 迁移和模型实现新建一张用户表和一个资料表用Artisan生成迁移并填充逻辑Schema::create(users, function($table) { $table-increments(id); $table-string(username); $table-string(email); $table-string(password); $table-timestamps(); });模型方面也不需要搞复杂的use语句class User extends Eloquent { public static $table users; public static $timestamps true; public function profile() { return $this-has_one(Profile); } }在一对一关联下你可以直接通过$user-profile访问关联模型。这套关联映射虽然不是Laravel 3的首创但它把定义方式的复杂度降到了“写一行方法”的水平。3.5 用过滤器控制登录访问给用户面板加上登录保护Route::filter(auth, function() { if ( ! Auth::check()) { return Redirect::to(login); } }); Route::get(users, array(before auth, uses usersindex)); Route::get(user/(:num), array(before auth, uses usersshow));如果用户没有登录before过滤器返回Redirect::to(login)页面会直接跳走后面的控制器方法根本不会执行。这个模式放到中间件时代也很好理解本质上就是“在进入业务逻辑前做安全检查”。当时我甚至会把权限判断写在多个路由里复用这正是Laravel后来把过滤器升级成中间件并能分配到路由组的基础场景。3.6 Artisan实操和最终效果整个流程跑起来只需要几条命令php artisan migrate:install php artisan migrate php artisan route:list启动MySQL、配置好虚拟主机、浏览器打开入口路由用户面板就能用了。登录后看到的是数据库里的用户列表未登录直接访问/users会被拦到登录页。这套从命令到页面的闭环体验在2012年对PHP开发者来说相当新鲜——以前写CodeIgniter从没这么顺过。4. 3.X到4.0的巨变经典为何被重写4.1 Composer和命名空间是最大的分水岭Laravel 4.0在2013年发布时官方口径是“几乎全部重写”。最根本的变化有两个全面引入Composer和PHP命名空间。3.X时期没有Composer依赖管理靠手动下载和Bundle自动加载靠框架自己的autoloader。到了4.XLaravel摇身一变成为Composer包每个类都在Illuminate命名空间下开发者需要通过use来引入门面Facade则变成了真正的设计模式。这个变化是历史必然Composer迅速成为事实标准PSR-0/PSR-4让PHP包生态走向正规化Laravel如果不跟就会失去整个现代PHP生态的支撑。但这意味着大量原有代码必须重写。Route::get变成了Route::get可是要加use Illuminate\Support\Facades\Route;DB::table同理order_by变成了orderBy。仔细看表面语法变化不大底层机制从“全局类”变成了“命名空间门面门面”理解框架的难度提升了生态的扩展性也提升了。4.2 路由、控制器写法的代差路由占位符的变化最直观Laravel 3.XLaravel 4.0user/(:num)user/{id}user/(:any)user/{slug}user/(:all)user/{path?}4.X的参数风格更接近主流Web框架可读性更强。控制器的写法也换了// Laravel 3.X class Users_Controller extends Base_Controller { public $restful true; public function get_index() {} } // Laravel 4.0 class UsersController extends BaseController { public function getIndex() {} }类名从下划线改成驼峰方法从get_index变成getIndex风格更符合PHP命名约定。这些改动单独看都不大但如果老项目代码很多改动量就很可观。4.3 从3.X演进到今天的特性对照3.X阶段形态当前Laravel10.x/11.x/12.x形态点评Route::filter(auth, ...)Route::middleware(auth)思想一致实现更精细User::where(...)-get()Eloquent同名方法支持更多关联和模型事件核心没变生态变厚php artisan migratephp artisan migrate命令几乎一样IoC::registerApp::bind/App::makeServiceContainerIoC理念直接晋级Form::text(...)Blade组件/原生HTML辅助函数逐步退场BundleComposer包 ServiceProvider模块化方案换代经过这个对照能看得很清楚Laravel的底层机制一直在换但面向业务的核心API活了下来。Eloquent、迁移、中间件前身、Artisan工具链这些“骨架”在3.X时期就被打好了后面的版本更多是在填充筋肉。5. 老版本实操中的疑难杂症与避坑指南5.1 环境兼容老版本不是扔进PHP 8就能跑千万别把Laravel 3.X的源码直接放到现在PHP 8.x环境里跑。3.X针对PHP 5.3/5.4设计用了不少今天已被移除或不兼容的语言行为比如某些构造函数写法、全局类自动加载方式、Magic Quotes时代的残留逻辑。即使勉强跑起来大概率会碰壁在语法错误和致命函数缺失上更别说大量不再安全的写法。想认真做版本考古最稳妥的做法是准备一个PHP 5.4环境的Docker容器或虚拟机把Laravel 3.2放进去局部使用。如果只是读源码研究设计而非实际调试那其实环境问题不大重点看核心逻辑即可。还要反复强调一点Laravel 3.X早已停止安全维护已知漏洞陆续公开生产环境无论如何都不要用它也别做公网可访问的演示。技术回顾和源码学习完全没问题但拿老框架搭面向公网的服务就是把数据往火坑里送。5.2 从3.X迁移到新版本最容易踩的雷如果你真的接手了一个Laravel 3老项目要把它升级到4.X以上这几个地方是重灾区数据库操作方法名order_by、group_by、get这些命名都需要改到4.X之后的驼峰风格orderBy、groupBy。模型基类3.X是extends Eloquent4.X之后变成了extends Model同时命名空间引入方式变了。Auth认证参数Auth::attempt的凭证数组结构在后续版本中基本保留但表单字段命名和session驱动配置方式有不少差异。视图辅助函数Form::text、HTML::link这类函数在新版中不再内建要么引入独立的laravelcollective/html包要么干脆用原生HTML加Blade组件重写。路由占位符(:num)改成{id}同时要在匹配模式上增加相应的正则约束。迁移时建议先画一张“3.X API对应新版API”的对照表逐个文件改比边猜边改快得多。顺序上先改路由和控制器入口再改模型最后处理视图辅助函数。5.3 推荐的低成本源码研读路线如果你纯粹想从Laravel 3.2的源码里吸收设计思路不用完整跑项目直接啃laravel/目录就行。我建议按这个顺序看先读laravel/laravel.php理解框架的引导加载流程接着读laravel/route.php看它如何解析URL、匹配路由、执行过滤器再读laravel/database/目录下的查询构造器和连接管理器最后读laravel/eloquent目录里Eager Loading和关联关系实现。代码量不大每块都对应一个独立职责比读现代Laravel那棵庞大的代码树轻松很多。看完你会明显感受到那个年代的框架克制——核心类文件不多很多功能是“恰好好用而不是无限扩展”。这种克制对今天的自己做技术选型和架构设计有启发作用。5.4 常见问题速查表症状常见原因处理方向Route::get(user/(:num))无效PHP版本过高或路由缓存问题用Composer镜像环境在PHP 5.4下复现User::find(1)返回null表名映射不对$table静态属性未匹配确认模型里的$table值是否和真实表名一致Auth::attempt总是失败密码哈希方式变化/驱动配置不对检查3.X的Hash::make和salt配置逻辑迁移执行后没生成表migrate:install未执行先执行php artisan migrate:install建迁移记录表视图不解析变量with方法没传递变量或模板变量名拼错检查控制器中-with(users, $users)的变量名一致性表单提交后CSRF报错3.X表单辅助函数自动生成的隐藏域与后端不匹配确认表单字段和服务端CSRF校验逻辑是否对应这张表基本覆盖了老项目维护者最常撞上的问题。真正动手时保留一份3.2的官方文档副本会帮你解决大部分疑惑那个年代的文档质量已经相当高。我在实际读Laravel 3.X源码和做旧版本迁移时最大的体会是一个框架的经典不是靠堆功能堆出来的而是靠一套清晰稳定的设计主线撑起来的。3.X用很小的身体把路由、ORM、CLI和迁移这四根柱子立住了后面的Laravel无论版本怎么跳都没有偏离这根主线。如果你对这种演进过程感兴趣可以下载一份3.2源码把user/(:num)的老路由和现在user/{id}的新路由并排读一遍你会直观感受到框架十年间“变了多少又没变多少”。这种时间感比单纯背API实在得多。