ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CodeIgniter 4实战:轻量PHP框架的安装、MVC与安全开发指南

CodeIgniter 4实战:轻量PHP框架的安装、MVC与安全开发指南 1. 为什么还在谈CodeIgniter——框架定位与上手前的准备工作聊到PHP框架很多人第一反应是Laravel、Symfony这些主流选手但CodeIgniter在我心里一直有个特殊位置。它体积小、起步快、文档清晰不需要命令行工具也能跑起来对于刚接触框架概念的开发者、或者维护老项目的朋友来说CodeIgniter依然是快速落地业务的高效工具。CodeIgniter下文简称CI的核心特点可以用一句话概括它是一个极端轻量的PHP MVC框架不依赖Composer也能完整运行。对比Laravel动辄几百MB的依赖体积CI 4.x完整解压也就1MB左右在没有外网环境、或者服务器配置较低的生产场景下这种轻量本身就是巨大优势。而且CI的学习曲线非常平缓如果你熟悉原生PHP的写法切换到CI几乎不需要重新学习一套“世界观”它只是帮你把代码梳理进MVC结构中。在开始写代码之前我必须先强调版本问题。CodeIgniter目前有两个大版本在活跃使用老牌的3.x和现代化的4.x。3.x发布于2015年前后语法更简单网上教程也最多4.x是2020年发布的重写版本引入了命名空间、PSR-4自动加载、内置测试支持等现代PHP特性。新项目建议直接用4.x3.x只推荐用于维护存量系统。本文所有内容基于CodeIgniter 4.x展开但核心的MVC思想在两个版本中是通用的。1.1 环境需求与安装方式CI 4.x对PHP版本的要求是7.4以上官方推荐8.1需要开启intl、mbstring这两个扩展其中intl扩展在某些精简版PHP环境中是缺失的安装前先确认一下。安装方式有两种我建议你根据实际场景选方式一Composer安装推荐composer create-project codeigniter4/appstarter myproject cd myproject php spark serve执行完以上命令后在浏览器访问localhost:8080看到CI的默认欢迎页就代表安装成功。php spark serve是CI内置的开发服务器命令相当于Laravel的php artisan serve本地调试非常方便。方式二手动下载直接从官网下载ZIP包解压到Web根目录比如htdocs/myproject访问http://localhost/myproject/public即可。这种方式适合没有Composer环境、或者需要离线部署的场景。这里有一个最容易被新手忽略的点生产环境部署时Web根目录必须指向public文件夹而不是项目根目录。CI 4.x把入口文件放在了public/index.php所有公开资源CSS、JS、图片也都在public目录下这样设计是为了保护系统文件不被直接访问。如果你把根目录指到项目根目录虽然也能跑但会产生严重的安全隐患——app/Config/Database.php里存放着数据库密码外部可以直接通过URL访问到。1.2 base_url配置第一个必须改的设置安装完成后第一件事不是写Hello World而是设置base_url。打开app/Config/App.php找到baseURL属性public $baseURL http://localhost:8080/;如果你是手动部署到子目录比如http://localhost/myproject这里必须写成public $baseURL http://localhost/myproject/;注意末尾的斜杠/不能丢。base_url的作用是告诉CI所有URL生成的基础路径如果这个配置不对base_url()等辅助函数生成的链接全部会指向错误位置表单提交、页面跳转会一片混乱。提示开发阶段如果不想频繁改配置可以在app/Config/App.php里开启自动检测。CI 4.2版本支持将baseURL留空系统会自动根据当前请求的域名和路径推断出正确值。但在生产环境我仍然建议显式配置避免多域名或HTTPS跳转场景下出现意外。2. 目录结构与MVC运转机制——看懂CI怎么处理一次请求很多初学者用框架时的痛苦在于“不知道文件该放哪里”这其实是没有理解框架的约定。CI 4.x的目录结构比3.x清晰得多核心目录如下表所示目录作用app/Controllers控制器文件存放处接收请求并协调业务逻辑app/Models模型文件存放处负责与数据库交互app/Views视图文件存放处输出HTML页面app/Config所有配置文件包括数据库、路由、自动加载等app/Routes路由定义目录4.x中路由文件在app/Config/Routes.phppublicWeb入口目录存放index.php和静态资源writable用于存放日志、缓存、上传文件等可写内容2.1 一次请求的完整流转路径理解CI的工作流程只需要记住一条链子URL → 入口文件 → 路由解析 → 控制器调用 → 模型读取数据 → 视图渲染 → 响应返回浏览器。举个例子当用户访问http://localhost:8080/blog/view/42时CI内部发生的事如下public/index.php接收所有请求通过Web服务器的URL重写规则。路由组件解析URL默认规则是控制器名/方法名/参数所以控制器是Blog方法是view参数是42。CI实例化Blog控制器调用view(42)方法。控制器的view()方法内部调用Blog_model获取ID为42的文章数据。控制器把数据传给视图文件视图渲染出完整的HTML并返回给浏览器。这个流程就是经典的MVC模式。控制器是协调者它本身不写SQL也不输出HTML只负责“调度”模型只管数据的存取视图只管展示。代码有了边界之后多人协作时不会互相干扰后期维护时定位问题也快很多。2.2 路由规则从默认路由到自定义路由默认路由规则虽然方便但真实项目里URL往往需要语义化。CI 4.x的路由定义在app/Config/Routes.php中支持非常灵活的自定义规则。常见用法有三种普通路由映射$routes-get(about, Page::about); $routes-get(blog/(:num), Blog::view/$1);第一个将/about映射到Page控制器的about方法第二个用(:num)占位符匹配数字$1将匹配到的数字传给Blog::view方法的第一个参数。闭包路由适合简单页面$routes-get(ping, function() { return pong; });HTTP动词路由$routes-post(api/user, Api\User::create); $routes-put(api/user/(:num), Api\User::update/$1); $routes-delete(api/user/(:num), Api\User::delete/$1);一个非常实用的技巧是路由分文件管理。项目后期路由规则多了之后全部堆在Routes.php里会非常臃肿。CI支持通过$routes-group()方法将路由按模块分组或者直接把不同模块的路由文件单独建在Routes.php里require进来。关于路由我踩过一个很典型的坑CI 4.x的路由定义有先后顺序前面的规则会优先匹配一旦匹配成功就不会继续往后查找。所以当你同时定义了product/(:num)和product/new时product/new必须写在product/(:num)之前否则new会被当作数字参数解析导致匹配失败。这类问题特别隐蔽页面打开明明URL正确结果却报404排查半天才发现是路由顺序的问题。3. 控制器、视图与传参——写第一个能跑的页面路由配好之后接下来就是实际写代码。控制器是请求处理的起点这一步我建议从零开始写一个完整的小例子跑通了再研究复杂的用法。3.1 控制器的标准写法与命名规范在app/Controllers/下新建Blog.php文件?php namespace App\Controllers; class Blog extends BaseController { public function index() { echo 这里是博客列表页; } public function view($id) { echo 文章ID . $id; } }访问http://localhost:8080/blog会看到“这里是博客列表页”访问http://localhost:8080/blog/view/5会输出“文章ID5”。有几个必须遵守的规范违背任何一个都会报错文件名必须与类名完全一致大小写敏感。Blog.php对应class Blog。文件必须放在app/Controllers目录下。命名空间App\Controllers不能丢这是4.x与3.x最大的语法区别之一。控制器类应该继承BaseController位于app/Controllers/BaseController.php这个基类里预加载了常用服务比如数据库连接、session、request对象等不继承的话很多功能需要自己手动调用。方法名中不建议使用下划线某些配置下会导致路由无法访问。3.2 视图加载与数据传递控制器里输出内容不应该直接用echo而是通过加载视图文件。在app/Views/下新建blog_view.php!DOCTYPE html html head title? $title ?/title /head body h1? $title ?/h1 ul ?php foreach ($articles as $article): ? li? esc($article[title]) ?/li ?php endforeach; ? /ul /body /html控制器中这样加载public function index() { $data[title] 博客列表; $data[articles] [ [title 第一篇], [title 第二篇], ]; return view(blog_view, $data); }注意三个细节view()方法属于全局辅助函数不需要额外引入。视图中不需要写html和body之类的骨架结构都行但实际项目中建议拆分一个header.php、一个footer.php页面中间部分用? $this-include(header) ?拼装避免每个页面重复整套HTML。视图中输出变量使用? $variable ?等同于?php echo $variable; ?简洁且常用。涉及用户输入或数据库中的内容时务必使用esc()函数包裹它能自动转义HTML字符防止XSS攻击? esc($article[title]) ?很多初学者省掉esc()在本地自己玩没事一旦上线处理真实用户数据二分之一的概率会成为被攻击的对象。这是安全红线不是可选项。3.3 布局模板与视图碎片化CI 4.x原生支持简单的模板布局功能。在app/Views/layouts/下建一个主布局文件!DOCTYPE html html head title? $this-renderSection(title) ?/title /head body header站点导航区/header ? $this-renderSection(content) ? footer版权信息/footer /body /html视图文件变为? $this-extend(layouts/default) ? ? $this-section(title) ?博客列表? $this-endSection() ? ? $this-section(content) ? h1文章列表/h1 ul.../ul ? $this-endSection() ?extend声明继承哪个布局文件section用来填充布局中对应的区块。这种做法在涉及大量页面的项目中能省下巨量的重复HTML代码。我第一次从“每个页面写全套HTML”切到布局模式时整体页面代码量大约少了40%动导航栏只需要改一个文件。4. 模型层与数据库操作——查询构造器的正确打开方式Web应用的核心说到底是对数据的操作。CI 4.x的模型层设计得很实用既支持简单的数据表映射也提供了功能完备的查询构造器Query Builder。这一节覆盖实战中最常用的数据操作场景。4.1 数据库配置与自动加载打开app/Config/Database.php配置默认组public $default [ DSN , hostname 127.0.0.1, username root, password , database blog_db, DBDriver MySQLi, DBPrefix , charset utf8mb4, compress false, ];理解一个关键配置DBPrefix是表前缀如果设置为blog_那么模型里写$this-table articles时实际查询的表是blog_articles。这个机制在多应用共享同一个数据库时很好用能避免表名冲突。4.2 模型的标准写法在app/Models/下新建ArticleModel.php?php namespace App\Models; use CodeIgniter\Model; class ArticleModel extends Model { protected $table articles; protected $primaryKey id; protected $allowedFields [title, content, status, created_at]; protected $returnType array; protected $useTimestamps true; }需要重点说明的是$allowedFields——它在CI中扮演“白名单”角色。不在这个数组里的字段无法通过模型的insert()或save()方法写入数据库。我第一次用CI时为了方便把所有字段都填进了$allowedFields后来发现这个机制存在意义在于防止批量赋值漏洞。比如用户通过表单提交了is_admin 1如果is_admin在白名单里这就会成为安全漏洞。所以$allowedFields应该只包含真正允许用户写入的字段。$useTimestamps true是CI比较贴心的一点。它会自动在插入时写入created_at更新时写入updated_at前提是表里要有这两个字段。省去手动组装时间戳的重复劳动。4.3 查询构造器链式调用的正确用法控制器里使用模型public function index() { $model new ArticleModel(); // 查询状态为已发布的文章按时间倒序每页10条 $articles $model-where(status, published) -orderBy(created_at, DESC) -paginate(10); return view(blog_view, [articles $articles]); }查询构造器支持完整的链式调用底层实际是CI自己封装的SQL构建器。常用的方法汇总如下表方法作用示例where(字段, 值)条件过滤-where(status, published)like(字段, 关键词)模糊搜索-like(title, PHP)orderBy(字段, 方向)排序-orderBy(created_at, DESC)limit(数量, 偏移)分页-limit(10, 20)join(表, 条件)表连接-join(users, users.id articles.user_id)select(字段列表)指定查询字段-select(id, title, created_at)countAllResults()统计总条数-countAllResults()paginate()方法特别提一下。CI内置了完整的分页支持在控制器里调用paginate(10)视图中用$pager-links()输出分页链接? $pager-links() ?底层自动生成上一页、下一页和页码导航不用手动写任何分页逻辑对于一个“快速上手”的框架来说这种设计非常友好。4.4 事务处理与原生SQL的边界涉及金额、库存这类多表联动操作时必须使用事务保证数据一致性$db \Config\Database::connect(); $db-transStart(); // 开始事务 $model-insert($data1); $model-insert($data2); $db-transComplete(); // 提交事务 if ($db-transStatus() false) { // 事务失败系统已自动回滚 return redirect()-back()-with(error, 操作失败请重试); }transStart()和transComplete()之间的所有数据库操作会被包进同一个事务中任意一步失败时自动回滚全部操作。使用模型方法时框架会在后台调用查询构造器生成SQL。但有些复杂场景如多表嵌套子查询、特殊数据库函数构造器表达起来会很别扭此时可以直接用原生SQL$db \Config\Database::connect(); $sql SELECT * FROM articles WHERE MATCH(title) AGAINST(PHP IN BOOLEAN MODE); $query $db-query($sql); $results $query-getResultArray();有个原则很重要能用查询构造器解决的就不要写原生SQL。一是因为构造器会自动处理参数绑定有效防止SQL注入二是因为CI的构造器自动适配不同数据库驱动MySQL、PostgreSQL、SQLite换数据库时无需改代码。原生SQL一旦用了特定数据库的函数就绑定死了。但全文搜索这类构造器支持不好的场景也不必硬凑直接上原生SQL反而清晰。5. 表单验证、CSRF与安全过滤——上线前必须补的功课本地开发时跑通页面很简单真正让框架发挥价值的时刻是处理用户输入。这一节的内容直接关系到一个PHP应用能不能安全地上生产环境。5.1 表单验证规则CI 4.x内置了完善的验证组件在控制器中应用public function create() { $validation \Config\Services::validation(); $rules [ title required|min_length[3]|max_length[100], content required, email required|valid_email, status permit_empty|in_list[draft,published], ]; if (! $this-validate($rules)) { return redirect()-back()-withInput()-with(errors, $this-validator-getErrors()); } $model new ArticleModel(); $model-save([ title $this-request-getPost(title), content $this-request-getPost(content), status $this-request-getPost(status), ]); return redirect()-to(/blog)-with(message, 发布成功); }规则的含义比较直白required必填min_length[3]最少3个字符valid_email校验邮箱格式in_list限定可选值。以上写法中$this-validate($rules)会在验证失败时记录错误并通过with(errors, ...)把错误信息闪存到session中视图中这样显示?php if (session()-getFlashdata(errors)): ? ul ?php foreach (session()-getFlashdata(errors) as $error): ? li? esc($error) ?/li ?php endforeach; ? /ul ?php endif; ?withInput()会把用户上次提交的旧数据带回去配合old(title)辅助函数回显到表单里。这个交互细节很关键用户填错时表单不会清空体验差距很大。5.2 CSRF防护默认开启但需要正确配合CI 4.x默认开启了CSRF跨站请求伪造防护。这意味着所有POST请求的表单必须包含一个令牌字段。在表单中这样添加form methodpost action/blog/create ? csrf_field() ? input typetext nametitle ... /formcsrf_field()会自动生成一个隐藏的csrf_test_name字段。如果每张POST表单漏掉这个字段提交时CI会直接拒绝请求。这里有个容易踩的坑AJAX请求也必须携带CSRF令牌。用fetch提交时从meta标签中读取令牌meta namecsrf-token content? csrf_hash() ?fetch(/api/article, { method: POST, headers: { Content-Type: application/json, X-CSRF-TOKEN: document.querySelector(meta[namecsrf-token]).getAttribute(content) }, body: JSON.stringify(data) });另外注意CI 4.x默认CSRF令牌在每次请求后会重新生成。多标签页场景下A标签页中的令牌可能已经失效导致POST请求被拒。应对方案有两种一种是在app/Config/Filters.php的CSRF过滤器配置中将regenerate true改为false更安全的做法是保持默认但前端在收到CSRF失败响应时重新加载页面获取新令牌。5.3 全局过滤器在请求进入控制器之前做好安全检查CI 4.x的过滤器Filters相当于中间件在请求到达控制器之前或响应返回之后执行一些通用逻辑。最常用的过滤器是内置的CSRF过滤器和路由过滤器。打开app/Config/Filters.php可以看到默认配置public $aliases [ csrf \CodeIgniter\Filters\CSRF::class, toolbar \CodeIgniter\Filters\DebugToolbar::class, honeypot \CodeIgniter\Filters\Honeypot::class, ]; public $globals [ before [ honeypot, csrf, ], after [ toolbar, ], ];$globals[before]中的过滤器会在所有请求前执行。如果想给某组路由单独加登录验证过滤器可以在$routes-group()中指定$routes-group(admin, [filter login_check], function($routes) { $routes-get(dashboard, Admin\Dashboard::index); $routes-get(settings, Admin\Settings::index); });自定义一个login_check过滤器在app/Filters/LoginCheck.php中实现逻辑然后在$aliases中注册public $aliases [ csrf \CodeIgniter\Filters\CSRF::class, login_check \App\Filters\LoginCheck::class, ];过滤器中的before()方法在控制器之前执行public function before(RequestInterface $request, $arguments null) { if (! session()-get(isLoggedIn)) { return redirect()-to(/login); } }返回一个redirect()响应时CI会中断后续请求直接跳转到登录页。这套机制让权限控制非常清爽不需要在每个控制器里重复写“是否登录”的判断逻辑。6. 环境配置、日志与开发调试——遇到问题时的生存技能任何一个框架写代码的时间远少于查问题的时间。掌握CI的调试手段能让你在遇到问题时保持清醒。6.1 环境配置三种环境与敏感信息管理CI 4.x默认配置文件位于env文件Composer安装时自动生成运行前先执行cp env .env在.env文件中可以覆盖app/Config/下的任何配置项比如# 环境类型development / testing / production CI_ENVIRONMENT development # 覆盖数据库配置 database.default.hostname 127.0.0.1 database.default.database blog_db database.default.username root database.default.password secret # 关闭调试工具栏 CI_DEBUG false不要把数据库密码写在app/Config/Database.php中而应该放在.env里并且.env必须加入.gitignore。.env文件不会被Web服务器直接访问到这样即使代码仓库被分享敏感信息也不会泄露。这个习惯从项目第一天就要养成后期再迁移环境只需要修改.env不需要改动PHP代码——开发环境和生产环境共用同一份代码配置各管各的。6.2 debug工具栏与日志使用在development环境下页面底部会显示一个调试工具栏Debug Toolbar上面实时展示每次请求执行的SQL语句、耗时、内存占用、包含的文件列表。这个工具栏对排查“页面慢到底慢在哪”很有帮助点开“Queries”标签能直接看到每一条SQL语句SELECT * FROM articles WHERE status published ORDER BY created_at DESC LIMIT 10调试工具栏在生产环境CI_ENVIRONMENT production下不会显示避免泄露数据库结构。自己排查问题时可以在代码中手动记录日志log_message(error, 文章ID {id} 查询失败, [id $id]);日志写入writable/logs/目录按照日期生成文件。log_message()支持emergency、alert、critical、error、warning、notice、info、debug八个级别不同级别的日志会被过滤由app/Config/Logger.php中的$threshold配置控制。6.3 高频异常与解决方案对照我在使用CI 4.x的过程中遇到过几个极其高频的问题整理成表格供读者对照排查症状根本原因解决方案页面404但URL看起来正确控制器名首字母未大写或路由顺序错误检查app/Controllers下的类名首个字母是否大写调整路由定义顺序表单提交返回403CSRF令牌缺失或过期表单添加csrf_field()AJAX携带令牌检查regenerate配置SQL执行报错 “Unknown column”$allowedFields中缺少该字段将字段加入模型的$allowedFields数组base_url()生成的URL缺少子目录baseURL未配置在app/Config/App.php或.env中显式配置完整URL修改PHP代码不生效使用了php spark serve某些OPcache环境未刷新重启开发服务器或清除OPcache扩展缓存时区错误时间差8小时PHP时区未设置在.env中配置app.defaultLocale zh-CN并设置date_default_timezone_set(Asia/Shanghai)第2个问题值得反复提醒CSRF的403错误非常容易让人一头雾水因为页面上没有任何提示只返回一个空白页面或浏览器自带的403页面。先把app/Config/Filters.php中的CSRF过滤器暂时注释掉如果能跑通了就说明问题在CSRF机制上然后依次检查表单令牌、session启动状态和令牌刷新策略。7. 几个让代码更规范的小技巧框架用熟练之后一些习惯会影响项目的长期体验。分享几个我实打实用过几年、回头觉得特别值得坚持的做法。尽量用路由到控制器再通过控制器调模型不要在视图中直接写SQL或调用模型。视图只负责数据展示一旦视图里出现$db-query()这类代码数据逻辑和展示逻辑就混在一起了后期改业务逻辑时极容易改出隐蔽Bug。模型的职责要收敛。一个模型尽量对应一张主表不要在模型里堆砌过多的跨表关联逻辑。实在需要多表关联查询时我会新建一个独立的服务类放在app/Services目录来处理复杂查询把控制器-模型-视图这条链路保持简单清晰。CI官方不强制这个做法但项目规模上来后你会发现“单表模型独立服务类”的组合比“模型里汇聚所有查询”好维护得多。表单验证规则不要散落在各个控制器方法里统一放到模型或专门的验证配置中管理。CI的模型类支持$validationRules属性验证规则可以定义在模型内部控制器中一行$model-save($data)框架自动验证不通过就返回错误。这个方式能够在多个控制器调用同一张表时不重复写验证规则。从安装环境到表单安全写这篇内容时我一直在想一件事CI之所以能在PHP陆续出现大批新框架的背景下被很多人持续使用恰恰是因为“务实”。它不追求花哨不会强制你学习一整套DSL或命令行体系而是回归框架的本质——把常用的问题解决好把不受用的复杂度挡在外面。这种特质放到今天确实能让人静下心来完成手头的事。
返回列表