-
Notifications
You must be signed in to change notification settings - Fork 236
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
Annotations 2.0 #75
Annotations 2.0 #75
Conversation
How does it affect userland? Any benefits/goals? |
composer.json
Outdated
], | ||
"require": { | ||
"php": ">=5.3.2", | ||
"doctrine/lexer": "1.*" | ||
"php": ">=7.0.0", |
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.
please don't align constraints. It leads to a nightmare when adding new constraints (merge conflicts for nothing due to alignment changes)
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.
Aligning constraints tells us you're manually editing composer.son
, which should not be necessary when requiring dependencies.
Why is it dropped ? |
} | ||
|
||
/** | ||
* @return \Doctrine\Annotations\Metadata\MetadataFactory |
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.
Sometimes you're using FQCN and sometimes you're using alias. It'd be better to use only one.
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.
@FabioBatSilva keep fqcn
@@ -82,7 +82,7 @@ public function getClassAnnotations(ReflectionClass $class) : array | |||
/** | |||
* {@inheritDoc} | |||
*/ | |||
public function getClassAnnotation(ReflectionClass $class, $annotationName) | |||
public function getClassAnnotation(ReflectionClass $class, string $annotationName) |
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.
I would suggest to split decorative CS changes and BC-breaking signature changes into separate distinct commits.
Adding a type hint changes the signature, which breaks any implementing classes.
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.
And then, the decorative code style changes can go into a dedicated PR to the 1.x branch.
* Constructor. | ||
* | ||
* @param \Doctrine\Annotations\Resolver $resolver | ||
* @param \Doctrine\Annotations\MetadataFactory $metadataFactory |
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.
The type is incorrect, it should be \Doctrine\Annotations\Metadata\MetadataFactory
.
src/Metadata/MetadataFactory.php
Outdated
|
||
$class = new \ReflectionClass($className); | ||
$constructor = $class->getConstructor(); | ||
$docComment = $class->getDocComment(); |
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.
Unused local variable $docComment
.
Did anyone run a benchmark to compare the hoa compiler to the one currently in use? |
@@ -107,11 +107,11 @@ public function getMetadataFor(string $className) | |||
*/ | |||
private function isAnnotation(ReflectionClass $class, array $annotations) : bool | |||
{ | |||
if ($class->isSubclassOf('Doctrine\Annotations\Annotation')) { | |||
if ($class->isSubclassOf(Annotation::CLASS)) { |
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.
::CLASS
should be lowercase ::class
.
Architecture: I propose to drop Metadata and MetadataFactory. class Builder
{
[..]
/**
* @param Context $context
* @param Reference $reference
*
* @return object
*/
public function create(Context $context, Reference $reference)
{
$target = $reference->nested ? Target::TARGET_ANNOTATION : $context->getTarget();
$fullClass = $this->resolver->resolve($context, $reference->name);
$values = $reference->values;
if (null === $factory = $this->factoryProvider->classGetFactory($fullClass)) {
throw InvalidAnnotationException::notAnnotationException($fullClass, $reference->name, $context->getDescription());
}
return $factory->instantiate($context, $values, $target);
}
} Now all the metadata stuff can be encapsulated in the $factory object. (*) I was initially going to call the "factory" "instantiator". But this name already exists in |
} | ||
|
||
return $this->imports = array_merge($classImports, $traitImports); | ||
} |
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.
I think this is confusing. If the method is coming from a trait, then the annotation is living in the trait file, and the imports should be from the trait's file only. If the method is declared in the class itself, then the annotation is living in the class file, and it should use the imports from the class file. I don't see a case where it should combine imports from different files.
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.
Here is how to find out if a method is defined in a trait, https://stackoverflow.com/a/45912866/246724.
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.
Btw, imo all of this reflection + inheritance adds unnecessary complexity.
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.
Well, forbidding people to use inheritance and traits when using annotations would mean that nobody would migrate to version 2
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.
From a functional perspective, it should stay the exact same, maybe with some API changes, but no behavioral ones.
The point here is getting rid of our own hacky parser, using a formalised one (HOA's)
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.
Oh, maybe I was unclear.
all of this reflection + inheritance adds unnecessary complexity.
What I mean is we don't need to inherit from \ReflectionClass to find the imports. Instead, have an ImportFinder or something like that.
Of course, people who write annotated classes should be allowed to use inheritance, and traits!
From a functional perspective, it should stay the exact same, maybe with some API changes, but no behavioral ones.
If the code in the PR is replicating existing behavior, then it needs to stay this way.
Or, if we agree that the old behavior is wrong, we could have two implementations of ImportFinder or of the class name resolver: One that operates the BC way, another that operates the "correct" way.
Why would we say that "the old behavior is wrong"?
Consider this example:
File T.php:
<?php
namespace Acme\Foo;
use Acme\Annotation\Hello;
trait T {
/**
* @Hello("I am an annotation on a trait method.")
* @Goodbye("I am annotation on a trait method, but the import is in the class file.")
*/
function foo() {}
}
File C.php:
<?php
namespace Acme\Bar;
use Acme\Annotation\Goodbye;
class C {
use T;
}
With the behavior proposed in the PR, which I assume is also the current behavior, the second annotation @Goodbye(..)
will use the import Acme\Annotation\Goodbye
from the class file.
I am saying this is wrong. It should only use the imports from the trait file. So the @Hello(..)
should work, but the @Goodbye(..)
should not.
This would be consistent with how the language itself works.
Imports are only available within the same file.
Well personally I think having annotations on a method in a trait is probably a bad idea anyway. but if we support it, it should at least be "correct". Unless, of course, it is for BC reasons.
The point here is getting rid of our own hacky parser, using a formalised one (HOA's)
Which I assume will be more maintainable, more reliable, more understandable (people have to look at the grammar only). So yeah, seems like a good idea.
Personally I care more about the registry going away.
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.
About the methods defined in traits: It gets even more interesting if the method is renamed.
Trait T {
/**
* @Hello()
*/
function foo() {}
}
class C {
use T {
foo as bar;
}
}
$m = new \ReflectionMethod('C', 'bar');
$reader->getMethodAnnotations($m);
The current behavior will not understand that the method is defined in a trait under a different name.
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.
And about properties in traits - this is more difficult. There is no \ReflectionProperty::getFileName().
https://stackoverflow.com/questions/18257158/how-to-extract-start-line-of-a-property-declaration-in-php
As a heuristic, we could say that:
- If none of the traits of the class has a property with the same name, then the property belongs to the class, obviously.
- If one or more of the traits define the property then we compare the doc comment. See https://3v4l.org/KY9nl.
Maybe all of this should be discussed in a separate issue. I only brought it up here because the PR affects the code where this behavior is implemented.
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.
@donquixote we kinda fixed all of these horrors in roave/better-reflection
, although it is not the primary aim here to provide very precise reflection of ugly stuff like traits.
Just a link about |
Thanks @Hywan ! In general, a hand-written or a generated parser should be faster than a parser combinator, or one that needs to interpret a piece of grammar in every step. Since it adds overhead and indirection to every micro-operation, the difference could be something like factor 2 (or more, or less). I remember this, because I used to experiment with the vektah parser combinator. Your linked article, "Exporting the parser into PHP code", claims that the parser can be exported to PHP code. If this is equivalent to a generator, then it should be similarly fast as a hand-written parser. Of course, even if it would take 2x longer, does not mean we have to care, if overall it is still "fast enough". |
I'm still against the namespace change if it does not have a migration path:
So this means we need a continuous migration path if you want to keep the namespace change. |
Yes it doesn't, but it gives an overview of the last big improvements :-). I should have clarified this, sorry.
True and false (well, you have started your sentence with “In general” 😉). In PHP, a parser combinator might be slower than a generated parser because of function calls and indirections, but it will not be the bottleneck I guess. The real bottleneck is the data copy. In a parser combinator, you have to copy the data being parsed into each parser. Even if PHP does a COW (Copy-On-Write), each data split ( In a parser combinator however, the lexer and the parser phases are “merged”, so the memory peak should be smaller than in a generated parser. However, regarding the last improvement in A parser combinator is like a hand-written parser, except it has a predefined formalism, is more testable, is more re-usable etc. Compared to a generated parser, the API is larger. However, a generated parser can be seen as a parser combinator with a small API. This thread is not the place to debate about this, but: A generated parser, a parser combinator, or a hand-written parser can all be fast and efficient, or slow and ineffective. It really depends of how they are implemented. They all have pros and cons. I personally prefer a parser combinator when working with Rust (see nom) because it is testable and brings interesting garantees, while when working with PHP, I prefer a generated parser.
I would claim that a hand-written parser is most of the time not fast. You have to re-optimise and re-implement everything, like the lexer (a good one is not simple) and the parser with all the optimisation. And the error-management, the AST builder, the memory management, the profiling etc. It's better to have a hackable compiler toolchain I guess. But indeed, once a The most obvious way to be faster now is to use really good data structures instead of generic
Correct. I don't want to speak for the Doctrine team, but my understanding of the problem is the following: Drop a hand-written, hard to maintain, hacky, and maybe buggy parser by a formal parser which is easy to maintain and fast enough.
These algorithms can help to test the Doctrine annotations, and DQL. |
Maybe I should have said "hardcoded" rather than "hand-written". This is not an argument against the hoa parser, just a conversation. |
We agree 😃. |
#constant: | ||
<identifier> (<colon> <colon> <identifier>)? | ||
|
||
string: |
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.
If a rule recognizes only one token, then it's faster to just use this token. I assume being fast is important in this context.
Same for the text
, number
, and identifier
rules.
Annotations 2.0: Read annotations from functions
What's going on with this? I'd very much like to see this because of multi line support. |
Closing this PR: there has been a second effort to create 2.0 which has been just as successful as this one. We'll be revisiting this at a later date. |
Doctrine\Annotations
(RemovingCommon
)hoa/compiler
instead ofdoctrine/lexer
( see : grammar )AnnotationRegistry
and all autoload magicAttribute/Attributes
annotationsSimpleAnnotationReader
FileCacheReader
IndexedReader
TODO:
Local reviews (checkout + run locally):