DbStorage.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\UrlRewrite\Model\Storage;
  7. use Magento\Framework\Api\DataObjectHelper;
  8. use Magento\Framework\App\ResourceConnection;
  9. use Magento\Framework\DB\Select;
  10. use Magento\UrlRewrite\Model\OptionProvider;
  11. use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
  12. use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory;
  13. use Psr\Log\LoggerInterface;
  14. use Magento\Framework\App\ObjectManager;
  15. use Magento\Framework\DB\Adapter\AdapterInterface;
  16. /**
  17. * Url rewrites DB storage.
  18. *
  19. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  20. */
  21. class DbStorage extends AbstractStorage
  22. {
  23. /**
  24. * DB Storage table name
  25. */
  26. const TABLE_NAME = 'url_rewrite';
  27. /**
  28. * Code of "Integrity constraint violation: 1062 Duplicate entry" error
  29. */
  30. const ERROR_CODE_DUPLICATE_ENTRY = 1062;
  31. /**
  32. * @var AdapterInterface
  33. */
  34. protected $connection;
  35. /**
  36. * @var Resource
  37. */
  38. protected $resource;
  39. /**
  40. * @var LoggerInterface
  41. */
  42. private $logger;
  43. /**
  44. * @param UrlRewriteFactory $urlRewriteFactory
  45. * @param DataObjectHelper $dataObjectHelper
  46. * @param ResourceConnection $resource
  47. * @param LoggerInterface|null $logger
  48. */
  49. public function __construct(
  50. UrlRewriteFactory $urlRewriteFactory,
  51. DataObjectHelper $dataObjectHelper,
  52. ResourceConnection $resource,
  53. LoggerInterface $logger = null
  54. ) {
  55. $this->connection = $resource->getConnection();
  56. $this->resource = $resource;
  57. $this->logger = $logger ?: ObjectManager::getInstance()
  58. ->get(LoggerInterface::class);
  59. parent::__construct($urlRewriteFactory, $dataObjectHelper);
  60. }
  61. /**
  62. * Prepare select statement for specific filter
  63. *
  64. * @param array $data
  65. * @return Select
  66. */
  67. protected function prepareSelect(array $data)
  68. {
  69. $select = $this->connection->select();
  70. $select->from($this->resource->getTableName(self::TABLE_NAME));
  71. foreach ($data as $column => $value) {
  72. $select->where($this->connection->quoteIdentifier($column) . ' IN (?)', $value);
  73. }
  74. return $select;
  75. }
  76. /**
  77. * @inheritdoc
  78. */
  79. protected function doFindAllByData(array $data)
  80. {
  81. return $this->connection->fetchAll($this->prepareSelect($data));
  82. }
  83. /**
  84. * @inheritdoc
  85. */
  86. protected function doFindOneByData(array $data)
  87. {
  88. if (array_key_exists(UrlRewrite::REQUEST_PATH, $data)
  89. && is_string($data[UrlRewrite::REQUEST_PATH])
  90. ) {
  91. $result = null;
  92. $requestPath = $data[UrlRewrite::REQUEST_PATH];
  93. $data[UrlRewrite::REQUEST_PATH] = [
  94. rtrim($requestPath, '/'),
  95. rtrim($requestPath, '/') . '/',
  96. ];
  97. $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data));
  98. if (count($resultsFromDb) === 1) {
  99. $resultFromDb = current($resultsFromDb);
  100. $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT];
  101. // If request path matches the DB value or it's redirect - we can return result from DB
  102. $canReturnResultFromDb = ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath
  103. || in_array((int)$resultFromDb[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true));
  104. // Otherwise return 301 redirect to request path from DB results
  105. $result = $canReturnResultFromDb ? $resultFromDb : [
  106. UrlRewrite::ENTITY_TYPE => 'custom',
  107. UrlRewrite::ENTITY_ID => '0',
  108. UrlRewrite::REQUEST_PATH => $requestPath,
  109. UrlRewrite::TARGET_PATH => $resultFromDb[UrlRewrite::REQUEST_PATH],
  110. UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT,
  111. UrlRewrite::STORE_ID => $resultFromDb[UrlRewrite::STORE_ID],
  112. UrlRewrite::DESCRIPTION => null,
  113. UrlRewrite::IS_AUTOGENERATED => '0',
  114. UrlRewrite::METADATA => null,
  115. ];
  116. } else {
  117. // If we have 2 results - return the row that matches request path
  118. foreach ($resultsFromDb as $resultFromDb) {
  119. if ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath) {
  120. $result = $resultFromDb;
  121. break;
  122. }
  123. }
  124. }
  125. return $result;
  126. }
  127. return $this->connection->fetchRow($this->prepareSelect($data));
  128. }
  129. /**
  130. * Delete old URLs from DB.
  131. *
  132. * @param UrlRewrite[] $urls
  133. * @return void
  134. */
  135. private function deleteOldUrls(array $urls): void
  136. {
  137. $oldUrlsSelect = $this->connection->select();
  138. $oldUrlsSelect->from(
  139. $this->resource->getTableName(self::TABLE_NAME)
  140. );
  141. $uniqueEntities = $this->prepareUniqueEntities($urls);
  142. foreach ($uniqueEntities as $storeId => $entityTypes) {
  143. foreach ($entityTypes as $entityType => $entities) {
  144. $oldUrlsSelect->orWhere(
  145. $this->connection->quoteIdentifier(
  146. UrlRewrite::STORE_ID
  147. ) . ' = ' . $this->connection->quote($storeId, 'INTEGER') .
  148. ' AND ' . $this->connection->quoteIdentifier(
  149. UrlRewrite::ENTITY_ID
  150. ) . ' IN (' . $this->connection->quote($entities, 'INTEGER') . ')' .
  151. ' AND ' . $this->connection->quoteIdentifier(
  152. UrlRewrite::ENTITY_TYPE
  153. ) . ' = ' . $this->connection->quote($entityType)
  154. );
  155. }
  156. }
  157. // prevent query locking in a case when nothing to delete
  158. $checkOldUrlsSelect = clone $oldUrlsSelect;
  159. $checkOldUrlsSelect->reset(Select::COLUMNS);
  160. $checkOldUrlsSelect->columns('count(*)');
  161. $hasOldUrls = (bool)$this->connection->fetchOne($checkOldUrlsSelect);
  162. if ($hasOldUrls) {
  163. $this->connection->query(
  164. $oldUrlsSelect->deleteFromSelect(
  165. $this->resource->getTableName(self::TABLE_NAME)
  166. )
  167. );
  168. }
  169. }
  170. /**
  171. * Prepare array with unique entities
  172. *
  173. * @param UrlRewrite[] $urls
  174. * @return array
  175. */
  176. private function prepareUniqueEntities(array $urls): array
  177. {
  178. $uniqueEntities = [];
  179. /** @var UrlRewrite $url */
  180. foreach ($urls as $url) {
  181. $entityIds = (!empty($uniqueEntities[$url->getStoreId()][$url->getEntityType()])) ?
  182. $uniqueEntities[$url->getStoreId()][$url->getEntityType()] : [];
  183. if (!\in_array($url->getEntityId(), $entityIds)) {
  184. $entityIds[] = $url->getEntityId();
  185. }
  186. $uniqueEntities[$url->getStoreId()][$url->getEntityType()] = $entityIds;
  187. }
  188. return $uniqueEntities;
  189. }
  190. /**
  191. * @inheritDoc
  192. */
  193. protected function doReplace(array $urls)
  194. {
  195. $this->deleteOldUrls($urls);
  196. $data = [];
  197. foreach ($urls as $url) {
  198. $data[] = $url->toArray();
  199. }
  200. try {
  201. $this->insertMultiple($data);
  202. } catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
  203. /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] $urlConflicted */
  204. $urlConflicted = [];
  205. foreach ($urls as $url) {
  206. $urlFound = $this->doFindOneByData(
  207. [
  208. UrlRewrite::REQUEST_PATH => $url->getRequestPath(),
  209. UrlRewrite::STORE_ID => $url->getStoreId(),
  210. ]
  211. );
  212. if (isset($urlFound[UrlRewrite::URL_REWRITE_ID])) {
  213. $urlConflicted[$urlFound[UrlRewrite::URL_REWRITE_ID]] = $url->toArray();
  214. }
  215. }
  216. if ($urlConflicted) {
  217. throw new \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException(
  218. __('URL key for specified store already exists.'),
  219. $e,
  220. $e->getCode(),
  221. $urlConflicted
  222. );
  223. } else {
  224. throw $e->getPrevious() ?: $e;
  225. }
  226. }
  227. return $urls;
  228. }
  229. /**
  230. * Insert multiple
  231. *
  232. * @param array $data
  233. * @return void
  234. * @throws \Magento\Framework\Exception\AlreadyExistsException|\Exception
  235. * @throws \Exception
  236. */
  237. protected function insertMultiple($data)
  238. {
  239. try {
  240. $this->connection->insertMultiple($this->resource->getTableName(self::TABLE_NAME), $data);
  241. } catch (\Exception $e) {
  242. if (($e->getCode() === self::ERROR_CODE_DUPLICATE_ENTRY)
  243. && preg_match('#SQLSTATE\[23000\]: [^:]+: 1062[^\d]#', $e->getMessage())
  244. ) {
  245. throw new \Magento\Framework\Exception\AlreadyExistsException(
  246. __('URL key for specified store already exists.'),
  247. $e
  248. );
  249. }
  250. throw $e;
  251. }
  252. }
  253. /**
  254. * Get filter for url rows deletion due to provided urls
  255. *
  256. * @param UrlRewrite[] $urls
  257. * @return array
  258. * @deprecated 101.0.3 Not used anymore.
  259. */
  260. protected function createFilterDataBasedOnUrls($urls)
  261. {
  262. $data = [];
  263. foreach ($urls as $url) {
  264. $entityType = $url->getEntityType();
  265. foreach ([UrlRewrite::ENTITY_ID, UrlRewrite::STORE_ID] as $key) {
  266. $fieldValue = $url->getByKey($key);
  267. if (!isset($data[$entityType][$key]) || !in_array($fieldValue, $data[$entityType][$key])) {
  268. $data[$entityType][$key][] = $fieldValue;
  269. }
  270. }
  271. }
  272. return $data;
  273. }
  274. /**
  275. * @inheritdoc
  276. */
  277. public function deleteByData(array $data)
  278. {
  279. $this->connection->query(
  280. $this->prepareSelect($data)->deleteFromSelect($this->resource->getTableName(self::TABLE_NAME))
  281. );
  282. }
  283. }