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.*) |