
Laravel 3.x这个版本放在今天看已经很老了但它恰恰是理解整个Laravel体系的最佳切入口。2012年前后PHP圈子还在被CodeIgniter的简单直接和Symfony 2的重量级统治着Laravel 3带着Artisan命令行、Eloquent ORM、Blade模板引擎这些后来成为招牌的特性横空出世。对于今天只用过Laravel 9/10/11的开发者来说3.x的很多设计看起来会很原始但你顺着这条线看下去就会发现现在的路由、容器、ORM、模板系统骨子里全是3.x定的调子。这篇文章我会把Laravel 3.x的核心特性一个个拆开讲不只讲它有什么更重要的是讲它为什么这么设计、在今天演化成了什么样子最后给还在维护老项目的人一些实际参考。1. 为什么Laravel 3.x值得拆开看时代背景与整体定位1.1 2012年的PHP生态与Laravel的突破口先回到当时的大环境。PHP 5.3刚普及不久命名空间和匿名函数这些特性开始进入普通开发者的日常但主流框架的思路还停留在帮你把HTTP请求接到数据库上这个层面。CodeIgniter 2.x很流行因为它轻、上手快、文档厚但它的类库组织方式比较传统模型和数据库操作基本靠手写SQL模板层本质就是PHP原生标签包裹HTML。Symfony 2虽然架构先进但概念密度太高路由、配置、Bundle体系对刚入门的PHPer来说像一座迷宫。Laravel 3就是在这样一个空档里出现的。它的核心主张很明确PHP开发不应该这么痛苦。同样是写一个博客或CMSLaravel 3把路由定义到控制器、模型操作数据库、视图渲染页面这条链路用非常顺滑的方式组织起来同时引入了当时PHP圈不太常见的东西——命令行工具Artisan、ORM、模板引擎、依赖注入容器。这几样东西单拎出来任何一个都不算全新概念但把它们集成到一个PHP框架里并且做到开箱即用Laravel 3是头一个。所以今天看3.x不能只盯着它有多少个类、多少个方法而要看到它在开发者体验这件事上开了先河。后面Laravel 4重写底层、Laravel 5定义目录规范本质都是把3.x验证过的理念做得更工程化。1.2 3.x在Laravel演进中的坐标定位如果列一张表对比Laravel 3和现代Laravel你会发现一个很有意思的现象核心名字几乎没变但实现方式换代了。维度Laravel 3.x现代Laravel安装方式下载源码/ZIP配置web根目录Composer create-project包管理Bundle系统Composer Packagist自动加载框架自带PSR-0加载器Composer autoload路由参数Route::get(user/(:num))Route::get(user/{id})控制器命名User_ControllerUserControllerORM方法has_many / belongs_tohasMany / belongsTo模板默认转义默认不转义默认转义依赖注入IoC::register静态调用完整Container Facade数据库迁移php artisan migratephp artisan migrate类似这张表不是让你背的而是想说明Laravel 3.x是老灵魂、新骨架的起点。很多今天让你觉得框架就该这样的东西比如迁移数据库结构、用命令行代替手工建表、用Blade的if/foreach省掉PHP标签全部是在3.x这段时期定型的。理解3.x等于理解Laravel设计哲学的最底层动机。另外运营层面有个很现实的原因国内到现在还有不少项目跑的是Laravel 3甚至2.x的二开系统那些2013、2014年外包出去的整站程序可能还在老服务器上服役。对维护这类系统的朋友来说这篇也是给你们的排查手册。2. Artisan与路由3.x时代的命令行和请求分发核心2.1 Artisan CLI从命令行解放重复劳动Laravel 3是Laravel系列里第一个引入命令行工具的版本。当时在PHP圈里提到命令行你会想到Rake、Symfony Console这类东西很少有人觉得PHPWeb框架需要一个CLI入口。但Artisan一出现立刻改变了很多人的开发习惯。最简单的例子是迁移。3.x之前改表结构通常是登录phpMyAdmin手敲SQL或者维护一份SQL补丁文件再手动导入多人协作时非常容易漏执行。有了Artisan你只要在项目根目录运行php artisan migrate:install php artisan migrate php artisan migrate:rollback第一行创建迁移记录表第二行执行所有未运行的迁移第三行回滚最近一次批量迁移。这三条命令永远不变但每次执行的都是最新的结构变更数据库结构变成了一支可以向前向后走的序列这是一个对团队协作极其友好的改进。Artisan还提供了不少实用命令。比如生成应用密钥php artisan key:generate处理Bundle安装php artisan bundle:install不记得有哪些命令的时候直接运行php artisan help回到当时这套东西的意义在于它第一次让PHP开发流程有了工程化脚本的感觉。以前你写PHP项目是在文件系统里堆文件现在你是项目的操作者有一组工具替你完成重复操作。2.2 路由系统与过滤器请求分发的灵活度Laravel 3的路由写法放在今天看有点复古但逻辑非常清晰。打开application/routes.php你会发现整个应用的URL映射全在文件里Route::get(/, function() { return Hello Laravel 3; }); Route::get(user/(:num), usershow); Route::post(user/create, usercreate);(:num)这种参数占位符就是3.x的风格只匹配数字括号里的值会自动传给控制器方法或闭包。类似地还有(:any)和(:all)。后来Laravel 4改成{id}和{id?}语义更清晰但思路完全一致。3.x还有一个非常关键的概念路由过滤器。它相当于今天中间件的前身。比如你想让后台管理这块必须登录才能访问写法是Route::filter(auth, function() { if (! Auth::check()) return Redirect::to(login); }); Route::get(admin, array(before auth, uses adminindex));before过滤器在请求到达控制器之前执行返回任何Response都会直接短路也就是说adminindex根本没机会跑。后来Laravel的Middleware本质上就是把这套前置/后置逻辑提取成了可复用的管道结构。这里的关键认知是框架的边界不是控制器处理完就完了而是可以统一插入认证、日志、权限、CORS这些横切逻辑而且定义得非常轻量。你在3.x里看到的before/after过滤器配置直接对应今天middleware中间件那层东西。2.3 RESTful控制器URL动词与HTTP方法对应3.x的RESTful控制器也是一个值得提的设计。传统PHP控制器是一个URL对应一个方法比如/user/show?id1或/user/create。RESTful思路则是让控制器本身作为资源的入口让HTTP方法承担动作表达。在Laravel 3里你只要在控制器里声明class User_Controller extends Base_Controller { public $restful true; public function get_index() { return View::make(user.index); } public function post_create() { // 处理POST创建 } }然后URL/user配合GET请求会进get_index/user/create配合POST请求会进post_create。方法名前面的get/post直接对应HTTP动词代码即文档。这个设计后来被整个PHP社区广泛采纳以致于今天你打开任何Laravel项目都能看到Route::resource(posts, PostController::class)生成的七条资源路由——它的鼻祖就是3.x的RESTful控制器。用一句话总结这个章节Artisan给了你项目级工具路由给了你请求分发中枢RESTful控制器给了你清晰的资源组织方式。这三样是3.x遇到的第一次现代框架冲击也是后来所有版本的基石。3. Eloquent与迁移数据库操作的优雅革命从这里开始3.1 查询构造器终于不用手动拼SQL了数据库操作是每个Web框架都绕不开的战场。Laravel 3最让我惊喜的部分就是它的查询构造器Fluent Query Builder链式调用第一次让写数据库条件变成了一种流畅的体验$users DB::table(users) -where(status, , 1) -order_by(created_at, desc) -take(10) -get();对比那时候的CodeIgniter写法虽然也有Active Record类但那种字符串数组方式的$this-db-get_where(users, array(status1))在灵活性和可读性上都差一截。Laravel 3的链式API更接近自然语言的表达方式而且每个方法都返回一个可继续调用的查询对象这种连贯接口的设计在当年的PHP圈里是很时髦的。3.x的命名风格有个特点方法名还是snake_case的比如order_by、take、get。到了Laravel 4才统一改成orderBy这类驼峰命名。所以如果你翻老代码看到一堆下划线别意外那只是时代的印记。另外值得说的是3.x的查询构造器本身也支持聚合函数、连接查询、子查询这些复杂操作。虽然SQL能力不如手写灵活但覆盖了80%的业务场景剩下的你再上DB::query(...)原生SQL兜底。这个构造器优先、原生SQL兜底的策略直到今天依然是Laravel的数据库层主路线。3.2 Eloquent模型把数据库行变成对象Eloquent是Laravel 3另一个里程碑式特性。它的核心思想很简单一张表对应一个类一行记录对应一个对象你操作对象而不是操作数组和SQL结果。定义一个模型极其简单class Post extends Eloquent { public static $table posts; }就这样你已经拥有了增删改查全套能力$post new Post; $post-title 第一篇文章; $post-content 内容; $post-save(); $posts Post::where(created_at, , 2023-01-01)-get(); $post Post::find(1); $post-title 新标题; $post-save(); $post-delete();注意这里没写任何SQLEloquent会根据表名、主键id自动拼接。这种约定优于配置的做法在当时极大降低了新手写CRUD的心理门槛。模型关联也是Eloquent的宣传重点。Laravel 3里关联方法用蛇形命名class User extends Eloquent { public function posts() { return $this-has_many(Post); } } class Post extends Eloquent { public function user() { return $this-belongs_to(User); } }有了关联定义之后取数据变得很自然$user User::find(1); foreach ($user-posts as $post) { echo $post-title; }它的外键默认规则是当前模型名单数化_id比如post_id。只要你按这个约定建字段框架替你处理JOIN和外部查询。这思想后来在Laravel 4改成hasMany/belongsTo继续发扬光大今天你看到-with(posts)预加载、$model-posts延迟加载模式源头就是这里。3.3 迁移与种子数据数据库结构版本化刚才Artisan里已经介绍了迁移命令这里重点看迁移文件的写法。迁移在Laravel 3中就是一个个PHP类放在application/migrations目录下class Create_Posts_Table { public function up() { Schema::create(posts, function($table) { $table-increments(id); $table-string(title, 100); $table-text(content); $table-timestamps(); }); } public function down() { Schema::drop(posts); } }up()定义向上走的结构down()定义如何回滚到之前状态。这跟后来的php artisan make:migration生成的类一模一样连$table-increments(id)这种写法都延续至今。它的意义在于团队里任何一个人执行php artisan migrate都能得到完全一致的数据库结构不再出现我本机加了字段没同步这种问题。种子数据Seeding在3.x时期相对简单。不像现在的php artisan db:seed这么体系化当时更常见的做法是在迁移的up()里直接插入基础数据或者写一个独立的Artisan任务来填充测试数据。不过结构迁移和数据填充分离的思路已经萌芽这也是后来Seeder机制的原型。3.4 缓存与其他数据周边能力数据库附近还少不了一个好用的缓存层。Laravel 3提供了很简洁的缓存接口Cache::put(post_1, $post, 10); // 存10分钟 $post Cache::get(post_1); // 取 Cache::forget(post_1); // 删驱动支持文件、数据库、Memcached等来看你项目规模。当时的PHP开发者对查询结果缓存这件事还没形成习惯Laravel把这个API做得这么简单之后很多人第一次开始对热点数据做缓存性能提升立竿见影。这套Cache::put / Cache::get的API到今天也没大改只是多了一堆更细的方法名而已。4. Blade模板与IoC容器前端渲染与依赖注入的早期形态4.1 Blade设计思路模板不该被PHP标签淹没PHP本身就是一门模板语言为什么还要Blade这个质疑在2012年被问过无数次。我的回答是原生PHP当模板用五分钟就会写出恶心的代码。展开说是三个痛点语法噪音大。一个简单的if判断要写?php if ($user) : ? ... ?php endif; ?标签挤占HTML阅读空间。布局复用困难。头部、尾部、侧边栏到处include参数传递很别扭。模板内混入复杂逻辑。很多页面文件越写越像Controller业务逻辑和HTML纠缠不清。Blade的解法是提供一套更简洁的指令语法比如if($user) h3欢迎回来{{ $user-name }}/h3 else a href/login登录/a endif foreach($posts as $post) h2{{ $post-title }}/h2 endforeachif、foreach这些指令在编译阶段会被翻译成原生PHP控制结构但模板里你看到的却是干净整洁、接近HTML本身的标记。这个模板预编译的思路一直延续到现在只是编译产物缓存更智能、指令更丰富。4.2 布局继承与页面渲染Blade另一个重要特性是布局继承。一个典型的场景全站有统一头部导航和底部版权每个页面只替换中间主体部分。在Laravel 3里这样写布局!-- application/views/layout.php -- !DOCTYPE html html headtitleyield(title)/title/head body header全站导航/header div classcontent yield(content) /div footer版权信息/footer /body /html子页面这样继承extends(layout) section(title, 文章列表) section(content) p页面主体内容/p endsectionextends指定父模板section定义要填充的区块yield声明区块插槽。这套机制比当时的include header.php高到不知道哪里去了。今天的Blade布局系统虽然加了组件、插槽这些更高级的玩法但extends、section、yield三件套依然是标准用法可见3.x设计之超前。需要提醒一下3.x的{{ }}默认就是echo输出转义不是自动的。也就是说如果演示代码里写了{{ $title }}在当时的Laravel 3里它可能不做htmlspecialchars转义这可能带来XSS风险。到了Laravel 4{{ }}才变成默认转义输出。如果你在维护老项目看到{{ }}最好确认一下使用方有没有自己做过滤或者干脆升级到新版本。4.3 IoC容器依赖注入从静态调用开始IoC容器在Laravel 3里有一个朴素但很清晰的实现叫IoC类。用法是这样的IoC::register(mailer, function() { return new Mailer; }); $mailer IoC::resolve(mailer);你注册一个名字和一段生成实例的代码之后需要时就通过名字解析容器负责创建、管理依赖。这就把new一个对象这件事从业务代码里解耦了换实现时只改注册闭包业务代码不用动。当时很多人不理解IoC到底有什么用直到他们开始写单元测试或者维护大型项目才明白。依赖注入容器解决的核心问题不是省几个new而是让类与类之间不硬编码绑定——中间隔了一层容器替换实现变成改一行配置或闭包的事。Laravel 3里的IoC还没有形成今天那种强大的自动解析、接口绑定、单例管理等机制但服务容器的雏形已经在了。Laravel 4重写的核心就是把这个容器做大做全并配合门面模式Facade给了你一套静态方法背后是动态绑定实例的神奇体验。可以说Laravel 3让你知道依赖注入是什么Laravel 4才让你离不开它。5. Bundle模块化与辅助功能3.x时期的外挂式扩展5.1 Bundle系统最早的应用模块化方案在Composer和Packagist还没统治PHP之前框架是如何扩展的Laravel 3的回答是Bundle。一个Bundle就是一堆可复用的代码集合可以包含自己的路由、控制器、模型、视图、迁移、配置文件。比如你想集成一个管理后台或者评论区不用把代码拷进来而是把它作为一个Bundle注册进来。安装一个Bundle的流程是php artisan bundle:install github_username/bundle_name安装完成后在application/bundles.php里注册它return array( bundle_name array(auto true), );auto true表示应用启动时就自动加载这个Bundle的路由和文件。之后它的路由、视图、迁移就都生效了和你自己写的代码一样。这对于多人协作、功能模块复用来说是一次重要尝试。不过Bundle有几个很实际的问题没有统一的依赖解析能力无法声明这个Bundle依赖另一个Bundle没有版本约束很容易装到不兼容的版本安装来源高度依赖GitHub仓库传播也不够标准化。所以后来Laravel 4直接拥抱Composer把包管理交给生态平台Bundle这段历史就被当过渡方案淡出了。但它把功能模块独立打包、按需装配的思想在今天的Composer包和设计模式里继承了下来。5.2 输入、验证、分页与Session的日常工具除了上面的大部件Laravel 3还内置了一批顺手的小工具摸爬滚打这么多年它们依然是日常开发里最常用的东西。输入处理$input Input::all(); $name Input::get(name);验证器$rules array( username required|min:3|max:20, email required|email, ); $validator Validator::make(Input::all(), $rules); if ($validator-fails()) { return Redirect::back()-with(errors, $validator-errors); }分页$posts DB::table(posts)-paginate(10); foreach ($posts-results as $post) { // 输出内容 } echo $posts-links; // 渲染分页链接SessionSession::put(user_id, $user-id); $id Session::get(user_id);这些API的形态几乎一直延续到现代Laravel只是命名从蛇形变成驼峰、返回类型更规范了。从实用角度讲Laravel 3已经把写一个完整的CRUD应用所需要的基础设施配得非常齐全路由、控制器、模型、视图、输入、验证、Session、分页一样不缺。5.3 老版本里的半自动认证机制认证在3.x里也已经内置。Auth类负责登录检查、取当前用户、存储会话标识if (Auth::attempt(array(username Input::get(username), password Input::get(password)))) { return Redirect::to(dashboard); } $currentUser Auth::user();它默认读取users表相关字段但你可以在application/config/auth.php里改表名、字段名。跟今天直接支持多守卫、邮箱验证、密码重置的体系相比3.x这套确实简陋很多情况你得自己扩展或写额外逻辑。不过一个框架自带认证基础还是比完全没有强得多至少新项目起步时不用从零写登录。6. 从零搭建Laravel 3.x留言板核心特性串起来跑一遍6.1 环境准备与安装细节想亲身体验Laravel 3.x的完整工作流建议搭一套PHP 5.6或更老的环境。上手之前先确认几个条件PHP 5.3及以上版本推荐5.6兼容性最好。开启必要的PHP扩展mcrypt、mbstring、pdo_mysql等。Apache开启mod_rewrite或者Nginx配好伪静态规则web根目录指向项目的public目录。MySQL数据库新建一个库比如laravel3。安装很简单不用Composer。直接在GitHub上获取Laravel 3.x的分支代码或者下载压缩包解压到站点目录就行。早期Laravel的项目结构是这样的laravel3/ ├── application/ │ ├── config/ │ ├── controllers/ │ ├── migrations/ │ ├── models/ │ └── views/ ├── laravel/ │ └── (框架核心) ├── bundles/ ├── public/ │ └── index.php └── (其他杂项)application放你的业务代码laravel是框架源码public是唯一对外暴露的入口。这种应用代码和框架源码放一起、web根目录隔离public的结构后来一路影响到了Laravel 5及以后。配置数据库是在application/config/database.php里return array( driver mysql, host localhost, database laravel3, username root, password , charset utf8, prefix , );6.2 定义数据库迁移、模型与路由先用Artisan建好迁移表php artisan migrate:install然后在application/migrations下创建一个迁移文件比如create_posts.phpclass Create_Posts_Table { public function up() { Schema::create(posts, function($table) { $table-increments(id); $table-string(title, 100); $table-text(content); $table-timestamps(); }); } public function down() { Schema::drop(posts); } }执行迁移php artisan migrate表结构就出来了。接着在application/models/Post.php写模型class Post extends Eloquent { public static $table posts; }在application/routes.php里定义页面路由Route::get(/, postindex); Route::get(post/create, postcreate); Route::post(post/store, poststore);6.3 写控制器与Blade视图控制器放在application/controllers/post.phpclass Post_Controller extends Base_Controller { public $restful true; public function get_index() { $posts Post::order_by(created_at, desc)-get(); return View::make(post.index)-with(posts, $posts); } public function get_create() { return View::make(post.create); } public function post_store() { $input Input::all(); $rules array( title required|max:100, content required, ); $validator Validator::make($input, $rules); if ($validator-fails()) { return Redirect::back()-with(errors, $validator-errors); } $post new Post; $post-title Input::get(title); $post-content Input::get(content); $post-save(); return Redirect::to(/); } }注意这里控制器的两个细节类名是Post_Controller3.x下划线风格$restful true让get_/post_前缀方法能对应HTTP动词。这一点和现在的PostController差异很大但对应关系是一目了然的。两个Blade视图。列表页application/views/post/index.blade.phpextends(layout) section(title, 留言板) section(content) h1全部留言/h1 foreach($posts as $post) article h2{{ $post-title }}/h2 p{{ $post-content }}/p small{{ $post-created_at }}/small /article endforeach pa href/post/create发表留言/a/p endsection表单页application/views/post/create.blade.phpextends(layout) section(title, 发表留言) section(content) h1发表留言/h1 if($errors) ul foreach($errors as $error) li{{ $error }}/li endforeach /ul endif form methodpost action/post/store input typetext nametitle placeholder标题 / textarea namecontent placeholder内容/textarea button typesubmit提交/button /form endsection最后加一个公共布局application/views/layout.blade.php用yield(title)和yield(content)输出区块。到这里一个提交留言、展示留言的最小应用就完整跑起来了。整个过程下来你会发现所有核心特性——路由、控制器、模型、迁移、验证、Blade布局——都是围绕着让业务代码尽量少写重复样板这个目标协同工作的。7. 老项目维护与新老对比常见问题与排查思路7.1 典型问题速查表维护Laravel 3项目最怕踩坑下面这些问题我都在实际环境里遇到过先给一张速查表症状常见原因处理方向页面全部404public目录没设为站点根目录重新配置虚拟主机或重写规则报Mcrypt相关错误PHP扩展未启用安装并开启php-mcrypt中文乱码数据库charset没配utf8检查config和表排序规则500错误但日志空白application/logs目录不可写授权写权限开启error_reportingphp artisan无法运行PHP CLI不在PATH或版本不符检查which php和PHP版本Eloquent关联查不到外键字段没按约定命名检查外键是否为post_id这种格式高版本PHP下Deprecated警告PHP 7/8移除了老语法支持尽量在PHP 5.6跑老项目或做兼容适配Bundle下载失败GitHub或网络问题手动下载到bundles目录并注册7.2 排查思路先看入口、再看日志、最后定位代码老项目调试比新项目难受因为很多框架自带的错误处理会把异常吞掉只给你一个空白页。我的实际经验是三步走第一步确认web根目录指向的是public而不是项目根目录。这个问题至少浪费了我一个下午。Apache的DirectoryIndex和重写规则不匹配会导致所有路由都404。Nginx的话要检查try_files配置是否把非文件请求转发到了index.php。第二步把错误显示完全打开。在public/index.php顶部临时加一下error_reporting(E_ALL); ini_set(display_errors, 1);或者直接在application/config/error.php里把ignore array()和detail true这些配置调到最宽容的状态。核心目的很简单让框架把真实的错误信息打印出来而不是拦截成一个空页面。排查完记得关掉不然线上会炸。第三步看application/logs目录下的日志。Laravel 3的日志会按日期记文件但前提是目录可写。如果权限没给够框架写不了日志你看到的就只剩空白页了。7.3 新旧版本对比3.x老代码如何迁移如果你要接手一个Laravel 3项目并准备升级给你几个最直接的转换规则控制器类名User_Controller改成UserController文件路径和命名空间相应调整。RESTful控制器的get_index改成public function index()再由路由显式区分GET/POST。路由参数user/(:num)改成user/{id}。ORM关联方法has_many(Post)改成hasMany(Post::class)。DB::table(users)-order_by(...)改成-orderBy(...)。Blade里的{{ }}建议重新梳理一遍新版默认转义会改变输出内容小心重复转义。不过说句掏心窝的话大版本跨越升级的成本往往比重写还高。Laravel 3到Laravel 4是底层重写4到5又是目录结构大改5以后才是稳定的现代形态。你要是没有强需求比如安全漏洞、新功能老项目维持原状、只做数据备份和性能优化很多时候是更明智的选择。真到了必须动的时候优先把数据库表结构和业务逻辑理清楚再设计过渡方案。8. 一点个人的实操体会这几年我帮人处理过的Laravel 3老项目加起来也有七八个了。最深的体会有两点。第一老项目最怕的不是框架旧而是没有文档、没有备份、没有测试。框架再怎么旧只要你能跑起来、能看到日志问题总能定位。真正让人崩溃的是连数据库都在一台不知道密码的服务器上那就不是框架层面能解决的事了。所以如果你是老项目的新接手人第一个月别急着改代码先把备份、文档、运行环境这三件事做扎实。第二不要带着今天的眼光去笑话3.x的土。像Route::get(user/(:num))这类写法在当年就是最先进的一批就像今天你用Route::resource不会觉得自己落后一样。技术选型是那时那刻的最优解你站在2024年回望桥3.x看的是它怎么帮PHP开发者趟出了一条更好写代码的路。从Artisan到Eloquent再到Blade这些概念二十年后依然活跃在每一行我曾经维护过的Laravel代码里这本身就足够说明问题了。平时有朋友问我想理解Laravel设计思想该从哪看我常常推荐他们先把3.x的源码翻一遍因为那个版本还足够小、足够直白没有容器自动解析和门面魔法的层层包装。读完3.x再去看现代Laravel你会恍然大悟原来这整套生态是从当年那个目录里装着laravel/核心、连Composer都没有的老框架里走出来的。