From e60bce1b4c18fd9c7c853389fc5712fc6b53adcc Mon Sep 17 00:00:00 2001
From: Edie Lemoine <edie@myparcel.nl>
Date: Thu, 24 Oct 2024 15:25:50 +0200
Subject: [PATCH] perf(admin): only save product settings once per request
 (#279)

---
 src/Hooks/HasPdkProductHooks.php | 100 +++++++++++++++++++++++--------
 1 file changed, 74 insertions(+), 26 deletions(-)

diff --git a/src/Hooks/HasPdkProductHooks.php b/src/Hooks/HasPdkProductHooks.php
index 91ec7679..903d973c 100644
--- a/src/Hooks/HasPdkProductHooks.php
+++ b/src/Hooks/HasPdkProductHooks.php
@@ -9,22 +9,33 @@
 use MyParcelNL\Pdk\Base\Support\Arr;
 use MyParcelNL\Pdk\Facade\Actions;
 use MyParcelNL\Pdk\Facade\Frontend;
+use MyParcelNL\Pdk\Facade\Logger;
 use MyParcelNL\Pdk\Facade\Pdk;
 use MyParcelNL\PrestaShop\Facade\EntityManager;
 use MyParcelNL\Sdk\src\Support\Str;
 use Symfony\Component\HttpFoundation\Request;
+use Throwable;
 use Tools;
 
 trait HasPdkProductHooks
 {
     /**
+     * Saves the MyParcel product settings when a product is updated.
+     *
      * @param  array $params
      *
      * @return void
      */
     public function hookActionProductUpdate(array $params): void
     {
-        $this->saveProductSettings((int) $params['id_product']);
+        try {
+            $this->saveProductSettings((int) $params['id_product']);
+        } catch (Throwable $e) {
+            Logger::error(sprintf('Failed to save product settings for product %d', (int) $params['id_product']), [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTrace(),
+            ]);
+        }
     }
 
     /**
@@ -39,6 +50,60 @@ public function hookDisplayAdminProductsExtra(array $params): string
         return $this->renderProductSettings((int) $params['id_product']);
     }
 
+    /**
+     * @param  int   $productId
+     * @param  array $productSettings
+     *
+     * @return \Symfony\Component\HttpFoundation\Request
+     * @throws \JsonException
+     */
+    private function createRequest(int $productId, array $productSettings): Request
+    {
+        $parameters = [
+            'action'    => PdkBackendActions::UPDATE_PRODUCT_SETTINGS,
+            'productId' => $productId,
+        ];
+
+        $requestBody = [];
+
+        foreach ($productSettings as $key => $value) {
+            $trimmedKey               = Arr::last(explode('-', $key));
+            $requestBody[$trimmedKey] = $value;
+        }
+
+        $content = json_encode(['data' => ['product_settings' => $requestBody]], JSON_THROW_ON_ERROR);
+
+        return new Request($parameters, [], [], [], [], [], $content);
+    }
+
+    /**
+     * The product update hook is called multiple (about 8) times by PrestaShop. This method checks if the product
+     * settings are already saved by comparing a checksum of the settings with the checksum we add to $_POST.
+     *
+     * @see https://www.prestashop.com/forums/topic/591295-ps17-hookactionproductupdate-gets-multiple-called-and-no-image-uploaded/
+     *
+     * @param  int   $idProduct
+     * @param  array $productSettings
+     *
+     * @return bool
+     * @throws \JsonException
+     */
+    private function isAlreadySaved(int $idProduct, array $productSettings): bool
+    {
+        $appInfo          = Pdk::getAppInfo();
+        $checksumKey      = "_$appInfo->name-product-save-checksum-$idProduct";
+        $existingChecksum = $_POST[$checksumKey] ?? null;
+        $checksum         = md5(json_encode($productSettings, JSON_THROW_ON_ERROR));
+
+        if ($existingChecksum === $checksum) {
+            return true;
+        }
+
+        $_POST[$checksumKey] = $checksum;
+
+        return false;
+    }
+
     /**
      * @param  int $idProduct
      *
@@ -54,42 +119,25 @@ private function renderProductSettings(int $idProduct): string
     }
 
     /**
-     * @param  int $idProduct
+     * @param  int $productId
      *
      * @return void
+     * @throws \JsonException
      */
-    private function saveProductSettings(int $idProduct): void
+    private function saveProductSettings(int $productId): void
     {
-        $postValues = Tools::getAllValues();
-        $name       = Pdk::getAppInfo()->name;
-
-        $productSettings = array_filter($postValues, static function ($key) use ($name) {
+        $name            = Pdk::getAppInfo()->name;
+        $productSettings = array_filter(Tools::getAllValues(), static function ($key) use ($name) {
             return Str::startsWith((string) $key, $name);
         }, ARRAY_FILTER_USE_KEY);
 
-        if (empty($productSettings)) {
+        if (empty($productSettings) || $this->isAlreadySaved($productId, $productSettings)) {
             return;
         }
 
-        $requestBody = [];
-
-        foreach ($productSettings as $key => $value) {
-            $trimmedKey               = Arr::last(explode('-', $key));
-            $requestBody[$trimmedKey] = $value;
-        }
+        $request = $this->createRequest($productId, $productSettings);
 
-        $request = new Request(
-            [
-                'action'    => PdkBackendActions::UPDATE_PRODUCT_SETTINGS,
-                'productId' => $idProduct,
-            ],
-            [],
-            [],
-            [],
-            [],
-            [],
-            json_encode(['data' => ['product_settings' => $requestBody]])
-        );
+        Logger::debug(sprintf('Saving product settings for product %d', $productId), ['settings' => $productSettings]);
 
         Actions::execute($request);