static methods in PHP

Submitted by admin on

In PHP, classes are the natural unit of organization even for stateless logic. Here’s how to think about it:
 

The core organizing principle


Group by what the functions operate on or what they produce — not by how they feel related to you. The grouping should survive a new developer asking “where would I look for this?”

Practical approaches
1. Name by the operation/domain, not the pattern
Instead of StringUtils or StringHelper, ask what kind of string work it is:

TextSanitizer::stripHtml($input)
SlugGenerator::fromTitle($title)
EmailNormalizer::lowercase($email)


Each class has one job and its name says what that job is.

2. Use static methods for pure, stateless logic
If there’s no configuration or state needed, static is fine and honest:

class MoneyFormatter {
   public static function toCurrency(int $cents, string $currency): string { ... }
   public static function toWords(int $cents): string { ... }
}


3. Use instantiable classes when configuration is involved
If behavior varies by context, inject the variation:

class DateFormatter {
   public function __construct(private string $timezone, private string $locale) {}
   
   public function format(\DateTimeInterface $date, string $pattern): string { ... }
}


This also makes it injectable and mockable in tests.

4. Organize by feature/domain, not by type
Rather than a flat Utils/ folder:

src/
 Formatting/
   MoneyFormatter.php
   DateFormatter.php
   SlugGenerator.php
 Validation/
   EmailValidator.php
   PhoneNormalizer.php
 Http/
   UrlBuilder.php
   QueryStringParser.php


The folder communicates intent. A new developer finds MoneyFormatter by thinking “I need to format money” — not by remembering which *Utils class absorbed it.

The key question for every method
Does this behavior belong on a domain object instead?
Order::calculateTax() beats TaxHelper::calculate($order). If the answer is yes, the utility class shouldn’t exist at all. Utility classes are for logic that genuinely has no natural domain owner.

TL;DR: Name classes after what they do, group by domain area, prefer instantiable classes when config is involved, and always ask if the logic belongs on a real domain object first.​​​​​​​​​​​​​​​​

 

 

 

Static is one of those features that’s easy to overuse. 

 

Here’s the full picture:


What static actually means


A static method or property belongs to the class itself, not to any instance. 

 

No $this, no object needed — you call it on the class directly.

 

class MathHelper {
   public static function square(int $n): int {
       return $n * $n;
   }
}

MathHelper::square(4); // 16 — no instantiation needed


Static properties


These are shared state across all instances of a class — essentially a class-level variable.

class Counter {
   private static int $count = 0;

   public static function increment(): void {
       self::$count++;
   }

   public static function get(): int {
       return self::$count;
   }
}


This is occasionally useful but dangerous — static properties are global mutable state in disguise. They persist across the entire request lifecycle, bleed between tests, and create hidden coupling. Use very rarely.

When static methods make sense
Pure functions with no state or configuration:

class Slug {
   public static function from(string $title): string {
       return strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title)));
   }
}


No object needed, no config, same input always gives same output. Static is honest here.
Named constructors (factory methods):

class Color {
   private function __construct(
       private int $r, private int $g, private int $b
   ) {}

   public static function fromHex(string $hex): self { ... }
   public static function fromRgb(int $r, int $g, int $b): self { ... }
}


This is one of the best uses of static — it gives you multiple readable ways to construct an object.

When to avoid static
When it makes testing hard — you can’t mock a static call. If PaymentGateway::charge($amount) is static, any class calling it is permanently coupled to that implementation.
When you need to swap implementations — static calls are hardcoded. Interfaces and dependency injection require instances.
When there’s hidden state involved — if a static method reads from a database, calls an API, or touches a file, it looks pure but isn’t. That’s the worst of both worlds.

// Looks innocent, is actually a hidden dependency
class User {
   public static function findById(int $id): self {
       return Database::query(...); // now you can't test without a DB
   }
}


The practical rule

 

|Situation                               |Use static?       |
|----------------------------------------|------------------|
|Pure transformation, no dependencies    |Yes               |
|Named constructors                      |Yes               |
|Needs config or varies by context       |No — inject it    |
|Touches external systems (DB, API, etc.)|No — use a service|
|Shared mutable state                    |Almost never      |

The tell: if you find yourself wanting to mock it in a test, it shouldn’t be static.​​​​​​​​​​​​​​​​

Recent Content
Drupal Topics