0-introduction
欢迎与讲师介绍
- 课程名称: PHP Fundamentals (PHP 基础)
- 讲师: Maximiliano Firtman (可以称呼 Max 或 Maxi)
- 网站: third.dev
- 背景:
- 来自阿根廷,现居布宜诺斯艾利斯。
- 移动 Web 开发者。
- 1996 年开始 Web 开发 (HTML, CSS, JavaScript)。
- 早期后端经验:ASP (使用 VBScript)。
- 1999 年开始使用 PHP (从 PHP 3 开始)。
- 本次课程将讨论 PHP 8。
- 开发了超过 150 个 Web 应用 (并非全部使用 PHP)。
- 著有 13 本书,大量关于 Web 开发、前端、移动应用开发和 Web 性能的课程。
- 最新著作:《Learn PWA》(由 Google 在 web.dev 发布,免费)。
- 在 Frontend Masters 上的内容:前端、PWA、原生 JavaScript、API、移动应用课程 (Kotlin, Swift, Dart with Flutter) 以及一些后端课程 (如 Go)。
课程内容概览
- 什么是 PHP,为什么选择 PHP?
- PHP 语法 (从零开始)。
- 超全局变量 (Superglobal variables)。
- PHP 的编程范式 (与其他语言如 JavaScript, Java, Python 的比较)。
- PHP 的面向对象编程 (OOP)。
- 处理数据:
- 小型项目实战。
- 使用 JSON 数据库。
- 构建 API。
- 高级技巧、调试和最佳实践。
课程先决条件
- 任何语言的基础编程经验 (例如 JavaScript, C 语言都可以)。
- 一些 OOP (面向对象编程) 概念 (例如了解什么是类 class, 超类 superclass)。
- 基础 HTML 知识 (不会写很多 HTML,但会涉及)。
所需工具
- PHP 解释器: (php.net, 免费, 开源)。稍后会解释如何安装。
- 浏览器: 任何浏览器均可。
- 代码编辑器: 讲师将使用 VS Code,但任何其他编辑器也可以。
1-why-php
什么是 PHP?
- 定义 (来自 Wikipedia): PHP 是一种广泛使用的开源脚本语言,特别适用于 Web 开发,并且可以嵌入到 HTML 中。
- 通常指服务器端脚本语言。
PHP 的市场份额与应用
- 惊人的市场份额:76%
- 在所有已知服务器端编程语言的网站中,PHP 占据了 76% 的份额。
- 这个数字可能让年轻开发者惊讶,因为感觉上 PHP 似乎不再流行,但事实恰恰相反。
- 数据解读:
- 在小型网站中的渗透率高于大型创业公司或银行。
- 大型网站仍在使用 PHP:
- Meta (Facebook, Instagram 部分)。
- Wikipedia。
- WordPress 的影响:
- 76% 的数据中,约 40% 可能来自 WordPress。WordPress 是最流行的 CMS (内容管理系统),它基于 PHP。
- 这意味着并非 76% 的 Web 开发者都在写 PHP,很多 WordPress 网站的拥有者可能从未写过一行 PHP 代码。
- 但如果你需要维护或扩展 WordPress (如创建主题、插件),就需要了解 PHP。
基于 PHP 的流行 CMS 和框架
- CMS: WordPress, Drupal, Joomla 等。
- 开发框架: Magento, Symfony, Laravel, CodeIgniter 等。
- 这些是开发者使用的 PHP 框架,类似于 Node.js 的 Express.js 或 Python 的 Django。
- 本次课程将使用原生 PHP (Vanilla PHP),不涉及框架。
PHP 的其他用途
- 命令行脚本:
- 可以用于计算机脚本任务,如清理数据、执行备份等,完全独立于 Web。
- (尽管 99.9% 的 PHP 用途是 Web 后端)。
- 桌面应用程序 (PHP-GTK):
- 一个非官方但相关的框架,允许用 PHP 创建跨平台 (Windows, Mac, Linux) 的桌面应用。
- 界面风格可能看起来比较老旧 (源于 90 年代)。
- 实验性项目:
- php-wasm: PHP 的 WebAssembly 解释器,可以在浏览器中客户端运行 PHP 代码 (实验性质)。
- 将 PHP 编译为 WebAssembly 模块: 允许 JavaScript 调用编译后的 PHP 代码。
2-history-of-php
PHP 的起源与创造者
- 创造者: Rasmus Lerdorf,1993 年,当时是一名网页设计师。
- 初衷: 为他自己的个人网站创建一套工具。
- 早期名称: Personal Homepage Tools (PHP 工具)。
- PHP 名称的演变:
- 最初:Personal Homepage (个人主页)。
- 现在:PHP: Hypertext Preprocessor (一个递归缩写,P 代表 PHP)。
PHP 的设计哲学与影响
- 非开发者导向的初衷:
- Rasmus Lerdorf 当时并非专业程序员。
- PHP 最初并非设计为一种编程语言,而是为网页设计师提供快速创建动态内容的工具集。
- 这是 PHP 语言内部不一致性存在的原因之一 (例如函数命名规则不统一,如
is_nullvsisset)。
- 灵感来源: Perl, C, Java, JavaScript 等。
- 语言的不一致性:
- 由于其发展历程,PHP 在某些方面缺乏一致性,这可能让来自更规范语言的开发者感到困扰。
- 例如:函数
is_null()和isset(),前者有下划线,后者没有。
- Facebook 的早期与 PHP:
- Mark Zuckerberg 使用 PHP 创建了 facebook.com。
- Rasmus Lerdorf 曾评价其早期代码“非常糟糕”,但也为这段“糟糕”的代码能发展成一家数十亿美元的公司而自豪,这体现了 PHP 的某种力量:即使是初级开发者也能快速构建可运行的应用。
PHP 版本历史里程碑
- PHP 3 (约 1998 年): 第一个被广泛使用的版本,也是讲师接触的第一个版本。
- PHP 4 (约 2000 年): 使 PHP 大受欢迎的版本。
- 令人惊讶的是,PHP 4 至今仍未被弃用 (仅 PHP 3 被弃用),仍在接受安全和错误修复。
- PHP 5 (约 2004 年): 一个重大的转变,与 PHP 4 不完全兼容,修复了许多安全问题。
- PHP 6: 从未发布。
- PHP 7 (约 2015 年): 在 PHP 5 之后大约 10 年发布。
- PHP 8 (约 2020 年): 当前版本(疫情期间发布)。
- 移除了许多历史遗留的“糟糕”语法。
- 正努力成为一门更好的编程语言。
PHP 的吉祥物:大象 (elePHPant)
- 形象: 一只大象。
- 灵感来源: 设计师观察大写字母 "PHP" 的形状,觉得像大象的脸和身体。
- 名称: ElePHPant (将 PHP 嵌入其中)。
- (讲师幽默地提到,现在人们可能因为 PHP 历史悠久、体量大而觉得它像大象,但这并非最初的含义。)
3-php-language-features
PHP 的语言类型定位
- 编译型语言 (Compiled Languages): 编写代码,交付机器码 (如 C, C++, Go)。
- 字节码语言 (Bytecode Languages): 编写代码,编译成中间语言/字节码 (如 C#, Java)。
- 解释型/脚本语言 (Interpreted/Scripting Languages): 直接交付源代码 (如 PHP, JavaScript)。
- PHP 属于这一类,部署时直接将源代码发布到服务器。
PHP 的主要特性
- 非常灵活 (Very flexible)。
- 跨平台 (Platform independent): 解释器可用于 Unix, Windows, Linux, Mac,甚至有 Android 和 JavaScript (WASM) 中的解释器。
- 广泛的数据库支持 (Wide database support): 几乎支持所有数据库。
- 开源 (Open source)。
- 多范式 (Multi-paradigm):
- 支持面向对象编程 (OOP),但非强制。
- 也可以编写过程式代码 ("spaghetti code") 并且能够运行。
- 丰富的标准库 (Rich standard library):
- 提供了大量的全局函数。
- 这也是批评点之一:函数命名不一致 (如
is_nullvsisset)。 - 同一个功能可能有多种实现方式。
- 内置会话管理 (Built-in session management):
- PHP 的一大优势,能够轻松在服务器端跨请求存储和跟踪用户会话数据 (如
$_SESSION)。 - 内部通过 Cookie 和会话 ID 实现。
- PHP 的一大优势,能够轻松在服务器端跨请求存储和跟踪用户会话数据 (如
- 快速开发 (Rapid development):
- 能快速地将想法转化为可运行的应用 (MVP)。
- 上手简单,几行代码就能工作。
为什么现在学习 PHP 基础?
- Web 的重要组成部分: 仍然是简历上的一个好技能。
- 维护遗留 Web 应用: 大量现有应用使用 PHP 构建。
- 扩展流行 CMS: 如 WordPress, Joomla, Drupal,自定义主题和插件需要 PHP。
- 使用现代 PHP 框架: 如 Symfony, Laravel, CodeIgniter,这些框架在 PHP 开发者中非常流行。
4-installation-setup
开发工具与 IDE
- VS Code:
- 默认不支持 PHP,需要安装扩展。
- 讲师推荐 "PHP Debug" 等扩展。
- 注意:一些扩展可能有免费版和付费 Pro 版,免费版可能会有升级提示。
- PhpStorm: 经典的 PHP IDE,功能强大,拥有忠实用户群。
- NetBeans / Eclipse PDT: 在企业级 PHP 开发中仍有使用。
- Zend Studio: 付费 IDE,主要面向企业用户和大型项目。
LAMP 技术栈简介
- 一个曾经非常流行的后端技术栈组合:
- Linux (操作系统)
- Apache (Web 服务器)
- MySQL (数据库,现在也可能是 MariaDB)
- PHP (编程语言)
- 注意:这只是一个经典的组合,如今 PHP 可以与各种技术搭配使用。本次课程仅关注 PHP 本身。
安装与设置 PHP
- 获取 PHP:
- 生产环境: 可以从 php.net 下载,甚至从源码编译。
- 开发环境 (推荐): 使用包管理器。
- Mac: 使用 Homebrew:
brew install php。 - Windows: XAMPP 是一个推荐的集成环境包,它会安装 Apache, MariaDB, PHP 和 Perl。XAMPP 也可用于 Mac 和 Linux。
- Linux: 使用系统自带的包管理器 (如 apt, yum)。
- Mac: 使用 Homebrew:
- 验证安装: 打开终端,输入
php -v,应能看到 PHP 版本信息。 - 其他方式:
- 通过 Docker 安装。
- 使用其他 LAMP/WAMP/MAMP 集成安装包。
- 重要: Mac 上安装 XAMPP 可能因签名问题遇到安全提示,需要手动允许。
Web 服务器
- PHP 脚本通常由 Web 服务器 (如 Apache, Nginx, Lightspeed, IIS) 调用来处理 HTTP 请求。
- PHP 本身不是 Web 服务器,它负责处理请求并生成响应。
- 大多数云服务提供商都支持 PHP 脚本的执行。
PHP 在现代托管平台 (如 Vercel)
- 像 Vercel 这样的现代托管平台也支持 PHP(例如通过 Serverless Functions)。
- 这意味着 PHP 也可以集成到现代化的开发和部署流程中,不仅仅是传统的 FTP 上传方式。
5-php-tags
VS Code PHP 扩展
- 安装 PHP 相关扩展可以获得语法高亮、调试等功能。讲师使用的是 "PHP Debug"。
运行 PHP 脚本
- 命令行运行:
- 打开终端或命令提示符。
- 进入 PHP 文件所在目录。
- 执行命令:
php your_script_name.php - 例如:
php test.php
- 注意:这只是在命令行执行脚本,尚未涉及 Web 服务器。
PHP 标记 (Tags)
-
标准标记:
<?php // PHP code goes here ?> -
文件默认行为:
- 在一个
.php文件中,位于<?php ... ?>标记之外的任何文本都会被 PHP 解释器直接输出。 - 这源于 PHP 最初被设计为嵌入 HTML 中的语言。
- 在一个
-
echo命令:- 用于输出字符串或变量内容。
echo "Hello World";echo是一个语言结构而非真正的函数,所以括号是可选的:echo("Hello");也是合法的。
混合输出与 PHP 代码
-
可以在一个 PHP 文件中混合使用直接输出的文本 (如 HTML) 和 PHP 代码块。
<p>This is static text.</p> <?php echo "<p>This is dynamic text from PHP.</p>"; ?> <p>More static text.</p>
省略 PHP 结束标记 ?> 的情况
- 条件: 如果一个
.php文件的末尾 只有 PHP 代码,没有任何后续的 HTML、文本或空格。 - 原因:
- 防止在
?>之后意外地输出空格或换行符。 - 这些意外的输出可能会在某些情况下导致问题,例如在发送 HTTP 头部信息 (headers) 之前输出了内容,会导致 "headers already sent" 错误。
- 防止在
- 最佳实践:
- 对于纯 PHP 文件 (例如类文件、配置文件等,它们不直接产生 HTML 输出),推荐省略文件末尾的
?>。 - 一些代码规范检查工具 (linter) 可能会提示你移除它。
- 对于纯 PHP 文件 (例如类文件、配置文件等,它们不直接产生 HTML 输出),推荐省略文件末尾的
6-variables
变量定义与基本规则
-
美元符号
$开头: 所有变量都以$符号开始。- 例如:
$name = "Max"; - 例如:
$year = 2024;
- 例如:
-
无需声明关键字: 定义变量时不需要像 JavaScript 中的
var,let,const或 C# 中的类型声明。直接赋值即可。 -
分号
;结尾: PHP 中的大多数语句 (包括变量赋值) 都需要以分号结束。- 唯一的例外是:如果 PHP 代码块是文件的最后一部分,并且使用了结束标记
?>,那么紧邻?>前的最后一条 PHP 语句可以省略分号。但通常建议总是加上分号。
- 唯一的例外是:如果 PHP 代码块是文件的最后一部分,并且使用了结束标记
-
动态类型: PHP 是一种动态类型语言。变量的类型是在运行时根据赋给它的值决定的,并且可以在脚本执行过程中改变。
-
例如:
$value = 100; // $value 是整数 $value = "Hello"; // $value 现在是字符串 $value = true; // $value 现在是布尔值
-
大小写敏感性
- 变量名:大小写敏感。
$name和$Name被视为两个不同的变量。
- 内建常量 (
true,false,null):大小写不敏感。true,TRUE,True都表示布尔真值。null,NULL,Null都表示空值。- (但函数名、类名等通常是大小写不敏感的,这体现了 PHP 的一些不一致性)。
注释
-
单行注释:
// 这是一个单行注释# 这也是一个单行注释 (Shell 风格,较少见于 PHP)
-
多行注释:
/* 这是一个 多行注释块 */
可变变量 (Variable Variables)
-
语法: 使用两个美元符号
$$variableName。 -
解释:
-
第一个变量 (
$variableName) 的值会被用作第二个变量的名称。 -
例如:
$foo = "bar"; $$foo = "Hello World"; // 这等价于 $bar = "Hello World"; echo $bar; // 输出: Hello World echo ${$foo}; // 也可以这样访问,输出: Hello World
-
-
用途与风险:
- 非常灵活,允许动态地创建和访问变量。
- 但会使代码难以阅读和调试,并可能引入安全风险(如果变量名来自不可信的输入)。
- 在实际开发中很少使用。
7-strings
字符串定义
-
PHP 支持多种定义字符串的方式。
-
1. 单引号 (
'...'):-
行为: 字符串内容按字面解释。
-
变量内插: 不进行变量内插。变量名会作为普通文本输出。
$name = "Max"; echo 'Hello $name'; // 输出: Hello $name -
转义序列: 只识别两个转义序列:
\':输出单引号本身。\\:输出反斜杠本身。- 其他如
\n(换行符) 会被视为普通文本\n。
-
性能: 通常比双引号字符串略快,因为 PHP 不需要扫描其中的变量。
-
-
2. 双引号 (
"..."):-
行为: 会解析字符串内容。
-
变量内插: 进行变量内插。字符串中的变量会被其值替换。
$name = "Max"; echo "Hello $name"; // 输出: Hello Max // 对于复杂变量 (如数组元素或对象属性),建议使用花括号包裹: echo "User: {$user['name']}"; echo "Value: {$obj->property}"; -
转义序列: 识别多种转义序列,如:
\n:换行符\r:回车符\t:制表符\$:美元符号本身\":双引号本身\\:反斜杠本身
-
表达式内插: 可以使用
{}包裹更复杂的表达式。花括号用途:主要用于复杂变量和消除歧义,不支持表达式计算。 // 复杂变量示例 echo "User: {$user['name']}"; echo "Value: {$obj->property}"; // 消除歧义示例 $fruit = 'apple'; echo "I like {$fruit}s"; // 输出: I like apples // 表达式需要在字符串外计算 $sum = $a + $b; echo "Sum: $sum";
-
字符串拼接
-
使用点操作符 (
.) 进行字符串拼接。$firstName = "Max"; $lastName = "Firtman"; $fullName = $firstName . " " . $lastName; echo $fullName; // 输出: Max Firtman
Heredoc 语法
-
用途: 定义复杂的多行字符串,行为类似双引号字符串 (支持变量内插和转义序列)。
-
格式:
$identifier = "PHP"; $heredocString = <<<EOT This is a multi-line string. Welcome to the world of $identifier! You can use "double quotes" and 'single quotes' freely. Newlines are preserved. EOT; // 注意: // 1. <<< 后面紧跟一个标识符 (如 EOT, HTML, SQL 等,通常大写)。 // 2. 标识符后不能有任何空格或字符,直接换行。 // 3. 字符串内容开始。 // 4. 结束标识符必须单独一行,且顶格书写 (前面不能有任何空格或制表符),其后可以紧跟分号。 -
示例:
$title = "PHP Fundamentals"; $message = <<<MSG Welcome to "$title". This course covers the basics of PHP. Enjoy your learning! MSG; echo $message;
Nowdoc 语法
-
用途: 定义复杂的多行字符串,行为类似单引号字符串 (行为类似单引号字符串,完全按字面意思处理所有内容)。
-
格式:
$nowdocString = <<<'EOT' This is a multi-line string. Variables like $identifier will NOT be parsed. \n will also be treated literally. EOT; // 注意: // 1. <<< 后面紧跟一个用单引号包裹的标识符 (如 'EOT')。 // 2. 其他规则与 Heredoc 类似 (标识符后直接换行,结束标识符单独一行顶格)。 -
示例:
$codeExample = <<<'CODE' <?php $name = "World"; echo "Hello $name"; // This $name will be literal ?> CODE; echo $codeExample;
8-arrays
数组定义
PHP 中的数组实际上是一种有序映射 (ordered map)。映射是一种把 values 关联到 keys 的类型。
-
1. 旧语法
array()构造函数:$numbers = array(1, 2, 3, 4, 5); $colors = array("red", "green", "blue"); -
2. 短数组语法
[](PHP 5.4+,推荐使用):$numbers = [1, 2, 3, 4, 5]; $colors = ["red", "green", "blue"];
数组类型
PHP 的数组非常灵活,可以作为多种数据结构使用:
-
1. 索引数组 (Numeric Indexed Arrays):
- 数组的键是默认的数字索引,从 0 开始。
$fruits = ["Apple", "Banana", "Cherry"]; // $fruits[0] is "Apple" // $fruits[1] is "Banana" // $fruits[2] is "Cherry" -
2. 关联数组 (Associative Arrays):
- 数组的键是自定义的字符串 (或数字)。类似于其他语言中的字典 (dictionary)、哈希表 (hash map) 或对象 (object literal)。
- 使用
=>操作符来分隔键和值。
$person = [ "name" => "Maximiliano", "age" => 30, // 假设年龄 "city" => "Buenos Aires" ]; // $person["name"] is "Maximiliano" // $person["age"] is 30 -
3. 混合数组:
- PHP 数组可以混合数字索引和关联键。
$mixedArray = [ "Apple", // 索引 0 "type" => "fruit", "Banana", // 索引 1 (PHP 会自动分配下一个可用数字索引) "color" => "yellow" ]; // echo $mixedArray[0]; // "Apple" // echo $mixedArray["type"]; // "fruit" // echo $mixedArray[1]; // "Banana"讲师示例中提到一个元素有键,另一个没有:
$list = ['id' => 123, 10]; // 10 的键是 0,因为 'id' 不是数字 $list_v2 = [0 => 'value0', 'id' => 123, 'value_next_numeric_idx']; // 'value_next_numeric_idx' 的键是 1
访问数组元素
-
使用方括号
[]和键来访问数组元素。$fruits = ["Apple", "Banana", "Cherry"]; echo $fruits[1]; // 输出: Banana $person = ["name" => "Max", "age" => 30]; echo $person["name"]; // 输出: Max
获取数组长度/元素数量
-
使用内置的
count()函数来获取数组中元素的数量。$numbers = [10, 20, 30, 40]; echo count($numbers); // 输出: 4 $person = ["name" => "Max", "city" => "BA"]; echo count($person); // 输出: 2 -
注意: 不是像某些语言那样的
$array->length或$array->size属性。
数组长度 vs. 字符串长度
- 获取数组长度:
count($array) - 获取字符串长度:
strlen($string) - 这体现了 PHP 标准库中函数命名和功能上的一些不一致性。
9-loops
PHP 提供了多种循环结构来重复执行代码块。
1. for 循环
-
传统的 C 风格
for循环,通常用于已知迭代次数的场景。 -
语法:
for (initialization; condition; increment/decrement) { // code to be executed } -
示例: 遍历索引数组
$colors = ["Red", "Green", "Blue"]; $count = count($colors); // 获取数组长度,避免在每次循环中重复计算 for ($i = 0; $i < $count; $i++) { echo $colors[$i] . "\n"; } // 注意变量 $i 需要美元符号 $
2. foreach 循环
-
专门用于遍历数组和对象的元素,是遍历数组最常用和推荐的方式。
-
语法 1:遍历值
foreach ($array as $value) { // code to be executed using $value } -
语法 2:遍历键和值
foreach ($array as $key => $value) { // code to be executed using $key and $value } -
示例:
$fruits = ["Apple", "Banana", "Cherry"]; foreach ($fruits as $fruit) { echo $fruit . "\n"; } $person = ["name" => "Max", "age" => 30]; foreach ($person as $attribute => $data) { echo $attribute . ": " . $data . "\n"; } -
注意顺序: 是
$collection as $item,这与某些语言 (如 JavaScript 的for...offor (item of collection)) 的元素和集合顺序相反。
3. while 循环
-
只要指定的条件为真,
while循环就会重复执行代码块。 -
语法:
while (condition) { // code to be executed } // 条件两边的括号是必需的 -
示例:
$i = 0; while ($i < 3) { echo "Number: " . $i . "\n"; $i++; }
4. do-while 循环
-
与
while循环类似,但它会先执行一次代码块,然后再检查条件。这意味着代码块至少会执行一次。 -
语法:
do { // code to be executed } while (condition); -
示例:
$i = 5; do { echo "Number (do-while): " . $i . "\n"; // 这行会执行一次 $i++; } while ($i < 3); // 条件 (5 < 3) 为假,循环结束
控制结构的替代语法 (Alternative Syntax)
-
当 PHP 代码嵌入到 HTML 中时,为了提高可读性,可以使用一种替代的控制结构语法。将花括号
{}替换为冒号:和相应的end...;语句。 -
适用于
if,while,for,foreach,switch。 -
foreach替代语法示例:<?php $items = ["Book", "Pen", "Laptop"]; ?> <ul> <?php foreach ($items as $item): ?> <li><?php echo htmlspecialchars($item); ?></li> <?php endforeach; ?> </ul> -
if替代语法示例:<?php $isLoggedIn = true; ?> <?php if ($isLoggedIn): ?> <p>Welcome back!</p> <?php else: ?> <p>Please log in.</p> <?php endif; ?> -
这种语法在模板文件中尤其有用,因为它能更清晰地分离 HTML 结构和 PHP 逻辑。
10-functions
函数是可重复使用的代码块,用于执行特定任务。
函数定义与调用
-
定义语法:
function functionName(parameter1, parameter2, ...) { // code to be executed // optional: return value; }- 使用
function关键字。 - 函数名通常使用驼峰式 (camelCase) 或下划线式 (snake_case)。函数名大小写不敏感 (但不推荐依赖此特性)。
- 参数 (parameters) 在括号内定义,以
$开头。 - 函数体包含在花括号
{}内。 - 可以使用
return语句返回一个值。如果函数没有return语句,或者return;不带值,则默认返回null。
- 使用
-
调用语法:
functionName(argument1, argument2, ...);- 通过函数名后加括号来调用。
- 传递的实际值称为参数 (arguments)。
-
示例:
// 定义一个简单的问候函数 function greet($name) { echo "Hello, " . $name . "!"; } greet("Max"); // 调用函数,输出: Hello, Max! // 定义一个带返回值的加法函数 function add($num1, $num2) { $sum = $num1 + $num2; return $sum; } $result = add(5, 3); echo "\\nSum is: " . $result; // 输出: Sum is: 8
类型提示 (Type Hinting / Type Declarations)
-
PHP 7+ 引入了对函数参数和返回值的类型声明。这有助于提高代码的健壮性和可读性。
-
如果传递的参数或返回的值与声明的类型不匹配 (且无法安全转换),PHP 会抛出一个
TypeError(除非开启了strict_types=1,否则 PHP 会尝试类型转换)。 -
参数类型提示:
function calculateTotalPrice(float $price, int $quantity): float { return $price * $quantity; } // $total = calculateTotalPrice(10.5, 3); // 正确 // $total = calculateTotalPrice("10.5", "3"); // 在非严格模式下可能工作 (PHP尝试转换) // 在 declare(strict_types=1); 下会抛出 TypeError -
返回值类型提示: 在函数定义的括号后使用冒号
:指定返回类型。function getGreeting(string $name): string { return "Hello, " . $name; } function logMessage(string $message): void { echo $message; } -
常用类型:
-
标量类型:
int,float,string,bool -
复合类型:
array,object,iterable(可以是数组或实现了Traversable接口的对象) -
特殊类型:
callable:可调用的,如函数名字符串、数组[$object, 'method']或闭包。self:表示当前类 (在类方法中使用)。parent:表示父类 (在类方法中使用)。void:表示函数不返回值 (PHP 7.1+)。mixed:表示任何类型 (PHP 8.0+)。static:用于后期静态绑定中的返回类型 (PHP 8.0+)。
-
可空类型 (Nullable Types): 在类型前加问号
?,表示参数或返回值可以是指定类型或null(PHP 7.1+)。function findUser(int $id): ?User { // User 是一个类名 // ... 查找用户 // return $user_object; or return null; }
-
默认参数值 (Default Argument Values)
-
可以为函数参数指定默认值。如果调用函数时未提供该参数,则使用默认值。
-
带默认值的参数应放在参数列表的末尾。
function setPower(int $level, string $unit = "MW") { echo "Power set to: " . $level . " " . $unit; } setPower(100); // 输出: Power set to: 100 MW setPower(50, "kW"); // 输出: Power set to: 50 kW
命名参数 (Named Arguments) (PHP 8.0+)
-
允许在调用函数时通过参数名指定值,这样参数的顺序就不重要了,并且可以跳过有默认值的可选参数。
-
语法:
parameterName: valuefunction createUser(string $username, string $email, bool $isActive = true, string $role = "subscriber") { echo "User: $username, Email: $email, Active: " . ($isActive ? 'Yes':'No') . ", Role: $role\\n"; } // 使用命名参数 createUser(username: "Maxi", email: "[email protected]"); // 输出: User: Maxi, Email: [email protected], Active: Yes, Role: subscriber createUser(email: "[email protected]", username: "JaneD", role: "admin"); // 输出: User: JaneD, Email: [email protected], Active: Yes, Role: admin (顺序不重要) createUser(username: "inactiveUser", email: "[email protected]", isActive: false); // 输出: User: inactiveUser, Email: [email protected], Active: No, Role: subscriber -
混合位置参数和命名参数:
- 位置参数必须在命名参数之前。
- 一旦使用了命名参数,其后的所有参数都必须是命名参数 (如果提供的话)。
// function calculateTax(float $price, float $taxRate = 0.05, string $taxName = "VAT") calculateTax(3000, taxName: "State Tax"); // 正确:第一个是位置参数,后续是命名参数 // calculateTax(price: 3000, 0.10); // 错误:命名参数后不能是位置参数
变量命名约定
- PHP 中对于变量和函数参数的命名约定比较灵活,常见的有:
- 驼峰式 (camelCase):
$taxName,$calculateTotal - 下划线式/蛇形 (snake_case):
$tax_name,$calculate_total
- 驼峰式 (camelCase):
- PHP 内置函数库本身就混合使用了这两种风格。
- 最重要的是在你的项目或团队中保持一致性。
- 烤串式 (
kebab-case,如$tax-name) 不允许用于 PHP 变量名,因为 会被解析为减号。
11-starting-a-php-development-server
PHP 与 Web 服务器
-
PHP 本身不是服务器: PHP 核心功能是执行脚本并输出结果。
-
需要 Web 服务器: 为了通过 Web (浏览器) 访问 PHP 脚本,通常需要一个 Web 服务器 (如 Apache, Nginx)。
-
PHP 内置开发服务器 (Development Server):
-
PHP 提供了一个仅用于开发目的的内置 Web 服务器。
-
启动命令: 在终端中,进入你的项目文件夹,运行:
php -S localhost:4000 # 或者 php -S 0.0.0.0:4000 (允许其他设备在同一网络中访问) # -S (大写 S) 代表 Serve # localhost 是主机名 # 4000 是端口号 (可以选择其他未被占用的端口) -
警告: 此服务器不适用于生产环境。生产环境应使用 Apache, Nginx 等更健壮的服务器。
-
使用内置服务器
-
启动服务器后,可以通过浏览器访问
http://localhost:4000。 -
默认文件:
- 如果服务器在根目录找不到默认文件,通常会显示 "Not Found" 或类似错误。
- Web 服务器通常会寻找名为
index.html或index.php的文件作为默认入口。
-
提供静态文件: PHP 内置服务器也可以提供静态文件 (如
.html,.css,.js文件)。-
创建一个
index.html文件,内容如下 (可使用 Emmet 快速生成): 刷新浏览器,应该能看到 HTML 内容。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>PHP Test</title> </head> <body> <h1>Hello from HTML!</h1> </body> </html> ```
-
.php 文件与 HTML
- 将
index.html重命名为index.php。 - 如果
index.php文件内容仍然是纯 HTML,浏览器依然会正确显示它。- 这表明 PHP 文件默认可以输出 HTML。
- 嵌入 PHP 代码:
<!DOCTYPE html> <html lang="en"> <head> <title>PHP Page</title> </head> <body> <h1>Welcome!</h1> <p> <?php echo "This message is from PHP! The current date is " . date('Y-m-d'); ?> </p> </body> </html>
服务器端渲染 (Server-Side Rendering - SSR)
- 当浏览器请求一个
.php文件时:- Web 服务器将请求传递给 PHP 解释器。
- PHP 解释器执行文件中的 PHP 代码。
- PHP 代码生成的任何输出 (通常是 HTML) 会与文件中的静态 HTML 结合。
- 最终生成的完整 HTML 页面被发送回浏览器。
- 查看页面源代码: 在浏览器中查看页面源代码,看不到 PHP 代码本身,只能看到 PHP 执行后生成的最终 HTML。
- 这与一些现代 Web 框架中的模板引擎 (如 Express.js 的 Pug/EJS,Django 的模板) 的概念类似。PHP 本身就具备模板能力。
HTTP 请求与响应
- 浏览器与服务器之间的通信遵循 HTTP 协议。
- 请求 (Request): 浏览器向服务器发送请求 (包括 URL、方法如 GET/POST、头部信息等)。
- 响应 (Response): 服务器向浏览器返回响应 (包括状态码如 200 OK/404 Not Found、头部信息、响应体内容等)。
- 可以使用浏览器开发者工具的 "Network" (网络) 标签页查看这些请求和响应的详细信息。
PHP 的请求处理模式:每个 URL 对应一个脚本
- 基本模式: 默认情况下,PHP 的工作方式是每个 URL 直接映射到一个服务器上的
.php文件。- 请求
http://localhost:4000/about.php会执行服务器上名为about.php的文件。
- 请求
- 路由 (Routing):
- 现代 Web 应用通常使用路由系统,使得 URL 更加友好 (例如
http://example.com/users/profile而不是http://example.com/users_profile_script.php)。 - 这种“友好 URL”或路由功能不是 PHP 语言本身提供的,而是通过:
- Web 服务器配置: 例如 Apache 的
mod_rewrite或 Nginx 的配置,将友好 URL 内部重写或代理到实际的.php脚本。 - PHP 框架: 像 Laravel, Symfony 这样的框架内置了强大的路由组件。
- Web 服务器配置: 例如 Apache 的
- 现代 Web 应用通常使用路由系统,使得 URL 更加友好 (例如
- 移除
.php扩展名: 从 URL 中移除.php扩展名是 Web 服务器配置的任务,不是 PHP 本身的问题。
12-creating-a-form
项目目标:加密货币转换器
- 创建一个简单的 Web 页面,允许用户输入加密货币数量和类型 (如比特币),然后将其转换为美元。
- 将使用一个公开的免费 API 来获取实时汇率。
- API 端点示例:
https://api.frontendmasters.com/api/crypto/btc(获取比特币对美元的汇率)
- API 端点示例:
步骤 1:设置项目和启动开发服务器
-
创建项目文件夹 (例如
crypto-converter)。 -
在项目文件夹中创建一个
index.php文件。 -
在项目文件夹的根目录下打开终端,启动 PHP 内置开发服务器:
php -S localhost:4000
步骤 2:创建 HTML 表单 (index.php)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crypto Masters</title>
<!-- 稍后可以添加 CSS -->
</head>
<body>
<h1>Crypto Converter</h1>
<form action="convert.php" method="post">
<p>
<label for="amount">Amount:</label>
<input type="number" id="amount" name="amount" step="any" required>
</p>
<p>
<label for="crypto">Crypto:</label>
<select id="crypto" name="crypto">
<option value="btc">Bitcoin (BTC)</option>
<option value="eth">Ethereum (ETH)</option>
<!-- 可以添加更多加密货币选项,如 sol (Solana) -->
</select>
</p>
<p>
<button type="submit">Convert</button>
<!-- 或者使用 <input type="submit" value="Convert"> -->
</p>
</form>
</body>
</html>
- HTML 结构:
- 一个简单的 HTML 页面。
- 一个
<h1>标题。 - 一个
<form>元素:action="convert.php":表单数据将提交到convert.php文件进行处理。method="post":表单数据将通过 HTTP POST 方法发送 (稍后会详细讨论 GET vs POST)。
- 输入字段:
- Amount (数量):
<label for="amount">:关联标签。<input type="number" id="amount" name="amount" step="any" required>:type="number":数字输入框。id="amount":用于<label>的for属性和 CSS/JS。name="amount":非常重要,这是服务器端用来识别该字段数据的键。step="any":允许输入小数。required:HTML5 表单验证,表示此字段必填。
- Crypto (加密货币类型):
<label for="crypto">:关联标签。<select id="crypto" name="crypto">:下拉选择框。id="crypto"。name="crypto":服务器端用来识别所选加密货币的键。
<option value="btc">Bitcoin (BTC)</option>:value="btc":当此选项被选中时,发送到服务器的值。
- 提交按钮:
<button type="submit">Convert</button>:点击后提交表单。- 或者
<input type="submit" value="Convert">,效果相同。
- Amount (数量):
- HTML 简洁性说明:
- 讲师提到,严格来说,HTML5 中
<head>,<body>,<html>标签在某些情况下是可选的,浏览器会自动补全。但为了结构清晰和最佳实践,通常都会包含它们。 - 本次示例中,为了简洁和聚焦 PHP,可能会省略一些标签。
- 讲师提到,严格来说,HTML5 中
- 当前状态:
- 此时
index.php仅包含 HTML,没有 PHP 代码。 - 在浏览器中访问
http://localhost:4000,应该能看到表单。 - 提交表单会尝试导航到
convert.php,但因为该文件尚不存在,会看到 404 错误。
- 此时
13-superglobal-variables-url-parameters
表单提交与数据传递
action属性:<form action="convert.php">指定了表单数据提交的目标 URL。method属性: 决定数据如何发送。- 如果未指定
method,或者method="get"(默认),数据会附加到 URL 的查询字符串中。 - 如果
method="post",数据会在 HTTP 请求体中发送。
- 如果未指定
HTTP GET 方法与 URL 参数
-
默认行为: 如果表单的
method未指定或为get,浏览器会将表单数据编码为 URL 参数(也称为查询字符串),附加到actionURL 之后。 -
name属性的重要性:-
HTML 表单控件 (如
<input>,<select>) 的name属性值将作为 URL 参数的键 (key)。 -
控件的值将作为 URL 参数的值 (value)。
-
示例: 如果
index.php中的表单代码是: 提交后,浏览器 URL 会变成类似:http://localhost:4000/convert.php?amount=3&crypto=btc<form action="convert.php" method="get"> <!-- 注意这里暂时改为 get 以演示 --> <input type="number" name="amount" value="3" /> <select name="crypto"> <option value="btc" selected>Bitcoin</option> </select> <button type="submit">Convert</button> </form> ``` - `?` 分隔主 URL 和查询字符串。 - `&` 分隔多个参数。 - `amount=3` 和 `crypto=btc` 是键值对。
-
PHP 超全局变量 (Superglobal Variables)
- PHP 提供了一系列预定义的、始终可用的数组变量,称为超全局变量。它们可以在脚本的任何作用域内访问,无需
global关键字。 - 这些变量通常以
$_(美元符号加下划线) 开头。 $_GET:- 一个关联数组,包含了通过 HTTP GET 方法传递给当前脚本的变量。
- 键是 URL 参数的名称,值是 URL 参数的值。
- 例如,对于 URL
convert.php?amount=3&crypto=btc:$_GET['amount']的值是字符串"3"。$_GET['crypto']的值是字符串"btc"。
在 convert.php 中接收 GET 参数
-
创建
convert.php文件。 -
在
convert.php中,可以使用$_GET来访问通过 URL 传递过来的数据。<?php // convert.php // 检查参数是否存在 (很重要,避免未定义索引的警告/错误) if (isset($_GET['amount']) && isset($_GET['crypto'])) { $amount = $_GET['amount']; // 获取 'amount' 参数的值 $crypto_code = $_GET['crypto']; // 获取 'crypto' 参数的值 // 输出,进行服务器端渲染 echo "<!DOCTYPE html><html><head><title>Conversion Results</title></head><body>"; echo "<h1>Conversion Results</h1>"; echo "<p>You want to convert $amount $crypto_code.</p>"; // 实际转换逻辑将在这里添加 echo "</body></html>"; } else { // 如果参数缺失,显示错误或提示 echo "<!DOCTYPE html><html><head><title>Error</title></head><body>"; echo "<h1>Error</h1>"; echo "<p>Required parameters (amount and crypto) are missing.</p>"; echo "<a href='index.php'>Try again</a>"; echo "</body></html>"; } ?>
- 代码解释:
isset($_GET['key']):检查$_GET数组中是否存在名为'key'的索引。这是避免因访问不存在的数组键而产生 "Undefined array key" 警告/错误的好习惯。- 将获取到的值赋给局部变量
$amount和$crypto_code。 - 使用
echo将包含这些动态值的 HTML 输出到浏览器。
- 服务器端渲染体现:
- PHP 代码在服务器上执行。
$amount和$crypto_code的值被嵌入到生成的 HTML 字符串中。- 浏览器接收到的是最终的 HTML,其中动态数据已经填充完毕。
- 查看浏览器中的页面源代码,会看到类似
<p>You want to convert 3 btc.</p>,而不是 PHP 代码。
- 直接通过 URL 访问:
- 由于数据是通过 URL 参数传递的,你可以直接在浏览器地址栏中构造 URL 来测试
convert.php,例如:http://localhost:4000/convert.php?amount=10&crypto=eth这会直接执行脚本并显示结果,无需通过index.php的表单。
- 由于数据是通过 URL 参数传递的,你可以直接在浏览器地址栏中构造 URL 来测试
14-post-parameters
HTTP POST 方法
- 目的: 当表单使用
method="post"时,表单数据会包含在 HTTP 请求的主体 (body) 中发送,而不是附加到 URL 上。 - 优点:
- 更安全 (相对 GET): 数据不会显示在 URL 中,不会被浏览器历史记录、服务器日志(以明文形式)等轻易暴露。但不意味着它是加密的,仍需 HTTPS 来保证传输安全。
- 无长度限制: URL 长度有限制,而 POST 请求体可以发送大量数据(如文件上传)。
- 非幂等性: POST 请求通常用于创建或修改资源,重复提交可能会产生不同结果。
修改表单以使用 POST
在 index.php 中,将 <form> 标签的 method 属性改为 post:
<form action="convert.php" method="post">
<!-- ... 其他表单元素保持不变 ... -->
</form>
PHP 超全局变量 $_POST
$_POST:- 一个关联数组,包含了通过 HTTP POST 方法传递给当前脚本的变量。
- 键是表单控件的
name属性值,值是控件提交的数据。
在 convert.php 中接收 POST 参数
修改 convert.php 以使用 $_POST 而不是 $_GET:
<?php
// convert.php
// 检查 POST 参数是否存在
if (isset($_POST['amount']) && isset($_POST['crypto'])) {
$amount = $_POST['amount'];
$crypto_code = $_POST['crypto'];
// 输出结果 (与之前 GET 示例类似,但现在从 $_POST 获取数据)
echo "<!DOCTYPE html><html><head><title>Conversion Results</title></head><body>";
echo "<h1>Conversion Results (POST)</h1>";
echo "<p>You want to convert $amount $crypto_code.</p>";
// 实际转换逻辑
echo "</body></html>";
} else {
// 如果参数缺失
echo "<!DOCTYPE html><html><head><title>Error</title></head><body>";
echo "<h1>Error</h1>";
echo "<p>Required POST parameters (amount and crypto) are missing.</p>";
echo "<a href='index.php'>Try again</a>";
echo "</body></html>";
}
?>
- 行为变化:
- 现在提交表单后,URL 将保持为
http://localhost:4000/convert.php,参数不会显示在地址栏。 - 如果直接在浏览器中访问
http://localhost:4000/convert.php(不通过表单提交),$_POST数组将为空,会触发else块的错误提示。
- 现在提交表单后,URL 将保持为
处理参数缺失的情况 (健壮性)
- 使用
isset()检查参数是否存在非常重要。 - 可以将逻辑包裹在
if (isset(...)) { ... } else { ... }结构中。
PHP 代码与 HTML 混合的替代语法 (再次提及)
讲师演示了如何使用替代语法来避免大量的 echo HTML 字符串,使得 PHP 和 HTML 的混合更易读:
<?php
// convert.php
if (isset($_POST['amount']) && isset($_POST['crypto'])) {
$amount = $_POST['amount'];
$crypto_code = $_POST['crypto'];
?>
<!DOCTYPE html>
<html>
<head><title>Conversion Results (POST)</title></head>
<body>
<h1>Conversion Results (POST)</h1>
<p>You want to convert <?php echo htmlspecialchars($amount); ?> <?php echo htmlspecialchars($crypto_code); ?>.</p>
<!-- 实际转换逻辑 -->
</body>
</html>
<?php
} else {
?>
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
<h1>Error</h1>
<p>Required POST parameters (amount and crypto) are missing.</p>
<a href='index.php'>Try again</a>
</body>
</html>
<?php
} // endif; // 如果使用 if (...): ... endif; 替代语法
?>
- 注意:
- 在 HTML 中输出变量时,使用
htmlspecialchars()是一个好习惯,可以防止 XSS (跨站脚本) 攻击。 - 讲师也提到了
if (...): ... else: ... endif;这种替代语法的可读性问题,尤其是在嵌套较多时。对于简单情况,直接echo或如上所示的 PHP 块切换可能更清晰。
- 在 HTML 中输出变量时,使用
其他超全局变量简介
$_REQUEST:- 一个关联数组,默认情况下包含了
$_GET、$_POST和$_COOKIE的内容。 - PHP 处理这些输入的顺序由
php.ini中的request_order或variables_order配置决定。 - 注意: 通常不推荐直接使用
$_REQUEST,因为它可能引入不确定性 (数据来源不明确)。明确使用$_GET或$_POST更安全。
- 一个关联数组,默认情况下包含了
$_COOKIE: 包含通过 HTTP Cookie 传递给当前脚本的变量。$_FILES: 包含通过 HTTP POST 方法上传到脚本的文件信息。$_SESSION: 包含当前脚本的会话变量。$_SERVER: 包含诸如头信息 (headers)、路径 (paths)、脚本位置 (script locations) 等信息。服务器创建此数组。$_ENV: 包含通过环境方法传递给当前脚本的变量。
15-phpinfo-server-variables
phpinfo() 函数
-
用途:
phpinfo()是一个非常有用的内置 PHP 函数,用于输出关于 PHP 当前状态的大量信息。 -
使用方法:
-
创建一个新的 PHP 文件 (例如
info.php)。 -
在该文件中写入以下代码:
<?php phpinfo(); ?> -
通过浏览器访问此文件 (例如
http://localhost:4000/info.php)。
-
-
输出内容:
- PHP 版本。
- 服务器信息和环境 (Server API, Virtual Directory Support, Configuration File (php.ini) Path 等)。
- PHP 核心配置指令的本地值和主值 (Local Value, Master Value)。
- 已加载的 PHP 扩展 (如 MySQLi, PDO, JSON, XML 等) 及其配置。
- HTTP 头信息。
- PHP 许可证信息。
- PHP 环境变量 (
$_ENV)。 - PHP 变量 (
$_GET,$_POST,$_COOKIE,$_SERVER等)。
-
调试价值:
- 快速检查 PHP 安装是否正确以及配置是否符合预期。
- 查看哪些扩展已启用。
- 了解服务器环境。
- 安全警告:
phpinfo()会泄露大量敏感信息,绝不应该在生产服务器上公开访问。开发和调试完成后应立即移除或保护此文件。
$_SERVER 超全局变量
$_SERVER是一个包含了由 Web 服务器提供的信息的数组,例如头信息、路径和脚本位置。- 这些条目的具体内容和可用性因服务器而异。
- 常用
$_SERVER键示例:$_SERVER['DOCUMENT_ROOT']:当前脚本执行的文档根目录。$_SERVER['REMOTE_ADDR']:正在浏览当前页面的用户的 IP 地址。$_SERVER['REQUEST_METHOD']:访问页面使用的请求方法 (例如 'GET', 'POST', 'PUT')。$_SERVER['SCRIPT_FILENAME']:当前执行脚本的绝对路径。$_SERVER['HTTP_HOST']:当前请求的 Host 头信息。$_SERVER['HTTP_USER_AGENT']:当前请求的用户代理 (浏览器) 信息。$_SERVER['REQUEST_URI']:访问此页面所需的 URI,例如/index.html。$_SERVER['PHP_SELF']:当前执行脚本的文件名,相对于文档根目录。$_SERVER['SERVER_SOFTWARE']:服务器标识字符串,在响应头中给出。$_SERVER['SERVER_PORT']:服务器正在使用的端口。
var_dump() 函数
-
用途:
var_dump()用于打印变量的相关信息,包括其类型和值。数组和对象会递归展开其结构。 -
与
echo的区别:echo只能输出简单类型 (如字符串、数字)。尝试echo一个数组或对象通常会导致 "Array to string conversion" 或类似错误,并只输出 "Array" 或 "Object"。var_dump()可以详细显示复杂类型的内部结构。
-
调试: 非常适合在开发过程中调试变量内容。
-
示例: 在
convert.php中查看$_SERVER的内容: 输出会比较冗长,但能清晰地看到$_SERVER中可用的所有键和值。<?php // ... (前面的代码) ... var_dump($_SERVER); // 这会输出 $_SERVER 数组的详细内容到浏览器 // ... (后续的代码) ... ?> -
注意:
var_dump()的输出通常是给开发者看的,不适合直接展示给最终用户。在生产代码中应移除或用更友好的方式处理调试信息。
16-classes-methods-constructors
面向对象编程 (OOP) 简介
- PHP 支持面向对象编程范式。
- 可以将代码组织成类 (classes) 和对象 (objects),以实现更好的结构、可重用性和可维护性。
创建类 (Classes)
-
语法:
<?php class ClassName { // Properties (member variables) // Methods (member functions) } ?>- 使用
class关键字。 - 类名通常使用帕斯卡命名法 (PascalCase),例如
CryptoConverter。 - 类体包含在花括号
{}内。 - 文件名与类名: PHP 不强制文件名必须与类名相同 (不像 Java)。但为了清晰和遵循约定,通常建议文件名与它包含的主要类名匹配 (例如,
CryptoConverter.php文件包含CryptoConverter类)。
- 使用
属性 (Properties)
-
属性是类内部的变量。
-
定义:
class MyClass { public $publicProperty = "I am public"; protected $protectedProperty = "I am protected"; private $privateProperty = "I am private"; public string $typedProperty; // 可以有类型提示 (PHP 7.4+) public ?string $nullableTypedProperty = null; // 可空类型提示 }- 访问修饰符 (Visibility Modifiers):
public:属性或方法可以在任何地方被访问 (类内部、子类、类外部)。protected:属性或方法只能在类本身及其子类中被访问。private:属性或方法只能在定义它的类本身中被访问。- 如果省略访问修饰符,属性默认为
public(但明确写出是好习惯)。
- 类型提示 (PHP 7.4+): 可以为属性声明类型。
- 变量名: 属性名以
$开头,例如$currencyCode。
- 访问修饰符 (Visibility Modifiers):
方法 (Methods)
-
方法是类内部的函数。
-
定义:
class MyClass { public function myPublicMethod(string $param): string { // ... return "Result"; } protected function myProtectedMethod() { // ... } private function myPrivateMethod() { // ... } }- 使用
function关键字定义方法。 - 访问修饰符 (
public,protected,private) 的规则与属性相同。如果省略,方法默认为public。 - 可以有参数类型提示和返回值类型提示。
- 使用
构造函数 (Constructor)
-
构造函数是一种特殊的方法,当使用
new关键字创建类的新实例 (对象) 时自动调用。 -
通常用于初始化对象的属性。
-
PHP 中的构造函数名:
__construct(两个下划线开头)。class User { public string $username; public string $email; public function __construct(string $username, string $email) { $this->username = $username; // $this 指向当前对象实例 $this->email = $email; echo "User object for {$this->username} created.\n"; } } $user1 = new User("MaxF", "[email protected]"); // 调用构造函数 -
$this关键字: 在类的方法内部,$this关键字用于引用当前对象实例。 -
访问属性和方法: 使用对象操作符
>(细箭头 / thin arrow) 来访问对象的属性和方法。$this->propertyName$this->methodName()- 注意:访问属性时,属性名不带
$符号 (例如$this->currencyCode而不是$this->$currencyCode)。
析构函数 (Destructor)
-
析构函数在对象即将被销毁时 (例如,当没有更多对它的引用或脚本结束时由垃圾回收器处理) 自动调用。
-
PHP 中的析构函数名:
__destruct(两个下划线开头)。class FileHandler { private $fileResource; private $filename; public function __construct(string $filename) { $this->filename = $filename; $this->fileResource = fopen($filename, 'w'); echo "File {$filename} opened.\n"; } public function write(string $data) { fwrite($this->fileResource, $data); } public function __destruct() { if ($this->fileResource) { fclose($this->fileResource); echo "File {$this->filename} closed in destructor.\n"; } } } -
用途: 通常用于执行清理操作,如关闭文件句柄、释放数据库连接等。
-
注意: PHP 有垃圾回收机制 (garbage collector),大多数情况下不需要手动管理内存。析构函数主要用于资源管理。
构造函数属性提升 (Constructor Property Promotion) (PHP 8.0+)
-
PHP 8.0 引入了一种更简洁的方式来声明和初始化类属性,尤其是在构造函数中。
-
可以在构造函数的参数列表中直接声明属性的访问修饰符和类型,PHP 会自动创建同名属性并赋值。
-
示例: 这大大减少了模板代码。
// 旧方法 class Point_Old { public float $x; public float $y; public function __construct(float $x, float $y) { $this->x = $x; $this->y = $y; } } // 使用构造函数属性提升 (PHP 8.0+) class Point_New { public function __construct( public float $x, // 自动创建 public float $x; 并赋值 public float $y // 自动创建 public float $y; 并赋值 ) { // 构造函数体可以为空,或包含其他逻辑 } } $p = new Point_New(10.5, 20.3); echo $p->x; // 输出 10.5
创建和使用对象
-
使用
new关键字创建类的实例 (对象)。 -
使用对象操作符
>访问对象的公共属性和方法。// CryptoConverter.php <?php class CryptoConverter { public function __construct(public string $currencyCode = 'usd') { // 属性提升 } public function convert(float $amount, string $fromCryptoSymbol): float { // 假设的转换逻辑 $rate = 0; // 获取汇率的逻辑会在这里 if ($fromCryptoSymbol === 'btc' && $this->currencyCode === 'usd') { $rate = 70000; // 假设的汇率 } elseif ($fromCryptoSymbol === 'eth' && $this->currencyCode === 'usd') { $rate = 3500; // 假设的汇率 } return $amount * $rate; } } ?> // 在另一个文件 (如 convert.php) 中使用 <?php require_once 'CryptoConverter.php'; // 确保类定义被加载 $converter = new CryptoConverter('usd'); // 创建对象,currencyCode 设为 'usd' $btcAmount = 2; $usdValue = $converter->convert($btcAmount, 'btc'); echo "$btcAmount BTC is approximately $usdValue USD."; ?>
17-include-require
代码组织与文件分离
- 当项目变大时,将所有代码放在一个文件中是不切实际的。
- PHP 允许将代码分散到多个文件中,例如将类定义放在单独的文件中。
问题:文件隔离
- 默认情况下,一个 PHP 文件并不知道其他 PHP 文件的存在或内容,即使它们在同一个文件夹中。
- 如果一个文件 (例如
convert.php) 尝试使用在另一个文件 (例如CryptoConverter.php) 中定义的类,而没有明确加载该类的定义,PHP 会报错 (例如 "Class 'CryptoConverter' not found")。
加载外部文件
PHP 提供了几种语言结构来包含和执行其他文件的内容:
- 1.
include- 语法:
include 'path/to/file.php';或include('path/to/file.php');(括号可选,因为它是一个语言结构,而非函数)。 - 行为:
include会获取指定文件中的所有文本/代码,并将其复制到include语句所在的位置,然后 PHP 解释器会执行这些代码。- 如果文件未找到,
include会产生一个 警告 (warning),但脚本会继续执行。
- 返回值: 如果包含成功,
include返回1。如果文件在include语句中包含return语句,则include返回该return语句的值。
- 语法:
- 2.
require- 语法:
require 'path/to/file.php';或require('path/to/file.php'); - 行为:
- 与
include类似,它也会包含并执行指定文件的内容。 - 主要区别: 如果文件未找到,
require会产生一个 致命错误 (fatal error) (E_COMPILE_ERROR),并停止脚本的执行。
- 与
- 用途: 当被包含的文件对于脚本的正确运行至关重要时 (例如包含核心库、配置文件或必要的类定义),应使用
require。
- 语法:
- 3.
include_once- 语法:
include_once 'path/to/file.php'; - 行为:
- 与
include类似,但它会检查该文件是否已经被包含过。 - 如果文件已经被包含,
include_once将不会再次包含它。 - 这有助于防止因多次包含同一个文件而导致的函数或类重定义错误。
- 与
- 性能: 比
include稍慢,因为它需要进行额外的检查。
- 语法:
- 4.
require_once- 语法:
require_once 'path/to/file.php'; - 行为:
- 结合了
require和include_once的特性。 - 如果文件未找到,产生致命错误并停止脚本。
- 如果文件已包含,则不再包含。
- 结合了
- 用途: 这是包含关键文件并确保它们只被包含一次的最常用和推荐的方法。
- 语法:
使用示例
假设我们有 CryptoConverter.php (包含 CryptoConverter 类定义) 和 convert.php (需要使用该类)。
在 convert.php 的顶部:
<?php
// convert.php
// 推荐使用 require_once 来加载类定义
require_once 'classes/CryptoConverter.php'; // 假设 CryptoConverter.php 在 'classes' 子目录中
// 现在可以安全地使用 CryptoConverter 类
if (isset($_POST['amount']) && isset($_POST['crypto'])) {
$amount = (float)$_POST['amount']; // 类型转换
$crypto_code = $_POST['crypto'];
$converter = new CryptoConverter('usd'); // 创建对象
$result = $converter->convert($amount, $crypto_code);
// ... (输出结果) ...
} else {
// ... (处理参数缺失) ...
}
?>
包含路径
- 可以使用相对路径 (相对于当前执行脚本的路径) 或绝对路径。
.代表当前目录。..代表上一级目录。- 示例:
include 'lib/utils.php';(在当前目录的lib子目录中)include '../config.php';(在上一级目录中)
模块化与依赖管理
-
PHP 没有内置的模块系统 (如 ES Modules): 不能像 JavaScript 中那样
import { SpecificClass } from './file.js';。include/require会包含整个文件的内容。 -
副作用: 如果被包含的文件不仅定义了类/函数,还执行了代码或输出了 HTML,这些都会在包含时发生。
-
管理多个包含:
-
“主包含文件”模式: 创建一个中心文件 (例如
includes.php或bootstrap.php),该文件负责require_once所有必要的类和库文件。然后,在其他需要这些功能的脚本中,只需require_once这个中心文件即可。// includes.php <?php require_once 'classes/Database.php'; require_once 'classes/User.php'; require_once 'classes/CryptoConverter.php'; // ... 其他所有类和库 ?> // convert.php <?php require_once 'includes.php'; // 只需要包含这一个文件 // ... 现在所有在 includes.php 中加载的类都可用了 ?> -
自动加载 (Autoloading): 更高级的解决方案。PHP 提供了
spl_autoload_register()函数,允许你定义一个或多个函数,当代码尝试使用尚未定义的类时,这些函数会被自动调用。这些函数通常根据类名来推断文件路径并require它。这是现代 PHP 框架 (如 Composer 管理的依赖) 的核心机制。 (讲师提到稍后会讲到autoload)
-
include 文件夹权限
- Web 服务器通常配置为不允许直接通过 URL 访问某些文件夹中的
.php文件 (例如包含类库或配置文件的文件夹),即使这些文件被其他 PHP 脚本include。 - 这是 Web 服务器的配置问题,不是 PHP 本身的问题。例如,可以将类文件放在文档根目录之外,或者使用
.htaccess(Apache) 或 Nginx 配置来阻止直接访问。
18-loading-data-from-api-parsing-json
目标:在 CryptoConverter 类中实现转换逻辑
- 从外部 API 获取加密货币汇率。
- 解析 API 返回的 JSON 数据。
- 使用汇率计算转换后的金额。
从网络获取数据:file_get_contents()
-
file_get_contents(string $filename, ...):- 这是一个多功能的函数,通常用于读取文件内容到字符串。
- 关键特性: 如果
$filename参数是一个以协议 (如http://或https://) 开头的 URL,file_get_contents()会尝试像浏览器一样访问该 URL 并获取其内容。 - 返回值: 成功时返回文件/URL 的内容 (字符串),失败时返回
false。
-
示例: 获取比特币对美元的汇率
// CryptoConverter.php class CryptoConverter { public string $baseApiUrl = '<https://api.frontendmasters.com/api/crypto/>'; public function __construct(public string $targetCurrency = 'usd') { // ... } public function getRate(string $cryptoSymbol): float|false { $apiUrl = $this->baseApiUrl . strtolower($cryptoSymbol); // 例如: .../api/crypto/btc // 错误处理: 确保 URL 有效,或者使用 try-catch 进行更复杂的错误处理 $jsonData = @file_get_contents($apiUrl); // 使用 @ 抑制潜在的 warning,然后检查 false if ($jsonData === false) { // API 请求失败,可以记录错误或抛出异常 return false; } // ... 解析 JSON 的逻辑 ... return 0.0; // 占位符 } public function convert(float $amount, string $fromCryptoSymbol): float|false { $rate = $this->getRate($fromCryptoSymbol); if ($rate === false) { return false; // 传播错误 } // ... 解析并使用 rate ... return 0.0; // 占位符 } }- 注意文件名和类名: 讲师在演示中遇到了文件名 (
converter.php) 和类名 (CryptoConverter) 不匹配导致require_once后类未找到的问题。最佳实践是保持一致。 - 创建
classes.php进行统一包含: 讲师演示了创建一个classes.php文件,该文件require_once了所有类文件,然后在主脚本中只需要require_once 'classes.php';。
- 注意文件名和类名: 讲师在演示中遇到了文件名 (
解析 JSON 数据:json_decode()
-
API 通常以 JSON (JavaScript Object Notation) 格式返回数据。
-
json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed:- 将 JSON 编码的字符串转换为 PHP 变量。
$json: 要解码的 JSON 字符串。$associative(可选):- 当为
true时,返回的对象将被转换为关联数组。 - 当为
false(默认) 或省略时,返回的对象将是 PHPstdClass对象。
- 当为
- 返回值:
- 成功时返回转换后的 PHP 值 (对象、数组、字符串、数字、布尔值或
null)。 - 如果 JSON 无法解码,或者编码数据深度超过递归限制,则返回
null。可以使用json_last_error()和json_last_error_msg()来获取解码错误信息。
- 成功时返回转换后的 PHP 值 (对象、数组、字符串、数字、布尔值或
-
API 响应示例 (
https://api.frontendmasters.com/api/crypto/btc):{ "name": "Bitcoin", "symbol": "BTC", "price_usd": 70000.12, // 假设价格 "last": 70000.12 // 我们将使用这个值 // ... 其他数据 } -
在
getRate方法中解析 JSON:// CryptoConverter.php (续) public function getRate(string $cryptoSymbol): float|false { $apiUrl = $this->baseApiUrl . strtolower($cryptoSymbol); $jsonData = @file_get_contents($apiUrl); if ($jsonData === false) { return false; } // 解析 JSON 为 PHP 对象 (默认行为) $dataObject = json_decode($jsonData); // 错误处理: 检查 JSON 是否成功解析 if ($dataObject === null && json_last_error() !== JSON_ERROR_NONE) { // JSON 解析失败,可以记录错误 json_last_error_msg() return false; } // 访问对象属性 (假设 API 返回了 'last' 字段作为汇率) // 确保属性存在 if (isset($dataObject->last) && is_numeric($dataObject->last)) { return (float)$dataObject->last; } else { // "last" 属性不存在或不是数字 return false; } }- 访问对象属性: 如果
json_decode返回一个对象,使用>操作符访问其属性 (例如$dataObject->last)。 - 解析为关联数组: 如果使用
json_decode($jsonData, true),则返回的是关联数组,使用[]访问元素 (例如$dataArray['last'])。 - 讲师在演示中遇到了
Cannot use object of type stdClass as array的错误,这是因为他尝试用数组语法[]访问一个对象,后来改用了对象语法>。
- 访问对象属性: 如果
完成转换逻辑
// CryptoConverter.php (续)
public function convert(float $amount, string $fromCryptoSymbol): float|false {
$rate = $this->getRate($fromCryptoSymbol);
if ($rate === false) {
return false; // 如果获取汇率失败,则转换失败
}
return $amount * $rate;
}
更新 convert.php 以使用新的转换器
<?php
// convert.php
require_once 'classes.php'; // 或者直接 require_once 'classes/CryptoConverter.php';
if (isset($_POST['amount']) && isset($_POST['crypto'])) {
$amount = (float)$_POST['amount'];
$crypto_code = $_POST['crypto'];
$converter = new CryptoConverter('usd'); // 目标货币为 USD
$result = $converter->convert($amount, $crypto_code);
// ... (HTML 输出部分) ...
if ($result !== false) {
echo "<p>$amount " . strtoupper($crypto_code) . " is approximately " . number_format($result, 2) . " USD.</p>";
} else {
echo "<p>Could not perform conversion. Please check the crypto code or try again later.</p>";
}
// ... (HTML 输出部分结束) ...
} else {
// ... (处理参数缺失) ...
}
?>
- 类型声明和联合类型 (Union Types) (PHP 8.0+):
- 讲师在
convert方法的返回类型中使用了联合类型float|false,表示该方法可能返回一个浮点数或布尔值false(表示失败)。 - 这使得调用者可以明确地检查转换是否成功。
- 讲师在
调试 echo vs var_dump
- 当
echo一个复杂类型 (如对象或数组) 时,PHP 通常只会输出 "Object" 或 "Array" 并可能伴随一个转换错误。 - 使用
var_dump($variable)可以详细地打印出变量的结构和内容,非常有助于调试。
19-handling-null-values-safe-function-calls
创建 API 端点 (api.php)
- 目标: 创建一个 PHP 脚本,它不返回 HTML,而是返回 JSON 数据。这个脚本可以被其他应用 (如移动应用、JavaScript 前端) 调用。
- 步骤:
- 创建
api.php文件。 - 包含必要的类 (如
CryptoConverter,通过classes.php或直接包含)。 - 从请求参数中获取输入 (例如,加密货币代码,可能通过
$_GET)。 - 使用
CryptoConverter获取数据。 - 将结果格式化为 JSON 字符串并输出。
- 设置正确的 HTTP Content-Type 头部为
application/json。
- 创建
<?php
// api.php
require_once 'classes.php'; // 包含 CryptoConverter 类
// 1. 获取输入参数 (例如从 $_GET 获取加密货币代码)
// 并提供默认值
$cryptoCode = $_GET['code'] ?? 'btc'; // PHP 7.0+ Null Coalescing Operator
// 如果 $_GET['code'] 存在且不为 null,则使用其值,否则使用 'btc'
// 也可以获取数量,如果API需要的话
// $amount = (float)($_GET['amount'] ?? 1); // 默认为 1 单位
// 2. 创建转换器实例
$converter = new CryptoConverter('usd'); // 假设目标货币固定为 USD
// 3. 获取汇率 (或者更复杂的转换结果)
// 这里简化为直接调用 convert 方法获取 1 单位加密货币的价值
$rate = $converter->convert(1, $cryptoCode); // 获取1单位的汇率
// 4. 准备要返回的数据 (通常是一个关联数组)
$responseData = [];
if ($rate !== false) {
$responseData = [
'crypto_code' => strtoupper($cryptoCode),
'target_currency' => 'USD',
'rate' => $rate,
// 可以添加更多信息,如时间戳等
// 'timestamp' => time()
];
} else {
// 如果获取汇率失败,可以返回错误信息
// http_response_code(400); // 设置 HTTP 状态码为 Bad Request 或其他
$responseData = [
'error' => 'Could not retrieve rate for the specified crypto code.',
'crypto_code' => strtoupper($cryptoCode)
];
}
// 5. 设置 HTTP Content-Type 头部
header('Content-Type: application/json');
// 6. 将数据编码为 JSON 并输出
echo json_encode($responseData);
?>
PHP 7.0+ Null Coalescing Operator (??)
- 语法:
$variable = $value_to_check ?? $default_value; - 行为:
- 如果
$value_to_check存在并且不为null,则$variable被赋值为$value_to_check的值。 - 否则 (如果
$value_to_check不存在或为null),$variable被赋值为$default_value。
- 如果
- 与
isset()的三元运算符比较:$code = isset($_GET['code']) ? $_GET['code'] : 'btc';(旧方法)$code = $_GET['code'] ?? 'btc';(PHP 7.0+,更简洁)
- 这对于提供参数的默认值非常方便。
PHP 8.0+ Nullsafe Operator (?->)
-
语法:
$result = $object?->method();或$result = $object?->property; -
行为:
- 如果
$object不为null,则调用其方法或访问其属性,行为与普通>一样。 - 如果
$object为null,则整个表达式的结果为null,并且不会尝试调用方法或访问属性,从而避免了因在null上调用方法而产生的致命错误。
- 如果
-
用途: 当你处理一个可能为
null的对象,并且想安全地调用其方法或访问其属性时非常有用,可以避免写很多if ($object !== null)的检查。 -
示例 (非本课程 CryptoConverter 案例,仅为演示):
class User { public function getProfile(): ?Profile { /* ... */ } } class Profile { public function getAddress(): ?Address { /* ... */ } } class Address { public string $street; } function getStreetAddress(?User $user): ?string { // 如果 $user, $user->getProfile(), 或 $user->getProfile()->getAddress() 中任何一个是 null, // $street 会是 null,且不会报错。 $street = $user?->getProfile()?->getAddress()?->street; return $street; } -
讲师提到这个操作符可以减少很多
if判断,使代码更简洁。
20-formatting-returning-json-data
API 端点 (api.php) 的完善
- 目标: 确保
api.php正确返回 JSON 数据,并设置适当的 HTTP 头部。
问题 1:手动构造 JSON 字符串的风险
-
在
api.php中,如果直接使用字符串拼接或echo来手动构造 JSON,很容易出错:- 引号问题: JSON 规范要求字符串使用双引号
"。如果 PHP 字符串使用单引号',或者双引号没有正确转义,生成的输出将不是有效的 JSON。 - 特殊字符转义: JSON 字符串中的特殊字符 (如换行符
\n,制表符\t, 双引号", 反斜杠\\等) 需要正确转义。 - 数据类型: 布尔值应为
true/false(无引号),数字不应有引号。
- 引号问题: JSON 规范要求字符串使用双引号
-
示例 (错误的方式):
// 错误示范:手动构造 JSON,容易出错 // echo "{'rate': $rate, 'code': '$cryptoCode'}"; // 单引号错误 // echo "{\\"rate\\": $rate, \\"code\\": \\"$cryptoCode\\"}"; // 如果 $cryptoCode 含特殊字符则可能出错
解决方案:使用 json_encode()
json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false:- 将 PHP 变量 (通常是关联数组或对象) 转换为 JSON 格式的字符串。
- 这是创建 JSON 响应的正确且安全的方法。
- 步骤:
- 将要返回的数据组织成一个 PHP 关联数组。
- 调用
json_encode()将该数组转换为 JSON 字符串。 echo该 JSON 字符串。
<?php
// api.php (续)
// ... (获取 $cryptoCode, $rate 的代码) ...
// 4. 准备要返回的数据 (关联数组)
$responseData = [];
if ($rate !== false) {
$responseData = [
'crypto_code' => strtoupper($cryptoCode),
'target_currency' => 'USD',
'rate' => $rate, // $rate 是数字
'success' => true // 布尔值
];
} else {
$responseData = [
'error' => 'Could not retrieve rate.',
'crypto_code' => strtoupper($cryptoCode),
'success' => false
];
// 考虑设置 HTTP 错误状态码,例如:
// http_response_code(400); // Bad Request
}
// 5. 设置 HTTP Content-Type 头部 (重要!)
header('Content-Type: application/json');
// 6. 将数据编码为 JSON 并输出
echo json_encode($responseData);
?>
问题 2:HTTP Content-Type 头部
-
默认行为: PHP 脚本 (尤其是以
.php结尾的文件) 通常被 Web 服务器配置为默认发送Content-Type: text/html头部。 -
API 的要求: 对于返回 JSON 数据的 API,客户端 (如 JavaScript
fetch或移动应用) 期望Content-Type头部为application/json。 -
后果: 如果
Content-Type不正确:- 某些客户端库可能无法正确解析响应。
- 浏览器开发者工具可能无法正确显示 JSON。
- 严格的客户端可能会拒绝处理响应。
-
解决方案: 使用
header()函数在脚本输出任何内容之前设置正确的Content-Type。header('Content-Type: application/json');
header() 函数的注意事项
- 调用时机:
header()函数必须在脚本向客户端发送任何输出 (包括 HTML、空格、换行符,甚至 PHP 错误/警告信息) 之前调用。 - 原因: HTTP 头部是响应的第一部分,一旦响应体 (body) 开始发送,就不能再修改头部了。
- 常见错误:
- 在
<?php标签之前有空格或换行符。 - 在调用
header()之前有echo或print语句。 - PHP 配置文件 (
php.ini) 中的output_buffering设置可能会影响此行为 (如果开启,输出会被缓冲,允许稍后发送头部,但不应依赖此特性)。
- 在
- 最佳实践: 将所有
header()调用放在脚本的最顶部,紧随<?php标签之后。
<?php // 确保这是文件的第一行,前面没有任何字符
header('Content-Type: application/json');
// header('Access-Control-Allow-Origin: *'); // 如果需要CORS
require_once 'classes.php';
// ... (后续的 API 逻辑) ...
echo json_encode($responseData);
?>
CORS (Cross-Origin Resource Sharing)
- 问题: 如果你的 API 托管在一个域名下 (例如
api.example.com),而调用它的 JavaScript 代码运行在另一个域名下 (例如app.otherdomain.com),浏览器出于安全原因会阻止这种跨域请求,除非服务器明确允许。 - 解决方案: 服务器需要在响应中包含特定的 CORS 头部。
Access-Control-Allow-Origin: 最基本的 CORS 头部。header('Access-Control-Allow-Origin: *');// 允许来自任何域的请求 (不安全,仅用于公共 API)。header('Access-Control-Allow-Origin: <https://app.otherdomain.com>');// 只允许来自特定域的请求。
- 还有其他 CORS 头部用于更复杂的场景 (如允许特定的 HTTP 方法、头部等)。
- 注意: CORS 是浏览器实施的安全策略。服务器到服务器的请求通常不受 CORS 限制。PHP 本身不处理 CORS,但可以通过
header()函数发送必要的 CORS 响应头。
通过这些步骤,api.php 就能正确地作为 JSON API 端点工作了。
21-using-php-in-a-client-application
项目目标:前端博物馆应用 (服务器端渲染改造)
- 初始状态: 一个现有的客户端 Web 应用 (HTML, CSS, JavaScript),它从某个数据源 (可能是硬编码的 JS 对象或 JSON 文件) 加载数据并在前端渲染一个图片画廊。
- 项目文件结构:
index.html(主页面)script.js(负责数据加载和 DOM 操作)style.cssimages/(存放图片)data/(存放数据源)data.php(PHP 数组格式的数据)data.json(JSON 格式的数据)data.db(SQLite 数据库文件)
- 改造目标:
- 将应用从客户端渲染 (Client-Side Rendering - CSR) 迁移到服务器端渲染 (Server-Side Rendering - SSR) 使用 PHP。
- 原因:
- 性能: 某些情况下 SSR 可以提供更快的初始加载体验 (FCP - First Contentful Paint)。
- SEO (搜索引擎优化): 确保搜索引擎 (包括像 ChatGPT 这样的 AI,它们可能不执行 JavaScript) 能够抓取和索引页面内容。
- 移除原有的 JavaScript 数据加载和渲染逻辑。
- 使用 PHP 从不同的数据源 (PHP 数组, JSON 文件, SQLite 数据库) 读取数据并在服务器端生成 HTML。
步骤 1:准备工作
-
打开项目文件夹
FRONTENDMUSEUM。 -
启动 PHP 开发服务器: (确保你位于
FRONTENDMUSEUM文件夹的根目录)php -S localhost:5000 ``` -
观察现有应用:
- 访问
http://localhost:5000/index.html(或http://localhost:5000如果服务器默认提供index.html)。 - 查看页面源代码,会发现
<main>标签内可能只有一个模板化的<article>元素。 - 使用浏览器开发者工具检查 DOM,会看到
<main>标签内有多个由 JavaScript 动态生成的<article>元素。
- 访问
步骤 2:移除客户端 JavaScript 渲染
-
删除
script.js文件 (或将其重命名)。 -
从
index.html中移除对script.js的引用:<!-- <script src="script.js" defer></script> --> -
刷新页面,现在应该只看到 HTML 中硬编码的那个模板
<article>。
步骤 3:将 HTML 转换为 PHP 文件
- 重命名
index.html为index.php。- 这使得我们可以在文件中嵌入 PHP 代码。
- 此时,如果访问
http://localhost:5000/index.php,页面看起来应该和之前一样 (只有一个模板文章),但现在它是由 PHP 提供的。
步骤 4:使用 PHP 数组数据进行服务器端渲染
- 目标: 从
data/data.php文件中加载展品数据 (一个名为$exhibits的 PHP 数组),并使用foreach循环在服务器端生成多个<article>元素。
-
在
index.php顶部包含数据文件:<?php require_once 'data/data.php'; // $exhibits 数组现在可用 ?> <!DOCTYPE html> <!-- ... rest of the HTML ... --> -
找到 HTML 中用于渲染单个展品的模板
<article>结构。 -
使用
foreach循环包裹该模板,并动态填充数据:<main> <?php foreach ($exhibits as $object): ?> <article> <img src="images/<?php echo htmlspecialchars($object['image']); ?>" alt="<?php echo htmlspecialchars($object['title']); ?>"> <div class="info"> <h2><?php echo htmlspecialchars($object['title']); ?></h2> <p><?php echo htmlspecialchars($object['description']); ?></p> </div> </article> <?php endforeach; ?> </main>$exhibits来自data.php。$object在每次循环中代表一个展品 (它本身是一个关联数组,包含title,description,image键)。- 使用
<?php echo ...; ?>(或短标签<?= ... ?>) 将 PHP 变量的值插入到 HTML 的相应位置。 - 使用
htmlspecialchars()是一个好习惯,用于防止 XSS 攻击,确保特殊字符被正确编码。 - 使用了
foreach (...): ... endforeach;的替代语法,这在混合 HTML 和 PHP 时更易读。 - 注意图片路径是
images/加上从数据中获取的文件名。
- 关于
endforeach;后的分号: 讲师提到,如果endforeach;是 PHP 块中的最后一条语句,则其后的分号是可选的。但为了防止将来在该块中添加更多代码时忘记补充分号,通常建议加上。
22-displaying-dynamic-data-exercise
(这是对上一节练习的完成和调试过程的总结)
任务:完成 index.php 中展品数据的动态显示
- 背景: 已经设置了
foreach循环来遍历$exhibits数组。现在需要将每个$object(代表一个展品) 的数据显示在 HTML 模板中。
调试技巧:使用 var_dump()
-
在循环内部,可以使用
var_dump($object);来查看每个$object的结构和内容,确保数据如预期。 这会帮助确认键名 (如title,description,image) 是否正确。<?php foreach ($exhibits as $object): ?> <?php // var_dump($object); // 临时用于调试 ?> <article> <!-- ... --> </article> <?php endforeach; ?> ```
填充 HTML 模板
- 标题 (
<h2>):
或者使用短 echo 标签:<h2><?php echo htmlspecialchars($object['title']); ?></h2><h2><?= htmlspecialchars($object['title']) ?></h2> - 描述 (
<p>):<p><?php echo htmlspecialchars($object['description']); ?></p> - 图片 (
<img>):- 关键点:
- 图片
src属性的值需要拼接正确的路径 (例如,images/目录加上图片文件名)。 - 可以在 HTML 属性值内部嵌入 PHP
echo语句。
- 图片
<img src="images/<?php echo htmlspecialchars($object['image']); ?>" alt="<?php echo htmlspecialchars($object['title']); ?>" />- 常见错误 (调试过程):
- 忘记
echo: 如果在src属性的 PHP 块中只写了$object['image']而没有echo,那么图片路径将不会被输出,导致图片无法显示。PHP 不会因为没有echo而报错,它只是计算了表达式的值但没有输出。 - 路径问题: 确保
images/目录和图片文件名 ($object['image']) 的组合是正确的相对路径。
- 忘记
- 关键点:
最终的循环体示例:
<main>
<?php
// 假设 require_once 'data/data.php'; 已经在文件顶部执行
// $exhibits 数组可用
foreach ($exhibits as $object):
?>
<article>
<img src="images/<?= htmlspecialchars($object['image']) ?>" alt="<?= htmlspecialchars($object['title']) ?>">
<div class="info">
<h2><?= htmlspecialchars($object['title']) ?></h2>
<p><?= htmlspecialchars($object['description']) ?></p>
</div>
</article>
<?php
endforeach;
?>
</main>
- 刷新页面后,应该能看到所有展品都已通过服务器端渲染正确显示出来,包括图片、标题和描述。
23-working-with-external-data-sources
目标:扩展应用,实现主从视图导航并从数据库加载数据
- 主从视图 (Master-Detail):
- 主视图 (
index.php): 显示所有展品的标题列表。每个标题是一个链接。 - 从视图 (
details.php): 点击主视图中的链接后,显示该展品的详细信息 (图片、标题、描述)。
- 主视图 (
- 数据源切换:
- 从 PHP 数组 (
data.php)。 - 从 JSON 文件 (
data.json)。 - 最终目标: 从 SQLite 数据库 (
data.db)。
- 从 PHP 数组 (
- 使用 OOP: 创建一个
DB类来封装数据库操作。
1. 从 JSON 文件加载数据 (快速演示)
-
如果数据源是
data.json,可以使用file_get_contents()读取文件内容,然后用json_decode()解析。// index.php (或任何需要数据的脚本) <?php $jsonString = file_get_contents('data/data.json'); if ($jsonString === false) { die("Error: Could not read data.json"); } // true 参数使 json_decode 返回关联数组,而不是 stdClass 对象 // 这与 data.php 中 $exhibits 的结构更一致 $exhibits = json_decode($jsonString, true); if ($exhibits === null && json_last_error() !== JSON_ERROR_NONE) { die("Error: Could not decode JSON from data.json - " . json_last_error_msg()); } // 现在 $exhibits 数组可用,后续渲染逻辑与使用 data.php 类似 // ... ?> -
讲师提到,使用 JSON 作为数据源很简单,重点将放在数据库操作上。
2. SQLite 数据库简介
- SQLite: 一个轻量级的、基于文件的、自包含的 SQL 数据库引擎。
- 无需服务器: 与 MySQL 或 PostgreSQL 不同,SQLite 不需要单独的服务器进程。数据库就是一个普通文件。
- SQL 兼容: 支持标准的 SQL 语法 (创建表、插入、查询、更新、删除等)。
- 广泛使用: 内置于许多操作系统 (Android, iOS) 和应用程序中。
- PHP 支持: PHP 通过
SQLite3扩展 (通常默认启用) 或PDO_SQLite(与 PDO 一起使用) 提供对 SQLite 的支持。
- 项目中的
data.db:- 这是一个已经创建好的 SQLite 数据库文件,其中包含一个名为
exhibits的表,表结构与data.php中的数组类似 (有title,description,image等列)。
- 这是一个已经创建好的 SQLite 数据库文件,其中包含一个名为
- 查看 SQLite 数据库:
- 由于是二进制文件,不能直接用文本编辑器查看。
- 需要使用 SQLite 数据库浏览器/管理器工具。
- 讲师演示了使用 Chrome 扩展 "SQLite Viewer" 来打开
data.db文件,查看exhibits表的内容。其他工具如 DB Browser for SQLite (桌面应用) 也很常用。
3. 创建 DB 类 (classes/DB.php)
-
目标: 封装数据库连接和查询逻辑。
-
初始结构:
<?php // classes/DB.php class DB { private $pdo; // 或 $sqlite_connection; 根据选择的API private $dbPath = 'data/data.db'; // 数据库文件路径 public function __construct() { // 连接数据库的逻辑 } public function executeQuery(string $sql, array $params = []) { // 执行查询并返回结果的逻辑 } // 可以添加其他方法,如 executeUpdate, getLastInsertId 等 } ?> -
抽象类 (Abstract Classes) 概念提及:
-
讲师提到,可以创建一个抽象的
DB基类,然后为不同的数据库系统 (SQLite, MySQL) 创建具体的子类。 -
抽象类不能被直接实例化,必须被继承。它可以包含具体实现的方法和抽象方法 (没有实现,由子类提供)。
-
例如:
abstract class AbstractDB { abstract public function connect(); abstract public function query(string $sql); public function log(string $message) { // 通用日志记录方法 } } class SQLiteDB extends AbstractDB { /* ... */ } -
(本次课程不会深入抽象类,仅作概念介绍。)
-
PHP 数据库 API 选择
- 1. 特定数据库扩展 (例如
SQLite3类,mysqlifor MySQL):- 直接使用针对特定数据库的 API。
- 优点: 可能提供对数据库特定功能的最完整访问。
- 缺点: 如果将来需要更换数据库系统,可能需要重写大量数据库交互代码。
- 2. PDO (PHP Data Objects):
- 一个数据库访问抽象层。
- 提供了一致的 API 来与多种数据库 (MySQL, PostgreSQL, SQLite, Oracle, SQL Server 等) 进行交互,只需更改连接字符串和可能的一些 SQL 方言差异。
- 优点: 数据库无关性,代码更具可移植性。
- 缺点: 可能无法利用某些数据库特有的高级功能。
- PDO 通常是推荐的方式,尤其是在可能需要支持多种数据库或希望代码更灵活的项目中。
- PDO 扩展 (
php_pdo) 和特定数据库的 PDO 驱动 (php_pdo_sqlite,php_pdo_mysql等) 需要在 PHP 中启用。通常默认启用。
讲师决定先演示使用 SQLite3 类的直接方式。
24-connecting-to-a-sqlite-database
在 DB 类中实现 SQLite 连接和查询 (使用 SQLite3 类)
<?php
// classes/DB.php
class DB {
private $sqlite; // 用于存储 SQLite3 对象
private $dbPath = __DIR__ . '/../data/data.db'; // 数据库文件路径,使用 __DIR__ 确保相对路径正确
public function __construct() {
try {
// SQLite3 的构造函数参数是数据库文件的路径
// 如果文件不存在,它会尝试创建它 (取决于权限)
$this->sqlite = new SQLite3($this->dbPath);
} catch (Exception $e) {
// 处理连接错误,例如记录日志并退出,或抛出自定义异常
die("SQLite Connection Error: " . $e->getMessage());
}
}
/**
* 执行一个 SQL 查询并返回所有结果行作为关联数组。
*
* @param string $sql SQL 查询语句.
* @return array|false 结果数组,或在失败时返回 false.
*/
public function executeQuery(string $sql): array|false {
if (!$this->sqlite) {
return false; // 如果连接未建立
}
// $this->sqlite->query($sql) 执行查询,返回一个 SQLite3Result 对象或 false
$result = $this->sqlite->query($sql);
if ($result === false) {
// 查询失败,可以记录错误: $this->sqlite->lastErrorMsg()
return false;
}
$rows = [];
// $result->fetchArray(SQLITE3_ASSOC) 从结果集中获取下一行作为关联数组
// 当没有更多行时返回 false
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$rows[] = $row;
}
// SQLite3Result 对象在不再需要时应被释放 (通常在对象销毁时自动处理,但明确调用更好)
// fetchArray 会在内部处理结果集指针,循环结束后结果集通常会被消耗完。
// 对于 SELECT 查询,一旦所有数据被提取,结果集通常会自动清理。
// $result->finalize(); // 可以显式调用,但对于 fetchArray 循环不是严格必需
return $rows;
}
// 在对象销毁时关闭数据库连接 (好习惯)
public function __destruct() {
if ($this->sqlite) {
$this->sqlite->close();
}
}
}
?>
- 数据库路径 (
$dbPath):- 使用
__DIR__ . '/../data/data.db'。__DIR__是一个魔术常量,代表当前文件 (DB.php) 所在的目录。 ../表示上一级目录。- 这样可以确保无论
DB.php从哪里被包含,数据库文件的相对路径都是正确的。
- 使用
new SQLite3($this->dbPath): 创建SQLite3类的实例,尝试打开 (或创建) 数据库文件。$this->sqlite->query($sql):- 执行 SQL 查询。
- 对于
SELECT查询,成功时返回一个SQLite3Result对象,失败时返回false。
$result->fetchArray(SQLITE3_ASSOC):- 从
SQLite3Result对象中获取一行数据。 SQLITE3_ASSOC参数指定返回关联数组 (列名为键)。- 其他选项:
SQLITE3_NUM:返回数字索引数组。SQLITE3_BOTH(默认):同时返回关联和数字索引。
- 每次调用
fetchArray(),内部指针会移向结果集中的下一行。当没有更多行时,返回false。
- 从
- 循环获取所有行: 使用
while循环和fetchArray()来遍历所有结果行,并将它们收集到一个数组 ($rows) 中。 - 错误处理:
- 在构造函数中使用
try-catch来捕获连接时可能发生的异常。 - 检查
query()和fetchArray()的返回值是否为false以判断操作是否成功。 - 可以使用
$this->sqlite->lastErrorMsg()获取 SQLite 的最后错误信息。
- 在构造函数中使用
__destruct(): 在对象被销毁时,通过$this->sqlite->close()关闭数据库连接,释放资源。
在 index.php 中使用 DB 类
<?php
// index.php
// 假设 classes.php 或 autoloading 已经设置好,DB 类可用
// 或者直接 require_once 'classes/DB.php';
// 包含头部 (如果使用了分离的 header.inc.php)
// require_once 'includes/header.inc.php';
$db = new DB(); // 创建 DB 对象,会自动连接数据库
$sql = "SELECT title, description, image FROM exhibits ORDER BY title ASC"; // 获取所有展品
$exhibits = $db->executeQuery($sql);
if ($exhibits === false) {
echo "<p>Error retrieving data from the database.</p>";
// 可能需要包含页脚并退出
// require_once 'includes/footer.inc.php';
exit;
}
?>
<main>
<!-- <h1>Museum Exhibits</h1> --> <!-- 标题可能在 header.inc.php 中 -->
<?php if (empty($exhibits)): ?>
<p>No exhibits found.</p>
<?php else: ?>
<ul> <!-- 改为列表显示标题 -->
<?php foreach ($exhibits as $exhibit): ?>
<li>
<a href="details.php?id=<?php echo urlencode($exhibit['title']); /* 假设 title 为唯一标识,实际应使用 ID */ ?>">
<?php echo htmlspecialchars($exhibit['title']); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</main>
<?php
// 包含页脚 (如果使用了分离的 footer.inc.php)
// require_once 'includes/footer.inc.php';
?>
new DB(): 创建DB类的实例,构造函数会自动连接到 SQLite 数据库。$db->executeQuery($sql): 执行 SQL 查询并获取结果。- 渲染逻辑:
- 如果
$exhibits获取成功且不为空,则遍历它。 - 这里改为只显示展品标题列表,每个标题链接到
details.php。 - 注意链接参数:
details.php?id=<?php echo urlencode($exhibit['title']); ?>- 暂时使用
title作为标识符传递给details.php。在实际应用中,通常会有一个唯一的数字 ID 列。 urlencode()用于确保参数值在 URL 中正确编码。
- 暂时使用
- 如果
- 问题提及 (SQL 注入): 讲师指出,直接将用户输入或不可信数据拼接到 SQL 查询中是不安全的,会导致 SQL 注入漏洞。正确的做法是使用预处理语句 (prepared statements) 和参数绑定。 (
SQLite3类和PDO都支持预处理语句)。由于时间关系,本次课程可能不会详细展开,但这是数据库安全的关键点。
25-autoloading-classes
问题:手动 require_once 多个类文件
- 当项目中有很多类时,在每个需要它们的文件顶部写一长串
require_once语句会变得繁琐且容易出错。 - 每次添加新类,都需要记得去更新这些包含列表。
解决方案:自动加载 (Autoloading)
- 概念: 自动加载是一种机制,当 PHP 遇到一个尚未定义的类名时 (例如,在
new ClassName()或ClassName::staticMethod()时),它会触发一个预先注册的回调函数。这个回调函数负责根据类名找到并包含对应的类文件。 spl_autoload_register():- PHP 推荐的注册自动加载函数的方式。
- 可以注册多个自动加载函数,它们会按注册顺序依次尝试加载类。
- 语法:
spl_autoload_register(?callable $autoload_function = null, bool $throw = true, bool $prepend = false): bool$autoload_function:一个可回调的参数 (例如函数名字符串、匿名函数、[$object, 'method']数组等)。这个函数接收一个参数:需要加载的类名。
创建自动加载文件 (classes.php 或 autoload.php)
我们可以创建一个中心文件 (例如,讲师使用的 classes.php,或者更通用的 autoload.php) 来设置自动加载逻辑。这个文件只需要在应用的入口点 (如 index.php, details.php, api.php) require_once 一次。
<?php
// classes.php (或 autoload.php)
spl_autoload_register(function ($className) {
// 假设所有类文件都存放在 'classes' 目录下
// 并且文件名与类名完全一致 (例如 DB 类在 classes/DB.php)
$filePath = __DIR__ . '/' . $className . '.php';
// __DIR__ 是当前文件 (classes.php) 所在的目录
// 所以 $filePath 会是类似 /path/to/project/classes/DB.php
// 检查文件是否存在,然后包含它
if (file_exists($filePath)) {
require_once $filePath;
} else {
// 可选:如果文件未找到,可以记录错误或抛出异常
// error_log("Autoload error: Could not load class {$className}. File not found: {$filePath}");
// 或者不处理,让 PHP 后续抛出 "Class not found" 错误
}
});
// 注意:如果类文件在不同的子目录或遵循更复杂的命名约定 (如 PSR-4),
// 这里的逻辑需要相应调整,例如将命名空间转换为目录路径。
// Composer 的自动加载器就是这样做的。
?>
- 匿名函数作为回调:
function ($className) { ... }是一个匿名函数 (闭包),它被传递给spl_autoload_register。 - 参数
$className: 当 PHP 尝试使用一个未定义的类 (例如new DB()) 时,这个匿名函数会被调用,$className的值就是"DB"。 - 路径构建:
- 代码假设类文件与类名同名,并带有
.php扩展名,且都位于classes.php文件所在的目录 (即classes/目录,如果classes.php位于项目根目录下的classes/文件夹,则应为__DIR__ . '/' . $className . '.php',确保与实际文件结构匹配)。 - 讲师的示例中,
classes.php与DB.php在同一classes/目录下,所以是__DIR__ . '/' . $className . '.php'。如果classes.php在项目根目录,而类在classes/子目录,则应为__DIR__ . '/classes/' . $className . '.php'。 (确保理解__DIR__指向的是包含spl_autoload_register的文件所在的目录。)
- 代码假设类文件与类名同名,并带有
file_exists(): 检查文件是否存在,避免require_once不存在的文件时产生错误。require_once: 加载找到的类文件。
在应用入口点使用自动加载
现在,在 index.php, details.php 等文件的顶部,只需要包含这个自动加载配置文件:
<?php
// index.php (或 details.php 等)
// 包含自动加载设置文件
require_once 'classes/classes.php'; // 假设 classes.php 在 classes 目录下
// 或者如果 classes.php 在根目录,则 require_once 'classes.php';
// 现在可以直接使用类,PHP 会在需要时自动加载它们
$db = new DB();
// $converter = new CryptoConverter(); (如果也有这个类且遵循命名约定)
// ... 后续代码 ...
?>
- 当执行
new DB()时,如果DB类尚未定义,spl_autoload_register注册的函数会被调用,$className传入"DB"。 - 自动加载函数会尝试
require_once 'classes/DB.php'(根据路径构建逻辑)。 - 如果成功,
DB类就定义好了,new DB()就可以继续执行。
魔术方法 (Magic Methods) 提及
- 讲师简要提到了 PHP 中的“魔术方法”,它们是以双下划线
__开头的特殊方法名 (如__construct,__destruct,__get,__set,__call,__toString等)。 - 这些方法在特定情况下会被 PHP 自动调用。例如:
__get($name):当读取一个不可访问 (私有或保护的) 或不存在的属性时调用。__set($name, $value):当给一个不可访问或不存在的属性赋值时调用。__call($name, $arguments):当调用一个不可访问或不存在的对象方法时调用。
- 自动加载在早期 PHP 版本中曾通过一个名为
__autoload()的魔术函数实现,但现在spl_autoload_register()是推荐的方式,因为它更灵活 (可以注册多个加载器)。 - 这些魔术方法使得 PHP 非常灵活,但也可能使代码行为更难预测,需要谨慎使用。
26-header-footer-details
问题:HTML 结构重复
- 在多个 PHP 页面 (如
index.php,details.php) 中,通常会有相同的 HTML 头部 (DOCTYPE,<html>,<head>, 导航栏等) 和页脚 (版权信息,闭合标签等)。 - 直接在每个文件中复制粘贴这些通用部分会导致:
- 代码冗余: 相同代码多处存在。
- 维护困难: 如果需要修改头部或页脚,必须在所有文件中都进行修改。
解决方案:分离头部和页脚到独立文件
- 将通用的 HTML 头部内容提取到一个单独的文件 (例如
includes/header.inc.php)。 - 将通用的 HTML 页脚内容提取到另一个单独的文件 (例如
includes/footer.inc.php)。 - 在每个主页面文件的适当位置使用
require_once(或include) 来包含这些头部和页脚文件。
1. 创建 includes/header.inc.php:
<?php
// includes/header.inc.php
// 这里可以包含任何需要在每个页面顶部执行的 PHP 逻辑,
// 例如会话启动、配置加载、权限检查等。
// 也可以在这里定义通用的变量,如页面标题
if (!isset($pageTitle)) {
$pageTitle = "Frontend Museum"; // 默认标题
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle); ?></title>
<link rel="stylesheet" href="style.css"> <!-- 假设 style.css 在根目录 -->
<!-- 可以有其他通用的 <meta>, <link> 标签 -->
</head>
<body>
<header>
<h1><a href="index.php">Frontend Museum</a></h1>
<!-- 可以有导航菜单等 -->
</header>
<div class="container"> <!-- 一个包裹主要内容的容器 -->
- 文件名后缀
.inc.php是一种常见的约定,表示这是一个被包含的文件,通常不直接通过 URL 访问。 - 可以定义一个
$pageTitle变量,并在主 PHP 文件中设置它,以便每个页面有不同的标题。
2. 创建 includes/footer.inc.php:
<?php
// includes/footer.inc.php
?>
</div> <!-- 关闭 .container -->
<footer>
<p>© <?php echo date('Y'); ?> Frontend Museum. All rights reserved.</p>
</footer>
<!-- 可以有通用的 JavaScript 文件引用 -->
<!-- <script src="main.js"></script> -->
</body>
</html>
3. 在主页面 (如 index.php, details.php) 中使用:
<?php
// index.php (或 details.php)
// (可选) 设置特定于此页面的变量,如标题
$pageTitle = "All Exhibits - Frontend Museum";
// 包含自动加载和数据库类 (如果需要)
require_once 'classes/classes.php'; // 或 autoload.php
$db = new DB();
// 包含头部
require_once 'includes/header.inc.php';
// 获取数据 (示例)
$exhibits = $db->executeQuery("SELECT title FROM exhibits");
?>
<main class="master"> <!-- 特定于此页面的内容 -->
<?php if ($exhibits): ?>
<ul>
<?php foreach ($exhibits as $exhibit): ?>
<li>
<a href="details.php?title=<?php echo urlencode($exhibit['title']); ?>">
<?php echo htmlspecialchars($exhibit['title']); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p>No exhibits to display.</p>
<?php endif; ?>
</main>
<?php
// 包含页脚
require_once 'includes/footer.inc.php';
?>
- 优点:
- 减少重复: 通用 HTML 只需编写一次。
- 易于维护: 修改头部或页脚只需在一个地方进行。
- 结构清晰: 主页面文件更专注于其核心内容。
- 文件夹结构: 将包含文件放在一个单独的目录 (如
includes/或partials/) 是一个好习惯。
路径问题和数据库连接调试
-
DB.php中的数据库路径:- 之前在
DB.php的构造函数中,数据库路径使用了'../data/data.db'。 - 讲师解释,
include和require的行为类似复制粘贴。当DB.php被index.php(位于项目根目录) 包含时,DB.php中的代码实际上是在index.php的上下文中执行的。 - 因此,相对路径
'../data/data.db'(从classes目录出发) 可能会导致找不到文件,因为执行上下文是根目录。 - 解决方案:
- 使用
__DIR__:private $dbPath = __DIR__ . '/../data/data.db';(如果DB.php在classes/目录,这会正确指向project_root/data/data.db)。这是更健壮的方式。 - 或者,如果
DB.php的代码是在根目录的上下文中执行,则路径应相对于根目录:private $dbPath = 'data/data.db';。 (讲师在演示中改为这个,因为include后的执行上下文是index.php所在的根目录)。
- 使用
- 之前在
-
SQLite3Result::fetchArray()的行为:-
讲师最初认为
fetchArray()会返回所有行,但实际上它每次只获取结果集中的一行,并将内部指针移到下一行。 -
为了获取所有行,需要在
DB::executeQuery()方法中使用一个while循环来持续调用fetchArray(),直到它返回false(表示没有更多行),并将每一行收集到一个数组中。// DB.php - executeQuery 方法内的修改 $rows = []; while ($row = $result->fetchArray(SQLITE3_ASSOC)) { $rows[] = $row; } return $rows; // 返回包含所有行的数组
-
"主从视图" (index.php 和 details.php) 逻辑
index.php(主视图):- 从数据库获取所有展品的标题 (或 ID 和标题)。
- 遍历结果,为每个展品创建一个列表项
<li>。 - 每个列表项包含一个链接
<a>,指向details.php。 - 链接中通过 URL 参数传递展品的唯一标识符 (例如
details.php?id=EXHIBIT_ID或details.php?title=URL_ENCODED_TITLE)。
details.php(从视图):- 从 URL 参数 (
$_GET) 中获取传递过来的展品标识符。 - 使用该标识符从数据库中查询特定展品的详细信息。 (理想情况下,SQL 查询应使用
WHERE子句来只获取那一条记录)。 - 将获取到的详细信息渲染到 HTML 结构中。
- 从 URL 参数 (
- 讲师在演示中,为了简化,
details.php仍然获取了所有展品,然后根据 URL 中的索引从数组中选择一个。这不是最高效的方式,但能演示页面间数据传递。更优化的做法是details.php只查询所需的单个展品。
27-passing-data-to-the-details-page
index.php (Master View) - 生成链接
- 目标: 在
index.php中为每个展品生成一个链接,点击后能导航到details.php并显示该展品的详细信息。 - 传递标识符: 需要通过 URL 参数将选定展品的唯一标识符传递给
details.php。- 理想情况: 数据库表应有一个主键 (如
id列)。链接会是details.php?id=123。 - 当前情况 (无
id列): 讲师暂时使用展品的title作为标识符。由于标题可能包含空格或特殊字符,需要使用urlencode()。 - 改进 (使用数组索引): 后来讲师改为使用
foreach循环的索引$i作为参数传递:details.php?index=0,details.php?index=1等。这在当前数据源是整个数组时可行,但如果details.php直接从数据库按 ID 查询则更好。
- 理想情况: 数据库表应有一个主键 (如
<?php
// index.php (部分)
require_once 'classes/classes.php';
require_once 'includes/header.inc.php';
$db = new DB();
// 假设 executeQuery 返回所有展品
$exhibits = $db->executeQuery("SELECT title, description, image FROM exhibits");
?>
<main class="master">
<?php if ($exhibits): ?>
<ul>
<?php foreach ($exhibits as $index => $exhibit): // 获取索引 $index ?>
<li>
<a href="details.php?index=<?php echo $index; ?>">
<?php echo htmlspecialchars($exhibit['title']); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p>No exhibits found.</p>
<?php endif; ?>
</main>
<?php
require_once 'includes/footer.inc.php';
?>
details.php (Detail View) - 接收参数并显示数据
-
获取 URL 参数: 使用
$_GET超全局变量来获取从index.php传递过来的参数。<?php // details.php (顶部) $pageTitle = "Exhibit Details"; // 设置页面标题 require_once 'classes/classes.php'; require_once 'includes/header.inc.php'; $db = new DB(); $exhibits = $db->executeQuery("SELECT title, description, image FROM exhibits"); // 仍然获取所有数据 // 获取 URL 中的 'index' 参数,如果不存在则默认为 0 $selectedIndex = $_GET['index'] ?? 0; $selectedIndex = (int)$selectedIndex; // 确保是整数 $object = null; // 初始化选定的展品对象 // 检查索引是否有效,并获取对应的展品 if ($exhibits && isset($exhibits[$selectedIndex])) { $object = $exhibits[$selectedIndex]; $pageTitle = htmlspecialchars($object['title']) . " - Details"; // 更新页面标题 // 需要在 header.inc.php 输出 $pageTitle 之前重新包含或更新它, // 或者将页面标题逻辑移到 header.inc.php 之后 } else { // 处理索引无效或展品不存在的情况 echo "<p>Error: Exhibit not found.</p>"; // 可以重定向到错误页面或 index.php } ?>-
注意: 上述代码仍然加载所有展品,然后通过索引选取。更高效的做法是修改
DB::executeQuery或添加一个新方法 (如getExhibitByIndex或getExhibitById),使其能够只从数据库获取特定的一个展品,例如使用WHERE子句和可能的LIMIT 1。// 假设有一个 id 列 // $sql = "SELECT title, description, image FROM exhibits WHERE id = :id LIMIT 1"; // 然后使用预处理语句绑定 $id -
Null Coalescing Operator (
??) 用于提供默认索引。 -
(int)$selectedIndex将参数转换为整数。
-
-
渲染展品详情: 如果找到了对应的展品 (
$object不为null),则使用与之前index.php渲染单个展品类似的 HTML 结构来显示其详情。<main> <?php if ($object): ?> <article class="detail-view"> <img src="images/<?= htmlspecialchars($object['image']) ?>" alt="<?= htmlspecialchars($object['title']) ?>"> <div class="info"> <h2><?= htmlspecialchars($object['title']) ?></h2> <p><?= htmlspecialchars($object['description']) ?></p> </div> <p><a href="index.php">« Back to all exhibits</a></p> </article> <?php else: ?> <p>The requested exhibit could not be found. Please <a href="index.php">return to the main list</a>.</p> <?php endif; ?> </main> <?php require_once 'includes/footer.inc.php'; ?>- 这里使用了简化的
article结构,可以根据需要调整。 - 添加了一个返回主列表的链接。
- 这里使用了简化的
优化 details.php 的数据获取
理想情况下,details.php 不应该加载所有展品。DB 类应该有一个方法,例如 getExhibitById(int $id) 或 getExhibitByTitle(string $title) (如果 title 是唯一的),它会执行一个带有 WHERE 子句的 SQL 查询。
示例 (假设 DB 类有一个新方法):
// DB.php (新增方法,使用预处理语句更安全)
public function getExhibitByIndex(int $index): ?array {
// 注意:直接使用索引作为查询条件通常不适用于数据库,除非数据保证顺序
// 更好的方式是基于 ID。这里仅为演示概念。
// 实际上,如果基于索引,你可能需要获取所有数据然后选择,
// 或者使用 LIMIT 和 OFFSET (但 OFFSET 性能可能不佳)。
// 最好的方式是有一个主键 ID。
// 假设我们有一个名为 'row_id' (或类似) 的自增主键,或者使用 LIMIT/OFFSET (不推荐)
// 为了简单起见,如果必须按“索引”获取,但数据库没有直接的行号概念,
// 我们可能还是需要获取所有然后选择,或者使用数据库特定的行号函数。
// 以下是基于主键 ID 的更现实的例子
// public function getExhibitById(int $id): ?array {
// $stmt = $this->sqlite->prepare("SELECT title, description, image FROM exhibits WHERE id = :id");
// $stmt->bindValue(':id', $id, SQLITE3_INTEGER);
// $result = $stmt->execute();
// if ($result) {
// $row = $result->fetchArray(SQLITE3_ASSOC);
// return $row ?: null; // 如果没有找到行,fetchArray 返回 false
// }
// return null;
// }
// 沿用讲师的“获取所有然后按索引选择”的简化逻辑,但应意识到其局限性
$allExhibits = $this->executeQuery("SELECT title, description, image FROM exhibits");
if ($allExhibits && isset($allExhibits[$index])) {
return $allExhibits[$index];
}
return null;
}
// details.php (修改数据获取部分)
<?php
// ... (includes) ...
$db = new DB();
$selectedIndex = (int)($_GET['index'] ?? 0);
// 使用新方法获取单个展品
$object = $db->getExhibitByIndex($selectedIndex); // (或者 getExhibitById 如果有 ID)
if ($object) {
$pageTitle = htmlspecialchars($object['title']) . " - Details";
// 可能需要重新包含 header 或在 header 输出前设置好 pageTitle
}
// ... (后续渲染逻辑) ...
?>
这样 details.php 就只处理它需要的数据了。
28-error-handling
PHP 错误类型和处理的复杂性
- PHP 中的错误处理机制比较多样,历史上也有演变。
- 错误可以分为不同级别:
- Notices (通知): 运行时发生的非严重错误,脚本通常会继续执行 (例如,访问未定义的变量或数组索引)。
- Warnings (警告): 更严重的运行时错误,脚本通常也会继续执行 (例如,
include一个不存在的文件)。 - Errors (错误):
- Parse errors (解析错误/语法错误): 代码不符合 PHP 语法,脚本根本无法开始执行。
- Fatal errors (致命错误): 严重的运行时错误,导致脚本立即终止 (例如,调用未定义的函数,
require不存在的文件,在null上调用方法)。 - Recoverable errors (可恢复错误): 可以被捕获和处理的致命错误 (例如,类型提示不匹配但可以转换)。
- Exceptions (异常):
- PHP 5 引入了更现代的面向对象的异常处理机制 (
try-catch-finally)。 - 许多内置函数和操作现在在出错时会抛出异常,而不是仅仅产生传统错误。
- 用户也可以自定义并抛出异常。
- PHP 5 引入了更现代的面向对象的异常处理机制 (
try-catch 块
-
用于捕获和处理异常。
-
语法:
try { // 可能抛出异常的代码 $result = someFunctionThatMightThrow(); if ($result === false) { throw new Exception("Something went wrong with the result."); } } catch (SpecificExceptionType $e) { // 处理特定类型的异常 // echo "Caught SpecificException: " . $e->getMessage(); } catch (AnotherExceptionType $e) { // 处理另一种特定类型的异常 } catch (Throwable $e) { // Throwable 是所有 Error 和 Exception 的基接口 (PHP 7+) // 捕获所有可抛出的错误和异常 (作为最后的捕获) // echo "An error or exception occurred: " . $e->getMessage(); // $e->getFile(), $e->getLine(), $e->getTraceAsString() 等方法可获取更多信息 } finally { // (可选) 无论是否发生异常,finally 块中的代码总会执行 // 通常用于资源清理 // closeDatabaseConnection(); } -
Throwable: 在 PHP 7+ 中,Exception和Error(代表传统 PHP 错误被封装成的对象) 都实现了Throwable接口。所以catch (Throwable $e)可以捕获几乎所有可捕获的运行时问题。 -
注意: 不是所有的 PHP 传统错误 (如 E_NOTICE, E_WARNING) 都会自动转换为可以被
try-catch捕获的Error对象。这取决于 PHP 版本和配置。
显示/隐藏错误和错误报告级别 (开发 vs. 生产)
-
开发环境:
-
应该显示所有错误、警告和通知,以便开发者及时发现和修复问题。
-
可以在 PHP 脚本顶部设置:
<?php ini_set('display_errors', 1); // 1 = On, 0 = Off ini_set('display_startup_errors', 1); error_reporting(E_ALL); // 报告所有类型的 PHP 错误 ?>ini_set():临时修改php.ini配置指令的值 (仅对当前脚本有效)。display_errors:控制错误是否直接输出到浏览器。error_reporting(E_ALL):设置报告所有级别的错误。PHP 定义了许多E_系列常量 (如E_ERROR,E_WARNING,E_NOTICE,E_PARSE,E_DEPRECATED等)。
-
-
生产环境:
- 绝对不能直接向用户显示详细的 PHP 错误信息,因为这会:
- 暴露敏感信息 (如文件路径、数据库凭据、代码片段)。
- 影响用户体验。
- 应该:
ini_set('display_errors', 0);或在php.ini中设置为Off。- 将错误记录到服务器日志文件中 (
log_errors = On,error_log = /path/to/php-error.log在php.ini中配置)。 - 设置自定义错误处理函数 (
set_error_handler()) 来捕获传统错误,并以友好的方式处理它们 (例如,显示通用错误页面,记录错误详情)。 - 使用
try-catch块来处理预期可能发生的异常,并提供适当的用户反馈或回退逻辑。
- 绝对不能直接向用户显示详细的 PHP 错误信息,因为这会:
示例:处理无效索引
在 details.php 中,如果用户在 URL 中提供了一个不存在的 index:
<?php
// details.php
// ... (includes and setup) ...
$selectedIndex = (int)($_GET['index'] ?? 0);
$object = null;
// 假设 $exhibits 已经从数据库加载
if ($exhibits && $selectedIndex >= 0 && $selectedIndex < count($exhibits)) {
$object = $exhibits[$selectedIndex];
} else {
// 处理无效索引
header("HTTP/1.0 404 Not Found"); // (可选) 发送 404 状态码
echo "<h1>Exhibit Not Found</h1>";
echo "<p>The exhibit you requested does not exist.</p>";
echo "<p><a href='index.php'>Return to gallery</a></p>";
require_once 'includes/footer.inc.php'; // 确保页面结构完整
exit; // 停止脚本执行,防止后续代码输出
}
// ... (后续渲染 $object 的代码) ...
?>
count($exhibits)获取数组中的元素数量。- 检查
$selectedIndex是否在有效范围内。 - 如果索引无效,输出错误消息并使用
exit;(或die();) 终止脚本,防止它尝试渲染不存在的数据。 - 可以考虑发送适当的 HTTP 状态码 (如 404 Not Found)。
exit 或 die
exit;和die();功能相同,它们都会终止当前脚本的执行。- 可以接受一个字符串参数,在终止前输出该字符串:
exit("An error occurred."); - 常用于在发生严重错误或处理完请求后立即停止脚本。
29-wrapping-up
课程回顾与总结
- PHP 基础: 涵盖了 PHP 的核心概念和语法。
- 目标: 让学员能够理解 PHP 文件,掌握服务器端渲染的基本模式。
- 适用场景:
- WordPress, Joomla 等 CMS 的定制 (插件、主题开发)。
- 学习 Laravel, CodeIgniter, Symfony 等 PHP 框架的基础。
- 维护或开发基于 PHP 的 Web 应用。
主要内容回顾
- 什么是 PHP?
- 一种服务器端脚本语言。
- PHP 本身不是 Web 服务器,它依赖 Web 服务器 (如 Apache, Nginx,或内置开发服务器) 来处理 HTTP 请求并执行 PHP 脚本。
- 为什么学习 PHP?
- 仍然是 Web 开发领域的重要技能。
- 市场需求 (维护现有系统,WordPress 生态,PHP 框架)。
- 许多开发者仍在学习和使用。
- PHP 语法基础:
- 变量 (
$),数据类型 (动态类型,但支持类型提示)。 - 字符串 (单引号、双引号、Heredoc, Nowdoc),拼接 (
.),内插。 - 数组 (索引数组、关联数组)。
- 控制结构 (
if,else,switch,for,foreach,while) 及其替代语法。 - 函数 (定义、参数、返回值、类型提示、默认参数、命名参数)。
- 变量 (
- 超全局变量:
$_GET,$_POST(用于处理表单数据和 URL 参数)。$_SERVER(服务器和执行环境信息)。$_COOKIE,$_SESSION(用于 Cookie 和会话管理,课程中简要提及)。
- 面向对象编程 (OOP) in PHP:
- 类 (
class),属性,方法。 - 构造函数 (
__construct),析构函数 (__destruct)。 - 访问修饰符 (
public,protected,private)。 - 构造函数属性提升 (PHP 8.0+)。
- 继承 (
extends),接口 (implements) (简要提及)。
- 类 (
- 处理数据:
- 从 PHP 数组加载数据。
- 从 JSON 文件加载数据 (
file_get_contents,json_decode)。 - 数据库交互 (SQLite):
- 使用
SQLite3类连接和查询数据库。 - 执行 SQL (
query),获取结果 (fetchArray)。 - 提及 PDO 作为数据库抽象层。
- 使用
- 创建 API 端点,返回 JSON 数据 (
json_encode,header('Content-Type: application/json'))。
- 文件包含与代码组织:
include,require,include_once,require_once。- 分离头部和页脚到独立文件。
- 自动加载 (Autoloading): 使用
spl_autoload_register动态加载类文件,避免手动require_once。
- 调试与错误处理:
var_dump()用于调试变量。phpinfo()查看 PHP 配置。try-catch块处理异常。ini_set('display_errors', ...)和error_reporting(E_ALL)控制错误显示 (开发 vs. 生产)。exit或die终止脚本。
未涵盖或可深入学习的内容
- Traits (特性): PHP 中一种代码复用机制,用于在类之间共享方法 (类似其他语言中的 mixins)。
- Namespaces (命名空间): 用于组织代码和避免名称冲突,尤其在大型项目和库中非常重要。
- 高级 OOP: 抽象类、接口的深入使用、设计模式等。
- 更多标准库函数: PHP 拥有庞大的函数库,用于处理字符串、数组、文件系统、日期时间、网络等。
- 引用 (References): 变量按引用传递 (
&$variable),函数参数按引用传递。 - Composer: PHP 的依赖管理工具,用于管理项目库和自动加载。
- PHP 框架: Laravel, Symfony, CodeIgniter, Laminas (原 Zend Framework), CakePHP 等。
- 安全性: XSS 防护 (
htmlspecialchars), SQL 注入防护 (预处理语句), CSRF 防护, 文件上传安全等。 - 性能优化。
- 单元测试和集成测试。
结语
- 本课程旨在提供 PHP 的坚实基础,使学员能够理解和开始使用 PHP 进行 Web 开发。
- 鼓励学员继续学习和探索 PHP 的更高级特性和生态系统。