tasko-products / php-codestyle
tasko Products GmbH PHP 代码风格与 PHP CS Fixer 规则集的描述
Requires (Dev)
- php: >=8.0
- friendsofphp/php-cs-fixer: ^3.0.0
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); }
好
创建类模拟和配置函数调用可能会产生非常大、难以阅读的代码,具体取决于函数参数的复杂度。
为了进一步提高可读性和测试代码的可重用性,让我们看看我们自己的断言函数语法。
断言函数分为with、without和expects断言。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