Repository pattern in PHP and its use cases

The Repository Pattern is a design pattern that provides a centralized location for data access and management in an application. It acts as an intermediary between the data layer (such as a database) and the application layer. The Repository Pattern is used to separate the application’s data access logic from the rest of the application.

Here is an example of the Repository Pattern in PHP:

<?php

interface RepositoryInterface {
    public function getAll();
    public function getById($id);
    public function create($data);
    public function update($id, $data);
    public function delete($id);
}

class UserRepository implements RepositoryInterface {
    private $db;

    public function __construct(PDO $db) {
        $this->db = $db;
    }

    public function getAll() {
        $stmt = $this->db->prepare("SELECT * FROM users");
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_OBJ);
    }

    public function getById($id) {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id=?");
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_OBJ);
    }

    public function create($data) {
        $stmt = $this->db->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
        return $stmt->execute([$data['name'], $data['email']]);
    }

    public function update($id, $data) {
        $stmt = $this->db->prepare("UPDATE users SET name=?, email=? WHERE id=?");
        return $stmt->execute([$data['name'], $data['email'], $id]);
    }

    public function delete($id) {
        $stmt = $this->db->prepare("DELETE FROM users WHERE id=?");
        return $stmt->execute([$id]);
    }
}

In this example, the RepositoryInterface defines the methods that a repository must implement. The UserRepository class implements the RepositoryInterface and provides the actual implementation for the methods. The UserRepository class uses the PDO class to access the database.

With the Repository Pattern, the application can access the data through the repository and the repository takes care of the actual data access logic. This helps to keep the data access logic separate from the rest of the application and makes it easier to maintain and update the application.

The Repository Pattern has several use cases, including:

  1. Decoupling the data access logic from the rest of the application: By using the Repository Pattern, the data access logic can be separated from the rest of the application. This makes it easier to change the data access logic without affecting the rest of the application.
  2. Centralizing data access: The Repository Pattern provides a centralized location for data access and management. This makes it easier to manage the data access logic and to maintain a consistent way of accessing data throughout the application.
  3. Enhancing testability: By using the Repository Pattern, it is easier to write unit tests for the data access logic. The tests can be written against the repository interface, which allows for easy mocking of the data access layer.
  4. Improving security: The Repository Pattern can help to improve the security of the application by centralizing the data access logic and enforcing a consistent way of accessing data. This makes it easier to implement security measures, such as input validation and output sanitization, in a single location.
  5. Supporting multiple data sources: The Repository Pattern can be used to support multiple data sources. By creating a separate repository for each data source, the application can access each data source in a consistent way.
  6. Facilitating code reuse: The Repository Pattern makes it easier to reuse the data access logic across multiple parts of the application. This can lead to more consistent and efficient data access, as well as faster development times.

Overall, the Repository Pattern is a useful tool for organizing the data access logic in an application and making it easier to maintain and update.

Here is an example of how the Repository Pattern can enhance testability in PHP:

<?php

interface UserRepositoryInterface {
    public function getByEmailAndPassword($email, $password);
}

class UserRepository implements UserRepositoryInterface {
    private $db;

    public function __construct(PDO $db) {
        $this->db = $db;
    }

    public function getByEmailAndPassword($email, $password) {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE email=? AND password=?");
        $stmt->execute([$email, hash('sha256', $password)]);
        return $stmt->fetch(PDO::FETCH_OBJ);
    }
}

class UserService {
    private $userRepository;

    public function __construct(UserRepositoryInterface $userRepository) {
        $this->userRepository = $userRepository;
    }

    public function login($email, $password) {
        $user = $this->userRepository->getByEmailAndPassword($email, $password);
        if (!$user) {
            throw new Exception("Invalid email or password");
        }
        return $user;
    }
}

In this example, the UserRepository implements the UserRepositoryInterface and provides the implementation for the getByEmailAndPassword method, which retrieves a user from the database based on the email and password. The UserService class depends on the UserRepositoryInterface and uses the repository to access the data. The login method in the UserService class uses the repository to retrieve the user based on the email and password, and throws an exception if the user is not found.

This design makes it easy to write unit tests for the UserService class, as it can be tested in isolation from the data access layer. You can mock the UserRepository and configure it to return specific values for the getByEmailAndPassword method, which makes the tests more reliable and easier to write.

Here’s an example of a unit test for the login method in the UserService class:

<?php

use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase {
    public function testLoginSuccess() {
        $email = "test@example.com";
        $password = "password";
        $user = (object) [
            "id" => 1,
            "email" => $email,
            "password" => hash('sha256', $password),
        ];

        $userRepositoryMock = $this->createMock(UserRepositoryInterface::class);
        $userRepositoryMock->expects($this->once())
            ->method('getByEmailAndPassword')
            ->with($this->equalTo($email), $this->equalTo($password))
            ->willReturn($user);

        $userService = new UserService($userRepositoryMock);
        $result = $userService->login($email, $password);

        $this->assertEquals($user, $result);
    }

    public function testLoginFail() {
        $email = "test@example.com";
        $password = "password";

        $userRepositoryMock = $this->createMock(UserRepositoryInterface::class);
        $userRepositoryMock->expects($this->once())
            ->method('getByEmailAndPassword')
            ->with($this->equalTo($email), $this->equalTo($password))
            ->willReturn(null);

        $userService = new UserService($userRepositoryMock);

        $this->expectException(Exception::class);
        $this->expectExceptionMessage("Invalid email or password");
        $userService->login($email, $password);
    }
}

In this example, the test class extends the PHPUnit\Framework\TestCase class, which provides a testing framework for writing unit tests in PHP. The testLoginSuccess method creates a mock object for the UserRepository using the createMock method, and configures it to return a specific user when the getByEmailAndPassword method is called. The UserService is instantiated with the mock repository, and the login method is called with the email and password. The result is then compared to the expected user to ensure that the method is working as expected.

The testLoginFail method is similar, but it configures the mock repository to return null, which represents an invalid email and password. The test then expects an exception to be thrown with a specific message, which is checked to ensure that the exception is thrown correctly.

These tests ensure that the UserService class is working correctly, and that it will behave correctly in the event of an invalid login attempt. The tests are also isolated from the actual data access layer, which makes them more reliable and easier to maintain over time.