探索 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,003