Most of the examples that have been given so far are either trivial
boolean flags or data validation rules to be evaled. In practice, very
little of Drupal's use of annotations in Drupal 8 fit either category.
Rather, they're used primarily as, in essence, a serialized metadata
object describing a class, which is used for registering that class and
potentially others. I figured I'd give the proposed syntax a try with
some Drupal examples and see how well it fit.
Disclaimer: I'm sure someone will pipe up with "your use case is invalid
because you shouldn't be using annotations that way." I will be the
first to agree that Drupal loves to take everything it does to an
extreme, and some things may be better done other ways. However, these
are still real-world use cases (currently built with Doctrine
Annotations) that people are going to want to try and reimplement
eventually using a core language feature. This much data is put in one
place primarily for DX reasons, to give developers a one-stop-shop for
defining a given extension. Saying "well just abandon your approach
entirely" is not a satisfying answer.
Summary: It doesn't fit well at all, and there's features missing that
would prevent Drupal from being able to use the proposed attributes RFC
as-is, even without talking about classes-as-annotations. A series of
improvement request/suggestions are listed at the end of this email.
Simple example:
Drupal plugins (usually) use annotations. Here's a simple example:
/**
* Provides a block to display 'Site branding' elements.
*
* @Block(
* id = "system_branding_block",
* admin_label = @Translation("Site branding")
* )
*/
class SystemBrandingBlock {
}
This defines a "block" (type of plugin). It's unique machine name
identifier is "system_branding_block", and its human-facing label is
"Site branding", which is marked as a translatable string. That all
seems reasonable to include here.
Here's what I came up with for a possible attributes version:
<<PluginType('Block')>>
<<PluginId('system_branding_block')>>
<<PluginAdminLabel('Site branding')>>
class SystemBrandingBlock {
}
Not too bad at first blush, but there's 2 problems.
1) There's no indication that the label is a translatable string. One
could hard-code that logic into whatever processing happens for
PluginAdminLabel, but then there's no indication for our gettext scanner
that "Site branding" is translatable and should be extracted for
translation.
2) If we want to say that the value "Block" corresponds to a class
(something that would be up to the parser to do), there's no indication
of the namespace against which to resolve "Block". The alternative
would be to require including the full class name string, like so:
<<PluginType('Drupal\Core\Block\Annotation\Block')>>
But that DX is quite terrible. We introduced ::class in 5.5 for a
reason. Better would be:
<<PluginType(Block::class)>>
But that works only if the attribute parser resolves Block::class
against the currently "use"d namespaces so that it's a full class name
string when reflection picks it up. If not, then that means the
user-space parser needs to catch that, then go back to the file and
figure out the available use statements and resolve against those. It's
doable, but ugly and certainly more work than I'd want to put in as
someone writing such a parser.
I don't know if that's a feature of the patch at the moment, but it
would need to be.
So even in a simple case we have insufficient functionality.
Complex example:
OK, let's go to the other end and look at an example that is way more
complicated. (Yes, maybe too complicated.) Doctrine annotations are
also used to define Entity Types, which correspond to a class. Here's
the annotation for a Node, in all its glory:
/**
* Defines the node entity class.
*
* @ContentEntityType(
* id = "node",
* label = @Translation("Content"),
* bundle_label = @Translation("Content type"),
* handlers = {
* "storage" = "Drupal\node\NodeStorage",
* "storage_schema" = "Drupal\node\NodeStorageSchema",
* "view_builder" = "Drupal\node\NodeViewBuilder",
* "access" = "Drupal\node\NodeAccessControlHandler",
* "views_data" = "Drupal\node\NodeViewsData",
* "form" = {
* "default" = "Drupal\node\NodeForm",
* "delete" = "Drupal\node\Form\NodeDeleteForm",
* "edit" = "Drupal\node\NodeForm"
* },
* "route_provider" = {
* "html" = "Drupal\node\Entity\NodeRouteProvider",
* },
* "list_builder" = "Drupal\node\NodeListBuilder",
* "translation" = "Drupal\node\NodeTranslationHandler"
* },
* base_table = "node",
* data_table = "node_field_data",
* revision_table = "node_revision",
* revision_data_table = "node_field_revision",
* translatable = TRUE,
* list_cache_contexts = { "user.node_grants:view" },
* entity_keys = {
* "id" = "nid",
* "revision" = "vid",
* "bundle" = "type",
* "label" = "title",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "status" = "status",
* "uid" = "uid",
* },
* bundle_entity_type = "node_type",
* field_ui_base_route = "entity.node_type.edit_form",
* common_reference_target = TRUE,
* permission_granularity = "bundle",
* links = {
* "canonical" = "/node/{node}",
* "delete-form" = "/node/{node}/delete",
* "edit-form" = "/node/{node}/edit",
* "version-history" = "/node/{node}/revisions",
* "revision" = "/node/{node}/revisions/{node_revision}/view",
* }
* )
*/
class Node {
}
Yoinks. Now let's try and turn that into attributes. Here's my first
attempt:
<<PluginType('ContentEntity')>>
<<PluginId('node')>>
<<ContentEntity\Label('Content')>>
<<ContentEntity\BundleLabel('Content type')>>
<<ContentEntity\Handler\Storage(NodeStorage::class)>>
<<ContentEntity\Handler\StorageSchema(NodeStorageSchema::class)>>
<<ContentEntity\Handler\ViewBuilder(NodeViewBuilder::class)>>
<<ContentEntity\Handler\Access(NodeAccessControlHandler::class)>>
<<ContentEntity\Handler\ViewsData(NodeViewsData::class)>>
<<ContentEntity\Handler\Form\Default(NodeForm::class)>>
<<ContentEntity\Handler\Form\Delete(NodeDeleteForm::class)>>
<<ContentEntity\Handler\Form\Edit(NodeForm::class)>>
<<ContentEntity\RouteProvider\Html(NodeRouteProvider::class)>>
// ...
class Node {
}
I gave up at that point, as I'd already identified several problems.
1) The attribute names themselves are horribly long, because they're not
namespaced at all. That means I have to namespace them myself in the
annotation, which leads to grotesquely long tokens.
2) Again, I need to list the name of a class in the annotation. That
means either a stupid-long-string (which therefore means I can't catch
stupid typos with static analysis) or resolving ::class constants. I
went with the latter here because the alternative is basically unusable.
3) Nested data is unsupported, except through manual namespacing.
4) Hashes are totally unsupported.
<<ContentEntity\Handler\Form\Default(NodeForm::class)>> is a terrible
idea, as even with my own parser I now cannot tell which part of that
string is supposed to be a class name to map the data to and which isn't.
5) Because the attribute names are just strings, I am repeating very
long values with no static analysis support at all. One typo in that
string, even though it's not quoted like a string, means stuff will just
not work and I don't know how to catch it other than visual inspection
(aka, "hey developer, guess!").
OK, let's try using the AST format. That gets a little bit better:
<<PluginType('ContentEntity')>>
<<PluginId('node')>>
<<ContentEntity\Label('Content')>>
<<ContentEntity\BundleLabel('Content type')>>
<<ContentEntity\Handler\Storage(NodeStorage::class)>>
<<ContentEntity\Handler\StorageSchema(NodeStorageSchema::class)>>
<<ContentEntity\Handler\ViewBuilder(NodeViewBuilder::class)>>
<<ContentEntity\Handler\Access(NodeAccessControlHandler::class)>>
<<ContentEntity\Handler\ViewsData(NodeViewsData::class)>>
<<ContentEntity\Handler\Form(['default' => NodeForm::class,
'delete' => NodeDeleteForm::class
'edit' => NodeForm::class]>>
<<ContentEntity\RouteProvider(['html' => NodeRouteProvider::class]>>
class Node {
}
At least now I can define the hash in a parsable fashion. However, it's
not clear to me if I can catch errors there with static analysis. Note,
for instance, that I omitted the comma on the 'delete' line. That would
be a parse error in normal code. Would it be a parse error on compile?
I would hope so, since it would be a parse error when I tried to reflect
the attributes. It doesn't address the other issues, though.
OK, let's take that to its logical conclusion and directly map in the
full annotation:
<<ContentEntity([
'id' => 'node',
'label' => 'Content',
'bundle_label' => 'Content type',
'handlers' => [
'storage' => NodeStorage::class,
'storage_schema' => NodeStorageSchema::class,
'view_builder' => NodeViewBuilder::class,
'access' => NodeAccessControllerHandler::class,
'views_data' => NodeViewsData::class,
'form' => [
'default' => NodeForm::class,
'delete' => NodeDeleteForm::class,
'edit' => NodeForm::class,
],
'route_provder' => [
'html' => NodeRouteProvider::class,
],
],
// ...
])>>
<<ContentEntity\Translatable>>
<<Links([
"canonical" => "/node/{node}",
"delete-form" => "/node/{node}/delete",
"edit-form" => "/node/{node}/edit",
"version-history" => "/node/{node}/revisions",
"revision" => "/node/{node}/revisions/{node_revision}/view",
])>>
class Node {
}
I broke it up a little bit, but that may or may not make sense in practice.
What do we get with this approach? Well, we can model the full data
structure. That's good. If ContentEntity is supposed to map to a class
in my system it would be slightly less fugly to use a full class name
there now, as there's only maybe 3 attributes instead of 30. Still,
we're really only getting any benefit out of it if:
1) The ::class constant acutally works as I've shown here.
2) The parser understands that I'm trying to define an array structure
and can catch syntax typos for me.
Otherwise, there's really no benefit over sticking the string in the
docblock as we do now.
Additionally, even then it's still "just an array", by which I mean an
anonymous struct, by which I mean "you get no help at all on typos."
You may have noticed that I typoed "route_provder" instead of
"route_provider" above. An an array key, that is a silent runtime error
that's crazy hard to debug. Forcing any meaningful data structure into
anonymous structs is doing everyone a disservice. (As a Drupal
developer, I can attest to that fact with personal scars.)
Recommended changes:
1) ::class constants should resolve at compile time, relative to use
statements, anywhere in the annotation.
2) Namespacing for attribute names. This is necessary for both
collision-avoidance and to minimize typing of obscenely long attribute
names. The easiest way to do that would be:
3) Some way to provide a data definition for the annotation that can be
checked at compile time. This could be classes, a la Doctrine. It could
be a new Annotation type as others have suggested. It could be
something else, akin to a struct like Liz Smith keeps toying with. I'm
flexible here. But some way to provide a data definition for the
annotation that is more robust than "you get an AST for an associative
array that you can eval to an array yourself, good luck" is, I believe,
mandatory for these to be successful for more than the most trivial use
cases. If that data definition is a class or class-like type, that also
resolves the namespace question very easily.
Note: We do NOT need objects created at compile time or include time, or
even instantiation of the annotated class time. Only at
reflection-lookup time. All that's needed before that is syntax
validation (both compiler and IDE, once IDEs catch on). If that can go
as far as key validation at compile time, great. If that can only
happen at reflection-lookup time, that's acceptable as long as it happens.
4) I don't know if there's a reasonable way to "nest" annotations. I do
not have a solution for the translatable string marker issue, but it is
a feature of Doctrine that Drupal leverages heavily and we would be
loathe to lose. (It may even be a deal breaker.)
I hope this case study has been useful. To reiterate, I am in favor of
PHP adding annotation/attribute/whatever-you-call-it support; I just
want to make sure it is robust enough to support the use cases that I
know exist in the wild.
--Larry Garfield
--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php