laravel代码整洁之道
咻兔哔 / 技术文章

我最近遇到了 这条 Twitter,其中 @samuelstancl 列出了在 Laravel 中编写更干净代码的技巧, 以及一些通用的 Laravel 编码建议。 这些是培养对什么是好的代码和什么是坏的代码的感觉的一个很好的起点 - 所以我在下面整理了它们(带有代码示例),没有特定的顺序。

细节决定成败

干净的代码是微观层面不断做出正确决策的结果。

使用表查找

不要编写重复的 else if 语句,而是使用数组根据您拥有的键查找所需值。 代码将更清晰且更具可读性,如果出现问题,您将看到可以理解的异常。 没有半途而废的边缘情况。

// 不好的
if ($order->product->option->type === 'pdf') {
    $type = 'book';
} else if ($order->product->option->type === 'epub') {
    $type = 'book';
} else if ($order->product->option->type === 'license') {
    $type = 'license';
} else if ($order->product->option->type === 'artwork') {
    $type = 'creative';
} else if $order->product->option->type === 'song') {
    $type = 'creative';
} else if ($order->product->option->type === 'physical') {
    $type = 'physical';
}

if ($type === 'book') {
    $downloadable = true;
} else if ($type === 'license') {
    $downloadable = true;
} else if $type === 'creative') {
    $downloadable = true;
} else if ($type === 'physical') {
    $downloadable = false;
}
// 正确的
$type = [
    'pdf'      => 'book',
    'epub'     => 'book',
    'license'  => 'license',
    'artwork'  => 'creative',
    'song'     => 'creative',
    'physical' => 'physical',
][$order->product->option->type];

$downloadable = [
    'book'     => true,
    'license'  => true,
    'creative' => true,
    'physical' => false,
][$type];

尽早返回

通过尽早返回值来避免不必要的嵌套。过多的嵌套和 else 语句往往会使代码难以理解。

// 糟糕的示例
if ($notificationSent) {
  $notify = false;
} else if ($isActive) {
  if ($total > 100) {
    $notify = true;
  } else {
    $notify = false;
  } else {
    if ($canceled) {
      $notify = true;
    } else {
      $notify = false;
    }
  }
}

return $notify;
// 推荐的示例
if ($notificationSent) {
  return false;
}

if ($isActive && $total > 100) {
  return true;
}

if (! $isActive && $canceled) {
  return true;
}

return false;

正确分割线

不要在随机的地方分割线,但也不要让它们太长。 使用 [ 打开数组并缩进值往往效果很好。 与长函数参数值相同。 其他拆分行的好地方是链式调用和闭包。

// 糟糕的示例
// 没有分行
return $this->request->session()->get($this->config->get('analytics.campaign_session_key'));

// 无意义分行
return $this->request
  ->session()->get($this->config->get('analytics.campaign_session_key'));
// 推荐的示例
return $this->request->session()->get(
  $this->config->get('analytics.campaign_session_key')
);

// 闭包
new EventCollection($this->events->map(function (Event $event) {
  return new Entries\Event($event->code, $event->pivot->data);
}));

// 数组
$this->validate($request, [
  'code' => 'string|required',
  'name' => 'string|required',
]);

不要创建没用的变量

可以直接传值,就不要创建没用的变量。

// 坏的
public function create()
{
  $data = [
    'resource' => 'campaign',
    'generatedCode' => Str::random(8),
  ];

  return $this->inertia('Resource/Create', $data);
}
// 好的
public function create()
{
  return $this->inertia('Resource/Create', [
    'resource'      => 'campaign',
    'generatedCode' => Str::random(8),
  ]);
}

能提高可读性的时候再创建变量

和上一条相反,有时候一个值来自一整套复杂的计算,因此创建一个变量,可以提高可读性,甚至连注释都省了。记住,上下文很重要,并且你编写代码的最终目标是让代码更具有可读性。

// 坏的
Visit::create([
  'url' => $visit->url,
  'referer' => $visit->referer,
  'user_id' => $visit->userId,
  'ip' => $visit->ip,
  'timestamp' => $visit->timestamp,
])->conversion_goals()->attach($conversionData);
// 好的
$visit = Visit::create([
  'url'       => $visit->url,
  'referer'   => $visit->referer,
  'user_id'   => $visit->userId,
  'ip'        => $visit->ip,
  'timestamp' => $visit->timestamp,
]);

$visit->conversion_goals()->attach($conversionData);

根据业务逻辑来创建模型的方法

控制器应该尽量保持简单。 比如以 “为订单创建发票” 这样的方式调用方法。调用方法不需要关心你的数据库表结构的细节,这些由模型自己内部实现。

// 糟糕的方式
// 为订单创建发票
DB::transaction(function () use ($order) {
  $invoice = $order->invoice()->create();

  $order—>pushStatus(new AwaitingShipping);

  return $invoice;
});
// 优雅的方式
$order->createInvoice();

创建动作类

让我们来继续刚才的例子。有时,可以为某个动作单独创建一个类,这样会使代码更加整洁。模型封装的业务逻辑可以基于动作类,但是记得动作类不可太大。

// 糟糕的方式
public function createInvoice(): Invoice
{
  if ($this->invoice()->exists()) {
    throw new OrderAlreadyHasAnInvoice('Order already has an invoice.');
  }

  return DB::transaction(function () use ($order) {
    $invoice = $order->invoice()->create();

    $order->pushStatus(new AwaitingShipping);

    return $invoice;
  });
}
// 优雅的方式
// 订单模型
public function createInvoice(): Invoice {
  if ($this->invoice()->exists()) {
    throw new OrderAlreadyHasAnInvoice('Order already has an invoice.');
  }

  return app(CreateInvoiceForOrder::class)($this);
}

// 订单创建发票动作类
class CreatelnvoiceForOrder
{
  public function _invoke(Order $order): Invoice
  {
    return DB::transaction(function () use ($order) {
      $invoice = $order->invoice()->create();

      $order->pushStatus(new AwaitingShipping);

      return $invoice;
    });
  }
}

考虑表单请求

考虑使用表单请求,它们是隐藏复杂验证逻辑的好地方,但要注意这一点 — 隐藏的东西。当您的验证逻辑很简单时,在控制器中执行它并没有错,将其移至表单请求使其变得不那么明确。

/**
 * 获取适用于请求的验证规则.
 *
 * @返回数组
 */
public function rules()
{
  return [
    'title' => 'required|unique:posts|max:255',
    'body'  => 'required',
  ];
}

使用事件

考虑将一些逻辑从控制器写到事件。例如,在创建模型时,好处是创建这些模型在任何地方 (控制器,任务,…) 并且控制器不必担心 DB 模式的细节。

// 糟糕的示例
// 只在这个地方有效并关注它
// 模型应该关心的细节.
if (! isset($data['name'])) {
  $data['name'] = $data['code'];
}

$conversion = Conversion::create($data);
// 推荐的示例
$conversion = Conversion::create($data);

// 模型
class ConversionGoal extends Model
{
  public static function booted()
  {
    static::creating(function (self $model) {
      $model->name ??= $model->code;
    });
  }
}

拆分方法

如果某些方法太长或是太复杂,很难理解究竟做了什么,可以尝试将复杂的逻辑拆分成多个方法。

public function handle(Request $request, Closure $next)
{
  // 我们将三段逻辑分别提取成单独的方法
  $this->trackVisitor();
  $this->trackCampaign();
  $this->trackTrafficSource($request);

  $response = $next($request);

  $this->analytics->log($request);

  return $response;
}

创建助手函数

如果你多次重复一段代码,考虑一下将它们提取为助手函数是不是可以让代码更简洁。

// app/helpers.php 文件,在 composer.json 中自动加载
function money(int $amount, string $currency = null): Money
{
  return new Money($amount, $currency ?? config('shop.base_currency'));
}

function html2text($html = ''): string
{
  return str_replace(' ', ' ', strip_tags($html));
}

避免使用助手类

有时候人们会使用类来归类助手函数,可要小心了,这可能会让代码变得更混乱。常见的做法是定义一个只包含一个作为助手函数使用的静态方法的类。更好的做法是将这些方法放入具有具体逻辑的类中,或者是只将它们当做是全局函数。

// 坏的
class Helper
{
  public function convertCurrency(Money $money, string $currency): self
  {
    $currencyConfig = config("shop.currencies.$currency");
    $decimalDiff = ...

    return new static(
      (int) round($money->baseValue() * $currencyConfig[value] * 10**$decimalDiff, 0),
      $currency
    );
  }
}

// 使用
use App\Helper;
Helper::convertCurrency($total, 'EUR');
// 好的
class Money
{
  // 其他的 money/currency 逻辑

  public function convertTo(string $currency): self
  {
    $currencyConfig = config("shop.currencies.$currency");
    $decimalDiff = ...

    return new static(
      (int) round($this->baseValue() * $currencyConfig[value * 10**$decimalDiff, 0),
      $currency
    );
  }
}

// 使用
$EURtotal = $total->convertTo('EUR');

拿出一个周末来学习 OO

了解静态(static)/ 实例(instance)方法和变量,还有私有的(private)/ 保护的(protected)/ 公共的(public)之间的可见性的区别。还要了解 Laravel 如何使用魔法方法。当你是初学者的时候可能不会很常用,但是随着你的编码水平增长,这些是至关重要的。

不要在类中只写过程代码

这将前面的推文与此处的其他提示联系起来。OOP 的存在就是为了让你的代码更加具有可读性,请使用 OOP。不要再在控制器中写好几百行的过程代码了。

阅读 SRP 之类的内容,并进行合理的扩展

避免使用那种处理很多和当前类不相关逻辑的类,但是也不要为每件事都创建一个类。你是为了写干净的代码,而不是想在每件事上都做分离。

避免函数中参数过多

当您看到具有大量参数的函数时,它可能意味着:

  1. 该函数包含太多职责,应该分离。
  2. 职责没问题,但你应该学会重构他的长签名.

以下是修复第二种情况的两种策略.

使用数据传输对象 (DTO)

与其以特定顺序传递大量参数,不如考虑创建一个具有属性的对象来存储这些数据。 如果您发现某些行为可以移入此对象,则可以加分。

// 糟糕的示例
public function log($url, $route_name, $route_data, $campaign_code, $traffic_source, $referer, $user_id, $visitor_id, $ip, $timestamp)
{
  // ...
}
// 推荐的示例
public function log(Visit $visit)
{
  // ...
}

class Visit
{
  public string $url;
  public ?string $routeName;
  public array $routeData;

  public ?string $campaign;
  public array $trafficSource[];

  public ?string $referer;
  public ?string $userId;
  public string $visitorId;
  public ?string $ip;

  public Carbon $timestamp;

  // ...
}

创建流式对象

你可以使用流式 API 来创建对象。使用单独的方法调用来逐渐添加数据,并且只要构造函数中的绝对最小值。正是因为每个方法都返回 $this ,你可以在任意一次调用后让整个流程停下来。

Visit::make($url, $routeName, $routeData)
  ->withCampaign($campaign)
  ->withTrafficSource($trafficSource)
  ->withReferer($referer)
  // ... 等等

使用自定义集合

创建自定义集合可以更好地写出更富有表现力的语法。参考这个订单合计的示例:

// 坏的
$total = $order->products->sum(function (OrderProduct $product) {
  return $product->price * $product->quantity * (1 + $product->vat_rate);
});
// 好的
$order->products->total();

class OrderProductCollection extends Collection
{
  public function total()
  {
    $this->sum(function (OrderProduct $product) {
      return $product->price * $product->quantity * (1 + $product->vat_rate);
    });
  }
}

不要使用缩写

不要觉得很长的变量名 / 方法名就是不对的,才不是这样,它们很有表现力。使用一个长的方法名比短的更好,配合查阅文档能更完整地了解它的功能。变量也是如此。不要使用无意义的几个字母的缩写。

// 坏的
$ord = Order::create($data);

// ...

$ord->notify();
// 好的
$order = Order::create($data);

// ...

$order->sendCreatedNotification();

尝试在控制器中只使用 CURD 动作

如果可以的话,只使用控制器中的 7 个 CURD 动作,通常来说会更少。不要在控制器中创建 20 多个方法,更短的控制器更好一些。
使用更具有表现力的方法名称

考虑「这个对象可以完成什么事情」,而不是「这个对象能做什么」。也会有例外,比如操作类。这是个很好的经验。

// 坏的
$gardener->water($plant);

$orderManager->lock($order);
// 好的
$plant->water();

$order->lock();

创建单次使用的 trait

将方法添加到它所属的类中,比为每件事都创建操作类简洁得多,但是这会让类变得很大。尝试使用特征 traits,它主要是为了代码复用,但是单次使用的 trait 并没有错。

class Order extends Model
{
  use HasStatuses;

  // ...
}

trait HasStatuses
{
  public static function bootHasStatuses() { ... }
  public static $statusMap = [ ... ];
  public static $paymentStatusMap = [ ... ];
  public static function getStatusId($status) { ... }
  public static function getPaymentStatusId($status): string { ... }
  public function statuses() { ... }
  public function payment_statuses() { ... }
  public function getStatusAttribute(): OrderStatusModel { ... }
  public function getPaymentStatusAttribute(): OrderPaymentStatus { ... }
  public function pushStatus($status, string $message = null, bool $notification = null) { ... }
  public function pushPaymentStatus($status, string $note = null) { ... }
  public function status(): OrderStatus { ... }
  public function paymentStatus(): PaymentStatus { ... }
}

创建一次性引入

类似于一次性 traits. 当您有一个很长的模板并且希望使其更易于管理时,这种策略非常有用。布局中的 @include-ing 页眉和页脚或页面视图中的复杂表单等都没有问题。
导入命名空间而不是使用别名

有时您可能有多个同名的类。 与其使用别名导入它们,不如导入命名空间。

// 糟糕的示例
use App\Types\Entries\Visit as VisitEntry;
use App\Storage\Database\Models\Visit as VisitModel;

class DatabaseStorage
{
  public function log(VisitEntry $visit)
  {
    $visitModel = VisitModel::create([
      // ...
    ]);
  }
}
// 推荐的示例
use App\Types\Entries;
use App\Storage\Database\Models;

class DatabaseStorage
{
  public function log(Entries\Visit $visit)
  {
    $visitModel = Models\Visit::create([
      // ...
    ]);
  }
}

where() 创建查询方法

使用更具有表现力的名字创建查询方法,而不是编写完整的 where()。这可以让你的代码(例如控制器)尽可能更少地与数据库结构产生耦合,并且可以让代码更清晰。

// 不好的
Order::whereHas('status', function ($status) {
  return $status->where('canceled', true);
})->get();
// 好的
Order::whereCanceled()->get();

class Order extends Model
{
  public function scopeWhereCanceled(Builder $query)
  {
    return $query>whereHas('status', function ($status) {
      return $status->where('canceled', true);
    });
  }
}

不要使用模型方法来检索数据

如果你想要从模型中获取数据,可以创建一个访问器。保留以某种方式改变模型的方法。

// 坏的
$user->gravatarUrl();

class User extends Authenticable
{
  // ...

  public function gravatarUrl()
  {
    return "https://www.gravatar.com/avatar/" . md5(strtolower(trim($this->email)));
  }
}
// 好的
$user->gravatar_url;

class User extends Authenticable
{
  // ...

  public function getGravatarUrlAttribute()
  {
    return "https://www.gravatar.com/avatar/" . md5(strtolower(trim($this->email)));
  }
}

使用自定义配置文件

你可以在配置文件中存储类似「每页几条数据」这样的内容。不要直接将它们放在 app 配置文件中。创建你自己的配置文件。例如,在一个电商项目中,你可以使用 config/shop.php

// config/shop.php
return [
  'vat rates' => [
    0.21,
    0.15,
    0.10,
    0.0,
  ],
  'fee_vat_rate' => 0.21,

  'image_sizes' => [
    'base' => 500, // detail
    't1'   => 250, // index
    't2'   => 50,  // search
  ],
];

不要使用控制器命名空间

使用可调用的数组语法 [PostController::class, 'index'],而不是直接写控制器和方法名 PostController@index。这样写的话你可以点击 PostController 来跳转到类的定义。

// 坏的
Route::get('/posts', 'PostController@index');
// 好的
Route::get('/posts', [PostController::class, 'index']);

考虑使用单动作控制器

如果你有复杂的路由操作,考虑将它放在单独的控制器中。对于 OrderController::create,你可以创建 CreateOrderController。另一个解决方法是将该逻辑转移到一个动作类 —— 在你的实际情况中选择最好用的方法。

// 我们使用上面提到的类语法
Route::post('/orders/', CreateOrderController::class);

class CreateOrderController
{
  public function _invoke(Request $request)
  {
    // ...
  }
}

友好型 IDE

安装扩展,编写注释,使用类型提示。 您的 IDE 将帮助您让您的代码正常工作,这让您可以将更多的精力花在编写可读的代码上。

$products = Product::with('options')->cursor();

foreach ($products as $product) {
  /** @var Product $product */
  if ($product->options->isEmpty()) {
    // ...
  }
}

///////////////////////////

foreach (Order::whereDoesntHave('invoice')->whereIn('id', $orders->pluck('id'))->get() as $order) {
  /** @var Order $order */
  $order->createInvoice();

  // ...
}

///////////////////////////

$productImage
  ->help('Max 2 MB')
  ->store(function (NovaRequest $request, ProductModel $product) {
    /** @var UploadedFile $image */
    $image = $request->image;

    // ...
  });

使用短运算符

PHP 有很多很棒的操作符可以替代丑陋的 if 检查。 记住它们。

// 糟糕的
// truthy test
if (! $foo) {
  $foo = 'bar';
}

// null test
if (is_null($foo)) {
  $foo = 'bar';
}

// isset test
if (! isset($foo)) {
  $foo = 'bar';
}
// 优雅的
// truthy test
$foo = $foo ?: 'bar';

// null test
$foo = $foo ?? 'bar';
// PHP 7.4
$foo ??= 'bar';

// isset test
$foo = $foo ?? 'bar';
// PHP 7.4
$foo ??= 'bar';

决定您是否喜欢运算符周围的空格

在上面你可以看到我在 ! 和我要否定的值之间使用了空格。 我喜欢这个,因为它清楚地表明该值被否定了。 我在点周围做同样的事情。 可以按照您的喜好来清理您的代码。

助手函数而不是 Facades

考虑使用助手函数而不是 Facades 。 因为他们可以使得代码变得很整洁。 这在很大程度上取决于个人喜好,但调用全局函数而不是导入类并静态调用方法对我来说感觉更好。 session('key') 语法的加分项。

// 糟糕的示例
Cache::get('foo');
// 推荐的示例
cache()->get('foo');
// Better
cache('foo');

为业务逻辑创建自定义 Blade 指令

你可以通过创建自定义指令来让你的 Blade 模板更具有表现力。举个例子,
你可以使用 @admin 来检查用户是否是管理员,而不是使用具体的逻辑来判断。

// 不好的写法
@if(auth()->user()->hasRole('admin'))
  // ...
@else
  // ...
@endif
// 好的写法
@admin
  // ...
@else
  // ...
@endadmin

避免在 Blade 中查询数据库

可能有的时候你想要在 Blade 中查询数据库。有些情况下这是可以的,例如布局文件。但是如果是在视图文件中,则需要在控制器中向视图传入查询好的数据。

// 不好的写法
@foreach(Product::where('enabled', false)->get() as $product)
  // ...
@endforeach
// 好的写法
// 控制器
return view('foo', [
  'disabledProducts' => Product::where('enabled', false)->get(),
]);

// 视图
@foreach($disabledProducts as $product)
  // ...
@endforeach

使用精确的比较运算符

始终使用严格比较(===!==)。 如果需要,在比较之前将事物转换为正确的类型。 比奇怪的 == 结果要好。 还要考虑在您的代码中启用严格类型。 这将防止将错误数据类型的变量传递给函数。

// 糟糕的示例
$foo == 'bar';
// 推荐的示例
$foo === 'bar';
// Better
declare(strict_types=1);

仅当他们澄清事情时才使用文档块

很多人会不同意这一点,因为他们这样做了。 但这没有任何意义。 当它们不提供任何额外信息时,使用 文档是没有意义的。 如果类型提示足够了,就不要添加文档块,那样只是多余的。

// 糟糕的示例
// 全部没有类型
function add_5($foo)
{
  return $foo + 5;
}

// @param 注释精确地添加了 0% 值和 100% 噪声。
/**
 *给一个数加 5。
 *
 * @param int $foo
 * @return int
 */
function add_5(int $foo): int
{
  return $foo + 5;
}
// 推荐的示例
// 没有文档块,一切都清楚
function add_5(int $foo)
{
  return $foo + 5;
}

//类型提示说得尽可能多,注释说得更多。
/**
 * 把单词变成句子。
 *
 * @param string[] $words
 * @返回字符串
 */
function sentenceFromWords(array $words): string
{
  return implode(' ', $words) . '.';
}

// 个人最爱。 只使用能带来价值的注解。 不要仅仅因为它太常见就使用 description 或 @return。
/** @param string[] $words */
function sentenceFromWords(array $words): string
{
  return implode(' ', $words) . '.';
}

验证规则有单一的真实来源

如果你在多个地方验证某个资源的属性,你肯定希望将这些验证规则集中起来,这样你就不会在一个地方更改它们而忘记其他地方。 我经常发现自己在模型的方法中保留了验证规则。 这让我可以在任何需要的地方重用它们 —— 包括在控制器或表单请求中。

class Reply extends Model
{
  public static function getValidationRules(): array
  {
    return [
      'thread_id' => ['required', 'integer'],
      'user_id'   => ['required', 'integer'],
      'body'      => ['required', 'string', new SpamRule()],
    ];
  }
}

可以使用集合来优化代码

不要仅仅因为 Laravel 提供它们就将所有数组转换为集合,而是当您可以使用集合语法来优化代码时将数组转换为集合。

$collection = collect([
  ['name' => 'Regena', 'age' => null],
  ['name' => 'Linda', 'age' => 14],
  ['name' => 'Diego', 'age' => 23],
  ['name' => 'Linda', 'age' => 84],
]);

$collection->firstWhere('age', '>=', 18);

在对你有利的时候编写函数式代码

函数式代码不仅能使代码整洁,还能降低代码的易读性。 将常见循环重构为函数调用,但不要为了避免编写循环而编写愚蠢复杂的 reduce ()。 两者都有一个用例。

// 糟糕的示例
return array_unique(array_reduce($keywords, function ($result, $keyword) {
  return array_merge($result, array_reduce($this->variantGenerators, function ($result2, $generator) use ($keyword) {
    return array_merge($result2, array_map(function ($variant) {
      return strtolower($variant);
    }, $generator::getVariants($keyword)));
  }, []));
}, []));
// 推荐的示例
return $this->items()->reduce(function (Money $sum, OrderItem $item) {
  return $sum->addMoney($item->subtotal());
}, money(0, $this->currency));

注释通常表明代码设计不佳

在撰写评论之前,问问自己是否可以重命名某些内容或创建变量以提高可读性。 如果这是不可能的,请以您和您的同事在 6 个月后都能理解的方式撰写评论。
上下文问题

上面我说将业务逻辑转移到逻辑类 / 服务类是好的。 但上下文很重要, 这是来自流行的 “Laravel 最佳实践” 存储库的代码设计建议。 绝对没有理由将 3 行检查放入类中。 这只是过度设计。

// 糟糕的示例
public function store(Request $request)
{
  $this->articleService->handleUploadedImage($request->file('image'));
}

class ArticleService
{
  public function handleUploadedImage($image)
  {
    if (!is_null($image)) {
      $image->move(public_path('images') . 'temp');
    }
  }
}
// 推荐的示例
public function store(Request $request)
{
  if ($request->hasFile('image')) {
    $request->file('image')->move(public_path('images') . 'temp');
  }

  // ...
}

只使用对你有帮助的东西,忽略其他一切

您的目标是编写更具可读性的代码。您的目标不是按照某人在互联网上所说的那样去做。这些技巧只是有助于写出优雅的代码。 牢记你的最终目标并问自己「这样更好吗?」

原文地址:https://sohambanerjee.me/2020/06/26/clean-code-laravel/
译文地址:https://learnku.com/laravel/t/62638

支付宝捐赠
请使用支付宝扫一扫进行捐赠
微信捐赠
请使用微信扫一扫进行赞赏
有 0 篇文章