Skip to content

Commit

Permalink
[WIP][FEATURE] Synchronization status
Browse files Browse the repository at this point in the history
  • Loading branch information
christianfutterlieb committed Mar 10, 2024
1 parent c0c70f2 commit a908f80
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 1 deletion.
70 changes: 70 additions & 0 deletions Classes/Imaging/IconHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?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
*/
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 $table
* @param array $row
* @param array $status
* @param string $iconName
* @return string
*/
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;
}

$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->isSyncOk()) {
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 NOT_SYNCED = 0;
public const OUT_OF_SYNC = 1;
public const SYNC_OK = 2;

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

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

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

public function isSyncOk(): bool
{
return $this->status === self::SYNC_OK;
}
}
102 changes: 102 additions & 0 deletions Classes/Role/SynchronizationStatusFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?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
*/
class SynchronizationStatusFactory implements SynchronizationStatusFactoryInterface
{
public function __construct(
protected Loader $loader,
protected readonly Formatter $formatter,
protected DefinitionFactory $definitionFactory
) {}

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
* @return SynchronizationStatus
*/
public function createFromBackendGroupRecord(array $backendGroupRecord): SynchronizationStatus
{
$identifier = $this->getIdentifierFromBackendGroupRecord($backendGroupRecord);

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

// 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::OUT_OF_SYNC;
if ($this->areRoleDefinitionsEqual($definitionFromConfiguration, $definitionFromDatabase)) {
$status = SynchronizationStatus::SYNC_OK;
}

return new SynchronizationStatus($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
* @return string
*/
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'];
}
}
32 changes: 32 additions & 0 deletions Classes/Role/SynchronizationStatusFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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
{
/**
* @param int $backendGroupuid
* @return SynchronizationStatus
*/
public function createFromBackendGroupUid(int $backendGroupuid): SynchronizationStatus;

/**
* @param array $backendGroupRecord
* @return SynchronizationStatus
*/
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
1 change: 1 addition & 0 deletions Configuration/TCA/Overrides/be_groups.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

use AawTeam\BackendRoles\FormEngine\BackendRoleSelectItemsProcessor;
use AawTeam\BackendRoles\Imaging\IconHandler;

// Add columns
$columns = [
Expand Down
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::NOT_SYNCED)
);
self::assertInstanceOf(
SynchronizationStatus::class,
new SynchronizationStatus(SynchronizationStatus::OUT_OF_SYNC)
);
self::assertInstanceOf(
SynchronizationStatus::class,
new SynchronizationStatus(SynchronizationStatus::SYNC_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::NOT_SYNCED))->isSynced()
);
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::NOT_SYNCED))->isOutOfSync()
);
self::assertFalse(
(new SynchronizationStatus(SynchronizationStatus::NOT_SYNCED))->isSyncOk()
);

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

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

/**
* @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/visualize the synchronization status of be_groups records
showSynchronizationStatus = 1
Loading

0 comments on commit a908f80

Please sign in to comment.