composer create-project --prefer-dist laravel/laravel event-management
DB_DATABASE=laravel-10-event-management
DB_USERNAME=[USERNAME]
DB_PASSWORD=[PASSWORD]
php artisan migrate
# create model with migration files
php artisan make:model Event -m
# create model with migration files
php artisan make:model Attendee -m
# create api controller for Attendee
php artisan make:controller Api\AttendeeController --api
# create api controller for Event
php artisan make:controller Api\EventController --api
# use namespace on top
use App\Http\Controllers\Api\AttendeeController;
use App\Http\Controllers\Api\EventController;
# add routes below
Route::apiResource('events', EventController::class);
Route::apiResource('events.attendees', AttendeeController::class)
->scoped(['attendee' => 'event']);
https://github.com/jeremyabulencia/MasterLaravel10-events-management-REST/commit/e0c7ce8ce2d8df604f2ba7c0fbb6225933eeed62
php artisan migrate:refresh --seed
routes.php
Route::apiResource('events', EventController::class);
Route::apiResource('events.attendees', AttendeeController::class)
->scoped(['attendee' => 'event']);
EventController.php
curl --location 'http://127.0.0.1:8000/api/events'
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return Event::all();
}
curl --location 'http://127.0.0.1:8000/api/events/202'
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
public function show(Event $event)
{
return $event;
}
curl --location 'http://127.0.0.1:8000/api/events' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"name": "FirstEvent",
"start_time": "2023-08-30 08:00:00",
"end_time": "2023-09-01 07:59:59"
}'
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
public function store(Request $request)
{
$event = Event::create([
...$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time'
]),
"user_id" => 1
]);
return $event;
}
curl --location --request PUT 'http://127.0.0.1:8000/api/events/202' \
--header 'Content-Type: application/json' \
--data '{
"name": "FirstEvent Updated 2",
"start_time": "2023-08-30 08:00:00",
"end_time": "2023-09-01 07:59:59"
}'
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
public function update(Request $request, Event $event)
{
$event->update($request->validate([
'name' => 'sometimes|string|max:255',
'description' => 'nullable|string',
'start_time' => 'sometimes|date',
'end_time' => 'sometimes|date|after:start_time'
]));
return $event;
}
curl --location --request DELETE 'http://127.0.0.1:8000/api/events/202'
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
public function destroy(Event $event)
{
$event->delete();
return response(status: 204);
}
php artisan make:resource UserResource
php artisan make:resource EventResource
php artisan make:resource AttendeeResource
UserResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}
EventResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EventResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->description,
'description' => $this->description,
'start_time' => $this->start_time,
'end_time' => $this->end_time,
'user' => new UserResource($this->whenLoaded('user')),
'attendees' => AttendeeResource::collection(
$this->whenLoaded('attendees')
),
];
}
}
AttendeeResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class AttendeeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}
http://127.0.0.1:8000/api/events?include=user,attendees
class EventController extends Controller
{
public function index()
{
$query = Event::query();
$relations = ['user', 'attendees', 'attendees.user'];
foreach ($relations as $relation) {
$query->when(
$this->shouldIcludeRelation($relation),
fn($q) => $q->with($relation)
);
}
return EventResource::collection(
$query->latest()->paginate()
);
}
protected function shouldIcludeRelation(string $relation): bool
{
$include = request()->query('include');
if (!$include) {
return false;
}
$relations = array_map('trim', explode(',', $include));
return in_array($relation, $relations);
}
CanLoadRelationships.php
<?php
namespace App\Http\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
trait CanLoadRelationships
{
public function loadRelationships(
Model|EloquentBuilder|QueryBuilder $for,
?array $relations = null
): Model|EloquentBuilder|QueryBuilder
{
$relations = $relations ?? $this->relations ?? [];
foreach ($relations as $relation) {
$for->when(
$this->shouldIcludeRelation($relation),
fn($q) => $for instanceof Model ? $for->load($relation) : $for->with($relation)
);
}
return $for;
}
protected function shouldIcludeRelation(string $relation): bool
{
$include = request()->query('include');
if (!$include) {
return false;
}
$relations = array_map('trim', explode(',', $include));
return in_array($relation, $relations);
}
}
EventController.php
use App\Http\Traits\CanLoadRelationships;
class EventController extends Controller
{
use CanLoadRelationships;
private array $relations = ['user', 'attendees', 'attendees.user'];
public function index()
{
$query = $this->loadRelationships(Event::query());
...
}
public function store(Request $request)
{
...
return new EventResource($this->loadRelationships($event));
}
public function show(Event $event)
{
return new EventResource($this->loadRelationships($event));
}
public function update(Request $request, Event $event)
{
...
return new EventResource($this->loadRelationships($event));
}
https://laravel.com/docs/10.x/sanctum
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
php artisan make:controller Api/AuthController
https://laravel.com/docs/10.x/sanctum#revoking-tokens
// Revoke all tokens...
$user->tokens()->delete();
// Revoke the token that was used to authenticate the current request...
$request->user()->currentAccessToken()->delete();
// Revoke a specific token...
$user->tokens()->where('id', $tokenId)->delete();
AuthServiceProvider.php
public function boot(): void
{
Gate::define('update-event', function ($user, Event $event) {
return $user->id === $event->user_id;
});
Gate::define('delete-attendee', function ($user, Event $event, Attendee $attendee) {
return $user->id === $event->user_id || $user->id === $attendee->user_id;
});
}
EventController.php
public function update(Request $request, Event $event)
{
// if (Gate::denies('update-event', $event)) {
// abort(403, 'You are not authorized to update this event.');
// }
$this->authorize('update-event', $event);
$event->update($request->validate([
'name' => 'sometimes|string|max:255',
'description' => 'nullable|string',
'start_time' => 'sometimes|date',
'end_time' => 'sometimes|date|after:start_time'
]));
return new EventResource($this->loadRelationships($event));
}
AttendeeController.php
public function destroy(Event $event, Attendee $attendee)
{
$this->authorize('delete-attendee', [$event, $attendee]);
$attendee->delete();
return response(status : 204);
}
php artisan make:policy EventPolicy --model=Event
php artisan make:policy AttendeePolicy --model=Attendee
AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
// not required only to override the default behavior (best to use standard ways)
protected $policies = [
// Event::class => EventPolicy::class,
// Attendee::class => AttendeePolicy::class
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
// commented because Policies were added
// Gate::define('update-event', function ($user, Event $event) {
// return $user->id === $event->user_id;
// });
// Gate::define('delete-attendee', function ($user, Event $event, Attendee $attendee) {
// return $user->id === $event->user_id || $user->id === $attendee->user_id;
// });
}
}
EventController.php
public function __construct()
{
$this->middleware('auth:sanctum')->except(['index', 'show']);
$this->authorizeResource(Event::class, 'event');
}
AttendeeController.php
public function __construct()
{
$this->middleware('auth:sanctum')->except(['index', 'show', 'update']);
$this->authorizeResource(Attendee::class, 'attendee');
}
EventPolicy.php
class EventPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(?User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(?User $user, Event $event): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Event $event): bool
{
return $user->id === $event->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Event $event): bool
{
return $user->id === $event->user_id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Event $event): bool
{
//
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Event $event): bool
{
//
}
}
AttendeePolicy.php
class AttendeePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(?User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(?User $user, Attendee $attendee): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Attendee $attendee): bool
{
return $user->id === $attendee->event->user_id ||
$user->id === $attendee->user_id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Attendee $attendee): bool
{
//
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Attendee $attendee): bool
{
//
}
}
# create app/Console/Commands/SendEventReminders.php
php artisan make:command SendEventReminders
SendEventReminders.php
<?php
namespace App\Console\Commands;
use App\Models\Event;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
class SendEventReminders extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:send-event-reminders';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends notifications to all event attendees that event starts soon';
/**
* Execute the console command.
*/
public function handle()
{
$events = Event::with('attendees.user')
->whereBetween('start_time', [now(), now()->addDay()])
->get();
$eventCount = $events->count();
$eventLabel = Str::plural('event', $eventCount);
$this->info("Found {$eventCount} {$eventLabel}.");
$events->each(
fn($event) => $event->attendees->each(
fn($attendee) =>
$this->info("Notifying the user {$attendee->user->id}")
)
);
$this->info('Reminder notifications sent successfully!');
}
}
# run the event
$ php artisan app:send-event-reminders
Found 1 event.
Notifying the user 138
Notifying the user 573
Reminder notifications sent successfully!
https://laravel.com/docs/10.x/scheduling
Kernel.php
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->command('app:send-event-reminders')
->everyMinute();
}
# to run command continously
php artisan schedule:work
$ php artisan schedule:work
INFO Running scheduled tasks every minute.
2024-01-11 08:13:00 Running ["artisan" app:send-event-reminders] ...................................................................... 450ms DONE
⇂ "C:\php8\php.exe" "artisan" app:send-event-reminders > "NUL" 2>&1
2024-01-11 08:14:00 Running ["artisan" app:send-event-reminders] ...................................................................... 416ms DONE
⇂ "C:\php8\php.exe" "artisan" app:send-event-reminders > "NUL" 2>&1
2024-01-11 08:15:00 Running ["artisan" app:send-event-reminders] ...................................................................... 433ms DONE
⇂ "C:\php8\php.exe" "artisan" app:send-event-reminders > "NUL" 2>&1
php artisan make:Notification EventReminderNotification
EventReminderNotification.php
public function __construct(
public Event $event
)
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->line('Reminder: You have an upcoming event!')
->action('View Event', route('events.show', $this->event->id))
->line(
"This event {$this->event->name} start at {$this->event->start_time}"
);
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'event_id' => $this->event->id,
'event_name' => $this->event->name,
'event_start_time' => $this->event->start_time,
];
}
SendEventReminders.php
public function handle()
{
$events = Event::with('attendees.user')
->whereBetween('start_time', [now(), now()->addDay()])
->get();
$eventCount = $events->count();
$eventLabel = Str::plural('event', $eventCount);
$this->info("Found {$eventCount} {$eventLabel}.");
$events->each(
fn($event) => $event->attendees->each(
fn($attendee) =>
$attendee->user->notify(
new EventReminderNotification(
$event
)
)
)
);
$this->info('Reminder notifications sent successfully!');
}
.env
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=jabulencia@examp.com
MAIL_FROM_NAME="${APP_NAME}"
php artisan app:send-event-reminders
Encountered an error on mailpit
Connection could not be established with host "mailpit:1025": stream_socket_client(): php_network_getaddresses: getaddrinfo for mailpit failed: No such host is known.
I ran this and discovered that it cached the unconfigured settings for mailpit
$ grep -R mailpit * | egrep -v storage
bootstrap/cache/config.php: 'host' => 'mailpit',
vendor/laravel/sail/src/Console/Concerns/InteractsWithDockerComposeServices.php: 'mailpit',
vendor/laravel/sail/src/Console/Concerns/InteractsWithDockerComposeServices.php: protected $defaultServices = ['mysql', 'redis', 'selenium', 'mailpit'];
vendor/laravel/sail/src/Console/Concerns/InteractsWithDockerComposeServices.php: if (in_array('mailpit', $services)) {
vendor/laravel/sail/src/Console/Concerns/InteractsWithDockerComposeServices.php: $environment = preg_replace("/^MAIL_HOST=(.*)/m", "MAIL_HOST=mailpit", $environment);
vendor/laravel/sail/stubs/mailpit.stub:mailpit:
vendor/laravel/sail/stubs/mailpit.stub: image: 'axllent/mailpit:latest'
then I run this to remove cache
php artisan config:clear
Result
php artisan app:send-event-reminders
Found 5 events.
Reminder notifications sent successfully!
I set up the mailpit via docker
https://github.com/jeremyabulencia/setting-up/commit/29e4ad7da295bcc19df182d9dc128a335b5c4931
Redis
Amazon SQS
Database
// create a migration file for jobs table
php artisan queue:table
// create a table for queueing
php artisan migrate
.env
QUEUE_CONNECTION=database
EventReminderNotification.php
class EventReminderNotification extends Notification implements ShouldQueue
run scheduler
php artisan app:send-event-reminders
data to be run is saved on jobs table
// to execute the queued jobs (needs to be running all the time)
php artisan queue:work
EventController.php
public function __construct()
{
$this->middleware('auth:sanctum')->except(['index', 'show']);
$this->middleware('throttle:60,1')
->only(['store','update','destroy']);
$this->authorizeResource(Event::class, 'event');
}
AttendeeController.php
public function __construct()
{
$this->middleware('auth:sanctum')->except(['index', 'show', 'update']);
$this->middleware('throttle:60,1')
->only(['store','destroy']);
$this->authorizeResource(Attendee::class, 'attendee');
}