Skip to content

命名参数

概述

命名参数(Named Arguments)是 PHP 8.0 引入的重要特性,允许在调用函数时通过参数名传递值,而不仅仅依赖位置。这一特性使代码更具可读性、更安全,尤其在调用具有多个可选参数的函数时非常实用。

PHP 版本说明

命名参数自 PHP 8.0 引入。它与位置参数、可变参数和参数展开完全兼容。PHP 8.1 进一步增强了与 First-class 可调用语法的配合使用。

基础概念

命名参数 vs 位置参数

特性位置参数命名参数
引入版本PHP 所有版本PHP 8.0+
语法func($a, $b)func(a: 1, b: 2)
参数顺序必须匹配定义顺序可以任意顺序
跳过可选参数不支持支持
可读性需要查看函数签名参数名即文档
重构安全性参数重命名会影响调用方IDE 可以自动更新

语法与代码

基本语法

命名参数使用 参数名: 值 的语法,类似于数组键值对的写法。

php
<?php

declare(strict_types=1);

function createUser(
    string $name,
    string $email,
    int $age = 18,
    string $role = 'user'
): array {
    return compact('name', 'email', 'age', 'role');
}

// 传统位置参数调用
$user1 = createUser('Alice', 'alice@example.com', 30, 'admin');

// 命名参数调用
$user2 = createUser(
    name: 'Alice',
    email: 'alice@example.com',
    age: 30,
    role: 'admin'
);

// 两种方式结果完全相同
print_r($user2);
// Array ( [name] => Alice [email] => alice@example.com [age] => 30 [role] => admin )

跳过可选参数

命名参数最大的优势之一是可以跳过中间的可选参数,直接指定想要修改的参数。

php
<?php

declare(strict_types=1);

function setCookie(
    string $name,
    string $value = '',
    int $expiresOrOptions = 0,
    string $path = '/',
    string $domain = '',
    bool $secure = false,
    bool $httponly = false
): string {
    return "Set-Cookie: {$name}={$value}; Path={$path}; Domain={$domain}; " .
           "Secure=" . ($secure ? 'true' : 'false') . "; HttpOnly=" . ($httponly ? 'true' : 'false');
}

// 使用位置参数:只想设置 httponly,却需要传入所有中间参数
$cookie1 = setCookie('session', 'abc123', 0, '/', '', false, true);

// 使用命名参数:只需指定关心的参数
$cookie2 = setCookie(
    name: 'session',
    value: 'abc123',
    httponly: true
);

echo $cookie2 . "\n";
// Set-Cookie: session=abc123; Path=/; Domain=; Secure=false; HttpOnly=true

参数顺序无关

使用命名参数时,参数的传递顺序可以与定义顺序不同。

php
<?php

declare(strict_types=1);

function formatDate(
    string $input,
    string $inputFormat = 'Y-m-d',
    string $outputFormat = 'd/m/Y',
    string $timezone = 'UTC'
): string {
    $date = DateTime::createFromFormat($inputFormat, $input, new DateTimeZone($timezone));

    return $date ? $date->format($outputFormat) : 'Invalid date';
}

// 任意顺序传参
echo formatDate(input: '2024-01-15', outputFormat: 'F j, Y') . "\n";
// January 15, 2024

echo formatDate(outputFormat: 'j F Y', input: '2024-01-15', timezone: 'Asia/Shanghai') . "\n";
// 15 January 2024

与位置参数混用

命名参数可以与位置参数混合使用,但命名参数必须放在位置参数之后。

php
<?php

declare(strict_types=1);

function query(
    string $table,
    array $where = [],
    array $orderBy = [],
    int $limit = 100,
    int $offset = 0
): string {
    $sql = "SELECT * FROM {$table}";

    if (!empty($where)) {
        $conditions = array_map(fn($k, $v) => "{$k} = '{$v}'", array_keys($where), $where);
        $sql .= ' WHERE ' . implode(' AND ', $conditions);
    }

    if (!empty($orderBy)) {
        $sql .= ' ORDER BY ' . implode(', ', $orderBy);
    }

    $sql .= " LIMIT {$limit} OFFSET {$offset}";

    return $sql;
}

// 位置参数在前,命名参数在后
echo query('users', limit: 10, offset: 20) . "\n";
// SELECT * FROM users LIMIT 10 OFFSET 20

echo query('users', ['status' => 'active'], orderBy: ['created_at' => 'DESC']) . "\n";
// SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC

顺序规则

位置参数必须在命名参数之前。不能在位置参数之前使用命名参数,也不能在命名参数之后再用位置参数。

php
<?php

declare(strict_types=1);

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

// 合法:位置参数在前,命名参数在后
echo example(1, c: 3, b: 2) . "\n"; // 6

// 非法:命名参数后跟位置参数
// echo example(a: 1, 2, 3);        // Error

在内部函数调用中使用

命名参数对 PHP 内置函数同样适用,这在处理参数众多的内置函数时尤其有用。

php
<?php

declare(strict_types=1);

// PHP 内置函数同样支持命名参数
// array_slice(array $array, int $offset, ?int $length = null, bool $preserve_keys = false)

$fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry'];

// 传统方式:需要传入 offset 和 length 才能指定 preserve_keys
$slice1 = array_slice($fruits, 2, null, true);

// 命名参数方式:直接指定 preserve_keys
$slice2 = array_slice($fruits, offset: 2, preserve_keys: true);

print_r($slice2);
// Array ( [2] => cherry [3] => date [4] => elderberry )

// htmlspecialchars 的 flags 参数
$html = '<p>Hello & "World"</p>';
$safe = htmlspecialchars(
    string: $html,
    flags: ENT_QUOTES | ENT_HTML5,
    double_encode: false
);

echo $safe . "\n";
// &lt;p&gt;Hello &amp; &quot;World&quot;&lt;/p&gt;

详细说明

参数名解析规则

命名参数使用函数签名中定义的参数名。在调用父类方法时,子类可以改变参数名,PHP 会根据实际调用的类来解析参数名。

php
<?php

declare(strict_types=1);

class ParentClass
{
    public function process(
        int $firstArg,
        int $secondArg
    ): int {
        return $firstArg + $secondArg;
    }
}

class ChildClass extends ParentClass
{
    // 子类可以修改参数名(PHP 8.0 允许)
    public function process(
        int $renamedArg1,
        int $renamedArg2
    ): int {
        return $renamedArg1 * $renamedArg2;
    }
}

$child = new ChildClass();

// 根据实际类的方法签名解析参数名
echo $child->process(firstArg: 3, secondArg: 4) . "\n";
// 这里会使用 ParentClass 的参数名解析,所以 firstArg 对应 renamedArg1
// 输出: 12

兼容性注意

PHP 8.0 允许子类方法的参数名与父类不同,但这可能导致使用命名参数时出现混淆。PHP 团队建议子类方法的参数名应与父类保持一致。

展开运算符与命名参数

PHP 8.0+ 中,使用 ... 展开数组时支持包含命名参数的关联数组。

php
<?php

declare(strict_types=1);

function buildQuery(
    string $table,
    string $select = '*',
    array $where = [],
    int $limit = 100
): string {
    $sql = "SELECT {$select} FROM {$table}";
    if (!empty($where)) {
        $conditions = array_map(fn($k, $v) => "{$k} = ?", array_keys($where));
        $sql .= ' WHERE ' . implode(' AND ', $conditions);
    }
    $sql .= " LIMIT {$limit}";

    return $sql;
}

// 展开包含命名参数的数组
$params = [
    'select' => 'id, name, email',
    'limit' => 50,
];

echo buildQuery('users', ...$params) . "\n";
// SELECT id, name, email FROM users LIMIT 50

// 混合展开和直接指定
$commonParams = [
    'where' => ['status' => 'active'],
];

echo buildQuery('products', ...$commonParams, limit: 20) . "\n";
// SELECT * FROM products WHERE status = 'active' LIMIT 20

不能传递未知参数名

PHP 会在编译时检查命名参数名是否存在于函数签名中,传入未知参数名会导致错误。

php
<?php

declare(strict_types=1);

function greet(string $name): string
{
    return "Hello, {$name}!";
}

// 正确
echo greet(name: 'Alice') . "\n"; // Hello, Alice!

// 错误:未知命名参数
// echo greet(firstName: 'Alice');
// Fatal error: Unknown named parameter $firstName

实战示例

数据库查询构建器

php
<?php

declare(strict_types=1);

function buildSelectQuery(
    string $table,
    array $columns = ['*'],
    array $conditions = [],
    array $orderBy = [],
    int $limit = 0,
    int $offset = 0,
    string $groupBy = ''
): string {
    $sql = 'SELECT ' . implode(', ', $columns) . " FROM {$table}";

    if (!empty($conditions)) {
        $where = array_map(
            fn(string $col, mixed $val) => is_null($val)
                ? "{$col} IS NULL"
                : "{$col} = ?",
            array_keys($conditions),
            $conditions
        );
        $sql .= ' WHERE ' . implode(' AND ', $where);
    }

    if ($groupBy !== '') {
        $sql .= " GROUP BY {$groupBy}";
    }

    if (!empty($orderBy)) {
        $orderParts = array_map(
            fn(string $col, string $dir) => "{$col} {$dir}",
            array_keys($orderBy),
            $orderBy
        );
        $sql .= ' ORDER BY ' . implode(', ', $orderParts);
    }

    if ($limit > 0) {
        $sql .= " LIMIT {$limit}";
        if ($offset > 0) {
            $sql .= " OFFSET {$offset}";
        }
    }

    return $sql;
}

// 利用命名参数灵活构建查询
echo buildSelectQuery(
    table: 'orders',
    columns: ['id', 'total', 'created_at'],
    conditions: ['status' => 'completed', 'user_id' => 42],
    orderBy: ['created_at' => 'DESC'],
    limit: 10
) . "\n";
// SELECT id, total, created_at FROM orders
// WHERE status = ? AND user_id = ?
// ORDER BY created_at DESC LIMIT 10

HTTP 客户端请求封装

php
<?php

declare(strict_types=1);

function httpClient(
    string $url,
    string $method = 'GET',
    array $headers = [],
    array $data = [],
    int $timeout = 30,
    bool $verifySsl = true,
    string $contentType = 'application/json'
): string {
    $options = [
        'method' => $method,
        'timeout' => $timeout,
        'verify_ssl' => $verifySsl,
        'content_type' => $contentType,
    ];

    // 模拟请求构建
    $output = "{$method} {$url}\n";
    $output .= "Content-Type: {$contentType}\n";
    foreach ($headers as $key => $value) {
        $output .= "{$key}: {$value}\n";
    }

    return $output . json_encode($options, JSON_PRETTY_PRINT);
}

// 使用命名参数,清晰表达每个参数的含义
$request = httpClient(
    url: 'https://api.example.com/users',
    method: 'POST',
    headers: ['Authorization' => 'Bearer token123'],
    data: ['name' => 'Alice'],
    timeout: 10,
    contentType: 'application/x-www-form-urlencoded'
);

echo $request . "\n";

注意事项

命名参数的限制

  1. 不支持动态参数名:参数名必须是字面量,不能通过变量指定
php
<?php

declare(strict_types=1);

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

$paramName = 'a';
// test($paramName: 1);  // 解析错误:不能使用变量作为参数名
// test($$paramName: 1); // 同样不行
  1. 与可变参数的交互
php
<?php

declare(strict_types=1);

function variadicExample(int ...$numbers): int
{
    return array_sum($numbers);
}

// 可变参数可以通过命名参数传递
echo variadicExample(numbers: [1, 2, 3]) . "\n"; // 无效:可变参数没有命名
echo variadicExample(1, 2, 3) . "\n";            // 正确:使用位置参数
  1. 构造函数的命名参数
php
<?php

declare(strict_types=1);

class User
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly int $age = 18,
    ) {}
}

// 使用命名参数创建对象(PHP 8.0+)
$user = new User(
    email: 'alice@example.com',
    name: 'Alice',
    age: 30
);

echo "{$user->name} ({$user->age})\n"; // Alice (30)

... 展开同名覆盖

使用 ... 展开后,可以再通过命名参数覆盖已展开的值。

php
<?php

declare(strict_types=1);

function configure(
    string $host = 'localhost',
    int $port = 80,
    bool $debug = false
): void {
    echo "Host: {$host}, Port: {$port}, Debug: " . ($debug ? 'true' : 'false') . "\n";
}

$defaults = ['host' => '127.0.0.1', 'port' => 8080];

// 展开后再用命名参数覆盖 port
configure(...$defaults, port: 9090);
// 输出: Host: 127.0.0.1, Port: 9090, Debug: false

覆盖顺序

同一参数不能多次传递,包括通过展开和直接指定。但通过展开后再用命名参数覆盖是允许的,后指定的值会覆盖先指定的值。

最佳实践

1. 高可读性的 API 调用

对于参数众多的函数或方法,优先使用命名参数以提高代码可读性。

php
<?php

declare(strict_types=1);

// 不推荐:很难理解每个值的含义
$image = imagecrop($img, ['x' => 10, 'y' => 20, 'width' => 100, 'height' => 50]);

// 推荐:命名参数使意图一目了然
$image = imagecrop(
    image: $img,
    rectangle: ['x' => 10, 'y' => 20, 'width' => 100, 'height' => 50]
);

2. 构建选项数组时使用命名参数

php
<?php

declare(strict_types=1);

function parseHtml(
    string $content,
    bool $stripTags = true,
    string $encoding = 'UTF-8',
    bool $preserveLineBreaks = false,
    int $maxLineLength = 0
): string {
    if ($stripTags) {
        $content = strip_tags($content);
    }

    return mb_substr($content, 0, $maxLineLength > 0 ? $maxLineLength : mb_strlen($content), $encoding);
}

// 从配置中构建选项
$options = [
    'stripTags' => false,
    'preserveLineBreaks' => true,
];

echo parseHtml(content: '<h1>Hello</h1>', ...$options) . "\n";

3. 在编写库/API 时考虑命名参数

设计函数时,参数名应具有描述性,让调用方通过命名参数就能理解含义。

php
<?php

declare(strict_types=1);

// 好的参数名设计
function sendNotification(
    string $recipient,     // 清晰:通知接收者
    string $message,        // 清晰:通知内容
    string $channel = 'email',  // 清晰:通知渠道
    int $priority = 1,          // 清晰:优先级
    bool $silent = false,       // 清晰:静默发送
): bool {
    return true;
}

// 调用时参数名本身就是文档
sendNotification(
    recipient: 'user@example.com',
    message: 'Your order has been shipped',
    channel: 'sms',
    priority: 3,
);

参考链接