PHP 8.5's `clone with`: Immutable Objects Without the Boilerplate
PHP 8.5 adds clone with syntax to update readonly properties while cloning. Learn how to ditch boilerplate wither methods and write cleaner immutable objects.
If you have been using readonly properties since PHP 8.1, you have almost certainly written a method like this:
public function withEmail(string $email): self
{
$values = get_object_vars($this);
$values['email'] = $email;
return new self(...$values);
}
This is the “with-er” pattern: create a copy of an immutable object with one property changed, leaving the original untouched. It works fine, but as soon as a class grows beyond three or four properties, the boilerplate becomes genuinely tedious to write and maintain. PHP 8.5 solves this with a new clone with syntax that cuts all of it down to a single line.
What the Old Pattern Looks Like at Scale
To appreciate why this matters, consider a common UserProfile value object:
readonly class UserProfile
{
public function __construct(
public string $name,
public string $email,
public string $timezone,
public string $locale,
public bool $newsletterOptIn,
) {}
public function withName(string $name): self
{
return new self(
name: $name,
email: $this->email,
timezone: $this->timezone,
locale: $this->locale,
newsletterOptIn: $this->newsletterOptIn,
);
}
public function withEmail(string $email): self
{
return new self(
name: $this->name,
email: $email,
timezone: $this->timezone,
locale: $this->locale,
newsletterOptIn: $this->newsletterOptIn,
);
}
// ... one method per property
}
Five properties means five methods, and each one manually threads all the other values through to the constructor. Adding a sixth property means touching every single method. This is the kind of mechanical work that makes developers reach for mutable classes just to avoid the maintenance burden.
The PHP 8.5 Solution
PHP 8.5 adds a new overload to the clone keyword. You can now pass an associative array of property overrides as a second argument:
readonly class UserProfile
{
public function __construct(
public string $name,
public string $email,
public string $timezone,
public string $locale,
public bool $newsletterOptIn,
) {}
public function withName(string $name): self
{
return clone($this, ['name' => $name]);
}
public function withEmail(string $email): self
{
return clone($this, ['email' => $email]);
}
}
PHP copies all properties from the original object, then applies only the overrides you specified. The original object is never modified. You get a fresh instance with the changes applied and everything else preserved automatically.
You can also override multiple properties in a single clone call:
public function withContactInfo(string $name, string $email): self
{
return clone($this, [
'name' => $name,
'email' => $email,
]);
}
Working with Readonly Classes Internally
The clone with syntax has full access to readonly properties when called from inside the class itself. PHP unlocks the readonly constraint specifically during the clone operation, so all the normal write restrictions apply everywhere else.
Here is a practical example with a Money value object:
readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Cannot add different currencies');
}
return clone($this, ['amount' => $this->amount + $other->amount]);
}
public function convertTo(string $currency, float $rate): self
{
return clone($this, [
'amount' => (int) round($this->amount * $rate),
'currency' => $currency,
]);
}
}
$price = new Money(1000, 'USD');
$tax = new Money(80, 'USD');
$total = $price->add($tax); // Money { amount: 1080, currency: 'USD' }
The original $price is unchanged. Each operation returns a new object. This is the classic immutable value object pattern, and PHP 8.5 makes it this clean without any extra packages or workarounds.
External Cloning and Visibility
There is an important nuance: cloning readonly properties from outside the class requires those properties to have a public write visibility. By default, a readonly property in a readonly class has no external write access.
If you want external code to be able to clone-with a specific property, you can use asymmetric visibility syntax from PHP 8.4:
readonly class Config
{
public function __construct(
public string $environment,
public(set) bool $debug, // explicitly writable externally
) {}
}
$config = new Config('production', false);
$devConfig = clone($config, ['debug' => true]);
Without the public(set) modifier, trying to override debug from outside the class would throw an error. This is intentional behavior: the visibility rules you set are respected during cloning. Readonly is unlocked, but visibility is not changed.
Laravel: Cleaner Request DTOs
The Laravel community has already moved toward readonly DTOs for passing validated request data through the application. Before PHP 8.5, transforming a DTO mid-pipeline required either accepting mutability or writing manual wither methods:
readonly class CreatePostRequest
{
public function __construct(
public string $title,
public string $body,
public ?string $slug,
public bool $published,
) {}
public function withGeneratedSlug(string $slug): self
{
return clone($this, ['slug' => $slug]);
}
}
In a service class, the pipeline stays immutable all the way through:
class CreatePostService
{
public function handle(CreatePostRequest $request): Post
{
if ($request->slug === null) {
$slug = Str::slug($request->title);
$request = $request->withGeneratedSlug($slug);
}
return Post::create([
'title' => $request->title,
'body' => $request->body,
'slug' => $request->slug,
'published' => $request->published,
]);
}
}
The incoming request object is never modified. The pipeline is easy to test because each step returns a new, well-defined state.
Symfony: Readonly Commands and Messages
Symfony developers working with the Messenger component often use readonly classes for messages and commands. PHP 8.5 makes the pattern even cleaner:
readonly class SendEmailMessage
{
public function __construct(
public string $to,
public string $subject,
public string $body,
public ?string $replyTo = null,
public int $retryCount = 0,
) {}
public function withRetry(): self
{
return clone($this, ['retryCount' => $this->retryCount + 1]);
}
}
A retry handler can produce a new message with an incremented counter without touching the original:
class RetryEmailHandler
{
public function handle(SendEmailMessage $message): void
{
if ($message->retryCount >= 3) {
throw new MaxRetriesExceededException();
}
$this->bus->dispatch($message->withRetry());
}
}
The __clone Hook Still Works
If your class uses a __clone method, it still fires when using the new syntax. The properties from the override array are applied after __clone runs, so the lifecycle is:
- PHP copies the object’s current state
__clone()executes on the new copy- The property overrides from the array are applied
This means you can still use __clone for side effects like resetting a unique ID or clearing an internal cache, and the clone with overrides will layer on top of that.
When Should You Use It?
Reach for clone with whenever you have a class where:
- Properties are readonly (or the class is declared
readonly) - You need to produce modified copies without changing the original
- You find yourself writing
new self(/* all the values */)to simulate mutation
It is the right tool for value objects, DTOs, command and event messages, configuration containers, and any other structure where immutability is a design goal rather than an accident.
If your class is mutable and you just want to copy it, the plain clone keyword is still fine. PHP 8.5 does not change that behavior at all.
Sources
- RFC: clone with v2 - PHP Wiki
- What’s new in PHP 8.5 - stitcher.io
- Update Properties While Cloning Object in PHP 8.5 - Lindevs
- PHP 8.5 Stable Release: ‘Clone With’, #[NoDiscard], and New URL Classes - Jose Jimenez
- PHP 8.5 “Clone With”: Immutable Objects Without the Boilerplate - Medium
- PHP 8.5 Release Announcement - php.net