Skip to content
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

NGSTACK-811 tags content command #53

Merged
merged 36 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8515255
NGSTACK-811 add tag content command
Jul 23, 2024
723eded
NGSTACK-811 fix code styling TagContentByTypesCommand.php
Jul 23, 2024
c2ccf4d
NGSTACK-811 assing tag to content in batches
Jul 23, 2024
2516943
NGSTACK-811 add empty line before return
Jul 23, 2024
76ae62d
NGSTACK-811 move field identifier logic out of loop
Jul 23, 2024
14aef55
NGSTACK-811 use sudo only for parts that require it
Jul 23, 2024
efdb0cc
NGSTACK-811 call getTag method only once
Jul 23, 2024
872ee9f
NGSTACK-811 add transaction inside loop
Jul 23, 2024
c45ef64
NGSTACK-811 add empty line parameters.yaml
Jul 23, 2024
8d1482b
NGSTACK-811 add empty line commands.yaml
Jul 23, 2024
2887c4c
NGSTACK-811 allow only one field identifier
Jul 23, 2024
0ab8f8e
NGSTACK-811 don't try to access input field-identifier unnecessary
Jul 23, 2024
ab29c2a
NGSTACK-811 fix code styling
Jul 23, 2024
bd3915a
NGSTACK-811 remove getTag method
Jul 24, 2024
59a7508
NGSTACK-811 add empty line between properties
Jul 24, 2024
19f0e1b
NGSTACK-811 remove column alignment in constructor
Jul 24, 2024
3dd233e
NGSTACK-811 remove input property
Jul 24, 2024
3200afb
NGSTACK-811 inject ContentService and SearchService
Jul 24, 2024
a0d5652
NGSTACK-811 remove default field identifier
Jul 24, 2024
5d773e1
NGSTACK-811 do not stop command if field identifier is not typeof eztags
Jul 24, 2024
72dc9df
NGSTACK-811 add static to arrow function
Jul 24, 2024
ed2b379
NGSTACK-811 make method getParentLocationPrivate
Jul 24, 2024
c86eaf6
NGSTACK-811 make method getTagId
Jul 24, 2024
7660f8e
NGSTACK-811 make method getContentTypes
Jul 24, 2024
e5f99dd
NGSTACK-811 remove unnecessary hasField method
Jul 24, 2024
91ba9de
NGSTACK-811 solve php stan errors
Jul 24, 2024
b54b2e4
NGSTACK-811 add return type to getContentTypes method
Jul 24, 2024
243b184
NGSTACK-811 fix code styling
Jul 24, 2024
b696439
NGSTACK-811 move transaction out of batch level
Jul 25, 2024
fbca72c
NGSTACK-811 remove "Input" suffix from variables
Jul 25, 2024
80319d9
NGSTACK-811 make argument of parseCommaDelimited method not nullable
Jul 25, 2024
3a34186
NGSTACK-811 move transaction to content level
Jul 25, 2024
b4de0ec
NGSTACK-811 fix code styling
Jul 25, 2024
0308d30
NGSTACK-811 move transaction commit inside try and skip content that …
Jul 25, 2024
c7b85f0
NGSTACK-811 fix code styling
Jul 25, 2024
ce20a57
NGSTACK-811 add use count
Jul 25, 2024
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
181 changes: 181 additions & 0 deletions bundle/Command/TagContentByTypesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

declare(strict_types=1);

namespace Netgen\Bundle\SiteBundle\Command;

use Ibexa\Contracts\Core\Repository\ContentService;
use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Contracts\Core\Repository\Values\Content\Query;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
use Ibexa\Core\Repository\SearchService;
use Netgen\TagsBundle\API\Repository\TagsService;
use Netgen\TagsBundle\Core\FieldType\Tags\Value as TagFieldValue;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;

use function array_filter;
use function array_map;
use function array_unique;
use function array_values;
use function count;
use function explode;
use function sprintf;
use function trim;

final class TagContentByTypesCommand extends Command
{
private SymfonyStyle $style;

public function __construct(
private Repository $repository,
private ContentService $contentService,
private SearchService $searchService,
private TagsService $tagsService,
) {
// Parent constructor call is mandatory for commands registered as services
parent::__construct();
}

protected function configure(): void
{
$this->setDescription('Add tags to content')
->addOption('parent-location', null, InputOption::VALUE_REQUIRED)
->addOption('content-types', null, InputOption::VALUE_REQUIRED)
->addOption('tag-id', null, InputOption::VALUE_REQUIRED)
->addOption('field-identifier', null, InputOption::VALUE_REQUIRED);
}

protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->style = new SymfonyStyle($input, $output);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$query = new Query();
$query->filter = new Criterion\LogicalAnd([
new Criterion\ParentLocationId($this->getParentLocation((int) $input->getOption('parent-location'))),
new Criterion\ContentTypeIdentifier($this->getContentTypes($input->getOption('content-types'))),
]);

$batchSize = 50;

$searchResults = $this->searchService->findContent($query);
$totalResults = $searchResults->totalCount ?? 0;

$this->style->newLine();
$this->style->progressStart($totalResults);

$fieldIdentifier = $input->getOption('field-identifier');

for ($offset = 0; $offset < $totalResults; $offset += $batchSize) {
$query->offset = $offset;
$query->limit = $batchSize;
$searchResults = $this->searchService->findContent($query);

foreach ($searchResults->searchHits as $searchHit) {
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */
$content = $searchHit->valueObject;

if ($content->getField($fieldIdentifier) === null) {
continue;
}

if (!$content->getField($fieldIdentifier)->value instanceof TagFieldValue) {
$this->style->warning(sprintf('Field with identifier %s must be a type of eztags', $fieldIdentifier));

continue;
}

$tag = $this->tagsService->loadTag($this->getTagId((int) $input->getOption('tag-id')));
$alreadyAssignedTags = $content->getFieldValue($fieldIdentifier)->tags;
$targetTagAlreadyAssigned = array_filter($alreadyAssignedTags, static fn ($alreadyAssignedTag) => $tag->id === $alreadyAssignedTag->id);

if (count($targetTagAlreadyAssigned) > 0) {
continue;
}

$tagsToAssign = $alreadyAssignedTags;
$tagsToAssign[] = $tag;

$this->repository->sudo(
function () use ($content, $fieldIdentifier, $tagsToAssign): void {
$this->repository->beginTransaction();

try {
$contentDraft = $this->contentService->createContentDraft($content->contentInfo);
$contentUpdateStruct = $this->contentService->newContentUpdateStruct();

$contentUpdateStruct->setField($fieldIdentifier, new TagFieldValue($tagsToAssign));

$this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct);
$this->contentService->publishVersion($contentDraft->versionInfo);

$this->repository->commit();
} catch (Throwable $t) {
$this->style->error($t->getMessage());

$this->repository->rollback();
}
},
);
$this->style->progressAdvance();
}
}

$this->style->progressFinish();
$this->style->success('Tags assigned successfully');

return Command::SUCCESS;
}

private function getParentLocation(int $parentLocation): int
{
if ($parentLocation < 1) {
throw new InvalidOptionException(
sprintf("Argument --parent-location must be an integer > 0, you provided '%s'", $parentLocation),
);
}

return $parentLocation;
}

/**
* @return array<string>
*/
private function getContentTypes(string $contentTypes): array
{
return $this->parseCommaDelimited($contentTypes);
}

private function getTagId(int $tagId): int
{
if ($tagId < 1) {
throw new InvalidOptionException(
sprintf("Argument --tag-id must be an integer > 0, you provided '%s'", $tagId),
);
}

return $tagId;
}

/**
* @return array<string>
*/
private function parseCommaDelimited(string $value): array
{
$value = trim($value);

if ($value === '') {
return [];
}

return array_values(array_unique(array_filter(array_map('trim', explode(',', $value)))));
}
}
10 changes: 10 additions & 0 deletions bundle/Resources/config/services/commands.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,13 @@ services:
- "@?profiler"
tags:
- { name: console.command, command: 'ngsite:profiler:clear-cache' }

ngsite.command.tag_content:
class: Netgen\Bundle\SiteBundle\Command\TagContentByTypesCommand
arguments:
- "@ibexa.api.repository"
- "@ibexa.api.service.content"
- "@netgen.ibexa_site_api.repository.filtering_search_service"
- "@netgen_tags.api.service.tags"
tags:
- { name: console.command, command: 'ngsite:content:tag-content' }
Loading