PHP
Completed
-
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
165: Data Providers in PHPUnit
When you're testing a piece of logic that needs to be verified against a dozen different inputs, your first instinct might be to reach for a foreach loop. I've seen this in countless PRs. You have a set of inputs and expected outputs, and you just want to iterate through them and assert that the result is correct. It feels efficient because you've only written one test method.
The trap of the manual loop
public function testPasswordValidation(): void
{
$cases = [
['short', false],
['TooShort1!', false],
['ValidPass123!', true],
['NoSpecialChar123', false],
['alllowercase1!', false],
];
foreach ($cases as [$password, $expected]) {
$this->assertEquals($expected, $this->validator->isValid($password));
}
}
On the surface, this works. But here is where it breaks: the moment the second case fails, the entire test stops. PHPUnit sees this as one single test. If 'TooShort1!' fails, you have no idea if the other three cases would have passed or failed because the execution never reaches them. You end up in a tedious cycle of fixing one input, rerunning the test, finding the next failure, and repeating. It's a slow feedback loop, and it hides the full scope of your regressions.
Treating inputs as individual tests
This is where Data Providers come in. Instead of looping inside the test, you move the data to a separate method that "feeds" the test. PHPUnit then treats every single entry in that provider as a distinct test case. If case #2 fails, PHPUnit marks it as a failure but continues to run cases #3, #4, and #5. You get a full report of exactly which edge cases are broken.
public function passwordProvider(): array
{
return [
'too short' => ['short', false],
'still too short' => ['TooShort1!', false],
'perfectly valid' => ['ValidPass123!', true],
'missing special' => ['NoSpecialChar123', false],
'missing uppercase' => ['alllowercase1!', false],
];
}
#[DataProvider('passwordProvider')]
public function testPasswordValidation(string $password, bool $expected): void
{
$this->assertEquals($expected, $this->validator->isValid($password));
}
Notice how I used string keys like 'too short' in the array. I highly recommend doing this. When a test fails, PHPUnit will print that key in the output. Seeing "Failed asserting that false is true" is annoying; seeing "Failed asserting that false is true in dataset 'missing uppercase'" tells you exactly what's wrong before you even open the code.
The trade-off in setup
Is there a downside? You're writing a bit more boilerplate—an extra method and an attribute. If you only have two test cases, a data provider is probably overkill. But the moment you hit four or five variations, the cost of the extra method is dwarfed by the time you save during debugging. You're essentially trading a few lines of setup for a much higher resolution of failure reporting.
📋 Practical Task
Refactoring a Discount Code Validator
You have a DiscountValidator class with a method isValid(string $code): bool. The current test suite uses a foreach loop to check several codes: 'SUMMER20' (valid), 'WINTER10' (valid), 'EXPIRED' (invalid), and 'INVALID_CODE' (invalid).
Your task is to refactor the test. Create a dedicated data provider method named discountCodeProvider that returns a keyed array of these cases. Update the test method to use the #[DataProvider('discountCodeProvider')] attribute, ensuring that each single code is treated as an independent test case by PHPUnit.
There are no comments for now.