In PHP, how do a private constructor and static named constructors like Money::fromCents() work together, and what does the private constructor enforce?
answer
- only one constructor per class
- new from outside throws an Error
- static methods share private access
- each entry point states its units
- clone and reflection still bypass it
basics
~20 sMake __construct private and expose public static methods such as Money::fromCents() that call new self(...). Outside code can then only create objects through those named entry points, while the private constructor keeps a single validation path.
solid answer
~40 sPHP allows only one constructor per class, so a type with several meaningful inputs uses **static creation methods** (named constructors): `Money::fromCents(1999, 'EUR')`, `Money::fromDecimal('19.99', 'EUR')`, `Money::zero('EUR')`. Each converts its input and calls `new self(...)`. Declaring `__construct()` **private** means `new Money(...)` from outside the class throws `Error: Call to private Money::__construct() from global scope`, so every object passes through a named entry point whose name states the unit, and through one constructor that checks invariants. Static methods of the same class can call the private constructor because visibility is per class, not per instance. Two caveats: a private constructor is not a security boundary, since `clone`, deserialization and `ReflectionClass::newInstanceWithoutConstructor()` create objects without calling it; and value objects built this way are usually `final` and use `new self`, so no subclass can reinterpret the constructor.
code
php · 34 lines<?php
declare(strict_types=1);
final class Money
{
private function __construct(
private int $cents,
private string $currency,
) {
if (!preg_match('/^[A-Z]{3}$/', $currency)) {
throw new InvalidArgumentException("Bad currency: $currency");
}
}
public static function fromCents(int $cents, string $currency): self
{
return new self($cents, $currency);
}
public static function zero(string $currency): self
{
return new self(0, $currency);
}
public function cents(): int { return $this->cents; }
}
$fee = Money::fromCents(250, 'EUR');
try {
new Money(250, 'EUR');
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL; // Call to private Money::__construct() from global scope
}go deeper
Recall that a private __construct blocks new from outside the class and that public static methods can still create instances.
Explain why PHP's single constructor leads to named constructors, how class-scoped visibility lets them call the private constructor, and the exact Error raised otherwise.
Design value objects so every entry point funnels into one validating constructor, and know the paths that bypass it: clone, deserialization and reflection.
Decide where named constructors are required for domain types, balancing explicit units and invariants against the extra API surface teams must maintain.
## The problem: one constructor, many meanings A `Money` value object in a banking app can be built from different inputs: an integer number of minor units (cents), a decimal string from a form or a file, or the zero amount. PHP supports **a single constructor per class** and no overloading, so a public constructor `new Money(1999, 'EUR')` leaves every caller to guess whether `1999` is cents or euros. Mistaking one for the other is a factor-of-100 bug in a ledger. ## The pattern in PHP 1. Declare `private function __construct(private int $cents, private string $currency)` and put **all invariant checks** there (currency code shape, allowed range). 2. Add public static methods with descriptive names that prepare their input and call `new self(...)`: - `fromCents(int $cents, string $currency): self` - `fromDecimal(string $amount, string $currency): self` which parses and converts to cents - `zero(string $currency): self` 3. Mark the class `final` unless it is designed for extension. The manual calls these **static creation methods** and describes this exact structure: with a private or protected constructor, only a static method can instantiate the class, and because it is in the same class definition it may call private methods even on other instances. ## What the private constructor enforces | Caller | `new Money(...)` | `Money::fromCents(...)` | |---|---|---| | code outside the class | `Error: Call to private Money::__construct() from global scope` | allowed | | a method of another class | `Error: Call to private Money::__construct() from scope Ledger` | allowed | | a static or instance method of `Money` | allowed | allowed | So the guarantees are: - **Named intent**: callers must choose `fromCents` or `fromDecimal`, which removes the unit ambiguity. - **One validation path**: every named constructor funnels into the same private constructor, so checks are written once. - **Freedom to change**: the constructor's parameters are internal and can change without breaking callers, as long as the named methods keep their signatures. Instance methods can also use the private constructor, which is how operations return new values: `public function add(Money $other): self { return new self($this->cents + $other->cents, $this->currency); }` after checking that currencies match. ## What it does not enforce The private constructor controls `new`, not object existence: - `clone $money` creates a copy without calling any constructor; - deserialization restores objects without calling the constructor; - `ReflectionClass::newInstanceWithoutConstructor()` creates an instance with typed properties uninitialized. These are not attacks in normal code, but they explain why an invariant that must hold everywhere is often also protected by `readonly` properties and by rejecting untrusted serialized input. ## `new self` versus `new static` Inside a named constructor, `new self(...)` always creates the class the method is written in. `new static(...)` creates the class the method was **called on**, which lets a subclass inherit the factory but also requires every subclass constructor to accept the same arguments, a contract nothing enforces. For a value object like `Money`, a `final` class with `new self` avoids the question entirely; late static binding itself is a separate topic. ## Naming conventions Common prefixes carry meaning that readers learn quickly: - `from...` converts from another representation: `fromCents()`, `fromDecimal()`, `fromArray()`. - `of()` or `create()` builds from the natural components when there is only one obvious way. - a noun for well-known values: `zero()`, `none()`, `today()`. - `try...` returns `null` instead of throwing when invalid input is expected, mirroring the `tryFrom()` of backed enums. Whatever the convention, a return type of `self` (or `static` when subclassing is intended) keeps static analysis aware of the concrete type. ## Practical guidance - Keep named constructors thin: convert input, delegate to the constructor. - Validate in the constructor, not in each named method, so no path can skip a check. - Give each method a name that states its unit or source, which is the whole point. - Do not add a public constructor "for convenience"; it reintroduces the ambiguity.
- Why can `Money::fromCents()` call the private constructor when outside code cannot?PHP checks visibility against the class in which the calling code is written, not against a particular instance. A static method of `Money` runs in `Money`'s scope, so it may call `Money`'s private methods, including `__construct()`, and may even read private properties of other `Money` instances.
- Is the error from `new Money(...)` outside the class an `Exception`?No. It is an `Error` with the message `Call to private Money::__construct() from global scope` (or `from scope X` inside another class). `catch (Exception $e)` does not catch it; `Error` and `Exception` only share the `Throwable` interface.
saying these in an interview costs you the question
- PHP lets you overload __construct with different parameter lists.
- A private constructor also stops clone from creating copies.
- Calling a private constructor from outside throws an Exception you can catch as Exception.
- Static methods cannot call a private constructor because they have no $this.
- A private constructor guarantees no instance can exist without passing validation.