1. 为什么今天还要学 CodeIgniter?一个被低估的 PHP 轻量级实战派
很多人看到“CodeIgniter”四个字,第一反应是:“这不早就过时了吗?Laravel、Symfony 不香吗?”——我去年在给一家做本地政务系统集成的客户做技术选型时,也听到过一模一样的质疑。但当我打开他们正在维护的、运行了8年的旧系统后台代码,发现里面混着手写 PDO、裸 SQL 拼接、全局变量污染、模板里塞满逻辑判断……那一刻我意识到:不是框架过时了,而是我们把“简单”和“简陋”彻底搞混了。
CodeIgniter 的核心价值,从来不是堆砌功能,而是用极小的认知成本,把 MVC 的骨架立得笔直。它不强制你写 Service 层、不抽象出 17 个接口、不让你为一个路由配置写三行 YAML——它只做一件事: 让一个刚学完 PHP 基础语法的人,在 20 分钟内跑通一个带数据库读写的完整请求闭环,并且每一行代码你都能看懂、能改、能 debug 。这不是教学玩具,而是真实生产环境里的“防崩底线”:当业务要快速上线一个内部审批小工具、当外包团队需要接手一个遗留系统做局部迭代、当运维同事临时要加个数据导出接口——CodeIgniter 就是那个你翻出文档、复制粘贴、改两行就能直接部署的“确定性答案”。
它不解决高并发、不封装微服务、不内置队列——但它把 PHP 最本质的 Web 请求处理流程(接收 → 处理 → 响应)拆解得像教科书一样清晰。你不会被 Laravel 的 Container 绑定绕晕,也不会被 Symfony 的 HttpKernel 生命周期搞懵;你看到的就是
$this->load->model('user_model')
、
$data['users'] = $this->user_model->get_all()
、
$this->load->view('user/list', $data)
这三步铁律。这种“所见即所得”的控制感,在调试一个线上报错的
Undefined index: name
时,比任何花哨的自动注入都管用。
更关键的是,它的轻量是真实的:整个框架核心文件加起来不到 2MB,没有 Composer 自动加载的隐式依赖链,没有 vendor 目录爆炸式增长的风险。我在某次应急响应中,曾用它在一台只有 512MB 内存、PHP 7.2 环境的老旧 VPS 上,30 分钟内搭起一个实时日志查看器——而同期尝试部署 Laravel 的同事,卡在
composer install
的内存溢出上整整两小时。这不是怀旧,这是对“最小可行交付”的尊重。当你需要的只是一个能稳定跑三年、没人维护也不会突然崩溃的后台管理页,CodeIgniter 就是那个沉默但可靠的螺丝钉。
2. 从零开始:5 分钟搭建你的第一个 CI 应用(不装 Composer,不配虚拟主机)
别急着去官网下载 ZIP 包、别急着开终端敲
composer create-project
——那不是 CodeIgniter 的起点,那是现代 PHP 工具链的起点。真正的 CI 入门,应该从你最熟悉的地方开始:一个能放 PHP 文件的目录,一个能解析
.php
后缀的 Web 服务器,以及一份干净的框架源码。下面这个流程,我保证你在 Windows 本地、Mac 的 MAMP、甚至 Linux 的 Apache 默认 DocumentRoot 下,都能 1:1 复现。
2.1 下载与解压:选择“最原始”的方式
去 CodeIgniter 官网(注意是
codeigniter.com
,不是 GitHub 仓库)下载最新稳定版 ZIP 包(截至 2024 年,推荐使用 CI 4.4.x)。解压后你会看到一个
codeigniter4/
目录。现在,请做一件反直觉的事:
不要把它整个扔进你的 Web 根目录
。相反,把
public/
子目录里的全部内容(
index.php
,
.htaccess
,
robots.txt
)直接复制到你的 Web 服务器根目录(比如
C:\xampp\htdocs\myapp\
)。然后,把
app/
,
writable/
,
vendor/
这三个目录,放到
myapp/
的同级目录下(即
C:\xampp\htdocs\myapp/
和
C:\xampp\htdocs\app/
是平级的)。这个结构叫“Web 根分离”,是 CI 4 的安全基石——它确保
app/
目录下的控制器、模型、配置文件,永远无法被用户通过 URL 直接访问,连
index.php
都被挪到了公开可访问的
public/
里。
提示:如果你用的是 Nginx,
.htaccess会失效。你需要手动在 Nginx 配置里添加重写规则,核心就一条:try_files $uri $uri/ /index.php?$query_string;。别抄网上那些复杂版本,CI 的路由就是靠这一行兜底。
2.2 配置第一步:修改
index.php
中的路径常量
打开你刚复制到
myapp/
目录下的
index.php
。找到第 29 行左右的
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR);
。这行定义了“Front Controller Path”,也就是
index.php
所在的位置。紧接着,你会看到:
// The path to the "app" directory
if (!defined('APPPATH')) {
define('APPPATH', __DIR__ . '/../app/');
}
这里的关键是
__DIR__ . '/../app/'
。因为
index.php
在
myapp/
,所以
../app/
就指向了同级的
app/
目录。如果你把
app/
放在了别的地方(比如
/var/www/myapp/app/
),这里就必须改成绝对路径:
define('APPPATH', '/var/www/myapp/app/');
。我见过太多人卡在这一步,错误地认为 CI 一定要按 ZIP 包里的目录结构来放,结果
App
类根本加载不出来,报错
Class 'App\Controllers\Home' not found
——根源就是这个路径没对上。
2.3 创建第一个控制器:告别“Hello World”,直接操作数据库
在
app/Controllers/
目录下,新建一个文件
User.php
。内容如下:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
class User extends BaseController
{
public function index()
{
// 1. 加载数据库服务(CI 4 默认已配置好 MySQL)
$db = \Config\Database::connect();
// 2. 执行查询(注意:这里用原生查询,不走 Query Builder,先建立最直接的连接感)
$result = $db->query("SELECT id, name, email FROM users LIMIT 5");
$users = $result->getResultArray(); // 获取关联数组
// 3. 加载视图并传入数据
return view('user/list', ['users' => $users]);
}
}
这个控制器做了三件事:连接数据库、查数据、传给视图。它没用 Model,没用 Helper,就是最朴素的 PHP 数据库操作。为什么?因为初学者的第一个困惑永远是:“我的数据怎么从数据库跑到网页上?” 如果一上来就引入
UserModel
,再引入
UserTable
,再讲 Repository 模式,信息量就炸了。先让他看到
$users
变量是怎么从 SQL 结果变成网页上
<li>
列表的,认知链条才完整。
2.4 创建第一个视图:用最基础的 HTML + PHP 混合
在
app/Views/
目录下,新建
user/list.php
:
<!DOCTYPE html>
<html>
<head>
<title>User List</title>
</head>
<body>
<h1>用户列表</h1>
<?php if (empty($users)): ?>
<p>暂无用户数据。</p>
<?php else: ?>
<ul>
<?php foreach ($users as $user): ?>
<li><?= esc($user['name']) ?> (<?= esc($user['email']) ?>)</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</body>
</html>
注意
esc()
函数——这是 CI 内置的 XSS 过滤函数,它会自动转义输出内容,防止
<script>
注入。很多新手会忽略这点,直接
<?= $user['name'] ?>
,结果上线后被恶意用户提交
<script>alert(1)</script>
,整个后台页面就弹窗了。
esc()
是 CI 给你的第一道安全护栏,它不显眼,但至关重要。
现在,启动你的 Web 服务器,访问
http://localhost/myapp/public/index.php/user
。如果看到一个带标题的用户列表,恭喜,你的第一个 CI 应用已经活了。整个过程,你没碰 Composer,没配
.env
,没写一行路由配置——因为 CI 4 的默认路由规则就是
index.php/{controller}/{method}
。这就是“Getting Started”的本意:
用最少的前置动作,获得最大的正向反馈
。
3. MVC 的骨架到底长什么样?拆解 CI 中每个字母的真实含义
MVC 不是玄学,不是必须画三张 UML 图才能讲明白的概念。在 CodeIgniter 里,它就是三个物理存在的文件夹,以及它们之间明确的调用关系。很多教程把 MVC 讲成了哲学思辨,结果新手看完更迷糊。我们用刚才那个
User
例子,把它钉死在硬盘上。
3.1 “M” —— Model:不是 ORM,而是“数据操作的专属办公室”
在
app/Models/
下创建
UserModel.php
:
<?php
namespace App\Models;
use CodeIgniter\Model;
class UserModel extends Model
{
protected $table = 'users'; // 指定操作的数据库表名
protected $allowedFields = ['name', 'email', 'created_at']; // 白名单字段,防止 mass assignment
}
这个类继承自 CI 的
Model
基类,但它本身
不包含任何业务逻辑
。它的唯一职责,就是把“对
users
表的增删改查”这件事,封装成一个干净的、可复用的对象。你看不到
getUserById($id)
这样的方法,因为基类已经提供了
find($id)
、
findAll()
、
insert($data)
等通用方法。你也不需要写 SQL,Query Builder 会帮你生成。
那么,业务逻辑放哪?比如“注册新用户时,要检查邮箱是否已存在,并发送欢迎邮件”。答案是:
放在控制器里,或者单独建一个
Services
目录
。CI 的哲学是:Model 只管“怎么存”,不管“为什么存”。这和 Laravel 的 Eloquent 把验证、事件、关联都塞进 Model 是截然不同的设计。CI 的 Model 更像一个“数据库代理”,它轻、快、透明。当你在控制器里调用
$this->userModel->insert($data)
,你知道自己调用的是一段纯粹的数据写入代码,没有隐藏的钩子、没有自动触发的事件——这对排查问题极其友好。
注意:
$allowedFields是安全红线。如果你忘了设它,又用了$model->insert($_POST),攻击者就能 POST 一个is_admin=1字段,直接提权。CI 强制你声明白名单,就是逼你思考“哪些字段是用户可以改的”。
3.2 “V” —— View:不是模板引擎,而是“纯展示的 HTML 工厂”
app/Views/
目录下的所有
.php
文件,都是 View。它和普通 PHP 文件唯一的区别,是它
只能接收控制器传来的
$data
数组,不能主动去数据库查数据,也不能调用 Model
。这是一个硬性隔离。你在
list.php
里写
<?php $db = \Config\Database::connect(); ?>
是完全合法的,但这是反模式——它破坏了职责分离,让视图有了不该有的数据获取能力。
View 的核心价值,在于“渲染一致性”。CI 提供了
view()
辅助函数,它背后是一个简单的文件包含机制:
// view() 函数的本质,就是执行了类似这样的代码:
include APPPATH . 'Views/user/list.php';
它把
$data
数组
extract()
成变量,所以你在视图里能直接用
$users
。这种简单粗暴,恰恰是优势:没有编译缓存、没有模板语法学习成本、没有 AST 解析开销。你改完 HTML,刷新页面就生效。对于一个需要频繁调整 UI 的后台管理系统,这种“所见即所得”的开发体验,远胜于折腾 Twig 的
{% for user in users %}
语法。
3.3 “C” —— Controller:不是“万能胶水”,而是“请求的交通警察”
app/Controllers/User.php
是整个请求流的入口和总控。它的核心任务,是
协调 Model 和 View,决定“谁该做什么”
。它不处理具体的数据格式化(那是 Helper 或 Library 的事),不负责生成 HTML(那是 View 的事),也不直接操作数据库(那是 Model 的事)。它就像一个交警,看到一辆车(HTTP 请求)过来,就指挥它:“去左边 Model 那里取数据”,“拿到数据后,去右边 View 那里组装页面”,“最后把组装好的页面交给我,我发回给浏览器”。
所以,一个典型的 CI 控制器方法,结构非常固定:
public function create()
{
// 1. 接收输入(来自 $_POST, $_GET, 或路由参数)
$data = $this->request->getPost(['name', 'email']);
// 2. 调用 Model 处理业务(这里是保存)
$result = $this->userModel->insert($data);
// 3. 根据结果,决定下一步(跳转 or 渲染)
if ($result) {
return redirect()->to('/user')->with('success', '用户创建成功!');
} else {
return view('user/create', ['errors' => $this->userModel->errors()]);
}
}
这个结构清晰地划分了边界:输入校验在 Controller(用
$this->request->getPost()
),数据持久化在 Model,错误处理在 Controller(
$this->userModel->errors()
返回的是 Model 层的验证失败信息),最终的跳转或渲染决策也在 Controller。没有模糊地带,没有责任推诿。当你遇到一个
500 Internal Server Error
,你立刻知道:要么是 Model 的 SQL 写错了,要么是 View 的 PHP 语法错了,要么是 Controller 的某个方法调用崩了——定位范围被压缩到三个文件,而不是整个框架源码。
4. 数据库碎片、权限、性能:那些搜索热词背后的真实战场
翻看那些热搜词——“php mysql 某个表有碎片,一般怎么处理”、“php图片权限”、“php为什么无法抗高并发”——它们不是空洞的问题,而是开发者在真实项目里被锤过的痛点。CodeIgniter 不提供银弹,但它给了你一把趁手的扳手,让你能直面这些问题。
4.1 处理 MySQL 表碎片:从“看不见”到“可监控”
表碎片(Table Fragmentation)是 InnoDB 表在大量 DELETE、UPDATE 后产生的空间浪费。它不会让程序报错,但会让查询变慢、备份变大。CI 本身不提供碎片分析命令,但你可以用它轻松集成原生 SQL:
在
app/Commands/
下创建
AnalyzeTable.php
(CI 4 的命令行工具):
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class AnalyzeTable extends BaseCommand
{
protected $group = 'Database';
protected $name = 'db:analyze';
protected $description = 'Analyze table fragmentation';
public function run(array $params)
{
$db = \Config\Database::connect();
$tables = $db->query("SHOW TABLE STATUS WHERE Engine='InnoDB'")->getResultArray();
CLI::write("InnoDB Tables Fragmentation Report", 'yellow');
CLI::newLine();
foreach ($tables as $table) {
$data_free = $table['Data_free'];
$data_length = $table['Data_length'];
if ($data_free > 0 && $data_length > 0) {
$fragment_ratio = round(($data_free / $data_length) * 100, 2);
if ($fragment_ratio > 10) { // 超过10%碎片率,标红警告
CLI::write(sprintf("%-30s | Free: %6s KB | Ratio: %5s%%",
$table['Name'],
number_format($data_free / 1024, 0),
$fragment_ratio), 'red');
} else {
CLI::write(sprintf("%-30s | Free: %6s KB | Ratio: %5s%%",
$table['Name'],
number_format($data_free / 1024, 0),
$fragment_ratio));
}
}
}
}
}
运行
php spark db:analyze
,你就能看到所有表的碎片率。如果某个表碎片率超过 20%,执行
OPTIMIZE TABLE users;
即可。这个命令不是 CI 的魔法,而是你用 CI 的脚手架,把 MySQL 的原生命令包装成了一个可复用、可记录、可定时的任务。这才是框架该干的事:
降低底层能力的使用门槛,而不是替代底层能力
。
4.2 PHP 图片权限:用 CI 的 File Helper 筑起安全墙
“php图片权限”这个问题,本质是文件上传后的存储与访问控制。很多新手把用户上传的头像直接存到
public/uploads/
,然后用
<img src="/uploads/123.jpg">
显示——这等于把服务器文件系统完全暴露给公网。CI 提供了
Writable
目录(默认是
writable/
),它被设计为
Web 服务器可写、但 Web 服务器不可直接访问
的区域。
正确做法:
-
上传时,用
move_uploaded_file()把文件存到WRITEPATH . 'uploads/'(即writable/uploads/)。 -
显示时,
绝不
用
<img src="/writable/uploads/xxx.jpg">(因为writable/目录默认被.htaccess或 Nginx 规则禁止访问)。 -
创建一个
ImageController,专门负责“按需读取并输出图片”:
class Image extends BaseController
{
public function show($filename)
{
$path = WRITEPATH . 'uploads/' . $filename;
if (!is_file($path)) {
throw new \CodeIgniter\Exceptions\PageNotFoundException();
}
// 设置正确的 Content-Type(根据文件扩展名)
$mime = mime_content_type($path);
$this->response->setContentType($mime);
// 输出文件内容
return $this->response->setBody(file_get_contents($path));
}
}
然后在视图里用
<img src="<?= base_url('image/show/123.jpg') ?>">
。这样,图片的访问路径是受控的 PHP 脚本,你可以在
show()
方法里加入权限检查(比如“只有登录用户才能看自己的头像”)、防盗链(检查
Referer
)、甚至动态缩略图(用 GD 库处理)。
writable/
目录的存在,就是 CI 给你划的一条安全红线:
所有用户生成的内容,必须经过你的代码审核,才能见光
。
4.3 高并发瓶颈在哪?CI 的“轻”恰恰是解药
“php为什么无法抗高并发”是个伪命题。PHP 本身不是瓶颈,瓶颈在于你的代码写法、数据库设计、服务器配置。CI 的轻量架构,反而在高并发场景下有独特优势:
-
无自动加载开销
:Laravel 的 Composer Autoloader 在每次请求时都要扫描
vendor/autoload.php,解析数百个 PSR-4 映射。CI 4 的类加载是“按需加载”,控制器、模型、库只有在new或use时才加载,冷启动更快。 -
无复杂中间件链
:一个 Laravel 请求要经过 Kernel、Middleware、HttpKernel、Router 等至少 5 层对象传递。CI 的请求流是线性的:
index.php→Router→Controller→View,中间没有装饰器、没有拦截器,调用栈深度浅,CPU 时间消耗少。 -
可预测的内存占用
:CI 4 的核心类(如
Response,Request)都是单例,且生命周期明确。你用memory_get_usage()测量一个简单index()请求,内存占用通常在 2-3MB,而同等功能的 Laravel 请求可能在 8-12MB。这意味着在 1GB 内存的服务器上,CI 能同时处理更多并发连接。
当然,CI 不解决数据库连接池、不内置 Redis 缓存、不提供消息队列。但它的“轻”,让你能更早、更清晰地看到真正的瓶颈。当你的 CI 应用在 1000 QPS 下开始变慢,你立刻知道:问题不在框架,而在你的
SELECT * FROM users
查询没加索引,或者
file_get_contents()
读了一个 10MB 的日志文件。这种“问题归因”的确定性,比任何“全自动优化”都珍贵。
5. 从入门到落地:一个基于 CI 的商品管理系统实战切片
光讲理论没用。我们用一个真实的、高频的业务场景——“基于 MVC 架构的商品管理系统”——来演示如何把前面所有知识点串起来,形成一个可交付的模块。这个例子不追求大而全,只聚焦一个核心闭环: 商品列表页(带搜索、分页)、商品详情页、后台新增/编辑表单(含图片上传) 。它足够小,能在一个小时内写完;又足够真,包含了所有 MVC 的典型交互。
5.1 数据库设计:三张表,撑起整个系统
我们只用三张表,拒绝过度设计:
-
products:商品主表(id, name, description, price, stock, image_path, created_at) -
categories:分类表(id, name, slug) -
product_categories:多对多关联表(product_id, category_id)
创建
products
表的 SQL:
CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text,
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
`stock` int(11) NOT NULL DEFAULT '0',
`image_path` varchar(255) DEFAULT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`),
KEY `idx_price` (`price`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意两个索引:
idx_name
加速搜索,
idx_price
加速价格区间筛选。没有外键约束(CI 不强制要求),但我们在应用层保证数据一致性。
5.2 构建商品控制器:把 CRUD 拆成原子操作
在
app/Controllers/Products.php
:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\ProductModel;
use CodeIgniter\HTTP\Files\UploadedFile;
class Products extends BaseController
{
protected $productModel;
public function __construct()
{
$this->productModel = new ProductModel();
}
// 商品列表页(支持搜索、分页)
public function index()
{
$keyword = $this->request->getGet('q');
$page = $this->request->getGet('page', FILTER_SANITIZE_NUMBER_INT) ?? 1;
$perPage = 10;
// 构建查询条件
$builder = $this->productModel->select('id, name, price, stock, image_path')
->orderBy('created_at', 'DESC');
if (!empty($keyword)) {
$builder->like('name', $keyword)->orLike('description', $keyword);
}
// 获取分页数据(CI 4 的 paginate() 会自动计算总数、生成分页链接)
$products = $builder->paginate($perPage, 'default', $page);
// 生成分页链接(CI 4 的 Pager 会自动处理 GET 参数,如保留 ?q=xxx)
$pager = $this->productModel->pager;
return view('products/index', [
'products' => $products,
'pager' => $pager,
'keyword' => $keyword
]);
}
// 商品详情页
public function view($id)
{
$product = $this->productModel->find($id);
if (!$product) {
throw new \CodeIgniter\Exceptions\PageNotFoundException("商品不存在");
}
return view('products/view', ['product' => $product]);
}
// 新增商品表单(GET)
public function create()
{
return view('products/create');
}
// 处理新增(POST)
public function store()
{
// 1. 验证输入
$validation = \Config\Services::validation();
$validation->setRules([
'name' => 'required|min_length[3]',
'price' => 'required|numeric|greater_than[0]',
'stock' => 'required|integer|greater_than_equal_to[0]'
]);
if (!$validation->withRequest($this->request)->run()) {
return view('products/create', ['errors' => $validation->getErrors()]);
}
// 2. 处理图片上传
$imagePath = null;
$imageFile = $this->request->getFile('image');
if ($imageFile && $imageFile->isValid() && !$imageFile->hasMoved()) {
$newName = $imageFile->getRandomName();
$imageFile->move(WRITEPATH . 'uploads/', $newName);
$imagePath = $newName;
}
// 3. 保存到数据库
$data = [
'name' => $this->request->getPost('name'),
'description' => $this->request->getPost('description'),
'price' => $this->request->getPost('price'),
'stock' => $this->request->getPost('stock'),
'image_path' => $imagePath
];
if ($this->productModel->insert($data)) {
return redirect()->to('/products')->with('success', '商品添加成功!');
} else {
return back()->withInput()->with('errors', ['database' => '保存失败,请稍后重试']);
}
}
}
这个控制器展示了 CI 的典型工作流:验证(Validation)、文件上传(File Handling)、数据保存(Model Insert)、重定向(Redirect)。它没有用事务(因为单表插入,不需要),也没有用事件(因为业务简单)。一切以“完成任务”为第一目标。
5.3 视图与用户体验:让后台不再“丑得让人想辞职”
app/Views/products/index.php
的关键片段:
<!-- 搜索框 -->
<form method="get" class="mb-4">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="搜索商品名称或描述..." value="<?= esc($keyword) ?>">
<button class="btn btn-outline-secondary" type="submit">搜索</button>
<?php if (!empty($keyword)): ?>
<a href="<?= base_url('/products') ?>" class="btn btn-outline-secondary">清空</a>
<?php endif; ?>
</div>
</form>
<!-- 商品列表 -->
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-md-4 mb-4">
<div class="card h-100">
<?php if ($product['image_path']): ?>
<img src="<?= base_url('image/show/' . esc($product['image_path'])) ?>"
class="card-img-top" alt="<?= esc($product['name']) ?>"
style="height: 150px; object-fit: cover;">
<?php else: ?>
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
style="height: 150px;">无图</div>
<?php endif; ?>
<div class="card-body d-flex flex-column">
<h5 class="card-title"><?= esc($product['name']) ?></h5>
<p class="card-text flex-grow-1"><?= ellipsize(esc($product['description']), 60) ?></p>
<div class="mt-auto">
<p class="card-text"><strong>¥<?= esc($product['price']) ?></strong></p>
<p class="card-text text-muted">库存:<?= esc($product['stock']) ?></p>
<a href="<?= base_url('products/view/' . esc($product['id'])) ?>"
class="btn btn-sm btn-primary">查看详情</a>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- 分页 -->
<?= $pager->links() ?>
这里用了 CI 的
ellipsize()
Helper 函数(自动截断长文本并加
...
),用了
base_url()
生成绝对 URL,用了
$pager->links()
自动生成 Bootstrap 风格的分页 HTML。这些 Helper 不是炫技,而是
把重复的、易错的 HTML 生成逻辑,封装成一行代码
。你不用再手写
<li class="page-item"><a class="page-link" href="?page=2">2</a></li>
,
$pager->links()
会根据当前页、总页数、URL 参数,自动生成完整的、语义化的分页导航。这种“减少样板代码”的设计,才是框架提升生产力的核心。
6. 我踩过的坑与最后的建议:一个老手的真心话
写了十年 PHP,用过七八个框架,CodeIgniter 是我唯一一个在离职交接时,会特意给新同事手写一页纸《CI 快速上手备忘录》的框架。不是因为它最强,而是因为它最“诚实”。它不承诺你“一键生成百万行代码”,它只承诺你“每一步你都看得见、改得了、debug 得到”。以下是我用 CI 做项目时,反复摔过的几个坑,以及我现在的应对方式。
第一个坑,是关于“自动加载”的幻觉。早期 CI 3 版本,你可以在
autoload.php
里配置
helper
、
library
,让它们全局可用。这很方便,但也很危险。我曾经在一个项目里,把
url_helper
设为自动加载,结果在某个 API 接口里,
redirect()
函数被意外调用,导致 JSON 响应里混进了 HTML 重定向头,前端解析直接报错。教训是:
永远只 autoload 绝对安全、绝对通用的 helper(如
url
,
form
),其他一律按需
helper()
。CI 4 更进一步,取消了全局 autoload,强制你在控制器里
helper(['url'])
,这看似麻烦,实则是把“依赖可见性”做到了极致。
第二个坑,是关于“数据库连接”的误解。新手常以为
$db = \Config\Database::connect()
每次都会新建一个连接。其实不是。CI 4 的
Database
类内部维护了一个连接池,
connect()
第一次调用时创建连接,后续调用返回同一个实例。但如果你在循环里写
$db = \Config\Database::connect(); $db->query(...);
,就会产生大量冗余的连接对象引用。正确姿势是:
在控制器构造函数里
connect()
一次,赋值给
$this->db
,然后在所有方法里复用它
。这能减少 10%-15% 的内存分配开销,对于高频请求的接口,积少成多。
第三个坑,也是最隐蔽的,是关于“时间戳”的陷阱。CI 4 的
created_at
、
updated_at
字段,默认是
datetime
类型,但它的
setTimestamps(true)
功能,会自动填充
Y-m-d H:i:s
格式的字符串。这没问题。但如果你的数据库时区是
+08:00
,而 PHP 的
date_default_timezone_set()
是
UTC
,那么
now()
函数返回的时间,和数据库里存的时间,就会差 8 小时。线上订单时间全乱了。解决方案只有一个:
在
app/Config/App.php
里,把
appTimezone
设置为
'Asia/Shanghai'
,并且在
app/Config/Database.php
的
DSN
里,加上
?timezone=Asia%2FShanghai
。两处必须一致,缺一不可。这个细节,官方文档藏得很深,但它是生产环境的生死线。
最后,一个真诚的建议: 不要试图用 CodeIgniter 去“对标” Laravel 或 Symfony 。它不是你的竞争对手,它是你的“快速原型机”和“遗留系统救火员”。当你需要在 48 小时内上线一个内部数据录入工具,当你需要给一个只有 2G 内存的老服务器部署一个稳定的后台,当你需要让一个刚毕业的实习生,三天内能独立维护一个模块——CodeIgniter 就是那个最稳、最快、最不给你添乱的选择。它的价值,不在于它有多酷炫,而在于它有多可靠。就像一把瑞士军刀,没有激光瞄准器,但剪刀、螺丝刀、开瓶器,样样精准,样样耐用。在技术世界里,有时候,“能用”和“好用”,比“先进”重要得多。

226

被折叠的 条评论
为什么被折叠?



