Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 075 / 100

Common PHP mistakes: a PHP & Laravel guide for developers

Habits that cause bugs.

Expona Admin6 min read

After seventy-five days of building with PHP and Laravel, you've likely encountered bugs that seemed obvious in retrospect. We're going to walk through common PHP mistakes: a PHP & Laravel guide for developers that still trip up experienced teams in production. These aren't syntax errors—they're logical patterns that compound over time and create subtle, expensive defects.

Loose comparisons with ==

PHP's type juggling is infamous, and the loose comparison operator == is where most of that danger lives. When you compare two values without enforcing type matching, PHP will attempt to convert them to a common type before comparing. This sounds helpful until it isn't.

Consider this real scenario from an e-commerce system:

php
$user_id = "0";
$product_id = 0;

if ($user_id == $product_id) {
    // This block executes. "0" == 0 is true.
    processRefund($user_id);
}

The string "0" and the integer 0 are considered equal. Your refund logic just triggered for the wrong user. Now multiply this across authentication checks, array key lookups, and conditional logic throughout a codebase and you have a security vulnerability waiting to happen.

The fix is straightforward: use the strict comparison operator === everywhere. It checks both value and type:

php
$user_id = "0";
$product_id = 0;

if ($user_id === $product_id) {
    // This block does not execute. "0" === 0 is false.
    processRefund($user_id);
}

In Laravel projects, this becomes especially important in middleware and policy checks. A single loose comparison in an authorization gate can grant access to the wrong user. We recommend enabling strict mode in your CI pipeline with tools like PHPStan at level 6 or higher, which will flag loose comparisons as violations.

Ignoring return types

PHP 7.0 introduced return type declarations, and PHP 7.1 added nullable types. Yet many codebases still ignore them or declare return types inconsistently. The cost is high: without explicit return types, you lose static analysis, IDE autocomplete, and explicit contracts between functions.

Here's a common pattern in legacy Laravel applications:

php
public function getUserById($id) {
    $user = User::find($id);
    
    if (!$user) {
        return null;
    }
    
    return $user;
}

The return type is unclear. Is it always a User? Sometimes null? Sometimes a collection? Any code calling this function has to handle uncertainty. Worse, a teammate might assume it always returns a User and skip null checks, causing null pointer exceptions in production.

Here's the corrected version with explicit return types:

php
public function getUserById(int $id): ?User {
    return User::find($id);
}

Now the contract is explicit: this returns either a User instance or null. Your IDE knows this. Static analysis tools know this. Any calling code that doesn't handle null will fail during analysis, catching the bug before production.

In Laravel, this becomes critical in service classes, repositories, and API responses. We've seen teams spending hours debugging route responses only to discover the controller method returns mixed types depending on execution path. Declare return types on every method. Use nullable types (?Type) or union types (Type|null, available in PHP 8.0+) explicitly. Your future self will thank you.

Using arrays for everything

PHP arrays are flexible and powerful, but flexibility often becomes a liability at scale. We've seen teams use associative arrays to represent domain objects, API requests, database rows, and configuration structures all interchangeably. This leads to ambiguity, reduces IDE support, and makes refactoring dangerous.

Consider a ride-hailing application processing ride requests:

php
$ride = [
    'id' => '123',
    'user_id' => '456',
    'driver_id' => null,
    'pickup_lat' => 40.7128,
    'pickup_lng' => -74.0060,
    'dropoff_lat' => 40.7489,
    'dropoff_lng' => -73.9680,
    'fare' => 12.50,
    'status' => 'pending'
];

function assignDriver($ride, $driver_id) {
    $ride['driver_id'] = $driver_id;
    return $ride;
}

The array is passed through multiple functions. Which keys are required? Which are optional? What happens if a key is missing? If someone refactors and removes the pickup_lat key, which functions break? Your IDE can't help. Static analysis tools can't infer the structure.

Use value objects or data transfer objects instead:

php
class RideRequest {
    public function __construct(
        public readonly string $id,
        public readonly string $userId,
        public readonly ?string $driverId,
        public readonly float $pickupLat,
        public readonly float $pickupLng,
        public readonly float $dropoffLat,
        public readonly float $dropoffLng,
        public readonly float $fare,
        public readonly string $status
    ) {}
    
    public function assignDriver(string $driverId): self {
        return new self(
            $this->id,
            $this->userId,
            $driverId,
            $this->pickupLat,
            $this->pickupLng,
            $this->dropoffLat,
            $this->dropoffLng,
            $this->fare,
            $this->status
        );
    }
}

Now the structure is explicit. Your IDE knows every property. Removing a property breaks the constructor immediately. You can't accidentally pass an array where a RideRequest is expected. In Laravel, use Data Transfer Objects (DTOs) from packages like Spatie's data package, or leverage Eloquent models when appropriate. Arrays are fine for truly dynamic data, but most domain objects deserve a class.

Suppressing errors with @

The error suppression operator @ masks warnings and errors rather than handling them. It's a band-aid that hides problems until they explode. We regularly see production incidents traced back to a single @ hiding a critical warning.

Here's a typical CRM integration:

php
$contact = @json_decode($response_body);

if ($contact) {
    // Process contact
}

If json_decode fails silently (because of the @), you might continue processing invalid data. The error is hidden but the logic is broken. Months later, your team finds that some contacts aren't being synced, and nobody knows why because the error was suppressed.

Handle errors explicitly:

php
$contact = json_decode($response_body, flags: JSON_THROW_ON_ERROR);

try {
    // Process contact
} catch (JsonException $e) {
    Log::error('Failed to decode contact', ['error' => $e->getMessage()]);
    throw new ContactImportException('Invalid contact data');
}

Or, for older PHP versions without JSON_THROW_ON_ERROR:

php
$contact = json_decode($response_body);

if (json_last_error() !== JSON_ERROR_NONE) {
    Log::error('Failed to decode contact', ['error' => json_last_error_msg()]);
    throw new ContactImportException('Invalid contact data');
}

Now errors are logged and handled deliberately. Your monitoring system can alert you. Your team understands the failure mode. In Laravel applications, never suppress errors. Enable strict error reporting in development and staging. Use proper exception handling in production. The @ operator should not exist in your codebase.

Common mistakes summary

Mistake Risk Solution
Loose comparisons (==) Security, authorization bugs, silent logic failures Always use strict comparison (===)
Missing return types Null pointer exceptions, IDE blind spots, refactoring hazards Declare return types on every method
Arrays for domain objects Lost structure, reduced analysis, unclear contracts Use classes, DTOs, or value objects
Error suppression (@) Hidden errors, silent failures, difficult debugging Handle errors explicitly with try-catch

Key takeaways

  • Replace every == with === in authorization checks, comparisons against user input, and conditional logic that affects behavior.
  • Declare return types on every public and private method; use nullable types to express intentional nullability.
  • Create classes or DTOs for structured data instead of relying on associative arrays; treat arrays as truly dynamic when needed.
  • Never suppress errors with @; catch exceptions, log them, and let your monitoring system alert the team.
  • Use static analysis tools (PHPStan, Psalm) in CI to catch these patterns automatically before code review.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles