We should provide a method of DI for entities in order to avoid \Drupal calls in classes. This is especially pertinent now that we have bundle classes in core, where the only alternative to entity-specific services is \Drupal calls on the bundle class.
Original:
Entities have never been injected with services, because it would not have kept them serializable, a feature required by the REST module. However, if we make all entities extend \Drupal\Core\DependencyInjection\DependencySerialization that was introduced by #2004282: Injected dependencies and serialization hell , services will no longer be a problem for serialization as only their IDs will be serialized.
Doing this will let us write cleaner production code, as we no longer need the wrappers that #2134857: PHPUnit test the entity base classes introduces for testing, and our tests will no longer have to mock those wrapper methods.
| Comment | File | Size | Author |
|---|---|---|---|
| #1 | drupal_2142515_1.patch | 734 bytes | xano |
Issue fork drupal-2142515
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 #1
xanoHere's a first patch that just does the extension.
Comment #2
berdirREST serialization has nothing to do with this/php serialization.
This will conflict with #2027795: Optimize content entity serialization. I'd rather duplicate those lines and add it to the sleep/wakeup methods added by the referenced than making the entity class hierarchy even more complicated.
Comment #3
yched commentedNote: "REST serialization" has no issues with services in the object. It's only PHP serialize() that's problematic.
+ I'm not sure DependencySerialization will be needed for entities:
#2027795: Optimize content entity serialization provides dedicated/optimized serialization for ContentEntities
#1977206: Default serialization of ConfigEntities does the same for ConfigEntities (a reroll would be welcome though...)
Comment #4
berdirWe'd still need something if we have injected services, because we need to get them back when we unserialize, though.
Comment #8
mile23Calling this a duplicate of #2208115: Add DependencySerializationTrait
Feel free to re-open if it's not. :-)
Comment #9
berdirIt's not. Entities still don't support dependency injection.
Comment #10
mile23OK.
Why should we inject services into entities?
The various layers of entity management classes have injection points, and can manipulate entities based on those services.
Shouldn't we expand on the repository pattern and inject there?
Comment #16
avpadernoComment #19
derekcresswell commentedComment #20
derekcresswell commentedThe EntityBase class uses the DependencySerialization trait now. I think the bigger point of contention is just where we want to inject services to these entities as we cannot use the symfony create function. Perhaps we can just add a function to the entity base class called 'InjectServices' or similar and it just takes in the container and allows the developer to pull in whichever services they want.
We would also need to change the entity base classes to remove functions that call
\Drupal::service()and convert entities that currently do not inject services as properties (might be big enough to be a separate issue).I think this is necessary to do for two main reasons. Consistency and performance. Consistency with the rest of Drupal DI, though the method here might be different the methodology can stay the same. And performance by avoiding excess calls to the
\Drupalclass.Comment #21
ambient.impact#20: many core classes already have
createInstance()static methods specifically for dependency injection - includingEntityHandlerInterface::createInstance()- so I would suggest that name for consistency, unless there's naming conflict I've missed.Comment #23
avpadernoComment #24
anybody+1!
Comment #25
johnpitcairn commented++1
Now that we have bundle classes available, and people start (enthusiastically) adopting those without any prior exposure to defining entity classes, it's a very WTF moment to discover that you can't inject dependencies into your shiny new bundle class...
Comment #26
alexverb commentedTyped entities is wrapping the entities to provide dependency injection on them:
https://www.drupal.org/project/typed_entity Can be used in conjunction with bundle classes.
Still it's a wrapper, I think it would be nice to be able to do it straight on the entity.
Comment #27
anybody+ in core, without further contrib module :)
Comment #29
mrweiner commented+1 -- started implementing some bundle classes and then realized that there's no DI available, so back to calling \Drupal in classes. :(
It's easy enough to add
createInstance()onEntityBase, but I'm not totally sure where it would need to actually be implemented. I'd imagine it would need to be implemented inEntityStorageBasewherever an entity is returned, since the save/create/load methods inEntityBaseall rely on$entity_type_manager->getStorageanyway.For instance
I'd imagine we'd just want to return an
$entity_class::createInstance()instead?Then there's
EntityStorageBase::load(), which eventually calls the abstractEntityStorageBase::doLoadMultiple(). Obviously since it's abstract, we'd need to add the handling somewhere else. Not sure where that would be. Same withdoSave().Comment #30
mrweiner commentedDigging a bit more, it certainly seems more complex. For instance,
Drupal\Core\Entity\KeyValueStore\KeyvalueEntityStorage::doLoadMultiple()internally callsEntityStorageBase::mapFromStorageRecords.Easy enough, just another call to
$entity_class::createInstance(). But then you haveNodeStorage, which extends Drupal\Core\Entity\Sql\SqlContentEntityStorage, whosemapFromStorageRecords()which returns new instances of the entities but without invokingEntityStorageBase::mapFromStorageRecords(). So I suspect we'd essentially need to go through allEntityStorageclasses looking for versions ofnew $entity_class()to be replaced.Comment #31
mrweiner commentedAh actually, searching all of core for the string
new $entity_classonly shows 6 results, so it might not be so bad at all, assuming that that string is used consistently in all relevant cases.Comment #32
ambient.impact@mrweiner Yup, injecting dependencies via the storage class is how I've gotten it to work on Omnipedia. Having Drupal core support this officially would be awesome.
Comment #34
mrweiner commentedDid some initial work on this based on my earlier comments. Anybody care to give things a look to see that they pass a sniff test? I'm assuming that some tests will need to be updated as well.
At the moment I'm also doing
$this->container = \Drupal::getContainer()in EntityStorageBase::__construct() for proof of concept, but might need to add the container as a constructor argument and update all of the extending classes, though I can see this as being potential BC for contrib.Marked the MR as ready just to see if it would kick off tests to see what happens.
Comment #35
mrweiner commentedActually, realizing that I already made that $container constructor update to ContentEntityStorageBase and SqlContentEntityStorageBase, which may be equally problematic from a BC perspective.
Comment #36
andypostlooks interesting!
Comment #37
mrweiner commentedUpdating title and description
Comment #38
mrweiner commentedWell, 4 failures is certainly better than 212! https://www.drupal.org/pift-ci-job/2468490. Remaining failures are related to the $container itself.
This is simply because I forgot to remove the container as an argument on
KeyValueEntityStorage::__construct(). How this is addressed really depends on how we address...Apparently my WIP solution of using
$this->container = \Drupal::getContainer();in EntityStorageBase is not an option. I was hoping it would be, because it avoids us needing to update constructors for entity classes all over the place, in tests, etc. I'd imagine that this could potentially cause issues in userland code as well, although I do not know.Anybody more knowledgeable care to chime in? Would love the input.
Comment #39
volegerNeeds rebase over 10.1.x
Comment #41
drupals.user commentedIs this still actively being pursued or is there a workaround?
Comment #42
ptmkenny commented@drupals.user There haven't been any updates in 2+ years, so no, it isn't actively being updated.
As a workaround, you can use \Drupal calls without DI.
Comment #46
ambient.impactI used to think DI on entities was a good idea and even implemented a way to do it via overriding the storage class, but I've honestly completely come around to the Typed Entity module approach of using a (very clever) way of wrapping entities; see #26 (#2142515-26: Provide a method of DI for entities). I over-engineered a solution to use DI on node entities, and it turned into a maintenance nightmare on multiple occasions, until I'd had enough and moved all that code to entity wrappers.
Here's the big problem with adding DI to entities: entities are already complex, and this just loads more complexity into them, making them more tedious to test, mock up, etc.
I know the thought of having to add an additional step every time you act on an entity to wrap it may seem annoying - and it certainly held me back for a while - but the benefits end up far outweighing that by allowing you to mock up and test parts of the logic without baking it directly into entity classes. If you want to see a concrete example (and one I'm very proud of), here's an absolutely massive merge request from last year where I did such a refactor to replace overloaded entity class logic with entity wrappers.
Comment #47
mrweiner commentedI don't totally understand the benefits of Typed Entity approach in #26 -- which is fine! If it works for folks then that is great! (Though I wish there were some bite-sized, practical example on that module page that show the benefits. Your massive MR is...well...massive :D).
But the trouble with is -- per #27 -- this is still a contrib approach. It feels like something that should be addressed in core, if possible, for folks who just want to stick with Core. Whether bundle classes or basic entity classes, we end up with a de facto workaround being to just call \Drupal directly. The alternative being to accept the required services as method parameters, which works but isn't as nice of DX.
Comment #48
znerol commentedThis is actually one of the things OOP hooks could help with. Let's take a look at Drupal\system\Entity\Menu. All of the overridden methods (
preDelete(),save(),delete()) could be extracted to a hook class.So, my suggestion would be to first try to reduce close coupling of entities with services by leveraging OOP hooks or the event system (whatever works best for the given entity type).
Comment #49
mrweiner commented@znerol yesh I think you're correct for some cases but not all. For instance in Drupal Commerce, Order entities have this:
It's just a standard method unrelated to entity lifecycle.
Comment #50
znerol commentedYes, sure. I am suggesting incremental improvements.
Comment #51
berdirI have mixed opinions on this.
One one side, DI for things that are being serialized has some problematic drawbacks (and entities are serialized _a lot_), it requires trickery such as DependencySerializationTrait. And there's discussions around whether or not entities should be value object, which would imply they shouldn't do stuff that requires DI.
But also, we have bundle classes now (although they probably mostly serve as nice getters and setters), pre/post methods (Re #48, Hooks being services changes things bit, but I still generally prefer to have entity related logic on the entity itself) and ContentEntityBase itself has a number of \Drupal:: calls.
One problem is that, unlike other plugins, we never got the factory stuff working, so we don't have a single thing responsible for creating entity objects. We did make it work for bundle classes though, it's fairly well isolated in the storage handler.
To summarize, not against it, but I do very much expect that there will be cases where it will come back to bite us.
Comment #52
andypostComment #53
ambient.impact@mrweiner I know it's contrib and I would love it to go into core, but I don't feel like that's a downside - a lot of really awesome and super useful things are implemented in contrib, prove their worth there, and are adopted into core. The biggest benefit in using Typed Entity (in my opinion) is that you now have a clean separation of your data (entities, their fields) and the logic that allows you to interact with it (the wrapped entity plug-ins), which leads to more testable code and hopefully away from spaghetti code.
Edit: here's an example of a simpler wrapped entity plug-in I wrote that used to live on a node class back when we used to it that so we could inject a few services; we no longer do that and instead inject them into the plug-in which also provides methods to interact with the data on the nodes.
@berdir That's also a good point that entities are serialized/deserialized a lot which adds even more potential headaches.