Problem/Motivation
Currently, when an Image object is instantiated by the factory, a one-to-one ImageToolkit object is instantiated too and associated to the Image. Most of the methods on the Image object just defer execution to their ImageToolkit counterpart. This works but it's a bit confusing, duplicates methods, and requires the ImageToolkit object to keep a pointer to its Image object, and vice versa.
Some discussions happened in the past with @fietserwin and @claudiu.cristea to 'unify' Image and ImageToolkit, but that was premature at that stage. Now the Image object is much slimmer and we can think about this again.
Further explanation of the idea
Looking outside Drupal (at other Image libraries):
- If GD would be rewritten in a OOP way, there would be a GdImage class that contains the resource and provides all the operations.
- Imagick is already 1 class (with a few helper classes for color, text and drawing) containing bot the image and the operations.
- Imagine offers 1 interface (ImageInterface) and various implementations (GdImage, ImagickImage, GMagickImage). ImageInterface defines "all" the operations like load, save, resize, rotate, etc. Thus also here: data and operations in the same object.
This suggests that sticking with 1 Image interface and an implementation of that per underlying toolkit is the normal way to go.
Looking at sound OO principles:
- Keep data and operations in 1 class.
- To much coupling between 2 components is a code smell. The 1-1 relation between Image and ImageToolkit is such a code smell.
- Proxy classes are a code smell: Image is quite empty, it forwards almost all calls to its strongly coupled toolkit brother.
Looking at what the objects actually represent:
- Image kind of represents an Image file. You can instantiate it passing in the name of an image file and you can save it. But you can also apply operations to it that change the image.
- ImageToolkit represents an existing image manipulation library and as such only defines the operations you can apply to an image canvas.
But an object that represent an image file is not needed in the image system. If you want to approach an image as a file, use a file object (issue #2257163: Restrict image system to image processing). Moreover, there is also an issue that want to enable us to start manipulating with a blank canvas, not an image file (#2063373: Cannot save image created from scratch).
But why is it the way it is?
Simply: historical reasons. In D7, the image system was not OOP, there was an image object and toolkit operations were defined based on a naming scheme. OOP'ing this system in D8 was done by simply defining classes around existing concepts, not refactoring or rearchitecting the system.
Proposed resolution
This issue/patch:
- Removes Drupal\Core\Image\Image altogether. Methods that do not just forward the call to the toolkit will be merged into ImageToolkitBase.
- Keeps Drupal\Core\Image\ImageInterface and Drupal\Core\ImageToolkit\ImageToolkitInterface, but deduplicates methods that appear in both.
- Makes toolkits implement both interfaces.
- Changes the image factory to deliver ImageToolkit plugin objects in place of the Image objects.
By limiting this patch to the above, the changes for the rest of the system are minimal. This keeps the patch readable. The patch in the follow up will be much larger, but will mainly consist of moving around and renaming.
Remaining tasks
- This issue: agree and commit.
- Follow up #2336811: Remove Image, we only need ImageToolkit:
- merge ImageInterface and ImageToolkitInterface into Imageinterface?this will mostly be a lot of moving around with renaming
- merge ImageFactory and ImageToolkitManager: by merging these 2 classes/interfaces, Image becomes a plugin and thus we should have a manager, no longer a factory.
- are the now final methods on ImageToolkitBase and their do... counterparts (pareFile -> doParseFile and save -> doSave) logical or can some further refactoring/integration be done there?
- restore the "value" behavior of Image objects. An image object should not be reused for different files or for creating different blank canvases. So there should be no methods to set the file or create a blank canvas, this should be part of the constructor.
- Follow up #2109459: Review image test suite: are the tests sets still logical. Currently, we have quite some duplication in the test, This patch will probably already reduce this, but further clean up remains needed.
User interface changes
None.
API changes
Removes a few methods:
ImageInterface:
ImageInterface::getToolkit()ImageInterface::getToolkitId()
ImageToolkitInterface ("internal", normally not used outside the image system, in fact this is mostly deduplication):
ImageToolkitInterface::setImage()ImageToolkitInterface::getImage()ImageToolkitInterface::isValid()(already in ImageInterface)ImageToolkitInterface::save()(already in ImageInterface)ImageToolkitInterface::parseFile()(moved to ImageInterface)ImageToolkitInterface::getHeight()(already in ImageInterface)ImageToolkitInterface::getWidth()(already in ImageInterface)ImageToolkitInterface::getMimeType()(already in ImageInterface)ImageToolkitInterface::apply()(already in ImageInterface)
| Comment | File | Size | Author |
|---|---|---|---|
| #16 | 2331481-no_image-16.patch | 49.2 KB | mondrake |
| #16 | interdiff_11-16.txt | 7.74 KB | mondrake |
| #11 | 2331481-no_image-11.patch | 46.77 KB | mondrake |
Comments
Comment #1
mondrakeJust to see the bot response... PHPUnit tests not covered.
Comment #2
mondrakeComment #4
mondrakeThe Image class is removed, so there's no purpose to keep a PHPUnit test for it... changed the existing test to GDToolkitTest which is what it is actually doing, and moved from core test directory to a directory of the system module, which is where the GD toolkit (currently) belongs.
Comment #5
mondrakeComment #7
mondrakeOh yeah
Comment #8
mondrakeGreen. Adding 'beta target' tag to stimulate discussion before beta...
Comment #9
mondrakeReroll after
#2096703: Image toolkits should use PluginFormInterface and ContainerFactoryPluginInterface and
#2260121: PHPUnit Tests namespace of modules is ambiguous with regular runtime namespace (+ Simpletest tests)
Comment #11
mondrakeRerolled + fixed tests.
Comment #12
fietserwin(As expected) I fully agree with this idea. I updated the summary with the original reasoning behind this idea and further follow-up work.
I will try to review shortly.
Comment #13
fietserwinuse Drupal\Core\Image\ImageInterface;
is no longer used
Does this have to be part of the interface? I cannot find any real usage, only test and upon creation? If it is needed for testing purposes, we should find another way as to not clutter the interface. (If it should be part of the interface there's a @todo.)
In the merging we lost the source parameter on the constructor. I propose to reintroduce that. In doing so we can remove this method. Note that we currently cannot change the source after an Image object has been created and, IMO, that should remain so.
Add doParsefile() as an abstract method to document what toolkit implementers should do. Same for doSave() (see e.g. ImageToolkitOperationBase).
I'm happy that a first merge can be done with a relative small sized patch. I have identified some follow-ups but those will not really change the idea behind this patch and as such are not necessary to do directly in this patch.
Comment #14
mondrake@fietserwin thanks
1. sorry I do not understand, can you elaborate
2. reintroducing the
$sourceparameter in the constructor of a plugin class would mean to reintroduce a tailor madecreateInstance()method on ImageToolkitManager, which we just got rid of in #2096703: Image toolkits should use PluginFormInterface and ContainerFactoryPluginInterface with the benefit of allowing injection of any service in a toolkit. So I'll leave it in the interface and let the factory make the call toparseFile()(similarly, we may have the factory make a call to acreateNew()method in #2063373: Cannot save image created from scratch on getting non-file based images). I'll fix the todo and reintroduce the checking thatparseFile()cannot be called twice.3. Sure
Comment #15
fietserwinad 1) there's a now no longer used use statement at the top of that file.
ad 2) Then name it setSource() or createFromSource() as that mathces better with createNew())
Comment #16
mondrakeAll done. Let's see if bot is happy.
Comment #17
tim.plunkettThis class used to be a value object. Whatever broken state it is in now was caused by the refactoring done since it was added.
Comment #18
fietserwinre #16:
i did a new review and, given the follow-ups, I find this patch acceptable and a very good step into the right direction, therefore: RTBC, and I will create the follow-ups shortly.
re #17:
I am not sure what class you are talking about when you refer to "this class":
- if it is Image: that indeed used to be some kind of value object (not fully though), but that class will be gone with this patch.
- If you mean ImageToolkit (i.e. an object implementing ImageToolkitInterface): IMO this has never been a value object, it was and is the "in memory representation of an image (~ canvas) including a hint to its origin/destination". As such it has never been a value object, as we should be able to apply image operations to it that change the image canvas, as well as that we can save to another destination than its origin (image derivative), thereby changing the file info as well (that is currently also already done that way in Image).
If you are not happy with the createFromSource() method: neither am I, but due to @mondrake's reasoning in #14.2, I think that is better addressed in the follow up that will merge the image factory and image toolkit manager.
Comment #19
fietserwinCreated follow-up: #2336811: Remove Image, we only need ImageToolkit.
Comment #20
alexpottWe need the issue summary to be updated with what the final patch is doing. And it'd be nice to have one CR that all the Image / ImageToolkit architecture issues link to.
Comment #21
tim.plunkettI meant the class you are removing. Drupal\Core\Image\Image. It is a value object that represents an image, and now it has been consumed by ImageToolkit, which now represents two things.
And now the follow-up merges two more distinct classes.
By the time this is all done, we will have monolithic multipurpose classes, with no separation of concerns.
Comment #22
fietserwinRE #21: Thanks for clarifying, but I can not fully agree with you.
IMO, Image is a mixture of 1) redundant file functionality and 2) a proxy to image toolkit functionality.
ad 1)
Image contains getFileSize, getMimeType and getSource(). To me this is functionality better handled by a file class. Looking at current usage, there is no place where an Image object is used as both a file and as an image canvas. To get these methods out of Image, I filed (a while ago) issue #2257163: Restrict image system to image processing.
ad 2) All other methods on Image are forwarded to the toolkit object. Most without any further processing. Some with some default processing that is assumed to be the same for all toolkits or is needed to keep its own state correct. The former can be handled by a base class implementation of that method or, as this patch does, via a template pattern. The latter disappears on merging.
The way this patch (and other issues) is heading to, is the way that the Imagine library handles different "toolkits". You have an Image interface defining methods to open, alter and save an image (file) and you have actual implementations in the form of a Gd\Imagine class or an Imagick\Imagine class, that alter the image using the given toolkit. This is simple and plain OO, data and operations in 1 object, common functionality in a base class, and in this case I don't see a need to make it more complicated than that.
Comment #23
fietserwinI updated the title and issue summary. I will update the issue summary of the follow up as well.
The "overall" change record is https://www.drupal.org/node/2084547. As this change record is already published, we prefer to update it directly after committing issues.
Comment #24
alexpottIn talking to @catch about this issue I ended up repeating some of the statements of the issue summary - if you want to treat an image as a file use the file object and I was wondering what Imagine does. So I went to look at Imagine and it does not really do this. If you look at documentation https://imagine.readthedocs.org/en/latest/ it is up to the app that uses image to decide what toolkit to use. The way in which Image and ImageToolkit are coupled using the plugin manager is how we do this in Drupal 8. So Image is a proxy - it is a proxy onto the chosen toolkit - I think this is okay.
Comment #25
mondrake#24 - if we do not want to do this, fair enough. The bigger picture would be in #2336811: Remove Image, we only need ImageToolkit. There we are in fact proposing to convert the current Image to be a plugin, and remove the entire concept of 'toolkit'. We do not need a middleman between an Image object and the image library/module, in our opinion: an ImageBase class would be extended by plugin classes and contain all the code and data needed to access image libraries/modules like GD, Imagemagick, etc.
If I understand well, this would also address @tim.plunkett concerns about the Image class having been eroded since its introduction, with many properties having been moved from Image to ImageToolkit -- there the specific image plugin class will have its own properties back.
Comment #26
alexpottBut the image is not a plugin - the toolkit is - the image is composed of both file and an instance of the toolkit plugin. But yep if we are going to do this now we need to do whatever we do in one step - halfway steps aren't practical now since they move us further away from release - so I'm closing this issue to continue discussions on #2336811: Remove Image, we only need ImageToolkit