Skip to content

Commit

Permalink
Refactor the implementation to easily add/expand functionnality
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Dec 29, 2020
1 parent 84c2b90 commit aafbbb4
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 44 deletions.
134 changes: 92 additions & 42 deletions src/Multiavatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@

use InvalidArgumentException;
use TypeError;
use function array_combine;
use function array_fill_keys;
use function array_map;
use function array_reduce;
use function filter_var;
use function get_class;
use function gettype;
use function hash;
use function intdiv;
use function is_numeric;
use function is_object;
use function is_scalar;
use function method_exists;
Expand All @@ -27,6 +32,7 @@
use function preg_replace_callback;
use function round;
use function sprintf;
use function str_split;
use function strtoupper;
use function substr;
use function trim;
Expand All @@ -40,6 +46,7 @@ class Multiavatar
private const SVG_ELEMENT_HEAD = '<path d="m115.5 51.75a63.75 63.75 0 0 0-10.5 126.63v14.09a115.5 115.5 0 0 0-53.729 19.027 115.5 115.5 0 0 0 128.46 0 115.5 115.5 0 0 0-53.729-19.029v-14.084a63.75 63.75 0 0 0 53.25-62.881 63.75 63.75 0 0 0-63.65-63.75 63.75 63.75 0 0 0-0.09961 0z" style="fill:#000;"/>';
private const SVG_ELEMENT_BASE_STYLE_PROPERTIES = 'stroke-linecap:round;stroke-linejoin:round;stroke-width:';
private const THEME_LIST = [0 => 'A', 1 => 'B', 2 => 'C'];
private const BODY_PARTS = ['env', 'clo', 'head', 'mouth', 'eyes', 'top'];

public const DEFAULT_OPTIONS = [
'ver' => [
Expand Down Expand Up @@ -71,15 +78,17 @@ public function __invoke($avatarId, array $options = self::DEFAULT_OPTIONS): str
return '';
}

$svgElements = $this->partsToElements($this->avatarToParts($avatarId), $options);
$parts = $this->setParts($avatarId, $options);
$elements = $this->partsToElements($parts);
$elements = $this->filterElements($elements, $options);

return self::SVG_ROOT_OPEN_TAG
. $svgElements['env']
. $svgElements['head']
. $svgElements['clo']
. $svgElements['top']
. $svgElements['eyes']
. $svgElements['mouth']
. $elements['env']
. $elements['head']
. $elements['clo']
. $elements['top']
. $elements['eyes']
. $elements['mouth']
. self::SVG_ROOT_CLOSE_TAG;
}

Expand Down Expand Up @@ -108,9 +117,13 @@ private function filterOptions(array $inputOptions): array
$options['sansEnv'] = filter_var($inputOptions['sansEnv'] ?? false, FILTER_VALIDATE_BOOLEAN);

if (isset($inputOptions['ver']['part'])) {
if (!is_string($inputOptions['ver']['part']) && !is_numeric($inputOptions['ver']['part'])) {
throw new InvalidArgumentException('The version part is expected to be a scalar; '.gettype($inputOptions['ver']['part']).' was given.');
}

$part = sprintf("%'.02d", $inputOptions['ver']['part']);
if (1 !== preg_match('/^(0[0-9])|(1[0-5])$/', $part)) {
throw new InvalidArgumentException('The submitted part does not exists; expecting a value between `00` and `15`.');
throw new InvalidArgumentException('The version part does not exists; expecting a value between `00` and `15`.');
}

$options['ver']['part'] = $part;
Expand All @@ -120,8 +133,12 @@ private function filterOptions(array $inputOptions): array
return $options;
}

if (1 !== preg_match('/^([a-c])$/i', $inputOptions['ver']['theme'])) {
throw new InvalidArgumentException('The submitted theme does not exists; expecting a value between `A`, `B` and `C`.');
if (!is_string($inputOptions['ver']['theme'])) {
throw new InvalidArgumentException('The version theme is expected to be a string; '.gettype($inputOptions['ver']['theme']).' was given.');
}

if (1 !== preg_match('/^[a-c]$/i', $inputOptions['ver']['theme'])) {
throw new InvalidArgumentException('The version theme does not exists; expecting a value between `A`, `B` and `C`.');
}

$options['ver']['theme'] = strtoupper($inputOptions['ver']['theme']);
Expand All @@ -130,29 +147,54 @@ private function filterOptions(array $inputOptions): array
}

/**
* @return array<string,array{part:string,theme:string}>
* @param array{ver:array{part:string|null, theme:string|null}, sansEnv:bool} $options
*
* @return array{env:array{part:string, theme:string}, clo:array{part:string, theme:string}, head:array{part:string, theme:string}, mouth:array{part:string, theme:string}, eyes:array{part:string, theme:string}, top:array{part:string, theme:string}}
*/
private function avatarToParts(string $avatarId): array
private function setParts(string $avatarId, array $options): array
{
/** @var string $str */
$str = preg_replace("/\D/", "", hash('sha256', $avatarId));
if (isset($options['ver']['theme'], $options['ver']['part'])) {
return array_fill_keys(self::BODY_PARTS, $options['ver']);
}

/** @var string $str */
$str = preg_replace('/\D/', '', hash('sha256', $avatarId));
$hash = substr($str, 0, 12);

return array_map([$this, 'mapPart'], [
'env' => $hash[0] . $hash[1],
'clo' => $hash[2] . $hash[3],
'head' => $hash[4] . $hash[5],
'mouth' => $hash[6] . $hash[7],
'eyes' => $hash[8] . $hash[9],
'top' => $hash[10] . $hash[11],
]);
/** @var array{env:array{part:string, theme:string}, clo:array{part:string, theme:string}, head:array{part:string, theme:string}, mouth:array{part:string, theme:string}, eyes:array{part:string, theme:string}, top:array{part:string, theme:string}} $parts */
$parts = array_combine(self::BODY_PARTS, array_map([$this, 'stringToVersion'], str_split($hash, 2)));

if (isset($options['ver']['theme'])) {
$theme = $options['ver']['theme'];

/** @var array{env:array{part:string, theme:string}, clo:array{part:string, theme:string}, head:array{part:string, theme:string}, mouth:array{part:string, theme:string}, eyes:array{part:string, theme:string}, top:array{part:string, theme:string}} $parts */
$parts = array_map(function (array $settings) use ($theme): array {
$settings['theme'] = $theme;

return $settings;
}, $parts);

return $parts;
}

if (isset($options['ver']['part'])) {
$part = $options['ver']['part'];

/** @var array{env:array{part:string, theme:string}, clo:array{part:string, theme:string}, head:array{part:string, theme:string}, mouth:array{part:string, theme:string}, eyes:array{part:string, theme:string}, top:array{part:string, theme:string}} $parts */
$parts = array_map(function (array $settings) use ($part): array {
$settings['part'] = $part;

return $settings;
}, $parts);
}

return $parts;
}

/**
* @return array{part:string, theme:string}
*/
private function mapPart(string $part): array
private function stringToVersion(string $part): array
{
$part = (int) round(47 / 100 * (int) $part);

Expand All @@ -163,33 +205,26 @@ private function mapPart(string $part): array
}

/**
* @param array<string, array{part:string, theme:string}> $bodyParts
* @param array{ver: array{part:string|null, theme:string|null}, sansEnv: bool} $options
* @param array{env:array{part:string, theme:string}, clo:array{part:string, theme:string}, head:array{part:string, theme:string}, mouth:array{part:string, theme:string}, eyes:array{part:string, theme:string}, top:array{part:string, theme:string}} $parts
*
* @return array<string,string>
* @return array{env:string, clo:string, head:string, mouth:string, eyes:string, top:string}
*/
private function partsToElements(array $bodyParts, array $options): array
private function partsToElements(array $parts): array
{
$elements = [];
foreach ($bodyParts as $index => $settings) {
$elements[$index] = $this->mapElement($index, $settings, $options);
}
$reducer = function(array $elements, string $name) use ($parts): array {
$elements[$name] = $this->createElement($name, $parts[$name]['part'], $parts[$name]['theme']);

return $elements;
};

/** @var array{env:string, clo:string, head:string, mouth:string, eyes:string, top:string} $elements */
$elements = array_reduce(self::BODY_PARTS, $reducer, []);

return $elements;
}

/**
* @param array{part:string, theme:string} $settings
* @param array{ver: array{part:string|null, theme:string|null}, sansEnv: bool} $options
*/
private function mapElement(string $name, array $settings, array $options): string
private function createElement(string $name, string $part, string $theme): string
{
if ('env' === $name && $options['sansEnv']) {
return '';
}

$part = $options['ver']['part'] ?? $settings['part'];
$theme = $options['ver']['theme'] ?? $settings['theme'];
$colors = self::themes()[$part][$theme][$name];
$index = 0;
$replace = function (array $result) use ($colors, &$index): string {
Expand All @@ -202,6 +237,21 @@ private function mapElement(string $name, array $settings, array $options): stri
return $element;
}

/**
* @param array{env:string, clo:string, head:string, mouth:string, eyes:string, top:string} $elements
* @param array{ver:array{part:string|null, theme:string|null}, sansEnv:bool} $options
*
* @return array{env:string, clo:string, head:string, mouth:string, eyes:string, top:string}
*/
private function filterElements(array $elements, array $options): array
{
if ($options['sansEnv']) {
$elements['env'] = '';
}

return $elements;
}

/**
* @return array<int|string, array<int|string, string>>
*/
Expand Down
22 changes: 20 additions & 2 deletions src/MultiavatarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,34 @@ public function setUp(): void
public function it_will_throw_if_the_ver_part_is_not_valid(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The submitted part does not exists; expecting a value between `00` and `15`');
$this->expectExceptionMessage('The version part does not exists; expecting a value between `00` and `15`');

($this->multiavatar)('foobar', ['ver' => ['part' => 16]]);
}

/** @test */
public function it_will_throw_if_the_ver_part_is_not_a_valid_type(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The version part is expected to be a scalar; array was given.');

($this->multiavatar)('foobar', ['ver' => ['part' => []]]);
}

/** @test */
public function it_will_throw_if_the_ver_theme_is_not_a_supported_type(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The version theme is expected to be a string; object was given.');

($this->multiavatar)('foobar', ['ver' => ['theme' => new \stdClass()]]);
}

/** @test */
public function it_will_throw_if_the_ver_theme_is_not_valid(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The submitted theme does not exists; expecting a value between `A`, `B` and `C`.');
$this->expectExceptionMessage('The version theme does not exists; expecting a value between `A`, `B` and `C`.');

($this->multiavatar)('foobar', ['ver' => ['theme' => 'D']]);
}
Expand Down

0 comments on commit aafbbb4

Please sign in to comment.