Currently blocked on #2954562: [PP-2] Create provider based plugin managers
Problem/Motivation
Most of the effort in 8.x to "update the theme system" was focused primarily around converting everything to Twig.
While this is all well and good, Twig is ultimately the last step in the "theming" process.
We really didn't touch a lot of the theme system internals, just moved them around a bit into other OO classes/services.
We're still using antiquated hooks to define the theme registry and templates it may provide.
It's time we modernize it using existing systems.
See #2863819-22: Convert theme hooks (defined by hook_theme()) to be objects for more discussion regarding this topic.
Proposed resolution
Create two new annotated plugins/manangers:
@Theme- Used to define a theme and handle the preprocessing/rendering.@Template- Actual template definitions (formerly known as "theme hooks" provided byhook_theme())
An example in contrib (still a WIP):
https://cgit.drupalcode.org/plus/tree/src/ThemePluginManager.php?h=8.x-4.x
Remaining tasks
TBD
User interface changes
TBD
API changes
TBD
Data model changes
TBD
Issue fork drupal-2869859
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
tim.plunkettWhile the above was a blocker for layouts work, this is just an awesome follow-up.
Excited to work on this, but untagging from the other initiative.
Comment #5
markhalliwellComment #6
markhalliwellComment #7
markhalliwellPretend English is my native tongue...
Comment #8
donquixote commentedHow would the interface for these plugins look like?
What is the behavior we expect from one theme hook plugin?
Comment #9
markhalliwellI really recommend following my work in https://www.drupal.org/project/plus though as this is what I'll be basing #2554199: Bootstrap 4 on.
This new Drupal+ module is really just #2938060: Abstract non-Bootstrap specific code into separate project, but I am hoping to ultimately get these "solutions" into core (which have already proven to be wildly successful in BS3's 8.x code).
I'm still working out the logistics of
@Template(not yet committed in code), but have been tied up with client work.Basically, the gist of it is this: everything that was defined in a theme hook array can be specified as a
@Templateannotation. If the plugin also implementsTemplatePreprocessInterface, then it will be preprocessed by a@Themeduring the rendering process.Comment #10
donquixote commentedMaybe I will do this some day. But I think the main ideas belong in the issue summary here :)
What will you annotate? Where do you put the annotation?
Into a template file? Or on a class? Would you have to create a class for every theme hook? Which interface would this class implement?
Comment #11
markhalliwellI should note that while
@Themeand@Templatearen't in Drupal Bootstrap's 8.x-3.x code, they are the next evolutionary step of it's existing plugin system. It's essentially consolidating the concepts into "proper" plugins/managers in anticipation of this core issue.The reason I'm also doing this in https://www.drupal.org/project/plus first (for 8.4.x and 8.5.x... maybe 8.6.x if this doesn't get in time) is to first prototype it and provide a solid/stable working POC used by Drupal Bootstrap.
It will essentially mimc what should be done in core by subclassing/replacing methods in:
https://cgit.drupalcode.org/plus/tree/src/Core/Theme/Registry.php?h=8.x-4.x
https://cgit.drupalcode.org/plus/tree/src/Core/Theme/ThemeManager.php?h=...
This way, this issue can use that module as a reference of what to actually implement here.
I know you're passionate about this topic just as much as I am. This is a long road ahead, but its something that I have already put a lot of time and effort into already (for a couple years now). I don't have all the solutions yet, but I'm willing to help figure them all out because it's better for all of us in the long run.
Comment #12
markhalliwellAs I said above, this is still a WIP. The concept, in and of itself though, is quite solid.
I'd be happy to discuss this further at DC if you're attending?
Comment #13
polI'll be present at the next EU DC (if any!) and I'm also willing to be part of the conversation!
Comment #14
markhalliwellComment #15
donquixote commentedI think I will be on dev days Lisboa and on Drupal Europe this year.
I looked at https://cgit.drupalcode.org/plus/tree/src/Core/Theme/ThemeManager.php?h=....
Most of this is the same as the parent ::render() function. The same refactoring as proposed in #2954402: Refactor ThemeManager::render(), split into smaller immutable objects. could be applied here.
So it does not even conflict with what you are doing.
Probably what I see there is just a first step, and later it might all be different. I can only comment on what I see.
I still would like to see an example where you would put those annotations. Just to get an idea.
If I get a better idea how you envision the "theme hook as plugin" system, I might rethink some of my proposed refactoring in #2954402: Refactor ThemeManager::render(), split into smaller immutable objects..
Perhaps not. I think my motivation works differently.
I just look at pieces of code and am unhappy what I see (most of the time), so I think about how it could be refactored without changing the behavior.
This doesn't mean I care so much about the respective subsystem, just that I looked at the code long enough that it triggered a reaction.
In my own projects, I usually do this kind of refactoring before I introduce any API changes, just to get a better idea what I am dealing with.
Comment #16
andypostConcept looks interesting but overhead on class discovery/loading could kill performance that was polished in functional implementation
Comment #17
markhalliwellFirst, I'd like to say that I will gladly welcome any profiling once I get a working POC, if only for amazement.
That statement, though, is a misnomer.
Performance for procedural-based functions/hooks is not even in the same league as properly architected OO code.
These are two entirely different concepts/implementations and what is "performant" for one does not translate 100% of the time to the other.
It depends on what is considered "normal" or "performant" for each paradigm.
Historically, procedural/hook-based functions are less performant (cpu + memory).
That, along with many other reasons, is why we adopted OO.
The current/previous method(s) used to discover theme hooks and preprocess functions is far more expensive. Utilizing regular expressions and other various non-performant ways to determine user-defined procedural-based functions matching the antiquated hook system.
Yes, we have optimized it greatly over the years, but within the context of a procedural/hook-based system. Its optimizations are relatively moot when moved to OO because they're no longer needed.
Using the plugin system would actually probably be the same, if not less, considering it has already undergone its own performance optimization (otherwise we would have never have used it in 8.x in the first place). Not to mention 8.x uses Twig and render caching now.
Once it has been discovered/processed, it's cached.
In regards to runtime (rendering), the real "culprit" to performance here is memory. The current system actually tends to be a memory hog because every procedural-based function is put in the theme's
*.themefile; which is loaded every single time the system renders something.This gets really expensive in real life sites where a custom theme can have a ton of preprocessing.
By moving to an OO plugin based system, it only loads code that is actually needed for rendering whatever was actually requested. Not the entire theme itself.
I really don't understand why people tend to glorify or marginalize the current theme system.
It's not really truly performant. Its just "optimized" as much as it can be considering its current procedural/hook-based system.
It needs to be replaced with our modern plugin system.
Comment #18
donquixote commentedRe #17:
Before a POC, I would very much like to see some simple usage examples.
E.g. how would the following change?
- A module wants to register a theme hook (or whatever you want to call it).
- A module or theme wants to preprocess theme variables.
- A theme wants to override a template.
I suppose some of this would not change at all, but some of it would change with your annotations and plugins?
Of course, if I understand correctly, existing ("legacy") code would still work, but there would be new options how to do some of the things above?
-----
OP:
Do you mean what is now an extension with type = "theme" should become a class/plugin instead?
Where do you put the
@Templateannotation?And what do we gain by saying "template definition" instead of "theme hook"?
Imo, they are not the same.
A "theme hook" is a string key, which in the end will be mapped to a template, in some cases this mapping depends on the variables sent along with the hook name. But there is no fixed 1:1 mapping between theme hook names and template files.
So, I would strongly suggest to continue using separate names for these separate concepts.
"Theme hook" might remind us of something procedural, but it doesn't have to.
It is just a string key that will be mapped to a theme registry entry and a template.
I looked at https://cgit.drupalcode.org/plus/tree/src/Plugin/Theme/ThemeInterface.ph..., which seems to be the interface for this "plugin type".
It is far too big for one class or interface. Imo this makes it a dead end.
----
Back to #17
Any general statement about whether procedural code or OOP code is "faster" is usually bogus.
There are some statements we can make, see below.
The question is if and how they apply to your proposed changes. This is hard to say at this stage, where I don't see any code yet.
foo()is (a bit) faster than an object method call like$obj->foo().$foo()is slower than a static function call likefoo(). I would suspect it is also slower than an object method call like$obj->foo(), but faster than a dynamic object method call like$obj->{$foo}().$module . '_' . $hookconcat, or worse, anystrpos()andsubstr(), slow down procedural hook systems.However, this can be greatly improved by using tricks with get_defined_functions().
new $class()is likely slower than a static class instantiation likenew C().But I suspect a much bigger impact is from unnecessary junk that we serialize along with data that we actually care about.
I made these statements based on some common-sense considerations. I could be wrong.
But the essence is: Whether OOP or procedural code is faster, depends largely on what we do, and how we do it.
In our case, the biggest question is when do we perform these operations? On cache rebuild, or in every request?
If the answer is "on every request", then do we perform it once every time a theme hook is executed, or just the first time this theme hook is executed?
All of this is unanswerable at this stage.
I imagine this file can get quite long, if you have a lot of preprocess functions.
But can it really be that long that you have to worry about memory?
I haven't seen relevant THEMENAME.theme files for D8 in client projects, but I have seen a number of
template.phpfiles in D7 client projects. They can be long, but usually it is nothing compared to the accumulated core code that is included in every request.All of this will be in opcache.
Let's compare.
Procedural preprocess, as it is now:
$foo()) (*).\Drupal::service(..).(*)
The redundant function_exists() calls could be avoided, as proposed e.g. in #2954402: Refactor ThemeManager::render(), split into smaller immutable objects..
The dynamic calls could be avoided with code generation.. but it is not clear if this is a good idea.
Discovery of preprocess functions can be optimized with get_defined_functions(), like it is done in registryonsteroids. This would make it faster than any plugin discovery (I say without any proof, because we have no POC to compare with).
OOP preprocess, as I imagine you want to have it (please correct me if I'm wrong):
The class needs to be autoloaded.
This reduces the code to be included on requests with only few preprocess functions, but increases autoload overhead on requests where many preprocess functions are executed.
I think the plugin manager will take care of that.
Drupal::service(..).The injected dependencies are actually a relevant selling point. Also having an interface is nice.
For performance: The discovery might be faster than current procedural preprocess discovery. But I suspect that an optimized procedural preprocess discovery will be faster than a plugin discovery ever can be.
And then, even if move to something OOP, I am not convinced that the plugin system is the best way to achieve this.
-----
All of this is based on what I imagine your proposal would look like.
I might totally change my mind if I see more details.
I strongly doubt that.
Perhaps the execution of preprocess functions is as good as it can be without code generation. Except for the repeated function_exists() call.
The discovery, last time I looked, was far from what is technically possible.
For module hooks, ModuleHandler::invoke() is silly because we cache module names instead of function names, so we need a string concat before each call. And then we attempt to process the return value even for hooks where we don't care about it.
Comment #19
markhalliwellI don’t even know how to respond to ^ right now.
Like I said, you’re questions currently don’t have answers because I haven’t built it out yet (POCs are how I determine what is actually feasible/makes DX sense).
Until then, this level of scrutiny feels nothing more like than an attempt to hold onto/fix the procedural based theme system. It also feels like discouragement instead of help.
I probably won’t have an answer for any of this until after DC Nashville, where I suspect I will be discussing this with a few people.
Comment #20
donquixote commentedHi,
I apologize for the negate energy. I don't know if I can avoid it, and still say what I need to say.
I was coming from #2954402-11: Refactor ThemeManager::render(), split into smaller immutable objects. and -16.
One could categorize this as "discouragement", but in the end, I think you should scrutinize me all day, if you think I have bad ideas, as long as you explain your reasons.
In your comments over there, you suggested that the "theme hooks as plugins" would either conflict with my proposed refactoring, or make it obsolete. This is why I went here, hoping for a clearer picture of what you are planning to do, so that I could decide if I like it, and if so, adjust my own patches.
So now I don't have a clear picture what you are planning, but also cannot proceed with my own proposed refactoring.
I think I will wait until after DC Nashville. I became curious in this stuff during the last weeks, but it is not really that urgent.
I hope my "discouragement" contains at least some useful feedback.
As you say yourself, the old procedural system is not going away, at least not the public API part of it.
You can add alternatives, but you cannot remove the legacy parts.
So I imagine that refactoring this old logic will still be relevant, even after moving to plugins.
Btw, another refactoring site is the theme registry itself, see #2957440: Refactor ThemeRegistry and Theme\Registry.
I am not sure that my analysis there is accurate and complete, but I definitely think that these classes are not in the shape that they should be.
Imaginary consumer code is less work than a working POC. The DX equivalent to a paper prototype. And it can give you your first test case.
But of course a POC can also bring some insight. Do what you need to do.
Comment #21
markhalliwellNo worries. Just got a bit overwhelmed is all :D
Most definitely, just need time to parse all of it and merge with what I already know/comprehend of the theme system internals.
While true, that doesn't mean we still have to actively use them moving forward.
No one is suggesting that we "remove" the procedural/hook-based legacy APIs/code. That can never happen in 8.x.
It just has to be "supported" (i.e. convert to a legacy based plugin) for BC/migration purposes... that's all. We don't have to "fix" it.
Nor should we. That's a huge waste of time and a very large rabbit hole.
Attempting to wrap it with OO code, while this may work for your code, will only introduce confusion and more technical debt that we'll have to maintain until it can be removed in 9.x.
We'll be introducing new internal APIs that, ultimately, we should never really use nor advertise. I can see the benefit of this for personal/custom code when attempting to refactor or understand how something works, sure... but not for core.
Just because we keep the "old way" for BC reasons, doesn't mean we cannot refactor the entire thing to use something much better.
When we create the new system, we deprecate the old one and tell people to not use it. We don't have to "fix" the old sytem, it "works as designed", within the many, many limitations it has.
Also, the term "theme hook" implicates that it is procedural by nature. It's also a term from when we used PHP theme _functions_.
Because this is an attempt to modernize, this terminology should reflect what we're actually defining now: templates (not procedural/hook-based functions)
---
Re: plugins vs. your methodology
The primary reason plugins are the better choice is for extensibility and the fact that it already exists, we're not reinventing the wheel here. It has the majority of what is needed to manage this.
Themes have, historically, been a PITA to override whatever core (and/or the base theme) has implemented.
By implementing plugins, a sub-theme has the ability to quickly define the same plugin and simply extend from whatever it's overriding. Thus, giving them the ability to chose whether or not to inherit the parent plugin's implementation.
I personally believe this methodology is why the 8.x code for Drupal Bootstrap already has 44k+ installs https://www.drupal.org/project/usage/bootstrap
It has proven to be a very effective TX(theming experience)/technique given the vast complexities of what theming actually entails.
---
The concept of using plugins in the theme system though, is more prevalent for base themes and why a
@Themeplugin, I believe, is needed.This is not replacing theme extensions with a plugin. Think of it as an enhancement or a way to abstract the internal preprocessing/rendering phases of the theme itself in a way that is extensible.
A relatively "simple" theme would likely never have to implement a
@Themeplugin as it would inherit the "default"@Themeplugin from core.The real power of creating a
@Themeplugin though is primarily for base themes (like Drupal Bootstrap) that need a way to inject into those preprocessing/rendering phases.It's merely shifting where this stuff actually happens (from
ThemeManager) so that in the event a theme _needs_ to inject x.y.z, it can without having to rely on a module to alter the service.In the case of Drupal Bootstrap, these are things like adding extra variables (https://drupal-bootstrap.org/api/bootstrap/src%21Bootstrap.php/function/...) during the theme registry phase (https://drupal-bootstrap.org/api/bootstrap/src%21Plugin%21Alter%21ThemeR...)
As well as adding additional runtime variables like is done in https://drupal-bootstrap.org/api/bootstrap/src%21Bootstrap.php/function/....
All in all, plugins are the easiest and most performant way to allow better control over a system that natively has a hierarchical/inheriting/accumulative structure that is the theme system.
---
I'd definitely be willing to discuss this more in depth, perhaps we should start scheduling weekly or bi-weekly hangouts.
I hope to have more concrete "examples" in the next few weeks as well, which should help clarify/solidify this concept (which I'll admit is currently mostly in my head right now).
Comment #22
donquixote commentedWell, I am not attempting to "fix" the system itself, just refactor its implementation. This is a big difference.
Again, I am not "fixing" or changing the way people can use the old system.
And this "legacy plugin" can be nice-looking or ugly.
Judging by the complexity of the existing/old system and its current implementation, it will be ugly, and we will likely introduce bugs or unintended behavior changes when doing this.
I don't get this.
OOP code is not a nice-looking costume, it is, if done right, the most maintainable way to implement something. Whether this "something" is from a past era does not matter.
I don't do this stuff to make it look nice superficially, but because this is, as I see it, the best tool for the job.
If you mean the additional classes and interfaces that I proposed in the other issue:
Yes, these classes would only be used for this one purpose. They might still be usable as parts of your "legacy plugin", but still it would be a very limited application.
I think this does not matter at all. The worst that could happen is that we leave some classes and interfaces that are not used anymore. These files would not be included on an average request, and we'd only keep them in case some odd contrib or custom module ignored the "@internal" tag.
The idea that "internal" and one-off interfaces are to be avoided leads to the unhealthy situation we have now. Classes which do far too much on their own.
See #2956549: [policy, no patch]: Atomic behavior objects
Of course not every one-off internal interface is good. Perhaps in #2954402: Refactor ThemeManager::render(), split into smaller immutable objects. I do go one step too far. I have a branch locally, and made the patch based on the latest commit.
Comment #23
markhalliwellComment #25
olexyy.mails@gmail.com commentedAbout any hook to plugin:
https://www.drupal.org/project/hook_manager
Comment #36
joachim commented> But there is no fixed 1:1 mapping between theme hook names and template files.
This is something that's really confusing about the theming system. There is often a theme hook name and a template file with a matching name, which can make it seem like there's something automatic going on, but AFAICT there isn't.
This also makes it confusing to understand what you're going to get if you use #theme vs #type in render arrays.
E.g. #theme => table and #type => table give you slightly different results!
Comment #37
dqdExactly this ^^ !