Skip to content

Commit

Permalink
✨ inital commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Kanti committed Aug 3, 2024
0 parents commit 1b92718
Show file tree
Hide file tree
Showing 24 changed files with 10,694 additions and 0 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/tasks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Tasks

on: push

jobs:
lint-php:
name: "php: ${{ matrix.php }} TYPO3: ${{ matrix.typo3 }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ '8.2', '8.3' ]
typo3: [ '11', '12', '13' ]
steps:
- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
# - uses: mirromutth/[email protected]
# with:
# mysql version: '5.7'
# mysql database: 'typo3_test'
# mysql root password: 'root'
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/.composer/cache/files
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.php }}-composer-
- run: rm composer.lock
- run: composer require typo3/cms-core="^${{ matrix.typo3 }}" --dev --ignore-platform-req=php+
- run: composer install --no-interaction --no-progress --ignore-platform-req=php+
- run: ./vendor/bin/grumphp run --ansi
#- run: composer test
#- run: jq 'del(.logs.html)' infection.json > infection.json.new && mv infection.json.new infection.json
#- run: composer infection
#- uses: codecov/codecov-action@v3
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# file: Resources/Public/test-result/clover.xml
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/
/var/
/public/
63 changes: 63 additions & 0 deletions Classes/Command/ShowFeatureFlagsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Andersundsehr\Unleash\Command;

use Andersundsehr\Unleash\Service\FeatureService;
use Override;
use Andersundsehr\Unleash\Typo3UnleashContextProvider;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Unleash\Client\Configuration\Context;

class ShowFeatureFlagsCommand extends Command
{
public function __construct(
private readonly Typo3UnleashContextProvider $typo3UnleashContextProvider,
private readonly FeatureService $featureService,
) {
parent::__construct();
}

#[Override]
protected function configure(): void
{
$features = $this->featureService->getAllFeatures();

$this->setDescription('show current feature flags for this instance');
$this->addArgument('features', InputArgument::IS_ARRAY, 'Feature flag to check eg. some-feature-flag', $features);

$this->addOption('iterations', 'i', InputArgument::OPTIONAL, 'Number of iterations to check the feature flag', 20);

$this->addOption('user', 'u', InputArgument::OPTIONAL, 'User ID to check the feature flag', '');
$this->addOption('ip', '', InputArgument::OPTIONAL, 'IP address to check the feature flag', '');
$this->addOption('host', '', InputArgument::OPTIONAL, 'Hostname to check the feature flag', '');
$this->addOption('environment', 'e', InputArgument::OPTIONAL, 'Environment to check the feature flag (will not change the used environment in unleash, that is decided by the auth token)', '');
}

#[Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
$features = $input->getArgument('features');
$iterations = (int)$input->getOption('iterations');
$context = $this->createContext($input);

foreach ($this->featureService->analyticsForFeatures($features, $iterations, $context) as $feature => $message) {
$output->writeln($feature . ': ' . $message);
}

return Command::SUCCESS;
}

private function createContext(InputInterface $input): Context
{
return $this->typo3UnleashContextProvider->getContext()
->setCurrentUserId($input->getOption('user') ?: null)
->setIpAddress($input->getOption('ip') ?: null)
->setHostname($input->getOption('host') ?: null)
->setEnvironment($input->getOption('environment') ?: null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Andersundsehr\Unleash\ConfigurationModuleProvider;

use Andersundsehr\Unleash\Service\FeatureService;
use Andersundsehr\Unleash\Typo3UnleashContextProvider;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\AbstractProvider;

#[AutoconfigureTag(
name: 'lowlevel.configuration.module.provider',
attributes: [
'identifier' => 'unleash',
'label' => 'EXT:unleash - Features',
]
)]
final class UnleashConfigurationModuleProvider extends AbstractProvider
{
public const ITERATIONS = 10;

public function __construct(
private readonly FeatureService $featureService,
private readonly Typo3UnleashContextProvider $typo3UnleashContextProvider,
private readonly ExtensionConfiguration $extensionConfiguration,
) {
}

/**
* @return array<string, mixed>
*/
public function getConfiguration(): array
{
$context = $this->typo3UnleashContextProvider->getContext()
->setIpAddress(null)
->setCurrentUserId(null);

$analyticsForAll = $this->featureService->analyticsForAll(self::ITERATIONS, $context);
$features = array_map(strip_tags(...), iterator_to_array($analyticsForAll));
return [
'title' => 'Features: ' . count($features),
'description' => 'List of all features and their analytics (iterations: ' . self::ITERATIONS . ')',
'important' => 'Context fields that are ignored for this overview: userId, clientIp',
'features' => $features,
'configuration' => $this->extensionConfiguration->get('unleash'),
'context' => $context,
];
}
}
17 changes: 17 additions & 0 deletions Classes/Event/UnleashBuilderBeforeBuildEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Andersundsehr\Unleash\Event;

use Unleash\Client\UnleashBuilder;

/**
* Event that is dispatched right before the UnleashBuilder is built (`->build()`).
*/
final class UnleashBuilderBeforeBuildEvent
{
public function __construct(public UnleashBuilder $builder)
{
}
}
18 changes: 18 additions & 0 deletions Classes/Event/UnleashContextCreatedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Andersundsehr\Unleash\Event;

use Unleash\Client\Configuration\Context;

/**
* Event that is dispatched right after the UnleashContext is created with all the default values.
* will be called multiple times, once per `->isEnabled` or `->getVariant` call.
*/
final class UnleashContextCreatedEvent
{
public function __construct(public Context $context)
{
}
}
21 changes: 21 additions & 0 deletions Classes/Event/UnleashCustomContextEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Andersundsehr\Unleash\Event;

/**
* This event is dispatched when the Unleash context is created.
* use this if you only want to overwrite or add customContext data
* if you want to change anything else, use the `UnleashContextCreatedEvent`
*/
final class UnleashCustomContextEvent
{
public function __construct(
/**
* @var array<string, string|null>
*/
public array $customContext
) {
}
}
106 changes: 106 additions & 0 deletions Classes/Service/FeatureService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace Andersundsehr\Unleash\Service;

use Andersundsehr\Unleash\UnleashFactory;
use Generator;
use Unleash\Client\Configuration\Context;
use Unleash\Client\Unleash;

final readonly class FeatureService
{
public function __construct(
private UnleashFactory $unleashFactory,
private Unleash $unleash,
) {
}

/**
* @return list<string>
*/
public function getAllFeatures(): array
{
$features = [];
foreach ($this->unleashFactory->getUnleashBuilder()->buildRepository()->getFeatures() as $feature) {
$features[] = $feature->getName();
}

return $features;
}

/**
* @return Generator<string, string>
*/
public function analyticsForAll(int $iterations = 10, ?Context $context = null): Generator
{
return $this->analyticsForFeatures($this->getAllFeatures(), $iterations, $context);
}

/**
* @param list<string> $features
* @return Generator<string, string>
*/
public function analyticsForFeatures(array $features, int $iterations = 10, ?Context $context = null): Generator
{
foreach ($features as $feature) {
yield $feature => $this->analyticsForFeature($feature, $iterations, $context);
}
}

private function analyticsForFeature(string $feature, int $iterations, ?Context $context = null): string
{
$count = 0;
$variantValues = [];
for ($i = 0; $i < $iterations; $i++) {
$count += (int)$this->unleash->isEnabled($feature, $context);
$variantValue = $this->unleash->getVariant($feature, $context)->getPayload()?->getValue();
if ($variantValue !== null) {
$variantValues[$variantValue] ??= 0;
$variantValues[$variantValue]++;
}
}

if ($variantValues) {
return $this->combineVariantValues($variantValues, $iterations);
}

if ($count === $iterations) {
return '<info>Enabled</info>';
}

if ($count === 0) {
return '<error>Disabled</error>';
}

return '<comment>' . $this->percent($count, $iterations) . '%</comment> enabled';
}


/**
* @param array<string, int> $variantValues
*/
private function combineVariantValues(array $variantValues, int $maxTotal): string
{
$totalCount = 0;
$messages = [];
asort($variantValues, SORT_DESC);
foreach ($variantValues as $variantValue => $count) {
$messages[] = '<info>' . $variantValue . ': ' . $this->percent($count, $maxTotal) . '%</info>';
$totalCount += $count;
}

if ($totalCount !== $maxTotal) {
$messages[] = '<error>Off: ' . $this->percent($maxTotal - $totalCount, $maxTotal) . '%</error>';
}

return implode(' ', $messages);
}


private function percent(int $count, int $maxTotal): float
{
return round($count / $maxTotal * 100, 1);
}
}
Loading

0 comments on commit 1b92718

Please sign in to comment.