shrikeh / php-coding-bible
PHP 编程圣经及相关嗅探器
Requires
- php: >=8.2
- shrikeh/testing-metapackage: >=0.3
- slevomat/coding-standard: ^8.4
- squizlabs/php_codesniffer: ^3.7
Requires (Dev)
- ext-xdebug: *
- roave/security-advisories: dev-latest
- symfony/dotenv: ^6.2
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);
这将简单地输出123
现在我们添加declare(strict_types=1);
<?php declare(strict_types=1); function myFunc(string $str): void { echo $str; } myFunc(123);
现在我们得到
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);