Final Project
Final Project
Design and implement a complete enterprise-grade Laravel application from scratch
Integrate authentication, authorization, and role-based access control (RBAC)
Build RESTful APIs with proper validation, error handling, and documentation
Implement real-time features using Laravel Broadcasting and WebSockets
Utilize queues and background jobs for asynchronous processing
Write comprehensive tests (Unit, Feature, Integration) and deploy to production
Learn about Project Overview.
Build a comprehensive task management platform similar to Asana or Jira, featuring:
User Management:
Registration, authentication, profile management, MFA
Organizations & Teams:
Multi-tenancy with organization and team hierarchy
Projects & Tasks:
Create projects, assign tasks, set priorities and deadlines
Real-Time Collaboration:
Live updates, notifications, comments, @mentions
File Management:
Upload attachments, image processing, document storage
Activity Tracking:
Audit logs, activity feeds, time tracking
Reporting & Analytics:
Dashboard with charts, team performance metrics
Complete API with Swagger documentation
Email Notifications:
Queued emails for task assignments, mentions, deadlines
Full-text search using Laravel Scout with Meilisearch/Algolia
Technology Stack
Frontend & DevOps:
Learn about Database Schema Design.
users ├── id (bigint, PK) ├── name (string) ├── email (string, unique) ├── email_verified_at (timestamp, nullable) ├── password (string) ├── avatar (string, nullable) ├── mfa_enabled (boolean, default: false) ├── mfa_secret (string, nullable, encrypted) ├── timezone (string, default: 'UTC') ├── last_login_at (timestamp, nullable) ├── remember_token (string, nullable) ├── created_at, updated_at, deleted_at organizations ├── id (bigint, PK) ├── name (string) ├── slug (string, unique) ├── description (text, nullable) ├── logo (string, nullable) ├── owner_id (bigint, FK → users) ├── settings (json, nullable) ├── created_at, updated_at, deleted_at organization_user (pivot) ├── id (bigint, PK) ├── organization_id (bigint, FK → organizations) ├── user_id (bigint, FK → users) ├── role (enum: 'owner', 'admin', 'member', 'guest') ├── joined_at (timestamp) ├── created_at, updated_at teams ├── id (bigint, PK) ├── organization_id (bigint, FK → organizations) ├── name (string) ├── description (text, nullable) ├── created_by (bigint, FK → users) ├── created_at, updated_at, deleted_at team_user (pivot) ├── id (bigint, PK) ├── team_id (bigint, FK → teams) ├── user_id (bigint, FK → users) ├── role (enum: 'leader', 'member') ├── joined_at (timestamp) ├── created_at, updated_at projects ├── id (bigint, PK) ├── organization_id (bigint, FK → organizations) ├── team_id (bigint, FK → teams, nullable) ├── name (string) ├── description (text, nullable) ├── key (string, unique) // e.g., "PROJ" ├── status (enum: 'active', 'archived', 'completed') ├── visibility (enum: 'public', 'private') ├── owner_id (bigint, FK → users) ├── start_date (date, nullable) ├── end_date (date, nullable) ├── created_at, updated_at, deleted_at tasks ├── id (bigint, PK) ├── project_id (bigint, FK → projects) ├── parent_id (bigint, FK → tasks, nullable) // For subtasks ├── title (string) ├── description (text, nullable) ├── task_number (integer) // e.g., PROJ-123 ├── status (enum: 'todo', 'in_progress', 'in_review', 'done', 'blocked') ├── priority (enum: 'low', 'medium', 'high', 'critical') ├── assigned_to (bigint, FK → users, nullable) ├── created_by (bigint, FK → users) ├── due_date (timestamp, nullable) ├── estimated_hours (decimal, nullable) ├── actual_hours (decimal, nullable) ├── completed_at (timestamp, nullable) ├── position (integer) // For ordering ├── created_at, updated_at, deleted_at comments ├── id (bigint, PK) ├── commentable_type (string) // Polymorphic ├── commentable_id (bigint) // Polymorphic ├── user_id (bigint, FK → users) ├── content (text) ├── mentions (json, nullable) // Array of mentioned user IDs ├── created_at, updated_at, deleted_at attachments ├── id (bigint, PK) ├── attachable_type (string) // Polymorphic ├── attachable_id (bigint) // Polymorphic ├── user_id (bigint, FK → users) ├── filename (string) ├── original_name (string) ├── mime_type (string) ├── size (bigint) // bytes ├── path (string) ├── created_at, updated_at activities ├── id (bigint, PK) ├── organization_id (bigint, FK → organizations) ├── user_id (bigint, FK → users, nullable) ├── subject_type (string) // Polymorphic ├── subject_id (bigint) // Polymorphic ├── event (string) // e.g., 'created', 'updated', 'deleted' ├── description (text) ├── properties (json, nullable) // Old/new values ├── created_at notifications ├── id (uuid, PK) ├── type (string) ├── notifiable_type (string) ├── notifiable_id (bigint) ├── data (json) ├── read_at (timestamp, nullable) ├── created_at, updated_at tags ├── id (bigint, PK) ├── organization_id (bigint, FK → organizations) ├── name (string) ├── color (string, default: '#000000') ├── created_at, updated_at taggables (pivot) ├── tag_id (bigint, FK → tags) ├── taggable_type (string) ├── taggable_id (bigint)
Learn about Implementation Roadmap.
1.1 Project Initialization
1.2 Database Setup
1.3 Model Creation
1# Create new Laravel project
2composer create-project laravel/laravel task-management
3cd task-management
4
5# Install dependencies
6composer require laravel/sanctum laravel/horizon laravel/telescope
7composer require spatie/laravel-permission spatie/laravel-activity-log
8composer require spatie/laravel-query-builder
9composer require --dev laravel/pint pestphp/pest
10
11# Install frontend dependencies
12npm install -D tailwindcss postcss autoprefixer
13npm install @inertiajs/vue3 laravel-echo pusher-js
14npm install vue@next @vitejs/plugin-vue1# Configure .env
2DB_CONNECTION=mysql
3DB_HOST=127.0.0.1
4DB_PORT=3306
5DB_DATABASE=task_management
6DB_USERNAME=root
7DB_PASSWORD=
8
9CACHE_DRIVER=redis
10QUEUE_CONNECTION=redis
11SESSION_DRIVER=redis
12
13BROADCAST_DRIVER=pusher
14PUSHER_APP_ID=your_app_id
15PUSHER_APP_KEY=your_app_key
16PUSHER_APP_SECRET=your_app_secret
17PUSHER_APP_CLUSTER=mt1
18
19# Create migrations
20php artisan make:migration create_organizations_table
21php artisan make:migration create_teams_table
22php artisan make:migration create_projects_table
23php artisan make:migration create_tasks_table
24php artisan make:migration create_comments_table
25php artisan make:migration create_attachments_table
26php artisan make:migration create_activities_table
27php artisan make:migration create_tags_table1# Generate models with factories and seeders
2php artisan make:model Organization -mfs
3php artisan make:model Team -mfs
4php artisan make:model Project -mfs
5php artisan make:model Task -mfs
6php artisan make:model Comment -mfs
7php artisan make:model Attachment -mfs
8php artisan make:model Activity -s
9php artisan make:model Tag -mfs2.1 Authentication System
2.2 Role-Based Access Control
1<?php
2declare(strict_types=1);
3
4namespace App\\Http\\Controllers\\Auth;
5
6use App\\Http\\Controllers\\Controller;
7use App\\Http\\Requests\\Auth\\LoginRequest;
8use App\\Models\\User;
9use Illuminate\\Http\\JsonResponse;
10use Illuminate\\Support\\Facades\\Hash;
11use Illuminate\\Validation\\ValidationException;
12
13class AuthController extends Controller
14{
15 public function login(LoginRequest $request): JsonResponse
16 {
17 $user = User::where('email', $request->email)->first();
18
19 if (!$user || !Hash::check($request->password, $user->password)) {
20 throw ValidationException::withMessages([
21 'email' => ['The provided credentials are incorrect.'],
22 ]);
23 }
24
25 // Check if MFA is enabled
26 if ($user->mfa_enabled) {
27 return response()->json([
28 'requires_mfa' => true,
29 'temp_token' => $user->createToken('mfa-temp', ['mfa:verify'])->plainTextToken,
30 ]);
31 }
32
33 $user->update(['last_login_at' => now()]);
34
35 return response()->json([
36 'user' => $user->load('organizations'),
37 'token' => $user->createToken('auth-token')->plainTextToken,
38 ]);
39 }
40
41 public function verifyMfa(Request $request): JsonResponse
42 {
43 $request->validate([
44 'code' => ['required', 'string', 'size:6'],
45 ]);
46
47 $user = $request->user();
48
49 if (!$this->verifyMfaCode($user, $request->code)) {
50 throw ValidationException::withMessages([
51 'code' => ['The provided code is incorrect.'],
52 ]);
53 }
54
55 // Revoke temp token and create full auth token
56 $user->tokens()->where('name', 'mfa-temp')->delete();
57
58 $user->update(['last_login_at' => now()]);
59
60 return response()->json([
61 'user' => $user->load('organizations'),
62 'token' => $user->createToken('auth-token')->plainTextToken,
63 ]);
64 }
65
66 public function logout(Request $request): JsonResponse
67 {
68 $request->user()->currentAccessToken()->delete();
69
70 return response()->json(['message' => 'Logged out successfully']);
71 }
72
73 private function verifyMfaCode(User $user, string $code): bool
74 {
75 $google2fa = app('pragmarx.google2fa');
76
77 return $google2fa->verifyKey(
78 decrypt($user->mfa_secret),
79 $code
80 );
81 }
82}1<?php
2declare(strict_types=1);
3
4namespace App\\Policies;
5
6use App\\Models\\Project;
7use App\\Models\\User;
8
9class ProjectPolicy
10{
11 public function viewAny(User $user): bool
12 {
13 return true;
14 }
15
16 public function view(User $user, Project $project): bool
17 {
18 // Check if user is member of project's organization
19 return $user->organizations()
20 ->where('organizations.id', $project->organization_id)
21 ->exists();
22 }
23
24 public function create(User $user): bool
25 {
26 // User must be at least a member of an organization
27 return $user->organizations()->exists();
28 }
29
30 public function update(User $user, Project $project): bool
31 {
32 // Must be project owner or org admin
33 if ($user->id === $project->owner_id) {
34 return true;
35 }
36
37 return $user->organizations()
38 ->where('organizations.id', $project->organization_id)
39 ->wherePivotIn('role', ['owner', 'admin'])
40 ->exists();
41 }
42
43 public function delete(User $user, Project $project): bool
44 {
45 // Only project owner or org owner can delete
46 if ($user->id === $project->owner_id) {
47 return true;
48 }
49
50 return $user->organizations()
51 ->where('organizations.id', $project->organization_id)
52 ->wherePivot('role', 'owner')
53 ->exists();
54 }
55}
56
57// Register policies in AuthServiceProvider
58protected $policies = [
59 Project::class => ProjectPolicy::class,
60 Task::class => TaskPolicy::class,
61 Comment::class => CommentPolicy::class,
62];3.1 Project & Task Management
3.2 Comment System with Mentions
3.3 File Upload System
1<?php
2declare(strict_types=1);
3
4namespace App\\Http\\Controllers\\Api;
5
6use App\\Http\\Controllers\\Controller;
7use App\\Http\\Requests\\TaskRequest;
8use App\\Models\\Project;
9use App\\Models\\Task;
10use App\\Events\\TaskCreated;
11use App\\Jobs\\SendTaskAssignmentEmail;
12use Illuminate\\Http\\JsonResponse;
13use Spatie\\QueryBuilder\\QueryBuilder;
14use Spatie\\QueryBuilder\\AllowedFilter;
15
16class TaskController extends Controller
17{
18 public function __construct()
19 {
20 $this->authorizeResource(Task::class, 'task');
21 }
22
23 public function index(Project $project): JsonResponse
24 {
25 $tasks = QueryBuilder::for(Task::class)
26 ->where('project_id', $project->id)
27 ->allowedFilters([
28 'status',
29 'priority',
30 AllowedFilter::exact('assigned_to'),
31 AllowedFilter::scope('due_soon'),
32 AllowedFilter::scope('overdue'),
33 ])
34 ->allowedSorts(['created_at', 'due_date', 'priority', 'position'])
35 ->allowedIncludes(['assignee', 'creator', 'comments', 'attachments'])
36 ->defaultSort('-created_at')
37 ->paginate(request('per_page', 20));
38
39 return response()->json($tasks);
40 }
41
42 public function store(TaskRequest $request, Project $project): JsonResponse
43 {
44 $task = $project->tasks()->create([
45 ...$request->validated(),
46 'created_by' => $request->user()->id,
47 'task_number' => $project->getNextTaskNumber(),
48 ]);
49
50 $task->load(['assignee', 'creator']);
51
52 // Dispatch events and jobs
53 event(new TaskCreated($task));
54
55 if ($task->assigned_to) {
56 SendTaskAssignmentEmail::dispatch($task);
57 }
58
59 return response()->json($task, 201);
60 }
61
62 public function update(TaskRequest $request, Project $project, Task $task): JsonResponse
63 {
64 $oldAssignee = $task->assigned_to;
65
66 $task->update($request->validated());
67 $task->load(['assignee', 'creator']);
68
69 // Send email if assignee changed
70 if ($oldAssignee !== $task->assigned_to && $task->assigned_to) {
71 SendTaskAssignmentEmail::dispatch($task);
72 }
73
74 // Broadcast update
75 broadcast(new TaskUpdated($task))->toOthers();
76
77 return response()->json($task);
78 }
79
80 public function destroy(Project $project, Task $task): JsonResponse
81 {
82 $task->delete();
83
84 broadcast(new TaskDeleted($task))->toOthers();
85
86 return response()->json(null, 204);
87 }
88}
89
90// In Task model
91public function scopeDueSoon($query)
92{
93 return $query->where('due_date', '>=', now())
94 ->where('due_date', '<=', now()->addDays(3))
95 ->whereNotIn('status', ['done', 'blocked']);
96}
97
98public function scopeOverdue($query)
99{
100 return $query->where('due_date', '<', now())
101 ->whereNotIn('status', ['done', 'blocked']);
102}1<?php
2declare(strict_types=1);
3
4namespace App\\Http\\Controllers\\Api;
5
6use App\\Http\\Controllers\\Controller;
7use App\\Http\\Requests\\CommentRequest;
8use App\\Models\\Task;
9use App\\Models\\Comment;
10use App\\Events\\CommentAdded;
11use App\\Notifications\\MentionedInComment;
12use Illuminate\\Http\\JsonResponse;
13
14class CommentController extends Controller
15{
16 public function store(CommentRequest $request, Task $task): JsonResponse
17 {
18 $this->authorize('view', $task);
19
20 $mentions = $this->extractMentions($request->content);
21
22 $comment = $task->comments()->create([
23 'user_id' => $request->user()->id,
24 'content' => $request->content,
25 'mentions' => $mentions,
26 ]);
27
28 $comment->load('user');
29
30 // Broadcast comment
31 broadcast(new CommentAdded($comment))->toOthers();
32
33 // Notify mentioned users
34 foreach ($mentions as $userId) {
35 $mentionedUser = User::find($userId);
36 if ($mentionedUser && $mentionedUser->id !== $request->user()->id) {
37 $mentionedUser->notify(new MentionedInComment($comment));
38 }
39 }
40
41 return response()->json($comment, 201);
42 }
43
44 private function extractMentions(string $content): array
45 {
46 // Extract @mentions from content (e.g., @john or @[John Doe](user:123))
47 preg_match_all('/@\\[([^\\]]+)\\]\\(user:(\\d+)\\)/', $content, $matches);
48
49 return array_map('intval', $matches[2] ?? []);
50 }
51
52 public function update(CommentRequest $request, Task $task, Comment $comment): JsonResponse
53 {
54 $this->authorize('update', $comment);
55
56 $comment->update([
57 'content' => $request->content,
58 'mentions' => $this->extractMentions($request->content),
59 ]);
60
61 broadcast(new CommentUpdated($comment))->toOthers();
62
63 return response()->json($comment);
64 }
65
66 public function destroy(Task $task, Comment $comment): JsonResponse
67 {
68 $this->authorize('delete', $comment);
69
70 $comment->delete();
71
72 broadcast(new CommentDeleted($comment))->toOthers();
73
74 return response()->json(null, 204);
75 }
76}1<?php
2declare(strict_types=1);
3
4namespace App\\Http\\Controllers\\Api;
5
6use App\\Http\\Controllers\\Controller;
7use App\\Models\\Task;
8use App\\Models\\Attachment;
9use App\\Jobs\\ProcessUploadedImage;
10use Illuminate\\Http\\Request;
11use Illuminate\\Http\\JsonResponse;
12use Illuminate\\Support\\Facades\\Storage;
13
14class AttachmentController extends Controller
15{
16 public function store(Request $request, Task $task): JsonResponse
17 {
18 $this->authorize('view', $task);
19
20 $request->validate([
21 'file' => ['required', 'file', 'max:10240'], // 10MB max
22 ]);
23
24 $file = $request->file('file');
25
26 $path = $file->store('attachments/' . $task->project_id, 's3');
27
28 $attachment = $task->attachments()->create([
29 'user_id' => $request->user()->id,
30 'filename' => $file->hashName(),
31 'original_name' => $file->getClientOriginalName(),
32 'mime_type' => $file->getMimeType(),
33 'size' => $file->getSize(),
34 'path' => $path,
35 ]);
36
37 // Process images asynchronously
38 if (str_starts_with($file->getMimeType(), 'image/')) {
39 ProcessUploadedImage::dispatch($attachment);
40 }
41
42 return response()->json($attachment, 201);
43 }
44
45 public function destroy(Task $task, Attachment $attachment): JsonResponse
46 {
47 $this->authorize('delete', $attachment);
48
49 Storage::disk('s3')->delete($attachment->path);
50 $attachment->delete();
51
52 return response()->json(null, 204);
53 }
54
55 public function download(Task $task, Attachment $attachment)
56 {
57 $this->authorize('view', $task);
58
59 return Storage::disk('s3')->download(
60 $attachment->path,
61 $attachment->original_name
62 );
63 }
64}
65
66// ProcessUploadedImage Job
67namespace App\\Jobs;
68
69use App\\Models\\Attachment;
70use Illuminate\\Support\\Facades\\Storage;
71use Intervention\\Image\\Facades\\Image;
72
73class ProcessUploadedImage implements ShouldQueue
74{
75 use Dispatchable, InteractsWithQueue, Queueable;
76
77 public function __construct(public Attachment $attachment) {}
78
79 public function handle(): void
80 {
81 $image = Storage::disk('s3')->get($this->attachment->path);
82
83 // Create thumbnail
84 $thumbnail = Image::make($image)->fit(200, 200)->encode();
85
86 $thumbnailPath = str_replace(
87 $this->attachment->filename,
88 'thumb_' . $this->attachment->filename,
89 $this->attachment->path
90 );
91
92 Storage::disk('s3')->put($thumbnailPath, $thumbnail);
93
94 $this->attachment->update([
95 'thumbnail_path' => $thumbnailPath,
96 ]);
97 }
98}4.1 Broadcasting Setup
4.2 Laravel Echo Client Setup
4.3 Real-Time Notifications
1<?php
2declare(strict_types=1);
3
4namespace App\\Events;
5
6use App\\Models\\Task;
7use Illuminate\\Broadcasting\\PrivateChannel;
8use Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;
9
10class TaskUpdated implements ShouldBroadcast
11{
12 use Dispatchable, InteractsWithSockets, SerializesModels;
13
14 public function __construct(public Task $task) {}
15
16 public function broadcastOn(): array
17 {
18 return [
19 new PrivateChannel('project.' . $this->task->project_id),
20 ];
21 }
22
23 public function broadcastAs(): string
24 {
25 return 'task.updated';
26 }
27
28 public function broadcastWith(): array
29 {
30 return [
31 'task' => [
32 'id' => $this->task->id,
33 'title' => $this->task->title,
34 'status' => $this->task->status,
35 'priority' => $this->task->priority,
36 'assigned_to' => $this->task->assigned_to,
37 'updated_at' => $this->task->updated_at->toISOString(),
38 ],
39 ];
40 }
41}
42
43// Channel authorization in routes/channels.php
44Broadcast::channel('project.{projectId}', function ($user, $projectId) {
45 return $user->projects()
46 ->where('projects.id', $projectId)
47 ->exists();
48});
49
50Broadcast::channel('organization.{organizationId}', function ($user, $organizationId) {
51 return $user->organizations()
52 ->where('organizations.id', $organizationId)
53 ->exists();
54});1// resources/js/echo.js
2import Echo from 'laravel-echo';
3import Pusher from 'pusher-js';
4
5window.Pusher = Pusher;
6
7window.Echo = new Echo({
8 broadcaster: 'pusher',
9 key: import.meta.env.VITE_PUSHER_APP_KEY,
10 cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
11 forceTLS: true,
12 encrypted: true,
13 authEndpoint: '/api/broadcasting/auth',
14 auth: {
15 headers: {
16 Authorization: \`Bearer \${localStorage.getItem('auth_token')}\`,
17 },
18 },
19});
20
21// Listen to task updates in Vue component
22export default {
23 mounted() {
24 window.Echo.private(\`project.\${this.projectId}\`)
25 .listen('.task.updated', (event) => {
26 this.updateTaskInList(event.task);
27 })
28 .listen('.task.created', (event) => {
29 this.addTaskToList(event.task);
30 })
31 .listen('.task.deleted', (event) => {
32 this.removeTaskFromList(event.task.id);
33 })
34 .listen('.comment.added', (event) => {
35 this.addComment(event.comment);
36 });
37 },
38
39 beforeUnmount() {
40 window.Echo.leave(\`project.\${this.projectId}\`);
41 },
42};1<?php
2declare(strict_types=1);
3
4namespace App\\Notifications;
5
6use App\\Models\\Task;
7use Illuminate\\Notifications\\Notification;
8use Illuminate\\Notifications\\Messages\\BroadcastMessage;
9use Illuminate\\Notifications\\Messages\\MailMessage;
10
11class TaskAssigned extends Notification implements ShouldQueue
12{
13 use Queueable;
14
15 public function __construct(public Task $task) {}
16
17 public function via($notifiable): array
18 {
19 return ['database', 'broadcast', 'mail'];
20 }
21
22 public function toBroadcast($notifiable): BroadcastMessage
23 {
24 return new BroadcastMessage([
25 'id' => $this->id,
26 'type' => 'task_assigned',
27 'title' => 'New Task Assigned',
28 'message' => "You've been assigned to task: {$this->task->title}",
29 'task' => [
30 'id' => $this->task->id,
31 'title' => $this->task->title,
32 'project_id' => $this->task->project_id,
33 ],
34 'created_at' => now()->toISOString(),
35 ]);
36 }
37
38 public function toDatabase($notifiable): array
39 {
40 return [
41 'task_id' => $this->task->id,
42 'task_title' => $this->task->title,
43 'project_id' => $this->task->project_id,
44 'assigned_by' => $this->task->creator->name,
45 ];
46 }
47
48 public function toMail($notifiable): MailMessage
49 {
50 return (new MailMessage)
51 ->subject('New Task Assigned: ' . $this->task->title)
52 ->line("You've been assigned to a new task.")
53 ->line('Task: ' . $this->task->title)
54 ->line('Project: ' . $this->task->project->name)
55 ->action('View Task', url("/projects/{$this->task->project_id}/tasks/{$this->task->id}"))
56 ->line('Thank you for using our application!');
57 }
58}5.1 Activity Tracking
5.2 Full-Text Search with Scout
5.3 Analytics Dashboard
1<?php
2declare(strict_types=1);
3
4namespace App\\Observers;
5
6use App\\Models\\Task;
7use App\\Models\\Activity;
8
9class TaskObserver
10{
11 public function created(Task $task): void
12 {
13 Activity::create([
14 'organization_id' => $task->project->organization_id,
15 'user_id' => $task->created_by,
16 'subject_type' => Task::class,
17 'subject_id' => $task->id,
18 'event' => 'created',
19 'description' => "created task \"{$task->title}\"",
20 'properties' => [
21 'task' => $task->only(['id', 'title', 'status', 'priority']),
22 ],
23 ]);
24 }
25
26 public function updated(Task $task): void
27 {
28 if (!$task->wasChanged()) {
29 return;
30 }
31
32 $changes = [];
33 foreach ($task->getChanges() as $key => $value) {
34 if (in_array($key, ['updated_at', 'created_at'])) {
35 continue;
36 }
37
38 $changes[$key] = [
39 'old' => $task->getOriginal($key),
40 'new' => $value,
41 ];
42 }
43
44 Activity::create([
45 'organization_id' => $task->project->organization_id,
46 'user_id' => auth()->id(),
47 'subject_type' => Task::class,
48 'subject_id' => $task->id,
49 'event' => 'updated',
50 'description' => "updated task \"{$task->title}\"",
51 'properties' => [
52 'changes' => $changes,
53 ],
54 ]);
55 }
56
57 public function deleted(Task $task): void
58 {
59 Activity::create([
60 'organization_id' => $task->project->organization_id,
61 'user_id' => auth()->id(),
62 'subject_type' => Task::class,
63 'subject_id' => $task->id,
64 'event' => 'deleted',
65 'description' => "deleted task \"{$task->title}\"",
66 'properties' => [
67 'task' => $task->only(['id', 'title', 'status', 'priority']),
68 ],
69 ]);
70 }
71}
72
73// Register observer in AppServiceProvider
74public function boot(): void
75{
76 Task::observe(TaskObserver::class);
77 Project::observe(ProjectObserver::class);
78 Comment::observe(CommentObserver::class);
79}1<?php
2declare(strict_types=1);
3
4namespace App\\Models;
5
6use Laravel\\Scout\\Searchable;
7
8class Task extends Model
9{
10 use Searchable;
11
12 public function toSearchableArray(): array
13 {
14 return [
15 'id' => $this->id,
16 'title' => $this->title,
17 'description' => $this->description,
18 'task_number' => $this->task_number,
19 'status' => $this->status,
20 'priority' => $this->priority,
21 'project_id' => $this->project_id,
22 'project_name' => $this->project->name,
23 'assigned_to_name' => $this->assignee?->name,
24 'created_by_name' => $this->creator->name,
25 ];
26 }
27
28 public function shouldBeSearchable(): bool
29 {
30 return !$this->trashed();
31 }
32}
33
34// Search controller
35namespace App\\Http\\Controllers\\Api;
36
37class SearchController extends Controller
38{
39 public function search(Request $request): JsonResponse
40 {
41 $request->validate([
42 'query' => ['required', 'string', 'min:2'],
43 'type' => ['nullable', 'in:tasks,projects,users'],
44 ]);
45
46 $query = $request->query;
47 $type = $request->type ?? 'tasks';
48
49 $results = match($type) {
50 'tasks' => Task::search($query)
51 ->where('project_id', $request->user()->getCurrentProject()->id)
52 ->get(),
53 'projects' => Project::search($query)
54 ->whereIn('organization_id', $request->user()->organizations->pluck('id'))
55 ->get(),
56 'users' => User::search($query)
57 ->whereIn('id', $request->user()->getCurrentOrganization()->users->pluck('id'))
58 ->get(),
59 };
60
61 return response()->json($results);
62 }
63}1<?php
2declare(strict_types=1);
3
4namespace App\\Http\\Controllers\\Api;
5
6use App\\Models\\Project;
7use App\\Models\\Task;
8use Illuminate\\Http\\JsonResponse;
9use Illuminate\\Support\\Facades\\DB;
10
11class AnalyticsController extends Controller
12{
13 public function projectMetrics(Project $project): JsonResponse
14 {
15 $this->authorize('view', $project);
16
17 $metrics = [
18 'tasks_by_status' => $this->getTasksByStatus($project),
19 'tasks_by_priority' => $this->getTasksByPriority($project),
20 'completion_trend' => $this->getCompletionTrend($project),
21 'team_performance' => $this->getTeamPerformance($project),
22 'overdue_tasks' => $project->tasks()->overdue()->count(),
23 'completion_rate' => $this->getCompletionRate($project),
24 ];
25
26 return response()->json($metrics);
27 }
28
29 private function getTasksByStatus(Project $project): array
30 {
31 return $project->tasks()
32 ->select('status', DB::raw('count(*) as count'))
33 ->groupBy('status')
34 ->pluck('count', 'status')
35 ->toArray();
36 }
37
38 private function getTasksByPriority(Project $project): array
39 {
40 return $project->tasks()
41 ->select('priority', DB::raw('count(*) as count'))
42 ->groupBy('priority')
43 ->pluck('count', 'priority')
44 ->toArray();
45 }
46
47 private function getCompletionTrend(Project $project): array
48 {
49 $last30Days = collect(range(0, 29))->map(function ($daysAgo) use ($project) {
50 $date = now()->subDays($daysAgo)->toDateString();
51
52 $completed = $project->tasks()
53 ->whereDate('completed_at', $date)
54 ->count();
55
56 return [
57 'date' => $date,
58 'completed' => $completed,
59 ];
60 })->reverse()->values();
61
62 return $last30Days->toArray();
63 }
64
65 private function getTeamPerformance(Project $project): array
66 {
67 return $project->tasks()
68 ->whereNotNull('assigned_to')
69 ->select('assigned_to')
70 ->selectRaw('count(*) as total_tasks')
71 ->selectRaw('sum(case when status = "done" then 1 else 0 end) as completed_tasks')
72 ->selectRaw('avg(case when completed_at is not null then timestampdiff(hour, created_at, completed_at) end) as avg_completion_time')
73 ->with('assignee:id,name,email,avatar')
74 ->groupBy('assigned_to')
75 ->get()
76 ->map(function ($stat) {
77 return [
78 'user' => $stat->assignee,
79 'total_tasks' => $stat->total_tasks,
80 'completed_tasks' => $stat->completed_tasks,
81 'completion_rate' => round(($stat->completed_tasks / $stat->total_tasks) * 100, 2),
82 'avg_completion_time_hours' => round($stat->avg_completion_time, 2),
83 ];
84 });
85 }
86
87 private function getCompletionRate(Project $project): float
88 {
89 $total = $project->tasks()->count();
90 if ($total === 0) {
91 return 0;
92 }
93
94 $completed = $project->tasks()->where('status', 'done')->count();
95
96 return round(($completed / $total) * 100, 2);
97 }
98}6.1 Feature Tests
6.2 Unit Tests
1<?php
2
3use App\\Models\\User;
4use App\\Models\\Project;
5use App\\Models\\Task;
6
7test('user can create a task in their project', function () {
8 $user = User::factory()->create();
9 $project = Project::factory()->create();
10 $project->organization->users()->attach($user, ['role' => 'member']);
11
12 $response = $this->actingAs($user)->postJson("/api/projects/{$project->id}/tasks", [
13 'title' => 'Test Task',
14 'description' => 'Task description',
15 'status' => 'todo',
16 'priority' => 'high',
17 ]);
18
19 $response->assertStatus(201)
20 ->assertJsonStructure([
21 'id',
22 'title',
23 'description',
24 'status',
25 'priority',
26 'task_number',
27 'created_by',
28 ]);
29
30 $this->assertDatabaseHas('tasks', [
31 'title' => 'Test Task',
32 'project_id' => $project->id,
33 'created_by' => $user->id,
34 ]);
35});
36
37test('user cannot create task in project they dont belong to', function () {
38 $user = User::factory()->create();
39 $project = Project::factory()->create();
40
41 $response = $this->actingAs($user)->postJson("/api/projects/{$project->id}/tasks", [
42 'title' => 'Test Task',
43 ]);
44
45 $response->assertStatus(403);
46});
47
48test('task assignment sends notification', function () {
49 Notification::fake();
50
51 $creator = User::factory()->create();
52 $assignee = User::factory()->create();
53 $project = Project::factory()->create();
54
55 $project->organization->users()->attach([$creator->id, $assignee->id], ['role' => 'member']);
56
57 $task = Task::factory()->create([
58 'project_id' => $project->id,
59 'created_by' => $creator->id,
60 'assigned_to' => $assignee->id,
61 ]);
62
63 Notification::assertSentTo($assignee, TaskAssigned::class);
64});1<?php
2
3use App\\Models\\Task;
4use App\\Models\\Project;
5
6test('task number is auto-incremented per project', function () {
7 $project = Project::factory()->create();
8
9 $task1 = Task::factory()->create(['project_id' => $project->id]);
10 $task2 = Task::factory()->create(['project_id' => $project->id]);
11 $task3 = Task::factory()->create(['project_id' => $project->id]);
12
13 expect($task1->task_number)->toBe(1);
14 expect($task2->task_number)->toBe(2);
15 expect($task3->task_number)->toBe(3);
16});
17
18test('task scope overdue returns only overdue tasks', function () {
19 $project = Project::factory()->create();
20
21 Task::factory()->create([
22 'project_id' => $project->id,
23 'due_date' => now()->subDays(2),
24 'status' => 'todo',
25 ]);
26
27 Task::factory()->create([
28 'project_id' => $project->id,
29 'due_date' => now()->addDays(2),
30 'status' => 'todo',
31 ]);
32
33 Task::factory()->create([
34 'project_id' => $project->id,
35 'due_date' => now()->subDays(1),
36 'status' => 'done',
37 ]);
38
39 $overdueTasks = Task::overdue()->get();
40
41 expect($overdueTasks)->toHaveCount(1);
42});7.1 Production Optimization
7.2 CI/CD Pipeline (GitHub Actions)
1# Cache configuration
2php artisan config:cache
3php artisan route:cache
4php artisan view:cache
5php artisan event:cache
6
7# Optimize autoloader
8composer install --optimize-autoloader --no-dev
9
10# Build frontend assets
11npm run build
12
13# Run migrations
14php artisan migrate --force
15
16# Start queue workers with Supervisor
17sudo supervisorctl restart laravel-worker:*
18
19# Start Horizon
20php artisan horizon:publish
21sudo supervisorctl restart laravel-horizon1name: Laravel CI/CD
2
3on:
4 push:
5 branches: [main, staging]
6 pull_request:
7 branches: [main]
8
9jobs:
10 test:
11 runs-on: ubuntu-latest
12
13 services:
14 mysql:
15 image: mysql:8.0
16 env:
17 MYSQL_ROOT_PASSWORD: password
18 MYSQL_DATABASE: task_management_test
19 options: >-
20 --health-cmd="mysqladmin ping"
21 --health-interval=10s
22 --health-timeout=5s
23 --health-retries=3
24
25 redis:
26 image: redis:7
27 options: >-
28 --health-cmd="redis-cli ping"
29 --health-interval=10s
30 --health-timeout=5s
31 --health-retries=3
32
33 steps:
34 - uses: actions/checkout@v4
35
36 - name: Setup PHP
37 uses: shivammathur/setup-php@v2
38 with:
39 php-version: '8.2'
40 extensions: mbstring, pdo, pdo_mysql, redis
41 coverage: xdebug
42
43 - name: Install Dependencies
44 run: composer install --prefer-dist --no-progress
45
46 - name: Copy .env
47 run: cp .env.testing .env
48
49 - name: Generate Key
50 run: php artisan key:generate
51
52 - name: Run Migrations
53 run: php artisan migrate --force
54
55 - name: Run Tests
56 run: php artisan test --coverage --min=80
57
58 - name: Upload Coverage
59 uses: codecov/codecov-action@v3
60
61 deploy:
62 needs: test
63 runs-on: ubuntu-latest
64 if: github.ref == 'refs/heads/main'
65
66 steps:
67 - name: Deploy to Production
68 uses: appleboy/ssh-action@v1.0.0
69 with:
70 host: \${{ secrets.SSH_HOST }}
71 username: \${{ secrets.SSH_USERNAME }}
72 key: \${{ secrets.SSH_PRIVATE_KEY }}
73 script: |
74 cd /var/www/task-management
75 git pull origin main
76 composer install --optimize-autoloader --no-dev
77 npm ci --production
78 npm run build
79 php artisan migrate --force
80 php artisan config:cache
81 php artisan route:cache
82 php artisan view:cache
83 sudo supervisorctl restart laravel-worker:*
84 sudo systemctl reload php8.2-fpm nginxLearn about Hands-On Exercises.
Set up the foundation of the task management system with database schema and basic authentication.
Requirements:
Create all database migrations for users, organizations, teams, projects, tasks
Set up Laravel Sanctum authentication
Create User, Organization, and Project models with relationships
Implement registration and login API endpoints
Add basic authorization policies for projects
View Solution
Implement complete CRUD for tasks with real-time broadcasting and file attachments.
Requirements:
Create TaskController with full CRUD operations
Implement task filtering (status, priority, assignee)
Add file attachment support with S3 storage
Set up broadcasting for real-time task updates
Configure Laravel Echo on frontend
Add task assignment email notifications via queue
View Solution
Bearer ${token}, }, }, }); # Step 5: Listen to events in Vue component window.Echo.private(project.${projectId}) .listen('.task.updated', (e) => { console.log('Task updated:', e.task); this.updateTaskInList(e.task); }); # Test with multiple browser windows to see real-time updatesDeploy the full application to production with monitoring, testing, and CI/CD pipeline.
Requirements:
Write comprehensive tests (80%+ coverage) - Unit, Feature, Integration
Set up GitHub Actions CI/CD pipeline
Configure production server (Nginx, PHP-FPM, MySQL, Redis)
Implement zero-downtime deployment script
Set up Laravel Horizon for queue monitoring
Configure Sentry for error tracking
Add database backups with S3 upload
Implement health checks and monitoring
View Solution
A) To store user passwords
B) To cache organization data
C) To store additional relationship data like role and join date
D) To prevent SQL injection attacks
Show Answer
Pivot tables in many-to-many relationships can store additional metadata about the relationship.
In this case, the organization_user pivot table stores the user's role within the organization
(owner, admin, member, guest) and when they joined. This allows for fine-grained access control
and organizational management without requiring separate models.
A) To make notifications synchronous
B) To prevent notifications from being sent
C) To send notifications asynchronously via the queue, improving response time
D) To encrypt notification data
Show Answer
Implementing ShouldQueue on notifications pushes them to the queue system for asynchronous processing.
This is crucial for user experience because sending emails, SMS, or push notifications can take several
seconds. By queueing these operations, the API response returns immediately while notifications are
processed in the background by queue workers, significantly improving application responsiveness.
A) It makes queries faster
B) It allows data recovery and maintains referential integrity while appearing deleted
C) It reduces database storage
D) It's required by Laravel
Show Answer
Soft deletes mark records as deleted by setting a timestamp in the deleted_at column instead of physically
removing them from the database. This provides several benefits: (1) Data can be easily restored if deleted
by mistake, (2) Historical data and audit trails remain intact, (3) Foreign key relationships don't break,
(4) Activity logs and analytics can still reference deleted items. Laravel automatically excludes soft-deleted
records from queries unless you explicitly include them with withTrashed() or onlyTrashed().
A) To manage database migrations
B) To provide a dashboard for monitoring and configuring Redis queues
C) To handle user authentication
D) To serve static files
Show Answer
Laravel Horizon provides a beautiful dashboard and code-driven configuration for Redis queues. It offers
real-time monitoring of queue metrics including job throughput, runtime, and failure rates. Horizon also
provides auto-scaling capabilities, allowing you to configure how many workers should process jobs based
on queue load. It includes features like job tagging for better organization, metrics tracking, retry
management, and failed job inspection. This is essential for production environments where you need
visibility into your queue system's health and performance.
A) It's faster than Eloquent
B) It provides secure, standardized filtering, sorting, and including of relationships via URL parameters
C) It automatically creates database indexes
D) It's required for real-time features
Show Answer
Spatie's Query Builder package allows clients to control filtering, sorting, and relationship loading
through URL parameters (e.g., ?filter[status]=todo&sort=-created_at&include=assignee,comments) while
maintaining security through whitelisted allowed filters and sorts. This eliminates the need to create
separate endpoints for different filter combinations, reduces controller code, and provides a consistent
API interface. It prevents SQL injection and unauthorized data access by only allowing explicitly defined
filters, sorts, and includes. This is especially valuable for complex applications where users need
flexible querying capabilities.
Learn about Production Best Practices.
Learn about Security.
Learn about Performance.
Learn about Code Quality.
Learn about DevOps.
Learn about Database.
Learn about Real-Time Features.
Learn about API Design.
Learn about Testing.
Learn about Additional Resources.
Laravel 11 Documentation
Complete official documentation
Laravel Sanctum
API authentication
Laravel Horizon
Queue monitoring and management
Laravel Broadcasting
Real-time event broadcasting
Pest PHP Testing Framework
Modern PHP testing
Spatie Laravel Permission
Role and permission management
Spatie Laravel Query Builder
Flexible API filtering and sorting
Spatie Laravel Backup
Automated backup solution
Sentry for Laravel
Error tracking and monitoring
Inertia.js
Build SPAs with Laravel backend
Laravel Octane
Supercharge performance with Swoole/RoadRunner
Laravel Scout
Full-text search with Algolia/Meilisearch
Laravel Telescope
Application debugging and insights
Laravel Echo Documentation
Client-side broadcasting library
Laravel WebSockets
Self-hosted Pusher alternative
Premium video tutorials
Laravel News
Latest Laravel updates and tutorials
Awesome Laravel
Curated list of Laravel resources
Laravel Bootcamp
Official hands-on tutorial
Stack Overflow - Laravel
Community Q&A
You've completed the PHP Laravel Framework course! You now have the skills to build
production-ready, enterprise-grade web applications with Laravel.
Continue practicing by building your own projects, contributing to open source, and exploring
advanced Laravel features like Octane, Vapor, and Livewire.
Your progress is saved in this browser, on this device only.