Problem/Motivation
In order to support alternative stream wrappers like S3 and maintain performance, contributed modules often have to reimplement significant parts of the image generation process.
For example :
- with S3 we want to first serve an image locally, and then upload it in the background to mask the several seconds it takes to upload an image.
- This significantly simplifies the implementation for the Flysystem module, and helps avoid bugs in image token and request validation.
- with mediaContextualCrop, we want to create a new image delivering methode based on 80% of the ImageStyleDownloadController
The deliver method is write as a bloc of 130 lines, and contain some elements like validation (scheme, token, image source ... ) that throw exceptions. This elements can be encapsulated in protected method in order to be more easily be reused in child classes.
Steps to reproduce
Try to create an alternative ImageDeliver controller based on the ImageStyleDownloadController.
Proposed resolution
This patch takes the bare minimum approach, and refactors deliver() into several protected methods.
A more complete approach would be to refactor most of the new methods into a separate class that can be unit tested, decoupling generation from a Request object. It looks like that's underway over at #2359443: Allow creating image derivatives from an Image object, though that patch doesn't address the issues with the deliver() method.
Remaining tasks
NA
User interface changes
NA
Introduced terminology
NA
API changes
NA
Data model changes
NA
Release notes snippet
NA
Issue fork drupal-2685905
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
deviantintegral commentedComment #3
deviantintegral commentedComment #4
twang commentedBreak the original method into several methods makes the code much easier to read!
Does this long line ok with phpcs rules?
Feel like the response better include the same msg logged above, rather than just saying "Error generating image".
Comment #5
deviantintegral commentedIt does! The use statement is actually used in an @var declaration.
What would you suggest for the error message? I didn't change the text in this patch. The image style only returns TRUE or FALSE based on if the image could be generated or not, without any additional context. It'd be nice to fix this, but I think that's probably better done at #2359443: Allow creating image derivatives from an Image object?
Comment #6
mondrakeLooks good to me, contrib will be able to extend the class and change only some of the methods instead of rewriting the entire deliver() method.
#4.3 changing the existing error string would be out of scope here.
Comment #7
mondrakeSecond thinking :)
Actually since you mention #2359443: Allow creating image derivatives from an Image object, in order for this to work better with it later, I would suggest
pass $image as a parameter to send() instead of $derivative_uri
return an Image object instead of a bool - the image object of the generated derivative image, that can then be passed to send() - or FALSE/NULL in case of failure.
Comment #8
deviantintegral commentedGood points. I've addressed them in this patch, and changed generate() from returning booleans to throwing an exception.
Comment #9
mondrakeLooks good to me. It's purely refactoring within the ImageStyleDownloadController class - with introduction of some protected methods. Not sure whether it would need additional tests for that - let's bring to the right eyes and see feedback :)
RTBC
Comment #10
alexpottLet's add a specific exception for this and we need an @throws for this.
Comment #11
twistor commentedComment #12
dawehnerNice refactoring in general!
Is there a reason we catch any kind of exception and not just the more specific one like in the snippet above?
Comment #13
twistor commentedBecause I forgot to change it?
Comment #15
twistor commentedEntirely wrong patch. Time for bed.
Comment #16
mondrake#10 has been addressed. RTBC again.
Comment #17
mondrakeComment #18
claudiu.cristeaNice cleanup!
Few nits:
Lets' use modern square brackets syntax for arrays.
In such cases, by using double quotes, you can simply inject $derivative_uri into string and avoid any function call like spintf().
I would prefer to move
$this->config('image.settings')->get('allow_insecure_derivatives')in variable just forif (...)readability.Comment #33
mstrelan commentedThis is nice, but not help when more than one module needs to override the controller, e.g. stage_file_proxy and webp and avif. I'm wondering if it would be possible to have a service that can be decorated by multiple modules instead? See https://symfony.com/doc/current/service_container/service_decoration.html for details on service decoration in symfony. Then the ::deliver method can just call a method on the service and each module can decorate it with it's own behaviour.
Comment #34
drdam commentedSome update here and a "redo" of the patch for Drupal 11
Comment #35
drdam commented[replace patch by a cleaner one]
Comment #36
drdam commentedComment #37
nicxvan commentedCan you please put that code on a merge request? Tests don't run against patches anymore.
Comment #40
drdam commentedMerge Request available
Comment #41
smustgrave commentedThanks for converting to an MR.
Noticed that the summary appears incomplete, would recommend using the standard issue template.
Also will need test coverage for the new functionality
Thanks
Comment #42
drdam commentedComment #43
drdam commentedFor test coverage, I don't what it need to be tested, it just code management, no new functionnality.
Comment #44
nikolay shapovalov commentedThanks for MR.
Do you think we can add interface for these new methods?
Comment #45
oily commentedIn the issue summary it states
What does that mean? With (in the case of) the S3 contributed module? Or with the S3 stream wrapper?
Then
Who is 'we'? The reporter's company? Drupal.org?
How does 'we' 'want to first serve an image locally'? By using or creating a contributed module? Or a custom module?
I dont know the answers to these questions. If someone would like to update the issue summary or I will if I can get answers to these questions.
Comment #46
drdam commented@nikolay shapovalov :
"Do you think we can add interface for these new methods?"
I don't know ...
The purpose of the RM is simply to separate the processing/generation of the derivatives from the various validation/authorisation operations linked to this generation.
@oily : I don't know either what @deviantintegral want to say, when it create the issue.
I just done my best when @smustgrave ask me to update summary to the "good template"
I have my use case "with mediaContextualCrop, we want to create a new image delivering methodebased on 80% of the ImageStyleDownloadController" for the others I don't know their motivations.
Comment #47
smustgrave commentedThe test coverage could be following the steps to reproduce.
Will slightly agree with @nikolay shapovalov feels like an interface now.
Comment #48
drdam commentedI just add the interface, but I have a doubt about making all methods public ... if some one can check
Comment #50
_tarik_ commentedI've faced issues related to the namespace and the missing return type of the isSchemePublic method.
The .patch file is attached for the people who don't want to add thousands of commits to their patch section inside composer.json.
@drdam:
"I just add the interface, but I have a doubt about making all methods public ... if some one can check"
I don't think that we need to put into the interface methods that aren't planned for external usage. So, I believe we should describe only the method "deliver" as the one that could be executed externally(I might miss something because I spent a little time checking the code.)
Comment #51
drdam commentedComment #52
smustgrave commentedStill appears to be missing test coverage. May need a CR for the new interface too, I should of mentioned before that's on me.
Comment #56
drdam commentedHaving reviewed the module’s unit tests, I get the impression that none of the existing tests cover the module’s other public methods (sourceImageExists and getUriWithoutConvertedExtension).
Could you explain exactly why the refactoring of the code in this issue requires specific tests, whereas other refactorings do not?
Please understand that my point is not that "I don’t want to" write these tests, just that I don’t understand "what to test".
Comment #57
drdam commentedComment #58
heddnI was asked to weigh in on the architecture here. What I like about this is it makes things a little easier to extend/replace. But I'm not satisfied. Ideally, we would have a way for more than one extension to inject itself here. Either through decoration or maybe through event subscribers.
My long term plan (hope?) is that for responsive images we do things differently. I know this is specifically intended for image.module and its image_styles. And my hopes are a long term goal. See #3372932: [Meta] High-performance images (nearly) out of the box. Said goal would need a new dedicated route for responsive images. Anything we can do here and elsewhere that takes baby steps in that direction are good.
This only picks up half the equation. We're only handling images, not all general files. The controller here extends the file controller. For some of the alternate storage options mentioned in the IS, that more properly exists in the upstream controller. Not here.
Before we keep moving on this, a good IS update on _why_ we are doing what we are doing here and how the proposed solution would solve that problem would help convince me the approach here is good/bad. Examples are there already but if we could clarify which of them make sense to support images vs all files, that would be good.
Comment #59
smustgrave commentedThanks @heddn for taking a look
Comment #60
fathershawnYesterday's security patch broke this MR.
Comment #61
jrockowitz commentedRerolled for Drupal 10.6.13 after SA-CORE-2026-010.
The only code adjustment is in the extracted isSchemePublic() helper. The previous patch preserved the older token-based public/private decision. Drupal 10.6.13 changed ImageStyleDownloadController::deliver() to determine public access from the derivative scheme plus file_additional_public_schemes, so this reroll keeps that new logic inside the helper.
Patch and interdiff attached.
Assisted by Codex.
This patch was AI-generated, but I really had to push it to only make bare-minimum changes to #50.
Comment #62
mortona2k commentedPatch for 11.4.4, made with claude, from the MR.
I'm working on fixing this issue: https://www.drupal.org/project/media_contextual_crop/issues/3608131