Skip to content

Commit fdaf129

Browse files
committed
Make fromArray final and clean up hacks
1 parent 007d403 commit fdaf129

3 files changed

Lines changed: 94 additions & 92 deletions

File tree

src/Blocks/Block.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
namespace SlackPhp\BlockKit\Blocks;
66

77
use SlackPhp\BlockKit\Component;
8-
use SlackPhp\BlockKit\Exception;
98
use SlackPhp\BlockKit\Property;
109
use SlackPhp\BlockKit\Tools\Validation\ValidString;
11-
use SlackPhp\BlockKit\Type;
1210

1311
abstract class Block extends Component
1412
{
@@ -27,18 +25,4 @@ public function blockId(?string $blockId): static
2725

2826
return $this;
2927
}
30-
31-
public static function fromArray(?array $data): ?static
32-
{
33-
$type = Type::from($data['type'] ?? throw new Exception('Block components must have a type'));
34-
35-
// Since the "image" type is used in Slack as both a Block and Element type, we have to map the block-level
36-
// image to a different, unique type in order for `Block::fromArray` to resolve to the BlockImage class.
37-
// The reverse logic is handled by an `AliasType` attribute.
38-
if ($type === Type::IMAGE) {
39-
$data['type'] = Type::BLOCK_IMAGE->value;
40-
}
41-
42-
return parent::fromArray($data);
43-
}
4428
}

src/Component.php

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
namespace SlackPhp\BlockKit;
66

77
use JsonSerializable;
8-
use SlackPhp\BlockKit\Tools\Hydration\Dehydrator;
9-
use SlackPhp\BlockKit\Tools\Hydration\Hydrator;
10-
use SlackPhp\BlockKit\Tools\Hydration\HydrationException;
11-
use SlackPhp\BlockKit\Tools\Validation\{Context, ValidationException, Validator};
12-
use Throwable;
8+
use SlackPhp\BlockKit\Tools\Hydration\{Dehydrator, Hydrator};
9+
use SlackPhp\BlockKit\Tools\Validation\{Context, Validator};
1310

1411
abstract class Component implements JsonSerializable
1512
{
@@ -58,9 +55,6 @@ final public function tap(callable $tap): static
5855
return $this;
5956
}
6057

61-
/**
62-
* @throws ValidationException if the block kit item is invalid (e.g., missing data).
63-
*/
6458
final public function validate(?Context $context = null): void
6559
{
6660
$this->validator->validate($context);
@@ -76,45 +70,44 @@ final public function toArray(): array
7670
return $dehydrator->getArrayData();
7771
}
7872

79-
final public function toJson(bool $prettyPrint = false): string
73+
/**
74+
* @param array<string, mixed>|array<int, array<string, mixed>>|null $data
75+
*/
76+
final public static function fromArray(?array $data): ?static
8077
{
81-
$opts = JSON_THROW_ON_ERROR;
82-
if ($prettyPrint) {
83-
$opts |= JSON_PRETTY_PRINT;
78+
if ($data === null) {
79+
return null;
8480
}
8581

86-
return (string) json_encode($this, $opts);
82+
$hydrator = new Hydrator($data);
83+
84+
/** @var static $component */
85+
$component = $hydrator->getComponent(static::class);
86+
87+
return $component;
8788
}
8889

89-
public function jsonSerialize(): array
90+
final public function jsonSerialize(): array
9091
{
9192
return $this->toArray();
9293
}
9394

94-
final public static function fromJson(string $json): static
95+
final public function toJson(bool $prettyPrint = false): string
9596
{
96-
try {
97-
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
98-
} catch (Throwable $err) {
99-
throw new HydrationException('JSON error (%s) hydrating %s', [$err->getMessage(), static::class], $err);
97+
$opts = JSON_THROW_ON_ERROR;
98+
if ($prettyPrint) {
99+
$opts |= JSON_PRETTY_PRINT;
100100
}
101101

102-
return static::fromArray($data);
102+
return json_encode($this, $opts);
103103
}
104104

105-
/**
106-
* @param array<string, mixed>|array<int, array<string, mixed>>|null $data
107-
* @return static|null
108-
*/
109-
public static function fromArray(?array $data): ?static
105+
final public static function fromJson(string $json): static
110106
{
111-
$component = null;
107+
$hydrator = Hydrator::forJson($json);
112108

113-
if (!empty($data)) {
114-
$hydrator = new Hydrator($data, static::class);
115-
/** @var static $component */
116-
$component = $hydrator->getComponent();
117-
}
109+
/** @var static $component */
110+
$component = $hydrator->getComponent(static::class);
118111

119112
return $component;
120113
}

src/Tools/Hydration/Hydrator.php

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,79 @@
66

77
use Closure;
88
use ReflectionProperty;
9+
use SlackPhp\BlockKit\Blocks\Block;
910
use SlackPhp\BlockKit\Collections\ComponentCollection;
1011
use SlackPhp\BlockKit\Component;
1112
use SlackPhp\BlockKit\FauxProperty;
1213
use SlackPhp\BlockKit\Property;
1314
use SlackPhp\BlockKit\Tools\Reflector;
1415
use SlackPhp\BlockKit\Type;
16+
use Throwable;
1517

1618
class Hydrator
1719
{
18-
/** @var array<string, mixed> */
19-
private array $data;
20-
2120
/** @var array<string, bool> */
2221
private array $consumed = [];
2322

24-
public readonly Component $component;
23+
public static function forJson(string $json): self
24+
{
25+
try {
26+
return new self(json_decode($json, true, 512, JSON_THROW_ON_ERROR));
27+
} catch (Throwable $err) {
28+
throw new HydrationException('JSON error hydrating component', [], $err);
29+
}
30+
}
2531

2632
/**
2733
* @param array<string, mixed> $data
2834
*/
29-
public function __construct(array $data, ?string $initiatingClass = null)
35+
public function __construct(private array $data)
36+
{}
37+
38+
/**
39+
* @param class-string $targetClass
40+
*/
41+
public function getComponent(string $targetClass): Component
3042
{
31-
$this->data = $data;
32-
$this->component = $this->initComponent($initiatingClass);
43+
try {
44+
$component = $this->initComponent($targetClass);
45+
$this->fillComponent($component);
46+
return $component;
47+
} catch (HydrationException $ex) {
48+
throw $ex;
49+
} catch (Throwable $ex) {
50+
throw new HydrationException($ex->getMessage(), [], $ex);
51+
}
3352
}
3453

35-
private function initComponent(?string $initiatingClass): Component
54+
/**
55+
* @param class-string $targetClass
56+
*/
57+
private function initComponent(string $targetClass): Component
3658
{
3759
$type = Type::fromValue($this->useValue('type'));
3860

39-
$class = $type?->toClass() ?? $initiatingClass;
40-
if ($class === null) {
41-
throw new HydrationException('The class to hydrate could not be determine.');
61+
// Since the "image" type is used in Slack as both a Block and Element type, we have to map an image component
62+
// in a surface context to a different type in order to resolve it to the BlockImage class. The reverse logic
63+
// for dehydration is handled by the BlockImage's `AliasType` attribute.
64+
if ($type === Type::IMAGE && $targetClass === Block::class) {
65+
$type = Type::BLOCK_IMAGE;
66+
}
67+
68+
$typeClass = $type?->toClass() ?? $targetClass;
69+
70+
if (!is_a($typeClass, Component::class, true)) {
71+
throw new HydrationException('Class %s not a Component class', [$typeClass]);
4272
}
4373

44-
$reflection = Reflector::component($class);
45-
if ($reflection->isAbstract() || !$reflection->isSubclassOf(Component::class)) {
46-
throw new HydrationException('Class %s is abstract or is not a Component', [$reflection->getShortName()]);
74+
if (!is_a($typeClass, $targetClass, true)) {
75+
throw new HydrationException('Class %s is not a subclass of %s', [$typeClass, $targetClass]);
76+
}
77+
78+
$reflection = Reflector::component($typeClass);
79+
80+
if ($reflection->isAbstract()) {
81+
throw new HydrationException('Class %s is abstract and cannot be instantiated', [$reflection->getName()]);
4782
}
4883

4984
/** @var Component $component */
@@ -52,31 +87,25 @@ private function initComponent(?string $initiatingClass): Component
5287
return $component;
5388
}
5489

55-
public function getComponent(): Component
90+
private function fillComponent(Component $component): void
5691
{
57-
try {
58-
$class = Reflector::component($this->component);
59-
60-
foreach ($class->getProperties() as $classProperty) {
61-
$property = Reflector::propertyAttribute($classProperty);
62-
$fauxProperty = Reflector::fauxPropertyAttribute($classProperty);
63-
64-
if ($property || $fauxProperty) {
65-
$setter = $class->getMethod($classProperty->getName())->getClosure($this->component);
66-
if ($property) {
67-
$this->setProperty($classProperty, $property, $setter);
68-
} else {
69-
$this->setFauxProperty($fauxProperty, $setter);
70-
}
92+
$class = Reflector::component($component);
93+
94+
foreach ($class->getProperties() as $classProperty) {
95+
$property = Reflector::propertyAttribute($classProperty);
96+
$fauxProperty = Reflector::fauxPropertyAttribute($classProperty);
97+
98+
if ($property || $fauxProperty) {
99+
$setter = $class->getMethod($classProperty->getName())->getClosure($component);
100+
if ($property) {
101+
$this->setProperty($classProperty, $property, $setter);
102+
} else {
103+
$this->setFauxProperty($fauxProperty, $setter);
71104
}
72105
}
73-
74-
$this->component->extra(array_diff_key($this->data, $this->consumed));
75-
76-
return $this->component;
77-
} catch (\Throwable $ex) {
78-
throw new HydrationException($ex->getMessage(), [], $ex);
79106
}
107+
108+
$component->extra(array_diff_key($this->data, $this->consumed));
80109
}
81110

82111
private function setProperty(ReflectionProperty $reflection, Property $property, Closure $setValue): void
@@ -100,7 +129,7 @@ private function setProperty(ReflectionProperty $reflection, Property $property,
100129
private function setFauxProperty(FauxProperty $property, Closure $setValue): void
101130
{
102131
if ($property->fields === ['*']) {
103-
$setValue(array_map(fn(array $value) => Component::fromArray($value), $this->useComponents(null)));
132+
$setValue(array_map(fn(array $value) => Component::fromArray($value), $this->useAllAsArray()));
104133
} else {
105134
$setValue($this->useValues(...$property->fields));
106135
}
@@ -130,14 +159,8 @@ private function useValues(string ...$keys): array
130159
/**
131160
* @return array<int, mixed>
132161
*/
133-
private function useArray(?string $key): array
162+
private function useArray(string $key): array
134163
{
135-
if ($key === null) {
136-
$this->consumed += array_fill_keys(array_keys($this->data), true);
137-
138-
return array_values($this->data);
139-
}
140-
141164
$this->consumed[$key] = true;
142165

143166
return $this->data[$key] ?? [];
@@ -146,9 +169,11 @@ private function useArray(?string $key): array
146169
/**
147170
* @return array<int, array<string, mixed>>
148171
*/
149-
private function useComponents(?string $key): array
172+
private function useAllAsArray(): array
150173
{
151-
return $this->useArray($key);
174+
$this->consumed += array_fill_keys(array_keys($this->data), true);
175+
176+
return array_values($this->data);
152177
}
153178

154179
/**

0 commit comments

Comments
 (0)