PHPPHP 包管理并搭建管理后台
MeteorCat在 PHP 目前在经过不断迭代之后完善了 包管理 机制, 可以通过正规包管理来部署对应的第三方包和项目维护.
包管理工具: PHP-Composer
这里如果是日常开发安装即可, 而 Linux 环境可能需要在服务器部署对应服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| # 这边采用 debian 环境, 所以可以直接采用 apt 源部署 # 虽然直接官网配置能够安装最新版本, 但是还是习惯直接默认做源依赖好点 sudo apt install composer
# 很简单就安装上, 之后就是开始包管理, 这里先进入测试目录 cd /tmp
# 如果要初始化自己化的项目可以按照一下命令安装初始化 mkdir owner-project && cd owner-project # 创建并进入目录 composer init # 执行初始化 # 在命令行当中可以输入以下内容 # Package name (<vendor>/<name>) [meteorcat/owner-project] # Description []: # Author [n to skip]: # Minimum Stability []: # Package Type (e.g. library, project, metapackage, composer-plugin) []: # License []: # Would you like to define your dependencies (require) interactively [yes]? # Would you like to define your dev dependencies (require-dev) interactively [yes]? # Add PSR-4 autoload mapping? Maps namespace "Meteorcat\OwnerProject" to the entered relative path. [src/, n to skip]: # Do you confirm generation [yes]? # 之后就会在当前目录初始化完成
# 之后就是一些命令技巧 # 安装开发环境依赖( --dev 仅开发时使用) composer require --dev phpunit/phpunit
# 安装 >=1.0 且 <2.0 的版本 composer require phpunit/phpunit:^1.0
# 只更新某个包, 不改动其他依赖, 避免全局更新引发冲突 composer update phpunit/phpunit
# 如果不指定某个包就代表全部包更新 composer update
# 强制忽略 composer.lock, 拉取 composer.json 中定义的最新符合版本 composer update --ignore-platform-reqs
# 离线安装依赖, 如果本地已有依赖缓存,无需联网重新下载 # --no-dev: 不安装开发依赖 # --prefer-dist: 优先使用压缩包, 速度更快 composer install --no-dev --prefer-dist --optimize-autoloader
# 生成类映射表,提升加载速度 # 生产环境下优化自动加载映射, 减少文件查找耗时 composer dump-autoload --optimize composer dump-autoload --optimize --classmap-authoritative # 生产环境终极优化(合并类映射+生成优化文件)
# 扫描已安装的依赖是否存在已知安全漏洞, 正式部署需要注意扫描是否存在漏洞 composer audit
# 检查当前项目的 Composer 配置和 PHP 环境是否符合要求, 用于环境检测 composer diagnose
# Composer 会缓存下载的包, 缓存异常时可清理 composer clear-cache
# 生产环境部署时, 避免执行自定义脚本和交互提示 composer install --no-interaction --no-scripts --optimize-autoloader
# 推荐采用的正式环境部署配置如下, 可以自动检查并且安装需要的依赖 composer install --no-interaction --no-scripts --no-dev --optimize-autoloader
|
这里就是简单使用方式, 其实没什么需要处理, 具体要根据项目需要变动.
CodeIgniter
官网地址: CodeIgniter
CodeIgniter 是当年最喜欢用的 Web 框架, 小巧简约且功能完备, 哪怕到现在如果涉及到管理后台项目都喜欢用他来构建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| # 安装依赖 sudo apt install php-intl php-mbstring php-json php-dom php-zip php-gd php-curl cd /tmp
# 这里现在临时目录创建 # 构建最新项目并且声明为 pino-admin, 之后等待安装配置完成 composer create-project codeigniter4/appstarter pino-admin # 创建之后生成一下目录: # - app、public、tests、writable # - vendor/codeigniter4/framework/system
# 进入项目目录并追加默认系统翻译模板 cd pino-admin composer require codeigniter4/translations # 安装默认系统 i18n 翻译模板
# 需要注意内部的 writable 目录需要提供 777 权限 chmod -R 777 writable/
# 安装完成就可以启动项目, 内部已经默认携带命令行项目 php spark serve --port 19999 # 启动 Web 服务并指定 19999 端口 # 访问 http://localhost:19999 能够看到内容就代表服务已经启动, 能够看到默认的页面
# 推荐检查, 推荐(Recommended) 列显示了生产环境的推荐值, 它们在开发环境中可能会有所不同 # 可以按照推荐来修改对应值 php spark phpini:check
|
这里推荐几个配置, 可以直接大大简化开发速度:
至于涉及 .env 配置看下官方文档就行了, 基本上没什么需要说的.
Bootstrap
PHP 默认安装完成只有底层空架构, 所以一般需要采用 UI 框架, 大部分情况都会选择前后端分离, 两者互相分开仅有接口交互.
但是现实有时候就比较复杂, 出于成本和效率考量必须要短评快上线项目的时候, 前后端分离就带来大量的沟通和联调成本.
前后端全栈合并这种概念目前还没这么成熟, 基本上还在重走 PHP 老路, 前端有时候没有后端那种攻防经验(参照 CVE-2025-55182)
而开发后台部分我还是喜欢用 PHP + UI框架 来处理, 主要有时候仅仅只需要做个后台管理功能而已, 没必要大费周章去专门部署前端项目.
另外我很讨厌 NodeJS 项目的 node_modules 到处堪称 ‘拉屎’ 的行为, 每次看到这个目录都感觉头皮发麻
所以这么多年还是依旧采用 Bootstrap 作为底层 UI 框架, 截止到目前编写的版本已经是 Bootstrap v5.3.8.
这里推荐些搭建所需要的组件依赖(新版本的 Bootstrap 已经移除了 jQuery 依赖, 可以采用原生 JS 函数操作):
以上这些基本涵盖日常使用到的方方面面, 后续有需要可以扩展(还有 富文本编辑器 和 图表 功能, 太多了不好说哪个更好用)
另外新版本的 Bootstrap 提供了 Composer 的包管理器安装支持:
1 2 3 4 5 6 7 8 9
| # 安装最新版本的 Bootstrap 和图标库等相关库 composer require twbs/bootstrap composer require twbs/bootstrap-icons composer require wenzhixin/bootstrap-table
# 安装 5.3.8 UI 库和 1.13.1 Icon 库 composer require twbs/bootstrap:5.3.8 composer require twbs/bootstrap-icons:1.13.1 # 其他略
|
默认 composer require 这些安装包只是下载到当前 vendor 目录, 目前还没暴露给外部目录, 需要采用自定义钩子来将文件复制过去.
包括之前的 codeigniter4/translations 系统翻译 i18n 库也是仅仅下载到本地, 需要手动处理移动过去
需要在根目录的 composer.json 文件追加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "scripts": { "update-bootstrap-linux": [ "mkdir -p public/static/bootstrap/css public/static/bootstrap/js", "cp -rf vendor/twbs/bootstrap/dist/css/* public/static/bootstrap/css/", "cp -rf vendor/twbs/bootstrap/dist/js/* public/static/bootstrap/js/", "mkdir -p public/static/bootstrap-icons/icons public/static/bootstrap-icons/font/fonts", "cp -rf vendor/twbs/bootstrap-icons/icons/* public/static/bootstrap-icons/icons/", "cp -rf vendor/twbs/bootstrap-icons/font/* public/static/bootstrap-icons/font/", "mkdir -p public/static/bootstrap-table", "cp -rf vendor/wenzhixin/bootstrap-table/dist/* public/static/bootstrap-table/" ], "update-bootstrap-windows": [ "if not exist public\\static\\bootstrap\\css mkdir public\\static\\bootstrap\\css", "if not exist public\\static\\bootstrap\\js mkdir public\\static\\bootstrap\\js", "xcopy /E /Y vendor\\twbs\\bootstrap\\dist\\css\\* public\\static\\bootstrap\\css\\", "xcopy /E /Y vendor\\twbs\\bootstrap\\dist\\js\\* public\\static\\bootstrap\\js\\", "if not exist public\\static\\bootstrap-icons\\icons mkdir public\\static\\bootstrap-icons\\icons", "if not exist public\\static\\bootstrap-icons\\font\\fonts mkdir public\\static\\bootstrap-icons\\font\\fonts", "xcopy /E /Y vendor\\twbs\\bootstrap-icons\\icons\\* public\\static\\bootstrap-icons\\icons\\", "xcopy /E /Y vendor\\twbs\\bootstrap-icons\\font\\* public\\static\\bootstrap-icons\\font\\", "if not exist public\\static\\bootstrap-table mkdir public\\static\\bootstrap-table", "xcopy /E /Y vendor\\wenzhixin\\bootstrap-table\\dist\\* public\\static\\bootstrap-table\\" ] } }
|
这里区分 Linux 和 Window 平台的安装功能, 只要执行输入对应平台的安装指令就会默认更新到目录之中:
1 2 3 4 5
| # Linux/macOS 执行 composer run update-bootstrap-linux
# Windows 执行 composer run update-bootstrap-windows
|
其实可以追加命令钩子, 也就是 install 等相关指令一起跟随执行; 但是后来感觉没必要, 直接让开发者选择去更新就行
页面布局渲染
作为基本管理后台需要以下基础功能(这里提供用官方模板直接起步):
不过这里需要做 PHP 功能的页面切分, 把通用模块成以下的模块, 所以需要以下文件:
layout.php 该内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="author" content="<?= $TITLE ?? ''; ?>"> <title><?= $TITLE ?? ''; ?></title>
<link href="<?= base_url('/static/bootstrap/css/bootstrap.min.css') ?>" rel="stylesheet" crossorigin="anonymous"> <link href="<?= base_url('/static/bootstrap-icons/font/bootstrap-icons.css') ?>" rel="stylesheet" crossorigin="anonymous">
<?= $this->renderSection('head') ?> </head> <body>
<?= $this->renderSection('body') ?>
<script src="<?= base_url('/static/bootstrap/js/bootstrap.bundle.min.js') ?>"></script> <?= $this->renderSection('script') ?> </body> </html>
|
这里采用的 <?= $this->renderSection('') ?> 就是 CodeIgniter 的视图模板占位处理方式, 也就是后续其他页面会被填充进去.
内部 View 视图文件文件命令方法采用下划线命名法
之后创建 Login 渲染页面, 用来提供暴露外部登陆页面, 这里创建 app/Views/login.php 文件, 如下来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| <!-- 引入扩展视图 --> <?= $this->extend('layout') ?>
<!-- 找到 head 渲染块填充 --> <?= $this->section('head') ?> <style> html, body { height: 100%; }
body { display: flex; align-items: center; padding-top: 40px; padding-bottom: 40px; background-color: }
.form-login { max-width: 330px; padding: 15px; }
.form-login .form-floating:focus-within { z-index: 2; }
.form-login input[name="username"] { margin-bottom: -1px; border-top-left-radius: 0; border-top-right-radius: 0; }
.form-login input[name="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } </style> <?= $this->endSection() ?>
<!-- 找到 body 渲染块填充 --> <?= $this->section('body') ?>
<main class="form-login w-100 m-auto text-center"> <form id="form-login-container"> <img class="mb-4" src="" alt="<?= $TITLE ?? '' ?>" width="96" height="96"> <h1 class="h3 mb-3 fw-normal">登陆界面</h1>
<div class="form-floating"> <input type="text" class="form-control" id="floatingInput" placeholder="用户名" name="username"> <label for="floatingInput">用户名</label> </div> <div class="form-floating"> <input type="password" class="form-control" id="floatingPassword" placeholder="密码" name="password"> <label for="floatingPassword">密码</label> </div>
<hr> <a class="w-100 btn btn-lg btn-primary" id="form-login-container-submit" href="javascript:void(0)">Login</a> <p class="mt-5 mb-3 text-muted"><?= $TITLE ?? '' ?> © 2014–<?= date('Y') ?></p> </form> </main>
<?= $this->endSection() ?>
<!-- 找到 script 渲染块填充 --> <?= $this->section('script') ?> <script> window.addEventListener('DOMContentLoaded', function () {
}) </script> <?= $this->endSection() ?>
|
这里的 $this->section(区块名称) 和 $this->endSection() 必须成对包围, 内部会填充到 $this->renderSection(区块名称).
现在就能创建对应控制器路由文件, 这里先临时定义文件为 app/Controllers/Login.php(类文件都采用大驼峰声明):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php
namespace App\Controllers;
class Login extends BaseController {
public function getIndex(): string { return view("login"); } }
|
然后访问 http://localhost:8080/login 就能获取到渲染的页面内容, 如果没问题就可以考虑编写授权流程代码.
面板渲染
对于面板渲染推荐采用 iframe 这种独立分块渲染的方式, 因为有时候需要拆分网页页面来分别比较数据, 如果采用一体化就很不方便.
而且一体化渲染每次触发切换菜单会导致全局重新渲染, NodeJS 有局部渲染来处理则不会这样, 但总感觉在 Vue/React 渲染起来的速度总有卡顿延迟.
也可能是因为服务端这种一体成型渲染起来视觉感知比较弱, 而 Vue/React 这种前后端动态节点附加的时候能够感受到渲染延迟
这里可以采用上面官方 Dashboard 模板构建默认的 Layout 文件:
模板内容可以处理成以下类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
| <!-- 复用模块 --> <?= $this->extend('layout') ?>
<?= $this->section('head') ?> <!-- 设置后台主题色 --> <meta name="theme-color" content="#712cf9">
<!-- 常规重写样式 --> <style> body { font-size: .875rem; }
.feather { width: 16px; height: 20px; }
.sidebar { position: fixed; top: 0;
bottom: 0; left: 0; z-index: 100; padding: 48px 0 0; box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); }
@media (max-width: 768px) { .sidebar { top: 5rem; } }
.sidebar-sticky { height: calc(100vh - 48px); overflow-x: hidden; overflow-y: auto; }
.sidebar .nav-link { font-weight: 500; color: }
.sidebar .nav-link .feather { margin-right: 4px; color: }
.sidebar .nav-link.active { color: }
.sidebar .nav-link:hover .feather, .sidebar .nav-link.active .feather { color: inherit; }
.navbar-brand { padding-top: .75rem; padding-bottom: .75rem; background-color: rgba(0, 0, 0, .25); box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); }
.navbar .navbar-toggler { top: .25rem; right: 1rem; }
.navbar .form-control { padding: .75rem 1rem; }
.form-control-dark { color: background-color: rgba(255, 255, 255, .1); border-color: rgba(255, 255, 255, .1); }
.form-control-dark:focus { border-color: transparent; box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); }
@media (min-width: 992px) { flex: 0 0 auto; width: 13.66666667%; }
flex: 0 0 auto; width: 86.33333333%; } } </style>
<!-- 将这里填充的内容扩展给引用对象使用 --> <?= $this->renderSection('styles') ?> <?= $this->endSection() ?>
<?= $this->section('body') ?>
<header id="header-navbar" class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6 text-center" href="<?= base_url("/dashboard") ?>"><?= $TITLE ?? 'Dashboard' ?></a> <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button>
<div class="navbar-nav w-100 d-flex justify-content-end">
<!-- 移除了可能导致问题的外层nav-item容器 --> <!-- 使用d-flex确保元素水平排列 --> <div class="d-flex align-items-center justify-content-end">
<div class="px-1"> <div class="input-group input-group-sm"> <span class="input-group-text" id="navbar-language-label">语言</span> <select class="form-select form-select-sm border-0" aria-label="语言" aria-describedby="navbar-language-label" id="navbar-language"> <option <?= isset($LANGUAGE) && $LANGUAGE === 'en' ? 'selected' : '' ?> value="en"> en - English </option> <option <?= isset($LANGUAGE) && $LANGUAGE === 'zh-CN' ? 'selected' : '' ?> value="zh-CN"> zh-CN - 简体中文 </option> <option <?= isset($LANGUAGE) && $LANGUAGE === 'zh-TW' ? 'selected' : '' ?> value="zh-TW"> zh-TW - 繁体中文 </option> <option <?= isset($LANGUAGE) && $LANGUAGE === 'ja' ? 'selected' : '' ?> value="ja"> ja - 日本語 </option> </select> </div> </div>
<!-- 使用 Bootstrap 按钮组组件 --> <div class="btn-group px-1 py-0" role="group" aria-label="User Operate"> <a id="navbar-refresh-system" href="javascript:void(0)" class="btn btn-sm btn-success">刷新系统</a> <a id="navbar-change-account" href="javascript:void(0)" class="btn btn-sm btn-primary">修改信息</a> <a id="navbar-exit-system" href="javascript:void(0)" class="btn btn-sm btn-danger">退出系统</a> </div>
</div> </div> </header>
<div class="container-fluid"> <div class="row">
<nav id="sidebar-menu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse"> <div class="position-sticky pt-3 sidebar-sticky"> <ul class="nav flex-column">
<?php foreach ($MENU ?? [ [ "name" => "测试父页面", "children" => [ [ "url" => "/test", "name" => "测试子页面" ] ] ] ] as $k => $v): ?> <li class="nav-item"> <a class="nav-link d-flex justify-content-between align-items-center sidebar-menu-parent" data-bs-toggle="collapse" href="#dashboard-submenu-<?= $k ?>" role="button" aria-expanded="false"> <span class="d-flex align-items-center"> <?= isset($v['icon']) && $v['icon'] ? "<i class='{$v["icon"]} align-text-bottom me-2'></i>" : '' ?> <span class="ms-1"><?= $v['name'] ?></span> </span> <i class="bi bi-chevron-down align-text-bottom transition-transform duration-300"></i> </a> <div class="collapse" id="dashboard-submenu-<?= $k ?>"> <ul class="nav flex-column ms-3 m-1"> <?php foreach ($v['children'] ?? [] as $sub): ?> <li class="nav-item"> <a class="nav-link" href="<?= isset($sub['url']) && $sub['url'] ? base_url($sub['url']) : 'javascript:void(0)' ?>" target="system-iframe-container" rel="noopener noreferrer"> <?= isset($sub['icon']) && $sub['icon'] ? "<i class='{$sub["icon"]} align-text-center'></i>" : '' ?> <?= $sub['name'] ?> </a> </li> <?php endforeach; ?> </ul> </div> </li> <?php endforeach; ?>
</ul> </div> </nav>
<main id="iframe-main" class="col-md-9 ms-sm-auto col-lg-10 p-0 m-0" style="min-height: calc(100vh - 56px);"> <main class="w-100 h-100" style="overflow: hidden;"> <!-- 嵌入外部网页的 iframe --> <iframe name="system-iframe-container" src="about:blank" id="system-iframe-container" class="w-100 h-100 border-0" allowfullscreen loading="lazy" referrerpolicy="no-referrer-when-downgrade"> </iframe> </main> </main> </div> </div>
<?= $this->endSection() ?>
<?= $this->section('script') ?>
<!-- 将这里填充的内容扩展给引用对象使用 --> <?= $this->renderSection('extras') ?> <?= $this->endSection() ?>
|
最后追加个渲染控制器页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
namespace App\Controllers;
class Dashboard extends BaseController {
public function getIndex(): string { return view("dashboard"); }
}
|
启动之后访问 http://localhost:8080/dashboard 就可以看到具体的渲染效果:
