PHP 静态方法:何时使用,如何使用?
静态方法是 Laravel 和 PHP 开发人员必备的知识。在本指南中,我将通过简单的 Laravel/PHP 示例来解释何时使用静态方法。让我们开始吧!
静态方法可以用于简单类
例如,假设您有一个依赖服务来保存数据的控制器:
use App\Services\PropertyService;
use App\Http\Requests\StorePropertyRequest;
class PropertyController extends Controller
{
public function store(StorePropertyRequest $request)
{
PropertyService::save($request->allValid());
//附加逻辑并在此处返回...
}
}
您可能会问,服务中的 save()
方法可以是静态的吗?答案是可以的:
namespace App\Services;
use App\Models\Property;
class PropertyService
{
public static function save(array $propertyDetails): Property
{
$property = Property::add($propertyDetails);
// 可以在此处添加一些其他逻辑...
return $property;
}
}
这段代码看起来很不错,对吗?这是因为该方法是“无状态的”,这意味着它不依赖于其他类或变量。它只是获取输入数据并将其保存到数据库中。
在这种简单的情况下,使用静态方法是合理的。
但是,随着代码变得复杂,挑战就会出现,简单性就会消失。
当静态方法需要类参数时会发生什么?
例如,假设您有一个服务类 PropertyService
,它需要使用城市名称进行初始化。您可以使用 PHP 8 中的构造函数属性提升来实现此目的:
namespace App\Services;
class PropertyService
{
public function __construct(public string $city)
{}
public static function save(array $propertyDetails): Property
{
// 问题:$this->city 无法在静态方法中使用
$propertyDetails['cityName'] = $this->city;
return Property::add($propertyDetails);
}
}
现在,让我们考虑如何在传递城市名称时从控制器调用 PropertyService
。使用当前代码,我们似乎正在尝试执行以下操作:
PropertyService::save($propertyDetails);
PropertyService::save($propertyDetails);
但是,这会失败,因为 PropertyService
类没有正确初始化。
当静态方法需要类参数时,会出现一个限制:您无法使用构造函数初始化类参数。这可能会导致在方法中传递和使用某些参数变得具有挑战性。
静态方法调用其他方法的挑战
静态方法不能无缝调用同一类中的其他方法,这是常见的误解。让我们通过一个场景来了解为什么。
假设您有一个 PropertyService
类,需要从属性地址派生纬度/经度坐标。以下是尝试使用静态方法实现此目的的代码:
namespace App\Services;
class PropertyService
{
public static function save(array $propertyDetails): Property
{
$location = $this->fetchLocation($propertyDetails['address']);
$propertyDetails['latitude'] = $location[0];
$propertyDetails['longitude'] = $location[1];
return Property::add($propertyDetails);
}
private function fetchLocation(string $address): array
{
// For the sake of demonstration, let's use hardcoded coordinates
return [0.0001, -0.0001];
}
}
这段代码会抛出错误:
Using $this when not in object context
这是因为静态方法无法访问 $this
变量。 $this
变量是指向类实例的引用,而静态方法不依赖于类实例。
解决此问题的一种方法是使用 self::
引用而不是 $this->
。例如:
$location = self::fetchLocation($propertyDetails['address']);
另一种方法是将类中的所有方法都设为静态。但是,这不是最佳实践,因为它会使类变得更加混乱。
调用外部类的静态方法面临的挑战
在学习 Laravel 面向对象编程(OOP)时,开发人员可能会遇到静态方法需要调用另一个类的情况。这可能会带来一些挑战,我们将在下面进行讨论。
假设您决定将地理定位职责委托给一个新的服务类 GeolocationService
。
namespace App\Services;
class GeolocationService
{
public function fetchCoordinatesFromAddress(string $address): array
{
// Returning hardcoded coordinates for illustration
return [0.0001, -0.0001];
}
}
现在的问题是,PropertyService
如何访问这个新类?
namespace App\Services;
class PropertyService
{
public static function save(array $propertyDetails): Property
{
$location = (new GeolocationService())->fetchCoordinatesFromAddress($propertyDetails['address']);
$propertyDetails['latitude'] = $location[0];
$propertyDetails['longitude'] = $location[1];
return Property::add($propertyDetails);
}
}
这段代码可以正常工作,但如果 GeolocationService
中的多个方法都需要使用 PropertyService
,那么反复创建新实例会降低效率。
在 Laravel 中,开发人员通常会使用 Laravel 的自动解析来注入依赖项。但为了说明将静态方法与外部类或内部方法一起使用可能存在的问题,我们可以考虑使用构造函数注入依赖项的传统方法。
namespace App\Services;
class PropertyService
{
public function __construct(public GeolocationService $geoService)
{}
public static function save(array $propertyDetails): Property
{
$location = $this->geoService->fetchCoordinatesFromAddress($propertyDetails['address']);
// ... rest of the code ...
}
}
这段代码将抛出一个错误,因为静态方法无法访问构造函数中注入的依赖项。
尝试使用 self::
解决方案也将失败,因为它会导致未定义常量错误。
因此,结论是:将静态方法与外部类或内部方法一起使用可能很棘手,而且通常弊大于利。
了解静态方法的测试挑战:模拟测试
测试是开发过程的重要组成部分。模拟测试是一种常见的方法,尤其是在处理外部服务类时。模拟可以帮助您专注于测试应用程序的核心功能,而无需深入了解外部服务的本质;您假设这些服务对于某些输入和输出按预期工作。
为了说明这一点,让我们考虑一个旨在验证成功的属性存储的功能测试。
use App\Services\PropertyService;
class PropertyTest extends TestCase
{
use RefreshDatabase;
public function testPropertyIsStoredSuccessfully()
{
$address = '16-18 Argyll Street, London';
$response = $this->postJson('/api/properties', ['address' => $address]);
// 重点关注的代码段
$this->mock(PropertyService::class)
->shouldReceive('save')
->with(['address' => $address])
->once();
$response->assertStatus(200);
}
}
在该测试中,我们将检查 PropertyService
类中的 save()
方法是否使用正确的地址调用一次。
但是,运行 php artisan test
命令会导致错误:
FAILED Tests\Feature\PropertyTest > propertyIsStoredSuccessfully> propertyIsStoredSuccessfully
InvalidCountException: Method save(['address' => '16-18 Argyll Street, London']) from Mockery_2_App_Services_PropertyService should be called exactly 1 times but called 0 times.
这里发生了什么事?
出现错误的原因是模拟过程不能很好地与静态方法配合使用。当您尝试模拟静态方法时,测试无法识别对这些方法的调用,从而导致测试失败。
从本质上讲,虽然静态方法似乎是一种快速而简单的解决方案,但它们可能会带来挑战和限制,特别是当您需要执行复杂的测试例程时。
何时适合在 Laravel 中使用静态方法?
在 Laravel 中,静态方法可用于在独立环境中执行操作,例如解析时间或格式化文本。静态方法还可用于提供简便的访问点,例如访问全局配置或存储库。
但是,静态方法也有一些潜在的缺点。例如,静态方法难以测试,因为它们不依赖于特定的类实例。此外,静态方法可能会导致代码重复,尤其是如果它们被重复使用。
因此,在 Laravel 中使用静态方法时,应注意以下几点:
静态方法应仅用于在独立环境中执行操作。 静态方法应谨慎使用,最好作为临时解决方案。 静态方法应避免重复代码。
分析静态方法的开源使用
在开源世界中,静态方法的使用非常普遍。Monica
是一个很好的例子,它使用静态方法来实现日期操作。
示例 1:Monica 的 DateHelper 类
Monica
的 DateHelper
类位于 App\Helpers
命名空间下。它提供了一系列与日期相关的静态方法,例如 formatDateTime()
和 getDate()
。这些方法不依赖于 Laravel 之外的外部元素,因此将它们定义为静态方法非常合适。
namespace App\Helpers;
use Carbon\Carbon;
class DateHelper
{
public static function formatDateTime($inputDate, $timezone = null): ?Carbon
{
if (is_null($inputDate)) {
return null;
}
if ($inputDate instanceof Carbon) {
// It's already a Carbon instance
} elseif ($inputDate instanceof \DateTimeInterface) {
$inputDate = Carbon::instance($inputDate);
} else {
try {
$inputDate = Carbon::parse($inputDate, $timezone);
} catch (\Exception $e) {
// There was an error parsing the date
return null;
}
}
$appDefaultTimezone = config('app.timezone');
if ($inputDate->timezone !== $appDefaultTimezone) {
$inputDate->setTimezone($appDefaultTimezone);
}
return $inputDate;
}
// ... more date-time related helper methods ...
}
那么,Monica
在实践中如何利用这种静态方法呢?
// In the UserFactory:
$factory->define(App\Models\User\User::class, function (Faker\Generator $faker) {
return [
'first_name' => $faker->firstName,
// Other attributes...
'email_verified_at' => DateHelper::formatDateTime($faker->dateTimeThisCentury()),
];
});
// And in another factory:
$factory->define(App\Models\User\SyncToken::class, function (Faker\Generator $faker) {
return [
// Other attributes...
'timestamp' => DateHelper::formatDateTime($faker->dateTimeThisCentury()),
];
});
// In the ContactTest:
public function test_user_gets_single_event_reminder()
{
$reminderData = [
'title' => $this->faker->sentence('5'),
'initial_date' => DateHelper::getDate(DateHelper::formatDateTime(
$this->faker->dateTimeBetween('now', '+2 years'))),
'frequency_type' => 'one_time',
'description' => $this->faker->sentence(),
];
// More test code...
}
在实践中,Monica
使用 DateHelper
中的静态方法来格式化日期、获取日期的特定部分等。例如,在 UserFactory
中,formatDateTime()
方法用于将日期字符串格式化为 Carbon
对象。在 ContactTest
中,getDate()
方法用于获取日期的日期部分。
示例 2:BookStack 的 ApiToken 模型
BookStack
的 ApiToken
模型包含一个静态方法 defaultExpiry()
,用于确定 API 令牌的默认到期时间。
use Illuminate\Database\Eloquent\Model;
class ApiToken extends Model
{
// Other attributes and methods...
public static function defaultExpiry(): string
{
return Carbon::now()->addCentury()->toDateString();
}
}
该方法简单地将当前日期延长 100 年,为 API 令牌提供默认的过期时间。
但是 BookStack
实际上是如何使用这个静态方法的呢?
namespace App\Api;
use Illuminate\Http\Request;
class UserApiTokenController extends Controller
{
public function createToken(Request $request, int $userId)
{
// Other logic...
$token = (new ApiToken())->forceFill([
'name' => $request->input('name'),
// Other attributes...
'expires_at' => $request->input('expires_at') ?? ApiToken::defaultExpiry(),
]);
}
}
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class SampleDataSeeder extends Seeder
{
public function run()
{
// Other seeding logic...
$token = (new ApiToken())->forceFill([
'user_id' => $editorUser->id,
'name' => 'Sample API key',
'expires_at' => ApiToken::defaultExpiry(),
'secret' => bcrypt('password'),
'token_id' => 'api_token_id',
]);
}
}
BookStack
在两个地方使用 defaultExpiry()
方法:
在 UserApiTokenController
中,用于创建新的 API 令牌。在 SampleDataSeeder
中,用于在数据播种期间创建 API 令牌。
defaultExpiry()
方法的行为类似于一个配置值,但它将逻辑封装在一个方法中。这使得它可以用作实用函数,而无需依赖应用程序的其他部分。
示例 3:Accounting 的 Str 效用
Accounting
提供了一个名为 Str
的实用程序,用于从字符串中派生缩写。该实用程序包括多种方法来简化和帮助完成此任务。
代码示例
namespace App\Utilities;
use Illuminate\Support\Collection;
use Illuminate\Support\Str as IStr;
class Str
{
public static function getInitials($name, $length = 2)
{
$words = new Collection(explode(' ', $name));
if ($words->count() === 1) {
$initial = self::extractInitialFromSingleWord($name, $words, $length);
} else {
$initial = self::extractInitialFromMultipleWords($words, $length);
}
$initial = strtoupper($initial);
if (language()->direction() == 'rtl') {
$initial = collect(mb_str_split($initial))->reverse()->join('');
}
return $initial;
}
public static function extractInitialFromSingleWord($name, $words, $length)
{
$initial = (string) $words->first();
if (strlen($name) >= $length) {
$initial = IStr::substr($name, 0, $length);
}
return $initial;
}
public static function extractInitialFromMultipleWords($words, $length)
{
$initials = new Collection();
$words->each(function ($word) use ($initials) {
$initials->push(IStr::substr($word, 0, 1));
});
return self::pickInitialsFromList($initials, $length);
}
public static function pickInitialsFromList($initials, $length)
{
return $initials->slice(0, $length)->join('');
}
}
Accounting
在很多地方使用这个实用程序,特别是在 Eloquent Accessors
中:
namespace App\Models\Common;
class Item
{
public function getInitialsAttribute()
{
return Str::getInitials($this->name);
}
}
class Contact
{
public function getInitialsAttribute()
{
return Str::getInitials($this->name);
}
}
str
实用程序提供了一种获取缩写的简化方法,确保上面显示的两个访问器中没有重复的逻辑。然而,更清晰的命名约定,也许像 InitialsHelper
这样,可能会使其目的更加明显。
重要提示
当一个静态方法(如 getInitials()
)使用同一类中的其他方法时,这些辅助方法也应该是静态的。这是由于静态上下文没有 $this
引用。因此,任何后续方法调用都必须静态进行,从而强化整个实用程序的静态性质。
发表评论