One of the most common things you'll do with groupBy
is to generate reports. For example, you might want to generate a monthly report for employee work hours (timesheet table):
Let's quickly go through the project setup to see what we have:
Migrations
Schema::create('employees', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps();}); Schema::create('employee_timesheets', function (Blueprint $table) { $table->id(); $table->foreignIdFor(Employee::class)->constrained(); $table->dateTime('start'); $table->dateTime('end')->nullable(); $table->timestamps();});
app/Models/Employee.php
class Employee extends Model{ use HasFactory; protected $fillable = [ 'name' ]; public function employeeTimesheets(): HasMany { return $this->hasMany(EmployeeTimesheet::class); }}
app/Models/EmployeeTimesheet.php
class EmployeeTimesheet extends Model{ use HasFactory; protected $fillable = [ 'employee_id', 'start', 'end', ]; public function employee(): BelongsTo { return $this->belongsTo(Employee::class); }}
database/factories/EmployeeFactory.php
class EmployeeFactory extends Factory{ protected $model = Employee::class; public function definition(): array { return [ 'name' => $this->faker->name(), ]; }}
database/factories/EmployeeTimesheetFactory.php
class EmployeeTimesheetFactory extends Factory{ protected $model = EmployeeTimesheet::class; public function definition(): array { $startTime = fake()->dateTimeBetween('-2 weeks'); return [ 'employee_id' => Employee::factory(), 'start' => $startTime, // Note that here, we take our start time and add 8 hours to it to keep our maximum time per entry to 8 hours 'end' => fake()->dateTimeBetween($startTime, $startTime->format('Y-m-d H:i:s').' + 8 hours'), ]; }}
database/seeders/DatabaseSeeder.php
// ...Employee::factory() ->count(10) ->has(EmployeeTimesheet::factory()->count(14)) ->create();
That's it. Now we have ten employees with 14 timesheets each:
To generate our report, we will use the following code:
app/Http/Controllers/EmployeeTimesheetController.php
use App\Models\EmployeeTimesheet;use DB; // ... public function __invoke(){ $timesheet = EmployeeTimesheet::query() ->select('employee_id') // We will use this to calculate the total hours // TIMEDIFF will give us the difference between two times - end and start // TIME_TO_SEC will convert the time to seconds // SUM will sum all the seconds // SEC_TO_TIME will convert the seconds to time (00:00:00 format) ->addSelect( DB::raw('SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(end, start)))) as `total_hours`'), ) // We will use this to get the earliest start time ->addSelect( DB::raw('min(start) as `min_start`'), ) // We will use this to get the latest end time ->addSelect( DB::raw('max(end) as `max_end`'), ) // We will use this to get the total days ->addSelect( DB::raw('count(distinct(start)) as `total_days`') ) // Here, we apply the filters only to take the current month ->where('start', '>=', now()->startOfMonth()) ->where('end', '<=', now()->endOfMonth()) // Preloading employee details in a relationship ->with(['employee']) // Grouping by employee id ->groupBy('employee_id') // Ordering by total hours ->orderBy('total_hours', 'desc') ->get(); return view('employee.timesheet', ['timesheet' => $timesheet]);}
resources/views/employee/timesheet.blade.php
<table class="table-auto w-full mt-4"> <thead> <tr> <th class="px-4 py-2">Employee</th> <th class="px-4 py-2">Entries</th> <th class="px-4 py-2">Total Time</th> <th class="px-4 py-2">Earliest Date</th> <th class="px-4 py-2">Latest Date</th> </tr> </thead> <tbody> @foreach($timesheet as $entry) <tr> <td class="border px-4 py-2">{{ $entry->employee->name }}</td> <td class="border px-4 py-2">{{ $entry->total_days }}</td> <td class="border px-4 py-2">{{ $entry->total_hours }}</td> <td class="border px-4 py-2">{{ $entry->min_start }}</td> <td class="border px-4 py-2">{{ $entry->max_end }}</td> </tr> @endforeach </tbody></table>
This gives us two queries that are executed:
select `employee_id`, SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(end, start)))) as `total_hours`, min(start) as `min_start`, max(end) as `max_end`, count(distinct (start)) as `total_days`from `employee_timesheets`where `start` >= '2023-08-01 00:00:00' and `end` <= '2023-08-31 23:59:59'group by `employee_id`order by `total_hours` desc select *from `employees`where `employees`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
And the result is: