Skip to content

Commit

Permalink
[FEATURE] Add synchronization status visualization
Browse files Browse the repository at this point in the history
Resolves #6
  • Loading branch information
christianfutterlieb committed May 3, 2024
1 parent 0532e50 commit 4fe925b
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 0 deletions.
72 changes: 72 additions & 0 deletions Classes/Imaging/IconHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace AawTeam\BackendRoles\Imaging;

/*
* Copyright by Agentur am Wasser | Maeder & Partner AG
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

use AawTeam\BackendRoles\Role\SynchronizationStatus;
use AawTeam\BackendRoles\Role\SynchronizationStatusFactoryInterface;
use TYPO3\CMS\Core\Utility\MathUtility;

/**
* IconHandler
*/
final class IconHandler
{
public function __construct(
protected SynchronizationStatusFactoryInterface $synchronizationStatusFactory
) {}

/**
* Implementation of IconFactory hook:
*
* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Imaging\IconFactory::class]['overrideIconOverlay']
*
* @param string[] $row
* @param array<string, bool> $status
*/
public function postOverlayPriorityLookup(string $table, array $row, array $status, string $iconName): string
{
if ($table !== 'be_groups') {
return $iconName;
}
if (!array_key_exists('uid', $row) || !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
return $iconName;
}
// Do not override an existing overlay (in the case of be_groups records, this would be only the
// 'hidden' overlay [or anything else from other hooks])
if ($iconName !== '') {
return $iconName;
}

$syncStatus = $this->synchronizationStatusFactory->createFromBackendGroupUid((int)$row['uid']);

// Return incoming value ($iconName) when null was returned by mapping
return $this->mapSynchronizationStatusToIconOverlayName($syncStatus) ?? $iconName;
}

/**
* @todo register our own icons
*/
protected function mapSynchronizationStatusToIconOverlayName(SynchronizationStatus $syncStatus): ?string
{
if ($syncStatus->isOutOfSync()) {
return 'overlay-warning';
}
if ($syncStatus->isSynced()) {
return 'overlay-approved';
}

// Default: return null
return null;
}
}
46 changes: 46 additions & 0 deletions Classes/Role/SynchronizationStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace AawTeam\BackendRoles\Role;

/*
* Copyright by Agentur am Wasser | Maeder & Partner AG
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

/**
* SynchronizationStatus
*/
final class SynchronizationStatus
{
public const NONE = 0;
public const NOK = 1;
public const OK = 2;

public function __construct(public readonly int $status)
{
if (!in_array($status, [self::NONE, self::NOK, self::OK])) {
throw new \InvalidArgumentException('Invalid status: ' . $status);
}
}

public function isAvailable(): bool
{
return $this->status !== self::NONE;
}

public function isOutOfSync(): bool
{
return $this->status === self::NOK;
}

public function isSynced(): bool
{
return $this->status === self::OK;
}
}
105 changes: 105 additions & 0 deletions Classes/Role/SynchronizationStatusFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace AawTeam\BackendRoles\Role;

/*
* Copyright by Agentur am Wasser | Maeder & Partner AG
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

use AawTeam\BackendRoles\Role\Definition\Formatter;
use AawTeam\BackendRoles\Role\Definition\Loader;
use TYPO3\CMS\Backend\Utility\BackendUtility;

/**
* SynchronizationStatusFactory
*/
final class SynchronizationStatusFactory implements SynchronizationStatusFactoryInterface
{
public function __construct(
protected Loader $loader,
protected readonly Formatter $formatter,
protected DefinitionFactory $definitionFactory
) {}

public function create(int $status): SynchronizationStatus
{
return new SynchronizationStatus($status);
}

public function createFromBackendGroupUid(int $backendGroupuid): SynchronizationStatus
{
$backendGroupRecord = BackendUtility::getRecord('be_groups', $backendGroupuid);
if ($backendGroupRecord === null) {
throw new \RuntimeException('Cannot load be_groups record with uid ' . $backendGroupuid, 1708168047);
}
return $this->createFromBackendGroupRecord($backendGroupRecord);
}

/**
* @param mixed[] $backendGroupRecord
*/
public function createFromBackendGroupRecord(array $backendGroupRecord): SynchronizationStatus
{
$identifier = $this->getIdentifierFromBackendGroupRecord($backendGroupRecord);

// Not synchronized
if ($identifier === '') {
return $this->create(SynchronizationStatus::NONE);
}
$definitions = $this->loader->getRoleDefinitions();
if (!$definitions->offsetExists($identifier)) {
return $this->create(SynchronizationStatus::NOK);
}

// be_groups record is synchronized: load definitions
/** @var Definition $definitionFromConfiguration */
$definitionFromConfiguration = $definitions->offsetGet($identifier);
$definitionFromDatabase = $this->definitionFactory->create(
array_merge(
[
'identifier' => $definitionFromConfiguration->getIdentifier(),
'title' => $definitionFromConfiguration->getTitle(),
],
$this->formatter->formatFromDbToArray($backendGroupRecord)
)
);

// Compare the definitions
$status = SynchronizationStatus::NOK;
if ($this->areRoleDefinitionsEqual($definitionFromConfiguration, $definitionFromDatabase)) {
$status = SynchronizationStatus::OK;
}

return $this->create($status);
}

protected function areRoleDefinitionsEqual(Definition $definition1, Definition $definition2): bool
{
$a = $this->formatter->formatForDatabase($definition1);
$b = $this->formatter->formatForDatabase($definition2);
return array_diff_assoc($a, $b) === [];
}

/**
* @param mixed[] $backendGroupRecord
*/
protected function getIdentifierFromBackendGroupRecord(array $backendGroupRecord): string
{
// Input validation
if (!array_key_exists('tx_backendroles_role_identifier', $backendGroupRecord)) {
throw new \InvalidArgumentException('No field "tx_backendroles_role_identifier" found in $backendGroupRecord', 1708168285);
}
if (!is_string($backendGroupRecord['tx_backendroles_role_identifier'])) {
throw new \InvalidArgumentException('Field "tx_backendroles_role_identifier" in $backendGroupRecord must be string', 1708168332);
}

return $backendGroupRecord['tx_backendroles_role_identifier'];
}
}
29 changes: 29 additions & 0 deletions Classes/Role/SynchronizationStatusFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace AawTeam\BackendRoles\Role;

/*
* Copyright by Agentur am Wasser | Maeder & Partner AG
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

/**
* SynchronizationStatusFactoryInterface
*/
interface SynchronizationStatusFactoryInterface
{
public function create(int $status): SynchronizationStatus;

public function createFromBackendGroupUid(int $backendGroupuid): SynchronizationStatus;

/**
* @param string[] $backendGroupRecord
*/
public function createFromBackendGroupRecord(array $backendGroupRecord): SynchronizationStatus;
}
3 changes: 3 additions & 0 deletions Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ services:
AawTeam\BackendRoles\FormEngine\BackendRoleSelectItemsProcessor:
public: true

AawTeam\BackendRoles\Imaging\IconHandler:
public: true

locker.backend_roles_synchronization:
class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
factory: ['@TYPO3\CMS\Core\Locking\LockFactory', 'createLocker']
Expand Down
15 changes: 15 additions & 0 deletions Documentation/Configuration/Extconf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,18 @@ Configuration options reference
$GLOBALS['EXTCONF']['backend_roles']['hideManagedBackendUserGroupColumnns'] = true;
.. confval:: showSynchronizationStatus

:type: bool
:Default: true

If set, the synchronization status of a `be_groups` record will be shown. At
the moment this results in displaying an overlay to the `be_groups` record
icon (only if there is not yet another overlay active, like the red sign for
'hidden').

.. code-block:: php
$GLOBALS['EXTCONF']['backend_roles']['showSynchronizationStatus'] = true;
100 changes: 100 additions & 0 deletions Tests/Unit/Role/SynchronizationStatusTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace AawTeam\BackendRoles\Tests\Unit\Role;

/*
* Copyright by Agentur am Wasser | Maeder & Partner AG
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

use AawTeam\BackendRoles\Role\SynchronizationStatus;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

/**
* SynchronizationStatusTest
*/
class SynchronizationStatusTest extends UnitTestCase
{
#[Test]
public function constructorAcceptsKnownStatusValue(): void
{
self::assertInstanceOf(
SynchronizationStatus::class,
new SynchronizationStatus(SynchronizationStatus::NONE)
);
self::assertInstanceOf(
SynchronizationStatus::class,
new SynchronizationStatus(SynchronizationStatus::NOK)
);
self::assertInstanceOf(
SynchronizationStatus::class,
new SynchronizationStatus(SynchronizationStatus::OK)
);
}

#[Test]
#[DataProvider('unknownConstructorStatesDataProvider')]
public function constructorFailsWithUnknownStatusValue(int $status): void
{
self::expectException(\InvalidArgumentException::class);
new SynchronizationStatus($status);
}

#[Test]
public function statusConstantIsInterpretedCorrectly(): void
{
// NOT_SYNCED
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::NONE))->isAvailable()
);
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::NONE))->isOutOfSync()
);
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::NONE))->isSynced()
);

// OUT_OF_SYNC
self::assertTrue(
(new SynchronizationStatus(SynchronizationStatus::NOK))->isAvailable()
);
self::assertTrue(
(new SynchronizationStatus(SynchronizationStatus::NOK))->isOutOfSync()
);
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::NOK))->isSynced()
);

// SYNC_OK
self::assertTrue(
(new SynchronizationStatus(SynchronizationStatus::OK))->isAvailable()
);
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::OK))->isOutOfSync()
);
self::assertTrue(
(new SynchronizationStatus(SynchronizationStatus::OK))->isSynced()
);
}

/**
* @return mixed[]
*/
public static function unknownConstructorStatesDataProvider(): array
{
return [
'negative-status' => [-2],
'one-below-known' => [-1],
'one-above-known' => [3],
'positive-status' => [4],
];
}
}
3 changes: 3 additions & 0 deletions ext_conf_template.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# cat=basic/enable/001; type=boolean; label=Hide managed columns of be_groups records in TCA
hideManagedBackendUserGroupColumnns = 1

# cat=basic/enable/002; type=boolean; label=Show the synchronization status of be_groups records
showSynchronizationStatus = 1

0 comments on commit 4fe925b

Please sign in to comment.