Classes
Classes, interfaces, abstract classes, traits, enums, properties, and inheritance.
Class declaration
<?php
class Point {
public $x;
public $y;
public function __construct($x, $y) {
$this->x = $x;
$this->y = $y;
}
public function magnitude() {
return sqrt($this->x * $this->x + $this->y * $this->y);
}
public static function origin() {
return new Point(0, 0);
}
}
Class, interface, trait, and method lookup is case-insensitive like PHP:
new point(), POINT::origin(), and $p->MAGNITUDE() resolve to Point and
its declared methods. Object properties remain case-sensitive, so $p->x and
$p->X are distinct property names.
Interfaces
<?php
interface Named {
public function name();
}
class Product implements Named {
public function name() { return "widget"; }
public function label() { return strtoupper($this->name()); }
}
- signature-only methods and PHP 8.4 property hook contracts; method and hook bodies are not allowed in interfaces
- interface inheritance flattened transitively with cycle detection
Interface properties must be hooked contracts. A concrete class can satisfy a { get; } contract with a public readable property, a { set; } contract with a public writable property, or both with an invariant public property. Get-only contracts allow covariant concrete types; set-only contracts allow contravariant concrete types.
<?php
interface HasName {
public string $name { get; set; }
}
class Product implements HasName {
public string $name = "widget";
}
Built-in interfaces
The compiler injects the following interfaces, available without any
implements declaration on the user side:
| Interface | Methods |
|---|---|
Traversable | (marker) |
Iterator extends Traversable | current(): mixed, key(): mixed, next(): void, valid(): bool, rewind(): void |
IteratorAggregate extends Traversable | getIterator(): Traversable |
OuterIterator extends Iterator | getInnerIterator(): ?Iterator |
RecursiveIterator extends Iterator | getChildren(): ?RecursiveIterator, hasChildren(): bool |
SeekableIterator extends Iterator | seek(int $offset): void |
Countable | count(): int |
ArrayAccess | offsetExists(mixed $offset): bool, offsetGet(mixed $offset): mixed, offsetSet(mixed $offset, mixed $value): void, offsetUnset(mixed $offset): void |
SplObserver | update(SplSubject $subject): void |
SplSubject | attach(SplObserver $observer): void, detach(SplObserver $observer): void, notify(): void |
Stringable | __toString(): string |
JsonSerializable | jsonSerialize(): mixed |
Throwable | getMessage(): string |
count($obj) automatically dispatches to Countable::count() when
$obj is an instance of a class implementing Countable.
Classes implementing ArrayAccess can use PHP subscript syntax:
$obj[$key] dispatches to offsetGet(), $obj[$key] = $value dispatches to
offsetSet(), isset($obj[$key]) dispatches to offsetExists(), and
unset($obj[$key]) dispatches to offsetUnset().
Serializable is intentionally not provided: it is deprecated since
PHP 8.1. Use __serialize / __unserialize magic methods instead
(when those land).
Type checks with instanceof
<?php
interface Renderable {
public function render();
}
class Widget {
public function render() { return "widget"; }
}
class Button extends Widget implements Renderable {}
$item = new Button();
echo ($item instanceof Button) ? "yes" : "no"; // yes
echo ($item instanceof Widget) ? "yes" : "no"; // yes
echo ($item instanceof Renderable) ? "yes" : "no"; // yes
$target = "Button";
echo ($item instanceof $target) ? "yes" : "no"; // yes
The runtime check uses emitted class metadata, so subclasses match parent classes and implemented interfaces. The left-hand side may be a direct object or a boxed mixed / nullable / union value; non-object payloads return false once any dynamic target has been validated. Supported targets are named classes/interfaces, self, parent, late-bound static, dynamic class/interface strings, and dynamic object expressions.
Abstract classes
<?php
abstract class BaseGreeter {
abstract public function label();
public function greet() { return "hi " . $this->label(); }
}
- cannot be instantiated
- abstract methods must be bodyless
- non-abstract classes may not have abstract methods
Abstract properties
An abstract class may declare a PHP 8.4 hooked property contract as abstract. The declaration has no default value or hook body, and every concrete subclass must redeclare the property with a compatible public/protected property. Static, final, private, and readonly hooked abstract properties are rejected.
<?php
abstract class Shape {
abstract public int $sides { get; set; }
}
class Square extends Shape {
public int $sides = 4;
}
The concrete redeclaration reuses the parent’s slot (offsets are stable across the inheritance chain), so the property is accessible to both parent and child methods. elephc supports hook contracts ({ get; }, { set; }, and { get; set; }) in abstract classes, interfaces, and traits; executable hook bodies are not implemented yet.
Final classes, methods, and properties
<?php
final class InvoiceNumber {
final public $value = 42;
final public function label() {
return "invoice:" . $this->value;
}
}
final classcannot be extendedfinalmethods cannot be overridden by subclassesfinalproperties cannot be redeclared by subclassesfinaldoes not change object layout or dispatch for normal callsabstract finalclasses and methods are rejectedfinal privatemethods emit a warning, matching PHP, because private methods are not overridden;__constructis the exceptionfinal privateproperties are rejected, matching PHP
Properties
public,protected,privatevisibility- Optional default values
- Optional type declarations, for example
public int $idorpublic ?string $email = null readonlyproperties (only assigned in__construct)finalproperties, which can be read normally but cannot be redeclared by subclasses- Static properties with
public static,protected static, orprivate static, including typed static properties readonly classmakes all instance properties readonly; static properties stay mutable
<?php
class User {
public int $id;
public string $name = "Ada";
public ?string $email = null;
public function __construct($id) {
$this->id = $id;
}
}
Property type declarations are checked at compile time for both instance and static properties. Defaults and later assignments must be compatible with the declared type, including constructor assignments through untyped parameters. Typed properties without an explicit default start in PHP’s uninitialized state; reading an instance or static property before the first assignment is a fatal runtime error, while assigning values such as 0, false, "", or null to compatible nullable storage initializes the slot normally. Nullable shorthand (?T) and union storage use the compiler’s boxed mixed representation internally. void and callable property types are rejected.
Property redeclaration
A child class may redeclare a property inherited from a non-private parent. The redeclaration is checked at compile time and must follow PHP rules:
- Visibility cannot be reduced (
public→protectedis rejected;protected→publicis allowed). - Declared types are invariant. A typed parent property must be redeclared with the same type. A typed parent property cannot become untyped, and an untyped parent property cannot gain a type in the child.
readonlyis monotonic — areadonlyparent property must stayreadonlyin the child. A non-readonly parent property may becomereadonlyin the child.- The by-reference qualifier on a property cannot change across inheritance.
finalparent properties cannot be redeclared.- The child shares the parent’s slot, so reads of the property from inherited methods see the child’s value.
<?php
class Base {
public int $value = 0;
}
class Child extends Base {
public int $value = 5;
}
echo (new Child())->value; // 5
Private parent properties are still considered separate slots in PHP, but elephc rejects same-named redeclarations through them; declare a different name in the child for now.
Static properties
Static properties use class-scoped storage and are accessed with ::.
<?php
class Counter {
public static int $count = 1;
public static function bump() {
self::$count = self::$count + 1;
return self::$count;
}
}
echo Counter::$count; // 1
Counter::$count = 5;
echo Counter::bump(); // 6
Supported receivers are ClassName::$prop, self::$prop, parent::$prop, and static::$prop. Static property visibility and declared types are checked at compile time. Typed static properties without defaults use the same uninitialized-read fatal as typed instance properties. Inherited static properties share the declaring class storage until a subclass redeclares the property. Redeclarations follow PHP rules: non-private inherited properties keep invariant declared types, cannot reduce visibility, and cannot override final properties. Private static properties redeclared in subclasses are independent slots; static::$prop is still late-bound and reports a fatal runtime error if the current method scope cannot access the matched private slot.
Static properties in elephc, like in PHP, are always mutable — even on a readonly class. PHP’s readonly modifier only constrains instance properties; declaring public readonly static is a compile error in both PHP and elephc.
Static array properties support direct element writes:
<?php
class Registry {
public static array $items = [];
}
Registry::$items[] = 10;
Registry::$items[0] = 12;
echo Registry::$items[0]; // 12
Constructor
Called automatically with new:
$p = new Point(3, 4);
Constructor property promotion is supported. Visibility or readonly before a constructor parameter declares a property and assigns the incoming argument at the start of __construct.
<?php
class User {
public function __construct(
public int $id,
private string $name = "Ada",
readonly ?int $rank = null
) {}
public function name() {
return $this->name;
}
}
$user = new User(7);
echo $user->id; // 7
echo $user->name(); // Ada
Promoted properties support public, protected, private, readonly, nullable and union type declarations, constructor parameter defaults, and by-reference parameters. Variadic promotion is rejected, matching PHP.
By-reference promoted properties are supported when the constructor argument is a variable:
<?php
class Counter {
public function __construct(public int &$value) {}
}
$value = 1;
$counter = new Counter($value);
$value = 2;
echo $counter->value; // 2
$counter->value = 3;
echo $value; // 3
By-reference promoted parameters may also have defaults. If no argument is passed, elephc creates a private reference cell for the default value; if a variable is passed, the promoted property aliases that variable. readonly by-reference promoted properties are rejected at compile time because construction would have to bind an indirect mutable alias to a readonly slot.
Instance methods and $this
Virtual dispatch for overrides. Private methods are not virtual.
Nullsafe access
Use ?-> when a receiver may be null:
<?php
echo $user?->profile?->name ?? "anonymous";
echo $user?->profile?->label() ?? "missing";
echo $user?->profile->address?->city ?? "unknown";
$segment = "profile";
echo $user?->{$segment}?->name ?? "anonymous";
When a nullsafe receiver is null, elephc skips the rest of that postfix chain and returns null. This matches PHP for mixed chains such as $user?->profile->address: the ordinary ->address segment is skipped when $user is null, but still warns or fatals normally if $user is non-null and profile itself is null. Method arguments, array indexes, and callable arguments on the skipped branch are not evaluated.
parent::method()
Direct parent implementation call.
self::method()
Binds to lexical class, not runtime child.
static::method()
Late static binding — resolves against called class at runtime.
Static methods
Called with ::, no $this.
Class name reflection (::class)
::class returns the fully-qualified class name as a string at compile time.
<?php
namespace App;
class Logger {
public static function tag() {
return self::class; // "App\Logger"
}
}
echo Logger::class; // "App\Logger"
echo \App\Logger::class; // "App\Logger"
Supported receivers: Class::class, \Vendor\Class::class, self::class, parent::class, static::class.
static::class follows PHP late static binding and resolves to the called class.
For named receivers, elephc preserves PHP’s written/imported spelling for the
::class string while still using case-insensitive class lookup for executable
operations such as new, instanceof, static method calls, and static property
access.
Late static binding constructors (new self(), new static(), new parent())
The new self(), new static(), and new parent() factory patterns are supported inside class methods:
<?php
class Box {
public string $label = "default";
public static function make(): Box {
return new self();
}
}
$b = Box::make();
echo $b->label; // "default"
class Base {
public string $kind = "base";
}
class Child extends Base {
public static function makeBase(): Base {
return new parent();
}
}
new static() follows PHP late static binding and constructs an instance of the called class.
Override rules
Same parameter count, same pass-by-reference positions, same default layout, same variadic shape.
Traits
Flattened at compile time. Support: use Trait;, multiple traits, insteadof, as, trait properties, static trait methods.
Traits may declare abstract hooked property contracts. A concrete class using the trait must satisfy the contract directly or inherit it through an abstract base class that is later completed by a concrete child.
Property access
-> for properties and methods.
Dynamic property access
The property name can be computed at runtime with the $obj->{$expr} syntax,
where $expr is any expression that evaluates to a string. The same form works
as an assignment target and combines with the nullsafe operator ($obj?->{$expr}).
<?php
class Point {
public int $x = 1;
public int $y = 2;
}
$p = new Point();
$field = "x";
echo $p->{$field}; // 1
$p->{$field} = 9;
echo $p->x; // 9
Enums
<?php
enum Color: int {
case Red = 1;
case Green = 2;
}
echo Color::Red->value; // 1
echo Color::from(2) === Color::Green; // 1
Pure and backed enums. ->value, ::from(), ::tryFrom(), ::cases(). Only int and string backing types.
Magic methods
__toString()— string coercion__get($name)— reading undefined property__set($name, $value)— writing undefined property__invoke(...$args)— calling an object directly__call($name, $args)— intercepting missing instance methods
Attributes
PHP 8.0 attributes (#[Name]) decorate declarations. elephc parses attributes at every site PHP allows: classes, interfaces, traits, enums, enum cases, top-level functions, methods, properties, function/method/closure parameters (incl. promoted constructor params), closures, and arrow functions. Class, method, and property attributes have limited runtime reflection through the helpers below; attributes on other declaration sites are currently validated for syntax and kept only in the AST.
<?php
#[Author("Ada"), Version(1)]
class Greeter {
#[Slot]
public string $who;
public function __construct(#[Required] string $who) {
$this->who = $who;
}
#[Pure]
public function greet(): void { echo "Hello"; }
}
class LoudGreeter extends Greeter {
#[\Override]
public function greet(): void { echo "HELLO"; }
}
$pure = #[Pure] fn (int $x) => $x + 1;
#[Memoized]
function double(int $x): int { return $x * 2; }
Supported syntax:
- single attribute:
#[Foo] - attribute with arguments:
#[Bar(1, "two")] - multiple attributes per group:
#[A, B(1)] - stacked groups:
#[A] #[B] - fully-qualified names:
#[\Symfony\Contracts\Service\Attribute\Required]
# outside an attribute group introduces a PHP-style line comment, identical to //. Attributes before non-declaration statements (echo, if, assignments) are rejected — PHP’s strict rule.
Compile-time enforced attributes
#[\Override](PHP 8.3) — the type checker verifies that the marked method actually overrides a method declared in a parent class or implemented interface (transitively). A typo in the method name or a missing parent method becomes a compile-time error:<class>::<method>() has #[\Override] attribute, but no matching parent method was found. Both the unqualified#[Override]and fully-qualified#[\Override]forms are recognized.#[\Deprecated]/#[\Deprecated("reason")](PHP 8.4) — calls to the marked function, method, or static method emit a compile warning:Call to deprecated function: name() — reason. The reason argument (if a string literal) is appended to the message.#[\AllowDynamicProperties](PHP 8.2) — instances of the marked class accept assignment of undeclared properties at runtime. Each instance carries a per-object hashtable side-table allocated by the constructor (~296 bytes); the type checker accepts undeclared reads asmixed. The hashtable is freed automatically with the object.
Built-in attributes follow PHP class-name resolution. In a namespace, #[Deprecated] means #[CurrentNamespace\Deprecated]; use #[\Deprecated] or an import alias such as use Deprecated as Old; #[Old] to target the global built-in attribute.
<?php
#[\AllowDynamicProperties]
class Bag {
public int $declared = 1;
}
$b = new Bag();
$b->extra = 42; // accepted, stored in side-table
$b->name = "elephc"; // heterogeneous values supported
echo $b->declared; // 1
echo $b->extra; // 42
echo $b->name; // "elephc"
echo $b->missing; // empty (Mixed null)
User-defined attributes (e.g. #[Author], #[Pure], #[Memoized]) parse and persist in the AST. They have no compile-time semantics, but their names and positional literal arguments are reachable at runtime through lightweight helper builtins and the supported Reflection API:
<?php
#[Author("Ada"), Version(1)]
class Greeter {}
#[\Override]
class Solo {}
#[Route("/api/users", "GET", true)]
class UserController {}
foreach (class_attribute_names('Greeter') as $name) {
echo $name, "\n";
}
// Author
// Version
echo class_attribute_names('Solo')[0]; // "Override" (resolved name)
foreach (class_attribute_args('UserController', 'Route') as $arg) {
echo $arg, "\n";
}
// /api/users
// GET
// 1 ← `true` echoes as 1 in PHP
class_attribute_args() returns an array<mixed> whose elements preserve their original PHP type — strings stay strings, ints stay ints, booleans stay booleans, and null is null. The args are interned at compile time and boxed into mixed cells on demand at the call site.
For a more PHP-idiomatic API, class_get_attributes() and ReflectionClass::getAttributes() return the same data wrapped as ReflectionAttribute instances:
<?php
#[Author("Ada", 1815), Version("1.0", true)]
class Greeter {}
foreach (class_get_attributes('Greeter') as $attr) {
echo $attr->getName(), ": ";
foreach ($attr->getArguments() as $arg) {
echo "[", $arg, "]";
}
echo "\n";
}
// Author: [Ada][1815]
// Version: [1.0][1]
Reflection is also available for class members:
<?php
class Controller {
#[Route("/home", "GET")]
public function index() {}
#[Column("id")]
public int $id = 0;
}
$class = new ReflectionClass(Controller::class);
echo $class->getAttributes()[0]->getName();
$method = new ReflectionMethod('Controller', 'index');
echo $method->getAttributes()[0]->getArguments()[0]; // /home
$property = new ReflectionProperty('Controller', 'id');
echo $property->getAttributes()[0]->getName(); // Column
ReflectionAttribute is a final synthetic built-in class with getName(): string, getArguments(): array, and newInstance(): mixed methods. It is populated internally by class_get_attributes() and the supported Reflection lookups and cannot be constructed or populated directly from user code; its metadata slots are private. newInstance() constructs the attribute class on demand when the attribute class exists in the program and the captured arguments are supported literals:
<?php
class Route {
public function __construct(string $path) {
echo $path;
}
}
#[Route("/lazy")]
class Controller {}
$attr = (new ReflectionClass('Controller'))->getAttributes()[0];
$instance = $attr->newInstance(); // constructor runs here
echo ($instance instanceof Route) ? "yes" : "no";
| Function | Signature | Description |
|---|---|---|
class_attribute_names() | class_attribute_names($class_name): array | Return the resolved attribute names decorating the class |
class_attribute_args() | class_attribute_args($class_name, $attribute_name): array | Return the supported literal positional arguments for the first matching class attribute |
class_get_attributes() | class_get_attributes($class_name): array | Return ReflectionAttribute objects for the class attributes |
| Reflection method | Supported constructor | Description |
|---|---|---|
ReflectionClass::getAttributes() | new ReflectionClass($class_name) | Return ReflectionAttribute objects for class attributes |
ReflectionMethod::getAttributes() | new ReflectionMethod($class_name, $method_name) | Return ReflectionAttribute objects for method attributes |
ReflectionProperty::getAttributes() | new ReflectionProperty($class_name, $property_name) | Return ReflectionAttribute objects for property attributes |
ReflectionAttribute::newInstance() | Internal only | Instantiate the attribute class from captured literal args |
Limitations today:
- All arguments to
class_attribute_names(),class_attribute_args(),class_get_attributes(), andnew ReflectionClass/Method/Property(...)must be compile-time class/member strings.ClassName::classis accepted for the class-name argument ofnew ReflectionClass/Method/Property(...), and normal named-argument / static associative-spread normalization runs before the literal-string check. Dynamic class, method, property, or attribute names require a runtime name→id lookup table that is not yet implemented. - Only literal positional arguments are materialized by reflection helpers today (string, int, bool, null, plus
-Nfor negative ints). Other legal PHP attribute arguments can still be parsed and compiled, andclass_attribute_names()can still list the attribute name, butclass_attribute_args(),class_get_attributes(), and ReflectiongetAttributes()report an error if they would need unsupported argument metadata. - When several attributes share a name on the same class,
class_attribute_args()returns the args of the first match;class_get_attributes()does expose every occurrence as a separateReflectionAttributein source order. - Only
getAttributes()is implemented onReflectionClass,ReflectionMethod, andReflectionProperty; broader APIs such asgetProperties(),getMethods(), and object construction throughReflectionClass::newInstance()are not yet available.
Class constants
<?php
class Math {
const PI = 314;
public const E = 271;
const TAU = self::PI * 2;
}
echo Math::PI; // 314
echo self::PI; // inside Math methods
interface Limits {
const MAX = 100;
}
class Bound implements Limits {
public function get(): int { return Limits::MAX; }
}
Class constants (PHP 7.1+ visibility, PHP 8.1+ final) live on classes, interfaces, and traits. They are inherited from parents and implemented interfaces (transitively). At codegen time elephc inlines the constant’s foldable value at every access site — there is no runtime lookup. Class constant expressions may reference other class constants through ClassName::CONST, self::CONST, or parent::CONST; self::class and parent::class are also accepted. self:: and parent:: are early-bound to the declaring class, matching PHP. static::CONST is rejected in class constant expressions because PHP does not allow late-static binding in compile-time constants. Attributes on class constants are accepted and stored in the AST.
Limitations
readonly staticproperties are rejected to match PHP. Static properties in areadonly classare still mutable.- Property hook bodies are not implemented; elephc supports hook contracts only.
- Shadowing a private parent property with a same-named child property is not yet supported (PHP gives them separate slots; elephc uses one slot per name)
- Class constants must be literal-or-foldable expressions; cyclic constant references are not supported.
- Anonymous classes (
new class { ... }) are not yet supported. - Class attribute names and supported literal args are exposed at runtime through
class_attribute_names(),class_attribute_args(),class_get_attributes(), and the supportedReflectionClass/ReflectionMethod/ReflectionProperty::getAttributes()APIs; parameter reflection is not yet available.#[\Override],#[\Deprecated], and#[\AllowDynamicProperties]are enforced/diagnosed/honored at compile time and runtime;#[\SensitiveParameter]is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending).