Lesser Known Design Patterns

There are numerous lesser-known patterns that can significantly enhance your system design. This blog post will delve into some of them.

Pipeline Pattern

The Laravel Pipeline pattern allows for a sequence of tasks or processes to be passed through a series of filters or pipes, each with a single responsibility, enhancing flexibility, testability, and extensibility.

interface PipeInterface
{
	public function handle($passable, Closure $next);
}

class VerifyUserAge implements PipeInterface
{
	public function handle($user, Closure $next)
	{
		// Skip this pipe if user is admin
		if ($user->isAdmin()) {
			return $next($user);
		}

		if ($user->age < 18) {
			throw new \Exception("User is not old enough");
		}
		return $next($user);
	}
}

class ValidateUserEmail implements PipeInterface
{
	public function handle($user, Closure $next)
	{
		if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
			throw new \Exception("Invalid email format");
		}
		return $next($user);
	}
}

class NormalizeUserName implements PipeInterface
{
	public function handle($user, Closure $next)
	{
		$user->name = trim(ucwords(strtolower($user->name)));
		return $next($user);
	}
}

class ProcessByType implements PipeInterface
{
	public function __construct(private string $type) {}

	public function handle($data, Closure $next)
	{
		match ($this->type) {
			'premium' => $this->processPremiumUser($data),
			'standard' => $this->processStandardUser($data),
			default => null
		};

		return $next($data);
	}
}

// User registration pipeline
class UserRegistrationPipeline
{
	public function process($user)
	{
		$pipes = [
			VerifyUserAge::class,
			ValidateUserEmail::class,
			NormalizeUserName::class,
			new ProcessByType('premium'),
		];

		return app(\Illuminate\Pipeline\Pipeline::class)
			->send($user)
			->through($pipes)
			->thenReturn();
	}
}

// Usage example
class UserController
{
	protected $pipeline;

	public function __construct(UserRegistrationPipeline $pipeline)
	{
		$this->pipeline = $pipeline;
	}

	public function register(Request $request)
	{
		try {
			$user = $this->pipeline->process(new User($request->all()));
			// Save user if all pipes pass
			$user->save();
			return response()->json(['message' => 'User registered successfully']);
		} catch (\Exception $e) {
			return response()->json(['error' => $e->getMessage()], 400);
		}
	}
}

Tap pattern

The tap function takes two arguments: the first is the value to be passed, and the second is a closure. This closure receives the value, performs some operations on it, and then returns it.

// instead of doing this
$article = Article::findOrFail($id);
$article->incrementViews();
$article->doSomething();

return $article;


// we can keep related functionality to one block of code
return tap(Article::findOrFail($id), function (Article $article) {
	$article->incrementViews();
	$article->doSomething();
});

The update method typically returns a boolean value. However, by using the tap function, the update method instead returns the User model that was tapped. This functionality of tap makes it simple to ensure any method on an object returns the object itself.

return tap($user)->update(['email' => $email]);

The anatomy of Laravel’s tap() function

Cache Refresh Ahead Pattern

This pattern ensures that the cache is updated before it expires, guaranteeing that users receive fresh data at all times. If the data is frequently accessed, it will always be cached.

function cacheWithRefreshAhead(string $key, int $ttl, float $refreshAheadTime, \Closure $callback): mixed
{
	$cachedData = Cache::get($key);

	if ($cachedData) {
		$expiresAt = Cache::get("$key:expires_at");

		if ($expiresAt && Carbon::now()->diffInMinutes($expiresAt) <= $ttl * $refreshAheadTime) {
			// dispatch a Job to update cache:
			Cache::put($key, $callback(), $ttl);
			Cache::put("$key:expires_at", Carbon::now()->addMinutes($ttl), $ttl);
		}

		return $cachedData;
	}

	// Cache miss, so cache it
	$data = $callback();
	// dispatch a Job to update cache:
	Cache::put($key, $data, $ttl);
	Cache::put("$key:expires_at", Carbon::now()->addMinutes($ttl), $ttl);

	return $data;
}

// Usage example 
$data = cacheWithRefreshAhead('some-key', 30, 0.7, function () {
	// Retrieve fresh data
	return ['data' => 123];
});

This article was updated on December 10, 2024