跳至主要內容
技術

PHP 不是你記憶中的樣子:寫給現代開發者的 PHP 快速入門

PHP 不是你記憶中的樣子:寫給現代開發者的 PHP 快速入門
PHP/Laravel 完全指南 第 1 / 15 篇

本篇是「PHP/Laravel 完全指南」系列的第 1 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。

「PHP?那不是上個時代的語言嗎?」——如果你腦中閃過這個念頭,我完全理解。十年前的 PHP 確實混亂:沒有型別提示、沒有套件管理、一堆 mysql_* 全域函式散落各處。那個年代寫 PHP 的體驗,大概跟在泥地裡蓋房子差不多。

但 2026 年的 PHP 8.4+,已經是一門完全不同的語言了。它有嚴格的型別系統、Enum、Named Arguments、Property Hooks、Readonly Properties、Match Expression——這些特性放在任何現代語言裡都不會顯得突兀。2024 年 9 月,Laravel 拿到了 Accel 領投的 5,700 萬美金 A 輪融資,這是 PHP 框架生態系商業價值的最強信號。

這一章我們不會從零教你寫 echo "Hello World"。我假設你已經會至少一門程式語言(JavaScript、Python、Go 都行),所以我們用對照表的方式快速帶你過一遍 PHP 8.4+ 的核心語法。目標是讀完這章之後,你看得懂 Laravel 原始碼裡的 PHP,也知道怎麼用 Composer 管理套件。準備好了,我們開始。

2026 年了,PHP 還值得學嗎?

先看數字說話:

  • 71.7%——根據 W3Techs 2026 年 3 月的統計,全球已知伺服器端語言的網站中,有 71.7% 使用 PHP。遙遙領先第二名的 Ruby(約 6.8%)。
  • 42.2%——根據 W3Techs 2026 年 5 月的統計,所有網站中有 42.2% 跑在 WordPress 上,而 WordPress 是純 PHP 寫的。
  • 454,000+——PHP 的套件管理器 Packagist 上有超過 45 萬個套件,累計安裝次數超過 1,800 億次。
  • $57M——Laravel 在 2024 年 9 月完成了 Accel 領投的 A 輪融資,估值和商業前景都在成長。

「但 Stack Overflow 調查裡 PHP 用的人不多啊?」那個調查反映的是「誰在填問卷」,不是「誰在跑 production」。PHP 龐大到不需要被討論——它就是在那裡,安靜地服務著全世界七成以上的網站。

不過上面這幾個數字我得幫你補一下脈絡,不然會誤導你。那 71.7% 跟 42.2% 主要是「存量」——絕大多數是早就架好、跑了很多年的 WordPress 站和老站,不代表 2026 年的新專案都在選 PHP。而 Stack Overflow 那題我也不想全推給「誰在填問卷」:PHP 在「開發者想不想用」這個指標上確實偏弱,薪資中位數比不上 Go/Rust,AI、資料工程那一塊基本上是 Python 的天下,你拿 PHP 去硬擠不會太愉快。我的結論是:別把高市占當成「PHP 是新專案首選」的證據;但如果你要做的是 Web 後端、SaaS、接案、CMS,PHP 到今天仍然是非常務實、CP 值很高的選擇。適不適合,看你要解的是哪種問題,自己判斷比我幫你下定論好。

更重要的是,現代 PHP 和你記憶中的 PHP 完全不同。PHP 的每一個大版本都帶來實質的語法改進:

版本發布年份關鍵特性
PHP 8.02020Named Arguments、Match Expression、Union Types、Nullsafe Operator
PHP 8.12021Enums、Fibers(協程)、Readonly Properties、Intersection Types
PHP 8.22022Readonly Classes、Disjunctive Normal Form Types、true/false/null 獨立型別
PHP 8.32023Typed Class Constants、#[\Override]json_validate()
PHP 8.42024Property Hooks、Asymmetric Visibility、array_find()/array_any()/array_all()
PHP 8.52025Pipe Operator |>、URI Extension、array_first()/array_last()、Clone With(目前最新穩定版)

如果你的 PHP 印象停留在 5.x 時代,請直接跳到 8.4。那是一門不同的語言。

平反歸平反,現代 PHP 還是有幾個老問題你早晚會踩到:

  • 標準函式庫 API 不一致——in_array(needle, haystack)str_contains(haystack, needle) 參數順序剛好相反,函式名有的加底線(str_replace)有的駝峰(array_map),這個亂象到 8.5 都還在。IDE 補全會救你大半,但別期待它像 Python 標準庫那樣整齊。
  • 沒有原生泛型——你寫不出 Collection<User> 讓引擎幫你檢查型別,只能靠 PHPStan / Psalm 加註解,或在 docblock 標 array 形狀來補。
  • == 弱比較跟自動型別轉換的歷史包袱還在,習慣一律用 === 就能避掉大半的雷。
  • 生態系仍然新舊混雜——Google 隨手搜到的教學一大半是 PHP 5 的 procedural 老寫法,跟著抄你會學到一身壞習慣。

我把短板攤出來不是要打臉前面的平反,剛好相反——願意承認缺點的平反,才是有說服力的平反。

環境建置:安裝 PHP 8.4+ 與 Composer

macOS

最簡單的方式是用 Homebrew:

brew install php
php -v   # 確認版本 >= 8.4

Homebrew 預設安裝最新穩定版。如果你需要特定版本:

brew install php@8.4

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.4 php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip
php -v

Windows

推薦使用 Laragon——一鍵安裝 PHP + Composer + MySQL + Nginx,比手動設定 WAMP 省心太多。

安裝 Composer

Composer 是 PHP 的套件管理器,等同於 npm(Node.js)或 pip(Python)。Laravel 本身就是一個 Composer 套件。

# macOS / Linux
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer --version

安裝完 PHP 和 Composer,你就準備好了。後面的章節會用 Composer 來安裝 Laravel。

確認安裝:你的第一行 PHP

建立一個 hello.php 檔案:

<?php

declare(strict_types=1);

echo "Hello from PHP " . PHP_VERSION . PHP_EOL;

執行它:

php hello.php
# Hello from PHP 8.4.x

declare(strict_types=1) 是現代 PHP 的第一行——它強制啟用嚴格型別檢查。為節省篇幅,後續範例省略這兩行(<?phpdeclare(strict_types=1)),但你實際寫檔案時都要加上。

型別系統:PHP 也有 Type Hints 了

PHP 從 7.0 開始引入型別宣告,到了 8.4 已經相當完整。如果你從 TypeScript 或 Python type hints 來的,會覺得很熟悉:

基本型別宣告

<?php

declare(strict_types=1);

function add(int $a, int $b): int
{
    return $a + $b;
}

echo add(3, 4);    // 7
echo add('3', 4);  // ❌ TypeError(strict_types 開啟時)

可為 null 的型別

function findUser(int $id): ?User
{
    // 回傳 User 或 null
    return User::find($id);
}

Union Types(PHP 8.0+)

function formatId(int|string $id): string
{
    return "ID: {$id}";
}

formatId(42);      // ✅
formatId('abc');   // ✅
formatId(3.14);    // ❌ TypeError

Intersection Types(PHP 8.1+)

function process(Countable&Iterator $items): void
{
    // $items 必須同時實作 Countable 和 Iterator
    foreach ($items as $item) {
        // ...
    }
}

跨語言對照

概念PHP 8.4TypeScriptPython
基本型別int, string, float, boolnumber, string, booleanint, str, float, bool
陣列arrayArray<T> / T[]list[T]
可為 null?stringstring | nullOptional[str]
Unionint|stringnumber | stringint | str
回傳型別: int: number-> int
無回傳值: void: void-> None
嚴格模式declare(strict_types=1)預設嚴格mypy 靜態檢查

重點: PHP 的型別檢查是執行期(runtime)的,不像 TypeScript 只在編譯期。如果型別不符,PHP 會直接丟出 TypeError

現代語法快速對照:PHP vs JavaScript vs Python

如果你已經會 JS 或 Python,下面這張表讓你五分鐘看懂 PHP 語法:

變數與常數

// PHP
$name = 'Bobo';              // 變數用 $ 開頭
$age = 35;
const TAX_RATE = 0.05;       // 常數
define('APP_NAME', '揪好買'); // 另一種常數定義方式
// JavaScript
const name = 'Bobo';
let age = 35;
const TAX_RATE = 0.05;
# Python
name = 'Bobo'
age = 35
TAX_RATE = 0.05

字串插值

// PHP —— 雙引號才能插值,單引號不行
$name = 'Bobo';
echo "Hello, {$name}!";    // Hello, Bobo!
echo 'Hello, {$name}!';    // Hello, {$name}!(原樣輸出)
// JavaScript
console.log(`Hello, ${name}!`); // 反引號

陣列(Array)

PHP 的 array 同時扮演了 JS 的 Array 和 Object 的角色:

// 索引陣列(像 JS Array)
$fruits = ['apple', 'banana', 'cherry'];
echo $fruits[0]; // apple

// 關聯陣列(像 JS Object / Python dict)
$user = [
    'name' => 'Bobo',
    'age' => 35,
    'city' => 'Taipei',
];
echo $user['name']; // Bobo
// JavaScript
const fruits = ['apple', 'banana', 'cherry'];
const user = { name: 'Bobo', age: 35, city: 'Taipei' };
# Python
fruits = ['apple', 'banana', 'cherry']
user = {'name': 'Bobo', 'age': 35, 'city': 'Taipei'}

條件判斷

// PHP —— 幾乎跟 JS/C 一樣
if ($age >= 18) {
    echo '成年';
} elseif ($age >= 12) {
    echo '青少年';
} else {
    echo '兒童';
}

注意: PHP 用 elseif(連在一起),不是 Python 的 elif 也不是 JS 的 else if(雖然 else if 分開寫也能用)。

迴圈

// foreach —— PHP 裡最常用的迴圈
$items = ['蔥油餅', '雞排', '珍奶'];

foreach ($items as $item) {
    echo $item . PHP_EOL;
}

// 帶 key 的 foreach(像 Python 的 enumerate)
foreach ($items as $index => $item) {
    echo "{$index}: {$item}" . PHP_EOL;
}

// 關聯陣列的 foreach
$prices = ['雞排' => 70, '珍奶' => 50];
foreach ($prices as $name => $price) {
    echo "{$name} = \${$price}" . PHP_EOL;
}

函式

// PHP
function greet(string $name, string $greeting = '你好'): string
{
    return "{$greeting}, {$name}!";
}

echo greet('Bobo');           // 你好, Bobo!
echo greet('Bobo', '哈囉');   // 哈囉, Bobo!

Named Arguments、Enums、Match Expression

這三個是從 PHP 8.0/8.1 開始的殺手級特性。

Named Arguments(PHP 8.0+)

不用再數參數順序了:

function createProduct(
    string $name,
    int $price,
    string $category = '其他',
    bool $isActive = true,
): array {
    return compact('name', 'price', 'category', 'isActive');
}

// 傳統呼叫——你得記住每個位置
createProduct('雞排', 70, '小吃', true);

// Named Arguments——清楚明瞭
createProduct(
    name: '雞排',
    price: 70,
    category: '小吃',
);
// isActive 用預設值 true,不用特別傳

// 還可以跳過中間的參數
createProduct(name: '珍奶', price: 50, isActive: false);

JS/Python 開發者注意: Python 一直有 keyword arguments,JS 則是用解構物件來模擬。PHP 的 Named Arguments 是語言原生支援,IDE 自動補全很方便。

Enums(PHP 8.1+)

終於有了原生 Enum,不用再 const STATUS_ACTIVE = 1 這樣土法煉鋼:

// 基本 Enum
enum OrderStatus
{
    case Pending;
    case Confirmed;
    case Shipped;
    case Delivered;
    case Cancelled;
}

function updateOrder(int $orderId, OrderStatus $status): void
{
    // $status 只能是上面五個值之一,型別安全
}

updateOrder(1, OrderStatus::Confirmed);  // ✅
updateOrder(1, 'confirmed');             // ❌ TypeError

// Backed Enum(對應資料庫值)
enum OrderStatus: string
{
    case Pending = 'pending';
    case Confirmed = 'confirmed';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';
}

// 從資料庫值反查
$status = OrderStatus::from('confirmed');  // OrderStatus::Confirmed
$status = OrderStatus::tryFrom('invalid'); // null(不會拋例外)

// Enum 還能有方法
enum OrderStatus: string
{
    case Pending = 'pending';
    case Confirmed = 'confirmed';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending => '待確認',
            self::Confirmed => '已成團',
            self::Shipped => '已出貨',
            self::Delivered => '已送達',
            self::Cancelled => '已取消',
        };
    }
}

echo OrderStatus::Confirmed->label(); // 已成團

在我們後面打造「揪好買」團購平台時,Enum 會大量用在訂單狀態、團購狀態等地方。

Match Expression(PHP 8.0+)

matchswitch 的現代替代品——更簡潔、型別安全、回傳值:

// 傳統 switch(容易忘記 break)
switch ($status) {
    case 'pending':
        $label = '待確認';
        break;
    case 'confirmed':
        $label = '已成團';
        break;
    default:
        $label = '未知';
        break;
}

// match(更優雅)
$label = match ($status) {
    'pending' => '待確認',
    'confirmed' => '已成團',
    'shipped', 'delivered' => '已出貨/到貨',  // 多值對應
    default => '未知',
};

match vs switch 的關鍵差異:

  • match 用嚴格比較(===),不會有 0 == 'foo' 這種坑
  • match 是表達式,可以直接賦值
  • 沒有 break,不會 fall-through
  • 沒匹配到且沒 default 會拋 UnhandledMatchError

現代 PHP 的類別語法:Constructor Promotion 到 Property Hooks(8.0–8.4)

Constructor Promotion(PHP 8.0+)

這大概是讓 PHP class 寫法最精簡的一個特性:

// 傳統寫法(囉嗦)
class Product
{
    public string $name;
    public int $price;
    public string $category;

    public function __construct(string $name, int $price, string $category)
    {
        $this->name = $name;
        $this->price = $price;
        $this->category = $category;
    }
}

// Constructor Promotion(一行搞定)
class Product
{
    public function __construct(
        public string $name,
        public int $price,
        public string $category,
    ) {}
}

$product = new Product('雞排', 70, '小吃');
echo $product->name; // 雞排

TypeScript 開發者會覺得很像: constructor(public name: string) 的概念是一樣的。

Readonly Properties(PHP 8.1+)

設定一次就不能改,適合用在值物件和 DTO:

class GroupBuy
{
    public function __construct(
        public readonly string $title,
        public readonly int $minParticipants,
        public readonly \DateTimeImmutable $deadline,
    ) {}
}

$group = new GroupBuy('辦公室零食團', 5, new \DateTimeImmutable('2026-07-01'));
echo $group->title;         // 辦公室零食團
$group->title = '改名';     // ❌ Error: Cannot modify readonly property

Property Hooks(PHP 8.4+)

這是 PHP 8.4 最重磅的特性——類似 C# 的 getter/setter 或 Kotlin 的 property delegation:

class Temperature
{
    public float $celsius {
        set(float $value) {
            if ($value < -273.15) {
                throw new \ValueError('低於絕對零度');
            }
            $this->celsius = $value;
        }
    }

    public float $fahrenheit {
        get => $this->celsius * 9 / 5 + 32;
        set(float $value) => $this->celsius = ($value - 32) * 5 / 9;
    }
}

$temp = new Temperature();
$temp->celsius = 100;
echo $temp->fahrenheit;  // 212

$temp->fahrenheit = 32;
echo $temp->celsius;     // 0

Asymmetric Visibility(PHP 8.4+)

讀取和寫入可以有不同的存取權限:

class User
{
    public function __construct(
        public private(set) string $name,    // 外部可讀,只有內部可寫
        public protected(set) string $email, // 外部可讀,子類別可寫
    ) {}
}

$user = new User('Bobo', 'bobo@example.com');
echo $user->name;        // ✅ 可以讀
$user->name = '改名';    // ❌ Error: Cannot modify private(set) property

Arrow Functions 與 Closure

Closure(匿名函式)

PHP 的 Closure 跟 JS 的匿名函式類似,但有一個關鍵差異——不會自動捕獲外部變數,必須用 use 明確宣告:

$taxRate = 0.05;

// PHP Closure——必須用 use 捕獲外部變數
$calculateTax = function (int $price) use ($taxRate): float {
    return $price * $taxRate;
};

echo $calculateTax(1000); // 50.0
// JavaScript——自動捕獲(closure)
const taxRate = 0.05;
const calculateTax = (price) => price * taxRate;

這是很多 JS 開發者初學 PHP 最困惑的地方。PHP 的設計哲學是「顯式優於隱式」,use 讓你明確知道 closure 依賴了哪些外部變數。

Arrow Functions(PHP 7.4+)

短語法,自動捕獲外部變數,只能有單一表達式:

$taxRate = 0.05;

// Arrow Function —— 自動捕獲,不用 use
$calculateTax = fn(int $price): float => $price * $taxRate;

echo $calculateTax(1000); // 50.0

// 常見用途:陣列操作
$prices = [100, 200, 300];
$withTax = array_map(fn($p) => $p * (1 + $taxRate), $prices);
// [105.0, 210.0, 315.0]

陣列高階函式

PHP 8.4 新增了更多實用的陣列函式,讓函式風格程式設計更方便:

$products = [
    ['name' => '雞排', 'price' => 70, 'active' => true],
    ['name' => '珍奶', 'price' => 50, 'active' => true],
    ['name' => '臭豆腐', 'price' => 60, 'active' => false],
];

// array_find(PHP 8.4)—— 找到第一個符合條件的
$cheap = array_find($products, fn($p) => $p['price'] < 55);
// ['name' => '珍奶', 'price' => 50, 'active' => true]

// array_any / array_all(PHP 8.4)
$hasInactive = array_any($products, fn($p) => !$p['active']);  // true
$allActive = array_all($products, fn($p) => $p['active']);     // false

// 經典的 array_map / array_filter
$activeNames = array_map(
    fn($p) => $p['name'],
    array_filter($products, fn($p) => $p['active']),
);
// ['雞排', '珍奶']

JS 開發者注意: PHP 的 array_filterarray_map全域函式而非陣列方法,參數順序跟 JS 不同(array_map(callback, array) vs array.map(callback))。這是 PHP 最讓人不適應的地方之一,但習慣了就好。Laravel 的 Collection 類別會幫你解決這個問題——到第四章 Eloquent ORM你就會看到。

Composer:PHP 的 npm / pip

Composer 是 PHP 生態系的基石。截至 2026 年,Packagist 上有超過 45 萬個套件,累計安裝超過 1,800 億次。Laravel 本身就是透過 Composer 安裝的。

基本操作

# 建立新專案(從現有套件)
composer create-project laravel/laravel my-project

# 安裝相依套件(等同 npm install)
composer install

# 新增套件(等同 npm install package-name)
composer require guzzlehttp/guzzle

# 新增開發用套件(等同 npm install --save-dev)
composer require --dev pestphp/pest

# 更新所有套件
composer update

# 移除套件
composer remove guzzlehttp/guzzle

composer.json vs package.json

概念Composer (PHP)npm (Node.js)pip (Python)
設定檔composer.jsonpackage.jsonpyproject.toml
Lock 檔composer.lockpackage-lock.jsonrequirements.txt
套件目錄vendor/node_modules/site-packages/
執行指令composer runnpm run
全域安裝composer global requirenpm install -gpip install

一個典型的 composer.json

版本說明:本系列以 Laravel 12 為基準。Laravel 13 已於 2026 年 3 月 17 日正式發布,若你跟著做時裝到 13,核心概念完全相同,只需將 ^12.0 改為 ^13.0,其餘無需調整。

{
  "name": "bobo/jiu-hao-mai",
  "description": "揪好買——台灣團購平台",
  "type": "project",
  "require": {
    "php": "^8.4",
    "laravel/framework": "^12.0"
  },
  "require-dev": {
    "pestphp/pest": "^3.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    }
  }
}

PSR-4 自動載入

PHP 不像 JS 有 import/require,也不像 Python 有 import。PHP 的模組系統是透過 Composer 的 PSR-4 autoloading 來實現的:

app/Models/Product.php

// 不用手動 require 每一個檔案
// Composer 會根據 namespace 自動找到對應的檔案

namespace App\Models;

class Product
{
    // ...
}

app/Http/Controllers/ProductController.php

namespace App\Http\Controllers;

use App\Models\Product;  // 自動載入 app/Models/Product.php

class ProductController
{
    public function index()
    {
        $products = Product::all();
    }
}

規則很簡單:Namespace 對應目錄路徑App\Models\Product 對應 app/Models/Product.php。Composer 的 autoloader 幫你處理所有的檔案載入,你只需要寫 use

這跟 Python 的模組系統很像——import app.models.product 對應 app/models/product.py。差別是 PHP 用 \ 而不是 .,且 namespace 宣告是手動的。

PSR 標準:PHP 社群的共識

PSR(PHP Standards Recommendations)是 PHP-FIG(Framework Interoperability Group)制定的標準,確保不同框架和套件之間能互通。你不需要全部背下來,但知道這幾個最常見的就夠:

標準名稱一句話說明對應其他生態系
PSR-1Basic Coding Standard基本的命名規範(類別用 PascalCase、方法用 camelCase)ESLint 基本規則
PSR-4Autoloading StandardNamespace 對應目錄結構的自動載入規則Node.js module resolution
PSR-12Extended Coding Style程式碼風格規範(縮排、大括號位置等)Prettier / Black
PSR-7HTTP Message InterfaceHTTP Request/Response 的標準介面Express 的 req/res
PSR-11Container InterfaceDI Container 的標準介面

實務上,Laravel 完全遵循 PSR-4 和 PSR-12。你安裝一個叫 Laravel Pint 的工具,就能一鍵格式化程式碼:

# 安裝(Laravel 12 內建)
# 格式化所有 PHP 檔案
./vendor/bin/pint

就像 JavaScript 有 Prettier、Python 有 Black——PHP 有 Pint。風格爭論到此結束。

小結:準備好進入 Laravel 了

這一章我們用最快的速度帶你走過了現代 PHP 的核心:

  • 型別系統——intstring、Union Types、Intersection Types,執行期檢查
  • 現代語法——Named Arguments、Enums、Match Expression、Constructor Promotion
  • PHP 8.4 新特性——Property Hooks、Asymmetric Visibility、array_find()/array_any()/array_all()
  • Closure 與 Arrow Functions——注意 use 關鍵字的差異
  • Composer——套件管理、PSR-4 自動載入
  • PSR 標準——社群共識、Pint 格式化工具

如果你是 JavaScript 開發者,最需要適應的是:

  1. 變數要加 $
  2. Closure 要用 use 捕獲外部變數
  3. 陣列操作是全域函式而非方法(但 Laravel Collection 會解決這個問題)
  4. Namespace 用 \ 不是 ./

如果你是 Python 開發者,最需要適應的是:

  1. 每行結尾要加 ;
  2. 大括號 {} 而不是縮排
  3. $ 開頭的變數
  4. -> 而不是 . 存取屬性和方法

這些都是語法糖衣的差異,核心概念(型別、函式、類別、模組化)是通用的。你已經會程式設計了,只是在學一門新的方言。

下一章,我們正式進入 Laravel 12——從 composer create-project 開始,十分鐘內讓「揪好買」的專案骨架跑起來。

留言討論

esc
輸入關鍵字搜尋文章...
查看收藏 →