Mastering Laravel Package Development: From Composer Init to Custom Attributes
Overview: The Power of Custom Laravel Packages
Building a package allows you to encapsulate reusable logic, share it across multiple projects, or contribute to the vibrant open-source ecosystem. This tutorial demonstrates how to build a sophisticated caching package named . Unlike a basic "Hello World," this project implements a #[Cachable] PHP attribute that automatically handles method results through the system. This technique reduces boilerplate code and improves readability by replacing messy Cache::remember blocks with clean, declarative metadata.
Prerequisites
To follow this guide, you should have a firm grasp of or higher, as we utilize modern features like attributes and constructor property promotion. Familiarity with the service container is essential, as package development relies heavily on binding and resolving dependencies. You should also be comfortable with for dependency management and have a local development environment prepared.
Key Libraries & Tools
- : The backbone of PHP dependency management. Every Laravel package is, at its core, a Composer package.
- : An indispensable tool that simulates a full environment for testing packages without needing a standalone app.
- : A focus-driven testing framework used here for its expressive syntax.
- : An opinionated PHP code style fixer that ensures your package follows standards.
- : A popular boilerplate for jump-starting package development (though we build a minimal version here).
Code Walkthrough: Building the Foundation
1. Initializing the Package
Begin by creating a directory and initializing the composer.json file. This defines your namespace and dependencies.
{
"name": "yourname/method-cache",
"autoload": {
"psr-4": {
"YourName\\MethodCache\\": "src/"
}
},
"require-dev": {
"orchestra/testbench": "^9.0",
"pestphp/pest": "^2.0"
}
}
2. Creating the Service Provider
The Service Provider is the entry point of your package. It registers bindings in the container and handles auto-discovery. We use the beforeResolving hook to intercept classes and inject our caching logic.
namespace YourName\MethodCache;
use Illuminate\Support\ServiceProvider;
class MethodCacheServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->beforeResolving(function ($abstract, $app) {
if (!class_exists($abstract)) return;
if (in_array(HasCachableMethods::class, class_implements($abstract))) {
// Logic to override the class with a cached proxy
}
});
if ($this->app->runningInConsole()) {
$this->commands([MethodCacheCommand::class]);
}
}
}
3. Implementing the Cachable Attribute
Attributes provide metadata for our methods. We define a target of Attribute::TARGET_METHOD to ensure it's used correctly.
#[Attribute(Attribute::TARGET_METHOD)]
class Cachable
{
public function __construct(
public int $ttl = 3600,
public ?string $key = null
) {}
}
4. Crafting the Facade
A Facade provides a static interface to our underlying service. First, create the service class, then the Facade that references it.
// The Facade
namespace YourName\MethodCache\Facades;
use Illuminate\Support\Facades\Facade;
class MethodCache extends Facade
{
protected static function getFacadeAccessor()
{
return 'method-cache-service';
}
}
Syntax Notes: Reflection and Proxies
This package uses PHP Reflection to inspect classes at runtime. By looking for the #[Cachable] attribute on methods, the package can dynamically decide which logic to wrap in a cache layer. A key technique used here is Method Overriding, where we generate a temporary class that extends the user's service and wraps the original method calls in Cache::remember(). This avoids the "noise" of manual caching inside the business logic.
Practical Examples
Imagine a PodcastService that performs heavy data analysis. Instead of polluting the method with caching logic, you simply tag it:
class PodcastService implements HasCachableMethods
{
#[Cachable(ttl: 60, key: 'podcast_stats')]
public function getStatistics(int $id)
{
// Heavy database or API work here
return Podcast::analyze($id);
}
}
When this service is resolved from the container, the package automatically ensures that subsequent calls within 60 seconds return the cached result instantly.
Tips & Gotchas
- Interface Requirement: To optimize performance, we only inspect classes that implement a specific interface (e.g.,
HasCachableMethods). This prevents the package from running reflection on every single class resolved by the container, which would cause significant overhead. - Auto-Discovery: Don't forget to add the
extrasection to your package'scomposer.json. This allows to automatically register your service provider and facades upon installation. - Local Testing: Use a
pathrepository in your main app'scomposer.jsonto symlink your package locally. This allows you to see changes in real-time without pushing to or . - Recursion Warning: Be careful when using
beforeResolving. If your logic inside that hook triggers another resolution of the same class, you'll end up in an infinite loop. Always add checks to ensure you're only processing the target classes once.
- 39%· products
- 11%· products
- 6%· companies
- 6%· products
- 6%· products
- Other topics
- 33%

How To Build a Laravel Package 📦
WatchLaravel // 39:50
The official YouTube channel of Laravel, the clean stack for Artisans and agents. We will update you on what's new in the world of Laravel, from the framework to our products Cloud, Forge, and Nightwatch.