Skip to content

用户自定义函数

概述

函数是将一段可重复使用的代码封装起来的核心机制。在 PHP 中,用户可以通过 function 关键字定义自己的函数,实现代码的逻辑复用、模块化和抽象。合理使用函数是编写清晰、可维护 PHP 程序的基础。

PHP 函数遵循以下核心规则:

  • 函数名不区分大小写,但推荐统一使用驼峰命名(camelCase)
  • 函数无需在调用前定义(PHP 会先扫描整个文件),但条件定义除外
  • PHP 不支持函数重载(同名函数不能重复定义)
  • 支持递归调用,但需注意栈溢出风险

PHP 版本说明

本文档以 PHP 8.1+ 为基准。函数的基本语法自 PHP 4 起保持稳定,类型声明自 PHP 7.0 起逐步增强,返回类型自 PHP 7.0 引入,never 返回类型自 PHP 8.1 引入。

基础概念

什么是函数

函数(Function)是一个被命名的、可重复执行的代码块。它接收输入(参数),经过处理后可能返回输出(返回值)。函数的主要作用包括:

  1. 代码复用:避免重复编写相同逻辑
  2. 模块化:将复杂问题分解为小模块
  3. 抽象:隐藏实现细节,只暴露接口
  4. 可测试性:独立的函数更容易进行单元测试

函数与语言结构

PHP 中"函数"和"语言结构"是不同的概念。echoissetemptydie 等是语言结构(language construct),不是函数,因此不能作为可变函数调用。

php
<?php

declare(strict_types=1);

// echo 是语言结构,不是函数
echo "Hello, World!\n";

// strlen 是真正的函数,可以用可变函数调用
$func = 'strlen';
echo $func('hello') . "\n"; // 输出: 5

语法与代码

函数定义基本语法

使用 function 关键字定义函数,后面跟函数名、参数列表和函数体。

php
<?php

declare(strict_types=1);

// 最基本的函数定义
function sayHello(): void
{
    echo "Hello, World!\n";
}

sayHello(); // 输出: Hello, World!

// 带参数的函数
function greet(string $name): string
{
    return "Hello, {$name}!";
}

echo greet('Alice') . "\n"; // 输出: Hello, Alice!

// 带默认值的参数
function repeat(string $text, int $times = 2): string
{
    return str_repeat($text, $times);
}

echo repeat('Hi ') . "\n";      // 输出: Hi Hi
echo repeat('Hi ', 3) . "\n";   // 输出: Hi Hi Hi

命名规则

PHP 函数名遵循标识符命名规则,以字母或下划线开头,后面可以跟字母、数字和下划线。

php
<?php

declare(strict_types=1);

// 合法的函数名
function myFunction(): void {}
function my_function(): void {}
function _privateHelper(): void {}
function calculateTotal2(): void {}

// 函数名不区分大小写(但不推荐这样调用)
function MyFunction(): void
{
    echo "Called\n";
}

myfunction();     // 正常调用
MYFUNCTION();     // 正常调用(但不推荐)
MyFunction();     // 推荐:与定义时保持一致

注意事项

虽然 PHP 函数名不区分大小写,但为了代码可读性和团队协作,强烈推荐在定义和调用时保持一致的大小写。使用驼峰命名法(camelCase)是 PSR-12 推荐的风格。

函数定义顺序

PHP 在执行脚本之前会先扫描整个文件,因此函数可以在定义之前调用。但条件定义的函数只能在条件满足后才能调用。

php
<?php

declare(strict_types=1);

// 在定义之前调用(合法)
echo add(2, 3) . "\n"; // 输出: 5

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

// 条件定义的函数
$useStrict = true;

if ($useStrict) {
    function strictAdd(int $a, int $b): int
    {
        return $a + $b;
    }
}

echo strictAdd(10, 20) . "\n"; // 输出: 30

// 以下调用会报错,因为条件不满足
// if (!$useStrict) {
//     function anotherAdd(int $a, int $b): int { ... }
// }
// anotherAdd(1, 2); // Fatal error: Uncaught Error: Call to undefined function

递归函数

递归函数是调用自身的函数。递归需要有明确的终止条件,否则会导致栈溢出。

php
<?php

declare(strict_types=1);

// 计算阶乘
function factorial(int $n): int
{
    if ($n <= 1) {
        return 1;
    }

    return $n * factorial($n - 1);
}

echo factorial(5) . "\n";  // 输出: 120
echo factorial(10) . "\n"; // 输出: 3628800

// 计算斐波那契数列(第 n 项)
function fibonacci(int $n): int
{
    if ($n <= 0) {
        throw new InvalidArgumentException('n must be positive');
    }
    if ($n === 1 || $n === 2) {
        return 1;
    }

    return fibonacci($n - 1) + fibonacci($n - 2);
}

echo fibonacci(10) . "\n"; // 输出: 55

// 递归遍历目录(实用示例)
function listDirectory(string $dir, int $depth = 0): array
{
    $result = [];
    $items = scandir($dir);

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }

        $path = $dir . DIRECTORY_SEPARATOR . $item;
        $prefix = str_repeat('  ', $depth);
        $result[] = $prefix . $item;

        if (is_dir($path)) {
            $result = array_merge($result, listDirectory($path, $depth + 1));
        }
    }

    return $result;
}

递归深度限制

PHP 默认没有硬性的递归深度限制,但受 memory_limit 和栈空间约束。对于深度可能很大的递归(如遍历深层嵌套数据),建议使用迭代方式或尾递归优化(PHP 本身不自动优化尾递归)。

详细说明

函数的组成部分

一个完整的 PHP 函数包含以下部分:

组成部分说明是否必需
function 关键字声明函数的关键字
函数名遵循标识符规则
参数列表括号内的参数,可以为空是(括号必需)
返回类型声明冒号后的类型(PHP 7.0+)否(推荐)
函数体花括号内的代码块

作用域规则

PHP 函数内部有自己的作用域,无法直接访问外部变量。需要使用 global 关键字、use 语言结构或参数传递来访问外部变量。

php
<?php

declare(strict_types=1);

$globalVar = 'I am global';

function accessGlobal(): void
{
    // 无法直接访问 $globalVar
    // echo $globalVar; // Undefined variable

    // 使用 global 关键字(不推荐)
    global $globalVar;
    echo $globalVar . "\n"; // 输出: I am global
}

// 推荐方式:通过参数传递
function useParameter(string $value): void
{
    echo $value . "\n";
}

useParameter($globalVar); // 输出: I am global

最佳实践

避免使用 global 关键字,它会使函数产生隐式依赖,降低代码可测试性。推荐通过参数传递来注入外部值。

函数的返回值

使用 return 语句从函数返回值。如果函数没有 return 语句或 return 后没有值,则返回 null

php
<?php

declare(strict_types=1);

// 无返回值(实际返回 null)
function noReturn(): void
{
    echo "This function has no return statement\n";
}

$result = noReturn();
var_dump($result); // NULL

// 显式返回 null
function returnsNull(): ?string
{
    return null;
}

// 返回多种类型的值(PHP 8.0+ 联合类型)
function flexibleReturn(int $type): int|string|array
{
    return match ($type) {
        1 => 42,
        2 => 'hello',
        3 => [1, 2, 3],
        default => throw new InvalidArgumentException('Invalid type'),
    };
}

实战示例

格式化金额工具函数

php
<?php

declare(strict_types=1);

/**
 * 格式化金额
 *
 * @param float|int $amount  金额数值
 * @param string    $currency 货币符号
 * @param int       $decimals 小数位数
 * @return string 格式化后的金额字符串
 */
function formatCurrency(float|int $amount, string $currency = '¥', int $decimals = 2): string
{
    $formatted = number_format((float) $amount, $decimals);

    return $currency . $formatted;
}

echo formatCurrency(1234.5) . "\n";        // ¥1,234.50
echo formatCurrency(1234.5, '$') . "\n";  // $1,234.50
echo formatCurrency(1234, '€', 0) . "\n"; // €1,235

数据验证函数组

php
<?php

declare(strict_types=1);

function validateEmail(string $email): bool
{
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

function validateAge(int $age): bool
{
    return $age >= 0 && $age <= 150;
}

function validateRequired(array $data, array $fields): array
{
    $errors = [];

    foreach ($fields as $field) {
        if (!isset($data[$field]) || $data[$field] === '') {
            $errors[$field] = "The {$field} field is required";
        }
    }

    return $errors;
}

// 使用示例
$data = ['name' => 'Alice', 'email' => 'alice@example.com'];
$errors = validateRequired($data, ['name', 'email', 'phone']);

if (!empty($errors)) {
    print_r($errors);
    // 输出: Array ( [phone] => The phone field is required )
}

递归实现树形结构输出

php
<?php

declare(strict_types=1);

function printTree(array $nodes, int $parentId = 0, int $level = 0): void
{
    foreach ($nodes as $node) {
        if ($node['parent_id'] !== $parentId) {
            continue;
        }

        $indent = str_repeat('  ', $level);
        $prefix = $level > 0 ? '└─ ' : '';
        echo "{$indent}{$prefix}{$node['name']}\n";

        printTree($nodes, $node['id'], $level + 1);
    }
}

$categories = [
    ['id' => 1, 'name' => '编程语言', 'parent_id' => 0],
    ['id' => 2, 'name' => 'PHP', 'parent_id' => 1],
    ['id' => 3, 'name' => 'Laravel', 'parent_id' => 2],
    ['id' => 4, 'name' => 'JavaScript', 'parent_id' => 1],
    ['id' => 5, 'name' => 'Vue.js', 'parent_id' => 4],
    ['id' => 6, 'name' => '数据库', 'parent_id' => 0],
    ['id' => 7, 'name' => 'MySQL', 'parent_id' => 6],
];

printTree($categories);
// 编程语言
//   └─ PHP
//     └─ Laravel
//   └─ JavaScript
//     └─ Vue.js
// 数据库
//   └─ MySQL

注意事项

常见陷阱

  1. 函数名冲突:PHP 不支持函数重载,定义两个同名函数会导致 Fatal Error
  2. 条件定义风险:条件定义的函数在不同条件下可能未定义,调用前需检查
  3. 递归无终止:递归函数必须有明确的终止条件,否则会导致内存溢出
  4. 大小写陷阱:虽然函数名不区分大小写,但类方法名在 PHP 8.0+ 区分大小写
php
<?php

declare(strict_types=1);

// 错误:不能重复定义函数
// function calculate(): int { return 1; }
// function calculate(): int { return 2; } // Fatal error: Cannot redeclare calculate()

// 安全的条件定义方式
if (!function_exists('calculate')) {
    function calculate(): int
    {
        return 42;
    }
}

函数定义与 include/require

当函数通过 includerequire 引入时,需特别注意多次引入导致的重复定义问题。推荐使用 require_onceinclude_once

php
<?php

declare(strict_types=1);

// 安全引入函数文件
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/utils.php';

// 或者在被引入的文件中保护函数定义
// helpers.php
if (!function_exists('helper_function')) {
    function helperFunction(): void
    {
        // ...
    }
}

最佳实践

1. 单一职责原则

每个函数只做一件事,函数名应清晰描述其功能。

php
<?php

declare(strict_types=1);

// 不推荐:函数做了太多事情
function processUser(string $email, string $name): void
{
    // 验证
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('Invalid email');
    }

    // 保存到数据库
    $db = new PDO('mysql:...');
    $stmt = $db->prepare('INSERT INTO users ...');
    $stmt->execute([$email, $name]);

    // 发送邮件
    mail($email, 'Welcome', 'Hello ' . $name);
}

// 推荐:拆分为多个单一职责函数
function validateUserEmail(string $email): bool
{
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

function saveUser(string $email, string $name): int
{
    $db = new PDO('mysql:...');
    $stmt = $db->prepare('INSERT INTO users (email, name) VALUES (?, ?)');
    $stmt->execute([$email, $name]);

    return (int) $stmt->lastInsertId();
}

function sendWelcomeEmail(string $email, string $name): bool
{
    return mail($email, 'Welcome', "Hello {$name}");
}

2. 始终声明类型

php
<?php

declare(strict_types=1);

// 不推荐:没有类型声明
function calculate($a, $b)
{
    return $a + $b;
}

// 推荐:完整的类型声明
function calculateWithTypes(int $a, int $b): int
{
    return $a + $b;
}

3. 函数长度控制

  • 每个函数建议不超过 20-30 行
  • 超过一屏(约 50 行)应考虑拆分
  • 嵌套层级不超过 3 层

4. 添加文档注释

php
<?php

declare(strict_types=1);

/**
 * 计算两个日期之间的工作日天数
 *
 * @param string $startDate 开始日期(Y-m-d 格式)
 * @param string $endDate   结束日期(Y-m-d 格式)
 * @return int 工作日天数(不含周末)
 * @throws InvalidArgumentException 当日期格式无效时抛出
 */
function getBusinessDays(string $startDate, string $endDate): int
{
    $start = new DateTime($startDate);
    $end = new DateTime($endDate);

    if ($start > $end) {
        throw new InvalidArgumentException('Start date must be before end date');
    }

    $interval = new DateInterval('P1D');
    $period = new DatePeriod($start, $interval, $end);
    $businessDays = 0;

    foreach ($period as $day) {
        $dayOfWeek = (int) $day->format('N');
        if ($dayOfWeek < 6) { // 周一到周五
            $businessDays++;
        }
    }

    return $businessDays;
}

参考链接