By enabling strict types, you can catch type-related bugs early and make your code more self-documenting. This blog post will show you everything you need to know about strict types, including how to enforce them with Pint and test for them with Pest.
Strict types in PHP enforce type declarations at runtime. When enabled, PHP will throw a TypeError
if you try to pass a value of the wrong type to a function parameter or return the wrong type from a function.
To enable strict types, add this declaration at the very beginning of your PHP file:
<?php declare(strict_types=1);
Important: This declaration must be the very first statement in the file, even before any comments or whitespace.
Without strict types, PHP performs automatic type coercion, which can hide bugs:
Without strict types:
function calculateTotal(float $price, int $quantity): float
{
return $price * $quantity;
}
// This works but might not be what you intended
$total = calculateTotal("10.50", "2"); // Returns 21.0
With strict types:
<?php declare(strict_types=1);
function calculateTotal(float $price, int $quantity): float
{
return $price * $quantity;
}
// This throws a TypeError
$total = calculateTotal("10.50", "2"); // Fatal error: Uncaught TypeError
Strict types make your function signatures more meaningful:
<?php declare(strict_types=1);
function processUser(int $id, string $name, bool $isActive): User
{
// The function signature clearly shows what types are expected
return new User($id, $name, $isActive);
}
IDEs can provide better autocomplete and error detection when strict types are enabled.
<?php declare(strict_types=1);
// Scalar types
function greetUser(string $name): string
{
return "Hello, " . $name;
}
function calculateAge(int $birthYear): int
{
return date('Y') - $birthYear;
}
function calculateDiscount(float $price, float $percentage): float
{
return $price * ($percentage / 100);
}
function isEligible(bool $hasAccount): bool
{
return $hasAccount;
}
<?php declare(strict_types=1);
function processItems(array $items): array
{
return array_map('strtoupper', $items);
}
function countItems(array $items): int
{
return count($items);
}
<?php declare(strict_types=1);
class User
{
public function __construct(
private string $name,
private string $email
) {}
public function getName(): string
{
return $this->name;
}
}
function saveUser(User $user): void
{
echo "Saving user: " . $user->getName();
}
function createUser(string $name, string $email): User
{
return new User($name, $email);
}
<?php declare(strict_types=1);
function findUser(int $id): ?User
{
// Returns User or null
if ($id > 0) {
return new User("John", "john@example.com");
}
return null;
}
function processOptionalData(?string $data): string
{
return $data ?? 'No data provided';
}
<?php declare(strict_types=1);
function processId(string|int $id): string
{
return "Processing ID: " . $id;
}
function getValue(string|int|float $value): string
{
return "Value: " . $value;
}
Without strict types:
<?php
function registerUser($name, $email, $age)
{
// No type safety - anything can be passed
if (strlen($name) < 2) {
throw new InvalidArgumentException('Name too short');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
if ($age < 18) {
throw new InvalidArgumentException('Must be 18 or older');
}
return new User($name, $email, $age);
}
// These all "work" but might cause issues
$user1 = registerUser(123, "not-an-email", "25"); // Numbers and strings mixed
$user2 = registerUser([], null, true); // Completely wrong types
With strict types:
<?php declare(strict_types=1);
function registerUser(string $name, string $email, int $age): User
{
if (strlen($name) < 2) {
throw new InvalidArgumentException('Name too short');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
if ($age < 18) {
throw new InvalidArgumentException('Must be 18 or older');
}
return new User($name, $email, $age);
}
// Only correct types are accepted
$user = registerUser("John Doe", "john@example.com", 25); // ✓ Works
// These throw TypeError immediately
// registerUser(123, "john@example.com", 25); // ✗ TypeError
// registerUser("John", "john@example.com", "25"); // ✗ TypeError
Without strict types:
<?php
function calculateArea($width, $height)
{
return $width * $height;
}
// These all "work" due to type coercion
echo calculateArea("10", "20"); // 200 (strings converted to numbers)
echo calculateArea("10.5", 20); // 210 (string converted to float)
echo calculateArea(true, 10); // 10 (true converted to 1)
echo calculateArea("abc", 10); // 0 (non-numeric string converted to 0)
With strict types:
<?php declare(strict_types=1);
function calculateArea(float $width, float $height): float
{
return $width * $height;
}
// Only correct types work
echo calculateArea(10.0, 20.0); // ✓ 200.0
echo calculateArea(10, 20); // ✓ 200.0 (int promoted to float)
// These throw TypeError
// calculateArea("10", "20"); // ✗ TypeError
// calculateArea(true, 10); // ✗ TypeError
Pint is Laravel's opinionated PHP code style fixer built on top of PHP-CS-Fixer. You can configure Pint to automatically enforce strict types declarations.
composer require laravel/pint --dev
Create a pint.json
file in your project root:
{
"preset": "laravel",
"rules": {
"declare_strict_types": true,
"blank_line_after_opening_tag": false
}
}
For a more comprehensive setup:
{
"preset": "laravel",
"rules": {
"declare_strict_types": true,
"blank_line_after_opening_tag": false,
"strict_param": true,
"strict_comparison": true,
"native_function_type_declaration_casing": true,
"return_type_declaration": {
"space_before": "none"
}
}
}
# Check what would be fixed
./vendor/bin/pint --test
# Fix files
./vendor/bin/pint
Before Pint:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
// Controller logic
}
}
After Pint:
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
// Controller logic
}
}
Add Pint to your GitHub Actions workflow:
name: Code Style
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
pint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run Pint
run: ./vendor/bin/pint --test
Pest is a delightful PHP testing framework. You can use Pest's architecture testing features to ensure all your PHP files have strict types enabled.
composer require pestphp/pest --dev
composer require pestphp/pest-plugin-arch --dev
Create a test file tests/Architecture/StrictTypesTest.php
:
<?php declare(strict_types=1);
use Pest\Arch\Arch;
test('all PHP files have strict types enabled')
->expect(['app', 'config', 'database', 'routes'])
->toUseStrictTypes();
<?php declare(strict_types=1);
use Pest\Arch\Arch;
describe('Code Standards', function () {
test('all application files use strict types')
->expect(['app'])
->toUseStrictTypes();
test('all configuration files use strict types')
->expect(['config'])
->toUseStrictTypes();
test('all database files use strict types')
->expect(['database'])
->toUseStrictTypes();
test('all route files use strict types')
->expect(['routes'])
->toUseStrictTypes();
test('all test files use strict types')
->expect(['tests'])
->toUseStrictTypes();
});
<?php declare(strict_types=1);
test('all controllers use strict types')
->expect('App\Http\Controllers')
->toUseStrictTypes();
test('all models use strict types')
->expect('App\Models')
->toUseStrictTypes();
test('all services use strict types')
->expect('App\Services')
->toUseStrictTypes();
test('all repositories use strict types')
->expect('App\Repositories')
->toUseStrictTypes();
You can create custom rules for more specific requirements:
<?php declare(strict_types=1);
test('all classes in App namespace use strict types and have proper structure')
->expect('App')
->toUseStrictTypes()
->and('App\Http\Controllers')
->toExtend('Illuminate\Routing\Controller')
->and('App\Models')
->toExtend('Illuminate\Database\Eloquent\Model')
->and('App\Services')
->toHaveSuffix('Service');
# Run all tests
./vendor/bin/pest
# Run only architecture tests
./vendor/bin/pest tests/Architecture
# Run with coverage
./vendor/bin/pest --coverage
When you run the architecture tests, you'll see output like:
✓ all PHP files have strict types enabled
✓ all controllers use strict types
✓ all models use strict types
✓ all services use strict types
Tests: 4 passed
Time: 0.12s
If a file doesn't have strict types, you'll see:
✗ all PHP files have strict types enabled
Expected [App\Http\Controllers\UserController] to use strict types.
Make it a habit to start every new PHP file with:
<?php declare(strict_types=1);
Always declare parameter and return types:
<?php declare(strict_types=1);
// Good
function processUser(int $id, string $name): User
{
return new User($id, $name);
}
// Avoid
function processUser($id, $name)
{
return new User($id, $name);
}
Be explicit about nullable types:
<?php declare(strict_types=1);
function findUser(int $id): ?User
{
// Explicitly return null when user not found
return $this->users[$id] ?? null;
}
function processUser(?User $user): string
{
// Handle null case explicitly
if ($user === null) {
return 'No user provided';
}
return $user->getName();
}
<?php declare(strict_types=1);
function processId(string|int $id): string
{
return "Processing: " . $id;
}
function formatValue(string|int|float $value): string
{
return number_format((float) $value, 2);
}
Use strict types alongside tools like PHPStan or Psalm:
<?php declare(strict_types=1);
/**
* @param array<int, User> $users
* @return array<string, int>
*/
function getUserCounts(array $users): array {
$counts = [];
foreach ($users as $user) {
$role = $user->getRole();
$counts[$role] = ($counts[$role] ?? 0) + 1;
}
return $counts;
}
Problem:
<?php
// Missing declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
echo add("5", "10"); // Works due to type coercion, returns 15
Solution:
<?php declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
echo add("5", "10"); // TypeError: Argument must be of type int
Problem: Strict types only apply to the file where they're declared. If you call a strict function from a non-strict file, the calling file's rules apply.
Solution: Enable strict types in all files consistently.
Problem: Existing codebase without strict types.
Solution: Gradual migration:
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function index(): JsonResponse
{
$users = User::all();
return response()->json($users);
}
public function show(User $user): JsonResponse
{
return response()->json($user);
}
public function store(StoreUserRequest $request): JsonResponse
{
$user = User::create($request->validated());
return response()->json($user, 201);
}
public function update(StoreUserRequest $request, User $user): JsonResponse
{
$user->update($request->validated());
return response()->json($user);
}
public function destroy(User $user): JsonResponse
{
$user->delete();
return response()->json(null, 204);
}
}
<?php declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
use HasFactory;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function getFullNameAttribute(): string
{
return $this->first_name . ' ' . $this->last_name;
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
}
<?php declare(strict_types=1);
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
class UserService
{
public function createUser(array $data): User
{
$data['password'] = Hash::make($data['password']);
return User::create($data);
}
public function updateUser(User $user, array $data): User
{
if (isset($data['password'])) {
$data['password'] = Hash::make($data['password']);
}
$user->update($data);
return $user->fresh();
}
public function getActiveUsers(): Collection
{
return User::where('active', true)->get();
}
public function deleteUser(User $user): bool
{
return $user->delete();
}
public function findUserByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
}
PHP strict types are a powerful feature that can significantly improve your code quality by:
By combining strict types with tools like Pint for enforcement and Pest for testing, you can ensure your entire codebase maintains high type safety standards.
declare(strict_types=1)
in new PHP filesStart implementing strict types in your projects today, and you'll quickly see the benefits in terms of code reliability and maintainability.
Subscribe to my newsletter for the latest updates on my books and digital products.
Find posts, tutorials, and resources quickly.
Subscribe to my newsletter for the latest updates on my books and digital products.