Mastering Laravel Package Development: From Composer Init to Custom Attributes
Overview: The Power of Custom Laravel Packages
Building a #[Cachable] PHP attribute that automatically handles method results through the Cache::remember blocks with clean, declarative metadata.
Prerequisites
To follow this guide, you should have a firm grasp of
Key Libraries & Tools
- Composer: The backbone of PHP dependency management. Every Laravel package is, at its core, a Composer package.
- Orchestra Testbench: An indispensable tool that simulates a fullLaravelenvironment for testing packages without needing a standalone app.
- Pest PHP: A focus-driven testing framework used here for its expressive syntax.
- Laravel Pint: An opinionated PHP code style fixer that ensures your package followsLaravelstandards.
- Spatie Package Skeleton: 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 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
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 allowsLaravelto 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 toGitHuborPackagist. - 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.
