The with Keyword

Tier 1 · Story 11Complete

The with keyword in Tyhp provides a concise way to set properties on objects and structs. It is a binary operator that produces a fully initialized value in a single expression. Combined with readonly properties, clone ... with enables immutable update patterns.

The Three Forms of with

Tyhp supports three forms of the with keyword, distinguished by what appears on its left-hand side:

Basic Syntax

The with keyword follows a new expression, a clone expression, or an existing object/struct value, and is followed by a [...] list where keys are property names and values are the values to assign.

<?tyhp

class User {
    public string $name;
    public int $age;
    public string $role = 'user';

    public function __construct(string $name, int $age): void {
        $this->name = $name;
        $this->age = $age;
    }
}

// Using with after new
$user = new User('Alice', 30) with [
    role => 'admin'
];

// Using with after clone
$updated = clone $user with [
    name => 'Bob',
    age => 25
];

Compiled PHP Output

Emit depends on whether with is a statement or an expression, whether the properties are readonly, and the target PHP version.

Statement form (an assignment, or a bare $obj with [...]): non-readonly properties become direct assignments on the same variable — no temp, no helper.

<?php

$user = new User('Alice', 30);
$user->role = 'admin';

$updated = clone $user;
$updated->name = 'Bob';
$updated->age = 25;

Expression form (for example return clone $x with [...], or a with used as a value):

  • PHP 8.5: native clone($x, [...]) (including clone(new Color(), [...]) for new ... with)
  • PHP 8.2–8.4: \Tyhp\ObjectHelper::with(...)
<?php

// PHP 8.5 expression clone with
return clone($cfg, ['enabled' => false]);

// PHP 8.2–8.4 expression clone/new with (non-readonly)
return \Tyhp\ObjectHelper::with(clone $cfg, ['enabled' => false]);
return \Tyhp\ObjectHelper::with(new Widget(), ['color' => "blue"]);

Readonly clone ... with on PHP 8.2–8.4 (requires build.experimentalReadonlyCloneWith: true) emits a reflection IIFE: an anonymous subclass with $__tyhp_overrides, ReflectionClass::newInstanceWithoutConstructor(), a property copy via ReflectionObject, then clone $__wrapper. It is not a simple __clone() wrapper around the source object.

Readonly new ... with:

  • PHP 8.4: clone (new class(...) extends \Color { public function __clone(): void { ... } })
  • PHP 8.5: clone(new Color(), ['alpha' => 128])

In-Place with on an Existing Instance

When with is applied directly to an existing object instance (not preceded by new or clone), it mutates that instance in place and evaluates to the SAME instance. No clone is made and no new object is constructed — any other reference to the object sees the updated values. This is the form to use when you want to batch-assign several properties on an object you already hold.

<?tyhp

class User {
    public string $name;
    public int $age;
    public string $role = 'user';

    public function __construct(string $name, int $age): void {
        $this->name = $name;
        $this->age = $age;
    }
}

$user = new User('Alice', 30);

// In-place with: mutates $user and returns the same instance
$same = $user with [
    age => 31,
    role => 'admin'
];

// $same and $user are the SAME object
// $user->age is now 31 and $user->role is now 'admin'
// ($same === $user) is true

Compiled PHP Output for In-Place with

For an object instance, the in-place form compiles to direct property-assignment statements on the same variable. The expression's value is the object itself, so it can be assigned or chained without a clone or a constructor call.

<?php

$user = new User('Alice', 30);

// $user with [...] emits direct property assignments on the same variable
$user->age = 31;
$user->role = 'admin';
$same = $user;

// No clone, no new — $same and $user reference the same object

For a struct value (which is backed by a PHP associative array), the in-place form emits as an array replace/merge that writes the updated keys back into the same variable.

<?tyhp

struct Coordinates {
    float $lat;
    float $lng;
    float $alt = 0.0;
}

$coords = new Coordinates() with [lat => 40.7, lng => -74.0];

// In-place with on an existing struct value
$coords with [alt => 100.0];
<?php

$coords = ['lat' => 40.7, 'lng' => -74.0, 'alt' => 0.0];

// In-place struct with emits an array replace into the same variable
$coords = \array_replace($coords, ['alt' => 100.0]);

Note

Choose the form by intent: use new ... with or clone ... with when you want a brand-new value (the clone form preserves immutability of the source), and use $obj with [...] when you want to update the object you already hold and keep the same identity. Because the in-place form mutates the existing instance, it cannot assign readonly properties after construction — use clone $obj with [...] (or new ... with) for those.

Nested with Expressions

The with keyword supports nesting. Inner with expressions are processed before outer ones.

<?tyhp

class Config {
    public bool $debug = false;
    public string $env = 'dev';
}

class App {
    public Config $config;
    public string $name;
}

$app = new App() with [
    name => 'MyApp',
    config => clone $defaultConfig with [
        debug => true,
        env => 'production'
    ]
];
<?php

$app = new App();
$app->name = 'MyApp';
$app->config = \Tyhp\ObjectHelper::with(clone $defaultConfig, ['debug' => true, 'env' => 'production']);

with and readonly Properties

readonly properties cannot be assigned after construction. new ... with and clone ... with are allowed to set them on the new or cloned instance. Direct assignment and in-place $obj with [...] after construction are not.

On PHP 8.5+, clone ... with uses native clone($obj, [...]). On PHP 8.2–8.4, clone ... with on readonly properties requires build.experimentalReadonlyCloneWith: true in tyhp.json (the compiler emits a reflection IIFE: newInstanceWithoutConstructor, $__tyhp_overrides, then clone $__wrapper — not a simple __clone() wrapper around the source). new ... with on readonly does not need that flag (PHP 8.4 emits an anonymous class with __clone(); PHP 8.5 uses clone(new Color(), [...])). readonly plus with is the immutable-update pattern.

<?tyhp

class Point {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
        public readonly float $z = 0.0
    ): void {}
}

$p = new Point(1.0, 2.0);

$q = clone $p with [z => 5.0];
// $p is still Point(1.0, 2.0, 0.0)
// $q is Point(1.0, 2.0, 5.0)

// $p->z = 3.0;  // ERROR: readonly
// $p with [z => 3.0];  // ERROR after construction: in-place with cannot set readonly

with on Structs

Since structs compile to PHP associative arrays, the with keyword on structs compiles to \array_replace (not \array_merge).

<?tyhp

struct Coordinates {
    float $lat;
    float $lng;
    float $alt = 0.0;
}

$coords = new Coordinates() with [lat => 40.7, lng => -74.0];
$elevated = clone $coords with [alt => 100.0];
<?php

$coords = ['lat' => 40.7, 'lng' => -74.0, 'alt' => 0.0];
$elevated = \array_replace($coords, ['alt' => 100.0]);

Best Practices

Tip

Use with after new or clone to set properties concisely in a single expression.

Tip

Use readonly properties with clone ... with for immutable update patterns — the original object is never mutated.

Tip

Nest with expressions for deep property initialization instead of using multiple intermediate variables.

Tip

Use with on structs for functional-style updates — since structs are value types, this is both clean and efficient.

Tip

Prefer readonly over leaving properties mutable when you want immutability but still need functional updates via new/clone with.

Common Mistakes

Danger

Do not use in-place $obj with [...] on readonly properties — they cannot be mutated after construction. Use new ... with or clone ... with instead.

Danger

Do not assume $obj with [...] returns a copy — the in-place form mutates and returns the SAME instance. Use clone $obj with [...] when you need an independent copy that leaves the original unchanged.

Danger

Do not set private or protected properties via with from outside the class.

<?tyhp

class Example {
    public readonly string $id;
    public readonly string $name;

    public function __construct(string $id, string $name): void {
        $this->id = $id;
        $this->name = $name;
    }
}

$a = new Example('1', 'Alice');

// OK: clone ... with may set readonly properties on the copy
$c = clone $a with [name => 'Bob'];

// ERROR: in-place with cannot set readonly after construction
// $a with [name => 'Bob'];

// ERROR: 'nonExistent' is not a property of Example
// $d = clone $a with [nonExistent => 'value'];

Warning

On PHP 8.2–8.4, clone ... with on readonly properties requires build.experimentalReadonlyCloneWith: true in tyhp.json. PHP 8.5+ supports it natively. new ... with on readonly does not need the flag.

Compiler Errors

  • Setting a readonly property via in-place $obj with [...] after construction.
  • clone ... with on a readonly property on PHP < 8.5 without build.experimentalReadonlyCloneWith.
  • Setting a non-existent property via with.
  • Setting a private or protected property via with from outside the class.
  • Applying with to an expression that is not a new expression, a clone expression, or an addressable object/struct instance.