shrikeh/php-coding-bible

PHP 编程圣经及相关嗅探器

安装: 46

依赖者: 1

建议者: 0

安全性: 0

星级: 1

关注者: 2

分支: 0

开放性问题: 0

类型:phpcodesniffer-standard

0.2.0 2024-05-27 14:15 UTC

This package is auto-updated.

Last update: 2024-08-27 14:49:31 UTC


README

这是我们希望编写可维护的代码和应用程序的圣经。

道德规范

虽然《Python之禅》是关于Python的,但我们的代码也可以从Tim Peters的智慧中受益

  • 美丽胜过丑陋。
  • 明确胜过含糊。
  • 简单胜过复杂。
  • 复杂胜过复杂。
  • 扁平胜过嵌套。
  • 稀疏胜过密集。
  • 可读性很重要。
  • 特殊情况不足以打破规则。
  • 虽然实用性胜过纯粹性。
  • 错误不应该默默无闻地通过。
  • 除非明确地被抑制。
  • 面对歧义,拒绝猜测的诱惑。
  • 应该有一个——最好是唯一的一个——明显的做法。
  • 虽然一开始可能不明显,除非你是荷兰人。
  • 现在比永远都好。
  • 虽然永远往往比“现在”更好。
  • 如果实现难以解释,那是个坏主意。
  • 如果实现易于解释,那可能是个好主意。
  • 命名空间是一个绝妙的想法——让我们多做些吧!

总体概念

不要。发布。垃圾。

"你不猜代码能工作;你知道它能工作"

- Uncle Bob

这里是这个演讲. 必看。

没有测试的代码不是生产就绪的:就是这样简单。你的测试应该意味着在部署之前你就知道它是可以工作的。这可能意味着重构代码,但我们都是专业人士,工作不应该那么容易。

底层的东西

编写你的代码要

测试是你的应用程序中的第一类公民

想想看。这就是为什么你不需要写那么多文档,你怎么知道你没有打破别人的代码(以及他们不能打破你的代码)。它们需要快速、轻量,足够原子化,可以并行运行,并且易于维护。所以,是的,它们与应用程序代码同样有价值,在知道你完成了之后。像对待应用程序代码一样对待它们(这就是为什么它们也应该被lint和进行质量检查的原因)。

易于测试的类也易于使用和维护

测试驱动开发(TDD)的核心优势不在于测试,而在于编写代码以通过测试使你能够轻松设置 Subject Under Test,并且SUT具有有限的方法和排列组合需要测试。

优先选择组合而不是继承

应该有一个非常充分的理由去扩展一个类。继承会在测试、覆盖方法等方面引起各种问题。使你的类 final 并使用协作者。

类应在构造时自我验证

一个对象不应处于无效状态。对象应在构造时自我验证以确保它们是有意义的,并捕获早期问题。

# Bad
class Refund
{
    public function __construct(private float $amount)
    {}
}

# Good
final class Refund
{
    public function __construct(private float $amount)
    {
        if (0 >= $amount) {
            // Refunds must be above zero.
            throw RefundNotPositiveException::create($amount);
        }
    }
}

构造函数不需要规范化数据,只需要验证它

在对象可能由多种类型的数据创建的情况下,最好使 __construct() 方法为私有,并使用 静态命名构造函数。静态方法可以规范化数据,而真正的构造函数验证它

final class PdfPath
{
    public static function fromString(string $path): self
    {
        return new self (new SplFileObject($path));
    }
    
    public static function fromFileInfo(SplFileInfo $fileInfo): self
    {
        return new self($fileInfo->openFile());
    }
    
    private function __construct(private SplFileObject $pdfPath)
    {
        if (!$this->pdfPath->isReadable()) {
        // ...
        }
    }
}

“三法则”

一个方法,包括构造函数,应最多包含三个参数。

协作是关键

协作者,无论是聚合还是功能提供者,都提供了良好代码的构建模块。它们使得上述“规则”更容易遵循,有助于测试、维护和重用。

一个简单的聚合示例,用于在Customer中使用

namespace Shrikeh\Customer;

final class ContactDetails
{
    public function __construct(
        private Address $address, 
        private EmailAddress $emailAddress,
        private TelephoneNumber $telephoneNumber,
    ) {
    
    }
    
    // ... getters
}

由于上述类使用了值对象,这些对象会自我验证,因此聚合本身无需验证。这可以用来作为Customer构造函数的参数。

但是,您应该如何访问这些聚合细节呢?嗯...

实践最小知识法则

“当一个人想让狗走路时,他不会直接命令狗的腿走路;相反,他会命令狗,然后狗会命令自己的腿。”

- 维基百科

最小知识法则是一种最小知识原则;也就是说,使用对象的人不应该依赖于对象的属性和结构。相反,对象应该有一个方法来完成外部用户的工作

# Bad
$address = $customer->getProfile()->getHomeAddress();

# Good
$address = $customer->getHomeAddress();

失败应该是例外情况

类应该做它们应该做的事情,或者抛出一个特定的异常。不要返回true;它应该正常工作!

记住:如果你依赖于一个协作者,并且它抛出了异常,请使用之前的异常

final class DbalCustomerRegistryRepository
{
//...
    public function registerCustomer(Customer $customer): void
    {
        try {
            $this->client->save($customer->getName());
        } catch (ClientException $exc) {
            throw UnableToRegisterCustomerException::create($customer, $exc);
        }
    }
}

担心上述代码没有返回ID?你为什么需要ID,你的应用程序已经通过使用UUIDv5或UUIDv6在事先指定了UID。

注释是一种代码异味

想想看:为什么你的代码如此复杂,你需要解释它?强大的变量名和类型,加上同样强大的方法名和简洁性,不应该需要像《战争与和平》那样来描述。使用Gherkin的功能测试,结合遵循示例规格的单元测试,应该提供足够的信息。

集合是邪恶的

对象应该是不可变的,并且创建在特定的状态,它们在整个生命周期中携带这个状态。如果你必须使用set,它应该返回一个包含新值的新实例,并保持原始实例不变

final class Foo
{
    public function setBar(string $bar): self
    {
        return new self($bar);
    }
}

获取是邪恶的

或者至少,从PHP 8.1开始,获取器对于仅返回属性值来说不是必要的,因为添加了public readonly属性

final class Foo
{
    public function __construct(
        public readonly string $bar
    ) {
    }
}

$foo = new Foo('sample string');
echo $foo->bar; // sample string

替换

final class Foo
{
    private string $bar;
    
    public function __construct(
        string $bar
    ) {
        $this->bar = $bar;
    }
    
    public function getBar(): string
    {
        return $this->bar;
    }
}

$foo = new Foo('sample string');
echo $foo->getBar(); // sample string

为什么?

遵循public readonly方法有两个主要原因

  • 简洁性 - 上述第一个代码片段比第二个要短得多。它需要更少的代码来编写,也更容易阅读。
  • 节省单元测试 - 在一个具有最低代码覆盖率预期的项目中,有必要对包括获取器在内的方法进行单元测试。虽然这是一个微不足道的测试来编写,但在使用public readonly方法时不需要任何测试。

更好的是,尽可能使整个只读

final readonly class Foo
{
    public function __construct(
        public string $bar
    ) {
    }
}

新的是邪恶的

只有少数事物应该创建其他事物

  • 一个对象应该能够创建一个新的自身实例(即遵循上述不可变性的实践)
  • 一个专门的工厂,最好是符合接口的。
  • 依赖注入容器

让我们想象一个符合以下合同的数据库存储库

interface CustomerDetailsRepository
{
    public function fetchCustomerDetails(CustomerId $customerId): CustomerDetails;
}

现在,一个基本处理查询和异常处理的实现,而无需知道如何创建CustomerDetails

final readonly class DBCustomerDetailsRepository implements CustomerDetailsRepository
{
    public function __construct(private Client $client, private CustomerDetailsFactory $customerDetailsFactory)
    {
        
    }

    public function fetchCustomerDetails(CustomerId $customerId): CustomerDetails
    {
        try {
            $row = $this->queryCustomerDetails($customerId);
            
            return $this->customerDetailsFactory->build($row);
        } catch (SomeClientException $exc) {
            // ... throw a Repository exception
        }
    }

    private function queryCustomerDetails(): SomeDbRow
    {
        //...
    }
}

上述代码将更容易进行模拟、测试和维护。

默认情况下使变量和方法私有

你应该只公开必要的方法来满足外部使用的合同。

优先使用类常量而不是魔法数字

你知道是怎么回事。几个月前,你编写了一个速度快得惊人的优秀算法,你的同事们都很欣赏你,而现在你必须带着一个新的团队成员回到它。

final class AwesomeAlgorithm
{
    public function calc(float $marketVolatility, float $weight): float
    {
        return (2.17 ** $marketVolatility) - (1 - $weight);
    }
}

他们天真地问:“为什么是2.17?”你惊慌失措,不记得这个神奇数字是什么。你现在看得太清楚了:他们会看穿你那伪装的专业,你的团队会嘲笑你,你的家人会对你冷淡,LinkedIn会关闭你的个人资料。随着汗水从额头流下,你拼命地试图回忆任何计算的含义,但你已经太晚了,因为第一滴汗珠砸在你的键盘上,你闻到了电子产品的烧焦味。

如果当时你这样写会怎么样

final class AwesomeAlgorithm
{
    private const LONG_TERM_VOLATILITY_YIELD = 2.17;

    public function calc(float $marketVolatility, float $weight): float
    {
        return (self::LONG_TERM_VOLATILITY_YIELD ** $marketVolatility) - (1 - $weight);
    }
}

优先使用枚举而非多个相同“事物”的类常量

在你前任和那台受潮损坏的键盘都被替换后,你决心不再犯同样的错误。

以下是PHP还没有枚举功能之前一个类的(真实)摘录

class HsSomeBankCashTx extends DumbActiveRecord
{
    //...

    public const TLA_CODE_TRANSFER = 'TFR';
    public const TLA_CODE_DIRECT_DEBIT = 'DDR';
    public const TLA_BILL_PAYMENT = 'BBP';
    public const TLA_CODE_BANK_GIRO_CREDIT = 'BGC';
    public const TLA_CODE_FASTER_PAYMENT_CREDIT = 'FPC';
    public const TLA_CODE_FASTER_PAYMENT_DEBIT = 'FP';
    public const TLA_CREDIT = 'CR';
    public const TLA_CODE_FASTER_PAYMENT_TRANSFER = 'FT';
    public const TLA_STANDING_ORDER = 'STO';
    //...

你保持冷静,并通过提取这些常量到一个枚举来重构这个类

enum SomeBankTlaCode: string
{
    case Transfer = 'TFR';
    case DirectDebit = 'DDR';
    case BillPayment = 'BBP';
    case BankGiroCredit = 'BGC';
    case FasterPaymentCredit = 'FPC';
    case FasterPaymentDebit = 'FP';
    case Credit = 'CR';
    case FasterPaymentTransfer = 'FT';
    case StandingOrder = 'STO';
}

使用declare(strict_types=1)强制类型严格

PHP的新版本允许使用scalar类型声明。

不幸的是,这些类型仍然可以被强制转换为错误类型,以下是一个例子

<?php

function myFunc(string $str): void 
{	
	echo $str;	
}

myFunc(123);

参见https://3v4l.org/6YfXoK

这将简单地输出123

现在我们添加declare(strict_types=1);

<?php

declare(strict_types=1);

function myFunc(string $str): void 
{	
	echo $str;	
}

myFunc(123); 

参见https://3v4l.org/smJf1

现在我们得到

Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($str) must be of type string, int given

在php >= 8的情况下,对于php 7我们得到

Fatal error: Uncaught TypeError: Argument 1 passed to myFunc() must be of the type string, int given

因此,练习安全编码,并始终使用declare(strict_types=1);