-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
f6d6a62
Implement property hooks via virtual properties
thekid 8abde26
Fix PHP 7.3
thekid 87f629c
Add support for `__PROPERTY__`
thekid e3f2ae1
Do not declare virtual properties inside interfaces
thekid ffe6c4b
Change implementation to emit `__[hook]_[name]()` methods
thekid 4138f72
QA: Reorder code
thekid c9d348c
Test abstract hooks
thekid c92001d
QA: Make tests consistent
thekid e747819
Make $this->propertyName work
thekid 3756cf4
Support abstract properties
thekid 1d73741
Merge branch 'master' into feature/property-hooks
thekid 9830d04
Fix "Undefined property: lang\ast\nodes\Hook::$annotations"
thekid ed5003f
Add scope checks to private and protected properties
thekid 6031c35
Simplify withScopeCheck()
thekid f21a615
Add support for for `<T>::$field::hook()`
thekid bbdf5a1
Merge branch 'master' into feature/property-hooks
thekid 828ec93
Merge branch 'master' into feature/property-hooks
thekid d7650a2
MFH 8.12.0-RELEASE
thekid c32235c
Implement get-hooks to use by-reference semantics
thekid d96a487
Fix "Only variable references should be returned by reference"
thekid c1f1e61
Fix PHP 7.0 compatibility
thekid 707500f
MFH
thekid 5aa7780
Migrate to new reflection library
thekid 9b022fe
MFH
thekid 0cfa858
MFH
thekid 8584d20
Remove support for `$field`
thekid 5929c83
Implement short set in accordance with RFC
thekid 2fbca6f
Use simplemost `set { ... }` form
thekid 0b823d5
Use simplemost `set { ... }` form for block with exceptions
thekid 19f5645
Merge from master
thekid d02f93f
Merge branch 'feature/property-hooks' of github.com:xp-framework/comp…
thekid 39f4929
Remove abstract hooks, they are no longer included in the RFC
thekid df4a889
Fix "Type of parameter $times of hook T::$test::set must be compatibl…
thekid 9a4186a
Emit property hooks natively if its PR has been merged
thekid 53f0256
Change emitScope() to no longer include type in braces except for `new`
thekid 7cd4039
Refactor chaining scope operators for PHP 7.x
thekid 00e15f1
Add tests for reflective access to virtual properties
thekid e0b89f3
Simplify code
thekid e605a97
Use release version of AST library
thekid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php namespace lang\ast\emit; | ||
|
||
use lang\ast\Node; | ||
use lang\ast\nodes\Literal; | ||
|
||
/** | ||
* Allows chaining of scope operators and rewrites `[expr]::class` to `get_class($object)` | ||
* except if expression references a type - e.g. `self` or `ClassName`. | ||
* | ||
* @see https://wiki.php.net/rfc/class_name_literal_on_object | ||
* @see https://wiki.php.net/rfc/variable_syntax_tweaks#constant_dereferencability | ||
* @test lang.ast.unittest.emit.ChainScopeOperatorsTest | ||
*/ | ||
trait ChainScopeOperators { | ||
use RewriteDynamicClassConstants { emitScope as rewriteDynamicClassConstants; } | ||
|
||
protected function emitScope($result, $scope) { | ||
if (!($scope->type instanceof Node)) return $this->rewriteDynamicClassConstants($result, $scope); | ||
|
||
if ($scope->member instanceof Literal && 'class' === $scope->member->expression) { | ||
$result->out->write('\\get_class('); | ||
$this->emitOne($result, $scope->type); | ||
$result->out->write(')'); | ||
} else { | ||
$t= $result->temp(); | ||
$result->out->write('(null==='.$t.'='); | ||
$this->emitOne($result, $scope->type); | ||
$result->out->write(")?null:{$t}::"); | ||
$this->emitOne($result, $scope->member); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
<?php namespace lang\ast\emit; | ||
|
||
use ReflectionProperty; | ||
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; | ||
|
||
// Rewrite $this->propertyName to virtual property | ||
if ( | ||
$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); | ||
} | ||
|
||
return new Block([new Code($check), ...$nodes]); | ||
} | ||
|
||
protected function emitEmulatedHooks($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 | ||
]; | ||
|
||
// 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 => ['interface' === $scope->type->kind ? $modifiers | MODIFIER_ABSTRACT : $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 instanceof Block ? $hook->expression : new Assignment($virtual, '=', $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; | ||
} | ||
} | ||
|
||
protected function emitNativeHooks($result, $property) { | ||
$result->codegen->scope[0]->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 => [] | ||
]; | ||
|
||
$property->comment && $this->emitOne($result, $property->comment); | ||
$property->annotations && $this->emitOne($result, $property->annotations); | ||
$result->at($property->declared)->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); | ||
if (isset($property->expression)) { | ||
if ($this->isConstant($result, $property->expression)) { | ||
$result->out->write('='); | ||
$this->emitOne($result, $property->expression); | ||
} else if (in_array('static', $property->modifiers)) { | ||
$result->codegen->scope[0]->statics['self::$'.$property->name]= $property->expression; | ||
} else { | ||
$result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression; | ||
} | ||
} | ||
|
||
// TODO move this to lang.ast.emit.PHP once https://github.com/php/php-src/pull/13455 is merged | ||
$result->out->write('{'); | ||
foreach ($property->hooks as $type => $hook) { | ||
$hook->byref && $result->out->write('&'); | ||
$result->out->write($type); | ||
if ($hook->parameter) { | ||
$result->out->write('('); | ||
$this->emitOne($result, $hook->parameter); | ||
$result->out->write(')'); | ||
} | ||
|
||
if (null === $hook->expression) { | ||
$result->out->write(';'); | ||
} else if ($hook->expression instanceof Block) { | ||
$this->emitOne($result, $hook->expression); | ||
} else { | ||
$result->out->write('=>'); | ||
$this->emitOne($result, $hook->expression); | ||
$result->out->write(';'); | ||
} | ||
} | ||
$result->out->write('}'); | ||
} | ||
|
||
protected function emitProperty($result, $property) { | ||
static $hooks= null; | ||
|
||
if (empty($property->hooks)) { | ||
parent::emitProperty($result, $property); | ||
} else if ($hooks ?? $hooks= method_exists(ReflectionProperty::class, 'getHooks')) { | ||
$this->emitNativeHooks($result, $property); | ||
} else { | ||
$this->emitEmulatedHooks($result, $property); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See php/php-src#13455