2012年那会儿我做PHP用的还是CodeIgniter。谈不上痛不欲生但每次往模板里拼字符串、在控制器里手写SQL的时候总会陷入自我怀疑。直到某天在GitHub上刷到Laravel 3那句口号——为Web艺术家创造PHP框架——我一边觉得中二一边忍不住clone下来研究。后来我花了三个周末把一个小博客从CI迁到Laravel 3上再后来我的工具箱里就永远给Laravel留了一个位置。这篇文章想认真回顾Laravel 3.X不是考古也不是劝你回到PHP 5.3时代。我要拆的是那些当年定义了PHP框架审美的设计路由、Eloquent、Blade、IoC容器、过滤器。顺便聊聊它们和今天最新版Laravel的血缘关系。无论你是老开发想怀个旧还是刚接触Laravel、想弄明白它为什么长成这样这篇都可以当一次对照实验来读。1. 2012年的PHP生态Laravel 3到底改变了什么1.1 当时的主流入门框架和它们的痛点2012年前后的PHP领域是分裂的。新手圈子里最流行的是CodeIgniter下载一个压缩包解压改一下config/database.php马上能跑。但它的写法太过程式模型层基本靠$this-db-get_where(posts, array(status published))这类调用控制器里可以堂而皇之地写HTML模板系统简陋想做个公共布局得靠partials反复拼接。CakePHP走在“约定优于配置”的路子上功能足够全但生成大量CRUD脚手架新手往往被它的命名约定和ORM魔法绕晕。企业端则是Zend Framework和Symfony 2的天下面向对象设计很专业学习曲线也足够陡一个基础应用要写不少样板代码。还有一个经常被忽略的背景当时PHP 5.3刚成为主流很多虚拟主机还停留在5.2闭包和命名空间对相当一部分开发者来说是新鲜事物。Laravel 3选择直接要求PHP 5.3这点在那个年代算是明确表态了。换句话说2012年的PHP缺少一个中间地带既能保持CodeIgniter那样低门槛的入门体验又能提供接近Symfony 2那样规范现代的面向对象设计。Laravel 3正好填了这个坑。1.2 “给Web艺术家”不是口号而是一种编码审美Laravel 3的诞生与Taylor Otwell此前的CodeIgniter使用经历直接相关。他在做外包项目时积累了一堆挫败感隐约觉得PHP框架可以做得更优雅于是开始动手写一个既能保持CI简单、又能吸收Rails与Symfony思想的新框架。Laravel 3里到处都是这种“刻意设计”的痕迹路由可以直接收闭包数据库查询可以用方法链一口气写完模型继承Eloquent就自动获得一堆数据操作能力模板引擎让你写foreach而不是?php foreach ?。这种设计在当时的PHP社区里被一些人批评为“花架子”。但真上手之后才发现好的语法糖并不降低性能它降低的是脑力负担。写Laravel 3代码的感觉是我想要做什么代码读起来就是什么。不像以前我脑子里想着“查一下最新文章”手上却要翻译成$this-db-order_by(created_at, desc)-get(posts)再在视图里来回拼接HTML。1.3 为什么3.X才是真正的第一代这里有必要澄清一个历史细节Laravel 1和2从来就不是公开提供给普通开发者的发行版它们是Taylor在框架原型阶段迭代过程中的内部实验版。所以严格来说PHP社区第一次见到的Laravel就是3.X。从2012年2月公开发布到最后一个3.2.14版本Laravel 3.X差不多主导了那一整年的PHP社区讨论。也正是因为这个“第一代”相当成功Laravel 4才能在2013年5月大刀阔斧地重写底层引入Composer作为一等公民全面整合Symfony组件把应用目录从application/迁移到app/。没有Laravel 3积累的口碑和用户反馈后来那个庞大的Laravel生态是无从谈起的。2. Laravel 3.X经典特性逐个拆解当年这些设计有多超前2.1 路由系统闭包时代的第一个惊喜Laravel 3的路由是很多人入坑的第一站因为它在当时实在太反传统了Route::get(/, function() { return Hello, Laravel 3!; }); Route::get(user/(:num), function($id) { return User {$id}; }); Route::get(user/(:any), function($name) { return Name: {$name}; });没有控制器没有模板闭包直接返回字符串。(:num)表明这里只接受数字(:any)接受任意参数。这种占位符风格比后来采用的{id}还要简单直接对从CI转过来的人来说几乎是零门槛。RESTful控制器同样很有味道。先在路由里注册Route::controller(posts)。控制器写法有几种组织方式默认是action_前缀class Posts_Controller extends Base_Controller { public function action_index() { return View::make(posts.index); } public function action_show($id) { $post Post::find($id); return View::make(posts.show)-with(post, $post); } }如果打开$restful true方法名就变成get_index、post_store这样的HTTP动词前缀等于把路由表的一部分逻辑挪进控制器方法命名里。这种做法在2012年的PHP框架里非常新鲜也直接影响了后来的资源路由设计。现在看Route::resource(posts)生成一整套CRUD路由本质还是想解决同一件事让路由表短一点让常用操作有默认规则可循。2.2 Eloquent ORM让PHP开发者觉得操作数据库是享受在Laravel 3里定义一个模型只继承一个类就够了class Post extends Eloquent { public static $timestamps true; public function author() { return $this-belongs_to(User); } }查询的时候可以采用“动态方法”写法$posts Post::where_status(published)-order_by(created_at, desc)-get(); $post Post::find(1); $user $post-author;where_status(published)底层会被解析成WHERE status published。这种写法把SQL的筛选动作变成了英文句子业务代码的可读性比甩一长串数组条件高了几个档次。和CodeIgniter的$this-db-get_where(posts, array(status published))相比Eloquent至少让我知道“我在描述关系而不是在拼查询参数”。关系定义也简洁has_many、belongs_to、has_one。我当时的极简博客里文章和用户的关系写出来就是上面的样子控制器里直接读$post-author-name一点都不需要手动join。对于当年习惯了手写SQL的PHP开发者这种体验确实是“享受”级别的。2.3 Blade模板把“拼字符串”从PHP模板里解放出来Blade之前我见过太多模板长这样div当前用户?php echo $user-name; ?/div ?php if ($posts): ? ?php foreach ($posts as $post): ? h2?php echo $post-title; ?/h2 ?php endforeach; ? ?php endif; ?Blade直接把这一堆标记换成指令div当前用户{{ $user-name }}/div if ($posts-count()) foreach ($posts as $post) h2{{ $post-title }}/h2 endforeach endif配合布局视图layout(layouts.main) section(content) foreach ($posts as $post) h2a hrefposts/{{ $post-slug }}{{ $post-title }}/a/h2 endforeach endsectionlayout用来指定父模板section定义区块endsection结束区块。编译器最终会把它们翻译回PHP文件但你在源码里再也看不到那些糟糕的?php foreach ?嵌套了。当年很多人在“Smarty还是PHP模板”之间纠结Blade后来居上拿下一片天靠的正是“把常用语法变短、把布局逻辑变得像写文档一样自然”。这个设计思想被今天保留下来只是指令更多转义规则更严谨。2.4 Fluent查询构建器链式调用的启蒙如果你不想用ORMLaravel 3也有足够好用的查询构建器$users DB::table(users)-where(age, , 18)-order_by(name)-get(); $count DB::table(users)-count();方法名用order_by这种蛇形命名与Eloquent的动态方法一脉相承。Fluent类接收链式方法翻译成SQL片段。2012年把“链式方法查询逻辑”做成默认体验的PHP框架并不算多而这套API后来几乎原封不动地延续到了今天只是多了when、orWhere等更复杂的组合方法。可以说我后来写的很多结构化查询代码思考习惯都源自Laravel 3的这个小小设计。2.5 IoC容器当年最容易被忽略的底层设计很多第一次接触Laravel 3的人会忽略IoC这个静态类但我一直觉得它才是框架里最值得研究的部分。它提供了手动注册和解析服务的能力IoC::register(mailer, function() { return new Mailer; }); $mailer IoC::resolve(mailer);这样做最大的价值是依赖关系被集中管理替换组件时不用改业务代码。比如测试邮件功能时我可以重新注册一个假的Mailer让它在测试环境只把邮件内容写进数据库而不是真正发出去。这在当时是很先进的做法。这个IoC容器后来在Laravel 4里升级为Illuminate\Container并完全融入框架的请求生命周期。可以毫不夸张地说没有Laravel 3埋下的IoC种子就没有今天Laravel的依赖注入体系。很多开发者今天用App::make()或构造函数注入觉得理所当然却不知道这个能力的初代形态就是上面这几行代码。2.6 过滤器路由级中间件的原型“在路由执行前后插入逻辑”这个概念Laravel 3叫过滤器Route::filter(auth, function() { if (Auth::guest()) { return Redirect::to(login); } }); Route::get(dashboard, array(before auth, function() { return View::make(dashboard); }));before在路由执行前运行after在之后运行类似于今天中间件的before/after语义。把登录校验、CSRF保护、日志记录这类横切逻辑从控制器里抽出来放在路由层统一处理这种套路就是现代Laravel中间件的直接祖先。每次看到Route::middleware([auth])我都会想起当年before auth的样子。设计还是那个设计只是名字换了颗粒度更细了。3. 回到2012年用Laravel 3.X写一个极简博客3.1 安装流程没有Composer create-project的时代Laravel 3的安装和今天完全不是一回事。没有composer create-project没有Homestead更没有Valet。通常的做法是从GitHub下载压缩包或者git clone整个仓库然后放进本地Web环境XAMPP/WAMP/MAMP的根目录。目录结构大致是application/ config/ controllers/ models/ views/ routes.php start.php laravel/ ... public/ index.phppublic/是Web入口application/是业务代码laravel/是框架内核。服务器根目录指向public同时要设法让application和laravel不被直接URL访问。这种分层我在之前用CI时已经熟悉但Laravel 3稍微更“干净”一些入口层、业务层、框架层分得清清楚楚。配置方式也保留了PHP数组的亲切感return array( driver mysql, host localhost, database blog, username root, password , );没有环境变量没有.env所谓“环境配置”基本靠手动改这份PHP文件。今天看起来原始当年算是标配。3.2 极简博客的路由、模型、控制器、视图下面这份代码基本复刻了我当年的极简博客。先是application/routes.phpRoute::get(/, function() { return Redirect::to(posts); }); Route::controller(posts);然后是模型application/models/Post.phpclass Post extends Eloquent { public static $timestamps true; public function author() { return $this-belongs_to(User); } }接着是控制器application/controllers/posts_controller.php。我在这里打开了$restful方法名直接对应HTTP动词class Posts_Controller extends Base_Controller { public $restful true; public function get_index() { $posts Post::order_by(created_at, desc)-get(); return View::make(posts.index)-with(posts, $posts); } public function get_show($slug) { $post Post::where_slug($slug)-first(); return View::make(posts.show)-with(post, $post); } }最后是视图application/views/posts/index.blade.phplayout(layouts.main) section(content) foreach ($posts as $post) a hrefposts/{{ $post-slug }}{{ $post-title }}/a endforeach endsection整个流程从URL到数据库再到页面输出没有一处手写SQL没有一处?php echo也没有拼接HTML字符串。我第一次跑通时最大的感受是原来PHP项目写起来可以这么顺。这在大版本升级后的Laravel里可能不算什么但在2012年这一套组合相当能打。3.3 Artisan雏形期的命令行利器Laravel 3已经有Artisan了只是还比较朴素。那时它主要处理三类事管理bundle、跑数据库迁移、跑测试。php artisan bundle:install php artisan migrate php artisan test迁移在团队协作中非常关键。在Laravel 3里迁移文件的写法已经接近现代Laravelclass Create_Posts_Table { public function up() { Schema::create(posts, function($table) { $table-increments(id); $table-string(title); $table-text(body); $table-timestamps(); }); } public function down() { Schema::drop(posts); } }团队新增字段、改表结构不用再互相传一份SQL文件跑一遍php artisan migrate即可。现在看起来基础当时却是很多小团队认识“数据库版本控制”的启蒙。3.4 当时绕不开的坑第一类名冲突。Laravel 3时代的自动加载还在成型期控制器、模型多数以全局类存在名字起得稍微通用一点就可能和别人冲突。我吃过一次亏Post模型和一个手写的库撞了名字排查了很久才反应过来是加载顺序问题。第二手动管理第三方库。Composer当时还没有成为PHP的默认包管理器至少Laravel 3还没有把它整合进核心分发。我在项目里为了引入一个图片处理库得手动下载源码、手动配置自动加载有时候还得改框架的加载顺序。今天用惯了composer require再回头看那种操作确实有一种恍如隔世的感觉。第三部署更新麻烦。没有版本锁文件没有composer install生产环境更新依赖只能靠“我记得里面装了什么”手工同步。如果升级框架大版本覆盖laravel/目录的撞脸现场我现在都还记得。第四调试工具薄弱。Laravel 3的错误页和日志功能比CI强但远不如后来的异常上下文页面。遇到问题基本靠var_dump、写日志和读源码三件套。我养成了在laravel/目录里翻源码的习惯回头想想那反而是最早理解框架内部机制的契机。这些坑放在今天全都可以称之为槽点但2012年的开发体验里Laravel 3已经属于PHP世界里数一数二的舒服。也正因为如此Laravel 4的重写才显得顺理成章——大家都已经看到了一个更好的PHP样貌接下来的问题只是怎么让基础设施更现代。4. 古今对照从Laravel 3到最新版哪些设计被继承哪些被彻底重写4.1 目录和应用组织的变迁维度Laravel 3.X最新版Laravel应用代码位置application/app/入口public/index.phppublic/index.php扩展方式bundles/ 目录手动管理Composer包 Service Provider配置方式application/config/*.phpconfig/*.php测试方式Artisan test命令PHPUnit框架本体项目内laravel/目录vendor/laravel/framework从application/到app/只是目录名的变化真正的大变化是框架本体不再以源码目录形式躺在你项目里而是作为Composer依赖被锁版本第三方包不再用bundle复制文件夹而是通过Service Provider注册。应用层与框架层的界线一下子清晰了升级框架也从“覆盖目录”变成了composer update。4.2 过滤器变成中间件背后的设计演进Laravel 3的过滤器是“数组里塞字符串”现代Laravel的中间件是对象化管道。表面上只是说法的变化本质上是可组合性的升级。Laravel 3Route::get(dashboard, array(before auth, function() { return View::make(dashboard); }));现在Route::get(/dashboard, function() { return view(dashboard); })-middleware(auth);中间件支持多个组合、支持带参数、支持分组还能绑定到控制器类。那种“路由前后插入逻辑”的原始直觉在多年生产压力下被不断加固最终长成今天这套成熟机制。如果你今天看到中间件列表觉得绕回想一下过滤器只有一个字符串的时代就能理解这种复杂度其实是有意换来的可控性。4.3 Bundles没落与Composer生态的胜利Bundles的构想是像插件一样安装扩展用户执行php artisan bundle:install系统从指定仓库拉取并放进bundles/目录。思路不坏但没有解决两个关键问题包与包之间的依赖关系以及版本兼容。于是当Composer带着PSR-4自动加载和语义化版本号冲进PHP世界时很快就成了事实标准。Laravel团队做的决定非常干脆Laravel 4宁可重写底层也要把整个框架和扩展生态建在Composer之上。从结果看这个押注不仅救了Laravel自己也重塑了PHP生态。今天你随便打开一个PHP包composer.json都是标配。当年那种“去官网下载zip塞进目录”的时代就这样彻底落幕了。4.4 Eloquent和BladeAPI变了审美一脉相承Eloquent从Laravel 3的has_many变成今天的hasManyBlade从十几个指令扩展成几十个但底层审美没变关系式、链式、声明式。这解释了为什么老Laravel开发者在大版本升级时适应得很快——表面API换了背后的设计语言没有换。理解这一点很重要当你在今天看到whereHas这种新的关系查询方法时它仍然是在用Eloquent最初那套“把SQL动词翻译成英文句子”的思路为你服务。4.5 IoC容器与门面模式依赖注入不再教条Laravel 3的IoC::register手动注册更直观但也更繁琐。Laravel 4之后的容器支持构造器自动解析Facade则让你可以像调用静态类一样使用容器中的服务。这三段式演进很有意思Laravel 3手动注册、手动解析一切透明但要写很多代码。Laravel 4/5容器自动解析构造函数注入成为主流。今天的各路用法Facade、构造函数注入、app()辅助函数各自服务于不同场景。门面模式一直有争议但如果你从Laravel 3的IoC来理解会发现它本质上是服务定位器的一层语法糖。它的目标不是替代依赖注入而是让代码在保持简洁的同时依然能获得容器管理的便利。这是整个Laravel家族里最体现“写代码要优雅”这个价值观的设计之一。5. 三个过时的启示不到今天依然成立5.1 语法糖是生产力不是花架子Laravel 3被批评得最多的一点是“语法糖太多不够严肃”。十几年过去事实给出了答案语法糖让代码易读易读就是生产力。当你维护五年前的老项目时Blade指令和Eloquent链式查询带来的可读性会直接转化为定位问题的速度。好的语法糖不是掩盖复杂度而是把复杂度封装起来把业务逻辑放到台面上。这个道理到今天依然值得每一份框架选型加以考虑。5.2 框架是迭代出来的不存在一次到位的设计回看Laravel 3它并不“生来完美”Bundle体系后来被抛弃路由占位符被换成{id}IoC手动注册被更高级的自动解析取代。这些变化都说明框架设计永远是在反馈与迭代中前进的。对开发者来说拥抱升级不是背叛旧版本而是承认旧设计在解决新问题时会出现力不从心的角落。理解了这一点你就不会在自己的架构里神化任何版本或任何组件。5.3 生态和标准化的力量远大于单独一个框架Laravel 3让很多人第一次意识到PHP可以很现代但真正把PHP推向现代的是Composer、PSR标准、PHP 7之后的性能和类型系统这些全行业基础设施。Laravel只是在这套生态上成长得最显眼的那棵树。今天学习Laravel的人学的其实是一整套建立在PHP社区协作之上的产物。这也是我回看2012年时最大的体会框架再强也强不过社区标准化的力量。到这里主线就梳理完了。我知道很多人会觉得Laravel 3太老了没有学习价值。但我个人的经验恰恰相反当你遇到新版框架里某个难以理解的抽象时回到初代实现看看作者最初是怎么权衡的往往比读十遍文档更有用。我现在偶尔还会翻一翻Laravel 3.2.14的源码尤其是在研究容器和路由的时候“原来如此”的感觉依然很强烈。想动手研究的朋友建议直接下源码重点看/laravel/目录里的ioc.php、routing组件和database相关实现再和最新版的Illuminate\Container、Illuminate\Routing做对比。你很快会发现所谓框架进化其实就是一代又一代开发者在同一批底层问题上不断给出更优解的过程。