Functions & OOP in PHP
Typed functions, classes, constructor property promotion, interfaces, and traits.
Functions with typed parameters and return types
Modern PHP lets you declare types for parameters and return values — the engine enforces them at runtime (a TypeError is thrown on mismatch), which catches a large class of bugs that used to only surface much later.
<?php
function calculateTotal(float $price, int $quantity, float $taxRate = 0.0): float {
$subtotal = $price * $quantity;
return $subtotal + ($subtotal * $taxRate);
}
echo calculateTotal(19.99, 3); // 59.97
echo calculateTotal(19.99, 3, 0.1); // 65.967
<?php
function findUser(int $id): ?string { // ?string means "string or null" (a nullable return type)
$users = [1 => "Ada", 2 => "Grace"];
return $users[$id] ?? null;
}
var_dump(findUser(1)); // string(3) "Ada"
var_dump(findUser(99)); // NULL
Passing the wrong type where PHP can't coerce it throws immediately:
<?php
function greet(string $name): string {
return "Hello, $name!";
}
greet(42); // fine — PHP coerces int to string in non-strict mode
declare(strict_types=1); // placed at the top of a file, this disables that coercion
Adding declare(strict_types=1); as the very first line of a file disables PHP's automatic type coercion for that file's function calls — passing an int where a string is declared then throws a TypeError instead of silently converting. Most modern, professional PHP codebases enable this everywhere.
Classes
<?php
class BankAccount {
private float $balance;
public function __construct(float $initialBalance = 0.0) {
$this->balance = $initialBalance;
}
public function deposit(float $amount): void {
if ($amount <= 0) {
throw new InvalidArgumentException("Amount must be positive");
}
$this->balance += $amount;
}
public function getBalance(): float {
return $this->balance;
}
}
$account = new BankAccount(100.0);
$account->deposit(50.0);
echo $account->getBalance(); // 150
Constructor property promotion (PHP 8+)
Instead of declaring a property, then a constructor parameter, then manually assigning one to the other, PHP 8 lets you do all three in one place:
<?php
// Before PHP 8 — three separate steps
class Employee {
private string $name;
private float $salary;
public function __construct(string $name, float $salary) {
$this->name = $name;
$this->salary = $salary;
}
}
// PHP 8+ — constructor property promotion collapses all three into the signature
class Employee {
public function __construct(
private string $name,
private float $salary,
) {}
}
$employee = new Employee("Ada", 85000.0);
Adding a visibility modifier (private, protected, or public) directly on a constructor parameter is what triggers promotion — PHP declares the property and assigns it automatically, with no body needed at all if there's nothing else to do.
Interfaces
An interface defines a contract — method signatures a class must implement, with no implementation of its own. A class can implement multiple interfaces, even though PHP (like Java) only allows extending one parent class.
<?php
interface Payable {
public function calculatePay(): float;
}
class HourlyEmployee implements Payable {
public function __construct(
private float $hoursWorked,
private float $hourlyRate,
) {}
public function calculatePay(): float {
return $this->hoursWorked * $this->hourlyRate;
}
}
function printPaycheck(Payable $employee): void { // works with ANY Payable implementation
echo $employee->calculatePay();
}
printPaycheck(new HourlyEmployee(40, 25.0)); // 1000
Traits
A trait is a chunk of reusable method implementations you can mix into multiple, otherwise unrelated classes — PHP's answer to not having multiple inheritance, since a class can only extends one parent but can use many traits.
<?php
trait Loggable {
public function log(string $message): void {
echo "[" . static::class . "] " . $message . "\n";
}
}
trait Timestamped {
private ?DateTimeImmutable $createdAt = null;
public function markCreated(): void {
$this->createdAt = new DateTimeImmutable();
}
}
class Order {
use Loggable, Timestamped; // mix in behavior from both traits
}
$order = new Order();
$order->log("Order placed"); // [Order] Order placed
$order->markCreated();
If two traits used in the same class define a method with the same name, PHP raises a fatal error unless you explicitly resolve the conflict with insteadof/as — this is rare in practice, but worth knowing exists:
<?php
trait A {
public function hello(): string { return "Hello from A"; }
}
trait B {
public function hello(): string { return "Hello from B"; }
}
class Greeter {
use A, B {
A::hello insteadof B; // explicitly pick A's version for the conflicting method
B::hello as helloFromB; // and still expose B's version under a new name
}
}
Common mistakes
- Forgetting
declare(strict_types=1);and being surprised when a wrong-typed argument gets silently coerced instead of raising aTypeError. - Reaching for a trait to share state and behavior across classes that aren't really related at all — a trait should model "this class has this reusable capability," not a substitute for a shared parent class or a proper collaborator object.
- Forgetting an interface only declares method signatures — it can't hold implementation or (traditionally) any state; that's what a trait or abstract class is for.
Interview questions
Q: What's the difference between a trait and an interface?
An interface only declares a contract (method signatures) with no implementation — a class implementing it must provide its own code for every method. A trait provides actual, reusable method implementations that get copied into any class that uses it, which is how PHP works around not supporting multiple class inheritance.
Q: What does constructor property promotion do?
It lets you declare a class property, a constructor parameter, and the assignment of one to the other in a single line, by adding a visibility modifier directly to a constructor parameter — PHP generates the property declaration and the $this->x = $x assignment automatically, cutting out a lot of repetitive boilerplate for simple data-holding classes.