xp-framework / compiler

Compiles future PHP to today's PHP.
19 stars 0 forks source link

Implement readonly properties #115

Closed thekid closed 3 years ago

thekid commented 3 years ago

See https://wiki.php.net/rfc/readonly_properties_v2:

A readonly property can only be initialized once, and only from the scope where it has been declared. Any other assignment or modification of the property will result in an Error exception.

class Fixture {
  public readonly $prop;
  public function __construct($prop) { $this->prop= $prop; }
}

$f= new Fixture(1);

// Illegal, throws an error
$f->prop++;         

// Returns int(1)
var_dump($f->prop);
thekid commented 3 years ago

The following could be a head-start for an implementation:

class Fixture {
  /* public readonly $prop; */
  private $__readonly= ['prop' => null];

  public function __construct($prop) {
    $this->prop= $prop;
  }

  public function __get($member) {
    if (!array_key_exists($member, $this->__readonly)) {
      trigger_error('Undefined property '.__CLASS__.'::'.$member, E_USER_WARNING);
    }

    return $this->__readonly[$member][0] ?? null;
  }

  public function __set($member, $value) {

    // Illegal reassignment
    if (isset($this->__readonly[$member])) {
      throw new \Error('Cannot modify readonly property '.__CLASS__.'::'.$member);
    }

    // Illegal initialization outside of private scope
    $caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
    $scope= $caller['class'] ?? null;
    if (__CLASS__ !== $scope) {
      throw new \Error('Cannot initialize readonly property '.__CLASS__.'::'.$member.' from '.($scope
        ? 'scope '.$scope
        : 'global scope'
      ));
    }

    // Legal initialization
    $this->__readonly[$member]= [$value];
  }
}
thekid commented 3 years ago

The following could be a head-start for an implementation:

While using this, we could also go ahead and implement typed properties properly in PHP 7.x /cc @Danon

thekid commented 3 years ago

PHP 8.1 support depends on https://github.com/xp-framework/core/pull/278 and https://github.com/xp-framework/ast/pull/30. The only change needed is the following (which is included in 6.8.3):

diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php
index fb1693a..586a5d9 100755
--- a/src/main/php/lang/ast/emit/PHP.class.php
+++ b/src/main/php/lang/ast/emit/PHP.class.php
@@ -602,7 +602,7 @@ abstract class PHP extends Emitter {
       $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var';

       if (isset($param->promote)) {
-        $promoted.= $param->promote.' $'.$param->name.';';
+        $promoted.= $param->promote.' '.$this->propertyType($param->type).' $'.$param->name.';';
         $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name);
         $result->meta[0][self::PROPERTY][$param->name]= [
           DETAIL_RETURNS     => $param->type ? $param->type->name() : 'var',

However, for full support in PHP 7.x and PHP 8.0, we will need support for virtual properties, see xp-framework/rfc#340

thekid commented 3 years ago

Released in https://github.com/xp-framework/compiler/releases/tag/v6.9.0

thekid commented 3 years ago

Documentation updated in https://github.com/xp-framework/compiler/wiki/Implementation-status