Describe your bug or feature request.
Config Entities are not statically cached. When pages are loaded with multiple products, and prices are show, the price calculation triggers multiple "getTaxTypes()" in the TaxOrderProcessor.
See Drupal\Core\Config\Entity\ConfigEntityType and the protected $static_cache property.
TaxOrderProcessor::process()
TaxOrderProcessor::getTaxTypes()
The getTaxTypes call does a ConfigEntityStorage::loadByProperties and then sort, which calls getPlugin() and then getPluginCollection().
/**
* {@inheritdoc}
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
/** @var \Drupal\commerce_tax\Entity\TaxTypeInterface $a */
/** @var \Drupal\commerce_tax\Entity\TaxTypeInterface $b */
$a_plugin = $a->getPlugin();
$b_plugin = $b->getPlugin();
$a_weight = $a_plugin ? $a_plugin->getWeight() : 0;
$b_weight = $b_plugin ? $b_plugin->getWeight() : 0;
if ($a_weight == $b_weight) {
$a_label = $a->label();
$b_label = $b->label();
return strnatcasecmp($a_label, $b_label);
}
return ($a_weight < $b_weight) ? -1 : 1;
}
/**
* {@inheritdoc}
*/
public function getPlugin() {
return $this->getPluginCollection()->get($this->plugin);
}
/**
* Gets the plugin collection that holds the tax type plugin.
*
* Ensures the plugin collection is initialized before returning it.
*
* @return \Drupal\commerce\CommerceSinglePluginCollection
* The plugin collection.
*/
protected function getPluginCollection() {
if (!$this->pluginCollection) {
$plugin_manager = \Drupal::service('plugin.manager.commerce_tax_type');
$this->pluginCollection = new CommerceSinglePluginCollection($plugin_manager, $this->plugin, $this->configuration, $this);
}
return $this->pluginCollection;
}
Because the ConfigEntityStorage does not cache the Config Entity classes, the getPluginCollection() has to run again and again and instantiate all kinds of stuff behind it. Like profiles and stuff like that, for instance the buildCustomerProfile in TaxTypeBase.
/**
* Builds a customer profile for the given order.
*
* Constructed only for the purposes of tax calculation, never saved.
* The address comes one of the saved profiles, with the following priority:
* - Shipping profile
* - Billing profile
* - Store profile (if the tax type is display inclusive)
* The tax number comes from the billing profile, if present.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
*
* @return \Drupal\profile\Entity\ProfileInterface|null
* The customer profile, or NULL if not available yet.
*/
protected function buildCustomerProfile(OrderInterface $order) {
$order_id = $order->id();
if (!isset($this->profiles[$order_id])) {
$order_profiles = $order->collectProfiles();
$address = NULL;
foreach (['shipping', 'billing'] as $scope) {
if (isset($order_profiles[$scope])) {
$address_field = $order_profiles[$scope]->get('address');
if (!$address_field->isEmpty()) {
$address = $address_field->getValue();
break;
}
}
}
if (!$address && ($this->isDisplayInclusive() || $this->forceEarlyTaxCalculation())) {
// Customer is still unknown, but prices are displayed tax-inclusive
// (VAT scenario), better to show the store's default tax than nothing.
$address = $order->getStore()->getAddress();
}
if (!$address) {
// A customer profile isn't usable without an address. Stop here.
return NULL;
}
$tax_number = NULL;
if (isset($order_profiles['billing'])) {
$tax_number = $order_profiles['billing']->get('tax_number')->getValue();
}
$profile_storage = $this->entityTypeManager->getStorage('profile');
$this->profiles[$order_id] = $profile_storage->create([
'type' => 'customer',
'uid' => 0,
'address' => $address,
'tax_number' => $tax_number,
]);
}
Solutions.
- Set the static_cache property in the TaxType entity definition to true
- Implement a cache property for TaxOrderProcessor::getTaxTypes()
The first has my preference, because the performance gains would extend further than the TaxOrderProcess::getTaxTypes() method.
Issue fork commerce-3301586
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
jefuri CreditAttribution: jefuri as a volunteer and at Synetic commentedComment #4
jefuri CreditAttribution: jefuri as a volunteer and at Synetic commentedComment #5
jefuri CreditAttribution: jefuri as a volunteer and at Synetic commentedComment #6
jefuri CreditAttribution: jefuri as a volunteer and at Synetic commentedComment #7
jsacksick CreditAttribution: jsacksick at Centarro commentedI wonder why Core doesn't statically cache config entities by default though.
Comment #8
jsacksick CreditAttribution: jsacksick at Centarro commentedEven though tests are passing, it's actually hard to predict what could be the sideeffects of that change.
Comment #9
jefuri CreditAttribution: jefuri as a volunteer and at Synetic commentedhttps://www.drupal.org/project/drupal/issues/1885830#comment-14167413
It's opt-in from the beginning. I know that Roles do have static cache enabled.
The underlying load/loadMultiple/loadByProperty all use the ConfigFactory with all the override logic to determine the correct entity in the static cache.
The only reason I can think of that this might be an issue is that during a request your context changes that a configuration entity has different overrides.
For instance, early in the page request your current location resolves to EU somewhere, and taxtypes are loaded. But later in some kind of eventDispatcher it is set to something different, and taxtypes is loaded again (but it is cached in the old context). Than it would not work correctly anymore. To fix this somebody should reset the static cache as wel of the tax types.
But I checked it within commerce, this does not happen in this module. Custom modules, or other contribs might...
I have checked and tested it with https://www.drupal.org/project/commerce_store_domain and it keeps working.
Comment #12
jsacksick CreditAttribution: jsacksick at Centarro commentedCommitted, thanks!
Comment #13
jsacksick CreditAttribution: jsacksick at Centarro commented