Problem/Motivation
There have been multiple issues lately involving the theme registry.
Because the theme hook arrays are passed around to many places, it is very difficult to track down which code is modifying which key.
It is also hard to full understand the ways that certain keys interact together.
This is very similar to the issues with the $form_state
arrays in D7 and before.
Proposed resolution
Mimic the same solution used for $form_state: create a class to hold these theme hooks.
This time, we'll need full BC.
Remaining tasks
- review
- commit
- open follow-up #2873117: Remove ArrayAccess from ThemeHook
User interface changes
N/A
API changes
API addition and deprecation, no breaking changes
hook_theme() should now return an array of \Drupal\Core\Theme\ThemeHook
objects
ThemeHook
implements ArrayAccess
for BC purposes.
Data model changes
N/A
Comment | File | Size | Author |
---|---|---|---|
#144 | 2863819-nr-bot.txt | 90 bytes | needs-review-queue-bot |
Issue fork drupal-2863819
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.plunkettComment #3
tim.plunkettAdding some related issues for context.
Comment #4
phenaproximaInitial thoughts --
I like the direction this is going. A few minor observations, in no particular order:
Comment #5
tim.plunkett┬─┬ノ( º _ ºノ)
Putting the related issues back :)
Here's some more progress.
Comment #6
dawehnerI love how moving to value objects allow you to centralize documentation
The alternative could be to have a builder object which returns a ThemeHook object when its marked as complete. This would allow to make the ThemeHook object to be a bit easier. I'm not sure its worth the tradeoff though
I'm curious, how can the available variables be null? Could it not also be an empty array by default?I'm wondering whether having a RenderElementThemeHook and a normal ThemeHook would clarify things a bit
This behaviour is a bit weird ... the
$other
is ignored when a value in$result
is set.At least for me this is not an expected behaviour given how
array_merge
and\Drupal\Core\Cache\CacheableMetadata::merge
works.Maybe just document that the existing theme hook "wins"?
Should we unique in both places?
It feels a bit weird to have these complex functions on this value object. I can't articulate why though to be honest.
Should there maybe be a hasVariables method? Given that this is about renderElement vs. not renderElement maybe isRenderElement or
if ($info instance RenderElementThemeInfo)
or so could be also used hereNice find!
That is a great idea!
Comment #7
tim.plunkett#4
1) Discussed below in response to @dawehner
2) It's not really as reusable as you think, and it was pretty much lifted from \ArrayObject
3) Domain objects don't need interfaces. This is a mistake we made before (EntityTypeInterface, FormStateInterface) that we should have avoided, and have begun to rectify it in new classes (LayoutDefinition)
4) I disagree. The 99% use case is to not specify a path. The only systems in core that use it are Views and LayoutDiscovery, as they are doing the theme hook registry on behalf of plugins. The rest of everyone just let's them reside in /templates, which is the default. Additionally, if you are altering the path for some custom solution, you may not also be changing the template, so you'd need a separate setter anyway.
5) We can iterate on that later, but there are explicit
function_exists
checks that are out of scope to change.6) This is a terrible name. I put an in-code @todo to fix that, as I meant to do for the original patch.
#6
2) I thought about that as well. Still unsure, I just started with the basics and am untangling as I go
3/7) I added the has* methods only when necessary, I agree that needs adding.
4) I wrote it this way because it is what essentially happens now, and what would absolutely happen if #2861840: Preprocess functions are not merged when a module registers a theme hook for a theme-provided template went in. Note, this also fixes that issue.
5) There was already array_unique calls for preprocess. The includes, not sure that it matters, since it's just looped over and include_once. Could be an optimization, sure
6) It's not a real value object, it's more like a domain object/model? Maintaining the internal business logic for this object in another class seems wrong to me.
So there are two ways to divide up theme hooks.
First: whether they use a template or a function. That distinction will go away in D9.
Second: whether they use "variables" or "render element". This has always been a confusing distinction, and I'm not 100% sure that enough people care to expose it as a more prominent part of the API.
For the ideas about RenderElementThemeHook vs ThemeFunctionHook vs TemplateThemeHook vs VariablesThemeHook, I think it would be helpful to plan out the API people use within hook_theme().
Right now the one example is
The 'template' or 'function' part is completely optional, there is a fallback.
Every theme hook has to have either 'render element' or 'variables'.
But only after processing. It's also valid right now to just have something like
$items['field__node__title'] = ['base hook' => 'field'];
and the processing will take care of adding in the right values.
So, I'm not sure how to proceed on that front.
In the meantime, while reasearching that last point I made, I found two instances where things were broken.
Also I added a hasVariables.
Comment #8
tim.plunkettTo be sure that we had everything we needed, I also ported theme.inc over. It revealed one more necessary change.
I also tested a version with ArrayAccess removed, and everything else that consumes theme hooks ported over (not including hook_theme implementations). Keeping separate patches for now.
Comment #9
dawehnerOne thing I would wonder: how big is the theme registry cache entry afterwards? Is it growing significantly?
Comment #10
tim.plunkettYep, it did get a bit bigger. But this should help.
Comment #11
phenaproximaI didn't read through the entire patch, but I found these minor things:
The
!empty($pattern)
check is no longer necessary here.Why isn't this done in ThemeHook::createFromExisting()?
!empty($pattern)
is not necessary because the previous line guarantees a value.Same question as before -- seems like something that createFromExisting() should do.
A little early for a nitpick like this, but 'array' should be capitalized.
Comment #12
tim.plunkett1/3) This reveals a bug in my conversion. Before you could have
$info['pattern'] = '';
and bypass this code. We can't rely on the truthiness of getPattern(), this needs a hasPattern().2/4) Hah, it *is* done in createFromExisting() already. Good catch, that code was bugging me.
5) The full comment is
More info on the cache size thing.
In HEAD, the average theme hook is a serialized array of about 7 key/value pairs.
A full ThemeHook has 17 protected properties. But with the change in #10, the number is the same as before.
The only difference is that a serialized array would read
a:7:{...
whereas a serialized object isO:27:"Drupal\Core\Theme\ThemeHook":7:{...
So there are 33 extra characters per entry. That's not very much, but in aggregate it does add up to a few KB on a standard site install.
Without #10, it was around 2x the size.
Opened #2864354: Convert hook_theme() to use ThemeHook::create() instead of arrays as the follow-up
Comment #13
mpdonadio@tim.plunkett, is there a way to distill this down to a review-only patch? Something that just show the essentials of the change w/o all the conversions (maybe just one)? Or is this really a 100k patch to be reviewed?
I looked at this, and I think a test-only patch that has the essential changes and then relies on the BC array access for everything else would be a good idea when you feel this is stable. Maybe that is something that could also be a review-only patch?
Comment #14
tim.plunkettYep here's a review-only patch.
The thing is, I had to comment out the exception in ThemeHook::process, that's what necessitated most of the changes.
Also I fixed that bad method name.
There are no more known remaining tasks for this issue.
Comment #15
tim.plunkettOkay I've distilled down what should be a passing patch containing only necessary changes.
I'm keeping both changesets running locally.
Comment #16
mpdonadioThis is going to take some time to review; I am more of a casual bystander here. I am not sure if I can properly review this, as I don't fully understand this. However I spent some time look at this, and from an overall product standpoint, I think this is addresses some of the current baggage we have in core in a good way. I also want to give it a look applied, which I wasn't able to do.
One thought, though, that permeates the patch...
Do we buy anything if we make $function a callable? I am pondering if support now for allowing for the class/object+method will put in a better place for future refactoring (though I suspect that doing more of this type of stuff with annotations may be the road we go down).
Comment #17
tim.plunkett#4 asked the same thing. I think that's out of scope, considering we'd have to change all the function_exists checks as well.
It could be done right now in HEAD without this patch, or after this patch.
Comment #18
markhalliwellI really like the direction here.
I feel like, if we're going to go the route of turning these into objects, we may as well go all the way and have the full ability of the plugin system as well.
This is essentially the same thing as
@RenderElement
, but for#theme
instead of#type
.I personally would prefer something simple like
@Theme
over@ThemeHook
since "hook" is really just a way to indicate the procedural implementations in Drupal prior to 8.x.This would allow us to add a
preprocess
method and tie the definition of a theme hook with the necessary code that preprocesses it.Thus, ultimately allow subclassing to also give us a better ability to determine when something should be inherited or overridden.
I'm currently implementing a few custom/theme specific plugins in Bootstrap that do very similar things: @BootstrapPreprocess.
It's amazingly liberating to have that much control over when, where and how something is preprocessed in the theme system.
---
I'm adding the related "yml" issue because it's essentially trying to do the same thing here: get rid of arrays. However, given that theme hooks inherently come with PHP code (usually to preprocess variables), I doubt it would make much sense to continue down that direction. Plugins are a better approach.
Comment #19
tim.plunketta) If these were plugins, these ThemeHook objects would be the equivalent of a plugin definition, not a plugin instance.
b) Render elements were barely worth making plugins IMO, and only since they have the need for inheritance (MachineName is a Textfield is a FormElement)
c) The main reason to use plugins would be for a nice interface each would have to implement (this is where RenderElement falls over). Preprocess functions could not use such a thing.
I tried to make them plugins (see the issue you linked), it didn't work, I'd like to not try that again.
Comment #20
markhalliwellCorrect. This would mostly be the
@Theme
annotation class. Any actual logic for discovery/conversion of legacy theme hook arrays should be done in the plugin manager.There are multiple layers/opportunities to alter the final output of a theme hook, this is the exact the reason they should be plugins. Theme hook suggestions also increase the importance of why this necessary since they add even more variety. That's definitely more than "barely" worth it.
This isn't true.
We're doing it in Bootstrap just fine. Granted, there are some advanced work arounds because core's invocation of preprocess functions are still manual (i.e. not using CUFA), but the POC is stable and one of the reasons why Bootstrap already has 22,188 reported installs for 8.x.
This would obviously still need to keep an internal array of preprocess callbacks. One that could easily add
template_preprocess_*
legacy procedural functions if they existed.Allowing preprocessing of a theme hook inside a class is a massive DX improvement.
We'd have the ability to override existing implementations and choose wither or not to call
parent::preprocess()
.Are there a couple current "limitations" around this topic, sure?
However, it's mainly because the internals of the theme system were never really refactored to be OO and they are seriously overdue.
That doesn't mean this cannot easily be solved if we finally architected this properly.
The two primary pain points are:
twig_theme()
's use of$existing
as you mentioned in the other issue. Technically, this should just be moved to the alter phase of the discover because it's not actually providing new theme hook info. For legacyhook_theme()
implementations, we could easily pass the current list of definitions since they would/should be last in the plugin discovery process.I completely understand and I'm not trying to ignore previous efforts.
I am, however, extremely familiar with 8.x's current procedural implementation of the theme system + adding OO on top of it.
A lot of the code I've put in Bootstrap could easily be abstracted into core and what I had originally intended https://www.drupal.org/project/basetheme to be.
I don't believe we should add something that, if eventually replaced, will also have to be supported for BC reasons.
If this issue aims to make this OO, we may as well go all the way.
I'm willing to help make this a reality because, as someone who uses this on a daily basis, it's definitely needed in core moving forward.
---
edit: I'm just asking to keep an open mind and let me help solve these issues with you. I know it's going to be hard, there's no doubt. I'd just like the chance to do this right.
Comment #21
tim.plunkettNot talking about annotation classes, but object-based plugin definitions. See \Drupal\Core\Layout\LayoutDefinition for an example.
Don't have time today to respond to the rest, sorry!
Comment #22
markhalliwellI know exactly what you're talking about.
I maintain https://www.drupal.org/project/bootstrap_layouts too, remember...
Can you please stop being dismissive? I'm trying to help here and actually give this issue valid feedback from real world scenarios/development/contrib.
Your responses seem to be implying that I don't fully grasp this issue or the complexities of the theme system.
---
Theme hooks aren't layouts. Many layouts use the same code (PHP class) and it indeed makes sense to use LayoutDefinition/YAML in that instance.
Theme hook are, for the most part, completely unique and either implement or inherit a preprocessor function of some kind.
What you're proposing continues the fragmentation of code in the theme system. It keeps the theme hook definitions separate from its preprocessing. It also keeps them as procedural functions which is, inherently, a PITA to override and requires a mountain of boilerplate code in both core and contrib to provide proper inherence/overrides.
This is primarily about DX/TX. Especially in the contrib/custom realm.
I would much rather we have:
and
Than "object-based plugin definitions" from YAML files.
This doesn't need or warrant a middle-class.
Comment #23
dawehnerI think what @tim.plunkett tries to solve ia one specific problem of the theme system aka. the debuggablity of the theme registry, while @markcarver tries to improve the actual runtime operations. While they are connected with each other, they don't have to be resolved as part of this issue. Once you have the objects which contain the theme registry information (ThemeHookInfo()) you can afterwards still convert things to actual plugins. Doing things in two steps makes it possible, at least for me, to reason about it.
As a sidenode: On D8 projects I've used inheritance for preprocess functions using classes quite a bit, even well, I hope we move away from preprocess on the longrun, and keep things on the template level. There is also a lot of potential overlap with pre_render functions of elements.
Comment #24
tim.plunkett@markcarver Very sorry you took it this way, I'm just really spread thin right now, and trying to chime in when I can.
I promise you I'm not trying to shove this in as-is!
What you just explained is very informative, and also what @dawehner said.
Let's continue this discussion
Comment #25
markhalliwellSame here. I appreciate the clarification. I didn't mean to take it that way, but it was kind of hard not to.
Yes. Let's.
Yes and no. The actual runtime implementation/preprocess method can technically be done in subsequent follow-up issues.
I'm fine with that aspect since this will have to support BC with existing hooks anyway.
I just want to make sure that others realize the true potential we have with plugins here.
By coupling the theme hook definition inside the plugin, we have more cohesion and ability to notice or update a change to the theme hook.
I cannot tell you how many times I've run into bugs because I "forgot" to also update the default values implemented in said
hook_theme()
.Moving these to YAML files only keeps this level separation.
As far as debuggability goes, we'll have the same with plugins, if not more so. Annotated plugins are all classes. Classes that can be stepped through rather than a single YAML file that's only parsed once.
Yes. That was the ultimate goal, but the reality this is PHP. That isn't changing anytime soon. There are way too many real world complex "conditionals" that have to be done in the preprocess layer that simply don't work anymore in Twig. This is mainly because Twig isn't PHP and now requires building extensions... or the easier existing solution: preprocessing variables.
A simple example is: adding icons or classes based on contextual information that doesn't make its way directly to the template (i.e.
$element['#context']
).Possibly, but perhaps only the "callback/inheritance" bits. Pre-renders are usually only ever used on
#type
elements and ultimately still make their way to whatever implements#theme
.Comment #26
dawehnerGiven there is more potential we want the new object to be marked as internal so we can continue to refactor it.
Comment #27
tim.plunkettYep I had the same thought as @dawehner. This changes no @return or @param typehints on public methods, and no-one outside of Registry (and test code!) is using it now.
This gives us the debuggability win in the short-term, and opens the door to further improvements along @markcarver's suggestions.
Interdiff is against the smaller "clean" patch from before.
Comment #28
andypostThat looks very promissing, thanx for moving this forward!
Would great to get Mark's idea with inheritance for flowing issues
- #2307103: Follow-up: remove explicit comment field theme hook suggestion implementation
- #2808481: Introduce generic entity template with standard preprocess and theme suggestions
btw Related may need updates
Comment #29
tim.plunkettTagging
Comment #30
markhalliwellI'm OK with
ThemeHook
being marked as@internal
. As @tim.plunkett said, this would allow us to do anything to it in the future.I just didn't want themes to suddenly have to start providing
.yml
files which keeps the separation of definition and, more than likely, the PHP code necessary to preprocess the theme hook definition.This will allow us to create a proper plugin system for theme hooks in the future and likely migrate this OO code into that.
Comment #31
dawehnerI really like the changes. Its a bit the logic step what we did when we worked on this class YEARS ago.
I hop
Do you understand why phpstorm doesn't know the type based upon the typehint?
I'm glad that you didn't just removed the todo :)
This is weird for me at least. We have
::merge()
and$this->mergePreprocess()
now, even we had two methods before. Either their is some weird change in behaviour, or we could reuse some code?I'd be nice to have documentation why this clone is needed.
Can/Should we inline this line using
?
It is a bit weird that the information about preprocess got lost. I bet you did that on purpose, I'd just like to understand it a bit more ...
It is a bit weird that the parameter is optional to be honest. It is internal only, so I think different rules apply, but at least for a public api I'd have tried to avoid the optional parameter
Some
assertInstanceOf
could be nice here.Super nice idea for the test coverage.
Comment #32
tim.plunkett1) array_reverse eats the typehint :(
2) :)
3) The second mergePreprocessFunctions() call for base hooks in HEAD is my fault, see #2559825: Preprocess functions from base hooks not triggered for theme hooks not using the __ pattern
With this patch, mergePreprocessFunctions() is still misnamed. It actually itself does a full ->merge() call and then sets the base hook.
So in the second case when we already know the base hook, we need only the ->merge() call.
Renamed, hopefully it becomes clearer
4) Neither of the clones are needed, now that I reread the code. They were defensive against some relics of the code that only existed to support += of arrays.
5) I thought you were against assignment in complex conditionals? Leaving as is, it looks more confusing combined.
6) In the internal flow, the theme hook is marked as incomplete if it has a base hook. This is mostly because of preprocess, but also affects include files.
7) Fixed
8) Agreed!
9) Thanks
Comment #33
dawehnerNo worries, I was really just curious.
Oh totally! Thank you for that.
Oh nice!
Mh yeah I think I don't have a strong opinion about it, if I'm honest. Yeah keep it as it is.
Comment #34
markhalliwellOne tiny nit:
Since this is now marked as
@internal
I think this@todo
should be changed to mention refactoring into a proper plugin manager instead with this issue's link as a reference.Comment #35
tim.plunkettYes! We should never add @todos without issue links, my mistake.
@markcarver opened #2869859: [PP-1] Refactor theme hooks/registry into plugin managers and I adjusted the @todo accordingly.
Comment #36
markhalliwellWfm, thanks! :D
Comment #37
tim.plunkettWhoops, typo when I renamed that method
Comment #38
jibranLet's not use the snake case properties. @larowlan wrote some code to translate array values to object properties.
Comment #39
dawehnerThis is a LOT of code in order to simply deal with CS. I think the alternative is to simply hardcode a mapping between internal and external variable.
Comment #40
jibranThis can be simplified. I just shared it as a POC. I think mapping is a good option.
Comment #41
tim.plunkettI looked at doing a mapping. For BC it seems to be overkill.
Can't we iterate on this after its in?
Comment #42
jibranI know but snake_case properties bring back the D7 memories. :P
Sorry to change the status from RTBC but IMO why introduce the obsolete layer if we are going to replace it anyway. Anyway if you still think it's fine as is then feel free to RTBC it.
Comment #43
tim.plunkettI shouldn't be reRTBCing, but a theme system maintainer signed off and @dawehner RTBCd.
This only increases the "snake case properties" count from 100 to 106.
Comment #44
dawehnerWe have snake cases all over the place ...
Comment #45
tim.plunkettthat's in HEAD
Comment #46
dawehnerYeah like config entities for example.
Comment #47
markhalliwellBaby steps. The internals of the theme system API are extremely complex.
This is a temporary "solution" to unblock Layout API (@tim.plunkett, issue/reference?), that is why it's marked as
@internal
.The internal theme system is built using arrays that can be modified in any number of arbitrary ways at any given point in the process.
This issue at least gets us to a place where we can start start to OO and better test theme hook definitions.
We can iterate on it further or, the likely case, add on on top of/replace entirely with something better (e.g. plugins) as the follow-up issue suggests.
Comment #48
star-szrThis looks really nice! Would love to have a current theme system maintainer give this a look.
Comment #49
tim.plunkettOh! I missed that @markcarver stepped down from the theme system, or would have reached out earlier.
Thanks @Cottser
Comment #50
markhalliwellHeh, yeah, that was a while ago #2350981: Remove Mark Carver from MAINTAINERS.txt ;)
I wasn't aware you were referring to me, I thought you were referring to @effulgentsia as you had mentioned in IRC.
No worries :D
Comment #51
joelpittetSpent some time reviewing the patch and it looks great! Thanks for doing this @tim.plunkett!
A couple notes:
Doesn't this
unset()
need to be replaced by$cache[$previous_hook]->markComplete()?
or is it dealt with in another wayLeaving as RTBC because the last thing I mentioned is an oversight and the first is minor bikeshed gripe.
Comment #52
tim.plunkett1) yep, taken from the plugin system. A module or a theme can "provide" a plugin, or in this case, theme hook
3) This is done, but later and on all callers to this method:
Comment #53
tim.plunkettAdded links to all BC-related @todos, pointing to #2873117: Remove ArrayAccess from ThemeHook
Comment #54
star-szrI started reviewing this a few days ago and @lauriii and I sat down 1-2 days ago to discuss and review it together :)
hook_theme()
to tell people to use the new thing.@lauriii and I discussed and we agreed that it would be good to make this final for now so that people don't extend it.
Because we want people to use this new API in their
hook_theme()
implementations it might not make sense to mark it as@internal
. We'll need to support the API and the public methods going forward otherwise we break BC.So tl;dr - our suggestion is to make this final instead of internal.
Can we update this follow-up issue to talk about adding test coverage for the legacy array handling? I think we're well covered for tests at this point because there are so many legacy arrays now but that won't always be the case.
Comment #55
dawehnerIn general we should leverage final for objects which don't contain too much logic. If you would mock this class, you are lost with final classes.
Comment #56
tim.plunkett1) Expanded the hook_theme() docs
2) Wrote https://www.drupal.org/node/2873756
3) Technically there is nothing wrong with extending this class. There is no interface for a reason, so they would HAVE to extend it. If someone would choose to override the existing method implementations, that is their prerogative.
Agreed on removing @internal.
4) Yes, doing so now (but on #2864354: Convert hook_theme() to use ThemeHook::create() instead of arrays because I think that's a better place for it)
Comment #57
dawehnerFor me its about the following: If there are usecases they will come up and we can remove the final again.
Comment #58
tim.plunkettI misread #55, marking it
final
for now.Comment #59
dawehnerCool, we are finally increasing our knowledge about minimising our API surface.
Comment #60
tim.plunkettI think #54 is fully addressed now!
Comment #61
dawehnerThank you tim!
Comment #62
markhalliwell@laurii and I discussed this quite a bit and in great length at DC and I wish it made it's way here.
Marking as both final and @internal is probably best thing for this class.
Converting theme hook info array into objects is just the first baby step; one that will undoubtedly have additional follow-ups.
This implies that this may (or may not) be the end "product" that is created/consumed by the front facing public API.
Thus, we should not be publicly announcing that people (FE) should be using it.
It may be replaced with something better, or be used as a low level object that is created by a higher front facing API: plugins (which is what we'd really want to document).
Instead, and because this class has BC, we should keep the public facing API/documentation (for hook_theme) the way it is and not encourage people/contrib to use it.
Whether or not core converts them into arrays of ThemeHook objects is beside the point.
This issue is entirely for internal purposes and can of course be used by core.
---
Argument: Doing this wouldn't help improve our FX.
Counterpoint: This issue was never about FX, it was about turning arbitrary arrays into objects for better testing/handling/pin-pointing point of failures.
Second counterpoint: the reality is that anyone that is going to be using this in the first place is likely going to be backend developers or unicorns such as myself who actually understand PHP and our theme system in the first place. Regular "front end" developers could care less about this issue.
Third counterpoint: introducing new/changing existing front end facing APIs can have much larger consequences. If we start telling people who use X to start using Y, only to end up replacing it with Z... we're going to have a major cluster-*#&! on our hands. I think it would be wise if we didn't box ourselves in, we've been burned by that in the past.
Comment #63
alexpottReally nice to see the @todo here. It was my first though when I saw the \ArrayAccess.
Another good reason to mark this final and @internal is that I think the name is as about the BC rather than what it is it. Maybe ThemeDefinition or ThemeImplementation - the docs for hook_theme() say
Also I think the class docs here need improving to say more about what it does and point to other relevant parts of the document on theming.
Another thought is that we're creating another mutable value object - should we really be doing that? Mutability can lead to all sorts of odd bugs and new behaviour. At the moment a hook_theme can't change its path. With the new theme object a hook_theme implementation could call setPath after constructing the object but that doesn't make sense.
Comment #64
andypostBtw did anyone measure how that change affects memory usage?
Comment #65
jibranCan the next step be #2316941: Use the builder pattern to make it easier to create render arrays after this patch?
Comment #66
markhalliwell@jibran, that issue isn't really related to this issue or subsequent follow-up issues, e.g. using the Plugin API to create initial theme [hook] definitions instead of arbitrary arrays.
That issue primarily focuses on attempting to "OO render arrays". Which, IMO, is the wrong approach, but meh.
Comment #67
joelpittetTagging for #63.1 + #64 to get a performance test.
Interesting point @alexpott about mutability of this value object. It's kind of tricky to create this in BC kind of way and make bits less mutable, but maybe we can trim that down to bare minimum? The arrays were also mutable before, by definition, no?
@markcarver, Trying to understand your comment #64's next steps for the work still needing done: is it only related to the
final + @internal
, or did I miss something more important? Seemed all related to that but I could be missing a bigger message.Comment #68
markhalliwell@joelpittet, yes, current patch needs final + @internal so we can iterate on this in future issues (see #18 - #25). @tim.plunkett and I had planned to discuss this in length at DC, but unfortunately that didn't happen. We'll be discussing this next week in a little bit more depth. If only to maybe translate all that has been said so far into a better IS for #2869859: [PP-1] Refactor theme hooks/registry into plugin managers and what all that may entail. I suspect we may need future meetings about all this, but there's no reason we cannot leverage an already powerful API to help with a lot of the legwork.
Comment #69
tim.plunkettThere was some confusion among @Cottser, @lauriii, @markcarver, and I about what "final" meant in this case.
The PHP keyword
final
means that the class cannot be extended at all.It is not to mean that our API has been finalized!
Furthermore, I had a good discussion with @markcarver about #2869859: [PP-1] Refactor theme hooks/registry into plugin managers, which needs a good deal of updates.
Because of that issue (and other possible changes), this should remain as internal as possible. Therefore IMO no docs should be updated yet. It should be completely transparent.
Addressing #63: The most accurate name would be a ThemeImplementationDefinition. But I'd really like to push the naming to the follow-up, since the usage of it might change.
Mutability seems like a thing we could address.
But, currently changing the 'path' property is completely allowed.
views_theme()
uses it! (of course it does)The only issue with would be someone purposefully abusing this to modify the
$existing
parameter of hook_theme().Leaving the perf test for someone else.
Comment #71
tim.plunkettFigured out how the new deprecation stuff works.
Comment #74
tim.plunkettAttempted a reroll.
Comment #76
tim.plunkettNeed to trigger the deprecation at the end of processing. Let's see how many failures we get now.
Comment #78
tim.plunkettOkay should have a clean patch again.
Comment #79
tim.plunkettBump.
Comment #80
donquixote CreditAttribution: donquixote as a volunteer commentedJust arriving on this issue.
This sounds like a BC break.
Looking at the latest patch, the issue summary should be saying "Theme hooks may still return arrays for BC, which will then be transformed into ThemeHook objects automatically."
Can we call this
->setRenderElementName()
, please? My first thought looking at this was that it would accept an array, the "real" render element. It wouldn't make sense here, but as a first-time reader you don't know that.I would suggest to put this logic into a separate class.
The ThemeHook class is big enough for its main objective, which is, to contain the data of a theme hook definition.
It doesn't need to be able to process itself.
EDIT: I realize this was already discussed above between @dawehner and @tim.plunkett in #6 and #7.
I still think it should be elsewhere, because all of this can be implemented using the public API of ThemeHook.
But I am going to post another proposal (new comment) which makes this point obsolete.
So why not call it
mergeDefaultValues()
then?Let's use
===
instead of==
where possible.In this case it is safe to replace this, because we expect
$type
to be a string.I think the API would be more useful if it helped you to know which default keys need to be filled in.
E.g. in this case how do you know that you need to call ->setRenderElement()?
So, perhaps have "combined" constructors like ::createForRenderElement()?
More in the next comment.
Comment #81
donquixote CreditAttribution: donquixote as a volunteer commentedLooking at functions like ->markComplete(), ->process() etc.
I think the idea here is that hook_theme() returns incomplete ThemeHook objects, and the central registry code completes them.
However, there is no technical guarantee that a ThemeHook object is really complete, even if ->markComplete() was called.
I would like to propose something else:
Have distinct classes for theme hook definitions returned from hook_theme(), and those saved in the theme registry.
Then ->process() would not modify data on the same object, but instead create a new object. And it would be natural that ->process() would live outside either of those objects.
Comment #82
donquixote CreditAttribution: donquixote as a volunteer commentedWhere those saved in the theme registry are also those that are used in ThemeManager::render().
This split allows a more immutable design:
- Objects saved in the registry and sent to ThemeManager::render() can be completely immutable.
- Objects in hook_theme() could also be immutable, with wither methods (e.g. ->withTemplate()) that create clones (*). If not too many of those methods are called, the performance hit will be acceptable.
- Processor methods, e.g. to discover preprocess hooks, can do their calculations and then create a new object (the one which will be saved and sent to theme registry).
This makes sure that objects are not modified in the wrong place.
It also allows to hide some parts of the API to hook_theme(). E.g. it blocks hook_theme() implementations from adding preprocess functions.
(or is this explicitly allowed?)
And it makes it clear in the code where we have a complete or an incomplete theme hook object, because they have different types, instead of being the same object at different times with different states (which is a reoccurring problem in Drupal).
Comment #83
tim.plunkettThe current code is marked as @internal with an @todo explaining the plan for future improvement. None of this is a BC break or API expansion.
I'd much prefer that #81/82 be moved to a follow-up issue.
Comment #84
donquixote CreditAttribution: donquixote as a volunteer commentedBut the test shows that hook_theme() will be allowed to return objects. So it is an API extension. Perhaps documented as @internal, but is this enough?
If we want piecemeal change instead, we could remove this again in hook_theme(), only accept arrays, and transform those into objects internally.
We could implement one side of my proposed split, and leave the other for a follow-up.
So, make class HookTheme suitable for being stored in the registry and being used in ThemeManager::render(), but not (yet) as return value for hook_theme(). This would make possible follow-ups less disruptive.
Or, even we want to split it up further, we could implement something where the theme registry continues to store arrays, which are converted to objects before using them in ThemeManager::render().
Comment #85
tim.plunkettYes, the test coverage uses the new @internal code.
And yes, we have to be able to trust that @internal is enough.
I think forcibly preventing use of ThemeHook would be a step too far. Also, how would we test what we have?
Comment #86
donquixote CreditAttribution: donquixote as a volunteer commentedWe would simply assume that all definitions returned from hook_theme() are arrays, just as we did in the past.
Or maybe add a sanity check, instead of just assuming.
Then turn every such item into an object:
$theme_hook = ThemeHook::createFromLegacy($hook, $info);
.Note that I am using a different variable to hold the object.
Btw, currently the patch does not check for all cases. If we already check the type, maybe we should do a complete sanity check?
Current patch:
what if $info is neither an object nor an array?
Comment #87
donquixote CreditAttribution: donquixote as a volunteer commentedBecause they are all converted, all tests of theming functionality would use those objects.
Comment #88
donquixote CreditAttribution: donquixote as a volunteer commentedI also why we want to expose all the details of the former theme registry entry array as part of a public API?
Why not expose behavior instead?
E.g. ThemeManager::render(), we would then call
$theme_hook->render($variables);
(after applying all the suggestions logic). This would do everything internally: preprocess, and include the template or execute the theme function.Or we could expose smaller chunks of behavior:
-
$theme_hook->preprocess()
would execute all preprocess functions.-
$theme_hook->generateOutput($variables)
would call the theme function or include the template.We could even split this last part into a separate class/interface, with distinct implementations for theme function and template engines.
All the public getters could go away if we do this.
Or maybe we do both things? Use the class proposed here for storage (although I am not so excited about object serialization), then build those other objects I am talking about based on this information?
If we do this, I would prefer if the class discussed here would be called ThemeRegistryEntry, so that the name ThemeHook is free for new things.
I think those behavior objects would bring us more structural improvement than objectifying the raw theme registry entries with getters and setters.
Comment #89
donquixote CreditAttribution: donquixote as a volunteer commentedI created an issue for my proposed changes: #2954402: Refactor ThemeManager::render(), split into smaller immutable objects.
Work in progress.
Comment #90
markhalliwellWas going to set as RTBC, but its still tagged as needing a performance review.
Personally, I think any performance impact (which is negligible IMO due to caching) should not really be a concern for this issue.
Considering that this is just an internal/temporary solution and will be replaced with #2869859: [PP-1] Refactor theme hooks/registry into plugin managers, which has all the optimizations that come with plugins/managers.
Comment #91
markhalliwellRe #69:
I also think we should remove the change record. This isn't something that we really want to advertise as it's likely to go away in the future.
Comment #92
donquixote CreditAttribution: donquixote as a volunteer commentedSomething I missed to say in my previous comments.
The theme hook $info arrays are passed around to preprocess functions, which expect to see an array.
The ArrayAccess will be good enough for most of those implementations. But some array operations are not supported by ArrayAccess:
- If someone (like me) puts a native "array" type hint in front of the $info parameter of a preprocess function, it will break.
- If someone uses array addition operator (+, +=) on $info (which is a bad idea in preprocess functions).
So.. most preprocess functions will be ok.
Technically it is still a BC break for preprocess functions. It might be a small enough BC break to be acceptable when upgrading from 7.5.x to 7.6.x. But let's not pretend that it does not change the API.
Comment #93
markhalliwell@tim.plunkett, I'd like to set up another meeting to discuss this and other issues surrounding the theme system. Ping me in slack when you have a minute.
Comment #94
kostyashupenkoComment #95
markhalliwellI'm wondering if we shouldn't postpone this on #2972143: Create \Drupal\Component\Utility\ArrayObject which this issue could greatly benefit from by extending ThemeHook from ArrayObject.
Comment #96
markhalliwellComment #98
lauriii#95 I don't think we should block this issue on that, even though this could benefit from that.
Would be great if someone who know what is missing could update the remaining steps to the issue summary.
Comment #99
voleger#94 patch needs reroll against 8.7.x
Comment #100
kala4ekFresh reroll according to current 8.7.x
Comment #102
andypostComment #103
vacho CreditAttribution: vacho at Skilld commentedPatch rerolled
Comment #104
YurkinPark CreditAttribution: YurkinPark at Skilld commentedComment #107
andypostComment #108
andypostComment #109
mrinalini9 CreditAttribution: mrinalini9 at Srijan | A Material+ Company for Drupal India Association commentedRerolled patch for 9.1.x, please review.
Comment #110
mrinalini9 CreditAttribution: mrinalini9 at Srijan | A Material+ Company for Drupal India Association commentedPlease ignore patch #109, here is the rerolled patch for 9.1.x, please review.
Comment #112
rishab.singh CreditAttribution: rishab.singh as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedComment #113
rajandro CreditAttribution: rajandro as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedComment #114
rajandro CreditAttribution: rajandro as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedAdding the test case fixing.
Thanks
Comment #116
rajandro CreditAttribution: rajandro as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedComment #117
rajandro CreditAttribution: rajandro as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedFixed test case and phpcs coding standard violation of core/lib/Drupal/Core/Theme/Registry.php
Comment #118
rajandro CreditAttribution: rajandro as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedComment #119
Hardik_Patel_12 CreditAttribution: Hardik_Patel_12 at QED42 for Drupal India Association commentedLast patch failed to apply , re-rolling the patch kindly review.
Comment #122
naresh_bavaskarRe-rolling the patch for the latest branch
Comment #123
anish.a CreditAttribution: anish.a as a volunteer commentedRerolling based on #119
Comment #126
solideogloria CreditAttribution: solideogloria commentedComment #127
andypostComment #128
immaculatexavier CreditAttribution: immaculatexavier as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedComment #129
immaculatexavier CreditAttribution: immaculatexavier as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedRerolled #123 against 10.1.x
Comment #130
Anchal_gupta CreditAttribution: Anchal_gupta at Srijan | A Material+ Company for Drupal India Association commentedI have fixed CS error. Please review it
Comment #131
Medha KumariReroll the patch #130 with Drupal 10.1.x .Please review it.
Comment #132
needs-review-queue-bot CreditAttribution: needs-review-queue-bot as a volunteer commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #133
andypostStarting with #129 the patch after re-roll became x2 smaller
Comment #134
sourabhjainTrying to fix to custom command failed issue of #132
Comment #135
smustgrave CreditAttribution: smustgrave at Mobomo commentedRemoving credit for #134, 131, 130, 129, 123, 122 as it is expected to check a patch before uploading. You can check for build errors make sure to run
./core/scripts/dev/commit-code-check.sh
before uploading a patch to make sure there are no issues with code formatting. see https://www.drupal.org/docs/develop/development-tools/running-core-devel...Hiding those patches as well.
This was previously tagged for issue summary update which still needs to happen.
Also as @andypost pointed out the patch got cut in half. How come?
Comment #136
tim.plunkettThe entire ThemeHook.php was dropped from the patches. Working on this.
Comment #137
tim.plunkettSwitched to an MR.
This will probably break things, leaving assigned for now.
Fixed a bunch of credit.
Comment #140
lauriiiTried to rebase the MR. Needs performance testing still.
Comment #141
donquixote CreditAttribution: donquixote as a volunteer commentedJust a quick note not for performance but memory/storage.
I was suspecting that these objects significantly increase the size of the registry when serialized in cache, due to all the unused properties.
But actually, uninitialized properties take zero space when serialized: https://3v4l.org/eoLeo
Therefore the increase in cache space is much smaller than I previously expected, at least for plain core.
For me, the string length in the `cache_default` table grows from ~83.000 to ~100.000 for theme_registry:olivero.
Comment #142
andypostIt's getting bigger just because of namespaced class but as I see it has
\0
code so sqlite truncating itmy numbers
Comment #143
tim.plunkettComment #144
needs-review-queue-bot CreditAttribution: needs-review-queue-bot as a volunteer commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #145
kostyashupenkoRebased
Comment #146
smustgrave CreditAttribution: smustgrave at Mobomo commentedSeems rebase had some errors.