Skip to content

Commit

Permalink
Merge pull request #1 from netgen/NGSTACK-842-dev-branch
Browse files Browse the repository at this point in the history
Netgen ibexa scheduled visibility
  • Loading branch information
petarjakopec authored Jun 26, 2024
2 parents 6f455e8 + 6d7a8fe commit 70c3465
Show file tree
Hide file tree
Showing 45 changed files with 3,006 additions and 1 deletion.
33 changes: 33 additions & 0 deletions .github/workflows/tests.yaml
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
5 changes: 5 additions & 0 deletions .gitignore
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
56 changes: 56 additions & 0 deletions .php-cs-fixer.php
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)
;
339 changes: 339 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion README.md
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],
}
```
251 changes: 251 additions & 0 deletions bundle/Command/ScheduledVisibilityUpdateCommand.php
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];
}
}
Loading

0 comments on commit 70c3465

Please sign in to comment.