ilbronza/schedules is a Laravel package for schedules and deadlines for laravel.
It currently has 0 GitHub stars and 107 downloads on Packagist (latest version v2.5.1).
Install it with composer require ilbronza/schedules.
Discover more Laravel packages by ilbronza
or browse all Laravel packages to compare alternatives.
Last updated
Scadenze e soglie di notifica per Laravel, basate su un valore che avanza nel tempo (chilometri, date, ore, pezzi, …).
Il package non calcola “quanto manca” da solo: legge il valore corrente dal modello host, lo confronta con una scadenza, e quando una soglia è raggiunta notifica i ruoli configurati.
Se stai arrivando da v2.5.1, leggi anche UPGRADING.md.
Esempio classico: un veicolo ha un odometro. Vuoi:
Lo stesso schema vale per date (assicurazione tra 365 giorni, avviso a 30), ore di lavoro, pezzi prodotti, e qualunque unità gestita da ilbronza/measurementunits.
Il flusso è sempre questo:
Tipo (template) → applicazione su un modello → Schedule (istanza)
→ evaluator ogni minuto
→ notifiche alle soglie
→ scadenza marcata expired
→ operatore la marca managed
| Concetto | Classe | Ruolo |
|---|---|---|
| Tipo | Type | Template: unità di misura, durata (validity), modelli a cui si applica, ruoli da notificare |
| Soglia tipo | TypeNotification | “Avvisa N unità prima della deadline”, con eventuale ripetizione |
| Scadenza | Schedule | Istanza su un modello concreto (Vehicle #12), con starting_value e deadline_value |
| Soglia istanza | ScheduledNotification | Copia operativa di una TypeNotification su quella schedule |
| Invio | ScheduledNotificationDispatch | Storico di ogni invio (ripetizioni incluse) |
Una schedule vive su un morph (schedulable_type / schedulable_id). Il valore corrente non è salvato sulla schedule: viene riletto dal modello a ogni valutazione.
Type 1──* TypeNotification
│
└──* Schedule ── morph ── Vehicle / altro modello Schedulable
│
└──* ScheduledNotification ──* ScheduledNotificationDispatch
composer require ilbronza/schedules
php artisan vendor:publish --tag=schedules.config
php artisan migrate
Dipendenze runtime dichiarate in composer.json: Laravel 11, Carbon 3, Spatie Activitylog e Permission, e i pacchetti IlBronza crud, buttons, datatables, form, formfield, measurementunits, notifications.
Prima di lanciare le migration di Schedules deve già esistere la tabella Laravel notifications (di solito creata da ilbronza/notifications). Le scheduled notification tengono una foreign key su notifications.id.
Il package non conosce i modelli del progetto. Pubblica config/schedules.php e dichiara:
'applicableTo' => [
\App\Models\Vehicle::class => 'vehicles',
],
'notifications' => [
// Modello Eloquent che usa il trait Notifiable (tipicamente Role).
'roleModel' => \App\Models\Role::class,
],
'defaultRoles' => [
'superadmin',
'administrator',
'schedules',
],
'routeRoles' => [
// Override opzionale per singola route:
// 'ibSchedules.types.index' => ['schedules'],
],
'evaluator' => [
'enabled' => true,
'cron' => '* * * * *',
'chunkSize' => 100,
'overlapExpirationMinutes' => 10,
],
Punti fermi:
applicableTo deve implementare IlBronza\Schedules\Contracts\Schedulable.vehicles) è lo slug usato nelle URL. Non mettere mai un FQCN PHP in una route.RuntimeException.notifications.roleModel è null, si usa il role model di AccountManager.Schedulable fanno fallire il boot. Per disattivare (sconsigliato): 'validateConfiguration' => false.use IlBronza\Schedules\Contracts\Schedulable;
use IlBronza\Schedules\Traits\InteractsWithSchedule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Vehicle extends Model implements Schedulable
{
use InteractsWithSchedule;
/**
* Metodi che il package può chiamare per leggere il valore corrente.
* Qualunque altro nome viene rifiutato.
*/
protected array $allowedScheduleValueMethods = [
'getOdometer',
];
public function getOdometer(): float
{
return (float) $this->odometer;
}
/**
* Scope usato dalla UI di applicazione massiva.
* Il nome è scope + Studly(nome del Type).
* Tipo "Revisione" → scopeRevisione.
*/
public function scopeRevisione(Builder $query): Builder
{
return $query->where('active', true);
}
public function getName(): string
{
return $this->plate;
}
}
Il tipo di scadenza indica come leggere il valore, in uno di questi due modi:
| Campo sul Type | Effetto |
|---|---|
| source | odometer → chiama getOdometer() |
| method | chiama esattamente quel metodo, es. getOdometer |
Se sono presenti entrambi, vince method. In ogni caso il metodo deve stare in $allowedScheduleValueMethods. Un getter non allowlistato lancia RuntimeException (e in HTTP create/update del tipo dà 422).
Non è un accessor Eloquent: source = current_km risolve getCurrentKm(), non getCurrentKmAttribute().
| Metodo | Default | A cosa serve |
|---|---|---|
| getSchedulableModelNameAttribute() | getMorphClass() | Etichetta del modello nella lista “applica a…” |
| getSchedulableModelTableFieldsArray() | colonne vuote extra | Campi aggiuntivi nella datatable degli elementi |
| getName() | — | Nome mostrato nel payload della notifica |
getSchedulableElementsQuery() chiama scope{Studly(nome del Type)}. Se manca, l’indice di applicazione lancia un’eccezione. Se vuoi tutti i record:
public function scopeRevisione(Builder $query): Builder
{
return $query;
}
Applicando le scadenze da codice (ScheduleApplicatorHelper) lo scope non è necessario.
Un Type è il template. Campi rilevanti:
| Campo | Significato |
|---|---|
| name | Nome visibile. Dalla UI di applicazione deriva anche lo scope (Revisione → scopeRevisione) |
| measurement_unit_id | Unità di ilbronza/measurementunits (km, giorni, ore, …) |
| validity | Durata della scadenza in quell’unità. starting + validity = deadline |
| percentage_validity | Soglia di “in scadenza”: isExpiring() è true quando il progresso ≥ questo valore (default 99) |
| allow_multiple | Se false, un modello può avere una sola schedule corrente di questo tipo |
| models | Elenco dei FQCN a cui si applica, con source e/o method |
| roles | Ruoli Spatie che ricevono le notifiche (id del role model) |
models deve:
schedules.applicableTo;source e/o method;Esempio in codice:
use IlBronza\Schedules\Models\Type;
$type = Type::getProjectClassName()::create([
'name' => 'Revisione',
'validity' => 20000,
'percentage_validity' => 90,
'measurement_unit_id' => $kilometers->getKey(),
'allow_multiple' => false,
'models' => [
[
'model' => \App\Models\Vehicle::class,
'source' => 'odometer',
],
],
'roles' => [
['roles' => $maintenanceRoleId],
],
]);
roles accetta anche role_id o id come chiave dell’entry. Entry malformate o vuote lanciano RuntimeException in valutazione.
Dalla UI: Impostazioni → Scadenze → Tipologie (/schedules-management/types).
Una TypeNotification appartiene a un tipo e definisce quando avvisare rispetto alla deadline della schedule.
| Campo | Significato |
|---|---|
| before | Offset della prima ripetizione, prima della deadline. Obbligatorio |
| repeat_every | Intervallo tra un invio e il successivo (dopo il primo) |
| repeat_every_measurement_unit_id | Unità dell’intervallo (stessa base dell’unità del tipo) |
| last_repetition | Offset dell’ultima ripetizione ammessa, prima della deadline. Se omesso, si ripete fino alla deadline |
| urgency | Intero ≥ 0 scritto nel payload (urgency / priority) |
type_id è immutabile dopo la creazione.
repeat_every e l’unità di ripetizione stanno insieme: uno senza l’altro è errore.last_repetition richiede la ripetizione.repeat_every deve essere numerico e > 0.last_repetition deve essere numerico, ≥ 0, e più vicino alla deadline di before (last_repetition ≤ before).Tipo con validity = 20000 km, veicolo a 90.000 km:
90000, deadline 110000before = 2000 → prima notifica a 108000repeat_every = 500, last_repetition = 500 → invii a 108000, 108500, 109000, 109500Senza ripetizione la soglia parte una sola volta, anche se l’evaluator gira di nuovo.
Ogni invio viene scritto in schedules__scheduled_notification_dispatches. Sulla scheduled notification restano solo notification_id e last_dispatched_value dell’ultimo invio (idempotenza e calcolo del prossimo intervallo).
Il messaggio di default:
La scadenza "Revisione" per "AB123CD" ha raggiunto una soglia di notifica.
ScheduleApplicatorHelper crea (o aggiorna) la schedule e tutte le soglie in una transazione. O tutto o niente.
use IlBronza\Schedules\Helpers\Applicators\ScheduleApplicatorHelper;
use IlBronza\Schedules\Helpers\Applicators\BulkScheduleApplicatorHelper;
// Partenza = valore corrente del modello (getOdometer()), deadline = starting + validity
$schedule = ScheduleApplicatorHelper::applicateScheduleToModel($type, $vehicle);
// Partenza esplicita
$schedule = ScheduleApplicatorHelper::applicateStartingScheduleToModel($type, $vehicle, 90000);
// Deadline esplicita (starting = deadline − validity)
$schedule = ScheduleApplicatorHelper::applicateEndingScheduleToModel($type, $vehicle, 110000);
// Come i precedenti, ma riusano la schedule corrente dello stesso tipo.
// Non creano duplicati. Se i limiti cambiano, le soglie si riallineano;
// una soglia il cui threshold si sposta torna pending e può essere reinviata.
$schedule = ScheduleApplicatorHelper::findOrApplicateStartingScheduleToModel($type, $vehicle, 95000);
$schedule = ScheduleApplicatorHelper::findOrApplicateEndingScheduleToModel($type, $vehicle, 115000);
// Stesso tipo su tanti modelli, una transazione, lock ordinati
BulkScheduleApplicatorHelper::applicateScheduleToModels($type, $vehicles);
applicate* (senza findOr) crea sempre una schedule nuova. Se allow_multiple è false e ne esiste già una corrente, lancia RuntimeException.
vehicles).La route usa l’alias, non la classe PHP:
/schedules-management/types/applicate/{type}/models/vehicles
Il package registra schedules:evaluate e, di default, lo mette nello scheduler Laravel ogni minuto (withoutOverlapping). L’host deve comunque far girare lo scheduler:
* * * * * php /path/to/artisan schedule:run
A mano:
php artisan schedules:evaluate
php artisan schedules:evaluate --chunk=250
Per invalidare manualmente le schedule:
# Solo quelle appartenenti a un tipo
php artisan schedules:invalidate <type-id>
# Quelle di tutti i tipi; verifica prima il conteggio senza modifiche
php artisan schedules:invalidate --all --dry-run
php artisan schedules:invalidate --all
type e --all sono alternativi. L'invalidazione soft-delete di ogni schedule
e delle relative scheduled notification; non modifica expired_at e non fa
force-delete, quindi resta possibile il ripristino.
Per ogni schedule corrente (non expired, non managed) l’evaluator:
expired_at e fa scadere le soglie ancora pending.Le soglie già dispatched o managed non vengono toccate quando la schedule scade.
L’esecuzione è fail-fast: valore illeggibile, ruoli mancanti, unità di ripetizione incompatibile, errore di dispatch → il comando si ferma.
Il valore corrente della schedule è anche esposto come attributo current_value, cachato 60 secondi.
Per disattivare lo schedule automatico: 'evaluator.enabled' => false.
Quando l’intervento è stato fatto (revisione eseguita, rinnovo, …) si marca la schedule come managed. La transizione è transazionale: lo stesso managed_at va sulla schedule e su tutte le scheduled notification ancora senza timestamp. Ripetere l’azione non cambia il timestamp originale.
use IlBronza\Schedules\Services\ScheduleManager;
app(ScheduleManager::class)->manage($schedule);
app(ScheduleManager::class)->manageMany([$id1, $id2]);
Dalla tabella scadenze: azione singola o “Segna come gestite” sulla selezione.
Una schedule managed non viene più valutata.
Metodi del trait InteractsWithSchedule:
$vehicle->schedules; // morphMany
$vehicle->getSchedules();
$vehicle->getCurrentScheduleByType($type); // una, la più lontana in deadline
$vehicle->getCurrentSchedulesByType($type); // tutte le correnti di quel tipo
$vehicle->getLatestByType($type); // anche expired/managed
$vehicle->getApplicatedScheduleTypes(); // Type distinti già applicati
$vehicle->scheduledNotifications(); // hasManyThrough, filtrato sul morph
Sulla schedule:
$schedule->getStartingValue();
$schedule->getDeadlineValue();
$schedule->getCurrentValue(); // rilegge il modello (cache 60s)
$schedule->getPercentageValidity(); // 0 al starting, 100 alla deadline
$schedule->isCurrent();
$schedule->isExpiring(); // progresso ≥ percentage_validity del tipo
$schedule->isManaged();
$schedule->isMarkedExpired();
getPercentageValidity() lancia se starting e deadline coincidono (span zero).
Precedenza: managed > expired > current.
| Stato | Condizione |
|---|---|
| current | expired_at e managed_at nulli — viene valutata |
| expired | expired_at valorizzato — deadline raggiunta |
| managed | managed_at valorizzato — chiusa dall’operatore |
Scope: Schedule::current(), notExpired(), notManaged(), byType($type).
Precedenza: managed > expired > dispatched > pending.
| Stato | Condizione |
|---|---|
| pending | mai inviata, non expired, non managed |
| dispatched | almeno un invio (notification_id e/o riga in dispatches) |
| expired | la schedule è scaduta prima che questa soglia potesse partire |
| managed | la schedule è stata gestita |
Una soglia dispatched non viene sovrascritta a expired. markExpired() su una notifica non pending lancia RuntimeException.
Le soglie repeating restano eligible dopo il primo invio: lo scope pending() include anche quelle già dispatchate se il tipo ha repeat_every + unità.
Prefisso URL: /schedules-management. Prefisso nomi: ibSchedules. (config routePrefix).
Middleware: web, auth, schedules.roles. I ruoli ammessi sono defaultRoles, con override per route in routeRoles. Senza autenticazione o senza ruolo: accesso negato.
| Area | Route principali |
|---|---|
| Tipologie | ibSchedules.types.index/create/show/edit/destroy |
| Applica tipo | ibSchedules.types.applicate.index, types.applicate.classname.index/store |
| Soglie tipo | ibSchedules.types.typeNotifications.create/store, typeNotifications.* |
| Scadenze | ibSchedules.schedules.index/show/manage/manageMany |
Il menu (se il package Menu è registrato) aggiunge sotto Impostazioni: elenco tipologie e elenco scadenze.
Non c’è create/edit di una Schedule da form: si crea solo applicando un tipo.
Schedule soft-delete delle sue scheduled notification; il restore le ripristina.Type non si cancella se esistono schedule non cancellate che lo referenziano. Se la cancellazione è permessa, le type notification cascano.TypeNotification non si cancella se esistono scheduled notification che la referenziano.Tentativi non validi: RuntimeException, non silenzio.
Il default è IlBronzaScheduleNotificationDispatcher: invia ScheduleThresholdReachedNotification al primo ruolo del tipo, poi ChildDatabaseNotification agli altri, tramite ilbronza/notifications.
Per sostituirlo:
use IlBronza\Schedules\Contracts\ScheduleNotificationDispatcher;
$this->app->bind(
ScheduleNotificationDispatcher::class,
\App\Schedules\MyDispatcher::class
);
dispatch() deve restituire l’id della riga in notifications.
Se è installato ilbronza/filecabinet, le Dossierrow di tipo expiration-date possono fornire il valore corrente senza implementare Schedulable. È un ponte esplicito, non il percorso consigliato per i modelli nuovi.
In config/schedules.php → models.*.class puoi estendere Schedule, Type, ecc. La classe deve discendere da quella del package. La validazione di boot lo verifica.
Il package non ignora gli errori di dominio. In particolare:
| Situazione | Cosa succede |
|---|---|
| Classe in applicableTo inesistente o non Schedulable | eccezione al boot |
| Type.models invalido | eccezione al save / 422 in HTTP |
| Getter non allowlistato | RuntimeException |
| Configurazione ripetizione incompleta | eccezione al save / 422 |
| Tipo senza ruoli al momento del dispatch | l’evaluator si ferma |
| Role id inesistente | l’evaluator si ferma |
| Unità di ripetizione con base diversa da quella del tipo | l’evaluator si ferma |
| Cancellazione di Type/TypeNotification ancora referenziati | RuntimeException |
| Span starting–deadline a zero in percentage_validity | RuntimeException |
Questo è intenzionale: una scadenza silenziosa è peggio di un job che fallisce in evidenza.