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