ARTICLE DETAIL

资讯详情

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

PHP从入门到精通:环境、语法、数据库、队列与安全笔记

PHP从入门到精通:环境、语法、数据库、队列与安全笔记 简介《PHP从入门到精通完整笔记》是面向Web开发初学者、计算机专业学生及转行人员的一份系统学习文档共301页按HTML、CSS、PHP三条技术主线组织可用于课前预习、课后复盘以及面试前查漏补缺。压缩包中仅含1个PDF文件体积约2.14MB轻便易存适合在电脑或移动端随时翻阅。内容按讲次编号衔接前22讲覆盖HTML与CSS第23至149讲转入PHP从标记语言发展历史、HTML基本结构、单标记、字符实体、超链接与图片标签、表格布局及align、border、colspan、rowspan等属性讲起再过渡到样式定义与选择器继而延伸到PHP变量、条件语句、循环、数组、函数、类与对象以及MySQL数据库交互等核心知识点并穿插.html与.htm后缀区别一类面试小结。目前该笔记已有1083人学习下载适合希望用一份材料打通前端基础与PHP后端入门的读者。1. PHP 从入门到精通完整笔记三百页资料该怎么按顺序啃不少人拿到一份三百页的 PHP 笔记习惯从第一页翻起翻到第四章还在讲变量和 echo热情过去就放下了。这类资料叫「从入门到精通」真正的难点不在页数而在中间那条看不见的分界线前半本讲的是语法后半本讲的是工程。语法部分几天就能过一遍工程部分要拿真实项目反复磨。这份笔记实际覆盖的是五件事Windows 下把 PHP 环境跑起来、基本语法与运算符、PHP 类与接口数据结构、数据库和队列这类工程能力、以及上线前必须自己验一遍的错误处理与安全边界。适合刚接触 PHP 的在校生也适合写了两年 PHP、只会改别人源码但说不清参数含义的开发者。下面的顺序不是资料的目录顺序而是我按项目落地节奏排的读法。2. Windows 10 Nginx PHP 环境搭建零基础入门教程的第一道坎本地环境跑不起来后面所有代码都是纸上谈兵。PHP 在 Windows 上有两条路集成环境一键装或者 Nginx 加 PHP-CGI 手动配。选哪条取决于你打算把这套环境用来做什么——照着教程跑几个 demo集成包足够打算长期写项目、要接调试器和虚拟主机配置手动配一次更省事。2.1 phpstudy 这类集成包和手动配置的取舍对比项集成包如 phpstudy 面板手动配 Nginx PHP-CGI上手时间十分钟内可跑半小时到一小时版本切换面板点一下改 fastcgi_pass 端口或替换目录配置文件透明度面板改写容易被覆盖全部在自己手里可进版本库适合场景教学演示、临时跑别人的 php 网站源码长期项目、需要 Xdebug 和伪静态排错难度出问题不知道被哪层改了每一步都清楚报错可定位我一般建议先装集成包确认机器没问题再手动配一遍把 Nginx 配置和 php.ini 落到自己的项目目录里跟着仓库提交。换机器时拉下来就能跑比记面板截图靠谱。2.2 手写一份能跑的 Nginx PHP-CGI 配置先确认 PHP 解压目录例如D:/php下有php-cgi.exe然后在 php.ini 里打开fastcgi.impersonate、设置cgi.fix_pathinfo0用命令行启动 CGI# 监听 9000 端口-b 是 bind 地址-c 指定配置文件 D:/php/php-cgi.exe -b 127.0.0.1:9000 -c D:/php/php.iniNginx 侧的最小可用配置如下server { listen 80; server_name localhost; root D:/www/phpnote/public; # 只暴露 public源码放上一层 index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; # 这一行写错会直接报 No input file specified fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known) { deny all; # 挡住 .env、.git 这类隐藏文件 } }逻辑说明try_files把不存在的路径全部交给index.php后面接框架路由时不用再改fastcgi_pass必须和上面-b的地址一致端口冲突是最常见的 502 来源SCRIPT_FILENAME用$document_root拼接写死绝对路径换机器就废。隐藏文件那条正则里的(?!well-known)是给证书校验留的口子其余点开头的路径一律拒绝。php.ini 里有几个参数新手最容易忽略参数开发环境生产环境作用display_errorsOnOff是否把错误打到页面上error_reportingE_ALLE_ALL ~E_DEPRECATED报错级别log_errorsOnOn写入错误日志upload_max_filesize20M按业务定单文件上传上限post_max_size24M必须大于上传上限整个 POST 体上限date.timezoneAsia/ShanghaiAsia/Shanghai不设会让时间函数报警告提示post_max_size小于upload_max_filesize时大文件上传会表现为$_FILES为空、$_POST也空排查时先看这两个值。2.3 VSCode 与 NetBeans 的配置差异以及怎么确认环境真的通了习惯用 NetBeans 的人调试配置在项目属性里点选即可IDE 会自己管理 Xdebug 端口VSCode 更依赖仓库里的配置文件好处是团队可以共用一份。VSCode 的.vscode/settings.json{ php.validate.executablePath: D:/php/php.exe, php.validate.enable: true, php.suggest.basic: false, [php]: { editor.tabSize: 4, editor.formatOnSave: true } }配 Xdebug 断点还要一份.vscode/launch.json{ version: 0.2.0, configurations: [ { name: Listen for Xdebug, type: php, request: launch, port: 9003, pathMappings: { D:/www/phpnote: ${workspaceFolder} } } ] }对应的 php.ini 追加xdebug.modedebug和xdebug.start_with_requesttrigger端口要和 launch.json 一致。pathMappings是断点打不中的头号原因容器或虚拟机里的路径和本机路径不同时必须映射。验证环境不用写测试页两条命令就够php -v # 看版本和是否加载了 Xdebug php -m | findstr -i pdo_mysql redis gd mbstring第二条命令确认扩展有没有开。缺pdo_mysql会在连数据库时报 could not find driver缺mbstring则mb_strlen直接报未定义函数这两个报错信息本身不提示扩展缺失容易查半天。3. PHP 语言之基本语法与运算符把基础写成能复用的肌肉记忆语法章节最容易被跳读因为看起来都懂。但 PHP 的运算符和类型转换有几处反直觉的设计跨大版本改过行为线上事故不少是从这里来的。这一章不重复资料里的定义只挑会咬人的地方。3.1 类型转换与比较运算符的坑表达式PHP 7 结果PHP 8 结果说明0 atruefalse数字与非数字字符串比较规则改了1e3 1000truetrue两边都是数字字符串时按数字比10 9truetrue非数字字符串按字符串逐位比0.1 0.2 0.3falsefalse浮点比较必须用差值阈值null falsetruetrue用规避?php declare(strict_types1); // ?? 只在键不存在或值为 null 时取默认值不会把 0、 当成没传 $page max(1, (int)($_GET[page] ?? 1)); $size min(100, max(1, (int)($_GET[size] ?? 20))); var_dump(0 a); // PHP 8 起 false var_dump(in_array(0, [a, b], true)); // 第三个参数 true 走严格比较 // 金额别用浮点用整数分或者字符串加 bcmath $total bcadd(19.99, 0.01, 2);逻辑说明declare(strict_types1)只约束函数参数与返回值不改变的行为很多人以为开了它就安全了in_array第三个参数漏掉是线上高频事故点in_array(0, [a,b])在 PHP 7 下返回 true等于放行了一个不该放行的值。金额用整数分存储或bcadd这类任意精度函数是账目系统的底线。3.2 表单基础与接口数组对象请求参数怎么收、怎么回传统表单走$_POST但它只解析application/x-www-form-urlencoded和multipart/form-data。前端如果发Content-Type: application/json$_POST是空的必须读php://input。?php declare(strict_types1); header(Content-Type: application/json; charsetutf-8); $raw file_get_contents(php://input); $input json_decode($raw, true); if (!is_array($input)) { http_response_code(400); echo json_encode([code 400, msg body 必须是 JSON 对象], JSON_UNESCAPED_UNICODE); exit; } $name trim((string)($input[name] ?? )); $tags array_values(array_filter((array)($input[tags] ?? []), is_string)); if ($name || mb_strlen($name) 20) { http_response_code(422); echo json_encode([code 422, msg name 长度需在 1-20], JSON_UNESCAPED_UNICODE); exit; } echo json_encode([ code 0, data [name $name, tags $tags], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);逻辑说明php://input拿到的是原始请求体可以重复读但enctypemultipart/form-data时不可用array_filter带is_string回调把非字符串元素剔掉再用array_values重排索引保证返回给前端的数组是干净的列表而不是带洞的哈希JSON_UNESCAPED_UNICODE让中文保持原样前端不用再解一层转义。注意处理中文序列化时PHP 的serialize()是字节级格式跨语言比如给 Java 或 Go 消费不要用直接上 JSON。3.3 PHP 类与对象从函数堆叠到可维护的 Service 层入门教程里常见一个文件里几十个函数项目一大就没人敢改。把同一类职责收进 PHP 类是「入门」到「精通」最实际的一步。?php declare(strict_types1); final class UserService { // 构造器属性提升PHP 8.0 可用省掉一行赋值 public function __construct(private PDO $pdo) {} /** return array{id:int,name:string} */ public function find(int $id): array { $stmt $this-pdo-prepare(SELECT id, name FROM users WHERE id :id); $stmt-execute([:id $id]); $row $stmt-fetch(PDO::FETCH_ASSOC); if ($row false) { throw new RuntimeException(user {$id} not found); } return [id (int)$row[id], name (string)$row[name]]; } }逻辑说明PDO 用构造注入而不是在类里new测试时能塞内存库返回类型固定成array{id:int,name:string}这种形状接口层直接json_encode前端拿到的字段类型稳定不会今天 id 是字符串明天是数字。改类名或改方法名时用 IDE 的重命名重构别手工全局替换字符串——PHP 里类名出现在字符串中比如容器绑定、class_exists的情况太多了。4. PHP 数据库、文件上传与 Redis 队列把「精通」落到工程能力上语法过关之后真正拉开差距的是三件事数据库访问写不写对、上传会不会被人钻空子、耗时任务会不会把请求堵死。这三件事在任何一套 PHP 网站源码里都能找到也最容易写得将就。4.1 PDO 连接与图书管理系统这类 CRUD 的分页写法$pdo new PDO( mysql:host127.0.0.1;port3306;dbnamelib;charsetutf8mb4, lib_user, change_me, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ] ); $stmt $pdo-prepare( SELECT id, title, author FROM books WHERE title LIKE :kw ORDER BY id DESC LIMIT :off, :lim ); $stmt-bindValue(:kw, % . $kw . %, PDO::PARAM_STR); $stmt-bindValue(:off, $offset, PDO::PARAM_INT); $stmt-bindValue(:lim, $limit, PDO::PARAM_INT); $stmt-execute(); $rows $stmt-fetchAll();参数说明EMULATE_PREPARES false让 MySQL 做服务端真预处理注入面更小而且 INT 列能返回真正的整数类型不会全变字符串ERRMODE_EXCEPTION让错误抛异常否则默认静默返回 falsefetchAll()拿到空数组你还以为是没数据LIMIT的偏移和条数必须用bindValue配PDO::PARAM_INT用execute([...])传数组会被当成字符串MySQL 直接抛语法错误这是分页写不对最常见的原因。表结构上title列如果经常做前缀匹配LIKE %kw%用不上索引数据量上十万条就得考虑全文索引或者外部搜索别硬扛。4.2 PHP 上传漏洞的成因以及一份能直接抄的校验代码上传出问题几乎都是同一个模式只看扩展名顺手用了客户端提交的文件名落盘目录还能执行 PHP。环节危险做法安全做法类型判断看$_FILES[type]或文件后缀用 finfo 读文件头真实 MIME文件名直接用$_FILES[name]random_bytes生成新名目录和入口文件同目录独立目录Nginx 里禁止执行大小不限制或只靠前端后端校验字节数错误处理忽略$_FILES[error]显式处理每种错误码?php declare(strict_types1); $allow [image/jpeg jpg, image/png png]; $f $_FILES[cover] ?? null; if (!$f || $f[error] ! UPLOAD_ERR_OK) { exit(上传失败); // UPLOAD_ERR_INI_SIZE 说明被 php.ini 截断 } if ($f[size] 2 * 1024 * 1024) { exit(超过 2MB); } $finfo new finfo(FILEINFO_MIME_TYPE); $mime $finfo-file($f[tmp_name]); // 以文件内容判断不信任客户端 if (!isset($allow[$mime])) { exit(类型不允许); } $name bin2hex(random_bytes(16)) . . . $allow[$mime]; $dest __DIR__ . /uploads/ . date(Ym) . /; if (!is_dir($dest) !mkdir($dest, 0755, true)) { exit(目录创建失败); } if (!move_uploaded_file($f[tmp_name], $dest . $name)) { exit(落盘失败); }逻辑说明$_FILES[type]全部来自请求头可以随意伪造finfo-file()读的是临时文件的魔数改后缀骗不过去随机文件名同时解决两个问题——覆盖已有文件和文件名里带../造成的路径穿越move_uploaded_file比rename多一层「确实是上传上来的文件」检查别图省事换掉。上传目录还要在 Nginx 里补一条location ^~ /uploads/ { location ~ \.php$ { deny all; } # 目录内一切 PHP 请求直接拒绝 }4.3 php redis 消费组与 PHP 队列用 Stream 做异步任务发邮件、生成报表这类动作放进请求里页面就得等。用 Redis 的 Stream 加消费组比简单的LPUSH/BRPOP多出两个能力消息可以重复投递消费失败了不用手工塞回去。$redis new Redis(); $redis-connect(127.0.0.1, 6379); $stream mail:queue; $group mailer; try { // 第四个参数 mkstreamtrue队列不存在时自动创建 $redis-xGroup(CREATE, $stream, $group, 0, true); } catch (RedisException $e) { // BUSYGROUP组已存在忽略即可 } while (true) { // 表示只取从未投递给本组的新消息5000 是阻塞毫秒数 $res $redis-xReadGroup($group, worker-1, [$stream ], 1, 5000); if (!$res) { continue; } foreach ($res[$stream] as $id $fields) { if (sendMail($fields[to] ?? , $fields[subject] ?? )) { $redis-xAck($stream, $group, [$id]); // 成功才确认 } // 失败不确认消息留在 PEL 里等重试 } }生产端一行就够$redis-xAdd(mail:queue, *, [to ab.com, subject welcome]);参数说明xReadGroup的消费者名worker-1建议带上机器名或进程号多实例部署时才能在 pending 列表里分清是谁卡住了不能换成0换成0会把历史消息重读一遍。处理失败不XACK消息会进入 PEL之后用一个补偿脚本调XAUTOCLAIM把闲置超过 N 分钟的消息转给还活着的消费者否则某个 worker 崩掉之后那批消息就永远卡住了。提示xAdd的*让 Redis 自己生成时间戳 ID需要自己控制顺序时才手写 ID手写容易撞号。4.4 相隔月数与 Excel 批量处理两个高频业务函数计算两个日期相差几个月看起来简单边界却容易错。function monthsBetween(string $a, string $b): int { $d1 new DateTimeImmutable($a); $d2 new DateTimeImmutable($b); $diff $d1-diff($d2); return $diff-y * 12 $diff-m; } echo monthsBetween(2024-01-31, 2024-02-29); // 0diff是按日历字段算的从 1 月 31 日到 2 月 29 日天数够了但「整月」不满返回 0 个月。业务上要按「过了一个月就算一个月」的话得自己拿天数补判断。用时间戳除以 2592000 那种写法更不能用不同月份天数不一样结果会漂。Excel 批量导入的坑在内存。用 PhpSpreadsheet 整表load一份几万行的文件随随便便就吃掉几百兆。行数超过一万时先另存为 CSV 再流式读$fh fopen($file, r); fgetcsv($fh); // 跳过表头 $batch []; while (($row fgetcsv($fh)) ! false) { $batch[] [(int)$row[0], trim($row[1])]; if (count($batch) 500) { // 攒够 500 行批量写一次 insertBatch($pdo, $batch); $batch []; } } if ($batch) { insertBatch($pdo, $batch); } fclose($fh);逻辑说明fgetcsv一次只读一行内存占用基本恒定分批写库比逐行 insert 少两个数量级的往返但每批别超过一千行否则单条 SQL 长度会撞上max_allowed_packet。CSV 里的逗号、引号交给fgetcsv的默认参数处理自己explode一定出错。5. 错误处理、跨域与反序列化上线前自己先打一遍的检查项代码写完不算完要确认出错时系统是可控的、跨域是配对的、外部输入是不能被反序列化利用的。先把错误接管起来?php declare(strict_types1); error_reporting(E_ALL); ini_set(display_errors, 0); // 生产环境不往页面喷错误 ini_set(log_errors, 1); set_error_handler(function (int $no, string $msg, string $file, int $line): bool { throw new ErrorException($msg, 0, $no, $file, $line); }); set_exception_handler(function (Throwable $e): void { error_log(sprintf([%s] %s in %s:%d, get_class($e), $e-getMessage(), $e-getFile(), $e-getLine())); http_response_code(500); echo json_encode([code 500, msg server error]); });把 warning 提升成异常能让「数组键不存在」这类问题在测试阶段就炸出来异常处理器兜住所有未捕获异常接口永远返回 JSON前端不会突然收到一页 HTML 报错。跨域配置集中在入口文件$origin $_SERVER[HTTP_ORIGIN] ?? ; $allow [https://a.example.com, https://b.example.com]; if (in_array($origin, $allow, true)) { header(Access-Control-Allow-Origin: . $origin); header(Vary: Origin); header(Access-Control-Allow-Credentials: true); } if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; }注意Access-Control-Allow-Origin: *与Allow-Credentials: true不能同时出现浏览器会直接拒绝in_array记得带第三个参数true。反序列化这块只记一条规则用户能控制的字符串绝不给unserialize。风险点加固做法直接反序列化请求参数换json_decode只支持数组和标量缓存里存对象存 JSON或用allowed_classes白名单路径参数带phar://校验协议白名单只允许 http/https/本地路径中文数据跨语言传递用 JSON 而非 PHP serialize必须反序列化时白名单是唯一可接受的方式$data unserialize($raw, [allowed_classes [DateTimeImmutable::class]]);allowed_classes写false表示不允许任何对象只还原数组和标量这是缓存场景下最省心的写法。上线前拿自己的接口当攻击者打一遍传畸形 JSON、传超长字符串、改Content-Type、把文件名里的扩展名换成大小写混合看返回的是业务错误码还是一页调用栈。本文还有配套的精品资源点击获取
返回列表