探索 Laravel 源碼
發現好用的小工具
球魚
- 朝向一條龍工程師努力的魚
- 主要參加 COSCUP 社群
- 致力於收集各社群的吉祥物
當你開發了一個萬用的 Library
Validation::push($data)
->validateElementIsNumber()
->validateFirstIsZero()
->toJson();
就會開啟一連串的 issue
Macroable
/**
* Dynamically handle calls to the class.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public static function __callStatic($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
$macro = static::$macros[$method];
if ($macro instanceof Closure) {
$macro = $macro->bindTo(null, static::class);
}
return $macro(...$parameters);
}
/**
* Dynamically handle calls to the class.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
$macro = static::$macros[$method];
if ($macro instanceof Closure) {
$macro = $macro->bindTo($this, static::class);
}
return $macro(...$parameters);
}
快速讓 class 可以擴展 function
Macro
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Lang;
Collection::macro('toLocale', function ($locale) {
return $this->map(function ($value) use ($locale) {
return Lang::get($value, [], $locale);
});
});
$collection = collect(['first', 'second']);
$translated = $collection->toLocale('es');
PHP Magic Function
Method Override: __call/__callStatic
<?php
class Dog
{
public function bark()
{
print("One!\n");
}
public function __call(string $name, array $arguments)
{
if ($name == 'meow')
{
print("Meow!\n");
}
if ($name == 'bark') // 因為有定義過,所以不會執行
{
print("AARRR\n");
}
}
}
(new Dog())->bark();
(new Dog())->meow();
回到剛剛的例子
class Validation {
use Macroable;
}
Validation::macro('toLocale', function ($locale) {
return $this->map(function ($value) use ($locale) {
return Lang::get($value, [], $locale);
});
});
Validation::push($data)
->validateElementIsNumber()
->validateFirstIsZero()
->toLocale();
Manager
快速建立 Factory
Factory 模式
Factory + PHP 動態函式 = Manager
PHP 動態函式
class Magician
{
public function getFromHat($name)
{
$func = 'create' . $name;
$this->$func();
}
public function createApple()
{
print('apple');
}
public function createBanana()
{
print('banana');
}
public function createRabbit()
{
print('rabbit');
}
}
Manager
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
'Unable to resolve NULL driver for [%s].', static::class
));
}
// If the given driver has not been created before, we will create the instances
// here and cache it so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
protected function createDriver($driver)
{
// First, we will determine if a custom driver creator exists for the given driver and
// if it does not we will check for a creator method for the driver. Custom creator
// callbacks allow developers to build their own "drivers" easily using Closures.
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
範例:HashManager
<?php
namespace Illuminate\Hashing;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Manager;
class HashManager extends Manager implements Hasher
{
/**
* Create an instance of the Bcrypt hash Driver.
*
* @return \Illuminate\Hashing\BcryptHasher
*/
public function createBcryptDriver()
{
return new BcryptHasher($this->config->get('hashing.bcrypt') ?? []);
}
/**
* Create an instance of the Argon2i hash Driver.
*
* @return \Illuminate\Hashing\ArgonHasher
*/
public function createArgonDriver()
{
return new ArgonHasher($this->config->get('hashing.argon') ?? []);
}
// ...
/**
* Get the default driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->config->get('hashing.driver', 'bcrypt');
}
}
Pipeline
現成的責任鏈小工具
app(Pipeline::class)
->send([
'user' => $user,
'account' => $account,
])->through([
OneClass::class,
TwoClass::class,
ThreeClass::class,
])
->thenReturn();
<?php
use Closure;
class OneClass
{
public function handle(array $context, Closure $next)
{
// handle 1
$next($context);
// handle 6
return $context;
}
}
class TwoClass
{
public function handle(array $context, Closure $next)
{
// handle 2
$next($context);
// handle 5
return $context;
}
}
class ThreeClass
{
public function handle(array $context, Closure $next)
{
// handle 3
$next($context);
// handle 4
return $context;
}
}
OneClass
TwoClass
ThreeClass
handle 1
handle 2
handle 3 、 4
handle 5
handle 6
責任鏈模式
class 1
class 2
class 3
$one = new HandleOne();
$two = new HandleTwo();
$three = new HandleThree();
$one.setNext($two);
$two.setNext($three);
$one.handle($request);
幸好,我們有 reduce
public function then(Closure $destination)
{
// $one->handle($request, $two->handle($request, $three->handle($request, ...)))
$pipeline = array_reduce(
array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
return $pipe($passable, $stack);
} catch (Throwable $e) {
return $this->handleException($passable, $e);
}
};
};
}
如何三秒寫出一個 Pipeline
責任鏈的使用方式
class AwesomeClass
{
publuc function handle1() {}
publuc function handle2() {}
publuc function handle3() {}
publuc function handle4() {}
publuc function handle5() {}
publuc function handle6() {}
}
function main ()
{
$awesome = new AwesomeClass();
$awesome->handle1();
$awesome->handle2();
$awesome->handle3();
$awesome->handle4();
$awesome->handle5();
$awesome->handle6();
}
責任鏈的使用方式
-
單一功能原則 (SRP)
-
多個物件處理同一個請求
-
請求者和發送者解耦合
- 請假的簽合流程(大話設計模式中的例子)
- 多個 Log 處理器在處理 Log (wiki 的例子)
- Middleware (最常見的例子)
責任鏈的缺點
- 一個請求可能到了鏈的尾端都沒有被處理
- 鏈的順序性可能會造成結果不同
- 請求的資料可能會在鏈中修改後導致鏈尾取不到資料
Conditionable
鏈式語法的小工具
什麼是鏈式語法
DB::query()
->from('user')
->select('created_at', '<', Carbon::now())
->select('is_active', true)
->get();
你是否寫過以下的 code?
$query = User::query();
if ($request->has('account')) {
$query->where('account', $request->input('account'));
}
if ($request->has('created_at')) {
$query->where('created_at', >, $request->input('created_at'));
}
if ($request->has('is_active')) {
$query->where('is_active', $request->input('is_active'));
}
$query->get();
是否覺得這個 if 阻斷了鏈式
在你的 class 裡加點 Conditional
trait BuildsQueries
{
use Conditionable;
)
就可以用鏈式統一天下
User::query()
->when($request->has('account'), function($query) {
$query->where('account', $request->input('account'));
})
->when($request->has('created_at'), function($query) {
$query->where('created_at', >, $request->input('created_at'));
})
->when($request->has('is_active'), function($query) {
$query->where('is_active', $request->input('is_active'));
})
->get();
是不是覺得很好用
讓我們看看他裡面長什麼樣
public function when($value = null, callable $callback = null, callable $default = null)
{
$value = $value instanceof Closure ? $value($this) : $value;
if ($value) {
return $callback($this, $value) ?? $this;
} elseif ($default) {
return $default($this, $value) ?? $this;
}
return $this;
}
這種雖不明但覺厲的鏈式小工具
其實在 codebase 還有一處
Tappable
Tappable
trait Tappable
{
/**
* Call the given Closure with this instance then return the instance.
*
* @param callable|null $callback
* @return $this|\Illuminate\Support\HigherOrderTapProxy
*/
public function tap($callback = null)
{
return tap($this, $callback);
}
}
class AwesomeChain
{
use Tappable;
}
function main ()
{
(new AwesomeChain())->tap(fn() => print('awesome'));
}
我剛剛還省略了一個東西
public function when($value = null, callable $callback = null, callable $default = null)
{
$value = $value instanceof Closure ? $value($this) : $value;
if (func_num_args() === 0) {
return new HigherOrderWhenProxy($this);
}
if (func_num_args() === 1) {
return (new HigherOrderWhenProxy($this))->condition($value);
}
// ...
}
HigherOrderWhenProxy
HigherOrderWhenProxy
public function condition($condition)
{
[$this->condition, $this->hasCondition] = [$condition, true];
return $this;
}
public function __get($key)
{
if (! $this->hasCondition) {
$condition = $this->target->{$key};
return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition);
}
return $this->condition
? $this->target->{$key}
: $this->target;
}
public function __call($method, $parameters)
{
if (! $this->hasCondition) {
$condition = $this->target->{$method}(...$parameters);
return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition);
}
return $this->condition
? $this->target->{$method}(...$parameters)
: $this->target;
}
你還可以這樣寫
User::query()
->when()
->condition($request->has('account'))
->where('account', $request->input('account'))
->get();
或這樣寫
User::query()
->when($request->has('account'))
->where('account', $request->input('account'))
->get();
既然提到了HigherOrderWhenProxy
順便來說說「裝飾模式」
動態為類別添加額外職責的模式
PHP Magic Function 應用下的裝飾模式
HigherOrderProxy
扯遠了,讓我們回到用鏈式統一天下的事情
User::query()
->when($request->has('account'), function($query) {
$query->where('account', $request->input('account'));
})
->when($request->has('created_at'), function($query) {
$query->where('created_at', >, $request->input('created_at'));
})
->when($request->has('is_active'), function($query) {
$query->where('is_active', $request->input('is_active'));
})
->get();
是不是覺得我的 code 還是又臭又長呢?
好用的工具
不在 Laravel 裡
沒有工商
use EloquentFilter\ModelFilter;
class UserFilter extends ModelFilter
{
public function account($account)
{
return $this->where('account', $account);
}
public function createdAt($createdAt)
{
return $this->where('created_at', '>', $createdAt);
}
public function isActive($isActive)
{
return $this->where('is_active', $isActive);
}
}
User::query()
->filter($request->all(), UserFilter::class)
->get();
是不是覺得程式碼變得簡潔了呢owo
因為我們把又臭又長的地方移走了
因為沒有工商
在此不探討 EloquentFilter 的程式碼
結尾放隻貓
探索 Laravel 源碼 發現好用的小工具
By 球魚
探索 Laravel 源碼 發現好用的小工具
- 1,144