Best Way to Implement Repository Pattern in Laravel
10 min readJan 26, 2025
Introduction
- Briefly introduce the concept of the repository pattern and its purpose.
- Explain why using repositories can help in abstracting the data access logic and make the application more maintainable, testable, and scalable.
1. Create the Base Repository and IBase Repository
You already have a BaseRepository
class. Discuss how it implements common methods for CRUD operations, and explain the purpose of the IBaseRepository
interface.
- Methods included:
create
,findById
,findFirstByConditions
,findAll
,findAllWithPagination
, etc. - Advantages: Keeps your repository code DRY (Don’t Repeat Yourself) by centralizing common database operations.
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
interface IBaseRepository
{
/**
* Create a new resource.
*
* @param array $attributes
* @return Model
*/
public function create(array $attributes): Model;
/**
* Find a resource by its ID.
*
* @param int|string $id
* @param string[] $columns
* @return Model
*/
public function findById(int|string $id, array $columns = ['*']): ?Model;
/**
* Find a resource by its conditions.
*
* @param array $conditions
* @param array $columns
* @return Model|null
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findFirstByConditions(array $conditions, array $columns = ['*']): ?Model;
/**
* Get all resources with optional filters.
*
* @param array $conditions
* @param string[] $columns
* @return Collection|array
*/
public function findAll(array $conditions = [], array $columns = ['*']): Collection|array;
/**
* Get resources with pagination based on filters.
*
* @param int $limit
* @param array $conditions
* @param string[] $columns
* @return LengthAwarePaginator
*/
public function findAllWithPagination(array $conditions = [], array $columns = ['*'], int $limit): LengthAwarePaginator;
/**
* Get a limited set of resources with optional filters.
*
* @param int $limit
* @param array $conditions
* @param string[] $columns
* @return Collection
*/
public function findByLimit(int $limit, array $conditions = [], array $columns = ['*']): Collection;
/**
* Update a resource by its ID.
*
* @param array $conditions
* @param array $attributes
* @return int
*/
public function update(array $conditions, array $attributes): int;
/**
* Update or create a resource.
*
* @param array $conditions
* @param array $attributes
* @return mixed
*/
public function updateOrCreate(array $conditions, array $attributes): mixed;
/**
* Delete a resource by its ID.
*
* @param int|string $id
* @return int
*/
public function deleteById(int|string $id): int;
/**
* Delete multiple resources based on conditions.
*
* @param array $conditions
* @return int The number of deleted records.
*/
public function deleteAll(array $conditions): int;
}
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
abstract class BaseRepository implements IBaseRepository
{
protected $model;
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Create a new resource.
*
* @param array $attributes
* @return Model
*/
public function create(array $attributes): Model
{
return $this->model->create($attributes);
}
/**
* Find a resource by its ID.
*
* @param int|string $id
* @param array $columns
* @return Model|null
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findById(int|string $id, array $columns = ['*']): ?Model
{
return $this->model->findOrFail($id, $columns);
}
/**
* Find a resource by its conditions.
*
* @param array $conditions
* @param array $columns
* @return Model|null
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findFirstByConditions(array $conditions, array $columns = ['*']): ?Model
{
return $this->model->where($conditions)->select($columns)->first();
}
/**
* Get all resources with optional filters.
*
* @param array $conditions
* @param array $columns
* @return Collection|array
*/
public function findAll(array $conditions = [], array $columns = ['*']): Collection|array
{
return $this->model->where($conditions)->get($columns);
}
/**
* Get resources with pagination based on filters.
*
* @param int $limit
* @param array $conditions
* @param array $columns
* @return LengthAwarePaginator
*/
public function findAllWithPagination(array $conditions = [], array $columns = ['*'], int $limit): LengthAwarePaginator
{
return $this->model->where($conditions)->paginate($limit);
}
/**
* Get a limited set of resources with optional filters.
*
* @param int $limit
* @param array $conditions
* @param array $columns
* @return Collection
*/
public function findByLimit(int $limit, array $conditions = [], array $columns = ['*']): Collection
{
return $this->model->where($conditions)->limit($limit)->get($columns);
}
/**
* Update a resource by its ID.
*
* @param array $conditions
* @param array $attributes
* @return int
*/
public function update(array $conditions, array $attributes): int
{
return $this->model->where($conditions)->update($attributes);
}
/**
* Update or create a resource.
*
* @param array $conditions
* @param array $attributes
* @return Model
*/
public function updateOrCreate(array $conditions, array $attributes): Model
{
return $this->model->updateOrCreate($conditions, $attributes);
}
/**
* Delete a resource by its ID.
*
* @param int|string $id
* @return int
*/
public function deleteById(int|string $id): int
{
return $this->model->destroy($id);
}
/**
* Delete multiple resources based on conditions.
*
* @param array $conditions
* @return int The number of deleted records.
*/
public function deleteAll(array $conditions): int
{
return $this->model->where($conditions)->delete();
}
}
2. Create a Repository for Specific Models
- Show how to extend the
BaseRepository
for specific models, such asUserRepository
or any other model. - Demonstrate custom methods such as fetching users with specific roles or any related relations if needed.
<?php
namespace App\Repositories\User;
use App\Repositories\IBaseRepository;
use Illuminate\Database\Eloquent\Collection;
interface IUserRepository extends IBaseRepository
{
public function getUserWithRole(array $conditions = [], array $columns = ['*']): Collection|array;
}
<?php
namespace App\Repositories\User;
use App\Models\User;
use App\Repositories\BaseRepository;
use Illuminate\Database\Eloquent\Collection;
class UserRepository extends BaseRepository implements IUserRepository
{
public function __construct(User $model)
{
parent::__construct($model);
}
/**
* Get all User resources with optional filters.
*
* @param array $conditions
* @param array $columns
* @return Collection|array
*/
public function getUserWithRole(array $conditions = [], array $columns = ['*']): Collection|array
{
return $this->model
->with(['role'])
->where($conditions)->get($columns);
}
}
3. Service Layer for Business Logic
- Introduce the Service Layer to further abstract the business logic from controllers. This ensures that the controller remains thin.
- Show how a service class interacts with the repository to execute business logic.
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
interface IBaseService
{
/**
* Create a new resource.
*
* @param array $attributes
* @return mixed
*/
public function create(array $attributes): mixed;
/**
* Find a resource by its ID.
*
* @param int|string $id
* @param string[] $columns
* @return mixed
*/
public function findById(int|string $id, array $columns = ['*']): mixed;
/**
* Find a resource by its conditions.
*
* @param array $conditions
* @param string[] $columns
* @return mixed
*/
public function findFirstByConditions(array $conditions, array $columns = ['*']): mixed;
/**
* Get all resources with optional filters.
*
* @param array $conditions
* @param string[] $columns
* @return Collection|array
*/
public function findAll(array $conditions = [], array $columns = ['*']): Collection|array;
/**
* Get resources with pagination based on filters.
*
* @param int $limit
* @param array $conditions
* @param string[] $columns
* @return mixed
*/
public function findAllWithPagination(array $conditions = [], array $columns = ['*'], int $limit): mixed;
/**
* Get a limited set of resources with optional filters.
*
* @param int $limit
* @param array $conditions
* @param string[] $columns
* @return mixed
*/
public function findByLimit(int $limit, array $conditions = [], array $columns = ['*']): mixed;
/**
* Update a resource by its ID.
*
* @param array $conditions
* @param array $attributes
* @return mixed
*/
public function update(array $conditions, array $attributes): mixed;
/**
* Update or create a resource.
*
* @param array $conditions
* @param array $attributes
* @return mixed
*/
public function updateOrCreate(array $conditions, array $attributes): mixed;
/**
* Delete a resource by its ID.
*
* @param int|string $id
* @return mixed
*/
public function deleteById(int|string $id): mixed;
/**
* Delete multiple resources based on conditions.
*
* @param array $conditions
* @return int The number of deleted records.
*/
public function deleteAll(array $conditions): int;
/**
* Get a list of resources for DataTables.
*
* @param Request $request
* @return JsonResponse|array
*/
public function dataTableList(Request $request): JsonResponse|array;
}
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
abstract class BaseService implements IBaseService
{
private object $modelRepository;
public function __construct($modelRepository)
{
$this->modelRepository = $modelRepository;
}
/**
* Create a new resource.
*
* @param array $attributes
* @return mixed
*/
public function create(array $attributes): mixed
{
return $this->modelRepository->create($attributes);
}
/**
* Find a resource by its ID.
*
* @param int|string $id
* @param string[] $columns
* @return mixed
*/
public function findById(int|string $id, $columns = ['*']): mixed
{
return $this->modelRepository->findById($id, $columns);
}
/**
* Find a resource by its conditions.
*
* @param array $conditions
* @param string[] $columns
* @return mixed
*/
public function findFirstByConditions(array $conditions, array $columns = ['*']): mixed
{
return $this->modelRepository->findFirstByConditions($conditions, $columns);
}
/**
* Get all resources with optional filters.
*
* @param array $conditions
* @param string[] $columns
* @return Collection|array
*/
public function findAll(array $conditions = [], array $columns = ['*']): Collection|array
{
return $this->modelRepository->findAll($conditions, $columns);
}
/**
* Get resources with pagination based on filters.
*
* @param int $limit
* @param array $conditions
* @param string[] $columns
* @return mixed
*/
public function findAllWithPagination(array $conditions = [], array $columns = ['*'], int $limit): mixed
{
return $this->modelRepository->findAllWithPagination($conditions, $columns, $limit);
}
/**
* Get a limited set of resources with optional filters.
*
* @param int $limit
* @param array $conditions
* @param string[] $columns
* @return mixed
*/
public function findByLimit(int $limit, array $conditions = [], array $columns = ['*']): mixed
{
return $this->modelRepository->findByLimit($limit, $conditions, $columns);
}
/**
* Update a resource by its ID.
*
* @param array $conditions
* @param array $attributes
* @return mixed
*/
public function update(array $conditions, array $attributes): mixed
{
return $this->modelRepository->update($conditions, $attributes);
}
/**
* Update or create a resource.
*
* @param array $conditions
* @param array $attributes
* @return mixed
*/
public function updateOrCreate(array $conditions, array $attributes): mixed
{
return $this->modelRepository->updateOrCreate($conditions, $attributes);
}
/**
* Delete a resource by its ID.
*
* @param int|string $id
* @return mixed
*/
public function deleteById(int|string $id): mixed
{
return $this->modelRepository->deleteById($id);
}
/**
* Delete multiple resources based on conditions.
*
* @param array $conditions
* @return int The number of deleted records.
*/
public function deleteAll(array $conditions): int
{
return $this->modelRepository->deleteAll($conditions);
}
/**
* Get a list of resources for DataTables.
*
* @param Request $request
* @return JsonResponse|array
*/
public function dataTableList(Request $request): JsonResponse|array
{
return []; // Implement DataTables logic here.
}
}
<?php
namespace App\Services\User;
use App\Services\IBaseService;
interface IUserService extends IBaseService
{
public function getUserData();
}
<?php
namespace App\Services\User;
use App\Repositories\User\IUserRepository;
use App\Services\BaseService;
use Exception;
use Illuminate\Http\JsonResponse;
use Yajra\DataTables\Facades\DataTables;
class UserService extends BaseService implements IUserService
{
public function __construct(private IUserRepository $userRepository)
{
parent::__construct($userRepository);
}
/**
* Retrieve user data for DataTables.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getUserData(): JsonResponse
{
try {
$data = $this->userRepository->getUserWithRole([], ['id', 'name', 'email', 'created_at', 'role_id']);
return DataTables::of($data)
->addColumn('role_name', function($data) {
return $data->role->name;
})
->addColumn('action', function($data){
return $data->id;
})->toJson();
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Could not retrieve data. Please try again later.' . $e->getMessage(),
]);
}
}
}
4. Bind Repository and Service in provider more optimized way
<?php
namespace App\Providers;
use App\Repositories\User\IUserRepository;
use App\Repositories\User\UserRepository;
use App\Services\User\IUserService;
use App\Services\User\UserService;
use Illuminate\Support\ServiceProvider;
class ServiceRepositoryServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$repositories = [
IUserRepository::class => UserRepository::class,
];
$services = [
IUserService::class => UserService::class,
];
$bindings = array_merge($repositories, $services);
$this->bindServiceRepositories($bindings);
}
/**
* Helper function to bind repository interfaces to their implementations
*/
protected function bindServiceRepositories(array $repositories): void
{
foreach ($repositories as $interface => $implementation) {
$this->app->bind($interface, $implementation);
}
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}
5. Call Service into Controller
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\User\IUserService;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use App\Http\Requests\CreateUserRequest;
use App\Http\Requests\UpdateUserRequest;
use Illuminate\Http\JsonResponse;
use Exception;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
public function __construct(private IUserService $userService)
{
}
/**
* Display a listing of the resource.
*
* @return View
*/
public function index(): View
{
return view('admin.user.index')->with([]);
}
/**
* Get user data for DataTables.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getDatatables(Request $request): JsonResponse
{
if ($request->ajax()) {
return $this->userService->getUserData();
}
return response()->json([
'success' => false,
'message' => 'Invalid request.',
]);
}
/**
* Show the form for creating a new resource.
*
* @return View
*/
public function create(): View
{
return view('admin.user.create')->with([]);
}
/**
* Store a newly created resource in storage.
*
* @param UserRequest $request
* @return RedirectResponse
*/
public function store(CreateUserRequest $request): RedirectResponse
{
DB::beginTransaction();
try {
$response = $this->userService->create($request->all());
DB::commit();
return redirect()->back()->with('success', __('user_module.create_list_edit.user') . __('standard_curd_common_label.success'));
} catch (Exception $e) {
DB::rollBack();
return redirect()->back()->with('error', __('standard_curd_common_label.error'));
}
return redirect()->back()->with('error', __('standard_curd_common_label.error'));
}
/**
* Display the specified resource.
*
* @param string $id
* @return View
*/
public function show(string $id) // : View
{
// You can add logic to fetch and return data for the specific resource here.
}
/**
* Show the form for editing the specified resource.
*
* @param string $id
* @return View
*/
public function edit(string $id): View
{
try {
$response = $this->userService->findById($id);
return view('admin.user.edit')->with([
'data' => $response,
]);
} catch (Exception $e) {
return redirect()->back()->with('error', __('standard_curd_common_label.error'));
}
}
/**
* Update the specified resource in storage.
*
* @param UpdateUserRequest $request
* @param string $id
* @return RedirectResponse
*/
public function update(UpdateUserRequest $request, string $id): RedirectResponse
{
try {
$data = $request->except(['_token', '_method']);
if (!empty($data['password'])) {
$data['password'] = bcrypt($data['password']);
} else {
unset($data['password']);
}
$this->userService->update(['id' => $id], $data);
return redirect()->back()->with('success', __('user_module.create_list_edit.user') . __('standard_curd_common_label.update_success'));
} catch (Exception $e) {
return redirect()->back()->with('error', __('standard_curd_common_label.error'));
}
}
/**
* Remove the specified resource from storage.
*
* @param string $id
* @return JsonResponse
*/
public function destroy(string $id): JsonResponse
{
try {
$data = $this->userService->deleteById($id);
if ($data) {
return response()->json([
'message' => __('user_module.create_list_edit.user') . __('standard_curd_common_label.delete'),
'status_code' => ResponseAlias::HTTP_OK,
'data' => []
], ResponseAlias::HTTP_OK);
}
return response()->json([
'message' => __('user_module.create_list_edit.user') . __('standard_curd_common_label.delete_is_not'),
'status_code' => ResponseAlias::HTTP_BAD_REQUEST,
'data' => []
], ResponseAlias::HTTP_BAD_REQUEST);
} catch (Exception $e) {
return response()->json([
'message' => __('standard_curd_common_label.error'),
'status_code' => ResponseAlias::HTTP_INTERNAL_SERVER_ERROR,
'data' => []
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
Best Practices
- Use Dependency Injection: Avoid directly instantiating repositories inside controllers. Inject them via constructor injection.
- Separation of Concerns: Keep repositories focused on data handling, and services focused on business logic.
- Use of Interfaces: Always define interfaces for repositories to improve flexibility and make your code easily testable.
- Testable Code: Utilize PHPUnit to test repositories by mocking database queries.
Conclusion
Summarize the benefits of the repository pattern:
- Scalability: As your application grows, you can easily add more methods to repositories without affecting other parts of the code.
- Code Quality: Following design patterns such as the repository pattern leads to cleaner, more maintainable code.
- Laravel’s Eloquent Integration: You can leverage Laravel’s Eloquent ORM in your repository to handle data access while keeping your code structured.
You can also mention how the repository pattern fits well with other design patterns like the service and factory patterns.