tasko-products/php-codestyle

tasko Products GmbH PHP 代码风格与 PHP CS Fixer 规则集的描述

1.2.0 2023-11-30 13:08 UTC

This package is auto-updated.

Last update: 2024-08-30 01:44:13 UTC


README

tasko 代码风格基于PSR-12指南,并添加了一些内容。

每个新的 PHP 文件都必须有头部

此代码块提供了有关代码版权和许可证的信息,并声明了严格的类型。

<?php
/**
 * @copyright    (c) tasko Products GmbH
 * @license      Commercial
 */

declare(strict_types=1);

版权下使用4个空格,许可证下使用6个空格

注释你的代码

这并不意味着你在代码的任何地方都开始注释,并创建大量不想要的注释。如果你这样做,那么你就无法向其他开发者传达你编写的代码。

当需要时进行注释,当你有复杂的代码或表达式时,以便在未来的某个时间点开发者和你都能理解你试图实现的目标。

/** Get the user details */
public function getUserSalary(int $id): string 
{
    /** Fetch the user details by id */
    $user = User::where('id', $id)->first();

    /** Calling function to calculate user's current month salary */
    $this->currentMonthSalary($user);
    //...
}

/** Calculating the current month salary of user */
private function currentMonthSalary(User $user): void
{
    /** Salary = (Total Days - Leave Days) * PerDay Salary */
    // ...
}

public function getUserSalary(int $id): void
{
    $user = User::where('id', $id)->first();

    $this->currentMonthSalary($user);
    //...
}

private function currentMonthSalary(User $user): string
{
    /** Salary = (Total Days - Leave Days) * PerDay Salary */
    // ...
}

使用有意义的可发音变量名

$ymdstr = date('d-M-Y', strtotime($customer->created_at));

$customerRegisteredDate = date('d-M-Y', strtotime($customer->created_at));

使用适当的有意义的方法名

public function user($email): void
{
    // ...
}

public function getUserDetailsByEmail($email): array
{
    // ...
}

可读且可搜索的代码

尽可能使用常量或函数来编写更可读和可搜索的代码。这会在长期内对你非常有帮助

// What the heck is 1 & 2 for?
if ($user->gender == 1) {
    // ...
}

if ($user->gender == 2) {
    // ...
}

public const MALE = 1;
public const FEMALE = 2;

if ($user->gender === MALE) {
    // ...
}

if ($user->gender === FEMALE) {
    // ...
}

尽量避免深层次嵌套和早期返回

尽可能避免深层次嵌套并使用早期返回。深层次嵌套,也称为过度嵌套,发生在代码中使用多级缩进来表示复杂控制流时。虽然嵌套有时对于创建复杂算法是必要的,但通常最好避免在编码风格中使用过度嵌套,以下是一些原因

  • 降低可读性:当代码过度嵌套时,可能难以阅读和理解。这可能导致错误,特别是当其他开发者需要阅读或修改你的代码时。
  • 增加复杂性:过度嵌套会使代码比实际需要的更复杂。这使得它更难以维护、调试和测试。
  • 降低性能:过度嵌套会降低性能,因为每个嵌套级别都需要额外的处理时间。这在小型应用程序中可能不明显,但它可以在大型应用程序中产生重大影响。
  • 更高的错误风险:深层嵌套的代码更容易出错,因为它难以跟踪所有条件和逻辑。这可能导致错误、意外结果和安全漏洞。

为了避免过度嵌套,你可以使用早期返回、保护子句和平展条件语句等技术。这些技术可以帮助简化你的代码,使其更易于阅读,并降低错误风险

1) 早期返回

public function calculatePrice($quantity, $pricePerUnit): float {
    $price = 0;
    if ($quantity > 0) {
        $price = $quantity * $pricePerUnit;
        if ($price > 100) {
            //...
        }
    }
    
    return $price;
}

public function calculatePrice($quantity, $pricePerUnit): float {
    if ($quantity <= 0) {
        return 0;
    }
    
    $price = $quantity * $pricePerUnit;
    if ($price > 100) {
        //...
    }
    
    return $price;
}

2) 保护子句

public function calculateTax($price, $taxRate): int {
    $tax = 0;
    if ($price > 0) {
        $tax = $price * $taxRate;
    }
    
    return $tax;
}

public function calculateTax($price, $taxRate): int {
    if ($price <= 0) {
        return 0;
    }
    
    return $price * $taxRate;    
}

3) 平展条件语句

if ($user->isAdmin()) {
    if ($user->isSuperAdmin()) {
        // do something
    }
    else {
        // do something else
    }
}
else {
    // do something different
}

if ($user->isSuperAdmin()) {
    // do something
}
else if ($user->isAdmin()) {
    // do something else
}
else {
    // do something different
}

尽可能避免使用无用的变量

基本上,如果你在代码中使用了该变量超过两次,则保留该变量;否则,直接在代码中使用它。 这提高了可读性,并使你的代码更整洁。

public function addNumbers($a, $b): int {
    $result = $a + $b;
    $finalResult = $result * 2; // Useless variable
    return $finalResult;
}

public function addNumbers($a, $b): int {
    return ($a + $b) * 2; // Directly use the expression
}

空值合并运算符 (??)

空值合并运算符检查变量或条件是否为空。

if (!empty($user)) {
  return $user;
}

return false;

return $user ?? false;

比较 (===, !==)

简单的比较会将字符串转换为整数。

$a = '7';
$b = 7;

if ($a != $b) {
    // The expression will always pass
}

比较$a != $b返回FALSE,但实际上是TRUE!字符串“7”与整数7不同。

相同的比较会比较类型和值。

$a = '7';
$b = 7;

if ($a !== $b) {
    // The expression is verified
}

比较$a !== $b返回TRUE。

空值检查

使用null === $variable格式检查可空变量是一种好习惯

if (null === $variable) {
    // code here
}

这种格式有助于防止意外将null值赋给变量,例如

if ($variable = null) {
    
}

封装条件

编写条件语句并不坏,但如果将其封装到方法/函数中,则有助于提高可读性并有助于未来的代码维护。

if ($article->status === 'published') {
    // ...
}

if ($article->isPublished()) {
    // ...
}

使用IDE的80字符标尺

如果一行代码超过80个字符,则相应地进行换行

private function withOrderFromUri(\Mockery\MockInterface $someService, SomeOtherClass $otherClass, Order $order): void {
    $someService->expects()->getOrderByUri($otherClass->getResource()->getUri())->andReturns($order);
}

private function withOrderFromUri(
    \Mockery\MockInterface $someService,
    SomeOtherClass $otherClass,
    Order $order,
): void {
    $someService
        ->expects()
        ->getOrderByUri($otherClass->getResource()->getUri())
        ->andReturns($order)
    ;
}

在第二个函数中使用换行和缩进使代码更易于阅读和理解,尤其是处理较长的方法链时。

注意,在这个例子中,分号放置在新的一行上,使其更明显,这样就可以看到这个链式代码表达式的结束。

在链式方法中使用null安全运算符(或其它)时,有必要在新的一行开始时使用该运算符。这确保了运算符已正确应用于链中的上一个方法调用。

// Set the billing address for an invoice
$invoice->setBillingAddress(
    $order->getCustomer()
        ?->getAddress()
        ?->getBillingAddress()
);

长字符串 - 一个例外

然而,我们并不将此规则应用于长字符串。原因是将长字符串拆分成多个短字符串会使它们更难查找,尤其是在处理日志消息时。出于实用目的,我们避免在此处使用更易于阅读和访问的代码。

private function doSomething(Some $thing, string $callerId): void
{
    $this->logger->info(
        'Something really important happented within {something}, triggered'
        . 'from caller {callerId}',
        [
            'something' => $thing->getName(),
            'callerId' => $callerId,
        ],
    );
}

private function doSomething(Some $thing, string $callerId): void
{
    $this->logger->info(
        'Something really important happented within {something}, triggered from caller {callerId}',
        [
            'something' => $thing->getName(),
            'callerId' => $callerId,
        ],
    );
}

单元测试

单元测试应按照AAA模式设置,其中区域没有标签。这适用于简单的测试函数,也适用于表格驱动测试(数据提供者)。

public function testCalculateTotalPriceForArticles(): void
{
    $taxService = \Mockery::mock(TaxServiceInterface::class);
    $taxService->expects()->getTaxRate()->andReturns(0.19);

    $logger = \Mockery::mock(LoggerInterface::class);
    $logger
        ->expects()
        ->info(
            'A total price {totalPrice} has been calculated, including a tax portion of {taxRate}%',
            [
                'totalPrice' => 51.1581,
                'taxRate' => 0.19,
            ],
        )
    ;

    $this->assertEquals(
        51.1581,
        (new PriceCalculationService($taxService, $logger))
            ->calculateTotalPriceForArticles(
                [
                    (new Article)->setPrice(10.00),
                    (new Article)->setPrice(10.90),
                    (new Article)->setPrice(10.09),
                    (new Article)->setPrice(2.00),
                ],
            ),
    );
}

更好的

基于AAA模式,通过分离测试的必要区域,现在已通过遵循定义顺序的分组提高了单元测试的可读性。这种分离是通过分组实现的。

在测试的开始,如果测试用例需要,则在Arrange部分对被测试类的所有依赖项进行模拟 - 并非每个测试用例都需要模拟。

Arrange部分之后是Act部分,它初始化测试用例所需的参数。这些参数尽可能地放置在测试方法附近,以提高可读性和分组。

public function testCalculateTotalPriceForArticles(): void
{
    $taxService = \Mockery::mock(TaxServiceInterface::class);
    $taxService->expects()->getTaxRate()->andReturns(0.19);

    $logger = \Mockery::mock(LoggerInterface::class);
    $logger
        ->expects()
        ->info(
            'A total price {totalPrice} has been calculated, including a tax portion of {taxRate}%',
            [
                'totalPrice' => 51.1581,
                'taxRate' => 0.19,
            ],
        )
    ;

    $service = new PriceCalculationService($taxService, $logger);

    $articles = [
        (new Article)->setPrice(10.00),
        (new Article)->setPrice(10.90),
        (new Article)->setPrice(10.09),
        (new Article)->setPrice(2.00),
    ];

    $actual = $service->calculateTotalPriceForArticles($articles);

    $this->assertEquals(51.1581, $actual);
}

创建类模拟和配置函数调用可能会产生非常大、难以阅读的代码,具体取决于函数参数的复杂度。

为了进一步提高可读性和测试代码的可重用性,让我们看看我们自己的断言函数语法。

断言函数分为withwithoutexpects断言。with和without断言始终用于模拟函数是否提供数据的情况。这些通常用于存储库,但也有API或配置服务的示例。

最后,有expects断言。这些用于我们对模拟期望发生的所有其他操作。这包括经典交互,如发送消息、记录和API调用,即所有不能(很好地)与“with”或“without”结合的语言学操作 - 毕竟,我们的目标是编写可读的测试。

作为一个团队,我们同意断言函数的声明顺序也很重要。在 Arrange 部分的测试中,我们总是从提供带/不带断言及其模拟开始。然后是测试类的初始化之前的 expects 断言。带/不带 & expects 断言的模拟是例外。如果出现这种情况,它们应该放在带/不带断言和 expects 断言之间。再次强调,带/不带断言首先在模拟上定义,然后是 expects 断言。

值得注意的是,断言函数,特别是带/不带断言,通常可以通过特定于存储库的测试特性提供,因此可以跨所有集成存储库作为依赖项的测试使用。同样适用于 info、warning 和 error 日志的 expects 断言。

public function testCalculateTotalPriceForArticles(): void
{
    $taxService = \Mockery::mock(TaxServiceInterface::class);
    $this->withTaxRate($taxService);

    $logger = \Mockery::mock(LoggerInterface::class);
    $this->expectsPriceCalculatedInfoLog($logger);

    $service = new PriceCalculationService($taxService, $logger);

    $articles = [
        (new Article)->setPrice(10.00),
        (new Article)->setPrice(10.90),
        (new Article)->setPrice(10.09),
        (new Article)->setPrice(2.00),
    ];

    $actual = $service->calculateTotalPriceForArticles($articles);

    $this->assertEquals(51.1581, $actual);
}

private function withTaxRate(\Mockery\MockInterface $taxService): void
{
    $taxService->expects()->getTaxRate()->andReturns(0.19);
}

private function expectsPriceCalculatedInfoLog(
    \Mockery\MockInterface $logger,
): void {
    $logger
        ->expects()
        ->info(
            'A total price {totalPrice} has been calculated, including a tax portion of {taxRate}%',
            [
                'totalPrice' => 51.1581,
                'taxRate' => 0.19,
            ],
        )
    ;
}

尾随逗号

根据 PER 编码风格 2.0,我们使用尾随逗号来表示相关的多行语句,如函数参数、函数调用和数组。

这给我们带来了几个优点。首先,这种优点在初次看起来可能有点令人惊讶:我们得到了更一致、更清晰的语法。对于所有相关的多行语句,只有一个规则——缩进并添加一个逗号。不像以前,这个规则适用于除最后一行之外的所有行。没有尾随逗号,对于同事(其中一些添加尾随逗号,而另一些没有)以及必须纠正更复杂规则的风格修复器来说,都更难。

尾随逗号的另一个优点是简化了版本控制和代码审查。当使用尾随逗号添加额外的行时,只有一个 diff 在一行上,而不是两行。

糟糕的数组

$handbags = [
    'Hermes Birkin',
    'Chanel 2.55',
    'Louis Vuitton Speedy'
];

好的数组

$handbags = [
    'Hermes Birkin',
    'Chanel 2.55',
    'Louis Vuitton Speedy',
];

糟糕的函数声明

/**
 * @param string ...$other
 */
function compareHandbags(
    string $handbag,
    ...$other
) {
    // compare logic
}

好的函数声明

/**
 * @param string ...$other
 */
function compareHandbags(
    string $handbag,
    ...$other,
) {
    // compare logic
}

糟糕的函数调用

compareHandbags(
    'Prada Galleria',
    'Dior Saddle',
    'Gucci Dionysus'
);

好的函数调用

compareHandbags(
    'Prada Galleria',
    'Dior Saddle',
    'Gucci Dionysus',
);

糟糕的匹配

$recommendation = match ($userPreference) {
    'classic' => 'Chanel 2.55',
    'modern' => 'Stella McCartney Falabella',
    'versatile' => 'Louis Vuitton Neverfull'
};

好的匹配

$recommendation = match ($userPreference) {
    'classic' => 'Chanel 2.55',
    'modern' => 'Stella McCartney Falabella',
    'versatile' => 'Louis Vuitton Neverfull',
};

更多 CleanCode 原则:https://github.com/piotrplenik/clean-code-php