Laravel 编码技巧 - 其他

作者: 温新

图书: 【Laravel 编码技巧】

阅读: 180

时间: 2024-03-01 15:00:50

.env 中的本地主机

不要忘记在 .env 文件中将 APP_URLhttp://localhost 更改为实际的 URL,因为它将是电子邮件通知和其他地方的任何链接的基础。

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:9PHz3TL5C4YrdV6Gg/Xkkmx9btaE93j7rQTUZWm2MqU=
APP_DEBUG=true
APP_URL=http://your-real-url

过去/未来的时间值

如果你想在 过去/未来 设置一些时间值,你可以通过链接各种 Laravel/Carbon 的辅助函数来构建,比如 now()->[添加或减去某些时间]->setTime()

$product = Product::factory()->create([
    'published_at' => now()->addDay()->setTime(14, 00),
]);

在向浏览器发送响应后执行一些工作

你还可以使用中间件在响应已发送到浏览器后执行一些操作。这种中间件被称为 Terminable Middleware。

你可以通过在中间件上定义一个 terminate 方法来使中间件成为可终止的。

这个方法将在响应发送到浏览器后自动调用,它将具有请求和响应作为参数。

class TerminatingMiddleware
{
    public function handle($request, Closure $next)
    {
        return $next($request);
    }
 
    public function terminate($request, $response)
    {
        // ...
    }
}

使用 URL 片段重定向

你知道在 Laravel 重定向到路由时可以添加 URI 片段吗?

在重定向到页面的特定部分时非常有用,例如在产品页面上的评论部分。

return redirect()
    ->back()
    ->withFragment('contactForm');
    // domain.test/url#contactForm

return redirect()
    ->route('product.show')
    ->withFragment('reviews');
    // domain.test/product/23#reviews

使用中间件调整接收到的请求

Laravel 的中间件是转换传入请求的一种很好的方式。例如,我决定在我的应用程序中重新命名一个模型;而不是为了破坏性的更改而提高 API 版本,我简单地使用旧的引用转换这些请求。

class ConvertLicenseeIntoContact
{
     public function handle(Request $request, Closure $next)
     {
          if($request->json('licensee_id')) {
               $request->json()->set('contact_id', $request->json('licensee_id'));
          }

          return $next($request);
     }
}

重定向离开 Laravel 应用程序

有时,你可能需要将 Laravel 应用程序重定向到其他网站。在这种情况下,你可以在 redirect() 方法中调用一个方便的离开方法...

redirect()->away('https://www.google.com');

Blade 指令在特定环境中显示数据

你知道 Laravel 有一个 'production' Blade 指令,你可以用它来在生产环境下显示数据吗?

还有另一个 'env' 指令,你可以用它来在指定的环境中显示数据。

@production
     // 我只在生产环境可见...
@endproduction

@env('staging')
     // 我只在测试环境可见...
@endenv

@env(['staging', 'production'])
     // 我在测试和生产环境中都可见...
@endenv

根据时区调度 Laravel 作业

你知道你可以根据时区调度 Laravel 的任务吗?

为单个命令设置时区:

$schedule->command('report:generate')
         ->timezone('America/New_York')
         ->at('2:00');

如果你一直为所有的调度任务分配相同的时区,你可能希望在你的 app\Console\Kernel 类中定义一个 scheduleTimezone 方法:

protected function scheduleTimezone()
{
     return 'America/Chicago';
}

使用 assertModelMissing 代替 assertDatabaseMissing

在测试模型删除时,使用 assertModelMissing 而不是 assertDatabaseMissing

/** @test */
public function allowed_user_can_delete_task()
{
     $task = Task::factory()->for($this->project)->create();

     $this->deleteJson($task->path())
          ->assertNoContent();

     // 使用 assertModelMissing 直接检查模型是否在数据库中缺失
     $this->assertModelMissing($task);
}

用于格式化 diffForHumans() 的各种选项

在 Carbon 中,您知道您可以为 diffForHumans() 添加各种选项吗?阅读文档以获取更多示例。

$user->created_at->diffForHumans();
=> "17 hours ago"

$user->created_at->diffForHumans([
     'parts' => 2
]);
=> "17 hours 54 minutes ago"

$user->created_at->diffForHumans([
     'parts' => 3,
     'join' => ', ',
]);
=> "17 hours, 54 minutes, 50 seconds ago"

$user->created_at->diffForHumans([
     'parts' => 3,
     'join' => ', ',
     'short' => true,
]);
=> "17h, 54m, 50s ago"

在运行时创建自定义磁盘

你知道吗?您可以在运行时创建自定义磁盘,而无需将其配置在您的 config/filesystems 文件中。

这对于在不必将它们添加到配置中的情况下管理自定义路径中的文件可能很方便。

$avatarDisk = Storage::build([
    'driver' => 'local',
    'root' => storage_path('app/avatars'),
]);
$avatarDisk->put('user_avatar.jpg', $image);

何时(不)运行“composer update”

与 Laravel 关系不大,但... 永远不要在实时服务器上运行composer update,它很慢,而且会 "破坏 "版本库。一定要在本地计算机上运行 composer update,将新的 composer.lock 提交到版本库,然后在实时服务器上运行 composer install。

Composer:检查较新版本

如果你想知道哪些 composer.json 包发布了更新的版本,只需运行 composer outdated。你会得到一个完整的列表与所有的信息,像下面这样

phpdocumentor/type-resolver 0.4.0 0.7.1
phpunit/php-code-coverage   6.1.4 7.0.3 Library that provides collection, processing, and rende...
phpunit/phpunit             7.5.9 8.1.3 The PHP Unit Testing framework.
ralouphie/getallheaders     2.0.5 3.0.3 A polyfill for getallheaders.
sebastian/global-state      2.0.0 3.0.0 Snapshotting of global state

自动大写翻译

在翻译文件(resources/lang)中,您不仅可以将变量指定为 :variable,还可以大写为 :VARIABLE:Variable - 然后传递的任何值都将自动大写。

// resources/lang/en/messages.php
'welcome' => 'Welcome, :Name'

// Result: "Welcome, Taylor"
echo __('messages.welcome', ['name' => 'taylor']);

只含有小时的 Carbon

如果你想有当前日期不包含秒或者分钟,用Carbon的方法比如:setSeconds(0) 或者 setMinutes(0)

// 2020-04-20 08:12:34
echo now();

// 2020-04-20 08:12:00
echo now()->setSeconds(0);

// 2020-04-20 08:00:00
echo now()->setSeconds(0)->setMinutes(0);

// Another way - even shorter
echo now()->startOfHour();

单动作控制器

如果你想要创建一个只有一个操作的控制器,可以使用 __invoke() 方法,甚至创建一个 "invokable" 控制器。

路由:

Route::get('user/{id}', ShowProfile::class);

Artisan:

php artisan make:controller ShowProfile --invokable

控制器:

class ShowProfile extends Controller
{
    public function __invoke($id)
    {
        return view('user.profile', [
            'user' => User::findOrFail($id)
        ]);
    }
}

重定向至指定控制器方法

您不仅可以将 redirect() 重定向到 URL 或特定路由,还可以重定向到特定控制器的特定方法,甚至传递参数。使用以下方式:

return redirect()->action([SomeController::class, 'method'], ['param' => $value]);

使用旧版本的Laravel

如果你想用旧版本而非新版本的 Laravel,使用这个命令:

composer create-project --prefer-dist laravel/laravel project "7.*"

为分页链接添加参数

如果您有一个需要多次重复使用的回调函数,可以将其分配给一个变量,然后进行重复使用。

$userCondition = function ($query) {
    $query->where('user_id', auth()->id());
};

// 获取具有来自此用户的评论的文章
// 并仅返回此用户的这些评论
$articles = Article::with(['comments' => $userCondition])
    ->whereHas('comments', $userCondition)
    ->get();

Request: has any

您不仅可以使用 $request->has() 方法检查一个参数,还可以使用 $request->hasAny() 检查多个参数是否存在:

public function store(Request $request)
{
    if ($request->hasAny(['api_key', 'token'])) {
        echo 'We have API key passed';
    } else {
        echo 'No authorization parameter';
    }
}

简单分页

在分页中,如果您只想要“上一页/下一页”链接而不是所有的页数(并因此减少数据库查询),只需将 paginate() 更改为 simplePaginate()

// 替代
$users = User::paginate(10);

// 你可以这样做
$users = User::simplePaginate(10);

Blade 指令增加真假条件

Laravel 8.51 中新增了 @class Blade 指令,用于根据条件添加或移除 CSS 类。在模板中使用该指令可以简化条件样式的书写。具体使用方式如下:

在 Laravel 8.50 及之前版本:

<div class="@if ($active) underline @endif">`

在 Laravel 8.51 及以上版本,使用 @class 指令:

<div @class(['underline' => $active])>

另外,你还可以使用 @php 块中的条件来动态设置多个 CSS 类:

@php
    $isActive = false;
    $hasError = true;
@endphp

<span @class([
    'p-4',
    'font-bold' => $isActive,
    'text-gray-500' => ! $isActive,
    'bg-red' => $hasError,
])></span>

<!-- 上述代码等同于下面的 HTML 渲染结果 -->
<span class="p-4 text-gray-500 bg-red"></span>

无需队列即可使用 Job

在 Laravel 文档的 "Queues" 部分讨论了作业(Jobs),但你也可以在不使用队列的情况下使用作业,就像调用类来委托任务一样。只需从控制器中调用 $this->dispatchNow() 即可。

例如,你可以这样在控制器中使用作业:

use App\Jobs\ApproveArticle;
use App\Models\Article;
use Illuminate\Routing\Controller;

class ArticleController extends Controller
{
    public function approve(Article $article)
    {
        // 其他逻辑...
        $this->dispatchNow(new ApproveArticle($article));
        // 其他逻辑...
    }
}

上述代码中,ApproveArticle 作业将会立即执行,而不会放入队列。这种方式适用于那些不需要异步处理的任务。

在工厂类或 seeders 外部使用 Faker

如果你想生成一些虚假数据,你甚至可以在任何类中使用 Faker,而不仅仅是在工厂或种子中。

请注意:如果要在生产环境中使用它,你需要将 Faker 从 composer.json 文件中的 "require-dev" 移到 "require"。

示例代码如下:

use Faker;

class WhateverController extends Controller
{
    public function whateverMethod()
    {
        $faker = Faker\Factory::create();
        $address = $faker->streetAddress;
        // 其他逻辑...
    }
}

可定时执行的事

你可以以多种不同的结构安排每天/每小时运行的任务。

你可以调度 Artisan 命令、一个 Job 类、一个可调用类、一个回调函数,甚至执行一个 shell 脚本。

use App\Jobs\Heartbeat;

$schedule->job(new Heartbeat)->everyFiveMinutes();
$schedule->exec('node /home/forge/script.js')->daily();

use App\Console\Commands\SendEmailsCommand;

$schedule->command('emails:send Taylor --force')->daily();
$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

protected function schedule(Schedule $schedule)
{
    $schedule->call(function () {
        DB::table('recent_users')->delete();
    })->daily();
}

搜索 Laravel 文档

如果你想搜索 Laravel 文档中的某个关键词,默认情况下它只会给出前 5 个结果。也许还有更多?

如果你想查看所有结果,可以去 GitHub 的 Laravel 文档库直接搜索。https://github.com/laravel/docs。

过滤路由:列表

Laravel 8.34 的新特性: php artisan route:list 增加了 --except-path 标志,可以过滤掉不想看到的路由。参见原始 PR

Blade 指令用于避免代码重复

如果你在多个 Blade 文件中一直在进行相同的数据格式化,你可以创建自己的 Blade 指令。

这里有一个使用 Laravel Cashier 方法进行货币金额格式化的例子。

"require": {
    "laravel/cashier": "^12.9",
}
public function boot()
{
    Blade::directive('money', function ($expression) {
        return "<?php echo Laravel\Cashier\Cashier::formatAmount($expression, config('cashier.currency')); ?>";
    });
}
<div>Price: @money($book->price)</div>
@if($book->discount_price)
    <div>Discounted price: @money($book->dicount_price)</div>
@endif

Artisan 命令帮助

如果你不确定某个 Artisan 命令的参数,或者想知道有哪些参数可用,只需输入 php artisan help [你想要的命令] 即可。

当运行测试时禁用懒加载

如果你在运行测试时不想阻止惰性加载,你可以禁用它:

Model::preventLazyLoading(!$this->app->isProduction() && !$this->app->runningUnitTests());

使用 Laravel 中的两个出色辅助函数将产生神奇的结果...

在这种情况下,服务将被调用并进行重试(retry)。如果它仍然失败,它将被报告,但请求不会失败(rescue)。

rescue(function () {
    retry(5, function () {
        $this->service->callSomething();
    }, 200);
});

请求参数默认值

在这里,我们正在检查是否有 per_page(或任何其他参数)的值,如果有,我们将使用它,否则,我们将使用默认值。

// 替代这个
$perPage = request()->per_page ? request()->per_page : 20;

// 你可以这样做
$perPage = request('per_page', 20);

将中间件直接传递到路由中,而无需注册它

Route::get('posts', PostController::class)
    ->middleware(['auth', CustomMiddleware::class])

将数组转换为 CssClasses

use Illuminate\Support\Arr;

$array = ['p-4', 'font-bold' => $isActive, 'bg-red' => $hasError];

$isActive = false;
$hasError = true;

$classes = Arr::toCssClasses($array);

/*
 * 'p-4 bg-red'
 */

Laravel Cascade(Stripe)中的“upcomingInvoice”方法

你可以显示客户在下一个结算周期将支付多少。

Laravel Cashier(Stripe)中有一个 "upcomingInvoice" 方法用于获取即将到来的发票详情。

Route::get('/profile/invoices', function (Request $request) {
    return view('/profile/invoices', [
        'upcomingInvoice' => $request->user()->upcomingInvoice(),
        'invoices' => $request->user()->invoices(),
    ]);
});

Laravel 的 Request 中 exists() 与 has() 的区别

// https://example.com?popular
$request->exists('popular') // true
$request->has('popular') // false

// https://example.com?popular=foo
$request->exists('popular') // true
$request->has('popular') // true

有多种方式可以将变量传递给视图:

// 第一种方式 ->with()
return view('index')
    ->with('projects', $projects)
    ->with('tasks', $tasks);

// 第二种方式 - 作为数组
return view('index', [
    'projects' => $projects,
    'tasks' => $tasks
]);

// 第三种方式 - 与第二种类似,但使用变量
$data = [
    'projects' => $projects,
    'tasks' => $tasks
];
return view('index', $data);

// 第四种方式 - 最简洁的方式 - compact()
return view('index', compact('projects', 'tasks'));

调度标准 shell 命令

我们可以在 Laravel 定时命令中安排定期的 Shell 命令:

// app/Console/Kernel.php

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        $schedule->exec('node /home/forge/script.js')->daily();
    }
}

未经验证的 HTTP 客户端请求

有时,你可能希望在本地环境中发送不验证 SSL 的 HTTP 请求,可以这样做:

return Http::withoutVerifying()->post('https://example.com');

如果您想设置多个选项,可以使用 withOptions

return Http::withOptions([
    'verify' => false,
    'allow_redirects' => true
])->post('https://example.com');

不断言任何事情的测试

这个测试不断言任何东西,只是启动可能会抛出异常的操作。

class MigrationsTest extends TestCase
{
    public function test_successful_foreign_key_in_migrations()
    {
        // 我们只测试迁移是否成功或抛出异常
        $this->expectNotToPerformAssertions();

        Artisan::call('migrate:fresh', ['--path' => '/database/migrations/task1']);
    }
}

Str::mask() 方法

Laravel 8.69 发布了 "Str::mask()" 方法,该方法使用重复字符掩码字符串的一部分。

class PasswordResetLinkController extends Controller
{
    public function sendResetLinkResponse(Request $request)
    {
        $userEmail = User::where('email', $request->email)->value('email'); // username@domain.com

        $maskedEmail = Str::mask($userEmail, '*', 4); // user***************

        // 如果需要,可以将负数作为mask方法的第三个参数,该参数将指示方法从字符串末尾的给定距离开始掩码

        $maskedEmail = Str::mask($userEmail, '*', -16, 6); // use******domain.com
    }
}

扩展 Laravel 类

在许多内置的Laravel类上有一个称为macro的方法,例如Collection、Str、Arr、Request、Cache、File等等。

您可以像这样在这些类上定义自己的方法:

Str::macro('lowerSnake', function (string $str) {
    return Str::lower(Str::snake($str));
});

// 将返回:"my-string"
Str::lowerSnake('MyString');

can 特性

如果您正在运行Laravel v8.70,您可以直接链接can()方法,而不是使用middleware('can:..')

// 替代
Route::get('users/{user}/edit', function (User $user) {
    ...
})->middleware('can:edit,user');

// 您可以这样做
Route::get('users/{user}/edit', function (User $user) {
    ...
})->can('edit', 'user');

// 注意:在两种情况下,您必须编写UserPolicy才能执行此操作

临时下载 URL

您可以使用临时下载 URL 来防止未经授权的访问云存储资源。例如,当用户要下载文件时,我们会重定向到一个s3资源,但该 URL 将在5秒后过期。

public function download(File $file)
{
    // 通过重定向到在5秒后过期的临时s3 URL来启动文件下载
    return redirect()->to(
        Storage::disk('s3')->temporaryUrl($file->name, now()->addSeconds(5))
    );
}

处理深度嵌套数组

如果您有一个复杂的数组,可以使用 data_get() 辅助函数通过 "点" 表示法和通配符从嵌套数组中检索值。

$data = [
  0 => ['user_id' => 1, 'created_at' => 'timestamp', 'product' => {object Product}],
  1 => ['user_id' => 2, 'created_at' => 'timestamp', 'product' => {object Product}],
  2 => // 等等
];

// 现在我们想要获取所有产品的ID。我们可以这样做:

data_get($data, '*.product.id');

// 现在我们有所有产品的ID [1, 2, 3, 4, 5, 等...]

在下面的例子中,如果 requestusername 中任何一个缺失,您将收到错误。

$value = $payload['request']['user']['name'];

// data_get 函数接受一个默认值,如果未找到指定的键,则将返回该默认值。
$value = data_get($payload, 'request.user.name', 'John');

自定义异常的呈现方式

您可以通过在异常类中添加 render 方法来自定义异常的呈现方式。

例如,这允许您在请求期望 JSON 时返回 JSON 而不是 Blade 视图。

abstract class BaseException extends Exception
{
    public function render(Request $request)
    {
        if ($request->expectsJson()) {
            return response()->json([
                'meta' => [
                    'valid'   => false,
                    'status'  => static::ID,
                    'message' => $this->getMessage(),
                ],
            ], $this->getCode());
        }

        return response()->view('errors.' . $this->getCode(), ['exception' => $this], $this->getCode());
    }
}
class LicenseExpiredException extends BaseException
{
    public const ID = 'EXPIRED';
    protected $code = 401;
    protected $message = 'Given license has expired.'
}

tap 助手函数

tap 辅助函数是在调用对象方法后移除额外的返回语句的一种好方法,使代码更加简洁。

// 没有使用 tap
$user->update(['name' => 'John Doe']);

return $user;

// 使用 tap()
return tap($user)->update(['name' => 'John Doe']);

重置所有剩余的时间单位

你可以在 DateTime::createFromFormat 方法中插入感叹号来重置所有剩余的时间单元:

// 2021-10-12 21:48:07.0
DateTime::createFromFormat('Y-m-d', '2021-10-12');

// 2021-10-12 00:00:00.0
DateTime::createFromFormat('!Y-m-d', '2021-10-12');

// 2021-10-12 21:00:00.0
DateTime::createFromFormat('!Y-m-d H', '2021-10-12');

如果出现问题,控制台内核中的预定命令可以自动通过电子邮件发送其输出

你知道吗,如果出现问题,你在控制台内核中调度的任何命令都可以自动通过电子邮件发送它们的输出

$schedule
    ->command(PruneOrganizationsCOmmand::class)
    ->hourly()
    ->emailOutputOnFailure(config('mail.support'));

在使用 GET 参数构建自定义筛选查询时要小心

if (request()->has('since')) {
    // example.org/?since=
    // 在这里使用不当的操作符和值的组合会导致问题
    $query->whereDate('created_at', '<=', request('since'));
}

if (request()->input('name')) {
    // example.org/?name=0
    // 失败,因为评估为 false,无法应用查询过滤器
    $query->where('name', request('name'));
}

if (request()->filled('key')) {
    // 检查 GET 参数是否有值的正确方式
}

清理臃肿的路由文件

清掉臃肿的路由文件,并将其拆分以保持组织有序

class RouteServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->routes(function () {
            Route::prefix('api/v1')
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));

            Route::prefix('webhooks')
                ->namespace($this->namespace)
                ->group(base_path('routes/webhooks.php'));

            Route::middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));

            if ($this->app->environment('local')) {
                Route::middleware('web')
                    ->namespace($this->namespace)
                    ->group(base_path('routes/local.php'));
            }
        });
    }
}

您可以将电子邮件发送到自定义日志文件

在 Laravel 中,你可以将邮件发送到自定义的日志文件。

你可以像这样设置你的环境变量:

MAIL_MAILER=log
MAIL_LOG_CHANNEL=mail

同时配置你的日志通道:

'mail' => [
    'driver' => 'single',
    'path' => storage_path('logs/mails.log'),
    'level' => env('LOG_LEVEL', 'debug'),
],

现在你的所有邮件都会记录在 /logs/mails.log 文件中。

这是一个用例,可以快速测试你的邮件功能。

Markdown 变得简单

Laravel 提供了一个接口,可以直接将 Markdown 转换为HTML,无需安装新的 Composer 包。

$html = Str::markdown('# Changelogfy');

输出:

<h1>Changelogfy</h1>

使用 whenFilled() 辅助函数简化请求中的 if 功能

我们经常编写if语句来检查请求中是否存在某个值。

您可以使用whenFilled()助手函数来简化它。

public function store(Request $request)
{
    $request->whenFilled('status', function (string $status)) {
        // 在这里处理状态值
    }, function () {
        // 当状态值未填充时调用此处
    });
}

将参数传递给中间件

您可以通过在值后面添加:来为特定路由传递参数给中间件。例如,我正在使用单个中间件基于路由强制执行不同的身份验证方法。

Route::get('...')->middleware('auth.license');
Route::get('...')->middleware('auth.license:bearer');
Route::get('...')->middleware('auth.license:basic');
class VerifyLicense
{
    public function handle(Request $request, Closure $next, $type = null)
    {
        $licenseKey = match ($type) {
            'basic'  => $request->getPassword(),
            'bearer' => $request->bearerToken(),
            default  => $request->get('key')
        };

        // 验证许可证并基于身份验证类型返回响应
    }
}

从会话中获取值并删除

如果您需要从 Laravel 会话中获取某些值,然后立即将其删除,考虑使用 session()->pull($value)。它会为您完成这两个步骤。

// 之前
$path = session()->get('before-github-redirect', '/components');

session()->forget('before-github-redirect');

return redirect($path);

// 之后
return redirect(session()->pull('before-github-redirect', '/components'));

$request->date() 方法

本周的 Laravel v8.77 新增:$request->date() 方法。

现在你不需要手动调用 Carbon,可以像下面这样做: $post->publish_at = $request->date('publish_at')->addHour()->startOfHour();

使用分页时,用 through 代替 map

当您想映射分页数据并仅返回字段的子集时,使用 through 而不是 mapmap 会破坏分页对象并更改其标识。而 through 在分页数据本身上工作。

// 不要:映射分页数据
$employees = Employee::paginate(10)->map(fn ($employee) => [
    'id' => $employee->id,
    'name' => $employee->name
]);

// 做:映射分页数据
$employees = Employee::paginate(10)->through(fn ($employee) => [
    'id' => $employee->id,
    'name' => $employee->name
]);

在 HTTP 请求中快速添加 bearer token

有一个 withToken 方法,用于将 Authorization 标头附加到请求。

// 不好!
Http::withHeader([
    'Authorization' => 'Bearer dQw4w9WgXcq'
]);

// 是的!
Http::withToken('dQw4w9WgXcq');

复制文件夹中的文件或所有文件

你可以使用 readStreamwriteStream 来将文件(或从一个文件夹复制的所有文件)从一个磁盘复制到另一个,以保持低内存使用。

// 列出文件夹中的所有文件
$files = Storage::disk('origin')->allFiles('/from-folder-name');

// 使用普通的 get 和 put(一次性整个文件字符串)
foreach($files as $file) {
    Storage::disk('destination')->put(
        "optional-folder-name" . basename($file),
        Storage::disk('origin')->get($file)
    );
}

// 更好的方法:使用流来保持内存使用低(适用于大文件)
foreach ($files as $file) {
    Storage::disk('destination')->writeStream(
        "optional-folder-name" . basename($file),
        Storage::disk('origin')->readStream($file)
    );
}

Sessions has() vs exists() vs missing()

会话(Session)中的 has()exists()missing() 方法你知道吗?

// 如果项目存在且不为 null,则 has 方法返回 true。
$request->session()->has('key');

// 如果项目存在,即使其值为 null,exists 方法也返回 true。
$request->session()->exists('key');

// 如果项目不存在或者项目为 null,missing 方法返回 true。
$request->session()->missing('key');

测试是否将正确的数据传递给视图

需要测试是否向视图传递了正确的数据吗?您可以在响应上使用 viewData 方法。以下是一些示例:

/** @test */
public function it_has_the_correct_value()
{
    // ...
    $response = $this->get('/some-route');
    $this->assertEquals('John Doe', $response->viewData('name'));
}

/** @test */
public function it_contains_a_given_record()
{
    // ...
    $response = $this->get('/some-route');
    $this->assertTrue($response->viewData('users')->contains($userA));
}

/** @test */
public function it_returns_the_correct_amount_of_records()
{
    // ...
    $response = $this->get('/some-route');
    $this->assertCount(10, $response->viewData('users'));
}

使用 Redis 跟踪页面浏览量

追踪像页面浏览量这样的东西,使用 MySQL 可能在处理高流量时会带来相当大的性能损耗。在这方面,Redis 表现更为出色。您可以使用 Redis 和一个定期的命令来在固定的时间间隔内保持 MySQL 的同步。

// routes/web.php
Route::get('{project:slug}', function (Project $project) {
    // 而不是使用 $project->increment('views'),我们使用了 Redis
    // 我们将浏览量按项目 ID 进行分组
    Redis::hincrby('project-views', $project->id, 1);
});
// Console/Kernel.php
$schedule->command(UpdateProjectViews::class)->daily();

// Console/Commands/UpdateProjectViews.php
// 从我们的 Redis 实例中获取所有视图
$views = Redis::hgetall('project-views');

/*
[
    (id) => (views)
    1 => 213,
    2 => 100,
    3 => 341
]
*/

// 循环遍历所有项目视图
foreach ($views as $projectId => $projectViews) {
    // 在我们的 MySQL 表中递增项目视图
    Project::find($projectId)->increment('views', $projectViews);
}

// 从我们的 Redis 实例中删除所有视图
Redis::del('project-views');

to_route() 辅助函数

Laravel 9 提供了 response()->route() 的缩写版本,看下面的代码:

// 旧的方式
Route::get('redirectRoute', function() {
    return redirect()->route('home');
});

// Laravel 9 之后
Route::get('redirectRoute', function() {
    return to_route('home');
});

这个助手的工作方式与 redirect()->route('home') 相同,但比旧方式更为简洁。

当队列工作者关闭时,暂停长期运行的作业

在运行长时间任务时,如果队列工作者被关闭,可能发生以下情况:

  • 停止工作者。
  • 发送信号 SIGTERM(Horizon 使用 SIGINT)。
  • 按下 CTRL + C(Linux/Windows)。

然后,作业进程可能在执行过程中变得损坏。

通过检查 app('queue.worker')->shouldQuit,我们可以确定工作者是否正在关闭。通过这种方式,我们可以保存当前进度并重新排队作业,这样当队列工作者再次运行时,它可以从上次中断的地方继续。

这在容器化的世界(Kubernetes、Docker 等)中非常有用,其中容器随时可能被销毁和重新创建。

<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class MyLongJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $timeout = 3600;

    private const CACHE_KEY = 'user.last-process-id';

    public function handle()
    {
        $processedUserId = Cache::get(self::CACHE_KEY, 0); // 获取上次处理的项目 ID
        $maxId = User::max('id');

        if ($processedUserId >= $maxId) {
            Log::info("所有用户已经处理完毕!");
            return;
        }

        while ($user = User::where('id', '>', $processedUserId)->first()) {
            Log::info("处理用户 ID:{$user->id}");

            // 在这里执行长时间的工作以处理用户
            // 例如,调用计费 API,为用户准备发票等。

            $processedUserId = $user->id;
            Cache::put(self::CACHE_KEY, $processedUserId, now()->addSeconds(3600)); // 更新上次处理的项目 ID

            if (app('queue.worker')->shouldQuit) {
                $this->job->release(60); // 以 60 秒的延迟重新排队作业
                break;
            }
        }

        Log::info("所有用户已经成功处理!");
    }
}

这确保了在工作者关闭时,作业可以在重新排队后从中断的地方继续处理。

Laravel 测试中的冻结时间

在 Laravel 测试中,有时你可能需要冻结时间。

这在基于时间戳进行断言或需要根据日期和/或时间进行查询时特别有用。

// 以前,在测试的顶部你可以这样写来冻结时间:
Carbon::setTestNow(Carbon::now());
// 你也可以使用 "travelTo" 方法:
$this->travelTo(Carbon::now());
// 现在你可以使用新的 "freezeTime" 方法来使你的代码更易读和清晰:
$this->freezeTime();

新助手函数 squish

Laravel 9.7 Squish Helper 的新特性。

$result = Str::squish(' Hello   John,         how   are   you?    ');
// Hello John, how are you?

指定计划任务失败或成功时的操作

你可以指定在计划任务失败或成功时执行的操作。

$schedule->command('emails:send')
        ->daily()
        ->onSuccess(function () {
            // 任务成功时执行的操作
        })
        ->onFailure(function () {
            // 任务失败时执行的操作
        });

特定环境下的计划命令

在特定环境中运行Laravel计划任务。

// 不太好
if (app()->environment('production', 'staging')) {
    $schedule->command('emails:send')
        ->daily();
}

// 更好
$schedule->command('emails:send')
        ->daily()
        ->onEnvironment(['production', 'staging']);

向您自己的类中添加条件行为

你可以使用Conditionable Trait 来避免使用 if/else 并提倡方法链式调用。

<?php

namespace App\Services;

use Illuminate\Support\Traits\Conditionable;

class MyService
{
    use Conditionable;

    // ...
}
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Http\Requests\MyRequest;
use App\Services\MyService;

class MyController extends Controller
{
    public function __invoke(MyRequest $request, MyService $service)
    {
        // 不太好
        $service->addParam($request->param);

        if ($request->has('something')) {
            $service->methodToBeCalled();
        }

        $service->execute();
        // ---

        // 更好
        $service->addParam($request->param)
            ->when($request->has('something'), fn ($service) => $service->methodToBeCalled())
            ->execute();
        // ---

        // ...
    }
}

作业失败时执行操作

在某些情况下,我们希望在作业失败时执行一些操作,例如发送电子邮件或通知。

为此,我们可以在作业类中使用 failed() 方法,就像使用 handle() 方法一样:

namespace App\Jobs\Invoice;

use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Throwable;

class CalculateSingleConsignment implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // ... __construct() 方法,handle() 方法等。

    public function failed(Throwable $exception)
    {
        // 在作业失败时执行任何操作
    }
}

作业失败时忽略数据库

如果您在作业失败时需要绕过数据库,可以通过以下两种方式之一来跳过数据库:

  1. 将环境变量 QUEUE_FAILED_DRIVER 的值设置为 null。从 Laravel 8 及以上版本开始有效。

  2. config/queue.php 文件中将 failed 的值设置为 null,替换数组(如下面的代码)。对于 Laravel 7 及更早版本有效。

    'failed' => null,
    

为什么要这样做呢?对于不需要存储失败作业并需要具有非常高 TPS 的应用程序,跳过数据库可能是非常有利的,因为这样可以避免访问数据库,节省时间并防止数据库崩溃。

请登录后再评论