Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add comprehensive client extensions documentation

+435
+435
docs/extensions.md
··· 1 + # Client Extensions 2 + 3 + AtpClient provides an extension system that allows you to add custom functionality. You can register domain clients (like `$client->myDomain`) or request clients on existing domains (like `$client->bsky->myFeature`). 4 + 5 + ## Quick Reference 6 + 7 + ### Available Methods 8 + 9 + | Method | Description | 10 + |--------|-------------| 11 + | `AtpClient::extend($name, $callback)` | Register a domain client extension | 12 + | `AtpClient::extendDomain($domain, $name, $callback)` | Register a request client on an existing domain | 13 + | `AtpClient::hasExtension($name)` | Check if a domain extension is registered | 14 + | `AtpClient::hasDomainExtension($domain, $name)` | Check if a request client extension is registered | 15 + | `AtpClient::flushExtensions()` | Clear all extensions (useful for testing) | 16 + 17 + The same methods are available on `AtpPublicClient` for unauthenticated extensions. 18 + 19 + ### Extension Types 20 + 21 + | Type | Access Pattern | Use Case | 22 + |------|----------------|----------| 23 + | Domain Client | `$client->myDomain` | Group related functionality under a namespace | 24 + | Request Client | `$client->bsky->myFeature` | Add methods to an existing domain | 25 + 26 + ## Understanding Extensions 27 + 28 + Extensions follow a lazy-loading pattern. When you register an extension, the callback is stored but not executed. The extension is only instantiated when first accessed: 29 + 30 + ```php 31 + // Registration - callback stored, not executed 32 + AtpClient::extend('analytics', fn($atp) => new AnalyticsClient($atp)); 33 + 34 + // First access - callback executed, instance cached 35 + $client->analytics->trackEvent('login'); 36 + 37 + // Subsequent access - cached instance returned 38 + $client->analytics->trackEvent('post_created'); 39 + ``` 40 + 41 + This ensures extensions don't add overhead unless they're actually used. 42 + 43 + ## Creating a Domain Client 44 + 45 + A domain client adds a new namespace to AtpClient, accessible as a property. 46 + 47 + ### Step 1: Create Your Client Class 48 + 49 + ```php 50 + <?php 51 + 52 + namespace App\Atp; 53 + 54 + use SocialDept\AtpClient\AtpClient; 55 + 56 + class AnalyticsClient 57 + { 58 + protected AtpClient $atp; 59 + 60 + public function __construct(AtpClient $parent) 61 + { 62 + $this->atp = $parent; 63 + } 64 + 65 + public function trackEvent(string $event, array $properties = []): void 66 + { 67 + // Your analytics logic here 68 + // You have full access to the authenticated client via $this->atp 69 + } 70 + 71 + public function getEngagementStats(string $actor): array 72 + { 73 + $profile = $this->atp->bsky->actor->getProfile($actor); 74 + 75 + return [ 76 + 'followers' => $profile->followersCount, 77 + 'following' => $profile->followsCount, 78 + 'posts' => $profile->postsCount, 79 + ]; 80 + } 81 + } 82 + ``` 83 + 84 + ### Step 2: Register the Extension 85 + 86 + In your `AppServiceProvider`: 87 + 88 + ```php 89 + <?php 90 + 91 + namespace App\Providers; 92 + 93 + use App\Atp\AnalyticsClient; 94 + use Illuminate\Support\ServiceProvider; 95 + use SocialDept\AtpClient\AtpClient; 96 + 97 + class AppServiceProvider extends ServiceProvider 98 + { 99 + public function boot(): void 100 + { 101 + AtpClient::extend('analytics', fn(AtpClient $atp) => new AnalyticsClient($atp)); 102 + } 103 + } 104 + ``` 105 + 106 + ### Step 3: Use Your Extension 107 + 108 + ```php 109 + use SocialDept\AtpClient\Facades\Atp; 110 + 111 + $client = Atp::as('user.bsky.social'); 112 + 113 + $client->analytics->trackEvent('page_view', ['page' => '/feed']); 114 + 115 + $stats = $client->analytics->getEngagementStats('someone.bsky.social'); 116 + ``` 117 + 118 + ## Creating a Request Client 119 + 120 + A request client extends an existing domain (like `bsky`, `atproto`, `chat`, or `ozone`). This is useful when you want to add methods that logically belong alongside the built-in functionality. 121 + 122 + ### Step 1: Create Your Request Client Class 123 + 124 + Extend the base `Request` class to get access to the parent AtpClient: 125 + 126 + ```php 127 + <?php 128 + 129 + namespace App\Atp; 130 + 131 + use SocialDept\AtpClient\Client\Requests\Request; 132 + 133 + class BskyMetricsClient extends Request 134 + { 135 + public function getPostEngagement(string $uri): array 136 + { 137 + $thread = $this->atp->bsky->feed->getPostThread($uri); 138 + $post = $thread->thread['post'] ?? null; 139 + 140 + if (! $post) { 141 + return []; 142 + } 143 + 144 + return [ 145 + 'likes' => $post['likeCount'] ?? 0, 146 + 'reposts' => $post['repostCount'] ?? 0, 147 + 'replies' => $post['replyCount'] ?? 0, 148 + 'quotes' => $post['quoteCount'] ?? 0, 149 + ]; 150 + } 151 + 152 + public function getAuthorMetrics(string $actor): array 153 + { 154 + $feed = $this->atp->bsky->feed->getAuthorFeed($actor, limit: 100); 155 + $posts = $feed->feed; 156 + 157 + $totalLikes = 0; 158 + $totalReposts = 0; 159 + 160 + foreach ($posts as $item) { 161 + $totalLikes += $item['post']['likeCount'] ?? 0; 162 + $totalReposts += $item['post']['repostCount'] ?? 0; 163 + } 164 + 165 + return [ 166 + 'posts_analyzed' => count($posts), 167 + 'total_likes' => $totalLikes, 168 + 'total_reposts' => $totalReposts, 169 + 'avg_likes' => count($posts) > 0 ? $totalLikes / count($posts) : 0, 170 + ]; 171 + } 172 + } 173 + ``` 174 + 175 + ### Step 2: Register the Extension 176 + 177 + ```php 178 + use App\Atp\BskyMetricsClient; 179 + use SocialDept\AtpClient\AtpClient; 180 + 181 + public function boot(): void 182 + { 183 + AtpClient::extendDomain('bsky', 'metrics', fn($bsky) => new BskyMetricsClient($bsky)); 184 + } 185 + ``` 186 + 187 + The callback receives the domain client instance (`BskyClient` in this case), which is passed to your request client's constructor. 188 + 189 + ### Step 3: Use Your Extension 190 + 191 + ```php 192 + $client = Atp::as('user.bsky.social'); 193 + 194 + $engagement = $client->bsky->metrics->getPostEngagement('at://did:plc:.../app.bsky.feed.post/...'); 195 + 196 + $authorMetrics = $client->bsky->metrics->getAuthorMetrics('someone.bsky.social'); 197 + ``` 198 + 199 + ## Public Client Extensions 200 + 201 + The `AtpPublicClient` supports the same extension system for unauthenticated API access: 202 + 203 + ```php 204 + use SocialDept\AtpClient\Client\Public\AtpPublicClient; 205 + 206 + // Domain client extension 207 + AtpPublicClient::extend('discover', fn($atp) => new DiscoverClient($atp)); 208 + 209 + // Request client extension on existing domain 210 + AtpPublicClient::extendDomain('bsky', 'trending', fn($bsky) => new TrendingClient($bsky)); 211 + ``` 212 + 213 + For public request clients, extend `PublicRequest` instead of `Request`: 214 + 215 + ```php 216 + <?php 217 + 218 + namespace App\Atp; 219 + 220 + use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 221 + 222 + class TrendingPublicClient extends PublicRequest 223 + { 224 + public function getPopularFeeds(int $limit = 10): array 225 + { 226 + return $this->atp->bsky->feed->getPopularFeedGenerators($limit)->feeds; 227 + } 228 + } 229 + ``` 230 + 231 + ## Registering Multiple Extensions 232 + 233 + You can register multiple extensions in your service provider: 234 + 235 + ```php 236 + public function boot(): void 237 + { 238 + // Domain clients 239 + AtpClient::extend('analytics', fn($atp) => new AnalyticsClient($atp)); 240 + AtpClient::extend('moderation', fn($atp) => new ModerationClient($atp)); 241 + 242 + // Request clients 243 + AtpClient::extendDomain('bsky', 'metrics', fn($bsky) => new BskyMetricsClient($bsky)); 244 + AtpClient::extendDomain('bsky', 'lists', fn($bsky) => new BskyListsClient($bsky)); 245 + AtpClient::extendDomain('atproto', 'backup', fn($atproto) => new RepoBackupClient($atproto)); 246 + 247 + // Public client extensions 248 + AtpPublicClient::extend('discover', fn($atp) => new DiscoverClient($atp)); 249 + } 250 + ``` 251 + 252 + ## Conditional Registration 253 + 254 + Register extensions conditionally based on environment or configuration: 255 + 256 + ```php 257 + public function boot(): void 258 + { 259 + if (config('services.analytics.enabled')) { 260 + AtpClient::extend('analytics', fn($atp) => new AnalyticsClient($atp)); 261 + } 262 + 263 + if (app()->environment('local')) { 264 + AtpClient::extend('debug', fn($atp) => new DebugClient($atp)); 265 + } 266 + } 267 + ``` 268 + 269 + ## Testing Extensions 270 + 271 + ### Test Isolation 272 + 273 + Use `flushExtensions()` to clear registered extensions between tests: 274 + 275 + ```php 276 + use SocialDept\AtpClient\AtpClient; 277 + use PHPUnit\Framework\TestCase; 278 + 279 + class MyExtensionTest extends TestCase 280 + { 281 + protected function setUp(): void 282 + { 283 + parent::setUp(); 284 + AtpClient::flushExtensions(); 285 + } 286 + 287 + protected function tearDown(): void 288 + { 289 + AtpClient::flushExtensions(); 290 + parent::tearDown(); 291 + } 292 + 293 + public function test_extension_is_registered(): void 294 + { 295 + AtpClient::extend('test', fn($atp) => new TestClient($atp)); 296 + 297 + $this->assertTrue(AtpClient::hasExtension('test')); 298 + } 299 + } 300 + ``` 301 + 302 + ### Checking Registration 303 + 304 + Use the static methods to verify extensions are registered: 305 + 306 + ```php 307 + // Check domain extension 308 + if (AtpClient::hasExtension('analytics')) { 309 + $client->analytics->trackEvent('test'); 310 + } 311 + 312 + // Check request client extension 313 + if (AtpClient::hasDomainExtension('bsky', 'metrics')) { 314 + $metrics = $client->bsky->metrics->getAuthorMetrics($actor); 315 + } 316 + ``` 317 + 318 + ## Advanced Patterns 319 + 320 + ### Accessing the HTTP Client 321 + 322 + Domain client extensions can access the underlying HTTP client for custom API calls: 323 + 324 + ```php 325 + class CustomApiClient 326 + { 327 + protected AtpClient $atp; 328 + 329 + public function __construct(AtpClient $parent) 330 + { 331 + $this->atp = $parent; 332 + } 333 + 334 + public function customEndpoint(array $params): array 335 + { 336 + // Access the authenticated HTTP client 337 + $response = $this->atp->client->get('com.example.customEndpoint', $params); 338 + 339 + return $response->json(); 340 + } 341 + 342 + public function customProcedure(array $data): array 343 + { 344 + $response = $this->atp->client->post('com.example.customProcedure', $data); 345 + 346 + return $response->json(); 347 + } 348 + } 349 + ``` 350 + 351 + ### Using Typed Responses 352 + 353 + Return typed response objects for better IDE support: 354 + 355 + ```php 356 + use SocialDept\AtpClient\Data\Responses\Response; 357 + 358 + class MetricsResponse extends Response 359 + { 360 + public function __construct( 361 + public readonly int $likes, 362 + public readonly int $reposts, 363 + public readonly int $replies, 364 + ) {} 365 + 366 + public static function fromArray(array $data): static 367 + { 368 + return new static( 369 + likes: $data['likes'] ?? 0, 370 + reposts: $data['reposts'] ?? 0, 371 + replies: $data['replies'] ?? 0, 372 + ); 373 + } 374 + } 375 + 376 + class BskyMetricsClient extends Request 377 + { 378 + public function getPostMetrics(string $uri): MetricsResponse 379 + { 380 + $thread = $this->atp->bsky->feed->getPostThread($uri); 381 + $post = $thread->thread['post'] ?? []; 382 + 383 + return MetricsResponse::fromArray([ 384 + 'likes' => $post['likeCount'] ?? 0, 385 + 'reposts' => $post['repostCount'] ?? 0, 386 + 'replies' => $post['replyCount'] ?? 0, 387 + ]); 388 + } 389 + } 390 + ``` 391 + 392 + ### Composing Multiple Clients 393 + 394 + Extensions can use other extensions or built-in clients: 395 + 396 + ```php 397 + class DashboardClient 398 + { 399 + protected AtpClient $atp; 400 + 401 + public function __construct(AtpClient $parent) 402 + { 403 + $this->atp = $parent; 404 + } 405 + 406 + public function getOverview(string $actor): array 407 + { 408 + // Use built-in clients 409 + $profile = $this->atp->bsky->actor->getProfile($actor); 410 + $feed = $this->atp->bsky->feed->getAuthorFeed($actor, limit: 10); 411 + 412 + // Use other extensions (if registered) 413 + $metrics = AtpClient::hasDomainExtension('bsky', 'metrics') 414 + ? $this->atp->bsky->metrics->getAuthorMetrics($actor) 415 + : null; 416 + 417 + return [ 418 + 'profile' => $profile, 419 + 'recent_posts' => $feed->feed, 420 + 'metrics' => $metrics, 421 + ]; 422 + } 423 + } 424 + ``` 425 + 426 + ## Available Domains 427 + 428 + You can extend these built-in domains with `extendDomain()`: 429 + 430 + | Domain | Description | 431 + |--------|-------------| 432 + | `bsky` | Bluesky-specific operations (app.bsky.*) | 433 + | `atproto` | AT Protocol core operations (com.atproto.*) | 434 + | `chat` | Direct messaging operations (chat.bsky.*) | 435 + | `ozone` | Moderation tools (tools.ozone.*) |