rfc:short-and-inner-classes

This is an old revision of the document!


PHP RFC: Inner Classes with Short Syntax

Introduction

This RFC proposes two significant enhancements to the language: Short Class Syntax and Inner Classes. These features aim to streamline class definitions, reduce boilerplate, and introduce a new level of encapsulation and organization within PHP applications.

Short Class Syntax allows developers to define simple or data-oriented classes in a single line, leveraging constructor property promotion to enhance readability and maintainability. Inner Classes, on the other hand, enable the definition of within other classes, providing fine-grained visibility control and fostering better modularization.

By adopting these enhancements, PHP can offer developers more powerful tools to write clean, efficient, and well-structured code, ultimately improving the overall developer experience and code quality.

Proposal

Short Class Syntax

The proposed syntax for defining a short class consists of the class keyword, followed by the class name, and a list of properties in parentheses. Optionally, traits, interfaces, and a parent class can be specified.

// a simple class with two public properties
class Point(int $x, int $y);
 
// A readonly class with a parent class, interface, and traits
readonly class Vector(int $x, private int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable;

This is equivalent to the following full class definition:

class Point {
    public function __construct(public int $x, public int $y) {}
}
 
readonly public class Vector extends BaseVector implements JsonSerializable {
    use PointTrait, Evolvable;
 
    public function __construct(public int $x, private int $y) {}
}

Properties inside parentheses are automatically declared as class properties and default to public unless explicitly specified.

Default Values

Properties with type hints may have default values:

class Point(int $x = 0, int $y = 0);

Inheritance and Behavior

Short classes can extend other classes, implement interfaces, and use traits, but they cannot define additional methods except by using traits. If the parent class has a constructor, the constructor will be overridden and not called.

class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable;

Empty Classes

Short classes may be empty, in which case, the constructor is not overridden. This can be useful in some cases, such as when you want to define a simple exception:

class HttpError() extends Exception;
 
throw new HttpError('Not Found', 404);

Attributes

Attributes can be used with short classes:

#[MyAttribute]
class Password(#[SensitiveParameter] string $password);

Modifiers

Short classes support readonly, final, and abstract:

readonly class User(int $id, string $name);
 
final class Config(string $key, mixed $value);
 
abstract class Shape(float $area);

How it works

Short classes are purely syntactic sugar and compiled into standard class definitions during compile time.

Inner Classes

Inner classes allow defining classes within other classes, following standard visibility rules. This allows developers to declare a class as private or protected and restrict its usage to the outer class. Inner classes may only be nested one level deep, may not be a parent class, and may not be declared abstract:

class Outer {
    class Inner(public string $message); // using short syntax
 
    // using traditional syntax
    private class PrivateInner {
        public function __construct(public string $message) {}
    }
}
 
$foo = new Outer::Inner('Hello, world!');
echo $foo->message;
// outputs: Hello, world!
$baz = new Outer::PrivateInner('Hello, world!');
// Fatal error: Uncaught Error: Cannot access private inner class Outer::PrivateInner 

Modifiers

Inner classes support modifiers such as public, protected, private, final and readonly. When using these as modifiers on an inner class, there are some intuitive rules:

  • public, private, and protected apply to the visibility of the inner class.
  • final, and readonly apply to the class itself.
  • static is not allowed as a modifier since PHP does not support static classes and isn’t a property.
  • abstract is not allowed as an inner class cannot be parent classes.

Visibility Rules

Private and protected inner classes are only instantiatable within their outer class (or subclasses for protected) and may not be used as type hints outside their outer class.

For example, you may return a private inner class from any method inside that same inner class or the outer class, but you may not use it as a type hint in a function outside the outer class:

class Box {
  private class Point(int $x, int $y);
 
  private self::Point $center;
 
  public function __construct() {
    $this->center = new self::Point(0, 0);
  }
 
  public function getCenter(): self::Point {
    return $this->center;
  }
 
  public function setCenter(self::Point $center) {
    $this->center = $center;
  }
}
 
$box = new Box();
$center = $box->getCenter();
$center->x = 10;
$box->setCenter($center);
var_dump($box);

Outputs:

object(Box)#1 (1) {
  ["center":"Box":private]=>
  object(Box::Point)#2 (2) {
    ["x"]=>
    int(10)
    ["y"]=>
    int(0)
  }
}

However, if we try to use it outside the outer class as a type hint:

function mutateBox(Box::Point $point): Box::Point {
    $point->x = 10;
}
 
$center = mutateBox($center);

We receive the following error:

PHP Fatal error:  Private inner class Box::Point cannot be used in the global scope

This gives a great deal of control to developers, preventing accidental misuse of inner classes. Developers may have the inner class implement an interface so that the programmer must code to the interface, allowing large projects to enforce a consistent API.

Inheritance

Inner classes have inheritance similar to static properties; this allows you to redefine an inner class in a subclass, allowing rich hierarchies.

readonly class Point(int $x, int $y);
 
class Geometry {
    public array $points;
    protected function __construct(Point ...$points) {
        $this->points = $points;
    }
 
    public class FromPoints extends Geometry {
        public function __construct(Point ...$points) {
            parent::__construct(...$points);
        }
    }
 
    public class FromCoordinates extends Geometry {
        public function __construct(int ...$coordinates) {
            $points = [];
            for ($i = 0; $i < count($coordinates); $i += 2) {
                $points[] = new Point($coordinates[$i], $coordinates[$i + 1]);
            }
            parent::__construct(...$points);
        }
    }
}
 
class Triangle extends Geometry {
    protected function __construct(public Point $p1, public Point $p2, public Point $p3) {
        parent::__construct($p1, $p2, $p3);
    }
 
    public class FromPoints extends Triangle {
        public function __construct(Point $p1, Point $p2, Point $p3) {
            parent::__construct($p1, $p2, $p3);
        }
    }
 
    public class FromCoordinates extends Triangle {
        public function __construct(int $x1, int $y1, int $x2, int $y2, int $x3, int $y3) {
            parent::__construct(new Point($x1, $y1), new Point($x2, $y2), new Point($x3, $y3));
        }
    }
}
 
$t = new Triangle::FromCoordinates(0, 0, 1, 1, 2, 2);
 
var_dump($t instanceof Triangle); // true
var_dump($t instanceof Geometry); // true
var_dump($t instanceof Triangle::FromCoordinates); // true

However, no classes may not inherit from inner classes, but inner classes may inherit from other classes, including the outer class.

Names

Inner classes may not have any name that conflicts with a constant or static property of the same name.

class Foo {
    const Bar = 'bar';
    class Bar(public string $message);
 
    // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar
}
 
class Foo {
    static $Bar = 'bar';
    class Bar(public string $message);
 
    // Fatal error: Uncaught Error: Cannot redeclare Foo::$Bar
}

Backward Incompatible Changes

  • This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax.
  • Some error messages will be updated to reflect inner classes, and tests that depend on these error messages are likely to fail.
  • Tooling using AST or tokenization may need to be updated to support the new syntax.

Proposed PHP Version(s)

This RFC targets the next version of PHP.

RFC Impact

To SAPIs

None.

To Existing Extensions

Extensions accepting class names may need to be updated to support :: in class names. None were discovered during testing, but it is possible there are extensions that may be affected.

To Opcache

This change introduces a new opcode, AST, and other changes that affect opcache. These changes are included as part of the PR that implements this feature.

Open Issues

Pending discussion.

Unaffected PHP Functionality

There should be no change to any existing PHP syntax.

Future Scope

  • inner enums

Proposed Voting Choices

As this is a significant change to the language, a 2/3 majority is required.

Patches and Tests

A complete implementation is available on GitHub.

Implementation

After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any)

References

Rejected Features

  • “evolvable” syntax: A ->with() function built into short classes.
rfc/short-and-inner-classes.1741212772.txt.gz · Last modified: 2025/03/05 22:12 by withinboredom