Laravel AT Protocol Client (alpha & unstable)

Add PostBuilder for fluent post construction

Changed files
+257
src
Builders
+257
src/Builders/PostBuilder.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Builders; 4 + 5 + use Closure; 6 + use DateTimeInterface; 7 + use SocialDept\AtpClient\Builders\Concerns\BuildsRichText; 8 + use SocialDept\AtpClient\Builders\Embeds\ImagesBuilder; 9 + use SocialDept\AtpClient\Client\Records\PostRecordClient; 10 + use SocialDept\AtpClient\Contracts\Recordable; 11 + use SocialDept\AtpClient\Data\StrongRef; 12 + use SocialDept\AtpClient\Enums\Nsid\BskyFeed; 13 + 14 + class PostBuilder implements Recordable 15 + { 16 + use BuildsRichText; 17 + 18 + protected ?array $embed = null; 19 + 20 + protected ?array $reply = null; 21 + 22 + protected ?array $langs = null; 23 + 24 + protected ?DateTimeInterface $createdAt = null; 25 + 26 + protected ?PostRecordClient $client = null; 27 + 28 + /** 29 + * Create a new post builder instance 30 + */ 31 + public static function make(): self 32 + { 33 + return new self; 34 + } 35 + 36 + /** 37 + * Add images embed 38 + * 39 + * @param Closure|array $images Closure receiving ImagesBuilder, or array of image data 40 + */ 41 + public function images(Closure|array $images): self 42 + { 43 + if ($images instanceof Closure) { 44 + $builder = ImagesBuilder::make(); 45 + $images($builder); 46 + $this->embed = $builder->toArray(); 47 + } else { 48 + $this->embed = [ 49 + '$type' => 'app.bsky.embed.images', 50 + 'images' => array_map(fn ($img) => $this->normalizeImageData($img), $images), 51 + ]; 52 + } 53 + 54 + return $this; 55 + } 56 + 57 + /** 58 + * Add external link embed (link card) 59 + * 60 + * @param string $uri URL of the external content 61 + * @param string $title Title of the link card 62 + * @param string $description Description text 63 + * @param mixed|null $thumb Optional thumbnail blob 64 + */ 65 + public function external(string $uri, string $title, string $description, mixed $thumb = null): self 66 + { 67 + $external = [ 68 + 'uri' => $uri, 69 + 'title' => $title, 70 + 'description' => $description, 71 + ]; 72 + 73 + if ($thumb !== null) { 74 + $external['thumb'] = $this->normalizeBlob($thumb); 75 + } 76 + 77 + $this->embed = [ 78 + '$type' => 'app.bsky.embed.external', 79 + 'external' => $external, 80 + ]; 81 + 82 + return $this; 83 + } 84 + 85 + /** 86 + * Add video embed 87 + * 88 + * @param mixed $blob Video blob reference 89 + * @param string|null $alt Alt text for the video 90 + * @param array|null $captions Optional captions array 91 + */ 92 + public function video(mixed $blob, ?string $alt = null, ?array $captions = null): self 93 + { 94 + $video = [ 95 + '$type' => 'app.bsky.embed.video', 96 + 'video' => $this->normalizeBlob($blob), 97 + ]; 98 + 99 + if ($alt !== null) { 100 + $video['alt'] = $alt; 101 + } 102 + 103 + if ($captions !== null) { 104 + $video['captions'] = $captions; 105 + } 106 + 107 + $this->embed = $video; 108 + 109 + return $this; 110 + } 111 + 112 + /** 113 + * Add quote embed (embed another post) 114 + */ 115 + public function quote(StrongRef $post): self 116 + { 117 + $this->embed = [ 118 + '$type' => 'app.bsky.embed.record', 119 + 'record' => $post->toArray(), 120 + ]; 121 + 122 + return $this; 123 + } 124 + 125 + /** 126 + * Set as a reply to another post 127 + * 128 + * @param StrongRef $parent The post being replied to 129 + * @param StrongRef|null $root The root post of the thread (defaults to parent if not provided) 130 + */ 131 + public function replyTo(StrongRef $parent, ?StrongRef $root = null): self 132 + { 133 + $this->reply = [ 134 + 'parent' => $parent->toArray(), 135 + 'root' => ($root ?? $parent)->toArray(), 136 + ]; 137 + 138 + return $this; 139 + } 140 + 141 + /** 142 + * Set the post languages 143 + * 144 + * @param array $langs Array of BCP-47 language codes 145 + */ 146 + public function langs(array $langs): self 147 + { 148 + $this->langs = $langs; 149 + 150 + return $this; 151 + } 152 + 153 + /** 154 + * Set the creation timestamp 155 + */ 156 + public function createdAt(DateTimeInterface $date): self 157 + { 158 + $this->createdAt = $date; 159 + 160 + return $this; 161 + } 162 + 163 + /** 164 + * Bind to a PostRecordClient for creating the post 165 + */ 166 + public function for(PostRecordClient $client): self 167 + { 168 + $this->client = $client; 169 + 170 + return $this; 171 + } 172 + 173 + /** 174 + * Create the post (requires client binding via for() or build()) 175 + * 176 + * @throws \RuntimeException If no client is bound 177 + */ 178 + public function create(): StrongRef 179 + { 180 + if ($this->client === null) { 181 + throw new \RuntimeException( 182 + 'No client bound. Use ->for($client) or create via $client->bsky->post->build()' 183 + ); 184 + } 185 + 186 + return $this->client->create($this); 187 + } 188 + 189 + /** 190 + * Convert to array for XRPC (implements Recordable) 191 + */ 192 + public function toArray(): array 193 + { 194 + $record = $this->getTextAndFacets(); 195 + 196 + if ($this->embed !== null) { 197 + $record['embed'] = $this->embed; 198 + } 199 + 200 + if ($this->reply !== null) { 201 + $record['reply'] = $this->reply; 202 + } 203 + 204 + if ($this->langs !== null) { 205 + $record['langs'] = $this->langs; 206 + } 207 + 208 + $record['createdAt'] = ($this->createdAt ?? now())->format('c'); 209 + $record['$type'] = $this->getType(); 210 + 211 + return $record; 212 + } 213 + 214 + /** 215 + * Get the record type (implements Recordable) 216 + */ 217 + public function getType(): string 218 + { 219 + return BskyFeed::Post->value; 220 + } 221 + 222 + /** 223 + * Normalize image data from array format 224 + */ 225 + protected function normalizeImageData(array $data): array 226 + { 227 + $image = [ 228 + 'image' => $this->normalizeBlob($data['blob'] ?? $data['image']), 229 + 'alt' => $data['alt'] ?? '', 230 + ]; 231 + 232 + if (isset($data['aspectRatio'])) { 233 + $ratio = $data['aspectRatio']; 234 + $image['aspectRatio'] = is_array($ratio) && isset($ratio['width']) 235 + ? $ratio 236 + : ['width' => $ratio[0], 'height' => $ratio[1]]; 237 + } 238 + 239 + return $image; 240 + } 241 + 242 + /** 243 + * Normalize blob to array format 244 + */ 245 + protected function normalizeBlob(mixed $blob): array 246 + { 247 + if (is_array($blob)) { 248 + return $blob; 249 + } 250 + 251 + if (method_exists($blob, 'toArray')) { 252 + return $blob->toArray(); 253 + } 254 + 255 + return (array) $blob; 256 + } 257 + }