Repository navigation
Notifications
The Notification component is a channel-based notification system that allows you to send notifications to your users through multiple channels (email, SMS, database, webhooks, or custom channels) using a unified, expressive API.
It follows the same philosophy as Laravel's notification system but is built specifically for the Machinjiri Core framework with a focus on:
- Extensibility — Add custom channels in minutes
- Flexibility — Send via one or many channels in a single call
- Testability — Clean separation of concerns for easy mocking
- Async support — Queue notifications for background delivery
- Observability — Built-in logging and event hooks
| Feature | Description |
|---|---|
| Multi-channel delivery | Send via mail, SMS, database, webhook, or custom channels in one call |
| Fluent API | Chainable methods for queue names and delays |
| Custom channels | Register your own channel with a single call |
| Delayed delivery | Schedule notifications to be sent later |
| Queued delivery | Dispatch to a job queue for background processing |
| Conditional sending | Per-recipient, per-channel opt-out logic |
| Result objects | Structured responses with success/failure tracking |
| Logging & events | Automatic logging and event hooks (notification.sent, notification.failed, etc.) |
| Lazy resolution | Channel instances are resolved on demand |
The Notification component is part of the Machinjiri Core framework. If you're using the framework, it should already be available.
Then ensure the NotificationManager is bootstrapped in your application's service provider or bootstrap file:
use Mlangeni\Machinjiri\Core\Components\Notification\NotificationManager;
$manager = new NotificationManager($container);The NotificationManager automatically registers itself as a singleton, so the Notifiable trait can access it via NotificationManager::instance().
Here's a minimal example to get you up and running in under 2 minutes:
namespace App\Notifications;
use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;
class WelcomeNotification extends Notification
{
public function via(NotifiableInterface $notifiable): array
{
return ['mail'];
}
public function toMail(NotifiableInterface $notifiable): array
{
return [
'subject' => 'Welcome aboard!',
'html' => '<h1>Welcome!</h1><p>We are glad to have you.</p>',
];
}
}use Mlangeni\Machinjiri\Core\Components\Notification\Notifiable;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;
class User implements NotifiableInterface
{
use Notifiable;
public function __construct(
public int $id,
public string $email,
) {}
}$user = new User(1, 'jane@example.com');
$user->notify(new WelcomeNotification());That's it! 🎉
A Notification is a class extending Notification. It describes:
-
Which channels to use (
via()) -
What payload to send per channel (
toMail(),toSms(), etc.) - Optional behavior like queue name, delay, and conditional sending
abstract class Notification
{
abstract public function via(NotifiableInterface $notifiable): array;
public function toMail(NotifiableInterface $notifiable): mixed { return null; }
public function toSms(NotifiableInterface $notifiable): mixed { return null; }
public function toDatabase(NotifiableInterface $notifiable): ?array { return null; }
public function toWebhook(NotifiableInterface $notifiable): ?array { return null; }
public function shouldSend(NotifiableInterface $notifiable, string $channel): bool { return true; }
}A Notifiable is any entity that can receive notifications (typically a User or similar model). It implements NotifiableInterface and typically uses the Notifiable trait, which provides:
-
notify(),notifyNow(),notifyAsync()convenience methods -
routeNotificationFor()to resolve channel-specific addresses -
getNotifiableId()for tracking
A Channel implements ChannelInterface and is responsible for delivering a notification via one transport:
interface ChannelInterface
{
public function name(): string;
public function send(Notification $notification, NotifiableInterface $notifiable): NotificationResponse;
}Channels are registered with the ChannelManager under a name that matches the key returned from via().
The NotificationManager is the orchestrator. It:
- Resolves channel instances lazily
- Iterates over channels returned by
via() - Applies
shouldSend()filtering - Dispatches to the queue if needed
- Aggregates responses into a
NotificationResult - Logs and emits events
Notifications are simple classes. The only required method is via(), which returns an array of channel names:
namespace App\Notifications;
use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;
class InvoicePaidNotification extends Notification
{
public function __construct(
private readonly int $invoiceId,
private readonly float $amount,
) {}
public function via(NotifiableInterface $notifiable): array
{
return ['mail', 'sms'];
}
public function toMail(NotifiableInterface $notifiable): array
{
return [
'subject' => "Invoice #{$this->invoiceId} paid",
'html' => "<p>Your invoice of \${$this->amount} was paid.</p>",
];
}
public function toSms(NotifiableInterface $notifiable): array
{
return [
'to' => $notifiable->routeNotificationFor('sms'),
'body' => "Invoice #{$this->invoiceId} paid: \${$this->amount}",
];
}
}Use the Notifiable trait on your model and implement the two required methods:
use Mlangeni\Machinjiri\Core\Components\Notification\Notifiable;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;
class User implements NotifiableInterface
{
use Notifiable;
public function __construct(
public int $id,
public string $email,
public ?string $phone = null,
) {}
}The Notifiable trait automatically resolves:
| Channel | Resolved property |
|---|---|
mail |
$email / $email_address / $emailAddress
|
sms |
$phone / $phone_number / $mobile
|
webhook |
$webhook_url |
Override routeNotificationFor() if your model uses different property names:
public function routeNotificationFor(string $channel): mixed
{
return match ($channel) {
'mail' => $this->contact_email,
'sms' => $this->cell,
default => null,
};
}Three ways to send:
// 1. Convenience (uses the trait)
$user->notify(new InvoicePaidNotification(123, 99.50));
// 2. Explicit (uses the manager)
$manager = app(NotificationManager::class);
$manager->send($user, new InvoicePaidNotification(123, 99.50));
// 3. Fan-out to multiple recipients
$manager->send(
[$user1, $user2, $user3],
new SystemMaintenanceNotification()
);Every call returns a NotificationResult:
$result = $user->notify(new InvoicePaidNotification(123, 99.50));
if ($result->isSuccessful()) {
echo "All channels delivered!";
} else {
foreach ($result->failures() as $failure) {
echo "{$failure->channel} failed: {$failure->error}\n";
}
}Use shouldSend() to opt-out of delivery for specific recipients or channels. This is useful for things like "do not disturb" preferences, quotas, or unsupported channels:
public function shouldSend(NotifiableInterface $notifiable, string $channel): bool
{
// Only email users who have opted in
if ($channel === 'mail' && ! $notifiable->email_opt_in) {
return false;
}
// Only SMS on weekdays
if ($channel === 'sms' && in_array(date('N'), [6, 7])) {
return false;
}
return true;
}You can also conditionally return channels in via():
public function via(NotifiableInterface $notifiable): array
{
$channels = ['mail'];
if ($notifiable->phone) {
$channels[] = 'sms';
}
if ($notifiable->wants_digest) {
$channels[] = 'database';
}
return $channels;
}For slow channels (email, SMS, webhooks), prefer async delivery:
// Queue with default queue name
$user->notifyAsync(new InvoicePaidNotification(123, 99.50));
// Queue with a specific queue
$user->notifyAsync(new InvoicePaidNotification(123, 99.50), 'high-priority');
// Or use the manager
$manager->queue($user, $notification, 'notifications');The queue API returns an array of job IDs (one per notifiable):
$jobIds = $manager->queue([$user1, $user2], $notification);
// => ['job_abc', 'job_def']Delayed delivery — queue the notification for later:
$notification = (new InvoicePaidNotification(123, 99.50))
->delay(3600); // Send in 1 hour
$user->notifyAsync($notification);Note: The queue requires a
JobDispatcherInterfacebound in your container as'queue.dispatcher'. If no dispatcher is bound, callingqueue()will throw aNotificationException.
Register a custom channel in the manager:
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\ChannelInterface;
use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\NotificationResponse;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;
class SlackChannel implements ChannelInterface
{
public function __construct(private SlackClient $slack) {}
public function name(): string
{
return 'slack';
}
public function send(Notification $notification, NotifiableInterface $notifiable): NotificationResponse
{
$payload = $notification->toSlack($notifiable);
if ($payload === null) {
return NotificationResponse::failure('slack', 'No Slack payload provided.');
}
try {
$response = $this->slack->post($notifiable->routeNotificationFor('slack'), $payload);
return NotificationResponse::success('slack', $response);
} catch (\Throwable $e) {
return NotificationResponse::failure('slack', $e->getMessage());
}
}
}Register it:
$manager->extend('slack', new SlackChannel($slackClient));
// Or with a lazy factory
$manager->extend('slack', fn($app) => new SlackChannel($app->make(SlackClient::class)));Then in your notification:
public function via(NotifiableInterface $notifiable): array
{
return ['mail', 'slack'];
}
public function toSlack(NotifiableInterface $notifiable): ?array
{
return [
'text' => 'Deployment succeeded!',
'channel' => '#ops',
];
}Tip: Methods like
toSlack()are not part of the baseNotificationclass. PHP's dynamic dispatch means you can define arbitraryto{Channel}()methods — just make sure your custom channel calls them.
Each channel expects a specific payload shape. Returning null signals "this channel is unsupported for this notification":
Accepts either a string (treated as HTML body) or an array:
public function toMail(NotifiableInterface $notifiable): array
{
return [
'to' => 'override@example.com', // optional, overrides route
'cc' => ['cc@example.com'],
'bcc' => ['bcc@example.com'],
'subject' => 'Your invoice',
'html' => '<p>Hello!</p>',
'text' => 'Hello!',
'attachments' => ['/path/to/file.pdf'],
];
}You can also return a MailMessage instance directly for full control.
Accepts either a Message object or an array (via Message::fromArray()):
public function toSms(NotifiableInterface $notifiable): array
{
return [
'to' => $notifiable->routeNotificationFor('sms'),
'body' => 'Your verification code is 123456.',
];
}Returns a payload array. The database channel will inject notification_id, notification (class name), and created_at automatically:
public function toDatabase(NotifiableInterface $notifiable): ?array
{
return [
'type' => 'invoice_paid',
'message' => "Invoice #{$this->invoiceId} was paid.",
'amount' => $this->amount,
'meta' => ['invoice_id' => $this->invoiceId],
];
}Returns a payload with url (or relies on routeNotificationFor('webhook')), event, data, and optional headers/secret:
public function toWebhook(NotifiableInterface $notifiable): ?array
{
return [
'url' => 'https://example.com/hooks/payments',
'event' => 'invoice.paid',
'data' => ['invoice_id' => $this->invoiceId, 'amount' => $this->amount],
'secret' => 'shared-secret', // optional HMAC signature
];
}NotificationResult gives you structured access to what happened:
$result = $user->notify($notification);
$result->isSuccessful(); // true if no failures
$result->isEmpty(); // true if no channels were attempted
$result->count(); // number of responses
$result->all(); // NotificationResponse[]
$result->successes(); // successful responses
$result->failures(); // failed responses
foreach ($result->successes() as $response) {
echo "✓ {$response->channel}: " . json_encode($response->result);
}
foreach ($result->failures() as $response) {
echo "✗ {$response->channel}: {$response->error}";
}NotificationResult implements JsonSerializable, so it can be returned directly from a controller:
return response()->json($result);
// {
// "successful": true,
// "responses": [
// { "channel": "mail", "success": true, "result": {...}, "error": null, "meta": {} }
// ]
// }The ChannelManager is accessible via $manager->channels(). You can interact with it directly:
$channels = $manager->channels();
$channels->has('slack'); // bool
$channels->registered(); // string[] — all registered channel names
$channels->channel('mail'); // ChannelInterface
$channels->register('sms', $instance); // re-register/overrideThe ChannelManager::registerChannels() method reads from notification.channels in your configuration. This is the recommended way to register application-level channels:
// config/notification.php
return [
'channels' => [
'slack' => \App\Notifications\Channels\SlackChannel::class,
'telegram' => fn($app) => new TelegramChannel($app->make(TelegramClient::class)),
],
'notification_table' => 'notifications',
];Then call $manager->channels()->registerChannels() at bootstrap.
Resolution order for a channel registered as a class string:
- If the class is callable and a
ChannelInterface— use directly - If the class is callable only — invoke as factory with
$app - Otherwise — resolve via
$app->resolve($class)and verifyChannelInterface
Simply re-register with the same name:
$manager->extend('mail', fn($app) => new MyCustomMailChannel($app->make(MailManager::class)));The DatabaseChannel writes through any NotificationStoreInterface. The default is QueryBuilder, but you can swap it:
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotificationStoreInterface;
class RedisNotificationStore implements NotificationStoreInterface
{
public function __construct(private Redis $redis) {}
public function store(
string $notifiableId,
string $channel,
array $payload,
?string $notificationId = null
): mixed {
$key = "notifications:{$notifiableId}";
$id = $notificationId ?? bin2hex(random_bytes(12));
$this->redis->lpush($key, json_encode([
'id' => $id,
'channel' => $channel,
'payload' => $payload,
'created_at' => date(DATE_ATOM),
]));
return $id;
}
}Then bind it in the container or construct a DatabaseChannel with it:
$manager->extend('database', fn($app) => new DatabaseChannel($app->make(RedisNotificationStore::class)));The bundled LoggerNotificationStore is a no-op store that logs instead of persisting — useful in development or when you don't want a DB dependency.
The manager triggers the following events on the EventListener:
| Event | Payload |
|---|---|
notification.sending |
notification, notifiable, channel
|
notification.sent |
notification, notifiable, channel, response
|
notification.failed |
notification, notifiable, channel, response or exception
|
notification.queued |
notification, notifiable, queue
|
Attach listeners to react to these events (e.g., increment metrics, alert on failures):
$events->listen('notification.failed', function (array $payload) use ($alerting) {
$alerting->warning('Notification failed', [
'channel' => $payload['channel'],
'class' => get_class($payload['notification']),
]);
});All notification-specific errors are thrown as NotificationException with a context array and a code:
| Code | Meaning |
|---|---|
notification_channel |
Channel not registered, factory returned wrong type |
notification_config |
Manager not bootstrapped, invalid via() return, invalid notifiable, missing dispatcher |
Per-channel failures are not thrown. They're captured in the NotificationResult as NotificationResponse::failure(). This means a failing SMS channel never prevents the mail channel from being attempted.
To check if any failure occurred:
if (! $result->isSuccessful()) {
// At least one channel failed
}To bubble exceptions (e.g., in tests), use failures() and re-throw.
Register a spy channel to assert deliveries:
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\ChannelInterface;
use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\NotificationResponse;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;
class FakeChannel implements ChannelInterface
{
public array $sent = [];
public function name(): string { return 'fake'; }
public function send(Notification $notification, NotifiableInterface $notifiable): NotificationResponse
{
$this->sent[] = [$notification, $notifiable];
return NotificationResponse::success('fake', ['ok' => true]);
}
}$fake = new FakeChannel();
$manager->extend('fake', $fake);
$user->notify(new NotificationUsingFakeChannel());
$this->assertCount(1, $fake->sent);public function test_it_skips_sms_on_weekends()
{
$notification = new ReminderNotification();
$user = new User(1, 'a@b.c', phone: '123');
// Assume today is a Saturday in the test environment
$result = $user->notify($notification);
$this->assertEmpty(
array_filter($result->all(), fn($r) => $r->channel === 'sms')
);
}Mock the dispatcher and assert the job was dispatched:
$dispatcher = $this->createMock(JobDispatcherInterface::class);
$dispatcher->expects($this->once())
->method('dispatchToQueue')
->willReturn('job-123');
$manager = new NotificationManager($app, dispatcher: $dispatcher);
NotificationManager::setInstance($manager);
$user->notifyAsync(new ReminderNotification(), 'reminders');- Lazy resolution — Channels aren't instantiated until first use; the first send in a process pays the construction cost.
-
Per-notifiable channel re-resolution —
via()andshouldSend()are called per notifiable. Keep them cheap. -
Avoid expensive work in
via()— Prefer to do heavier checks insideshouldSend(), which can short-circuit. -
Queue slow channels — Mail, SMS, and webhooks are I/O bound. Use
notifyAsync()to avoid blocking request lifecycles. - Result aggregation — Fan-out to N recipients × M channels creates N×M responses in memory. For very large batches, chunk your recipients.
-
Webhook DNS lookups — The webhook channel re-resolves
HttpRequestper send when not bound to the container; bindHttpRequest::classin your container to reuse a single client.
Sends through MailManager. Accepts string, array, or MailMessage payload from toMail().
| Payload key | Description |
|---|---|
to, cc, bcc
|
Recipients (defaults to routeNotificationFor('mail')) |
subject |
Email subject |
html, text
|
Body (both, or one) |
attachments |
Array of file paths |
Sends through SMSManager. Accepts a Message or an array compatible with Message::fromArray(). If the notification has a queue name set, the SMS manager is told to send asynchronously.
Persists notifications to a store (default: QueryBuilder → database_channel_notifications table). Requires toDatabase() to return an array. Automatically injects notification_id, notification (class name), and created_at.
Bug note: In
DatabaseChannel::withFallback(), the defaultQueryBuilderis bypassed in favor of aLoggerNotificationStore. If you rely onwithFallback()for DB persistence, override the channel explicitly instead.
POSTs JSON to a URL. Requires toWebhook() to return an array with a url (or rely on routeNotificationFor('webhook')).
| Payload key | Description |
|---|---|
url |
Target URL (validated) |
event |
Event name (defaults to notification class) |
data |
Arbitrary data payload |
headers |
Extra request headers |
secret |
Adds X-Notification-Signature (HMAC-SHA256) |
| Method | Description |
|---|---|
via(NotifiableInterface $n): array |
Required — returns channel names |
toMail(), toSms(), toDatabase(), toWebhook()
|
Payload builders (return null to skip) |
shouldSend(NotifiableInterface $n, string $ch): bool |
Per-recipient/channel gate |
onQueue(?string $queue): static |
Set queue name |
queueName(): ?string |
Get queue name |
delay(?int $seconds): static |
Set delay |
delaySeconds(): ?int |
Get delay |
id(): ?string |
Unique notification ID (UUID v1) |
| Method | Description |
|---|---|
notify(Notification): NotificationResult |
Send synchronously |
notifyNow(Notification): NotificationResult |
Alias of notify()
|
notifyAsync(Notification, ?string $queue): array |
Queue for async delivery |
routeNotificationFor(string $channel): mixed |
Resolve recipient address |
getNotifiableId(): string |
Stable ID (uuid → id → spl_object_hash) |
| Method | Description |
|---|---|
static instance(): self |
Get singleton |
static setInstance(?self): void |
Set singleton (for tests) |
channels(): ChannelManager |
Access channel manager |
extend(string, callable|ChannelInterface): self |
Register a channel |
send($notifiables, Notification): NotificationResult |
Sync send |
sendNow($notifiables, Notification): NotificationResult |
Alias of send()
|
queue($notifiables, Notification, ?string $queue): string[] |
Async send; returns job IDs |
| Method | Description |
|---|---|
register(string $name, mixed $channel): self |
Register/override a channel |
has(string $name): bool |
Check registration |
channel(string $name): ?ChannelInterface |
Resolve a channel |
registered(): string[] |
List registered names |
registerChannels(): void |
Load channels from config |
| Property/Method | Description |
|---|---|
$channel |
Channel name |
$success |
Whether it succeeded |
$result |
Channel-specific result value |
$error |
Error message (on failure) |
$meta |
Extra metadata |
static success(string, mixed, array) |
Construct a success |
static failure(string, string, array) |
Construct a failure |
toArray(): array |
Array representation |
| Method | Description |
|---|---|
add(NotificationResponse): self |
Add a response |
merge(self): self |
Merge another result |
all(): NotificationResponse[] |
All responses |
successes(): NotificationResponse[] |
Successful responses |
failures(): NotificationResponse[] |
Failed responses |
isSuccessful(): bool |
No failures |
isEmpty(): bool |
No responses |
count(): int |
Response count |
jsonSerialize(): array |
JSON representation |
| Symptom | Cause | Fix |
|---|---|---|
NotificationManager has not been instantiated yet |
The manager was never constructed in the bootstrap | Construct a NotificationManager (or make it via the container) before sending |
Notification channel 'x' is not registered |
Channel name in via() has no registration |
Register it via $manager->extend() or config |
Factory for notification channel 'x' did not return a ChannelInterface |
A class-string channel resolved to a non-channel | Ensure the class implements ChannelInterface
|
Notification X::via() must return an array |
via() returned null/string |
Return [], ['mail'], etc. |
Job dispatcher not bound in Container |
notifyAsync() called without a dispatcher |
Bind JobDispatcherInterface as 'queue.dispatcher' in the container |
| Notifications silently skipped | Channel failed has() check or shouldSend() returned false |
Check the logger output; add notification.sending listener to debug |
MailChannel falls through with empty message |
toMail() returned null
|
Return a payload or remove 'mail' from via()
|
| Webhook failed with 4xx/5xx | Target server rejected the request | Inspect response->meta['body'] in the failure |
DatabaseChannel writes to log instead of DB |
Used withFallback() or notification_table missing |
Construct DatabaseChannel directly with a QueryBuilder
|
Warning on
ChannelManager::channel(): The method is documented as returning?ChannelInterface, but the branch that resolves a registeredChannelInterfaceinstance directly (when it's not a callable and not already resolved) falls off the end without areturn. This is a latent bug — if you hitnullwhen you expected a channel, this is likely why. Fix by addingreturn $this->resolved[$name] = $channel;at the end ofchannel(), or ensure your channels are registered as factories.
- Keep notifications focused — One notification = one logical event. Don't mix "invoice paid" and "welcome aboard" in one class.
-
Prefer
notifyAsync()for I/O channels — mail, SMS, and webhooks should always be queued in production. -
Use
shouldSend()overvia()for dynamic filtering —via()should describe what could be sent;shouldSend()should decide what should be sent. -
Return
nullto skip a channel — IftoMail()returnsnull, the mail channel records a failure. If you want to skip silently, remove the channel fromvia()or gate it inshouldSend(). -
Namespace your custom channels — e.g.,
App\Notifications\Channels\SlackChannel. -
Version your payloads — For webhooks, include a
versionfield in the payload so consumers can evolve safely. -
Sign webhooks — Always use the
secretoption in production webhooks to allow HMAC verification on the receiving end. -
Log correlation IDs — Include a
notification_idor a business-level correlation ID in all payloads. The base class providesid()for you. - Handle fan-out memory — For very large recipient lists, chunk sends into batches of ~100.
-
Test the whole pipeline — Assert both that channels received the correct payloads and that
shouldSend()filtering behaves as expected.
Q: Can I send the same notification to multiple channels per recipient?
Yes — return all channel names from via(). Each channel's send() is called independently.
Q: What happens if one channel fails — do the others still run?
Yes. Exceptions inside a channel's send() are caught by the manager and converted to a NotificationResponse::failure(). All channels are attempted.
Q: Can I change the queue used by a specific notification?
Yes — (new MyNotification())->onQueue('high-priority'), or pass the queue as the third argument to notifyAsync() / queue().
Q: How do I send a notification to many users efficiently?
Use queue() with an array of notifiables, or use notifyAsync() per user. Both dispatch one job per notifiable.
Q: Where are queued notifications processed?
Wherever your JobDispatcherInterface implementation processes jobs. The dispatch target is SendNotificationJob, which calls sendNow() on the recipient + notification.
Q: Can I serialize a Notification for the queue?
Yes — Notification implements __serialize()/__unserialize() and persists its id, queueName, and delay. If you add state to your subclass, override these methods to include it.
Q: Can I use a class-string as a channel registration?
Yes — $manager->extend('foo', MyChannel::class). The manager will resolve it from the container.
Q: How do I make a channel async-aware?
Channels don't need to know about queues — the manager handles async orchestration. Only channels that internally queue (like the current SMS channel, which checks queueName()) need extra care.
Q: Can I send a notification without a Notifiable model (e.g., a raw email address)?
Create an ad-hoc NotifiableInterface implementation (an anonymous class works) that returns the address from routeNotificationFor().
Machinjiri Docs
- Home
- Learn
- Build
-
Tutorials
- Routing
- Controllers and Middleware
- Views and Templates
- Database ORM
- Eloquent Database ORM
- Errors and Exceptions
- Forms and Validation
- Http
- Events and Logging
- Services and DI
- Security
- Authentication
- Caching
- File System
- Date and Time
- Debugging
- Queue and Jobs
- Task Scheduling
- Transport (Mail, SMS)
- LDAP
- Notifications
- Webhooks
- UUID Generations
- Terminal
- Helper Function
- Reference