DEV Community

A0mineTV
A0mineTV

Posted on

Structuring a Laravel Project with the Repository Pattern and Services πŸš€

Laravel is a robust and flexible framework, but without proper structure, controllers can easily become bloated with logic. To address this, many developers adopt a modular approach using the Repository Pattern and Services. This article explores these concepts with practical examples to help you build a clean, testable, and maintainable Laravel project.


What is the Repository Pattern ?

The Repository Pattern provides an abstraction layer between the database and the business logic of your application. Instead of directly interacting with Eloquent models in controllers or services, you use repositories to handle data access.

Advantages of the Repository Pattern

  1. Separation of Concerns: Isolates data access logic from business logic.
  2. Testability: Repositories can be mocked for testing.
  3. Reusability: Repository methods can be reused across different parts of the application.

Repository Structure

Repository Interface

Each repository is based on an interface (contract) that defines its methods. Here's an example of a BaseContract for basic CRUD operations:

<?php

declare(strict_types=1);

namespace App\Repositories\Contract;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

interface BaseContract
{
    /**
     * Find resource.
     *
     * @param int $id
     * @return Model|null
     */
    public function find(int $id): ?Model;

    /**
     * Find all resources.
     *
     * @return Collection
     */
    public function findAll(): Collection;

    /**
     * Create new resource.
     *
     * @param array $data
     * @return Model
     */
    public function create(array $data): Model;

    /**
     * Update existing resource.
     *
     * @param int $id
     * @param array $data
     * @return Model
     */
    public function update(int $id, array $data): Model;

    /**
     * Delete existing resource.
     *
     * @param int $id
     * @return bool
     */
    public function delete(int $id): bool;
}
Enter fullscreen mode Exit fullscreen mode

BaseRepository: A Generic Implementation

The BaseRepository implements the interface and provides generic CRUD functionality:

<?php

namespace App\Repositories;

use App\Repositories\Contract\BaseContract;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

abstract class BaseRepository implements BaseContract
{
    public function __construct(
        protected Model $model
    )
    {
    }

    public function find(int $id): ?Model
    {
        return $this->model->find($id);
    }

    public function findAll(): Collection
    {
        return $this->model->get();
    }

    public function create(array $data): Model
    {
        return $this->model->create($data);
    }

    public function update(int $id, array $data): Model
    {
        return $this->model
            ->where('id', $id)
            ->update($data);
    }

    public function delete(int $id): bool
    {
        return $this->model->delete($id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Specific Repository

A specific repository, like PostRepository, inherits from BaseRepository:

<?php

namespace App\Repositories;

use App\Models\Post;
use App\Repositories\Contract\PostContract;

final class PostRepository extends BaseRepository implements PostContract
{
    public function __construct(
        protected Post $post
    )
    {
        parent::__construct($this->post);
    }

    public function getPublishedPosts(): Collection
    {
        return $this->model->where('is_published', true)->get();
    }
}

Enter fullscreen mode Exit fullscreen mode

Services: The Business Logic Layer

Services encapsulate business logic, acting as a bridge between controllers and repositories. They manage complex rules and orchestrations.

Service Example

Here’s a PostService that leverages the PostRepository:

<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Post;
use App\Services\Contract\PostContract;
use App\Repositories\Contract\PostContract as PostRepositoryContract;
use Illuminate\Database\Eloquent\Collection;

final class PostService implements PostContract
{
    public function __construct(
        protected PostRepositoryContract $postRepository
    )
    {
    }

    public function get(int $id): ?Post
    {
        return $this->postRepository->find($id);
    }

    public function getAll(): Collection
    {
        return $this->postRepository->findAll();
    }

    public function create(array $data = []): Post
    {
        return $this->postRepository->create($data);
    }

    public function update(int $id, array $data = []): Post
    {
        return $this->postRepository->update($id, $data);
    }

    public function delete(int $id): bool
    {
        return $this->postRepository->delete($id);
    }
}

Enter fullscreen mode Exit fullscreen mode

Injecting Services into a Controller

Controllers benefit from dependency injection by using services. This keeps controllers lightweight and focused on request handling.

Controller Example

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\PostService;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;

final class HomeController extends Controller
{
    public function __construct(
        protected PostService $postService
    ) {}

    public function index(): Factory|View
    {
        $posts = $this->postService->getAll();
        return view('home', compact('posts'));
    }

    public function store(): RedirectResponse
    {
        $data = [
            'title' => 'A New Post',
            'content' => 'This is the content of the post.',
        ];

        $this->postService->create($data);

        return redirect()->route('home')->with('success', 'Post created successfully');
    }

    public function delete(int $id): RedirectResponse
    {
        if ($this->postService->delete($id)) {
            return redirect()->route('home')->with('success', 'Post deleted');
        }

        return redirect()->route('home')->with('error', 'Failed to delete post');
    }
}
Enter fullscreen mode Exit fullscreen mode

Β Why This Structure?

1. Clear Separation of Concerns

  • Controllers manage requests and responses.
  • Services handle business logic.
  • Repositories handle database access.

2. Ease of Maintenance

  • Business logic changes occur in services.
  • Database-specific changes occur in repositories.

3. Testability

  • Services and repositories can be tested in isolation.
  • Controllers can be tested with mocked services.

Conclusion

By combining the Repository Pattern and Services, you create a clean and modular architecture for Laravel projects. Each layer has a well-defined responsibility, improving code readability, maintainability, and testability.

How do you structure your Laravel projects? Do you use these concepts, or do you have other strategies to keep your code clean? Share your thoughts in the comments! πŸš€

Top comments (1)

Collapse
 
thibaultchatelain profile image
thiCha

Very nice and clear article, thank you !