Last active
February 13, 2025 10:13
-
-
Save ssr765/39c0966410c8a1bfaf1488d1cf676ff1 to your computer and use it in GitHub Desktop.
This is a Laravel trait I created to implement dynamic multi-model pagination. It supports soft deletes and eager loading. I'm looking to improve my Laravel/PHP skills, so feel free to leave any comments or suggestions on how improve it!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Traits; | |
| use App\Models\User; | |
| use Illuminate\Http\Request; | |
| use Illuminate\Pagination\LengthAwarePaginator; | |
| use Illuminate\Support\Facades\DB; | |
| trait MultiModelPagination | |
| { | |
| /** | |
| * Creates a paginator with the most recent records of the specified tables ($models). | |
| * | |
| * The logic on this code makes a total of 2 + 0(N models) queries: | |
| * - First, it retrieves the record references. | |
| * - Second, it counts the total number of records. | |
| * - Then, it makes a query to retrieve the data for each model. If references for a model were | |
| * found, it makes the database call; otherwise, no query is made for that model. | |
| * | |
| * The queries amount may change if loads are being used. | |
| * | |
| * @param \Illuminate\Http\Request $request The request instance. | |
| * @param array<string, array{ | |
| * model: class-string, | |
| * resource?: class-string, | |
| * loads?: array<string> | |
| * }> $models An associative array where: | |
| * | |
| * - **Key (`string`)**: The model name. | |
| * - **Value (`array`)**: Contains: | |
| * - `model` (**class-string**) → The fully qualified class name of the model. | |
| * - `resource` (**class-string**, optional) → The resource class for the model (required | |
| * if `$useResources` is `true`). | |
| * - `loads` (**array<string>**, optional) → The relationships to eager load. | |
| * @param int $pageSize The number of records per page for pagination. Defaults to 20. | |
| * @param \App\Models\User | null $user The user instance for retrieving the records. | |
| * @param 'only'|'any'|'no' $trashed The way it manages soft deletes: | |
| * - only: Includes only trashed records (soft deletes must be configured). | |
| * - any (default): Includes all records (soft deletes are not required). | |
| * - no: Includes non-trashed records (soft deletes must be configured). | |
| * @param bool $useResources If should use the model resources. Defaults to false. | |
| * @param string $typeColumnName The type column name, overridable if the original model really | |
| * has a type column. | |
| * | |
| * | |
| * @return \Illuminate\Pagination\LengthAwarePaginator The paginator for the mixed model | |
| * instances. | |
| * | |
| * @throws Illuminate\Database\QueryException If $trashed is ``'only'`` or ``'no'`` and soft | |
| * deletes are not configured, or id/user_id/created_at are missing. | |
| * @throws ErrorException If $useResources is true and some resource is missing in $models. | |
| */ | |
| protected function createMultiPaginator( | |
| Request $request, | |
| array $models, | |
| int $pageSize = 20, | |
| ?User $user = null, | |
| string $trashed = 'any', | |
| bool $useResources = false, | |
| string $typeColumnName = 'type' | |
| ) { | |
| $currentPage = LengthAwarePaginator::resolveCurrentPage(); | |
| /* Get the models references relative by the current page. */ | |
| // Get any post type id and created_at. | |
| $queries = []; | |
| foreach ($models as $modelName => $modelData) { | |
| $model = $modelData['model']; | |
| $query = $model::select(['id', 'created_at', DB::raw("'$modelName' as $typeColumnName")]); | |
| if ($trashed === 'only') { | |
| $query->addSelect('deleted_at'); | |
| $query->onlyTrashed(); | |
| } else if ($trashed === 'no') { | |
| $query->withoutTrashed(); | |
| } else if ($trashed === 'any') { | |
| $query->addSelect('deleted_at'); | |
| $query->withTrashed(); | |
| } | |
| if ($user) { | |
| $query->where('user_id', '=', $user->id); | |
| } | |
| $queries[] = $query; | |
| } | |
| // Union on all the querys. | |
| $finalQuery = array_shift($queries); | |
| foreach ($queries as $query) { | |
| $finalQuery->unionAll($query); | |
| } | |
| // Ordering and page management. | |
| if ($trashed === 'only') { | |
| $finalQuery->orderBy('deleted_at', 'desc'); | |
| } else { | |
| $finalQuery->orderBy('created_at', 'desc'); | |
| } | |
| $finalQuery->skip(($currentPage - 1) * $pageSize); | |
| $finalQuery->limit($pageSize); | |
| $references = $finalQuery->get(); | |
| /* Get the total count of all the models. */ | |
| $queries = []; | |
| foreach ($models as $modelName => $modelData) { | |
| $model = $modelData['model']; | |
| $query = $model::select(DB::raw('COUNT(*) as c')); | |
| if ($trashed === 'only') { | |
| $query->onlyTrashed(); | |
| } else if ($trashed === 'no') { | |
| $query->withoutTrashed(); | |
| } else if ($trashed === 'any') { | |
| $query->withTrashed(); | |
| } | |
| if ($user) { | |
| $query->where('user_id', '=', $user->id); | |
| } | |
| $queries[] = $query; | |
| } | |
| $finalQuery = array_shift($queries); | |
| foreach ($queries as $query) { | |
| $finalQuery->unionAll($query); | |
| } | |
| $count = $finalQuery->sum('c'); | |
| /* Get the models data. */ | |
| $data = []; | |
| foreach ($models as $modelName => $modelData) { | |
| $model = $modelData['model']; | |
| $loads = $modelData['loads'] ?? []; | |
| $idList = []; | |
| foreach ($references as $ref) { | |
| if ($ref->$typeColumnName === $modelName) { | |
| $idList[] = $ref->id; | |
| } | |
| } | |
| $query = $model::select(['*', DB::raw("'$modelName' as $typeColumnName")])->with($loads)->whereIn('id', $idList); | |
| if ($trashed === 'only') { | |
| $query->onlyTrashed(); | |
| } else if ($trashed === 'no') { | |
| $query->withoutTrashed(); | |
| } else if ($trashed === 'any') { | |
| $query->withTrashed(); | |
| } | |
| // Avoid running get() on null when there are no registers on some model. | |
| if (!empty($idList)) { | |
| $data = [ | |
| ...$data, | |
| ...$query->get() | |
| ]; | |
| } | |
| } | |
| /* Previously, we took the most recent data relative to the current page, we can sort it | |
| * safely. */ | |
| usort($data, function ($a, $b) use ($trashed) { | |
| if ($trashed === 'only') { | |
| $date_a = strtotime($a->deleted_at); | |
| $date_b = strtotime($b->deleted_at); | |
| } else { | |
| $date_a = strtotime($a->created_at); | |
| $date_b = strtotime($b->created_at); | |
| } | |
| return $date_b - $date_a; | |
| }); | |
| /* Convert the data to a resource. */ | |
| if ($useResources) { | |
| foreach ($data as $i => $record) { | |
| $resource = $models[$record->$typeColumnName]['resource']; | |
| $data[$i] = new $resource($record); | |
| } | |
| } | |
| // Instanciate the pagination. | |
| $paginator = new LengthAwarePaginator($data, $count, $pageSize, $currentPage, [ | |
| 'path' => $request->url(), | |
| 'query' => $request->query(), | |
| ]); | |
| return $paginator; | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Revision: changed how the trait manages the soft deletes, when developing the trait, I was using DB::table (that includes soft deletes), so before this revision, all soft deletes were ignored. Now works properly.