Skip to content

Commit

Permalink
[8.4] Add support for property hooks
Browse files Browse the repository at this point in the history
Add hooks subnode to Stmt\Property and Param, which contains an
array of PropertyHook.

The property hook support is considered experimental and subject
to change.

RFC: https://wiki.php.net/rfc/property-hooks
  • Loading branch information
nikic committed Jul 28, 2024
1 parent b11fc12 commit 03caf4c
Show file tree
Hide file tree
Showing 43 changed files with 2,834 additions and 1,407 deletions.
46 changes: 42 additions & 4 deletions grammar/php.y
Original file line number Diff line number Diff line change
Expand Up @@ -677,12 +677,12 @@ property_modifier:

parameter:
optional_attributes optional_property_modifiers optional_type_without_static
optional_arg_ref optional_ellipsis plain_variable
{ $$ = new Node\Param($6, null, $3, $4, $5, attributes(), $2, $1);
optional_arg_ref optional_ellipsis plain_variable optional_property_hook_list
{ $$ = new Node\Param($6, null, $3, $4, $5, attributes(), $2, $1, $7);
$this->checkParam($$); }
| optional_attributes optional_property_modifiers optional_type_without_static
optional_arg_ref optional_ellipsis plain_variable '=' expr
{ $$ = new Node\Param($6, $8, $3, $4, $5, attributes(), $2, $1);
optional_arg_ref optional_ellipsis plain_variable '=' expr optional_property_hook_list
{ $$ = new Node\Param($6, $8, $3, $4, $5, attributes(), $2, $1, $9);
$this->checkParam($$); }
| optional_attributes optional_property_modifiers optional_type_without_static
optional_arg_ref optional_ellipsis error
Expand Down Expand Up @@ -830,6 +830,11 @@ class_statement_list:
class_statement:
optional_attributes variable_modifiers optional_type_without_static property_declaration_list semi
{ $$ = new Stmt\Property($2, $4, attributes(), $3, $1); }
#if PHP8
| optional_attributes variable_modifiers optional_type_without_static property_declaration_list '{' property_hook_list '}'
{ $$ = new Stmt\Property($2, $4, attributes(), $3, $1, $6);
$this->checkPropertyHookList($6, #5); }
#endif
| optional_attributes method_modifiers T_CONST class_const_list semi
{ $$ = new Stmt\ClassConst($4, $2, attributes(), $1);
$this->checkClassConst($$, #2); }
Expand Down Expand Up @@ -926,6 +931,39 @@ property_declaration:
| property_decl_name '=' expr { $$ = Node\PropertyItem[$1, $3]; }
;

property_hook_list:
/* empty */ { $$ = []; }
| property_hook_list property_hook { push($1, $2); }
;

optional_property_hook_list:
/* empty */ { $$ = []; }
#if PHP8
| '{' property_hook_list '}' { $$ = $2; $this->checkPropertyHookList($2, #1); }
#endif
;

property_hook:
optional_attributes property_hook_modifiers optional_ref identifier_not_reserved property_hook_body
{ $$ = Node\PropertyHook[$4, $5, ['flags' => $2, 'byRef' => $3, 'params' => [], 'attrGroups' => $1]];
$this->checkPropertyHook($$, null); }
| optional_attributes property_hook_modifiers optional_ref identifier_not_reserved '(' parameter_list ')' property_hook_body
{ $$ = Node\PropertyHook[$4, $8, ['flags' => $2, 'byRef' => $3, 'params' => $6, 'attrGroups' => $1]];
$this->checkPropertyHook($$, #5); }
;

property_hook_body:
';' { $$ = null; }
| '{' inner_statement_list '}' { $$ = $2; }
| T_DOUBLE_ARROW expr ';' { $$ = $2; }
;

property_hook_modifiers:
/* empty */ { $$ = 0; }
| property_hook_modifiers member_modifier
{ $this->checkPropertyHookModifiers($1, $2, #2); $$ = $1 | $2; }
;

expr_list_forbid_comma:
non_empty_expr_list no_comma
;
Expand Down
42 changes: 41 additions & 1 deletion lib/PhpParser/Builder/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class Property implements PhpParser\Builder {
protected ?Node $type = null;
/** @var list<Node\AttributeGroup> */
protected array $attributeGroups = [];
/** @var list<Node\PropertyHook> */
protected array $hooks = [];

/**
* Creates a property builder.
Expand Down Expand Up @@ -88,6 +90,28 @@ public function makeReadonly() {
return $this;
}

/**
* Makes the property abstract. Requires at least one property hook to be specified as well.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeAbstract() {
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::ABSTRACT);

return $this;
}

/**
* Makes the property final.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeFinal() {
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::FINAL);

return $this;
}

/**
* Sets default value for the property.
*
Expand Down Expand Up @@ -142,20 +166,36 @@ public function addAttribute($attribute) {
return $this;
}

/**
* Adds a property hook.
*
* @return $this The builder instance (for fluid interface)
*/
public function addHook(Node\PropertyHook $hook) {
$this->hooks[] = $hook;

return $this;
}

/**
* Returns the built class node.
*
* @return Stmt\Property The built property node
*/
public function getNode(): PhpParser\Node {
if ($this->flags & Modifiers::ABSTRACT && !$this->hooks) {
throw new PhpParser\Error('Only hooked properties may be declared abstract');
}

return new Stmt\Property(
$this->flags !== 0 ? $this->flags : Modifiers::PUBLIC,
[
new Node\PropertyItem($this->name, $this->default)
],
$this->attributes,
$this->type,
$this->attributeGroups
$this->attributeGroups,
$this->hooks
);
}
}
9 changes: 7 additions & 2 deletions lib/PhpParser/Node/Param.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class Param extends NodeAbstract {
public int $flags;
/** @var AttributeGroup[] PHP attribute groups */
public array $attrGroups;
/** @var PropertyHook[] Property hooks for promoted properties */
public array $hooks;

/**
* Constructs a parameter node.
Expand All @@ -33,13 +35,15 @@ class Param extends NodeAbstract {
* @param array<string, mixed> $attributes Additional attributes
* @param int $flags Optional visibility flags
* @param list<AttributeGroup> $attrGroups PHP attribute groups
* @param PropertyHook[] $hooks Property hooks for promoted properties
*/
public function __construct(
Expr $var, ?Expr $default = null, ?Node $type = null,
bool $byRef = false, bool $variadic = false,
array $attributes = [],
int $flags = 0,
array $attrGroups = []
array $attrGroups = [],
array $hooks = []
) {
$this->attributes = $attributes;
$this->type = $type;
Expand All @@ -49,10 +53,11 @@ public function __construct(
$this->default = $default;
$this->flags = $flags;
$this->attrGroups = $attrGroups;
$this->hooks = $hooks;
}

public function getSubNodeNames(): array {
return ['attrGroups', 'flags', 'type', 'byRef', 'variadic', 'var', 'default'];
return ['attrGroups', 'flags', 'type', 'byRef', 'variadic', 'var', 'default', 'hooks'];
}

public function getType(): string {
Expand Down
78 changes: 78 additions & 0 deletions lib/PhpParser/Node/PropertyHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);

namespace PhpParser\Node;

use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeAbstract;

class PropertyHook extends NodeAbstract implements FunctionLike {
/** @var AttributeGroup[] PHP attribute groups */
public array $attrGroups;
/** @var int Modifiers */
public int $flags;
/** @var bool Whether hook returns by reference */
public bool $byRef;
/** @var Identifier Hook name */
public Identifier $name;
/** @var Param[] Parameters */
public array $params;
/** @var null|Expr|Stmt[] Hook body */
public $body;

/**
* Constructs a property hook node.
*
* @param string|Identifier $name Hook name
* @param null|Expr|Stmt[] $body Hook body
* @param array{
* flags?: int,
* byRef?: bool,
* params?: Param[],
* attrGroups?: AttributeGroup[],
* } $subNodes Array of the following optional subnodes:
* 'byRef' => false : Whether hook returns by reference
* 'params' => array(): Parameters
* 'attrGroups' => array(): PHP attribute groups
* @param array<string, mixed> $attributes Additional attributes
*/
public function __construct($name, $body, array $subNodes = [], array $attributes = []) {
$this->attributes = $attributes;
$this->name = \is_string($name) ? new Identifier($name) : $name;
$this->body = $body;
$this->flags = $subNodes['flags'] ?? 0;
$this->byRef = $subNodes['byRef'] ?? false;
$this->params = $subNodes['params'] ?? [];
$this->attrGroups = $subNodes['attrGroups'] ?? [];
}

public function returnsByRef(): bool {
return $this->byRef;
}

public function getParams(): array {
return $this->params;
}

public function getReturnType() {
return null;
}

public function getStmts(): ?array {
if ($this->body instanceof Expr) {
return [new Return_($this->body)];
}
return $this->body;
}

public function getAttrGroups(): array {
return $this->attrGroups;
}

public function getType(): string {
return 'PropertyHook';
}

public function getSubNodeNames(): array {
return ['attrGroups', 'flags', 'byRef', 'name', 'params', 'body'];
}
}
8 changes: 6 additions & 2 deletions lib/PhpParser/Node/Stmt/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class Property extends Node\Stmt {
public ?Node $type;
/** @var Node\AttributeGroup[] PHP attribute groups */
public array $attrGroups;
/** @var Node\PropertyHook[] Property hooks */
public array $hooks;

/**
* Constructs a class property list node.
Expand All @@ -27,17 +29,19 @@ class Property extends Node\Stmt {
* @param array<string, mixed> $attributes Additional attributes
* @param null|Identifier|Name|ComplexType $type Type declaration
* @param Node\AttributeGroup[] $attrGroups PHP attribute groups
* @param Node\PropertyHook[] $hooks Property hooks
*/
public function __construct(int $flags, array $props, array $attributes = [], ?Node $type = null, array $attrGroups = []) {
public function __construct(int $flags, array $props, array $attributes = [], ?Node $type = null, array $attrGroups = [], array $hooks = []) {
$this->attributes = $attributes;
$this->flags = $flags;
$this->props = $props;
$this->type = $type;
$this->attrGroups = $attrGroups;
$this->hooks = $hooks;
}

public function getSubNodeNames(): array {
return ['attrGroups', 'flags', 'type', 'props'];
return ['attrGroups', 'flags', 'type', 'props', 'hooks'];
}

/**
Expand Down
Loading

0 comments on commit 03caf4c

Please sign in to comment.