WebOrbiton
v1.0.0.0

StocketBase

133 lines · 5.2 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/database.php';
  6. ​
  7. final class ProductVersions
  8. {
  9. private const KEEP = 50;
  10. private const FIELDS = 'title, excerpt, content_blocks, category_id, cover_image_path, seo_title, seo_description, price_cents, compare_at_price_cents';
  11. ​
  12. public static function snapshot(int $productId, ?int $userId, string $userName, string $note = 'save'): void
  13. {
  14. try {
  15. $db = Database::site();
  16. ​
  17. $statement = $db->prepare('SELECT ' . self::FIELDS . ' FROM products WHERE id = :id LIMIT 1');
  18. $statement->execute(['id' => $productId]);
  19. $product = $statement->fetch();
  20. if (!$product) {
  21. return;
  22. }
  23. ​
  24. $hash = self::hash($product);
  25. ​
  26. $lastStatement = $db->prepare('SELECT content_hash FROM product_versions WHERE product_id = :id ORDER BY id DESC LIMIT 1');
  27. $lastStatement->execute(['id' => $productId]);
  28. if ($lastStatement->fetchColumn() === $hash) {
  29. return;
  30. }
  31. ​
  32. $db->prepare(
  33. 'INSERT INTO product_versions
  34. (product_id, content_hash, title, excerpt, content_blocks, category_id, cover_image_path, seo_title, seo_description, price_cents, compare_at_price_cents, saved_by, saved_by_name, note)
  35. VALUES (:product, :hash, :title, :excerpt, :content, :category, :cover, :seo_title, :seo_description, :price, :compare_price, :user, :user_name, :note)'
  36. )->execute([
  37. 'product' => $productId,
  38. 'hash' => $hash,
  39. 'title' => $product['title'],
  40. 'excerpt' => $product['excerpt'],
  41. 'content' => $product['content_blocks'],
  42. 'category' => $product['category_id'],
  43. 'cover' => $product['cover_image_path'],
  44. 'seo_title' => $product['seo_title'],
  45. 'seo_description' => $product['seo_description'],
  46. 'price' => $product['price_cents'],
  47. 'compare_price' => $product['compare_at_price_cents'],
  48. 'user' => $userId,
  49. 'user_name' => mb_substr($userName, 0, 120),
  50. 'note' => mb_substr($note, 0, 60),
  51. ]);
  52. ​
  53. self::prune($productId);
  54. } catch (PDOException $e) {
  55. error_log('StocketBase: could not save product version: ' . $e->getMessage());
  56. }
  57. }
  58. ​
  59. public static function recent(int $productId, int $limit = 15): array
  60. {
  61. try {
  62. $statement = Database::site()->prepare(
  63. 'SELECT id, title, saved_by_name, note, created_at FROM product_versions WHERE product_id = :id ORDER BY id DESC LIMIT ' . max(1, min(50, $limit))
  64. );
  65. $statement->execute(['id' => $productId]);
  66. ​
  67. return $statement->fetchAll();
  68. } catch (PDOException $e) {
  69. return [];
  70. }
  71. }
  72. ​
  73. public static function restore(int $productId, int $versionId, ?int $userId, string $userName): bool
  74. {
  75. $db = Database::site();
  76. ​
  77. $statement = $db->prepare('SELECT * FROM product_versions WHERE id = :version AND product_id = :product LIMIT 1');
  78. $statement->execute(['version' => $versionId, 'product' => $productId]);
  79. $version = $statement->fetch();
  80. if (!$version) {
  81. return false;
  82. }
  83. ​
  84. self::snapshot($productId, $userId, $userName, 'before restore');
  85. ​
  86. $db->prepare(
  87. 'UPDATE products SET title = :title, excerpt = :excerpt, content_blocks = :content, category_id = :category,
  88. cover_image_path = :cover, seo_title = :seo_title, seo_description = :seo_description,
  89. price_cents = :price, compare_at_price_cents = :compare_price, updated_at = NOW()
  90. WHERE id = :id'
  91. )->execute([
  92. 'title' => $version['title'],
  93. 'excerpt' => $version['excerpt'],
  94. 'content' => $version['content_blocks'],
  95. 'category' => $version['category_id'],
  96. 'cover' => $version['cover_image_path'],
  97. 'seo_title' => $version['seo_title'],
  98. 'seo_description' => $version['seo_description'],
  99. 'price' => $version['price_cents'],
  100. 'compare_price' => $version['compare_at_price_cents'],
  101. 'id' => $productId,
  102. ]);
  103. ​
  104. self::snapshot($productId, $userId, $userName, 'restored');
  105. ​
  106. return true;
  107. }
  108. ​
  109. private static function prune(int $productId): void
  110. {
  111. $db = Database::site();
  112. ​
  113. $statement = $db->prepare('SELECT id FROM product_versions WHERE product_id = :id ORDER BY id DESC LIMIT 1 OFFSET ' . self::KEEP);
  114. $statement->execute(['id' => $productId]);
  115. $threshold = $statement->fetchColumn();
  116. ​
  117. if ($threshold !== false) {
  118. $db->prepare('DELETE FROM product_versions WHERE product_id = :product AND id <= :threshold')
  119. ->execute(['product' => $productId, 'threshold' => (int) $threshold]);
  120. }
  121. }
  122. ​
  123. private static function hash(array $product): string
  124. {
  125. $parts = [];
  126. foreach (explode(', ', self::FIELDS) as $field) {
  127. $parts[] = (string) ($product[$field] ?? '');
  128. }
  129. ​
  130. return sha1(implode("\x1f", $parts));
  131. }
  132. }
  133. ​