Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement property hooks via virtual properties #166

Merged
merged 39 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f6d6a62
Implement property hooks via virtual properties
thekid May 13, 2023
8abde26
Fix PHP 7.3
thekid May 13, 2023
87f629c
Add support for `__PROPERTY__`
thekid May 14, 2023
e3f2ae1
Do not declare virtual properties inside interfaces
thekid May 14, 2023
ffe6c4b
Change implementation to emit `__[hook]_[name]()` methods
thekid May 14, 2023
4138f72
QA: Reorder code
thekid May 14, 2023
c9d348c
Test abstract hooks
thekid May 14, 2023
c92001d
QA: Make tests consistent
thekid May 14, 2023
e747819
Make $this->propertyName work
thekid May 14, 2023
3756cf4
Support abstract properties
thekid May 15, 2023
1d73741
Merge branch 'master' into feature/property-hooks
thekid May 18, 2023
9830d04
Fix "Undefined property: lang\ast\nodes\Hook::$annotations"
thekid May 18, 2023
ed5003f
Add scope checks to private and protected properties
thekid May 18, 2023
6031c35
Simplify withScopeCheck()
thekid May 18, 2023
f21a615
Add support for for `<T>::$field::hook()`
thekid May 18, 2023
bbdf5a1
Merge branch 'master' into feature/property-hooks
thekid May 18, 2023
828ec93
Merge branch 'master' into feature/property-hooks
thekid May 21, 2023
d7650a2
MFH 8.12.0-RELEASE
thekid May 21, 2023
c32235c
Implement get-hooks to use by-reference semantics
thekid May 21, 2023
d96a487
Fix "Only variable references should be returned by reference"
thekid May 21, 2023
c1f1e61
Fix PHP 7.0 compatibility
thekid May 21, 2023
707500f
MFH
thekid Jun 6, 2023
5aa7780
Migrate to new reflection library
thekid Jun 6, 2023
9b022fe
MFH
thekid Jun 8, 2023
0cfa858
MFH
thekid Feb 24, 2024
8584d20
Remove support for `$field`
thekid Feb 24, 2024
5929c83
Implement short set in accordance with RFC
thekid Mar 16, 2024
2fbca6f
Use simplemost `set { ... }` form
thekid Mar 16, 2024
0b823d5
Use simplemost `set { ... }` form for block with exceptions
thekid Mar 16, 2024
19f5645
Merge from master
thekid Apr 17, 2024
d02f93f
Merge branch 'feature/property-hooks' of github.com:xp-framework/comp…
thekid Apr 17, 2024
39f4929
Remove abstract hooks, they are no longer included in the RFC
thekid May 18, 2024
df4a889
Fix "Type of parameter $times of hook T::$test::set must be compatibl…
thekid May 18, 2024
9a4186a
Emit property hooks natively if its PR has been merged
thekid May 18, 2024
53f0256
Change emitScope() to no longer include type in braces except for `new`
thekid May 18, 2024
7cd4039
Refactor chaining scope operators for PHP 7.x
thekid May 18, 2024
00e15f1
Add tests for reflective access to virtual properties
thekid May 18, 2024
e0b89f3
Simplify code
thekid Jun 15, 2024
e605a97
Use release version of AST library
thekid Jun 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^11.0 | ^10.0",
"xp-framework/ast": "^10.1",
"xp-framework/ast": "dev-feature/property-hooks as 10.2.0",
"php" : ">=7.0.0"
},
"require-dev" : {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ protected function emitClass($result, $class) {
}
$result->out->write('];');

$result->out->write('public function __get($name) { switch ($name) {');
$result->out->write('public function &__get($name) { switch ($name) {');
foreach ($context->virtual as $name => $access) {
$result->out->write($name ? 'case "'.$name.'":' : 'default:');
$this->emitOne($result, $access[0]);
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP70.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ class PHP70 extends PHP {
OmitConstantTypes,
OmitPropertyTypes,
ReadonlyClasses,
ReadonlyProperties,
RewriteAssignments,
RewriteClassOnObjects,
RewriteEnums,
RewriteExplicitOctals,
RewriteLambdaExpressions,
RewriteMultiCatch,
RewriteProperties,
RewriteThrowableExpressions
;

Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP71.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ class PHP71 extends PHP {
OmitArgumentNames,
OmitConstantTypes,
OmitPropertyTypes,
ReadonlyProperties,
ReadonlyClasses,
RewriteAssignments,
RewriteClassOnObjects,
RewriteEnums,
RewriteExplicitOctals,
RewriteLambdaExpressions,
RewriteProperties,
RewriteThrowableExpressions
;

Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP72.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ class PHP72 extends PHP {
OmitArgumentNames,
OmitConstantTypes,
OmitPropertyTypes,
ReadonlyProperties,
ReadonlyClasses,
RewriteAssignments,
RewriteClassOnObjects,
RewriteEnums,
RewriteExplicitOctals,
RewriteLambdaExpressions,
RewriteProperties,
RewriteThrowableExpressions
;

Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP73.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ class PHP73 extends PHP {
OmitArgumentNames,
OmitConstantTypes,
OmitPropertyTypes,
ReadonlyProperties,
ReadonlyClasses,
RewriteClassOnObjects,
RewriteEnums,
RewriteExplicitOctals,
RewriteLambdaExpressions,
RewriteProperties,
RewriteThrowableExpressions
;

Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP74.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ class PHP74 extends PHP {
OmitArgumentNames,
OmitConstantTypes,
ReadonlyClasses,
ReadonlyProperties,
RewriteBlockLambdaExpressions,
RewriteClassOnObjects,
RewriteEnums,
RewriteExplicitOctals,
RewriteProperties,
RewriteThrowableExpressions
;

Expand Down
4 changes: 2 additions & 2 deletions src/main/php/lang/ast/emit/PHP80.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ class PHP80 extends PHP {
CallablesAsClosures,
OmitConstantTypes,
ReadonlyClasses,
ReadonlyProperties,
RewriteBlockLambdaExpressions,
RewriteDynamicClassConstants,
RewriteEnums,
RewriteExplicitOctals
RewriteExplicitOctals,
RewriteProperties
;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP81.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @see https://wiki.php.net/rfc#php_81
*/
class PHP81 extends PHP {
use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes;
use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes, PropertyHooks;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP82.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @see https://wiki.php.net/rfc#php_82
*/
class PHP82 extends PHP {
use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes;
use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes, PropertyHooks;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP83.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @see https://wiki.php.net/rfc#php_83
*/
class PHP83 extends PHP {
use RewriteBlockLambdaExpressions, ReadonlyClasses;
use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
171 changes: 171 additions & 0 deletions src/main/php/lang/ast/emit/PropertyHooks.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php namespace lang\ast\emit;

use lang\ast\Code;
use lang\ast\nodes\{
Assignment,
Block,
InstanceExpression,
InvokeExpression,
Literal,
Method,
OffsetExpression,
Parameter,
ReturnStatement,
ScopeExpression,
Signature,
Variable
};

/**
* Property hooks
*
* @see https://wiki.php.net/rfc/property-hooks
* @test lang.ast.unittest.emit.PropertyHooksTest
*/
trait PropertyHooks {

protected function rewriteHook($node, $name, $virtual, $literal) {

// Magic constant referencing property name
if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal;

// Special variable $field, $this->propertyName syntax
if ($node instanceof Variable && 'field' === $node->pointer || (
$node instanceof InstanceExpression &&
$node->expression instanceof Variable && 'this' === $node->expression->pointer &&
$node->member instanceof Literal && $name === $node->member->expression
)) return $virtual;

// <T>::$field::hook() => <T>::__<hook>_<field>()
if (
$node instanceof ScopeExpression &&
$node->member instanceof InvokeExpression &&
$node->member->expression instanceof Literal &&
$node->type instanceof ScopeExpression &&
$node->type->member instanceof Variable &&
is_string($node->type->type) &&
is_string($node->type->member->pointer)
) {
return new ScopeExpression($node->type->type, new InvokeExpression(
new Literal('__'.$node->member->expression->expression.'_'.$node->type->member->pointer),
$node->member->arguments
));
}

foreach ($node->children() as &$child) {
$child= $this->rewriteHook($child, $name, $virtual, $literal);
}
return $node;
}

protected function withScopeCheck($modifiers, $nodes) {
if ($modifiers & MODIFIER_PRIVATE) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);'
);
} else if ($modifiers & MODIFIER_PROTECTED) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);'
);
} else if (1 === sizeof($nodes)) {
return $nodes[0];
} else {
return new Block($nodes);
}

array_unshift($nodes, new Code($check));
thekid marked this conversation as resolved.
Show resolved Hide resolved
return new Block($nodes);
}

protected function emitProperty($result, $property) {
static $lookup= [
'public' => MODIFIER_PUBLIC,
'protected' => MODIFIER_PROTECTED,
'private' => MODIFIER_PRIVATE,
'static' => MODIFIER_STATIC,
'final' => MODIFIER_FINAL,
'abstract' => MODIFIER_ABSTRACT,
'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY
];

if (empty($property->hooks)) return parent::emitProperty($result, $property);

// Emit XP meta information for the reflection API
$scope= $result->codegen->scope[0];
$modifiers= 0;
foreach ($property->modifiers as $name) {
$modifiers|= $lookup[$name];
}
$scope->meta[self::PROPERTY][$property->name]= [
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
DETAIL_ANNOTATIONS => $property->annotations,
DETAIL_COMMENT => $property->comment,
DETAIL_TARGET_ANNO => [],
DETAIL_ARGUMENTS => [$modifiers]
];

$literal= new Literal("'{$property->name}'");
$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal));

// Emit get and set hooks in-place. Ignore any unknown hooks
$get= $set= null;
foreach ($property->hooks as $type => $hook) {
$method= '__'.$type.'_'.$property->name;
$modifierList= $modifiers & MODIFIER_ABSTRACT ? ['abstract'] : $hook->modifiers;
if ('get' === $type) {
$this->emitOne($result, new Method(
$modifierList,
$method,
new Signature([], null, $hook->byref),
null === $hook->expression ? null : [$this->rewriteHook(
$hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression),
$property->name,
$virtual,
$literal
)],
null // $hook->annotations
));
$get= $this->withScopeCheck($modifiers, [
new Assignment(new Variable('r'), $hook->byref ? '=&' : '=', new InvokeExpression(
new InstanceExpression(new Variable('this'), new Literal($method)),
[]
)),
new ReturnStatement(new Variable('r'))
]);
} else if ('set' === $type) {
$this->emitOne($result, new Method(
$modifierList,
$method,
new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null),
null === $hook->expression ? null : [$this->rewriteHook(
$hook->expression,
$property->name,
$virtual,
$literal
)],
null // $hook->annotations
));
$set= $this->withScopeCheck($modifiers, [new InvokeExpression(
new InstanceExpression(new Variable('this'), new Literal($method)),
[new Variable('value')]
)]);
}
}

// Declare virtual properties with __set and __get as well as initializations
// except inside interfaces, which cannot contain properties.
if ('interface' === $scope->type->kind) return;

$scope->virtual[$property->name]= [
$get ?? new ReturnStatement($virtual),
$set ?? new Assignment($virtual, '=', new Variable('value'))
];
if (isset($property->expression)) {
$scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression;
}
}
}
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/ReadonlyProperties.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected function emitProperty($result, $property) {

// Create virtual property implementing the readonly semantics
$scope->virtual[$property->name]= [
new Code(sprintf($check.'return $this->__virtual["%1$s"][0] ?? null;', $property->name)),
new Code(sprintf($check.'return $this->__virtual["%1$s"][0];', $property->name)),
new Code(sprintf(
($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;').
'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'.
Expand Down
17 changes: 17 additions & 0 deletions src/main/php/lang/ast/emit/RewriteProperties.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php namespace lang\ast\emit;

trait RewriteProperties {
use PropertyHooks, ReadonlyProperties {
PropertyHooks::emitProperty as emitPropertyHooks;
ReadonlyProperties::emitProperty as emitReadonlyProperties;
}

protected function emitProperty($result, $property) {
if ($property->hooks) {
return $this->emitPropertyHooks($result, $property);
} else if (in_array('readonly', $property->modifiers)) {
return $this->emitReadonlyProperties($result, $property);
}
parent::emitProperty($result, $property);
}
}
Loading