From aafbbb492b9da7c7d6884cc1e4dc18db1d482995 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 30 Dec 2020 00:23:30 +0100 Subject: [PATCH] Refactor the implementation to easily add/expand functionnality --- src/Multiavatar.php | 134 +++++++++++++++++++++++++++------------- src/MultiavatarTest.php | 22 ++++++- 2 files changed, 112 insertions(+), 44 deletions(-) diff --git a/src/Multiavatar.php b/src/Multiavatar.php index f94540e..d3cbed3 100644 --- a/src/Multiavatar.php +++ b/src/Multiavatar.php @@ -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; @@ -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; @@ -40,6 +46,7 @@ class Multiavatar private const SVG_ELEMENT_HEAD = ''; 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' => [ @@ -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; } @@ -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; @@ -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']); @@ -130,29 +147,54 @@ private function filterOptions(array $inputOptions): array } /** - * @return array + * @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); @@ -163,33 +205,26 @@ private function mapPart(string $part): array } /** - * @param array $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 + * @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 { @@ -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> */ diff --git a/src/MultiavatarTest.php b/src/MultiavatarTest.php index 676969c..b499977 100644 --- a/src/MultiavatarTest.php +++ b/src/MultiavatarTest.php @@ -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']]); }