-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from netgen/NGSTACK-842-dev-branch
Netgen ibexa scheduled visibility
- Loading branch information
Showing
45 changed files
with
3,006 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: Tests | ||
|
||
on: | ||
push: | ||
branches: | ||
- 'master' | ||
- '[0-9].[0-9]+' | ||
pull_request: ~ | ||
|
||
jobs: | ||
tests: | ||
name: ${{ matrix.php }} / ${{ matrix.phpunit }} | ||
runs-on: ubuntu-latest | ||
|
||
strategy: | ||
fail-fast: false | ||
matrix: | ||
php: ['8.1'] | ||
phpunit: ['phpunit.xml', 'phpunit-integration-legacy.xml'] | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: shivammathur/setup-php@v2 | ||
with: | ||
php-version: ${{ matrix.php }} | ||
coverage: none | ||
|
||
- run: composer --version | ||
- run: composer validate --strict | ||
|
||
- run: composer update --prefer-dist | ||
|
||
- run: vendor/bin/phpunit -c ${{ matrix.phpunit }} --colors=always |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/vendor/ | ||
composer.lock | ||
.phpunit.result.cache | ||
.php-cs-fixer.cache | ||
var |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
|
||
$finder = PhpCsFixer\Finder::create() | ||
->in(__DIR__) | ||
->exclude(['var', 'vendor']) | ||
->files()->name('*.php') | ||
; | ||
|
||
$config = new PhpCsFixer\Config(); | ||
return $config | ||
->setRules([ | ||
'@PSR12' => true, | ||
'@PSR12:risky' => true, | ||
'@PhpCsFixer' => true, | ||
'@PhpCsFixer:risky' => true, | ||
|
||
// Overrides for rules included in PhpCsFixer rule sets | ||
'concat_space' => ['spacing' => 'one'], | ||
'method_chaining_indentation' => false, | ||
'multiline_whitespace_before_semicolons' => false, | ||
'native_function_invocation' => ['include' => ['@all']], | ||
'no_superfluous_phpdoc_tags' => false, | ||
'no_unset_on_property' => false, | ||
'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], | ||
'php_unit_internal_class' => false, | ||
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], | ||
'php_unit_test_class_requires_covers' => false, | ||
'phpdoc_align' => false, | ||
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], | ||
'single_line_comment_style' => false, | ||
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments']], | ||
'yoda_style' => false, | ||
'php_unit_strict' => false, | ||
'php_unit_test_annotation' => false, | ||
|
||
// Additional rules | ||
'return_assignment' => false, | ||
'date_time_immutable' => true, | ||
'declare_strict_types' => true, | ||
'global_namespace_import' => [ | ||
'import_classes' => null, | ||
'import_constants' => true, | ||
'import_functions' => true, | ||
], | ||
'list_syntax' => ['syntax' => 'short'], | ||
'heredoc_indentation' => ['indentation' => 'same_as_start'], | ||
'mb_str_functions' => true, | ||
'native_constant_invocation' => true, | ||
'nullable_type_declaration_for_default_null_value' => true, | ||
'static_lambda' => true, | ||
'ternary_to_null_coalescing' => true, | ||
'use_arrow_functions' => true, | ||
]) | ||
->setRiskyAllowed(true) | ||
->setFinder($finder) | ||
; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,24 @@ | ||
# ibexa-scheduled-visibility | ||
# Netgen's Ibexa Scheduled Visibility | ||
|
||
**Netgen's Ibexa Scheduled Visibility** enables scheduled publishing of content | ||
based on ``publish_from`` and ``publish_to`` fields and further configuration. | ||
|
||
## Installation | ||
|
||
To install Ibexa CMS Scheduled Visibility first add it as a dependency to your project: | ||
|
||
```sh | ||
composer require netgen/ibexa-scheduled-visibility:^1.0 | ||
``` | ||
|
||
Once the added dependency is installed, activate the bundle in `config/bundles.php` file by adding it to the returned array, together with other required bundles: | ||
|
||
```php | ||
<?php | ||
|
||
return [ | ||
//... | ||
|
||
Netgen\Bundle\IbexaScheduledVisibilityBundle\NetgenIbexaScheduledVisibilityBundle::class => ['all' => true], | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Netgen\Bundle\IbexaScheduledVisibilityBundle\Command; | ||
|
||
use Doctrine\DBAL\Connection; | ||
use Doctrine\DBAL\Query\QueryBuilder; | ||
use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException; | ||
use Ibexa\Contracts\Core\Repository\Repository; | ||
use Ibexa\Contracts\Core\Repository\Values\Content\Language; | ||
use Netgen\Bundle\IbexaScheduledVisibilityBundle\Core\Configuration; | ||
use Netgen\Bundle\IbexaScheduledVisibilityBundle\Core\Exception\InvalidStateException; | ||
use Netgen\Bundle\IbexaScheduledVisibilityBundle\Core\ScheduledVisibilityService; | ||
use Pagerfanta\Doctrine\DBAL\QueryAdapter; | ||
use Pagerfanta\Pagerfanta; | ||
use Psr\Log\LoggerInterface; | ||
use Psr\Log\NullLogger; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Helper\ProgressBar; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Console\Question\ConfirmationQuestion; | ||
use Symfony\Component\Console\Style\SymfonyStyle; | ||
|
||
use function count; | ||
use function sprintf; | ||
|
||
final class ScheduledVisibilityUpdateCommand extends Command | ||
{ | ||
private SymfonyStyle $style; | ||
private array $languageCache = []; | ||
|
||
public function __construct( | ||
private readonly Repository $repository, | ||
private readonly ScheduledVisibilityService $scheduledVisibilityService, | ||
private readonly Configuration $configurationService, | ||
private readonly Connection $connection, | ||
private readonly LoggerInterface $logger = new NullLogger(), | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void | ||
{ | ||
$this->setDescription( | ||
'Updates content visibility based on publish_from and publish_to attributes and configuration.', | ||
); | ||
$this->addOption( | ||
'limit', | ||
'l', | ||
InputOption::VALUE_OPTIONAL, | ||
'Number of content objects to process in a single iteration', | ||
1024, | ||
); | ||
} | ||
|
||
protected function initialize(InputInterface $input, OutputInterface $output): void | ||
{ | ||
$this->style = new SymfonyStyle($input, $output); | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
$this->style->info( | ||
'This command fetches content and updates visibility based on its schedule from publish_from and publish_to fields.', | ||
); | ||
|
||
$question = new ConfirmationQuestion( | ||
'Continue with this action?', | ||
true, | ||
'/^(y)/i', | ||
); | ||
|
||
if (!$this->style->askQuestion($question)) { | ||
$this->style->success('Aborted'); | ||
|
||
return Command::SUCCESS; | ||
} | ||
|
||
if (!$this->configurationService->isEnabled()) { | ||
$this->style->warning('Scheduled visibility mechanism is disabled.'); | ||
|
||
return Command::FAILURE; | ||
} | ||
|
||
$allContentTypes = $this->configurationService->isAllContentTypes(); | ||
$allowedContentTypes = $this->configurationService->getAllowedContentTypes(); | ||
|
||
if (!$allContentTypes && count($allowedContentTypes) === 0) { | ||
$this->style->warning('No content types configured for scheduled visibility mechanism.'); | ||
|
||
return Command::FAILURE; | ||
} | ||
|
||
$pager = $this->getPager($allContentTypes, $allowedContentTypes); | ||
|
||
if ($pager->getNbResults() === 0) { | ||
$this->style->info('No content found'); | ||
|
||
return Command::FAILURE; | ||
} | ||
|
||
$limit = $input->getOption('limit'); | ||
$offset = 0; | ||
|
||
$progressBar = $this->style->createProgressBar($pager->getNbResults()); | ||
$progressBar->setFormat('debug'); | ||
$progressBar->start(); | ||
|
||
$results = $pager->getAdapter()->getSlice($offset, $limit); | ||
while (count($results) > 0) { | ||
$this->processResults($results, $progressBar); | ||
$offset += $limit; | ||
$results = $pager->getAdapter()->getSlice($offset, $limit); | ||
} | ||
|
||
$progressBar->finish(); | ||
|
||
$this->style->info('Done.'); | ||
|
||
return Command::SUCCESS; | ||
} | ||
|
||
private function processResults(array $results, ProgressBar $progressBar): void | ||
{ | ||
foreach ($results as $result) { | ||
try { | ||
$languageId = $result['initial_language_id']; | ||
$language = $this->loadLanguage($languageId); | ||
} catch (NotFoundException $exception) { | ||
$this->logger->error( | ||
sprintf( | ||
'Language with id #%d does not exist: %s', | ||
$languageId, | ||
$exception->getMessage(), | ||
), | ||
); | ||
|
||
$progressBar->advance(); | ||
|
||
continue; | ||
} | ||
|
||
try { | ||
$contentId = $result['id']; | ||
|
||
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ | ||
$content = $this->repository->sudo( | ||
fn () => $this->repository->getContentService()->loadContent( | ||
$contentId, | ||
[$language->getLanguageCode()], | ||
), | ||
); | ||
} catch (NotFoundException $exception) { | ||
$this->logger->error( | ||
sprintf( | ||
'Content with id #%d does not exist: %s', | ||
$contentId, | ||
$exception->getMessage(), | ||
), | ||
); | ||
|
||
$progressBar->advance(); | ||
|
||
continue; | ||
} | ||
|
||
try { | ||
$this->scheduledVisibilityService->updateVisibilityIfNeeded($content); | ||
} catch (InvalidStateException $exception) { | ||
$this->logger->error($exception->getMessage()); | ||
} | ||
|
||
$progressBar->advance(); | ||
} | ||
} | ||
|
||
private function getQueryBuilder(): QueryBuilder | ||
{ | ||
$query = $this->connection->createQueryBuilder(); | ||
$query | ||
->select('id', 'initial_language_id') | ||
->from('ezcontentobject') | ||
->where('published != :unpublished') | ||
->orderBy('id', 'ASC') | ||
->setParameter('unpublished', 0); | ||
|
||
return $query; | ||
} | ||
|
||
private function applyContentTypeLimit(QueryBuilder $query, array $contentTypeIds): void | ||
{ | ||
$query->where( | ||
$query->expr()->in('contentclass_id', ':content_type_ids'), | ||
)->setParameter('content_type_ids', $contentTypeIds, Connection::PARAM_INT_ARRAY); | ||
} | ||
|
||
private function getPager(bool $allContentTypes, array $allowedContentTypes): Pagerfanta | ||
{ | ||
$query = $this->getQueryBuilder(); | ||
|
||
$contentTypeIds = []; | ||
if (!$allContentTypes && count($allowedContentTypes) > 0) { | ||
foreach ($allowedContentTypes as $allowedContentType) { | ||
try { | ||
$contentTypeIds[] = $this->repository->getContentTypeService()->loadContentTypeByIdentifier($allowedContentType)->id; | ||
} catch (NotFoundException $exception) { | ||
$this->logger->error( | ||
sprintf( | ||
"Content type with identifier '%s' does not exist: %s", | ||
$allowedContentType, | ||
$exception->getMessage(), | ||
), | ||
); | ||
|
||
continue; | ||
} | ||
} | ||
} | ||
|
||
if (count($contentTypeIds) > 0) { | ||
$this->applyContentTypeLimit($query, $contentTypeIds); | ||
} | ||
|
||
$countQueryBuilderModifier = function (QueryBuilder $queryBuilder) use ($contentTypeIds): void { | ||
$queryBuilder->select('COUNT(id) AS total_results') | ||
->from('ezcontentobject') | ||
->where('published != :unpublished') | ||
->setParameter('unpublished', 0) | ||
->setMaxResults(1); | ||
|
||
if (count($contentTypeIds) > 0) { | ||
$this->applyContentTypeLimit($queryBuilder, $contentTypeIds); | ||
} | ||
}; | ||
|
||
return new Pagerfanta(new QueryAdapter($query, $countQueryBuilderModifier)); | ||
} | ||
|
||
private function loadLanguage(int $id): Language | ||
{ | ||
if (!isset($this->languageCache[$id])) { | ||
$language = $this->repository->getContentLanguageService()->loadLanguageById($id); | ||
$this->languageCache[$id] = $language; | ||
} | ||
|
||
return $this->languageCache[$id]; | ||
} | ||
} |
Oops, something went wrong.