Installation:
composer require moox/expiry
php artisan mooxexpiry:install
Define Expirable Models:
Add HasExpiry trait to your Eloquent models:
use Moox\Expiry\Traits\HasExpiry;
class User extends Model
{
use HasExpiry;
}
Set Expiry Fields: Configure expiry fields in your model:
protected $expiryFields = [
'expires_at' => '2025-12-31', // Carbon instance or string
];
Run Expiry Checks:
Use the Expiry facade to check or clean expired records:
use Moox\Expiry\Facades\Expiry;
// Check if a model is expired
$isExpired = Expiry::isExpired($user);
// Clean expired records (e.g., in a cron job)
Expiry::cleanExpired(User::class);
Verify Migrations:
Check the published migration (database/migrations/[timestamp]_create_expiry_logs_table.php) for the expiry_logs table, which tracks expired records.
Extend HasExpiry to trigger soft deletes:
use Moox\Expiry\Traits\HasExpiry;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use HasExpiry, SoftDeletes;
protected $expiryFields = ['expires_at' => now()->addDays(30)];
protected static function bootHasExpiry()
{
static::saved(function ($model) {
if ($model->isExpired()) {
$model->delete();
}
});
}
}
Schedule Cleanup:
Add a cron job in app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('expiry:clean')->daily();
}
Model Integration:
HasExpiry to models needing expiry tracking.$expiryFields (supports Carbon instances, timestamps, or static dates).isExpired() or handleExpiry() for bespoke behavior.Expiry Management:
// Check expiry status
Expiry::isExpired($model);
// Clean expired records (with optional query builder)
Expiry::cleanExpired(User::class, function ($query) {
return $query->where('role', 'trial');
});
// Log expiry events
Expiry::logExpiry($model, 'manual_cleanup');
// Filter expired models
$expiredUsers = User::expired()->get();
Logging and Auditing:
expiry_logs table stores:
model_type: Fully qualified class name.model_id: Primary key of the expired record.expired_at: Timestamp of expiry.reason: Optional custom reason (e.g., 'subscription_ended').$logs = ExpiryLog::where('model_type', User::class)->get();
Bulk Operations:
Expiry::cleanExpired() with query constraints to target specific subsets:
Expiry::cleanExpired(Product::class, function ($query) {
return $query->where('stock', '<', 10);
});
Event-Driven Expiry: Listen for expiry events to trigger actions (e.g., send notifications):
// In EventServiceProvider
public function boot()
{
Expiry::onExpiry(function ($model) {
Notification::send($model, new ExpiryNotification());
});
}
API Endpoints: Expose expiry status in API responses:
Route::get('/users/{user}/expiry', function (User $user) {
return response()->json([
'is_expired' => Expiry::isExpired($user),
'expires_at' => $user->expires_at,
]);
});
Admin Panels: Display expiry logs in admin interfaces (e.g., Laravel Nova):
// Nova Resource
public static $expiryLogs = [
'attribute' => 'expiry_logs',
'label' => 'Expiry History',
];
Testing: Mock expiry checks in tests:
$user = User::factory()->create(['expires_at' => now()->subDay()]);
$this->assertTrue(Expiry::isExpired($user));
Migration Conflicts:
mooxexpiry:install, ensure the expiry_logs table exists. The installer may skip migrations if they’re already up-to-date.php artisan migrate:fresh if migrations fail.Time Zone Issues:
config/app.php or models:
protected $dateFormat = 'Y-m-d H:i:s';
protected $connection->getDoctrineSchemaManager()->getDatabasePlatform()->getDateTimeType()->getName();
Circular Dependencies:
isExpired() or cleanExpired() recursively (e.g., in model observers or boot methods). This can cause infinite loops.protected static $isCheckingExpiry = false;
public function isExpired()
{
if (self::$isCheckingExpiry) return false;
self::$isCheckingExpiry = true;
try {
return parent::isExpired();
} finally {
self::$isCheckingExpiry = false;
}
}
Performance with Large Datasets:
cleanExpired() can be slow for tables with millions of records. Use chunking:
Expiry::cleanExpired(User::class)->chunk(1000);
Soft Deletes vs. Expiry:
SoftDeletes, ensure deleted_at is not null when checking expiry. Override isExpired() to skip soft-deleted models:
public function isExpired()
{
return !$this->deleted_at && parent::isExpired();
}
Log Expiry Events:
Enable logging in config/expiry.php:
'log_expiry_events' => env('EXPIRY_LOG_EVENTS', true),
Check storage/logs/laravel.log for expiry-related entries.
Verify Expiry Fields:
Ensure $expiryFields contains valid Carbon-compatible values. Use:
$this->assertInstanceOf(Carbon::class, $model->expires_at);
Check for Stale Logs:
The expiry_logs table may grow large. Add a cleanup job:
// In a scheduled command
ExpiryLog::where('created_at', '<', now()->subYears(1))->delete();
Custom Expiry Logic:
Override the handleExpiry() method to add logic (e.g., send emails, archive data):
protected function handleExpiry()
{
$this->archive();
Mail::to($this->email)->send(new ExpiryNotice());
}
Dynamic Expiry Fields: Use accessors to compute expiry dynamically:
public function getExpiresAtAttribute()
{
return now()->addDays($this->trial_days);
}
Expiry Conditions:
Add custom conditions to isExpired():
public function isExpired()
{
return parent::isExpired() && $this->status === 'active';
}
Expiry Notifications:
Extend the Expiry facade to add custom notification logic:
// In a service provider
Expiry::extend(function ($app) {
$app->bind('expiry.notifier', function () {
return new SlackNotifier();
});
});
Bulk Expiry Actions: Create a custom command for bulk expiry operations:
php artisan expiry:bulk-clean --model=User --reason="end_of_trial"
Implement the command in app/Console/Commands/ExpiryBulkCleanCommand.php.
How can I help you explore Laravel packages today?