Skip to content

Why Static Methods in Laravel Can Be Risky

Why Static Methods in Laravel Can Be Risky

Static methods can be really tempting. They’re easy to use and get the job done quickly. But as much as they seem like a shortcut, they come with some serious downsides, especially in a framework like Laravel.

I’ve seen a lot of developers overuse static methods, only to run into headaches later.

That’s why in this article, I’ll walk you through:

  • Why static methods can cause problems
  • When you should avoid using them
  • And the few cases where they actually make sense

📹Prefer watching over reading? Check out this video:

They Break Dependency Injection

One of Laravel’s biggest strengths is how it handles dependency injection (DI). DI makes your code flexible, testable, and easy to maintain by explicitly stating its dependencies.

Static methods completely ignore DI. You can’t inject them as dependencies, making testing and swapping out implementations way harder than it should be.

Here’s an example of a bad approach:

class UserService
{
    public function getUserData(int $userId): User
    {
        return UserRepository::getUserById($userId); // Static call
    }
}

Since UserRepository::getUserById() is a static call, there’s no way to replace UserRepository with a mock during testing. That’s a problem.

They Hide Dependencies

Another issue is that static methods create hidden dependencies. Instead of explicitly passing dependencies into a class, they’re just… there.

Let’s say you have an OrderService that logs events using a static method:

class OrderService
{
    public function __construct(
        private readonly OrderRepository $orderRepository,
        private readonly PaymentProcessor $paymentProcessor
    ) {}

    public function createOrder(array $orderData): Order
    {
        $order = new Order($orderData);
        
        return $this->orderRepository->save($order);
    }

    public function cancelOrder(int $orderId): bool
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order || $order->isShipped()) {
            return false;
        }

        $order->setStatus('canceled');
        
        return $this->orderRepository->update($order);
    }

    public function processOrder(int $orderId): bool
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order) {
            return false;
        }

        if (!$this->paymentProcessor->charge($order->getUserId(), $order->getTotalAmount())) {
            return false;
        }

        $order->setStatus('processing');
        
        // 👇 Hidden dependency buried in the middle of the method
        Logger::log("Processing order with ID: $orderId");
        
        return $this->orderRepository->update($order);
    }

    public function shipOrder(int $orderId): bool
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order || $order->getStatus() !== 'processing') {
            return false;
        }

        $order->setStatus('shipped');
        
        return $this->orderRepository->update($order);
    }

    public function getOrderSummary(int $orderId): array
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order) {
            return [];
        }

        return [
            'id' => $order->getId(),
            'status' => $order->getStatus(),
            'total' => $order->getTotalAmount(),
            'items' => $order->getItems(),
        ];
    }
}

There’s no way to tell that OrderService depends on Logger unless you dig through the code. That makes the code harder to read, test, debug, and extend.

A better way:

class OrderService
{
    public function __construct(
        private readonly OrderRepository $orderRepository,
        private readonly PaymentProcessor $paymentProcessor,
        private readonly Logger $logger // ✅ Dependency is no longer hidden
    ) {}

    public function createOrder(array $orderData): Order
    {
        $order = new Order($orderData);
        
        return $this->orderRepository->save($order);
    }

    public function cancelOrder(int $orderId): bool
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order || $order->isShipped()) {
            return false;
        }

        $order->setStatus('canceled');
        
        return $this->orderRepository->update($order);
    }

    public function processOrder(int $orderId): bool
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order) {
            return false;
        }

        if (!$this->paymentProcessor->charge($order->getUserId(), $order->getTotalAmount())) {
            return false;
        }

        $order->setStatus('processing');
        
        // ✅ Dependency is no longer hidden
        $this->logger->log("Processing order with ID: $orderId");
        
        return $this->orderRepository->update($order);
    }

    public function shipOrder(int $orderId): bool
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order || $order->getStatus() !== 'processing') {
            return false;
        }

        $order->setStatus('shipped');
        
        return $this->orderRepository->update($order);
    }

    public function getOrderSummary(int $orderId): array
    {
        $order = $this->orderRepository->findById($orderId);

        if (!$order) {
            return [];
        }

        return [
            'id' => $order->getId(),
            'status' => $order->getStatus(),
            'total' => $order->getTotalAmount(),
            'items' => $order->getItems(),
        ];
    }
}

Now, the Logger dependency is clear. You can see it directly in the constructor.

And if you ever need to replace it with a different logger, it’s as easy as swapping it out in the constructor.

They Make Your Code Harder to Extend

Static methods also mess with the Open-Closed Principle, which basically says that your code should be open for extension but closed for modification.

Let’s say you have an order notification system that always sends and email when an order is completed:

class OrderService
{
    public function completeOrder(int $orderId): void
    {
        EmailNotifier::send("Order $orderId has been completed.");
    }
}

This works… until you need to add SMS or push notifications. Now, you have to go into OrderService and modify it, which breaks the Open-Closed Principle.

A more flexible approach would be:

interface Notifier
{
    public function send(string $message): void;
}

class EmailNotifier implements Notifier
{
    public function send(string $message): void
    {
        echo "Email sent: $message";
    }
}

class SMSNotifier implements Notifier
{
    public function send(string $message): void
    {
        echo "SMS sent: $message";
    }
}

class OrderService
{
    public function __construct(private readonly Notifier $notifier) {}

    public function completeOrder(int $orderId): void
    {
        $this->notifier->send("Order $orderId has been completed.");
    }
}

$orderService = new OrderService(new SMSNotifier());
$orderService->completeOrder(123); // SMS sent: Order 123 has been completed.

$orderService = new OrderService(new EmailNotifier ());
$orderService->completeOrder(123); // Email sent: Order 123 has been completed.

Now, if you need to add push notifications, you just create a new PushNotifier class. No need to touch OrderService. That’s the power of dependency injection.

They Don’t Play Nice with Laravel’s Service Container

Laravel’s service container is a game-changer.

It automatically resolves dependencies, making it easy to swap out implementations, apply middleware, and even cache results.

Static methods ignore all of that. When you go all-in on static calls, you lose a lot of Laravel’s strengths, which makes your code less flexible in the long run.

They Create Global State (Which Can Be a Nightmare)

Static methods essentially introduce global state, which can lead to unpredictable behavior.

Take this example:

class ConfigHelper
{
    public static function getValue(string $key): string
    {
        return Config::get($key);
    }
}

At first glance, it looks fine.

But if Config::get($key) changes, every single place that calls ConfigHelper::getValue() could break.

And tracking down issues like that?

A nightmare.

When Are Static Methods Okay?

I’m not saying static methods are always bad.

They can be useful in certain cases, like utility and helper classes that don’t rely on external dependencies.

Laravel’s \Illuminate\Support\Str and \Carbon\Carbon classes are great examples.

use Carbon\Carbon;
use Illuminate\Support\Str;

$slug = Str::slug('Neutron Dev Tutorials');

$random = Str::random(10);

$isPrefixed = Str::startsWith('Laravel Framework', 'Laravel');

$now = Carbon::now()->toDateTimeString();

$past = Carbon::now()->subDays(10);

$diff = Carbon::now()
            ->diffForHumans(
                Carbon::now()->subMinutes(45)
            );

They provide simple, self-contained functionality that doesn’t need to be injected into anything.

If you’re using static methods for things like:

✅ String manipulation (i.e Str::slug())
✅ Date handling (i.e Carbon::now())
✅ Simple, self-contained helpers

Then you’re fine. Just avoid creating static methods service classes, repositories, or anything that might need to be swapped out later.

Final Thoughts

Static methods seem like a convenient shortcut, but they come with some serious trade-offs.

They break dependency injection, make code harder to extend, and don’t work well with Laravel’s service container.

If you want your Laravel code to be testable, maintainable, and scalable, avoid static methods in most cases. Use dependency injection instead. It’ll save you from a lot of headaches in the future.

But if you’re working with pure utility functions that don’t need dependencies, static methods can still be a good choice.

What do you think? Do you still use static methods in Laravel, or have you moved away from them?

Let’s discuss this in the comments down below! 👇

And as always, happy coding 💜


Let me know what you think about this article in the comments section below.

If you find this article helpful, please share it with others and subscribe to the blog to support me, and receive a bi-monthly-ish e-mail notification on my latest articles.   
  

Comments