自定义逻辑 Macros
Laravel 从 4.2 版本开始就有了宏的概念,本文将展示如何创建它们。
在 Laravel 代码库,会发现无数对名为 \Illuminate\Support\Traits\Macroable
宏的引用,这个特性允许实时扩展类的公共接口。
什么是宏
简单来说,Laravel Macro 是一种用 Laravel 类中不存在的代码向 Laravel 内部组件添加一些缺失功能的方法。
为了实现 Laravel 宏,Laravel 提供了一个名为 Macroable 的 PHP 特性。
可以检查位于 Illuminate\Http\Response
的 Laravel Response 类,它实现了 Macroable 特征,这意味着可以使用宏扩展 Response 类。
可宏化的类
以下 Laravel 类允许使用 Illuminate\Support\Traits\Macroable
特征创建宏。以下是一些最常用的创建宏的类:
Illuminate\Auth\RequestGuard
Illuminate\Auth\SessionGuard
Illuminate\Cache\Repository
Illuminate\Console\Command
Illuminate\Console\Scheduling\Event
Illuminate\Cookie\CookieJar
Illuminate\Database\Eloquent\FactoryBuilder
Illuminate\Database\Eloquent\Relations\Relation
Illuminate\Database\Grammar
Illuminate\Database\Query\Builder
Illuminate\Database\Schema\Blueprint
Illuminate\Filesystem\Filesystem
Illuminate\Foundation\Testing\TestResponse
Illuminate\Http\JsonResponse
Illuminate\Http\RedirectResponse
Illuminate\Http\Request
Illuminate\Http\Response
Illuminate\Http\UploadedFile
Illuminate\Mail\Mailer
Illuminate\Routing\PendingResourceRegistration
Illuminate\Routing\Redirector
Illuminate\Routing\ResponseFactory
Illuminate\Routing\Route
Illuminate\Routing\Router
Illuminate\Routing\UrlGenerator
Illuminate\Support\Arr
Illuminate\Support\Collection
Illuminate\Support\LazyCollection
Illuminate\Support\Str
Illuminate\Support\Testing\Fakes\NotificationFake
Illuminate\Translation\Translator
Illuminate\Validation\Rule
Illuminate\View\Factory
Illuminate\View\View
Illuminate\Validation\Rules\File
Illuminate\Support\Carbon
自定义宏
在创建 Laravel 宏之前,必须确保目标类使用 Macroable 特征。
在这里,将在 Illuminate\Support\Str
类上创建一个宏,它将检查名为 isLength
的给定字符串的长度。
可以在 AppServiceProvider
类的 boot()
方法中定义该宏。
<?php
namespace App\Providers;
use Illuminate\Support\Str; //[!code ++]
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Str::macro('isLength', function ($str, $length) { //[!code ++]
return static::length($str) == $length; //[!code ++]
}); //[!code ++]
}
}
在 AppServiceProvider
的 boot
方法中,我们在 Str
类上定义了一个 isLength
宏,它只是将第一个参数的长度与第二个参数的长度进行比较。
宏可以有许多决定的参数。需要注意的是,在自定义的宏中调用了 static::length
,因为宏仍然可以完全访问原始类上的方法。
现在可以在应用程序中的任何位置使用该宏:
use Illuminate\Support\Str;
Str::isLength('This is a Laravel Macro', 23); // true
Macroable
特征的内部机制允许从静态和实例上下文中调用宏。
向 Str
类添加另一个宏,它将把给定的字符附加到给定的字符串中。
Str::macro('appendTo', function ($str, $char) {
return $char.$str;
});
现在可以像下面这样调用上面的宏:
use Illuminate\Support\Str;
dd(Str::appendTo('LaraShout', '@')); // @LaraShout
创建多个宏
可以使用 AppServiceProvider
将两个宏添加到 Str 类中。
随着应用程序逻辑的增长,AppServiceProvider
将开始变得混乱。
从 Laravel 5.5 开始,可以定义基于类的宏,这将使代码不再那么混乱。
将继续前面的示例,并将两个宏移至一个新类中。
将创建一个名为 StrMixin
的新类并将其存储在 app/Mixins
文件夹中。添加以下代码:
<?php
namespace App\Mixins;
class StrMixin
{
/**
* @return \Closure
*/
public function isLength()
{
return function($str, $length) {
return static::length($str) == $length;
};
}
/**
* @return \Closure
*/
public function appendTo()
{
return function($str, $char) {
return $char.$str;
};
}
}
现在在 AppServiceProvider
中,可以删除以前的宏声明。
可以使用 mixin()
方法来初始化给定类的所有宏。
进行修改后 AppServiceProvider
将如下所示:
<?php
namespace App\Providers;
use App\Mixins\StrMixin; //[!code ++]
use Illuminate\Support\Str;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Str::mixin(new StrMixin); //[!code ++]
}
}
现在,如果在 Str
类上测试 isLength()
和 appendTo()
方法,将得到相同的结果。
一些示例
Model
<?php
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot()
{
// Define the 'whereLike' macro
Builder::macro('whereLike', function ($attributes, string $searchTerm) {
$this->where(function (Builder $query) use ($attributes, $searchTerm) {
foreach (Arr::wrap($attributes) as $attribute) {
$query->when(
// Check if the attribute is not an expression and contains a dot (indicating a related model)
!($attribute instanceof \Illuminate\Contracts\Database\Query\Expression) &&
str_contains((string) $attribute, '.'),
function (Builder $query) use ($attribute, $searchTerm) {
// Split the attribute into a relation and related attribute
[$relation, $relatedAttribute] = explode('.', (string) $attribute);
// Perform a 'LIKE' search on the related model's attribute
$query->orWhereHas($relation, function (Builder $query) use ($relatedAttribute, $searchTerm) {
$query->where($relatedAttribute, 'LIKE', "%{$searchTerm}%");
});
},
function (Builder $query) use ($attribute, $searchTerm) {
// Perform a 'LIKE' search on the current model's attribute
// also attribute can be an expression
$query->orWhere($attribute, 'LIKE', "%{$searchTerm}%");
}
);
}
});
});
}
//...
}
Post::query()
->whereLike([
'title',
// search in the current model's 'title' attribute
'description',
// search in the current model's 'description' attribute
'user.name',
// search in the related model's 'name' attribute
'user.email',
// search in the related model's 'email' attribute
DB::raw('DATE_FORMAT(created_at, "%d/%m/%Y")'),
// search in the formatted 'created_at' attribute
DB::raw('CONCAT(user.first_name, " ", user.last_name)'),
// search in the concatenated 'first_name' and 'last_name' attributes
], request()->search)
// search for the request's 'search' query parameter
->with('user')
->get();
Http
use Illuminate\Support\Facades\Http;
Http::macro('api-local', function() {
return Http::withHeaders([
'X-Header' => 'Value',
])->baseUrl('https://example.com');
});
Str
use Illuminate\Support\Str;
Str::macro('slugify', function($value) {
return Str::slug($value);
});
Response
use Illuminate\Support\Facades\Response;
Response::macro('api', function($data) {
return response()->json($data);
})
Validation
注意 Validator
使用是 extend
方法来自定义宏,并不是 micro
方法。
use Carbon\Carbon;
use Illuminate\Support\Facades\Validator;
Validator::extend('adult', function($attribute, $value) {
return Caron::parse($value)->age >= 18;
});
Cache
use Illuminate\Support\Facades\Cache;
Cache::macro('rememberForeverJson', function($key, $callback) {
return Cache::rememberForever($key, function() use ($callback) {
return json_decode($callback());
});
});
View
use Illuminate\Support\Facades\Blade;
Blade::macro('datetime', function($expression) {
return "<?php echo with[$expression]->format('Y-m-d H:i:s'); ?>";
});
Event
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Event;
Event::macro('logAndDispatch', function($event) {
Log::info("Event: {$event} dispatched");
event($event);
});
Form
Filament 中的 Form 类允许自定义宏。
use Filament\Forms\Form;
Form::macro('customInput', fn (string $name, mixed $value): string => "<input type='text' name='{$name}' value='{$value}' />");
it('has customInput method for form', function (string $name, mixed $value) {
$output = \Filament\Forms\Form::customInput($name, $value);
$expect = "<input type='text' name='{$name}' value='{$value}' />";
expect($output)->toEqual($expect);
})->with([
['name', null],
['name', ''],
['name', 'value']
]);
File
通过 File
提供的宏,可以方便的自定义文件类型验证规则。
// AppServiceProvider.php
use Illuminate\Validation\Rules\File;
File::macro('document', fn (): File => File::types(['pdf', 'rtf', 'doc', 'docx']));
// UploadController.php
use Illuminate\Validation\Rules\File;
public function store($request)
{
$request->validate([
'file' => [File::document()->max(20 * 1024)],
]);
// ...
}
/**
* @throws ReflectionException
*/
it('has custom types when using document macro', function () {
$file = (new \Illuminate\Validation\Rules\File())->document();
$types = ['pdf', 'rtf', 'doc', 'docx'];
$allowedMimetypes = (new ReflectionClass($file))->getProperty('allowedMimetypes')->getValue($file);
expect($allowedMimetypes)->toEqual($types);
});
Carbon
创建了一个名为 toUserTimezone
的宏,它将 Carbon 实例格式化为用户的时区或软件默认的时区。$this
关键字指的是调用该宏的 Carbon 实例。
// AppServiceProvider.php
use Illuminate\Support\Carbon;
Carbon::macro('toUserTimezone', fn (): Carbon => $this->tz(auth()->user()?->timezone ?? config('app.timezone')));
<?php
$date = Carbon::now()->toUserTimezone();
it('converts carbon date to default timezone', function () {
// Set default timezone in config
config()->set('app.timezone', 'PRC');
// Create a Carbon instance
$date = Carbon::now('UTC');
// Apply macro
$defaultDate = $date->toUserTimezone();
// Check if timezone is converted correctly
expect($defaultDate->timezoneName)
->toEqual('PRC');
});
it('converts carbon date to user timezone', function () {
// Create user and login
Auth::login($user = User::factory()->make(['timezone' => 'America/Los_Angeles']));
// Create a Carbon instance
$date = Carbon::now('UTC');
// Apply macro
$userDate = $date->toUserTimezone();
// Check if timezone is converted correctly
expect($userDate->timezoneName)
->toBe($user->timezone)
->toEqual('America/Los_Angeles');
});