Updated until comment #27
Problem/Motivation
When enabling a module that implements hook_default_payment_method()
(for example a feature module with a payment method), an extra payment method called "unavailable" is created. Even worse, each time that all caches are cleared another extra payment method called "unavailable" is created.
The issue is that PaymentMethodEntityController doesn't expect that payment method's machine names can be passed to its load()
method. This results in that PaymentMethodEntityController thinks no payment is found and instantiates a PaymentMethodUnavailable.
The Entity API module - that invokes hook_default_payment_method()
- on its turn saves the payment method, thinking it had received a "full" payment_method entity.
Proposed resolution
The proposed solution consist of two things:
- Support loading payment method's by machine name.
- When payment method is not found, instruct the Entity API to not save the payment method by setting the method's status to
ENTITY_FIXED
.
Remaining tasks
- Needs review by the module maintainer
User interface changes
None.
API changes
- The PaymentMethodEntityController supports loading payment method's by machine name.
- When a PaymentMethodUnavailable is constructed it gets the status
ENTITY_FIXED
.
Original report by arosboro
This issue was identified in #2131743: Upgraded Payment and now PaymentMethodUnavailable cannot be removed from payments. but the thread did not correctly identify the underlying cause, and proposed a hack to hide related error messages.
The purpose of opening a new thread is to inform the maintainer of circumstances that cause this issue to surface, so it can hopefully be reproduced.
Here are the steps I took to reproduce and, and temporarily fix the issue:
- Disable Payment and dependencies
- run query:
DELETE FROM `payment_method` WHERE `payment_method`.`controller_class_name` = 'PaymentMethodControllerUnavailable';
- Re-enable Payment and Dependencies one at a time
The final dependency, a feature that exports Basic payment methods "Bill me" and "External Payment", was causing the bad payment method to be created during hook_default_payment_method()
in module.features.inc. Clearing the cache after enabling this feature creates a new instance of the undefined payment method with an auto incremented pmid. This is problematic because over time there are hundreds of invalid payment methods in the system. The feature is nothing special, I used the features ui to export Basic payment methods created through the payment ui.
My resolution for this matter was to simply add a return statement on the first line of hook_default_payment_methods()
in the module.features.inc file.
Comment | File | Size | Author |
---|---|---|---|
#42 | payment_2174395_42.patch | 4.51 KB | Xano |
#3 | payment.payment-unavailable-load-by-name-2174395-3.patch | 768 bytes | MegaChriz |
Comments
Comment #1
arosboro CreditAttribution: arosboro commentedChanging this to postponed. I need to reproduce this with a fresh install, and I don't have time to continue investigating.
Comment #2
arosboro CreditAttribution: arosboro commentedJust confirmed this with a new installation. I'm using commerce_payment as well. When I export two basic payment methods with features and clear the cache, an Unavailable payment method appears in admin/config/services/payment/method. It can be removed, but re-appears after clearing the cache again.
Comment #3
MegaChriz CreditAttribution: MegaChriz commentedThe problem is that the PaymentMethodEntityController expects that payment_method entities are always loaded by ID, but entities can be loaded by name as well. See
entity_load_multiple_by_name()
in entity.module from the Entity API module.I think the payment methods are loaded by name when they are loaded from the feature, see
EntityDefaultFeaturesController::export_options()
in entity.features.inc from the Entity API module.The attached patch adds a check to PaymentMethodEntityController: it checks if the requested payment methods are one of the loaded payment methods' names before instantiating a new PaymentMethodUnavailable.
Comment #4
MegaChriz CreditAttribution: MegaChriz commentedOn a simple test environment with only one payment method in a feature, the patch in #3 seemed to work fine. But in a more complex environment with multiple payment methods in a feature, the PaymentMethodControllerUnavailable still shows up. I also get duplicate payment methods after first uninstalling Payment and then installing the feature. It seems as though Payment doesn't force the payment methods' machine names to be unique, but I yet have to verify this on a clean install.
I will investigate the issue further and I'm working on automated tests.
Comment #5
MegaChriz CreditAttribution: MegaChriz commentedI figured out that the fact payment methods can be loaded by name is not the only problem. When building default payment methods in
_entity_defaults_rebuild()
from Entity API module, Entity API checks if such payment method already exists by trying to load it. Payment module receives a request to load a payment method that does not exists and returns a PaymentMethodUnavailable, which in turn is saved by Entity API based on the state of the payment method, which isENTITY_CUSTOM
(see property$status
from the class "PaymentMethod").See this code from
_entity_defaults_rebuild()
entity.module:Entity API saves entities that have the status
ENTITY_CUSTOM
and not have the statusENTITY_OVERRIDDEN
. This is currently true for PaymentMethodUnavailable payment methods.Attached are two patches, one with tests and one without tests.
Tests
hook_default_payment_method()
.Fix
hook_default_payment_method()
an incomplete/experimental fix is added. The status property for PaymentMethodUnavailable is set toENTITY_FIXED
, so Entity API skips it in_entity_defaults_rebuild()
(instead of saving it). The test PaymentTestDefaultPaymentMethodWebTestCase will still have one "exception", though.Comment #8
MegaChriz CreditAttribution: MegaChriz commentedSome of the tests provided by #5 are not executed. See #2229787: PaymentTestEntityCrudWebTestCase is not executed by testbot.
Comment #9
Xano5: payment.payment-unavailable-load-by-name-2174395-5.patch queued for re-testing.
Comment #10
Xano5: payment.payment-unavailable-load-by-name-2174395-5-tests-only.patch queued for re-testing.
Comment #13
torotil CreditAttribution: torotil commentedI've rerolled the patches against the current 7.x-1.x.
Comment #16
torotil CreditAttribution: torotil commentedSpooky. Somehow the very-same patches created via git format-patch work while they don't when they are created via git diff.
Comment #17
torotil CreditAttribution: torotil commentedComment #20
MegaChriz CreditAttribution: MegaChriz commented@torotil
Thanks for the reroll. There is still one exception during the new added "Default payment hook" test that I didn't knew how to fix.
I also wasn't entire sure if the following is right:
This fix is experimental, as noted in #5:
Side note: the patch in #13 did not apply because it tried to patch a non-existing file:
This file was added by the patch in #5.
Comment #21
torotil CreditAttribution: torotil commentedI don't think that this is entirely wrong. ENTITY_FIXED is exactly what we should use for on-the-fly generated entities like PaymentMethodUnavailable (that should not be saved). Perhaps a better place to set it is in PaymentMethodEntityController::load() exactly when it is returned instead of a real PaymentMethod. Although this is a matter of taste as long as PaymentMethodUnavailable isn't used anywhere else.
I'm again attaching two patches - this time the tests should pass.
Comment #22
torotil CreditAttribution: torotil commentedComment #24
torotil CreditAttribution: torotil commentedSetting to needs review again since only the test-only patch failed.
Comment #25
xmacinfoAny traction on this issue? We are hit by the bug that increments regularly the PMID. So the Basic Payment Method was most of the time generating invalid rules (name based on PMID) in combination with Features.
Comment #26
eugene.ilyin CreditAttribution: eugene.ilyin commentedI analysed this problem, and I found that problem in function "load()" of class PaymentMethodEntityController. Problem occured because in code, compares ID's of loaded entities and ID's, which was sent into the "load()" function. In some cases to the "load()" function can be sent name of entity instead of ID (pmid). And when pmid compares with name of entity, I got this wrong unavailable payment method.
I wrote simple patch, which fixes this problem on my project.
Comment #27
XanoCan someone please update the issue summary with a description of the cause of this problem?
@eugene.ilyin: please don't just dump patches in issues where others are already working on a patch. It is very counter-productive. At the very least you should describe how and why your patch differs from the others.
Comment #28
MegaChriz CreditAttribution: MegaChriz commentedI've updated the issue summary. Hopefully, the cause of the problem is more clear now.
Comment #29
MegaChriz CreditAttribution: MegaChriz commentedComment #30
XanoIf you look at the documentation of
DrupalEntityControllerInterface::load()
, it says that the parameter contians entity IDs and not machine names. This sounds like we need to fix the calling code, instead of the entity controller.Comment #31
torotil CreditAttribution: torotil commentedXano: There is still the issue with ENTITY_FIXED.
PaymentMethod as an exportable entity has a status attribute. The on-the-fly generated PaymentMethodUnavailable can not be stored in the database whatsoever. So it should indeed have a status of ENTITY_FIXED:
That's what fixed the issues for me. Honestly I do not know what #26 is all about.
Maybe setting the status ENTITY_FIXED only hides the symptoms, though.
Comment #32
MegaChriz CreditAttribution: MegaChriz commentedThis is the calling code from entity.module:
This is the entity definition of entity type "payment_method". The entity type has a name key and is exportable.
From entity.api.php:
From class EntityAPIControllerExportable in entity.controller.inc:
I think because Payment supports the exportable feature of the Entity API, it should also support loading payment methods by name.
Comment #33
XanoAlright, thanks for looking into this! I'll have to dive into core's entity API and the Entity module's API a bit more to confirm this is the desired behavior, but it looks like we need to cater for this in Payment indeed.
Comment #34
XanoSee #2296483: entity_load_multiple_by_name() uses entity_load() with names instead of IDs.
Comment #35
XanoThe patch should suppress the error, but I don't think it will ever properly work, as
DrupalDefaultEntityController::buildQuery()
explicitly uses the passed on IDs in a condition on the entity type's ID field. If names are passed on, this will never result in any entities being loaded.Comment #36
MegaChriz CreditAttribution: MegaChriz commented@Xano
I just tried the following code and I got one payment method entity back (where "example" is the machine name of one payment method):
This works because EntityAPIControllerExportable overrides the
buildQuery()
method (and PaymentMethodEntityController extends EntityAPIControllerExportable):Comment #37
XanoSorry for causing the confusion. I looked at the wrong controller class. The patch looks good and I just committed it.
Thank you for helping out!
Comment #39
XanoI realize the fix I committed was for the loading issue only. If the default payment method/constant problem still exists, can we tackle that in a follow-up issue?
Comment #40
MegaChriz CreditAttribution: MegaChriz commented@Xano
I see you committed code based on the patch from #26, but the patch from #21 also included tests for loading payment methods by name. Should these tests be handled in a new issue as well?
Comment #41
XanoLet's commit the tests for the loading issue here. Thanks for noticing!
Comment #42
XanoThis contains only the test bit.
Comment #44
XanoThanks again!