Updated to #32.
Problem/Motivation
Some text filters, such as the media_filter from https://drupal.org/project/media need to be able to add both #attached assets and cache tags as well as just modifying a string.
Media filter allows the referencing of arbitrary media entities from a textarea, the render cache will need to be cleared for those as much as if it was an entity reference field.
Additionally, rendering an entity might result in #attached calls within the entity view triggered by the text filter, it's only possible for the string to get returned from that, not anything else at the moment.
Proposed resolution
- Add a
processed_text
element type. - Refactor
check_markup()
to use this type. - Add a
FilterProcessResult
class which is a simply setter-getter carrying the processed text, cache tags, attached and post render cache callbacks. I doubt we need more than the constructor taking these with everything but the processed text optional and then a__toString
and three getters.
See #16 for more details.
Remaining tasks
None.
User interface changes
None.
API changes
Filter callbacks will return FilterProcessResult
objects, which can optionally contain cache tags, assets to be attached and #post_render_cache
callbacks to be executed.
Comment | File | Size | Author |
---|---|---|---|
#78 | interdiff-2217877-78.txt | 3.52 KB | damiankloip |
#78 | 2217877-78.patch | 105.45 KB | damiankloip |
#74 | filter_assets_cache_tags_prc-2217877-74.patch | 108.63 KB | Wim Leers |
Comments
Comment #1
BerdirIt also affects the entity/field storage cache. See #597236: Add entity caching to core.
Comment #2
Wim LeersComment #3
Wim LeersComment #4
catchThis is almost certainly an API change (unless we do a mixed return value or something to keep bc). Bumping priority. I'd put in an API change for this after beta since there's a relatively limited number of text filters out there and it'll be a simple update for the ones that don't need to use the stuff added here.
Comment #5
Wim LeersOn this.
This is a MUST HAVE for things like media filters, or any filter that references any entity. And we actually have at least one example of that in core: any image that is inserted via CKEditor is registered by editor.module's
hook_entity_(insert|update|delete)
implementations, but that doesn't allow us to set a corresponding cache tag; this would enable that.Comment #6
Dave ReidComment #7
Dave ReidYes this is very, *very* much needed. I started to look at this solution before I saw this issue and got very discourage since the use of check_markup() is often so low-level that a render API isn't even close to the object. For example: https://api.drupal.org/api/drupal/core%21modules%21text%21lib%21Drupal%2... is deep inside TypedData.
Comment #8
Dave ReidWe also need to be able to attach #post_render_cache, not just #attached.
Comment #9
Dave ReidThis is also complicated by the fact that text fields perform check_markup() on field load, rather than when the field is viewed/rendered.
Comment #10
catchNow that we have render caching for entities, we could skip that optimization I think. It was only done for performance - i.e. to avoid 30 check_markup() cache hits when there were thirty entities rendered. But since we now cache the entire HTML, we'd not be making those cache hits all the time.
Comment #11
BerdirMaybe, but we can only do that once the #pre_render issue landed, we still call it right now..
Also, that brings up an interesting question that I'm pretty sure we're ignoring right now? Text filters can control if they can be cached or not, and if they can't, then that needs to bubble up somehow as well and disable the render cache?
Comment #12
Dave ReidIf filters can use post_render_cache, then I forsee no more need for filter having disabled caching. All the cases I could think about wouldn't need it anymore.
Comment #13
BerdirHm, not sure, there is for example the complexity of multiple filters that run in a given order and if one of them doesn't support caching, then a filter that runs later might change something so that a post render callback no longer works? I doubt that we can just drop the logic that we have there right now...
What might work is that we move the complete check markup processing to a post render callback automatically if one of them says it's not cacheable?
Comment #14
catchI'd be fine with removing the ability to have uncacheable text filters, even if we lose a feature somehow.
#836000: Media filter should not prevent the core text format cache from being used is a classic example of a non-cacheable filter - it was disabled caching precisely because of the lack of cache tags and #attached support.
I can think of one (not contributed) filter that can and should be refactored to use #post_render_cache if it gets ported to 8.x
Something like the PHP filter may or not need to disable caching depending on what the PHP is, in that case you still have the option of completely disabling render caching altogether, or #2099137: Entity/field access and node grants not taken into account with core cache contexts to increase granularity. So the option isn't taken away entirely if someone genuinely has a use case that absolutely cannot be solved any other way.
Comment #15
sunJust some closely related prior art.
Comment #16
Wim Leers#15: that's super helpful, thank you!
Comment #17
Wim LeersHad to get other stuff done first for a while, now actively working on this!
Comment #18
Wim LeersIn this first patch:
check_markup()
,FilterInterface::preprocess()
andFilterInterface::process()
.$langcode
on those 3 function signatures, because that exists not only for caching purposes — see #319788: Pass language information to filters.interdiff-1.txt
.#post_render_cache
support anyway, which allows you to be as granular as you like with uncacheable things, instead of preventing the entire processed text from being cached, and by extension containing entity and even the page.filter_format_allowcache()
, which had no more use.interdiff-2.txt
.check_markup()
upon loading of fields.interdiff-3.txt
.interdiff-rest.txt
.filter_caption
.data-caption
attribute on an image, the associated CSS should be loaded; this is currently done on all pages, usinghook_page_build()
.EditorImageDialog
), then adata-editor-file-uuid
attribute is set on the<img />
tag. This is how the Text Editor module is capable of correctly tracking in which nodes a file is used without that file being referenced via a file field, but being referenced through a processed text field instead.EditorFileReference
filter, along with test coverage.'#type' => 'processed_text'
,FilterInterface::process()
can return either a string or a render array,check_markup()
is a wrapper for the simple cases.check_markup()
is moved tofilter_pre_render_text
, which is the#pre_render
callback for the new'#type' => 'processed_text'
. The "native environment" for a piece of processed text is in a Drupal render array, which can have#attached
assets,#cache
tags and#post_render_cache
callbacks. The "atypical environment" for a piece of processed text is in tokens, e-mails, and so on, where cache tags, assets and#post_render_cache
callbacks don't make sense. In this case, we just define a code>'#type' => 'processed_text' element, calldrupal_render()
on it and return that result, and voila: we have the same "string only" output that we have today.Comes with test coverage at
FilterAPITest::testProcessedTextElement()
; allcheck_markup()
tests continue to exist, this ensures we didn't regress.#attached
,#cache
tags and#post_render_cache
callbacks may now return a render array, with the filtered text in#markup
. Filters that don't need this can continue to work as today.check_markup()
from 7 to 4 (converted use cases: user signature on comments,TextDefaultFormatter
,\Drupal\views\Plugin\views\area\Text
; remaining use cases:CKEditorPlugin\Internal
,EditorController:getUntransformedText()
,TextProcessed
,Drupal\views\Plugin\views\field\Markup
and tests). The remaining use cases are valid use cases ofcheck_markup()
(i.e. only string of markup is needed), with the exception of theMarkup
views field, which I unfortunately was unable to figure out where to use or configure, so I couldn't convert it.UserSignatureTest
updated to verify the cache tag of the text format is bubbled up.TextFormatterTest
written (we had zero test coverage for the text formatters!), this ensures we didn't regress.Whew. Hope that helps.
We could potentially do points 1, 2 and 3 in separate issues each. Hence the separate interdiffs for those, to make that splitting up easier.
Assigning to catch for a +1 or -1 on the general approach, since he opened the issue.
I think the biggest WTF is going to be this snippet, which occurs several time in the patch:
See #2099131-177: Use #pre_render pattern for entity render caching in particular. In a nutshell: in the case of the "processed text" element type, we need to do the text processing in some sort of preprocessing stage, for which
#pre_render
is the only fit. However, because of this, cache tags are only added during the#pre_render
stage. And usually, processed text is inside a field, which is inside an entity, which is "themed". A "themed" entity has a corresponding Twig template, which operates on a copy of the renderable array and callsrender()
whenever it wants to render a subtree of a renderable array into a Twig template. Which means that without the above hack, we unfortunately invoke the#pre_render
callback of "processed text" elements on a copy of the render array, and hence set cache tags on that copy of the render array, hence the cache tags don't bubble up. The only solution for now is to calldrupal_render()
(to invoke all#pre_render
callbacks) on the element,render()
will detect that the element was already rendered and will just return the final markup to the Twig template.This is very ugly, and solving this is very much out of scope.
Comment #20
BerdirMy arguments for not touching prepareCache() here were that it a) It makes the patch bigger as there are a bunch of not really related changes, removed/changed tests and so on. And b) apart from text fields, the only other use case for PrepareCacheInterface in core are date fields, and the same argument as we have here (it is no longer needed because it's supposed to make the formatter faster, which is now cached), so we could remove that too and possibly even remove the whole feature.
And that part certainly needs it's own issue, so we could just as well leave the text item stuff to be removed there as well :) but looks like you already removed it, so we can probably remove that here and discuss it further in a follow-up...
Didn't look at the patch yet, but what about #13? If filters themself need to add post_render_cache callbacks, it's impossible to control the filter order?
Comment #21
catchI didn't review the patch in depth, but the plan looks good.
@berdir the only filters I've ever come across that needed to prevent caching were either doing a (bad) workaround for the problems this issue fixes, or were the PHP filter where anything goes anyway. Do you have a concrete use case that is covered now that wouldn't be covered by this patch? I can see that there's a small regression, I just can't see a case where it would matter in practice.
I think it would make sense to remove the prepareCache changes out (and do them for date formats at the same time) if we can. If for some reason the issue blocks this one we could bump that to critical if we want.
I worked on the original patch that added the check_markup() in hook_field_load() and it was purely for optimization of content listings with lots of text fields (i.e. comments) so that they wouldn't do an extra round trip to the cache for each call to check_markup(). This is obsolete with render caching. Same for the date formats.
Comment #22
Wim LeersFixed the test failures in #18 (see
interdiff-fails.txt
). In doing so, I found a few more remnants that should've been deleted (seeinterdiff-remnants.txt
).Only one test fail in #18 was tricky/interesting: that in
EntityReferenceFormatterTest
. It's tricky because we're rendering an entity (rendered in a#pre_render callback
) that has an entity reference formatter that renders the entire referenced entity (also rendered in a#pre_render callback
), which contains amongst others a processed text field (also rendered in a#pre_render callback
). The#20:
PrepareCacheInterface
further :)#post_render_cache
callback execution order. It's true, this is impossible.#post_render_cache
is designed for this exact purpose: in the final HTML, find placeholders and replace them. This means that if you previously had an uncacheable filter that for example generated a<img src="llama.jpg" data-caption="here is a funky dynamic, user-customized llama" />
, thatfilter_caption
would still be able to convert that into an actual captioned image. With this, that's no longer possible.But I think that's only natural — if you need to retain that capability (which I'd question was ever used — counterevidence is welcome of course), then the filter system would be fundamentally incompatible with the render cache, and hence a single filter can destroy the performance of any Drupal site. I think that therefore that is an undesirable feature.
Comment #23
BerdirWell, it might not be *that* simple, because I guess it's now questionable if we even want to keep the ->processed computed property, it's only used in very few places (mostly token implementations and tests). encourages use of check_markup(), which I think is discouraged with this (?) and is a performance overhead (computed field properties are always created). And if we do, we could at least simplify it and remove the complexity that was added to support setting the cached value. But I can we can discuss that part also in the follow-up.
Comment #24
catchUnassigning myself.
Comment #25
Dave ReidThis is really great and wonderful job Wim implementing this! It made total sense to me. I took a stab at implementing this for the entity_embed module protoype: https://github.com/drupal-media/entity_embed/pull/6/files
The only odd thing I found is that when using post_render_cache placeholder HTML inside filtered text, because of the Html::normalize call in FilterHtmlCorrector::process(), the normal placeholder of
<drupal:render-cache-placeholder .. />
was converted to<render-cache-placeholder ... ></render-cache-placeholder>
, so I had to perform string replacement twice in my post_render_cache callback to make sure I was covering both placeholders. I had my entity_embed filter which ran *after* the HTML corrector filter, so I'm not sure what was up - and I think this is a test condition we're missing here.Comment #26
Wim Leers#25: Aha — great catch! I'll just rename the tag from
<drupal:render-cache-placeholder>
to just<drupal-render-cache-placeholder>
and that should fix it.FilterHtmlCorrector
usesDOMDocument
, which AFAIK doesn't support (XML) namespaces, which is probably the cause.Leaving at NR for further feedback.
Also provided feedback on https://github.com/drupal-media/entity_embed/pull/6/files :)
Comment #27
slashrsm CreditAttribution: slashrsm commentedReviewed and tested (with entity_embed PR that @DaveReid mentioned) this patch and it looks good to me. However, note that I'm not super experienced field expert so I may have missed something.
Comment #28
yched CreditAttribution: yched commentedDouble yay for the ability to remove the special handling of TextItem's 'processed' property and possibly the whole prepareCache() construct.
I agree with @Berdir that it would be best considered thoroughly in an issue of its own though. Although I never really liked it (or its D7 equivalent), the 'processed' property has the advantage of encapsulating the correct processing according to the field settings (either check_plain() or check_markup()). I guess that logic could move to a dedicated sanitize() method on TextItem though.
Comment #29
Dave Reid@Wim: Be also sure to note that Html::normalize also converts the single tag into an opening and closing tag.
Comment #30
BerdirAlso, see text_filter_format_update() and text_filter_format_disable() (not sure if that hook still exists with that name?!), that exists because we cache the processed texts in the field cache.
If that just works for the render cache, then we can remove the two hook implementations, see also FilterSecurityTest.
Comment #31
chx CreditAttribution: chx commentedAssigning to myself only to make sure it appears on my TODO list; I do not intend to patch this but I do intend to security review this. Someone must take care of Drupal 8 security and apparently noone else does.
Edit: to clarify, please continue developing/discussing etc but do not RTBC or commit this until I have approved it. I know this is harsh but something must be done. I do not like this but I do not really want to find security holes complete with the exploit PoC in the relevant change notice ever again either.
Comment #32
chx CreditAttribution: chx commentedYou see, the same pattern plays a role in the title XSS so I am superb anxious: you are separating assigning a template variable from the security guard of it. With the current code flow you have no way to know whether this is filtered already or some dimwit put a raw variable in there. Let me get back to this in a bit.
That's good, that was the first I thought of when I saw we can skip filters, but I'd word it like "All of the format's filters will be applied, except for filters of the types that are marked to be skipped. FilterInterface::TYPE_HTML_RESTRICTOR filters will always apply, they can not be skipped."
I would vastly simplify the code responsible for this:
You could save some continue and some array_diff both of which are prone to some really fun errors. array_diff seems really bad, as it compares on
(string) $elem1 === (string) $elem2
and I do not even want to think of the consequences later. Nah, let's not play this sort of roulette:Use a closure if you like that better. Separation of code because process needs it too.
In EntityReferenceFormatterTest
What happened here that it needs a full html filter format for testing? For posterity, this is the exact sort of question I'd like to see on issues. Even if you do not grasp the full code flow, this is an insecure text format entity so if it appears somewhere where previously it didn't exist then there is a chance something somewhere doesn't get filtered and that needs to be clarified at least, fixed at most. Maybe it's just because previously we didn't need the full blown entity in the test but ran individual filters or perhaps it is a quick shortcut in the test instead of listing tags -- whatever, it needs clarification.
filter_pre_render_text takes a #context which can contain langcode and ... and .... erm, nothing else. Why don't we use #langcode? If in the future more properties are neded, then we ... then we add more properties. What's the difference between #filter_types_to_skip and #langcode that #langcode needed to be stuffed inside a #context ?
Now the filter process can return a render array which is not rendered but processed by filter_pre_render_text. In the Middle Ages they were building cities where the streets were deliberately confusing to lead an invading army back to the walls. Are we trying to confuse the enemy here :P ? Why it doesn't return an object instead? Ie.
Or, you know, if you were to remove the (string) cast then we would have an excellent way to check for being filtered -- do instead
and then check in preprocess whether it's a FilterProcessResult and if not, then raise an exception, check_plain it, throw it out the window, whatever, just don't let it through. And when it actually becomes text then __toString will do the work automatically.
Comment #33
chx CreditAttribution: chx commentedI would appreciate a <h2>DO NOT COMMIT UNLESS CHX APPROVES<h2> in the issue summary but it'd be really obnoxious if I added that so I won't.
Comment #34
chx CreditAttribution: chx commentedComment #35
chx CreditAttribution: chx commentedComment #37
Dave ReidComment #38
Dave ReidI think the next steps are:
Comment #39
Wim Leers#23/#28/#30: Ok, removed the changes in
TextItemBase
. Opened a new issue for doing this: #2274517: Remove \Drupal\Core\Field\PrepareCacheInterface.#25/#26/#29: Changed
<drupal:render-cache-placeholder />
to<drupal-render-cache-placeholder></drupal-render-cache-placeholder>
(both tag name changed and closing tag added for compatibility withDOMDocument
and thereforeHtml::normalize()
).Test coverage updated to ensure that this works correctly and prevent regressions.
#31/#32:
We talked about this in chat, and chx' only concern is idiot-proofing Drupal here. Of course you shouldn't be rendering things in the theme layer. But unfortunately, that happens.
Yet another reason for #2060783: Remove the preprocess layer./
This is completely out of scope; this is not new code, this is just moving around of new code. Please open a new issue for this; this patch is very big as is.
The pre-existing test coverage was broken: the "Full HTML" text format was being used, but didn't exist! This issue merely uncovered that, and the code you cited is the fix.
Also, this does test not at all need Full HTML; it just needs a text format. It needs a text format to verify that the entity reference formatter bubbles up the cache tag of the text format of the body field of the referenced entity.
Because the langcode provides context to all invoked filter, but
filter_types_to_skip
is not at all related to the context and is not passed to filters. But, you're right, we could rename#langcode
to#context
at a later point if needed, this change is inappropriate.Changed to
#langcode
as you suggested.I don't think it's any more confusing or different than other APIs, but I agree that this is better. Added
FilterProcessResult
.#38:
filter_html
) must simply always run before any filter that adds#post_render_cache
callbacks (and render cache placeholders).Comment #41
chx CreditAttribution: chx commentedAbout the fails, we struggle with them in the twig autoescape (funny how things come together although this object suggestion predated the autoescape reboot) -- WebTestBase::buildXPathQuery needed
if (is_string($value) || $value instanceof SafeMarkup)
. In general, your enemies include json_encode not running __toString , we patched InsertCommand::__construct to read$this->html = (string) $html;
which I suspect will solve some of your fails too and PHP being stupid beyond imagination and not casting array indexes although I am not sure whether any check_markup'd code ends up in array index but if it does you need a manual string cast and also strpos $needle needs a string cast (I even wrote a phpwtf entry for that one).Comment #42
Wim LeersI talked to chx in chat, and we concluded that his suggested security hardening from #32 for
template_preprocess_comment()
can't work becausedrupal_render()
ensures that every instance of#markup
is a string (therefore it's impossible to do aif ($el['#markup'] instanceof FilterProcessResult)
check).Fortunately, his Twig autoescape work will already ensure this is safe.
Fixed all test failures. This should be good to go.
Comment #43
chx CreditAttribution: chx commentedShould be good.
Comment #44
Wim LeersComment #45
xjmPossibly messy reroll for #2247991: [May 27] Move all module code from …/lib/Drupal/… to …/src/… for PSR-4.
Comment #46
catchThe caching added here looks a bit out of scope?
We don't assign the result of drupal_render() to anything. Needs a comment if that's correct.
I'd expect this to be done in the filter that also generates the image tag etc., why a separate filter?
cahe.
Yay.
More yay.
This looks extremely close to HtmlFragment and CacheableInterface (see #2256373: Factor HtmlFragment out to an interface for example). Wondering why you went for a new class as opposed to returning an render array (then handling string/array return value) or extending one of those.
Comment #47
chx CreditAttribution: chx commentedOwie! The previous patch was a render array but it didn't go through drupal_render but a fixed list of properties was processed and so I recommended an object. It could've been a normal array instead of an object but an object is more D8-ish. A render array that doesn't get passed to drupal_render is just wrong.
Comment #48
Wim Leerscache_filter
cache was actually useful. This code gets executed on every single page that has a text editor. Sincecache_filter
is gone, we now do our own caching.FilterCaption
filter does both alignment and captioning, and is being split also, over at #2239413: Split FilterCaption into FilterAlign (for data-align) and FilterCaption (for data-caption).)@return string|array
strategy. But chx strongly opposed that:I honestly didn't consider
HtmlFragment
norCacheableInterface
because both of them allow both too much and too little: neither supports assets or#post_render_cache
callbacks, and both support cache keys, cache max-age, etc. We really only want three things: processed text (which can be considered HTML, indeed), cache tags and#post_render_cache
callbacks. Hence a new object.Comment #49
Dave ReidYeah I kind of agree with catch. Having a new class just for here seemed odd. If we can't repurpose HtmlFragment, then I'd probably prefer a render array instead.
The main problem I have with the existing FilterProcessResult is that I can't just *add* assets, cache tags or post_render_cache callbacks. If my filter needs to process more than one item, I have to run a NestedArray::mergeDeepArray every time I want to call setCacheTags or setPostRenderCacheCallbacks because it's such an all or nothing call.
Comment #50
Dave ReidTweaked changes to FilterProcessResult which added support for addCacheTags(), addAssets(), and addPostRenderCacheCallback(). Also adds regression protection for drupal_render_cache_generate_placeholder() to ensure it will not be modified by Html::normalize() in the future.
Comment #51
Dave ReidI think ultimately we want what FilterProcessResult to be is what is desired in #1843798: [meta] Refactor Render API to be OO, but that seems to be D9 material at this point.
Comment #52
Dave ReidComment #54
Dave ReidFixed the NestedArray issues. The other fails seem unrelated, so let's give this a try.
Comment #56
Wim LeersThe cause of those failures is
$all_file_cache_tags
not being an array of cache tags, but an array of arrays of cache tags :)Just one other minor change.
Comment #57
Wim LeersRTBC'ing to get feedback from catch, to see whether he likes Dave Reid's change, which addressed the only remaining concern.
Comment #58
catchI still don't like the class, but on the other hand if we change it again (i..e if CacheableInterface and friends were able to support what we need here) it's only going to affect the filter process API (which I'd be fine making changes to any time up until RC frankly).
Assigning to Alex for a second opinion.
Comment #59
catchAlso this is a bug.
Comment #60
chx CreditAttribution: chx commentedOn a somewhat related issue: check_markup must return an object after the twig autoescape patch; this object extending SafeMarkup is IMO no big deal.
Comment #62
Dave ReidRe-rolled for 8.x only.
Comment #64
Dave ReidFixes the regression in FilterTestPostRenderCache due to #2275679: Clean up render cache placeholder.
Comment #65
Wim LeersWas un-RTBC'd for rerolling against HEAD changes only, so back to RTBC.
Comment #66
alexpottWe need a change record.
Comment #67
alexpottDrupalUniteTestBase has been deprecated should KernelTestBase. This is very minor and if the point below is won't fix then we should ignore this.
Hmmm... a callable can be an array to call a method on a class. Does the post render cache system not support that?
Comment #68
Eric_A CreditAttribution: Eric_A commentedWhy the procedural wrapper?
EDIT: @Wim Leers, it's there since #56.
Comment #70
Dave Reidinterdiff is failing to generate for me, so:
- Fixed merge conflicts
- Clarified that the post_render_cache callback is a string callback, and not a callable.
- Use NestedArray::mergeDeep in FilterProcessResult:: addAssets()
- Fixed the processed_text element had default langcode of 'und' but check_markup() had a default langcode of an empty string. Used an empty string in both places now.
Comment #71
Wim LeersFixed #67.1.
#68: Because that's the standardized way of merging attached assets. Which means that if that way changes, this instance will also be updated automatically.
#70: ugh… if you fix the merge conflicts first next time, then you can actually generate an interdiff just fine. Now you forced us to re-read through this entire >100 K patch :( Manually verified, hence back to RTBC.
Comment #72
damiankloip CreditAttribution: damiankloip commentedIs this strictly true? I can understand we don't want to serialize objects here but you can call static methods etc.. in the form 'MyClass::method' or array('MyClass', 'method').
Should we cache this in the filter bin instead? Not sure why this needs to live in the default bin when filter has its own...
This dependency should also be injected but looks like ckeditor plugins don't do this at all right now. So could create a follow up for that.
This should use \Drupal::entityManager()->loadEntityByUuid() instead. Previous comment also applies here about follow up to inject deps.
Comment #74
Wim LeersThe sudden fail in #71 is due to #2183231: Make ContentEntityDatabaseStorage generate static database schemas for content entities getting committed between #70 and #71. Fixed in this reroll.
#72:
#post_render_cache
only accepts string callbacks — as you already indicate, a static method callable can be expressed in two different ways, and we only support one. Why? Because the callables are used as array keys, with the values being the different contexts in which they should be called.Change notice written: https://drupal.org/node/2278483
Comment #75
damiankloip CreditAttribution: damiankloip commentedThanks, looks good. So if you use something other than a string callable it is going to tell you anyway - cool.
My only pick would be that a constructor should always come ahead of a create() method.
Comment #76
Wim Leers#75: Okay, good :) Regarding that only nitpick: I ordered it in the same way as
NodeSearch
&UserSearch
, so let's not delay this over that.This can now go back to RTBC.
Comment #77
damiankloip CreditAttribution: damiankloip commentedThat would have taken a few seconds but ok.
Comment #78
damiankloip CreditAttribution: damiankloip commentedSorry, making that small change anyway.
Comment #79
Dave ReidStill confirming that #78 looks and works great!
Comment #80
alexpottCommitted d5be5c0 and pushed to 8.x. Thanks!
I can't think of a better name for the class either and at least it is called exactly what it is.
Comment #82
Wim LeersHurray! :)
Published the change record draft: https://drupal.org/node/2278483
Comment #83
Wim LeersI was able to close the following issues thanks to this one landing:
(Wow, that included two issues with node IDs below 1 million! Ancient issues!)
Comment #85
Gábor HojtsyFix media tag.