HomeOur Team
Tìm hiểu Facade Design Pattern trong Laravel

Tìm hiểu Facade Design Pattern trong Laravel

By chung.nguyen1
Published in Solutions
December 21, 2022
3 min read
  • Giới thiệu
  • Tìm hiểu về Facade trong Laravel
  • Tạo custom facade

Giới thiệu

Xin chào các bạn, hôm nay chúng ta sẽ cùng tìm hiểu về Facade Design Pattern qua bài viết này để các bạn có thể hiểu hơn về Facade và có thể ứng dụng nó vào các dự án của mình nhé.

Tìm hiểu về Facade trong Laravel

Điều đầu tiên các bạn cần là cài đặt dự án Laravel, sau khi cài đặt xong thì các bạn vào file routes/web.php và chạy thử ví dụ sau:

cache()->set('name', 'Tutorial');
dd(cache()->get('name'));

Tiếp theo chúng ta chạy lệnh php artisan serve ở thư mục gốc của dự án và truy cập đường dẫn http://localhost:8000/ để xem kết quả.

1_set_cache_name

Ở ví dụ trên ta thấy rằng setget là các hàm non-static, và chúng ta đến với ví dụ thứ 2:

use Illuminate\Support\Facades\Cache;
Cache::set('name', 'Tutorial');
dd(Cache::get('name'));

Sau khi chúng ta load lại đường dẫn http://localhost:8000/ và kết quả giống như ví dụ 1, nhưng trong ví dụ này hàm setget là static function. Chúng ta cùng vào Cached Repository (vendor/laravel/framework/src/Illuminate/Cache/Repository.php) để xem hàm setget được viết như thế nào:

<?php
// vendor/laravel/framework/src/Illuminate/Cache/Repository.php
namespace Illuminate\Cache;
use ArrayAccess;
use BadMethodCallException;
use Closure;
use DateTimeInterface;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Contracts\Cache\Repository as CacheContract;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Traits\Macroable;
/**
* @mixin \Illuminate\Contracts\Cache\Store
*/
class Repository implements ArrayAccess, CacheContract
{
use InteractsWithTime;
use Macroable {
__call as macroCall;
}
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Store
*/
protected $store;
/**
* The event dispatcher implementation.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* The default number of seconds to store items.
*
* @var int|null
*/
protected $default = 3600;
/**
* Create a new cache repository instance.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @return void
*/
public function __construct(Store $store)
{
$this->store = $store;
}
/**
* Determine if an item exists in the cache.
*
* @param string $key
* @return bool
*/
public function has($key): bool
{
return ! is_null($this->get($key));
}
/**
* Determine if an item doesn't exist in the cache.
*
* @param string $key
* @return bool
*/
public function missing($key)
{
return ! $this->has($key);
}
/**
* Retrieve an item from the cache by key.
*
* @param array|string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null): mixed
{
if (is_array($key)) {
return $this->many($key);
}
$value = $this->store->get($this->itemKey($key));
// If we could not find the cache value, we will fire the missed event and get
// the default value for this cache value. This default could be a callback
// so we will execute the value function which will resolve it if needed.
if (is_null($value)) {
$this->event(new CacheMissed($key));
$value = value($default);
} else {
$this->event(new CacheHit($key, $value));
}
return $value;
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @param array $keys
* @return array
*/
public function many(array $keys)
{
$values = $this->store->many(collect($keys)->map(function ($value, $key) {
return is_string($key) ? $key : $value;
})->values()->all());
return collect($values)->map(function ($value, $key) use ($keys) {
return $this->handleManyResult($keys, $key, $value);
})->all();
}
/**
* {@inheritdoc}
*
* @return iterable
*/
public function getMultiple($keys, $default = null): iterable
{
$defaults = [];
foreach ($keys as $key) {
$defaults[$key] = $default;
}
return $this->many($defaults);
}
/**
* Handle a result for the "many" method.
*
* @param array $keys
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function handleManyResult($keys, $key, $value)
{
// If we could not find the cache value, we will fire the missed event and get
// the default value for this cache value. This default could be a callback
// so we will execute the value function which will resolve it if needed.
if (is_null($value)) {
$this->event(new CacheMissed($key));
return isset($keys[$key]) ? value($keys[$key]) : null;
}
// If we found a valid value we will fire the "hit" event and return the value
// back from this function. The "hit" event gives developers an opportunity
// to listen for every possible cache "hit" throughout this applications.
$this->event(new CacheHit($key, $value));
return $value;
}
/**
* Retrieve an item from the cache and delete it.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function pull($key, $default = null)
{
return tap($this->get($key, $default), function () use ($key) {
$this->forget($key);
});
}
/**
* Store an item in the cache.
*
* @param array|string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function put($key, $value, $ttl = null)
{
if (is_array($key)) {
return $this->putMany($key, $value);
}
if ($ttl === null) {
return $this->forever($key, $value);
}
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return $this->forget($key);
}
$result = $this->store->put($this->itemKey($key), $value, $seconds);
if ($result) {
$this->event(new KeyWritten($key, $value, $seconds));
}
return $result;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function set($key, $value, $ttl = null): bool
{
return $this->put($key, $value, $ttl);
}
/**
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function putMany(array $values, $ttl = null)
{
if ($ttl === null) {
return $this->putManyForever($values);
}
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return $this->deleteMultiple(array_keys($values));
}
$result = $this->store->putMany($values, $seconds);
if ($result) {
foreach ($values as $key => $value) {
$this->event(new KeyWritten($key, $value, $seconds));
}
}
return $result;
}
/**
* Store multiple items in the cache indefinitely.
*
* @param array $values
* @return bool
*/
protected function putManyForever(array $values)
{
$result = true;
foreach ($values as $key => $value) {
if (! $this->forever($key, $value)) {
$result = false;
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function setMultiple($values, $ttl = null): bool
{
return $this->putMany(is_array($values) ? $values : iterator_to_array($values), $ttl);
}
/**
* Store an item in the cache if the key does not exist.
*
* @param string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
public function add($key, $value, $ttl = null)
{
$seconds = null;
if ($ttl !== null) {
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return false;
}
// If the store has an "add" method we will call the method on the store so it
// has a chance to override this logic. Some drivers better support the way
// this operation should work with a total "atomic" implementation of it.
if (method_exists($this->store, 'add')) {
return $this->store->add(
$this->itemKey($key), $value, $seconds
);
}
}
// If the value did not exist in the cache, we will put the value in the cache
// so it exists for subsequent requests. Then, we will return true so it is
// easy to know if the value gets added. Otherwise, we will return false.
if (is_null($this->get($key))) {
return $this->put($key, $value, $seconds);
}
return false;
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
return $this->store->increment($key, $value);
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function decrement($key, $value = 1)
{
return $this->store->decrement($key, $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
$result = $this->store->forever($this->itemKey($key), $value);
if ($result) {
$this->event(new KeyWritten($key, $value));
}
return $result;
}
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key
* @param \Closure|\DateTimeInterface|\DateInterval|int|null $ttl
* @param \Closure $callback
* @return mixed
*/
public function remember($key, $ttl, Closure $callback)
{
$value = $this->get($key);
// If the item exists in the cache we will just return this immediately and if
// not we will execute the given Closure and cache the result of that for a
// given number of seconds so it's available for all subsequent requests.
if (! is_null($value)) {
return $value;
}
$this->put($key, $value = $callback(), value($ttl));
return $value;
}
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @param string $key
* @param \Closure $callback
* @return mixed
*/
public function sear($key, Closure $callback)
{
return $this->rememberForever($key, $callback);
}
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @param string $key
* @param \Closure $callback
* @return mixed
*/
public function rememberForever($key, Closure $callback)
{
$value = $this->get($key);
// If the item exists in the cache we will just return this immediately
// and if not we will execute the given Closure and cache the result
// of that forever so it is available for all subsequent requests.
if (! is_null($value)) {
return $value;
}
$this->forever($key, $value = $callback());
return $value;
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
return tap($this->store->forget($this->itemKey($key)), function ($result) use ($key) {
if ($result) {
$this->event(new KeyForgotten($key));
}
});
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function delete($key): bool
{
return $this->forget($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteMultiple($keys): bool
{
$result = true;
foreach ($keys as $key) {
if (! $this->forget($key)) {
$result = false;
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(): bool
{
return $this->store->flush();
}
/**
* Begin executing a new tags operation if the store supports it.
*
* @param array|mixed $names
* @return \Illuminate\Cache\TaggedCache
*
* @throws \BadMethodCallException
*/
public function tags($names)
{
if (! $this->supportsTags()) {
throw new BadMethodCallException('This cache store does not support tagging.');
}
$cache = $this->store->tags(is_array($names) ? $names : func_get_args());
if (! is_null($this->events)) {
$cache->setEventDispatcher($this->events);
}
return $cache->setDefaultCacheTime($this->default);
}
/**
* Format the key for a cache item.
*
* @param string $key
* @return string
*/
protected function itemKey($key)
{
return $key;
}
/**
* Calculate the number of seconds for the given TTL.
*
* @param \DateTimeInterface|\DateInterval|int $ttl
* @return int
*/
protected function getSeconds($ttl)
{
$duration = $this->parseDateInterval($ttl);
if ($duration instanceof DateTimeInterface) {
$duration = Carbon::now()->diffInRealSeconds($duration, false);
}
return (int) ($duration > 0 ? $duration : 0);
}
/**
* Determine if the current store supports tags.
*
* @return bool
*/
public function supportsTags()
{
return method_exists($this->store, 'tags');
}
/**
* Get the default cache time.
*
* @return int|null
*/
public function getDefaultCacheTime()
{
return $this->default;
}
/**
* Set the default cache time in seconds.
*
* @param int|null $seconds
* @return $this
*/
public function setDefaultCacheTime($seconds)
{
$this->default = $seconds;
return $this;
}
/**
* Get the cache store implementation.
*
* @return \Illuminate\Contracts\Cache\Store
*/
public function getStore()
{
return $this->store;
}
/**
* Fire an event for this cache instance.
*
* @param object|string $event
* @return void
*/
protected function event($event)
{
$this->events?->dispatch($event);
}
/**
* Get the event dispatcher instance.
*
* @return \Illuminate\Contracts\Events\Dispatcher
*/
public function getEventDispatcher()
{
return $this->events;
}
/**
* Set the event dispatcher instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function setEventDispatcher(Dispatcher $events)
{
$this->events = $events;
}
/**
* Determine if a cached value exists.
*
* @param string $key
* @return bool
*/
public function offsetExists($key): bool
{
return $this->has($key);
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key): mixed
{
return $this->get($key);
}
/**
* Store an item in the cache for the default time.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function offsetSet($key, $value): void
{
$this->put($key, $value, $this->default);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return void
*/
public function offsetUnset($key): void
{
$this->forget($key);
}
/**
* Handle dynamic calls into macros or pass missing methods to the store.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->store->$method(...$parameters);
}
/**
* Clone cache repository instance.
*
* @return void
*/
public function __clone()
{
$this->store = clone $this->store;
}
}

Và bạn có thể thấy hàm set là hàm non-static, tiếp theo bạn tìm hàm get và cũng thấy nó là hàm non-static. Vậy thì thực tế hàm Cache::set('name', 'Tutorial');Cache::get('name') được gọi như thế nào, chúng ta cùng tìm hiểu bằng cách giữ phím ctrl và click vào hàm Cache ở đoạn code Cache::set('name', 'Tutorial'); hoặc Cache::get('name').

Sau khi click vào hàm Cache thì chúng ta đến file vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php:

<?php
// vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php
namespace Illuminate\Support\Facades;
/**
* @method static \Illuminate\Cache\TaggedCache tags(array|mixed $names)
* @method static \Illuminate\Contracts\Cache\Lock lock(string $name, int $seconds = 0, mixed $owner = null)
* @method static \Illuminate\Contracts\Cache\Lock restoreLock(string $name, string $owner)
* @method static \Illuminate\Contracts\Cache\Repository store(string|null $name = null)
* @method static \Illuminate\Contracts\Cache\Store getStore()
* @method static bool add(string $key, $value, \DateTimeInterface|\DateInterval|int $ttl = null)
* @method static bool flush()
* @method static bool forever(string $key, $value)
* @method static bool forget(string $key)
* @method static bool has(string $key)
* @method static bool missing(string $key)
* @method static bool put(array|string $key, $value, \DateTimeInterface|\DateInterval|int $ttl = null)
* @method static int|bool decrement(string $key, $value = 1)
* @method static int|bool increment(string $key, $value = 1)
* @method static mixed get(array|string $key, mixed $default = null)
* @method static mixed pull(string $key, mixed $default = null)
* @method static mixed remember(string $key, \DateTimeInterface|\DateInterval|int $ttl, \Closure $callback)
* @method static mixed rememberForever(string $key, \Closure $callback)
* @method static mixed sear(string $key, \Closure $callback)
*
* @see \Illuminate\Cache\CacheManager
* @see \Illuminate\Cache\Repository
*/
class Cache extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}

Ở đây chúng ta thấy class Cache là một class đơn giản extends class Facade và có 1 static function với tên getFacadeAccessor, function này trả về facade name.

Vậy chúng ta sử dụng facade để làm gì? Bởi vì việc sử dụng facade dễ dàng và code nhìn dễ hiểu. Khi sử dụng facade bạn không phải quan tâm lớp đó có khởi tạo những lớp nào khác không.

Tiếp theo chúng ta sẽ xây dựng facade riêng của mình.

Tạo custom facade

Chúng ta sẽ tạo một class có tên là Duck và class Duck này có method gọi là swim, trong method swim chúng ta trả về text swimming. Tương tự chúng ta tạo 1 method khác có tên là eat và trả về text eating.

<?php
// routes/web.php
class Duck {
public function swim()
{
return 'swimming';
}
public function eat()
{
return 'eating';
}
}

Thông thường để gọi method swim hoặc method eat chúng ta sẽ khởi tạo 1 object từ class Duck và gọi đến method swim hoặc method eat như sau:

<?php
// routes/web.php
...
$duck = new Duck;
dd($duck->swim(), $duck->eat());

Bởi vì class Duck này không có bất kỳ phụ thuộc nào nên nó sẽ đơn giản để sử dụng, nhưng khi chúng ta có 1 vài phụ thuộc như:

<?php
// routes/web.php
...
$duck = new Duck(new Other(new SomeOther));

Lúc này class Duck sẽ rất khó để sử dụng, vậy chúng ta sẽ xủ lý vấn đề này bằng cách nào?. Chúng ta sẽ bind duck với class Duck:

<?php
// routes/web.php
...
app()->bind('duck', function() {
return new Duck;
});

Tiếp theo chúng ta sẽ tạo facade của mình với tên DuckFacade, bên trong class DuckFacade chúng ta sẽ sử dụng 1 magic function có tên là __callStatic. Method __callStatic nhận vào 2 arguments là namearguments, bên trong hàm chúng ta sẽ dump die biến name:

<?php
// routes/web.php
...
class DuckFacade {
public static function __callStatic($name, $arguments)
{
dd($name);
}
}

Và cuối cùng chúng ta sẽ gọi hàm __callStatic trong class DuckFacade:

<?php
// routes/web.php
...
DuckFacade::anyRandomFunction();

Giờ chúng ta mở đường dẫn http://localhost:8000/ lên và xem kết quả

anyRandomFunction

Chúng ta thấy kết quả trả về anyRandomFunction, nhưng làm sao kết quả lại trả về như vậy, bởi vì chúng ta sử dụng magic function có tên __callStatic, function này sẽ được gọi mỗi khi bạn call bất kỳ static function nào và biến $name sẽ là tên của static function mà bạn gọi đến.

Tiếp theo ta thử đổi tên hàm anyRandomFunction thành swim và refresh lại đường dẫn http://localhost:8000/ và xem kết quả

swimFunction

Chúng ta thấy kết quả trả về swim, vậy chúng ta sẽ tìm cách để truy cập đến method swim bên trong class Duck. Để làm việc này, chúng ta cần chỉnh sửa method __callStatic bên trong class DuckFacade thành return app()->make('duck')->$name() và file routes/web.php sẽ trông như sau:

<?php
// routes/web.php
class Duck {
public function swim()
{
return 'swimming';
}
public function eat()
{
return 'eating';
}
}
app()->bind('duck', function() {
return new Duck;
});
class DuckFacade {
public static function __callStatic($name, $arguments)
{
return app()->make('duck')->$name();
}
}
dd(DuckFacade::swim());

Giờ chúng ta thử refresh lại đường dẫn http://localhost:8000/ và xem kết quả.

swimming

Ta thấy đường dẫn hiển thị swimming, vậy là chúng ta đã truy xuất được vào method swim bên trong class Duck. Chúng ta sẽ nhìn lại method __callStatic xem có ý nghĩa gì. Đầu tiên là hàm app()->make(class_name), hàm make sẽ khởi tạo 1 instance của class tương ứng với tham số class_name mà mình truyền vào. Trong ví dụ app()->make('duck) sẽ khởi tạo 1 instance của class Duck, và cuối cùng là truy xuất đến method bên trong class ->$name().

Giờ chúng ta sẽ xây dựng facade class giống của laravel như sau:

// vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php
class Cache extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}

Chúng ta sẽ tạo thêm 1 class nữa có tên Car, class Car có 1 method tên drive và method drive return driving:

<?php
// routes/web.php
...
class Car {
public function drive()
{
return 'driving';
}
}

Tiếp theo chúng ta sẽ binding class này và tạo thêm class CarFacade cho class Car:

<?php
// routes/web.php
...
app()->bind('car', function() {
return new Car;
});
class CarFacade {
public static function __callStatic($name, $arguments)
{
return app()->make('car')->$name();
}
}

Giờ chúng ta sẽ tạo class Facade và chuyển method __callStatic của class CarFacade tới class Facade:

<?php
// routes/web.php
...
class Facade {
public static function __callStatic($name, $arguments)
{
return app()->make('car')->$name();
}
}

Cả 2 class DuckFacadeCarFacade sẽ extends class Facade, ngoài ra chúng ta sẽ thêm vào 2 class đó 1 method có tên getFacadeAccessor:

<?php
// routes/web.php
...
class DuckFacade extends Facade {
protected static function getFacadeAccessor()
{
return 'duck';
}
}
class CarFacade extends Facade {
protected static function getFacadeAccessor()
{
return 'car';
}
}

Cả 2 class DuckFacadeCarFacade giờ đã nhìn giống như class vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php của laravel. Quay trở lại với class Facade thì thay vì hard-code trả về car trong method __callStatic thì chúng ta sẽ dynamic method __callStatic như sau:

<?php
// routes/web.php
...
class Facade {
public static function __callStatic($name, $arguments)
{
return app()->make(static::getFacadeAccessor())->$name();
}
}

File routes/web.php cuối cùng chúng ta sẽ trông thế này:

<?php
class Duck
{
public function swim()
{
return 'swimming';
}
public function eat()
{
return 'eating';
}
}
class Car {
public function drive()
{
return 'driving';
}
}
app()->bind('duck', function () {
return new Duck;
});
app()->bind('car', function() {
return new Car;
});
class Facade {
public static function __callStatic($name, $arguments)
{
return app()->make(static::getFacadeAccessor())->$name();
}
}
class DuckFacade extends Facade {
protected static function getFacadeAccessor()
{
return 'duck';
}
}
class CarFacade extends Facade {
protected static function getFacadeAccessor()
{
return 'car';
}
}
dd(DuckFacade::swim());

Các bạn có thể xem code tham khảo tại đây


Tags

laravelfacadedesign pattern

Share

chung.nguyen1

chung.nguyen1

Developer

Expertise

Related Posts

Xây dựng ứng dụng Authentication sử dụng Laravel & Nextjs
Solutions
Xây dựng ứng dụng Authentication sử dụng Laravel & Nextjs
October 20, 2022
4 min
© 2023, All Rights Reserved.
Powered By

Quick Links

HomeOur Team

Social Media