CodeIgniter实战指南:轻量PHP框架快速上手与MVC落地

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 服务器不可直接访问 的区域。

正确做法:

  1. 上传时,用 move_uploaded_file() 把文件存到 WRITEPATH . 'uploads/' (即 writable/uploads/ )。
  2. 显示时, 绝不 <img src="/writable/uploads/xxx.jpg"> (因为 writable/ 目录默认被 .htaccess 或 Nginx 规则禁止访问)。
  3. 创建一个 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 就是那个最稳、最快、最不给你添乱的选择。它的价值,不在于它有多酷炫,而在于它有多可靠。就像一把瑞士军刀,没有激光瞄准器,但剪刀、螺丝刀、开瓶器,样样精准,样样耐用。在技术世界里,有时候,“能用”和“好用”,比“先进”重要得多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值