Migration.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Module\Setup;
  7. use Magento\Framework\App\Filesystem\DirectoryList;
  8. use Magento\Framework\Filesystem;
  9. use Magento\Framework\Setup\ModuleDataSetupInterface;
  10. /**
  11. * Resource setup model with methods needed for migration process between Magento versions
  12. *
  13. * @api
  14. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  15. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  16. * @since 100.0.2
  17. */
  18. class Migration
  19. {
  20. /**#@+
  21. * Type of field content where class alias is used
  22. */
  23. const FIELD_CONTENT_TYPE_PLAIN = 'plain';
  24. const FIELD_CONTENT_TYPE_XML = 'xml';
  25. const FIELD_CONTENT_TYPE_WIKI = 'wiki';
  26. const FIELD_CONTENT_TYPE_SERIALIZED = 'serialized';
  27. /**#@-*/
  28. /**#@+
  29. * Entity type of alias
  30. */
  31. const ENTITY_TYPE_MODEL = 'Model';
  32. const ENTITY_TYPE_BLOCK = 'Block';
  33. const ENTITY_TYPE_RESOURCE = 'Model_Resource';
  34. /**#@-*/
  35. /**#@+
  36. * Replace pattern
  37. */
  38. const SERIALIZED_REPLACE_PATTERN = 's:%d:"%s"';
  39. /**#@-*/
  40. /**#@-*/
  41. protected $_confPathToMapFile;
  42. /**
  43. * List of possible entity types sorted by possibility of usage
  44. *
  45. * @var array
  46. */
  47. protected $_entityTypes = [self::ENTITY_TYPE_MODEL, self::ENTITY_TYPE_BLOCK, self::ENTITY_TYPE_RESOURCE];
  48. /**
  49. * Rows per page. To split processing data from tables
  50. *
  51. * @var int
  52. */
  53. protected $_rowsPerPage = 100;
  54. /**
  55. * Replace rules for tables
  56. *
  57. * [table name] => array(
  58. * [field name] => array(
  59. * 'entity_type' => [entity type]
  60. * 'content_type' => [content type]
  61. * 'additional_where' => [additional where]
  62. * )
  63. * )
  64. *
  65. * @var array
  66. */
  67. protected $_replaceRules = [];
  68. /**
  69. * Aliases to classes map
  70. *
  71. * [entity type] => array(
  72. * [alias] => [class name]
  73. * )
  74. *
  75. * @var array
  76. */
  77. protected $_aliasesMap;
  78. /**
  79. * Replacement regexps for specified content types
  80. *
  81. * @var array
  82. */
  83. protected $_replacePatterns = [];
  84. /**
  85. * Path to map file from config
  86. *
  87. * @var string
  88. */
  89. protected $_pathToMapFile;
  90. /**
  91. * List of composite module names
  92. *
  93. * @var array
  94. */
  95. protected $_compositeModules;
  96. /**
  97. * @var \Magento\Framework\Filesystem\Directory\Read
  98. */
  99. protected $_directory;
  100. /**
  101. * @var MigrationData
  102. */
  103. protected $_migrationData;
  104. /**
  105. * @var ModuleDataSetupInterface
  106. */
  107. private $setup;
  108. /**
  109. * @var \Magento\Framework\Serialize\Serializer\Json
  110. */
  111. private $serializer;
  112. /**
  113. * @param ModuleDataSetupInterface $setup
  114. * @param Filesystem $filesystem
  115. * @param MigrationData $migrationData
  116. * @param string $confPathToMapFile
  117. * @param array $compositeModules
  118. * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
  119. * @throws \RuntimeException
  120. */
  121. public function __construct(
  122. ModuleDataSetupInterface $setup,
  123. Filesystem $filesystem,
  124. MigrationData $migrationData,
  125. $confPathToMapFile,
  126. $compositeModules = [],
  127. \Magento\Framework\Serialize\Serializer\Json $serializer = null
  128. ) {
  129. $this->_directory = $filesystem->getDirectoryRead(DirectoryList::ROOT);
  130. $this->_pathToMapFile = $confPathToMapFile;
  131. $this->_migrationData = $migrationData;
  132. $this->_replacePatterns = [
  133. self::FIELD_CONTENT_TYPE_WIKI => $this->_migrationData->getWikiFindPattern(),
  134. self::FIELD_CONTENT_TYPE_XML => $this->_migrationData->getXmlFindPattern(),
  135. ];
  136. $this->_compositeModules = $compositeModules;
  137. $this->setup = $setup;
  138. $this->serializer = $serializer?: \Magento\Framework\App\ObjectManager::getInstance()
  139. ->get(\Magento\Framework\Serialize\Serializer\Json::class);
  140. }
  141. /**
  142. * Add alias replace rule
  143. *
  144. * @param string $tableName name of table to replace aliases in
  145. * @param string $fieldName name of table column to replace aliases in
  146. * @param string $entityType entity type of alias
  147. * @param string $fieldContentType type of field content where class alias is used
  148. * @param array $primaryKeyFields row pk field(s) to update by
  149. * @param string $additionalWhere additional where condition
  150. * @return void
  151. */
  152. public function appendClassAliasReplace(
  153. $tableName,
  154. $fieldName,
  155. $entityType = '',
  156. $fieldContentType = self::FIELD_CONTENT_TYPE_PLAIN,
  157. array $primaryKeyFields = [],
  158. $additionalWhere = ''
  159. ) {
  160. if (!isset($this->_replaceRules[$tableName])) {
  161. $this->_replaceRules[$tableName] = [];
  162. }
  163. if (!isset($this->_replaceRules[$tableName][$fieldName])) {
  164. $this->_replaceRules[$tableName][$fieldName] = [
  165. 'entity_type' => $entityType,
  166. 'content_type' => $fieldContentType,
  167. 'pk_fields' => $primaryKeyFields,
  168. 'additional_where' => $additionalWhere,
  169. ];
  170. }
  171. }
  172. /**
  173. * Start process of replacing aliases with class names using rules
  174. *
  175. * @return void
  176. */
  177. public function doUpdateClassAliases()
  178. {
  179. foreach ($this->_replaceRules as $tableName => $tableRules) {
  180. $this->_updateClassAliasesInTable($tableName, $tableRules);
  181. }
  182. }
  183. /**
  184. * Update class aliases in table
  185. *
  186. * @param string $tableName name of table to replace aliases in
  187. * @param array $tableRules replacing rules for table
  188. * @return void
  189. */
  190. protected function _updateClassAliasesInTable($tableName, array $tableRules)
  191. {
  192. foreach ($tableRules as $fieldName => $fieldRule) {
  193. $pagesCount = ceil(
  194. $this->_getRowsCount($tableName, $fieldName, $fieldRule['additional_where']) / $this->_rowsPerPage
  195. );
  196. for ($page = 1; $page <= $pagesCount; $page++) {
  197. $this->_applyFieldRule($tableName, $fieldName, $fieldRule, $page);
  198. }
  199. }
  200. }
  201. /**
  202. * Get amount of rows for table column which should be processed
  203. *
  204. * @param string $tableName name of table to replace aliases in
  205. * @param string $fieldName name of table column to replace aliases in
  206. * @param string $additionalWhere additional where condition
  207. * @return int
  208. */
  209. protected function _getRowsCount($tableName, $fieldName, $additionalWhere = '')
  210. {
  211. $connection = $this->setup->getConnection();
  212. $query = $connection->select()->from(
  213. $this->setup->getTable($tableName),
  214. ['rows_count' => new \Zend_Db_Expr('COUNT(*)')]
  215. )->where(
  216. $fieldName . ' IS NOT NULL'
  217. );
  218. if (!empty($additionalWhere)) {
  219. $query->where($additionalWhere);
  220. }
  221. return (int)$connection->fetchOne($query);
  222. }
  223. /**
  224. * Replace aliases with class names in rows
  225. *
  226. * @param string $tableName name of table to replace aliases in
  227. * @param string $fieldName name of table column to replace aliases in
  228. * @param array $fieldRule
  229. * @param int $currentPage
  230. * @return void
  231. */
  232. protected function _applyFieldRule($tableName, $fieldName, array $fieldRule, $currentPage = 0)
  233. {
  234. $fieldsToSelect = [$fieldName];
  235. if (!empty($fieldRule['pk_fields'])) {
  236. $fieldsToSelect = array_merge($fieldsToSelect, $fieldRule['pk_fields']);
  237. }
  238. $tableData = $this->_getTableData(
  239. $tableName,
  240. $fieldName,
  241. $fieldsToSelect,
  242. $fieldRule['additional_where'],
  243. $currentPage
  244. );
  245. $fieldReplacements = [];
  246. foreach ($tableData as $rowData) {
  247. $replacement = $this->_getReplacement(
  248. $rowData[$fieldName],
  249. $fieldRule['content_type'],
  250. $fieldRule['entity_type']
  251. );
  252. if ($replacement !== $rowData[$fieldName]) {
  253. $fieldReplacement = ['to' => $replacement];
  254. if (empty($fieldRule['pk_fields'])) {
  255. $fieldReplacement['where_fields'] = [$fieldName => $rowData[$fieldName]];
  256. } else {
  257. $fieldReplacement['where_fields'] = [];
  258. foreach ($fieldRule['pk_fields'] as $pkField) {
  259. $fieldReplacement['where_fields'][$pkField] = $rowData[$pkField];
  260. }
  261. }
  262. $fieldReplacements[] = $fieldReplacement;
  263. }
  264. }
  265. $this->_updateRowsData($tableName, $fieldName, $fieldReplacements);
  266. }
  267. /**
  268. * Update rows data in database
  269. *
  270. * @param string $tableName
  271. * @param string $fieldName
  272. * @param array $fieldReplacements
  273. * @return void
  274. */
  275. protected function _updateRowsData($tableName, $fieldName, array $fieldReplacements)
  276. {
  277. if (count($fieldReplacements) > 0) {
  278. $connection = $this->setup->getConnection();
  279. foreach ($fieldReplacements as $fieldReplacement) {
  280. $where = [];
  281. foreach ($fieldReplacement['where_fields'] as $whereFieldName => $value) {
  282. $where[$connection->quoteIdentifier($whereFieldName) . ' = ?'] = $value;
  283. }
  284. $connection->update(
  285. $this->setup->getTable($tableName),
  286. [$fieldName => $fieldReplacement['to']],
  287. $where
  288. );
  289. }
  290. }
  291. }
  292. /**
  293. * Get data for table column which should be processed
  294. *
  295. * @param string $tableName name of table to replace aliases in
  296. * @param string $fieldName name of table column to replace aliases in
  297. * @param array $fieldsToSelect array of fields to select
  298. * @param string $additionalWhere additional where condition
  299. * @param int $currPage
  300. * @return array
  301. */
  302. protected function _getTableData(
  303. $tableName,
  304. $fieldName,
  305. array $fieldsToSelect,
  306. $additionalWhere = '',
  307. $currPage = 0
  308. ) {
  309. $connection = $this->setup->getConnection();
  310. $query = $connection->select()->from(
  311. $this->setup->getTable($tableName),
  312. $fieldsToSelect
  313. )->where(
  314. $fieldName . ' IS NOT NULL'
  315. );
  316. if (!empty($additionalWhere)) {
  317. $query->where($additionalWhere);
  318. }
  319. if ($currPage) {
  320. $query->limitPage($currPage, $this->_rowsPerPage);
  321. }
  322. return $connection->fetchAll($query);
  323. }
  324. /**
  325. * Get data with replaced aliases with class names
  326. *
  327. * @param string $data
  328. * @param string $contentType type of data (field content)
  329. * @param string $entityType entity type of alias
  330. * @return string
  331. */
  332. protected function _getReplacement($data, $contentType, $entityType = '')
  333. {
  334. switch ($contentType) {
  335. case self::FIELD_CONTENT_TYPE_SERIALIZED:
  336. $data = $this->_getAliasInSerializedStringReplacement($data, $entityType);
  337. break;
  338. // wiki and xml content types use the same replacement method
  339. case self::FIELD_CONTENT_TYPE_WIKI:
  340. case self::FIELD_CONTENT_TYPE_XML:
  341. $data = $this->_getPatternReplacement($data, $contentType, $entityType);
  342. break;
  343. case self::FIELD_CONTENT_TYPE_PLAIN:
  344. default:
  345. $data = $this->_getModelReplacement($data, $entityType);
  346. break;
  347. }
  348. return $data;
  349. }
  350. /**
  351. * Get appropriate class name for alias
  352. *
  353. * @param string $alias
  354. * @param string $entityType entity type of alias
  355. * @return string
  356. */
  357. protected function _getCorrespondingClassName($alias, $entityType = '')
  358. {
  359. if ($this->_isFactoryName($alias)) {
  360. if ($className = $this->_getAliasFromMap($alias, $entityType)) {
  361. return $className;
  362. }
  363. list($module, $name) = $this->_getModuleName($alias);
  364. if (!empty($entityType)) {
  365. $className = $this->_getClassName($module, $entityType, $name);
  366. $properEntityType = $entityType;
  367. } else {
  368. // Try to find appropriate class name for all entity types
  369. $className = '';
  370. $properEntityType = '';
  371. foreach ($this->_entityTypes as $entityType) {
  372. if (empty($className)) {
  373. $className = $this->_getClassName($module, $entityType, $name);
  374. $properEntityType = $entityType;
  375. } else {
  376. // If was found more than one match - alias cannot be replaced
  377. return '';
  378. }
  379. }
  380. }
  381. $this->_pushToMap($properEntityType, $alias, $className);
  382. return $className;
  383. }
  384. return '';
  385. }
  386. /**
  387. * Replacement for model alias and model alias with method
  388. *
  389. * @param string $data
  390. * @param string $entityType
  391. * @return string
  392. */
  393. protected function _getModelReplacement($data, $entityType = '')
  394. {
  395. if (preg_match($this->_migrationData->getPlainFindPattern(), $data, $matches)) {
  396. $classAlias = $matches['alias'];
  397. $className = $this->_getCorrespondingClassName($classAlias, $entityType);
  398. if ($className) {
  399. return str_replace($classAlias, $className, $data);
  400. }
  401. }
  402. $className = $this->_getCorrespondingClassName($data, $entityType);
  403. if (!empty($className)) {
  404. return $className;
  405. } else {
  406. return $data;
  407. }
  408. }
  409. /**
  410. * Replaces class aliases using pattern
  411. *
  412. * @param string $data
  413. * @param string $contentType
  414. * @param string $entityType
  415. * @return string|null
  416. */
  417. protected function _getPatternReplacement($data, $contentType, $entityType = '')
  418. {
  419. if (!array_key_exists($contentType, $this->_replacePatterns)) {
  420. return null;
  421. }
  422. $replacements = [];
  423. $pattern = $this->_replacePatterns[$contentType];
  424. preg_match_all($pattern, $data, $matches, PREG_PATTERN_ORDER);
  425. if (isset($matches['alias'])) {
  426. $matches = array_unique($matches['alias']);
  427. foreach ($matches as $classAlias) {
  428. $className = $this->_getCorrespondingClassName($classAlias, $entityType);
  429. if ($className) {
  430. $replacements[$classAlias] = $className;
  431. }
  432. }
  433. }
  434. foreach ($replacements as $classAlias => $className) {
  435. $data = str_replace($classAlias, $className, $data);
  436. }
  437. return $data;
  438. }
  439. /**
  440. * Generate class name
  441. *
  442. * @param string $module
  443. * @param string $type
  444. * @param string $name
  445. * @return string
  446. */
  447. protected function _getClassName($module, $type, $name = null)
  448. {
  449. $className = implode('\\', array_map('ucfirst', explode('_', $module . '_' . $type . '_' . $name)));
  450. if (class_exists($className)) {
  451. return $className;
  452. }
  453. return '';
  454. }
  455. /**
  456. * Whether the given class name is a factory name
  457. *
  458. * @param string $factoryName
  459. * @return bool
  460. */
  461. protected function _isFactoryName($factoryName)
  462. {
  463. return false !== strpos($factoryName, '/') || preg_match('/^[a-z\d]+(_[A-Za-z\d]+)?$/', $factoryName);
  464. }
  465. /**
  466. * Transform factory name into a pair of module and name
  467. *
  468. * @param string $factoryName
  469. * @return array
  470. */
  471. protected function _getModuleName($factoryName)
  472. {
  473. if (false !== strpos($factoryName, '/')) {
  474. list($module, $name) = explode('/', $factoryName);
  475. } else {
  476. $module = $factoryName;
  477. $name = false;
  478. }
  479. $compositeModuleName = $this->_getCompositeModuleName($module);
  480. if (null !== $compositeModuleName) {
  481. $module = $compositeModuleName;
  482. } elseif (false === strpos($module, '_')) {
  483. $module = "Magento_{$module}";
  484. }
  485. return [$module, $name];
  486. }
  487. /**
  488. * Get composite module name by module alias
  489. *
  490. * @param string $moduleAlias
  491. * @return string|null
  492. */
  493. protected function _getCompositeModuleName($moduleAlias)
  494. {
  495. if (array_key_exists($moduleAlias, $this->_compositeModules)) {
  496. return $this->_compositeModules[$moduleAlias];
  497. }
  498. return null;
  499. }
  500. /**
  501. * Search class by alias in map
  502. *
  503. * @param string $alias
  504. * @param string $entityType
  505. * @return string
  506. */
  507. protected function _getAliasFromMap($alias, $entityType = '')
  508. {
  509. if ($map = $this->_getAliasesMap()) {
  510. if (!empty($entityType) && isset($map[$entityType]) && !empty($map[$entityType][$alias])) {
  511. return $map[$entityType][$alias];
  512. } else {
  513. $className = '';
  514. foreach ($this->_entityTypes as $entityType) {
  515. if (empty($className)) {
  516. if (isset($map[$entityType]) && !empty($map[$entityType][$alias])) {
  517. $className = $map[$entityType][$alias];
  518. }
  519. } else {
  520. return '';
  521. }
  522. }
  523. return $className;
  524. }
  525. }
  526. return '';
  527. }
  528. /**
  529. * Store already generated class name for alias
  530. *
  531. * @param string $entityType
  532. * @param string $alias
  533. * @param string $className
  534. * @return void
  535. */
  536. protected function _pushToMap($entityType, $alias, $className)
  537. {
  538. // Load map from file if it wasn't loaded
  539. $this->_getAliasesMap();
  540. if (!isset($this->_aliasesMap[$entityType])) {
  541. $this->_aliasesMap[$entityType] = [];
  542. }
  543. if (!isset($this->_aliasesMap[$entityType][$alias])) {
  544. $this->_aliasesMap[$entityType][$alias] = $className;
  545. }
  546. }
  547. /**
  548. * Retrieve aliases to classes map if exit
  549. *
  550. * @return array
  551. */
  552. protected function _getAliasesMap()
  553. {
  554. if (null === $this->_aliasesMap) {
  555. $this->_aliasesMap = [];
  556. $map = $this->_loadMap($this->_pathToMapFile);
  557. if (!empty($map)) {
  558. $this->_aliasesMap = $this->_jsonDecode($map);
  559. }
  560. }
  561. return $this->_aliasesMap;
  562. }
  563. /**
  564. * Load aliases to classes map from file
  565. *
  566. * @param string $pathToMapFile
  567. * @return string
  568. */
  569. protected function _loadMap($pathToMapFile)
  570. {
  571. if ($this->_directory->isFile($pathToMapFile)) {
  572. return $this->_directory->readFile($pathToMapFile);
  573. }
  574. return '';
  575. }
  576. /**
  577. * @param string $data
  578. * @param string $entityType
  579. * @return string
  580. */
  581. protected function _getAliasInSerializedStringReplacement($data, $entityType = '')
  582. {
  583. $matches = $this->_parseSerializedString($data);
  584. if (isset($matches['alias']) && count($matches['alias']) > 0) {
  585. foreach ($matches['alias'] as $key => $alias) {
  586. $className = $this->_getCorrespondingClassName($alias, $entityType);
  587. if (!empty($className)) {
  588. $replaceString = sprintf(self::SERIALIZED_REPLACE_PATTERN, strlen($className), $className);
  589. $data = str_replace($matches['string'][$key], $replaceString, $data);
  590. }
  591. }
  592. }
  593. return $data;
  594. }
  595. /**
  596. * Parse class aliases from serialized string
  597. *
  598. * @param string $string
  599. * @return array
  600. */
  601. protected function _parseSerializedString($string)
  602. {
  603. if ($string && preg_match_all($this->_migrationData->getSerializedFindPattern(), $string, $matches)) {
  604. unset($matches[0], $matches[1], $matches[2]);
  605. return $matches;
  606. } else {
  607. return [];
  608. }
  609. }
  610. /**
  611. * List of correspondence between composite module aliases and module names
  612. *
  613. * @return array
  614. */
  615. public function getCompositeModules()
  616. {
  617. return $this->_compositeModules;
  618. }
  619. /**
  620. * Decodes the given $encodedValue string which is
  621. * encoded in the JSON format
  622. *
  623. * @param string $encodedValue
  624. * @param int $objectDecodeType
  625. * @return string|int|float|bool|array|null
  626. * @throws \InvalidArgumentException
  627. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  628. * @deprecated 101.0.1
  629. * @see \Magento\Framework\Module\Setup\Migration::jsonDecode
  630. */
  631. protected function _jsonDecode($encodedValue, $objectDecodeType = 1)
  632. {
  633. return $this->jsonDecode($encodedValue);
  634. }
  635. /**
  636. * Decodes the given $encodedValue string which is
  637. * encoded in the JSON format
  638. *
  639. * @param string $encodedValue
  640. * @return string|int|float|bool|array|null
  641. * @throws \InvalidArgumentException
  642. */
  643. private function jsonDecode($encodedValue)
  644. {
  645. return $this->serializer->unserialize($encodedValue);
  646. }
  647. }