code at http://cgit.drupalcode.org/sandbox-sun-1255586/?h=theme-init-2228093-dawehner2
Problem
Theme system is a mess.
If you interact with the theme functionality, you currently use one of the following ways:
- You use global $theme, ..., to get information about the current theme.
- You use _theme()/drupal_render() to generate some output.
This direct access only works, because the current theme happens to be manually initialized in various spots of the code-base. (@see LegacyRequestSubscriber)
Proposed resolution
Provide a service ('theme') which you can use to either render some output or get the current theme.
This is the canonical place for interaction.
In case you don't have the current theme initialized yet, the theme service asks the negotiator to get one and initializes (direct or indirect) that theme.
In order to do that, it needs a domain object of the current theme, which for example contains the theme info, stylesheets and libraries, as well as a list of the same domain object representing the parent themes (if any).
// @todo Find a better name.
interface ThemeServiceInterface {
/**
* @return string
*/
public function render($hook, $variables);
/**
* @return \Drupal\Core\Theme\ActiveTheme
*/
public function getActiveTheme();
/**
* Execute the alter hook on the current theme.
*/
public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL);
}
The theme service has the following dependencies:
theme:
class: Drupal\Core\Theme\Theme
arguments: ['@theme_handler', '@theme.negotiator', '@theme.initialization', '@state']
State is used instead of cache to not rebuild the theme domain object all the time. (as in HEAD)
High-level service overview
-
ThemeHandler
only cares about managing theme extension objects, in the exact same way asModuleHandler
.The
Extension
objects are no longer manipulated with theme info/data. -
ThemeNegotiator
determines the theme to use for a particular request. -
ThemeInitialization
consultsThemeHandler
to retrieve the list of enabled themes, in order to instantiate theActiveTheme
domain object initialize the required theme engines.This is essentially
drupal_theme_initialize()
+_drupal_theme_initialize()
.ThemeHandler::rebuildThemeData()
+system_rebuild_theme_data()
is moved into this service. -
The
ActiveTheme
domain object is only built once for each theme and stored in state.This domain object consists of
global $theme_info
andglobal $base_theme_info
. -
The
Theme
service returns the initializedActiveTheme
domain object of the current/active theme for consumers.This replaces access to all
global $theme*
variables.
Notes
-
Theme::alter()
removes support for themes to participate in all alter hooks fromModuleHandler::alter()
.Instead,
Theme::alter()
will be specifically invoked manually for the few hooks in which themes are supposed to participate. That list is very limited; e.g.:hook_form_alter()
hook_page_alter()
hook_theme_suggestions_alter()
hook_theme_registry_alter() -
This plan should allow us to get rid of
$request->attributes->set('_theme_active')
inThemeNegotiator
, but we admit that we don't know how many things depend on that currently.
Comment | File | Size | Author |
---|---|---|---|
#49 | theme-init-2228093-49.patch | 88.13 KB | dawehner |
#32 | theme-2228093-32.patch | 81.35 KB | dawehner |
#30 | 2228093-30.patch | 79.02 KB | dawehner |
#30 | interdiff.txt | 16.59 KB | dawehner |
#27 | theme_init-2228093-27.patch | 76.97 KB | dawehner |
Comments
Comment #1
sunComment #2
sunComment #3
larowlanNote #2016629: Refactor bootstrap to better utilize the kernel touches parts of theme init. Reviews needed.
Comment #4
dawehnerComment #5
sunAdding some more context and info.
Comment #6
tim.plunkettI really like the proposal so far.
If the theme service is Drupal\Core\Theme\Theme, where will the Theme domain object live?
ViewExecutable::render() uses
$GLOBALS['base_theme_info']
andto invoke a couple other "hooks", we should consider those as well.
Comment #7
sunYeah, appropriate naming turned out to consume quite a chunk of time in the discussion, and ultimately, we decided to table that question... ;-)
Perhaps one sensible approach would be the following?
Drupal\Core\Theme\Theme
— The Theme service, your primary interaction point.Drupal\Core\Theme\ActiveTheme
— The domain object representing the currently active + initialized theme.That is, because at least I am not able to find a sensible name for the theme service class + interface, if
Theme
would be the name of the domain object (instead ofActiveTheme
) — in that case, the service would have to beThemeManager
or similar - but it actually does not manage themes, it only supplies the (one) theme for a particular request...Since the theme service is bare essential and the primary interaction point for pretty much the whole system, it would be great to simply call the service ID "theme". — Ideally, a service ID should map to a corresponding class name; i.e.,
'theme'
→Theme
.Comment #8
sunMore than two people got seriously confused by the current issue title, so we need to fix that. "Theme initialization" is more or less the actual meat of the functionality in question here, so
s/system/initialization/
will hopefully cut it ;-)In addition, I'm temporarily incorporating #7 into the issue summary, because it refers to an ambiguous "Theme" class/service/domain-object in some places... This change is only meant to clarify the proposal, not meant to be a final.
Comment #9
sun@larowlan just mentioned (once again) that the bootstrap kernelization patch touches major parts of the affected functionality.
Comment #10
sunAdded a note to Notes section in order to cover a detail that came up in the discussion:
Comment #11
dawehnerThis property is just a place to store the value, there is no other usage of it in core.
Comment #12
sunStatus update:
@dawehner started to hack on this — the initial prototype looks fantastic & pretty damn cool to me:
http://drupalcode.org/sandbox/sun/1255586.git/commitdiff/theme-init-base...
Very minor difference to the issue summary, just pointing it out for clarity:
While the service ID is
'theme'
, its class isThemeManager
. Not sure whether that (re)name was intentional and/or a good idea, but that's most definitely the least of our problems and to be ignored for now.Comment #13
dawehnerWe both agreed that we should not expose the underlying separate theme services, (init/negotiation etc.) so using 'theme' is probably a good idea in the first place. As a class name, though I really disliked 'theme' so I came up with 'ThemeManager' but yeah I also dislike that name.
One thing I really not sure yet are the following conglomerate of functions:
part of that logic is already in Initialization::buildActiveTheme()
Sadly there are quite some calls to listInfo() which require the parsed information, so I am not sure whether replacing that with a call to the Init service is a good idea (actually I think it is not a good idea).
and the init service, for the full initialized objects
Comment #14
dawehnerComment #15
dawehnerComment #16
dawehnerThis is actually no meta anymore.
Comment #17
dawehnerFixed mostly all of the failures, some high level review would be cool.
Comment #19
tstoecklerWent through this patch. Surprisingly most of the changes are very intuitive to understand even though I'm not exactly an expert on the current theme initialization system. Great work! It's quite awesome to be able to remove all these globals. I especially like that the
ThemeManager
stuff is its own class and not stuffed intoThemeHandler
like it is done withModuleHandler
. I do have to say however thatDrupal\Core\Extension\ThemeHandler
vs.Drupal\Core\Theme\ThemeManager
isvery
confusing - both the name and the namespace.I can sort of see the reasoning for the namespaces as
Drupal\Core\Extension
seems to be more about (un)installation-related stuff, but still. This is also probably the reason whyActiveTheme
does not extendExtension
. It's still kind of strange, though. Maybe as a follow-up we can have aThemeInterface
and thenTheme extends Extension implements ThemeInterface
andActiveTheme implements ThemeInterface
or something like that.Also the issue summary needs a slight update. It's pretty up-to date. Some class names are incorrect and also I'm not sure about the
Negotiator
stuff as that is not really in the patch, unless I missed it.Detailed review:
Don't really have a better suggestion as I'm reading this top to bottom but noting that a service named
theme
with a class namedThemeManager
that gets a thing calledtheme_handler
injected is not really self-documenting on who does what.Also it's kind of weird that
ThemeHandler
is inDrupal\Core\Extension
andDrupal\Core\Theme
.Wow, this looks exciting! :-)
(Although again the discrepancy between moduleHandler and theme is a bit strange.)
Super minor but it's sort of non-standard to start a function with a newline.
Didn't dig into this code so this comment might be moot, but AFAIK it is possible for install profiles to specify alternate installation themes. Does this code conflict with this?
Wow, I never knew these two globals are identical. Good thing you're removing all that crap!
Is there a reason the system_list cache cannot be cleared by
$this->systemListReset()
? Or at least by some method onSystemListing
itself?an -> a
on -> at
That said "Defines a theme" is not very clear IMO. However,
Extension
has the same comment, so I suppose it's fine for now.Lot's of docs missing in this class not noting each case here.
I assume that's supposed to be "The path to the theme."
What is a parent theme? I've never heard of that before.
Also I think "The parent theme, if there is one." is sufficient as documentation.
Super minor but maybe drop the "Contains"
It seems strange that I can have a theme without an engine. Why is that not required?
This presumably should be
isset($values['owner'])
In which case is
$this->config->get()
not sufficient? Maybe we should move the default theme tocore.extension.yml
?Again, there's lots of docs missing here.
I might be old school but to me a class name of
Initialization
is not really self-explaining, to be honest. (Yes, I have heard of namespaces... :-)) Feel free to ignore.?
Again, maybe remove the newline there.
I see now that this is just being copied, but nevertheless it's rather strange.
I'm mostly just documenting my thought process here, do not feel obligated to change any of this. (Although I will buy you cookies if you do! :-))
This documentation is not very clear to me. I do not understand how a theme can be "meant to be derivative of another theme". I would have thought either it has a base theme (or multiple) or it doesn't, end of story. Can you elaborate and possibly revise the documentation?
I'm sure that there's a reason for it, but I wanted to note that the fact that
ActiveTheme
does not extendExtension
is rather confusing. Especially as most of this code basically just "copies" values from one data structure to the other.I see this method being called quite a bunch above. Is there no value in adding some sort of static caching here? drupal_theme_initialize() had that in the form of checking whether
$GLOABLS['theme']
was set already. That might be the wrong comparison, though.Missing docs.
I don't think we do "(optional)" for @var documentation. Maybe just append a ", if given." to the end (or leave it out entirely).
Super minor but I think the other way around would be more intuitive?
Ahh, so this is that parent thing. So why would I specify parent instead of specifying the base themes directly? Unless I missed it
Initialization::getActiveTheme()
never uses parent either.Can you explain this change? I do not understand it at all.
Again, there's some docs missing here.
Yay!
Ahh so scratch the comment about the static cache above. The code is in fact calling this
getActiveTheme()
method not theInitialization
one. Makes sense. Although the duplicated method names are slightly confusing. Don't have any suggestions, though.Weird indentation here.
Could remove the newlines here.
I can't say that I really understand this test, but is there a specific reason for removing it?
Comment #20
dawehnerThank you for your really detailed review!
Maybe ActiveThemeManager (theme.active) would be a better name? In this case moving the creation process of these objects should be moved away from the initializer ...
I do totally agree. A good name could be similar to the suggestions before: \Drupal::activeTheme() maybe?
http://cgit.drupalcode.org/commerce_kickstart/tree/commerce_kickstart.in... shows an example how a profile does it. Given
that, I guess drupal_maintenance_theme() has to be converted to use the new machinery properly. A new theme negotiator for the maintenance mode would be introduced.
Interesting ... actually the same step is done by systemListReset()->system_list_reset() already. Good catch!
Oh right, we should call it base instead.
Yeah, this was more a leftover while writing stuff.
Yeah these are workarounds at the moment to get a proper working installer. Having a separate maintenance negotiator hopefully
resolves that situation
Would you go directly with ThemeInitialization? I would be fine with that.
Maybe now this is no longer the case, but at least in 4.7 there is chameleon which just has a .theme file with theme_page and whatnot.
All that code after line 80 is indeed just for themes without a theme engine.
No idea, tbh!
Totally. To be honest I can't imagine a usecase for that but probably someone out there misuses that feature somehow.
Rewrote the documentation to make it a little bit less verbose.
Our goal here was to decouple the concept of active themes from the concept of the discovery side. I can understand that an extension shares a lot with the active theme object, though an active theme has a couple of more information than just the extension. Therefere composition might be a good idea we could use instead copying all the information from one place to the other. Note: On runtime the information is cached in state anyway, so we don't have to worry about it.
Yeah, as written before, maintenance is the killer. On a normal drupal request though, nothing beside the theme manager will ever call it.
I do totall agree here.
You are totally right here, added a todo for now, working on it.
This change is a bit tricky. When the active theme is determined each theme negotiator is asked. If that one returns a theme, the access
checker is taken into account, which ensures that the theme is enabled. (HEAD). Previously this system was bypassed completly in the installer, (drupal_theme_maintenance()), so the global got set and what not. On install time no theme is actually enabled, so the status checks now always fails. If no config was installed yet, there is a workaround in ThemeHandler::refreshInfo()
So once the config is in place, ThemeHandler::rebuildThemeData() will set the status, so the result is basically the same. I guess
one "workaround" would be the follwing: add seven and stark to core.config.yml. Allow install profiles specify a theme in their .info.yml file, though then there is no way yet, to store the config, as there is no DB, neither the file system ready yet. Any ideas?
Well, this tests generic alter functionality (so we need to explicit call the theme alter now) (as in other places) but yeah I am fine with adding it back in.
Comment #22
dawehnerThe seems indeed mostly a random failure. Let's fix the schema issue.
Comment #24
dawehnerComment #26
neclimdulSort of a distracted skim of the patch.
Eww, I agree with this @todo. If for no other reason, how do I know where this is being called from?
Seems this deserves the same @todo as add_css.
what?
Not seeing where this is used.
yes. yes. yes.
Wow this is weird...
Had a comment but apparently this is a workaround. Should we make that clear for review/ongoing work on this patch?
empty? why?
I'd like to say no but really this is probably a yes.
Comment #27
dawehnerFixed the failures ... again :p
Agreed.
This is how I debug :p
Good catch!
Would you just use normal parameters in the constructor or pass along the extension object?
Added ...
Removed it for now. Maybe we want to not lazy-build the active theme objects, but I guess it is fine to build them over time, as we do now.
Comment #28
dawehnerJust realized that this could be quite some performance improvement for REST and other places where you don't have to initialize the theme system.
Comment #29
Wim Leers#28: Indeed! This can really help make non-HTML responses quite a bit faster.
Comment #30
dawehnerComment #31
moshe weitzman CreditAttribution: moshe weitzman commentedI read through the patch and its a superb cleanup.
Comment #32
dawehnerThere is a related issue for this already: #2235901: Remove custom theme settings from *.info.yml
While I do agree that filling follow ups is a good idea here is a fundamental flaw in our process. By requiring followups or removing todos
we reduce the honesty of people. They will not write down each todo they could think of, but rather just go with some big problems.
Worse than technical dept is hidden technical dept.
Here are a list of followups:
Comment #35
moshe weitzman CreditAttribution: moshe weitzman commentedThanks for opening those issues. IMO we are RTBC here.
Comment #36
alexpottNeeds a reroll
Comment #37
dawehnerRerolled
Comment #42
dawehnerCan someone explain why we have so many "random" test failures recently? I saw similar behaviour on the stack issue.
Comment #43
alexpottThis seems inconsistent - modules have a handler, themes have a handler and manager. For modules the handler fires hooks and for themes its the manager. I get why this is since the alter only get's fired for the active theme - which is something the ThemeManager knows about but not the ThemeHandler. As @dawehner said in IRC - perhaps we need to refactor ModuleHandler. :(
drupal_theme_initialize()
andpath_to_theme()
left inDrupal\Core\Theme\Registry
Drupal\Core\Theme\Registry
no longer needs to useDrupal\Core\Extension\Extension
theme.inc
no longer needs to useDrupal\Core\Routing\RouteMatch
Can we get an issue opened for this and referenced in the @todo
Can we get an issue for this?
Followup issue? Or is this not a problem anymore?
Missing new @param doc
Perhaps we could have a @see to the ThemeManager interface or something about the theme manager service so people know where such an object will be created and can be obtained from
Needs docblock - and what is it? $owner and path theme engine file? Confusing.
Needs followup issue.
Needs a followup issue.
@param's need description
Missing new line between namespace and use.
Not used and does not exist :)
Not used.
Missing docblock
Missing docblock
Missing param docs - I don;t think we should rely on the @see.
Why are we removing this test? If we need to then we should also remove the route and the method from
ThemeTestController
EDIT: fixed html
Comment #44
dawehner#2324055: Split up the module manager into runtime information and extension information.
Good catches!
See comment 32:
Added a reference to both relevant places.
Well sure, but nothing new for this patch. The owner is the theme engine, in case the theme is a root base path.
Well, sure.
Well yeah, this test checks generic alter functionality.
There are other places which still tests alterting though.
Comment #45
tim.plunkettI think those changes address @alexpott's concerns.
Comment #46
alexpottYep looks great but we need a CR for this.
Comment #47
dawehnerGood point!
Splitted it up into the two change records, as they deal with two different problem spaces:
Comment #49
dawehnerReroll.
Comment #50
moshe weitzman CreditAttribution: moshe weitzman commentedBack to RTBC.
Comment #51
alexpottCommitted a4125e6 and pushed to 8.0.x. Thanks!
Fixed on commit.
Comment #53
dawehnerThank you so much alex!!!
Comment #54
sunAwesome work, @dawehner!
Thank you for bringing this home. Sorry for not reviewing it earlier. The committed code still appears to fully match the architecture that we originally discussed (to great lengths ;)) though — very glad to see a proper plan to pan out in practice. :-)
That said, the following two changes are highly problematic, since each of them is priming theme configuration ahead of time, which doesn't actually exist yet, and unfortunately worse, they conflict with actual expectations of the installer/installation profile as well as tests:
The installer is supposed to use the
'distribution:install:theme'
property of the installation profile's.info.yml
file, if defined, otherwise fall back to 'seven'.Kernel tests are supposed to be executed against "no theme at all" by default; i.e., whatever gets rendered is exclusively rendered through the unmodified, original theme functions/templates of modules only. The primary purpose of asserting output in kernel tests is to assert the original output of a module (sans any kind of theme interaction).
These two aspects present a critical problem, since priming the configuration ahead of time will cause all new code to be based on the expectation that the configuration magically exists already. Therefore, we need to fix that ASAP.
Created #2325575: Theme must not be primed in core.extension and KernelTestBase
Comment #55
sunThe issue summary mentions an additional clean-up that didn't happen as part of this patch:
Follow-up for that: #2325689: Clean up temporary Extension class workarounds
Most likely, many further API clean-ups are possible and in order after this excellent refactoring — please link to them here.
Comment #56
Gábor HojtsyThis most probably caused the critical at #2331991: Theme missing when installing in non-English language in which Drupal is *very* confused if there is even an active theme. The active theme is only initialised partially (I have a half-baked patch there which makes it 2/3rds baked :P), but then no twig templates are used from the theme either.