If you are still FTPing files to a server or running a local WAMP stack that hasn’t been updated since the pre-pandemic era, we need to talk.
PHP in 2025 is a strictly typed, high-performance language that powers enterprise-grade applications. But the language is only half the battle. The ecosystem around it—how we write, check, test, and ship code—has evolved drastically.
A “Modern PHP Workflow” isn’t just about using the latest syntax; it’s about confidence and velocity. It is about catching bugs before they hit production and automating the boring stuff so you can focus on logic.
In this guide, we are going to tear down the legacy habits and build a streamlined, professional development environment from scratch.
1. The Foundation: Reproducible Environments #
The phrase “it works on my machine” should be extinct. In modern development, your environment must be defined as code. While you might use Laravel Herd for quick scaffolding, for true team-based development, Docker is the non-negotiable standard.
However, writing raw docker-compose.yml files can be tedious. Enter DDEV (or similar wrappers like Lando/Sail). It provides an abstraction layer over Docker that is specifically tuned for PHP.
Setting up the Environment #
Assuming you have Docker installed, let’s spin up a modern PHP 8.4 project.
# Install DDEV (if not already installed)
# macOS/Linux
curl -fsSL https://ddev.com/install.sh | bash
# Create a directory
mkdir modern-php-app && cd modern-php-app
# Initialize the config (PHP 8.4, Nginx, MariaDB)
ddev config --project-name=modern-php-app --docroot=public --php-version=8.4 --project-type=php
# Start the containerized environment
ddev startThis creates a contained ecosystem. You aren’t polluting your local machine with PHP binaries.
2. Dependency Management & Project Structure #
With the container running, we initialize our project using Composer. In 2025, we don’t just use Composer to install libraries; we use it to enforce platform requirements and script our workflow.
Access your container:
ddev sshNow, initialize the project. We are going to strictly define our platform to ensure no one on the team installs a package that requires an older PHP version.
{
"name": "phpdevpro/modern-workflow",
"description": "A template for modern PHP development",
"type": "project",
"license": "MIT",
"require": {
"php": "^8.4",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"pestphp/pest": "^3.0",
"phpstan/phpstan": "^2.0",
"laravel/pint": "^1.18",
"symfony/var-dumper": "^7.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"platform": {
"php": "8.4"
},
"sort-packages": true,
"optimize-autoloader": true
}
}Key Takeaway: The "config": { "platform": { "php": "8.4" } } line is crucial. It fakes the platform version so that even if your CI runner has PHP 8.5, Composer resolves dependencies as if it were 8.4, ensuring consistency.
3. The Quality Gate: Static Analysis #
If you aren’t using Static Analysis, you are essentially coding blindfolded. Tools like PHPStan or Psalm analyze your code without running it, finding type errors, missing handling of null values, and dead code.
In 2025, relying solely on runtime error reporting is unacceptable for senior-level work.
Configuring PHPStan #
Create a phpstan.neon file in your root. We will start at level 5 (standard rigor), but you should aim for level 9 (max) eventually.
parameters:
level: 5
paths:
- src
tmpDir: build/phpstan
checkMissingIterableValueType: falseThe Code Example #
Let’s look at a modern PHP 8.4 class and how we enforce types. Note the use of Constructor Property Promotion and Readonly Classes.
<?php
namespace App\Services;
use InvalidArgumentException;
readonly class PriceCalculator
{
public function __construct(
private float $taxRate = 0.20
) {}
/**
* @param array<int, float> $items
* @return float
*/
public function calculateTotal(array $items): float
{
$subtotal = 0.0;
foreach ($items as $price) {
if ($price < 0) {
throw new InvalidArgumentException("Price cannot be negative.");
}
$subtotal += $price;
}
return round($subtotal * (1 + $this->taxRate), 2);
}
}If you accidentally passed a string to $items, PHPStan would flag this immediately inside your IDE or terminal, long before you ran the code.
4. Visualizing the Workflow #
To understand how these tools fit together, let’s look at the lifecycle of a single code change. This is the “Feedback Loop.” The tighter this loop, the faster you develop.
5. Testing Frameworks: The Rise of PEST #
While PHPUnit remains the giant on whose shoulders we stand, Pest PHP has captured the hearts of the community for its developer experience (DX). It offers a jest-like syntax that is expressive and minimal.
Why Pest? Because tests that are easy to write are tests that actually get written.
Setting up a Test #
Run ./vendor/bin/pest --init to set up the harness. Then, create tests/Unit/PriceCalculatorTest.php:
<?php
use App\Services\PriceCalculator;
test('it calculates total with tax correctly', function () {
// Arrange
$calculator = new PriceCalculator(taxRate: 0.10);
$items = [10.00, 20.00];
// Act
$result = $calculator->calculateTotal($items);
// Assert
expect($result)->toBe(33.00);
});
test('it throws exception for negative prices', function () {
$calculator = new PriceCalculator();
expect(fn() => $calculator->calculateTotal([-5.00]))
->toThrow(InvalidArgumentException::class);
});To run it:
./vendor/bin/pestThe output is beautiful, concise, and tells you exactly where you failed.
6. Comparison: Legacy vs. Modern Stack #
Let’s break down exactly what has changed. If you are updating your resume or pitching a tech-stack upgrade to your boss, this table is your ammunition.
| Feature | The “Old School” Way | The Modern Workflow (2025) |
|---|---|---|
| Environment | WAMP / XAMPP / MAMP installed locally | Docker / DDEV / Containers |
| Dependencies | Downloading .zip files / include_path |
Composer with Platform enforcement |
| Code Quality | “I check it in the browser” | PHPStan (Level 5+) + CI Pipeline |
| Formatting | Manual indentation wars | Laravel Pint / PHP-CS-Fixer (Automated) |
| Testing | Manual clicking / var_dump |
Pest / PHPUnit (Automated TDD) |
| Typing | Loose typing ($var) |
Strict Types (declare(strict_types=1)) |
7. Automating via Composer Scripts #
A “Pro” tip to glue this all together: don’t make your team remember five different commands. Add a test script to your composer.json.
"scripts": {
"lint": "pint",
"analyze": "phpstan analyse",
"test": "pest",
"check": [
"@lint",
"@analyze",
"@test"
]
}Now, before anyone commits, they just run one command:
composer checkConclusion #
Modern PHP development is no longer about hacking scripts together; it is software engineering. By adopting Docker for consistency, Static Analysis for safety, and Pest for testing, you drastically reduce technical debt.
This workflow might seem like “extra steps” initially, but the time saved from debugging mysterious production errors pays for the setup ten times over.
Next Steps for You #
- Audit your current project: Can you run it with a single command?
- Install PHPStan: Start at level 0 and fix the errors. It is cathartic.
- Refactor one class: Convert an old class to use
readonlyproperties and strict return types.
The tools are there. The ecosystem is mature. It’s time to build better software.