-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
77: Practice Exercise: Building a Repository Pattern for Data Access
I was looking back at some old code I wrote for a client—a basic inventory system—and I noticed something that made me cringe. My controllers were bloated. Every time I wanted to fetch a product, I had a PDO instance right there in the action method, writing raw SQL and mapping results to an array. It worked, but it was a nightmare to test and even harder to change.
The 'Quick and Dirty' approach
Usually, when we start, we do something like this. I'll show you the snippet I found in that old project:
public function showProduct($id) {
$stmt = $this->pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();
// Now I'm doing business logic and DB access in one place...
if (!$product) {
throw new Exception("Product not found");
}
return $product;
}
At first glance, it's fine. It's fast. But here is where I hit a wall: what happens if I want to add a caching layer? Or what if I decide to move these products from MySQL to a JSON API or MongoDB? I'd have to hunt down every single controller method that mentions SELECT * FROM products and rewrite them. That's a recipe for introducing bugs.
Moving the plumbing out of the way
I figured I needed a way to hide the "how" of data retrieval. I don't care how the product gets from the disk to my variable; I just want the product. So, I tried creating a ProductRepository class. I basically just copy-pasted the SQL logic into a method.
class ProductRepository {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function findById(int $id): ?Product {
$stmt = $this->pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$data = $stmt->fetch();
return $data ? new Product($data['name'], $data['price']) : null;
}
}
Now the controller looks much cleaner. It just calls $this->repo->findById($id). I feel better, right? Not quite. I realized I've just moved the problem. My controller is now dependent on the ProductRepository class. If I want to write a unit test for the controller, I still need a real database connection because the repository demands a PDO instance.
The Interface epiphany
This is where I had to stop and rethink. The controller shouldn't depend on a class; it should depend on a contract. If I define an interface, the controller doesn't care if the data comes from MySQL, a CSV file, or a hard-coded array in a test suite.
I'll define what a "Product Repository" is supposed to be able to do, without saying how it does it:
interface ProductRepositoryInterface {
public function findById(int $id): ?Product;
public function save(Product $product): void;
}
Now, I can create my SqlProductRepository that implements this interface. But here's the magic: I can also create an InMemoryProductRepository for my tests. No database, no PDO, just a simple array. It's lightning fast and doesn't require any setup.
class InMemoryProductRepository implements ProductRepositoryInterface {
private array $products = [];
public function findById(int $id): ?Product {
return $this->products[$id] ?? null;
}
public function save(Product $product): void {
$this->products[$product->getId()] = $product;
}
}
Connecting the dots
The final piece of the puzzle is how I use this in the actual app. Instead of saying new SqlProductRepository($pdo) inside my controller, I type-hint the interface in the constructor. This is dependency inversion in action.
class ProductController {
private ProductRepositoryInterface $repository;
public function __construct(ProductRepositoryInterface $repository) {
$this->repository = $repository;
}
public function show($id) {
$product = $this->repository->findById($id);
// ... logic continues
}
}
I've effectively decoupled my business logic from my storage logic. If the boss comes to me next week and says, "We're moving to DynamoDB," I don't touch a single line of code in my controllers. I just write a new DynamoProductRepository, swap it in the configuration, and I'm done. That's the power of the Repository pattern—it turns your data source into a plug-in.
📋 Practical Task
Exercise: Implementing a User Management Repository
You are tasked with refactoring a User system. Currently, the user data is being fetched directly via PDO in the service layer. You need to implement a Repository pattern to decouple the data access.
Requirements:
- Create a
UserRepositoryInterfacethat defines two methods:findByEmail(string $email): ?Userandsave(User $user): void. - Create a
MysqlUserRepositoryclass that implements this interface using a providedPDOinstance. - Create a
MockUserRepositoryclass that implements the same interface but uses an internal array to store and retrieveUserobjects (for testing purposes). - Write a small bootstrap script that demonstrates switching between the
MysqlUserRepositoryand theMockUserRepositorywhile passing them into aUserServiceclass that depends on theUserRepositoryInterface.
Note: You may assume a simple User entity class with getId(), getEmail(), and setName() methods already exists.
There are no comments for now.