From 814d09010be4ad16376d8b4baf4474bf539a0b2e Mon Sep 17 00:00:00 2001 From: Jeremy Lindblom Date: Fri, 18 Jun 2021 13:14:24 -0700 Subject: [PATCH] Improvements to AppHome surface and added tapIf() --- src/Blocks/Virtual/VirtualBlock.php | 13 +++- src/Element.php | 30 ++++++++ src/Surfaces/AppHome.php | 4 +- src/Surfaces/Modal.php | 65 +--------------- src/Surfaces/Surface.php | 31 +++++--- src/Surfaces/View.php | 111 ++++++++++++++++++++++++++++ tests/ElementTest.php | 21 ++++++ tests/Surfaces/ModalTest.php | 80 ++++++++++++++++++++ 8 files changed, 279 insertions(+), 76 deletions(-) create mode 100644 src/Surfaces/View.php create mode 100644 tests/Surfaces/ModalTest.php diff --git a/src/Blocks/Virtual/VirtualBlock.php b/src/Blocks/Virtual/VirtualBlock.php index 3a007c0..a6ea39a 100644 --- a/src/Blocks/Virtual/VirtualBlock.php +++ b/src/Blocks/Virtual/VirtualBlock.php @@ -4,14 +4,18 @@ namespace SlackPhp\BlockKit\Blocks\Virtual; +use Iterator; +use IteratorAggregate; use SlackPhp\BlockKit\Blocks\BlockElement; use SlackPhp\BlockKit\HydrationData; use SlackPhp\BlockKit\HydrationException; /** * An encapsulation of multiple blocks acting as one virtual element. + * + * @implements IteratorAggregate */ -abstract class VirtualBlock extends BlockElement +abstract class VirtualBlock extends BlockElement implements IteratorAggregate { /** @var int */ private $index = 1; @@ -70,6 +74,13 @@ public function getBlocks(): array return $this->blocks; } + public function getIterator(): Iterator + { + foreach ($this->getBlocks() as $block) { + yield $block; + } + } + public function validate(): void { foreach ($this->blocks as $block) { diff --git a/src/Element.php b/src/Element.php index 8768372..6301736 100644 --- a/src/Element.php +++ b/src/Element.php @@ -65,6 +65,14 @@ final public function setExtra(string $key, $value): self } /** + * Allows you to "tap" into the fluent syntax with a callable. + * + * $element = Elem::new() + * ->foo('bar') + * ->tap(function (Elem $elem) { + * $elem->newSubElem()->fizz('buzz'); + * }); + * * @param callable $tap * @return static */ @@ -75,6 +83,28 @@ final public function tap(callable $tap): self return $this; } + /** + * Allows you to "tap" into the fluent syntax with a callable, if the condition is met. + * + * $element = Elem::new() + * ->foo('bar') + * ->tapIf($needsSubElem, function (Elem $elem) { + * $elem->newSubElem()->fizz('buzz'); + * }); + * + * @param bool $condition + * @param callable $tap + * @return static + */ + final public function tapIf(bool $condition, callable $tap): self + { + if ($condition) { + $tap($this); + } + + return $this; + } + /** * @throws Exception if the block kit item is invalid (e.g., missing data). */ diff --git a/src/Surfaces/AppHome.php b/src/Surfaces/AppHome.php index b882ac2..7f55fbe 100644 --- a/src/Surfaces/AppHome.php +++ b/src/Surfaces/AppHome.php @@ -9,7 +9,7 @@ * * @see https://api.slack.com/surfaces */ -class AppHome extends Surface +class AppHome extends View { - // No additions to base Surface functionality. + // No additions to base View and Surface functionality. } diff --git a/src/Surfaces/Modal.php b/src/Surfaces/Modal.php index 0c74a4d..b96ee7e 100644 --- a/src/Surfaces/Modal.php +++ b/src/Surfaces/Modal.php @@ -5,7 +5,6 @@ namespace SlackPhp\BlockKit\Surfaces; use SlackPhp\BlockKit\{ - Blocks\Input, Exception, HydrationData, Partials\PlainText, @@ -18,7 +17,7 @@ * * @see https://api.slack.com/surfaces */ -class Modal extends Surface +class Modal extends View { private const MAX_LENGTH_TITLE = 24; @@ -31,15 +30,6 @@ class Modal extends Surface /** @var PlainText */ private $close; - /** @var string */ - private $privateMetadata; - - /** @var string */ - private $callbackId; - - /** @var string */ - private $externalId; - /** @var bool */ private $clearOnClose; @@ -82,27 +72,6 @@ public function close(string $close): self return $this->setClose(new PlainText($close)); } - public function externalId(string $externalId): self - { - $this->externalId = $externalId; - - return $this; - } - - public function callbackId(string $callbackId): self - { - $this->callbackId = $callbackId; - - return $this; - } - - public function privateMetadata(string $privateMetadata): self - { - $this->privateMetadata = $privateMetadata; - - return $this; - } - public function clearOnClose(bool $clearOnClose): self { $this->clearOnClose = $clearOnClose; @@ -117,14 +86,6 @@ public function notifyOnClose(bool $notifyOnClose): self return $this; } - public function newInput(?string $blockId = null): Input - { - $block = new Input($blockId); - $this->add($block); - - return $block; - } - public function validate(): void { parent::validate(); @@ -160,18 +121,6 @@ public function toArray(): array $data['close'] = $this->close->toArray(); } - if (!empty($this->externalId)) { - $data['external_id'] = $this->externalId; - } - - if (!empty($this->callbackId)) { - $data['callback_id'] = $this->callbackId; - } - - if (!empty($this->privateMetadata)) { - $data['private_metadata'] = $this->privateMetadata; - } - if (!empty($this->clearOnClose)) { $data['clear_on_close'] = $this->clearOnClose; } @@ -199,18 +148,6 @@ protected function hydrate(HydrationData $data): void $this->setClose(PlainText::fromArray($data->useElement('close'))); } - if ($data->has('external_id')) { - $this->externalId($data->useValue('external_id')); - } - - if ($data->has('callback_id')) { - $this->callbackId($data->useValue('callback_id')); - } - - if ($data->has('private_metadata')) { - $this->privateMetadata($data->useValue('private_metadata')); - } - if ($data->has('clear_on_close')) { $this->clearOnClose($data->useValue('clear_on_close')); } diff --git a/src/Surfaces/Surface.php b/src/Surfaces/Surface.php index 6899b4f..e63b84d 100644 --- a/src/Surfaces/Surface.php +++ b/src/Surfaces/Surface.php @@ -4,15 +4,7 @@ namespace SlackPhp\BlockKit\Surfaces; -use SlackPhp\BlockKit\Blocks\{ - Actions, - BlockElement, - Context, - Divider, - Header, - Image, - Section, -}; +use SlackPhp\BlockKit\Blocks\{Actions, BlockElement, Context, Divider, Header, Image, Input, Section}; use SlackPhp\BlockKit\Blocks\Virtual\{VirtualBlock, TwoColumnTable}; use SlackPhp\BlockKit\{ Exception, @@ -49,6 +41,19 @@ public function add(BlockElement $block): self return $this; } + /** + * @param iterable|BlockElement[] $blocks + * @return static + */ + public function blocks(iterable $blocks): self + { + foreach ($blocks as $block) { + $this->add($block); + } + + return $this; + } + /** * @return BlockElement[] */ @@ -116,6 +121,14 @@ public function newImage(?string $blockId = null): Image return $block; } + public function newInput(?string $blockId = null): Input + { + $block = new Input($blockId); + $this->add($block); + + return $block; + } + /** * @param string|null $blockId * @return Section diff --git a/src/Surfaces/View.php b/src/Surfaces/View.php new file mode 100644 index 0000000..fc30aa3 --- /dev/null +++ b/src/Surfaces/View.php @@ -0,0 +1,111 @@ +callbackId = $callbackId; + + return $this; + } + + /** + * @param string $externalId + * @return static + */ + public function externalId(string $externalId): self + { + $this->externalId = $externalId; + + return $this; + } + + /** + * @param string $privateMetadata + * @return static + */ + public function privateMetadata(string $privateMetadata): self + { + $this->privateMetadata = $privateMetadata; + + return $this; + } + + /** + * Encodes the provided associative array of data into a string for `private_metadata`. + * + * Note: Can be decoded using `base64_decode()` and `parse_str()`. + * + * @param array $data + * @return static + */ + public function encodePrivateMetadata(array $data): self + { + return $this->privateMetadata(base64_encode(http_build_query($data))); + } + + public function toArray(): array + { + $data = []; + + if (!empty($this->callbackId)) { + $data['callback_id'] = $this->callbackId; + } + + if (!empty($this->externalId)) { + $data['external_id'] = $this->externalId; + } + + if (!empty($this->privateMetadata)) { + $data['private_metadata'] = $this->privateMetadata; + } + + $data += parent::toArray(); + + return $data; + } + + protected function hydrate(HydrationData $data): void + { + if ($data->has('callback_id')) { + $this->callbackId($data->useValue('callback_id')); + } + + if ($data->has('external_id')) { + $this->externalId($data->useValue('external_id')); + } + + if ($data->has('private_metadata')) { + $this->privateMetadata($data->useValue('private_metadata')); + } + + parent::hydrate($data); + } +} diff --git a/tests/ElementTest.php b/tests/ElementTest.php index 2321d04..ff3617a 100644 --- a/tests/ElementTest.php +++ b/tests/ElementTest.php @@ -76,6 +76,27 @@ public function testCanTapIntoElementForChaining() ]); } + public function testCanConditionallyTapIntoElementForChaining() + { + $callable = function (Element $e) { + $e->setExtra('fizz', 'buzz'); + }; + $tappedElement = $this->getMockElement()->tapIf(true, $callable); + $untappedElement = $this->getMockElement()->tapIf(false, $callable); + + $this->assertInstanceOf(Element::class, $tappedElement); + $this->assertInstanceOf(Element::class, $untappedElement); + $this->assertJsonData($tappedElement, [ + 'type' => 'mock', + 'text' => 'foo', + 'fizz' => 'buzz', + ]); + $this->assertJsonData($untappedElement, [ + 'type' => 'mock', + 'text' => 'foo', + ]); + } + private function getMockElement(bool $valid = true): Element { return new class ($valid) extends Element { diff --git a/tests/Surfaces/ModalTest.php b/tests/Surfaces/ModalTest.php new file mode 100644 index 0000000..8d8f567 --- /dev/null +++ b/tests/Surfaces/ModalTest.php @@ -0,0 +1,80 @@ + Type::SECTION, + 'text' => [ + 'type' => Type::MRKDWNTEXT, + 'text' => 'foo', + ], + ], + [ + 'type' => Type::SECTION, + 'text' => [ + 'type' => Type::MRKDWNTEXT, + 'text' => 'bar', + ], + ], + ]; + + public function testCanCreateSimpleModal(): void + { + $modal = Modal::new() + ->title('foo bar') + ->callbackId('foo-bar') // in View + ->externalId('fizz-buzz') // in View + ->privateMetadata('foo=bar') // in View + ->text('foo') + ->text('bar'); + + $expectedData = [ + 'type' => 'modal', + 'title' => [ + 'type' => 'plain_text', + 'text' => 'foo bar', + ], + 'callback_id' => 'foo-bar', + 'external_id' => 'fizz-buzz', + 'private_metadata' => 'foo=bar', + 'blocks' => self::TEST_BLOCKS, + ]; + + $this->assertJsonData($expectedData, $modal); + + // Test basic hydration too. + $hydratedModal = Modal::fromArray($modal->toArray()); + $this->assertJsonData($expectedData, $hydratedModal); + } + + public function testModalMustHaveTitle(): void + { + $modal = Modal::new()->text('foo'); + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Modals must have a "title"/'); + $modal->validate(); + } + + public function testModalMustHaveSubmitIfContainsInputBlocks(): void + { + $modal = Modal::new()->title('foo'); + $modal->newInput()->label('foo')->newTextInput(); + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Modals must have a "submit" button/'); + $modal->validate(); + } +}