Skip to content

IP-314 Expand the CMS import / export module to cover store blocks/pages #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Console/Command/ImportCmsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ImportCmsData extends \Symfony\Component\Console\Command\Command
private const INPUT_TYPE_VALUES = ['block', 'page', 'all'];
private const INPUT_KEY_IDENTIFIER = 'identifier';
private const INPUT_KEY_IMPORT_ALL = 'importAll';
private const INPUT_KEY_STORE = 'store';
private \RocketWeb\CmsImportExport\Model\Service\ImportCmsDataService $importCmsDataService;

public function __construct(
Expand Down Expand Up @@ -60,6 +61,12 @@ protected function configure()
'a',
InputOption::VALUE_NONE,
'Flag to import all files'
),
new InputOption(
self::INPUT_KEY_STORE,
's',
InputOption::VALUE_OPTIONAL,
'Specific Store Code'
)
]);
parent::configure();
Expand Down Expand Up @@ -88,7 +95,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$identifiers = explode(',', $identifiers);
}

$this->importCmsDataService->execute($types, $identifiers, $importAll);
$storeCode = empty($input->getOption(self::INPUT_KEY_STORE)) ?
null :
$input->getOption(self::INPUT_KEY_STORE);

$this->importCmsDataService->execute($types, $identifiers, $importAll, $storeCode);

return 0;
}
Expand Down
108 changes: 91 additions & 17 deletions Model/Service/ImportCmsDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace RocketWeb\CmsImportExport\Model\Service;

use Magento\Cms\Api\Data\BlockInterface;
use Magento\Cms\Api\Data\PageInterface;
use Magento\Framework\App\Filesystem\DirectoryList;

class ImportCmsDataService
Expand All @@ -39,7 +41,9 @@ public function __construct(
\Magento\Framework\Filesystem\DirectoryList $directoryList,
\Magento\Framework\Filesystem $filesystem,
\Magento\Framework\Serialize\SerializerInterface $serializer,
\Magento\Store\Api\StoreRepositoryInterface $storeRepository
\Magento\Store\Api\StoreRepositoryInterface $storeRepository,
private readonly \Magento\Cms\Api\GetBlockByIdentifierInterface $getBlockByIdentifier,
private readonly \Magento\Cms\Api\GetPageByIdentifierInterface $getPageByIdentifier
) {
$this->pageRepository = $pageRepository;
$this->blockRepository = $blockRepository;
Expand All @@ -51,7 +55,7 @@ public function __construct(
$this->storeRepository = $storeRepository;
}

public function execute(array $types, ?array $identifiers, bool $importAll)
public function execute(array $types, ?array $identifiers, bool $importAll, ?string $storeCode = null)
{
$workingDirPath = 'sync_cms_data';

Expand All @@ -70,9 +74,9 @@ public function execute(array $types, ?array $identifiers, bool $importAll)
}

if ($type == 'block') {
$this->importBlocks($typeDirPath, $identifiers);
$this->importBlocks($typeDirPath, $identifiers, $storeCode);
} else if ($type == 'page') {
$this->importPages($typeDirPath, $identifiers);
$this->importPages($typeDirPath, $identifiers, $storeCode);
}
}
}
Expand All @@ -97,7 +101,7 @@ private function getStoreIds($storeCodes): array
return $storeIds;
}

private function importBlocks(string $dirPath, ?array $identifiers): void
private function importBlocks(string $dirPath, ?array $identifiers, ?string $storeCode = null): void
{
$filePaths = $this->directoryRead->read($this->varPath . $dirPath);
foreach ($filePaths as $filePath) {
Expand All @@ -112,12 +116,17 @@ private function importBlocks(string $dirPath, ?array $identifiers): void
// If we have a list of items, we skip if its not in the list
continue;
}

try {
$block = $this->blockRepository->getById($identifier);
} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
$block = $this->blockFactory->create();
if ($storeCode !== null && ($this->getStoreCode($filePath) !== $storeCode)) {
// Skip identifiers not assigned to specific store when storeCode parameter is set
echo sprintf(
'Skipping identifier %s because requested update only for store %s %s',
$identifier,
$storeCode,
PHP_EOL
);
continue;
}

$content = $this->directoryRead->readFile($filePath);
$jsonData = $this->directoryRead->readFile(str_replace('.html', '.json', $filePath));
$jsonData = $this->serializer->unserialize($jsonData);
Expand All @@ -128,6 +137,13 @@ private function importBlocks(string $dirPath, ?array $identifiers): void
'is_active' => $block->isActive()
];*/
$storeIds = $this->getStoreIds($jsonData['stores']);
try {
$block = $this->getBlockByIdentifier->execute($identifier, (int)reset($storeIds));
$this->validateStoreAssociation($filePath, $block, $storeIds, 'Block');
} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
$block = $this->blockFactory->create();
}

$block->setTitle($jsonData['title']);
$block->setContent($content);
$block->setIdentifier($jsonData['identifier']);
Expand All @@ -145,7 +161,7 @@ private function importBlocks(string $dirPath, ?array $identifiers): void
}
}

private function importPages(string $dirPath, ?array $identifiers): void
private function importPages(string $dirPath, ?array $identifiers, ?string $storeCode = null): void
{
$filePaths = $this->directoryRead->read($this->varPath . $dirPath);
foreach ($filePaths as $filePath) {
Expand All @@ -155,23 +171,35 @@ private function importPages(string $dirPath, ?array $identifiers): void
}
$identifier = str_replace($dirPath, '', $filePath);
$identifier = str_replace('.html', '', $identifier);
$identifier = substr_replace($identifier, '', strpos($identifier, '---'));
$identifier = substr_replace($identifier, '', strrpos($identifier, '---'));
$identifier = str_replace('---', '/', $identifier);
$identifier = str_replace('_html', '.html', $identifier);
if ($identifiers !== null && !in_array($identifier, $identifiers)) {
// If we have a list of items, we skip if its not in the list
continue;
}

try {
$page = $this->pageRepository->getById($identifier);
} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
$page = $this->pageFactory->create();
if ($storeCode !== null && ($this->getStoreCode($filePath) !== $storeCode)) {
// Skip identifiers not assigned to specific store when storeCode parameter is set
echo sprintf(
'Skipping identifier %s because requested update only for store %s %s',
$identifier,
$storeCode,
PHP_EOL
);
continue;
}

$content = $this->directoryRead->readFile($filePath);
$jsonData = $this->directoryRead->readFile(str_replace('.html', '.json', $filePath));
$jsonData = $this->serializer->unserialize($jsonData);
$storeIds = $this->getStoreIds($jsonData['stores']);
try {
$page = $this->getPageByIdentifier->execute($identifier, (int)reset($storeIds));
$this->validateStoreAssociation($filePath, $page, $storeIds, 'Page');
} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
$page = $this->pageFactory->create();
}
/*$jsonContent = [
'title' => $page->getTitle(),
'is_active' => $page->isActive(),
Expand All @@ -193,8 +221,54 @@ private function importPages(string $dirPath, ?array $identifiers): void
try {
$this->pageRepository->save($page);
} catch (\Exception $exception) {
echo $exception->getMessage() . ' | Block ID: ' . $identifier . "\n";
echo $exception->getMessage() . ' | Page ID: ' . $identifier . "\n";
}
}
}

/**
* We are validating here is store association is correct
* string $filePath - HTML filename, may contain either store code or _all_
* BlockInterface | PageInterface $entity - either block or page if already exists
* array $storeIds - array of stores to associate from JSON file
* string $entityType - either "block" or "page", for accurate messaging
*
* We load store by store code specified in $filePath
* Further we validate it against the data we have in JSON and if currently existing block/page
*/
private function validateStoreAssociation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docblocks, thanks! also add a small readme of how this should work from top to bottom using the store parameter, like how to export and how to import. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if i was confusing about the README, i actually meant, create a small docblock comment but add a .README file to the module which explains how the store parameter works (not in the docblocks :P )

string $filePath,
BlockInterface | PageInterface $entity,
array $storeIds,
string $entityType
) : void {
$exceptionMessage = sprintf('%s with path %s has inconsistent store data', $entityType, $filePath);
if (count($storeIds) > 1) {
throw new \LogicException($exceptionMessage);
}
$storeCode = $this->getStoreCode($filePath);
$storeId = (int)reset($storeIds);
$currentStoreIds = $entity->getStoreId();
if ($storeCode === '_all_') {
if ($storeId !== 0 || count($currentStoreIds) > 1 || (int)reset($currentStoreIds) !== 0) {
throw new \LogicException($exceptionMessage);
}
return ;
}
$store = $this->storeRepository->get($storeId);
if ($store->getCode() !== $storeCode) {
throw new \LogicException($exceptionMessage);
}

if (array_diff($currentStoreIds, $storeIds) !== []) {
throw new \LogicException($exceptionMessage);
}
}

private function getStoreCode(string $filePath) : string
{
$storeCode = str_replace('.html', '', $filePath);
$storeCode = substr($storeCode, strrpos($storeCode, '---') + 3);
return $storeCode;
}
}
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,21 @@ Import cms pages/blocks from var/sync_cms_data
Options:
-t, --type=TYPE Which type are we importing - block/page/all
-i, --identifier[=IDENTIFIER] identifier to process (one or CSV list)
-a, --importAll Flag to import all files
-s, --store[STORE_CODE] Store code to process only pages/blocks specific to this store
```

This command works by using files in `var/sync_cms_data/cms/` path. As you can see from the options, we need to define:
- type - which can be CMS block, CMS page or both - **required**
- type - which can be CMS block or CMS page - **required**
- identifier - either a CMS block or CMS page identifier - **optional**

There are optional parameters:
- importAll - when identifiers not specified we'll import all blocks or pages
- store - store code (like default) to import block(s)/pages(s) only for specific store
With the combination of these two, we can **import**:
- all CMS content (using --type=all)
- all CMS pages (using --type=page)
- all CMS blocks (using --type=block)
- specific CMS page or pages (using --type=page --identifier=about-us.html,homepage-new)
- all CMS pages (using --type=page and importAll)
- all CMS blocks (using --type=block and importAll)
- specific CMS page or pages (using --type=page --identifier=about-us,homepage-new)
- specific CMS block or blocks (using --type=block --identifier=who-are-we,homepage-carousel)

- specific CMS page by store (using --type=page --identifier=about-us-default --store=default)
Once you execute the command, the content will be created/updated in Magento Admin.
By executing `php bin/magento cache:flush` you should be able to see the updated CMS content on frontend also!
Loading