Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

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

Merge branch 'refs/heads/dev'

+812
+36
README.md
··· 946 946 ATP_SCOPE_REDIRECT=/login 947 947 ``` 948 948 949 + ## Extending the Client 950 + 951 + Add custom functionality to AtpClient by registering your own domain clients or request clients. Extensions are lazily instantiated on first access. 952 + 953 + ### Register Extensions 954 + 955 + Register extensions in your service provider's `boot()` method: 956 + 957 + ```php 958 + use SocialDept\AtpClient\AtpClient; 959 + 960 + // Add a new domain client: $client->analytics 961 + AtpClient::extend('analytics', fn(AtpClient $atp) => new AnalyticsClient($atp)); 962 + 963 + // Add to an existing domain: $client->bsky->metrics 964 + AtpClient::extendDomain('bsky', 'metrics', fn($bsky) => new MetricsClient($bsky)); 965 + ``` 966 + 967 + ### Usage 968 + 969 + ```php 970 + $client = Atp::as('user.bsky.social'); 971 + 972 + $client->analytics->trackEvent('post_created'); 973 + $client->bsky->metrics->getEngagement(); 974 + ``` 975 + 976 + For complete documentation including creating custom clients, testing, and advanced patterns, see [docs/extensions.md](docs/extensions.md). 977 + 949 978 ## Available Commands 950 979 951 980 ```bash 952 981 # Generate OAuth private key 953 982 php artisan atp-client:generate-key 983 + 984 + # Create a domain client extension 985 + php artisan make:atp-client AnalyticsClient 986 + 987 + # Create a request client extension for an existing domain 988 + php artisan make:atp-request MetricsClient --domain=bsky 954 989 ``` 955 990 956 991 ## Requirements ··· 971 1006 - [AT Protocol Documentation](https://atproto.com/) 972 1007 - [Bluesky API Docs](https://docs.bsky.app/) 973 1008 - [CRYPTO.md](CRYPTO.md) - Cryptographic implementation details 1009 + - [docs/extensions.md](docs/extensions.md) - Client extensions guide 974 1010 975 1011 ## Support & Contributing 976 1012
+14
config/client.php
··· 176 176 177 177 'redirect_to' => env('ATP_SCOPE_REDIRECT', '/login'), 178 178 ], 179 + 180 + /* 181 + |-------------------------------------------------------------------------- 182 + | Generator Settings 183 + |-------------------------------------------------------------------------- 184 + | 185 + | Configure paths for the make:atp-client and make:atp-request commands. 186 + | Paths are relative to the application base path. 187 + | 188 + */ 189 + 'generators' => [ 190 + 'client_path' => 'app/Services/Clients', 191 + 'request_path' => 'app/Services/Clients/Requests', 192 + ], 179 193 ];
+462
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 + ### Generator Commands 27 + 28 + Quickly scaffold extension classes using artisan commands: 29 + 30 + ```bash 31 + # Create a domain client extension 32 + php artisan make:atp-client AnalyticsClient 33 + 34 + # Create a public domain client extension 35 + php artisan make:atp-client DiscoverClient --public 36 + 37 + # Create a request client extension for an existing domain 38 + php artisan make:atp-request MetricsClient --domain=bsky 39 + 40 + # Create a public request client extension 41 + php artisan make:atp-request TrendingClient --domain=bsky --public 42 + ``` 43 + 44 + The generated files are placed in `app/Services/Clients/` (domain clients) and `app/Services/Clients/Requests/` (request clients). You can customize these paths in `config/client.php`: 45 + 46 + ```php 47 + 'generators' => [ 48 + 'client_path' => 'app/Services/Clients', 49 + 'request_path' => 'app/Services/Clients/Requests', 50 + ], 51 + ``` 52 + 53 + ## Understanding Extensions 54 + 55 + 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: 56 + 57 + ```php 58 + // Registration - callback stored, not executed 59 + AtpClient::extend('analytics', fn($atp) => new AnalyticsClient($atp)); 60 + 61 + // First access - callback executed, instance cached 62 + $client->analytics->trackEvent('login'); 63 + 64 + // Subsequent access - cached instance returned 65 + $client->analytics->trackEvent('post_created'); 66 + ``` 67 + 68 + This ensures extensions don't add overhead unless they're actually used. 69 + 70 + ## Creating a Domain Client 71 + 72 + A domain client adds a new namespace to AtpClient, accessible as a property. 73 + 74 + ### Step 1: Create Your Client Class 75 + 76 + ```php 77 + <?php 78 + 79 + namespace App\Atp; 80 + 81 + use SocialDept\AtpClient\AtpClient; 82 + 83 + class AnalyticsClient 84 + { 85 + protected AtpClient $atp; 86 + 87 + public function __construct(AtpClient $parent) 88 + { 89 + $this->atp = $parent; 90 + } 91 + 92 + public function trackEvent(string $event, array $properties = []): void 93 + { 94 + // Your analytics logic here 95 + // You have full access to the authenticated client via $this->atp 96 + } 97 + 98 + public function getEngagementStats(string $actor): array 99 + { 100 + $profile = $this->atp->bsky->actor->getProfile($actor); 101 + 102 + return [ 103 + 'followers' => $profile->followersCount, 104 + 'following' => $profile->followsCount, 105 + 'posts' => $profile->postsCount, 106 + ]; 107 + } 108 + } 109 + ``` 110 + 111 + ### Step 2: Register the Extension 112 + 113 + In your `AppServiceProvider`: 114 + 115 + ```php 116 + <?php 117 + 118 + namespace App\Providers; 119 + 120 + use App\Atp\AnalyticsClient; 121 + use Illuminate\Support\ServiceProvider; 122 + use SocialDept\AtpClient\AtpClient; 123 + 124 + class AppServiceProvider extends ServiceProvider 125 + { 126 + public function boot(): void 127 + { 128 + AtpClient::extend('analytics', fn(AtpClient $atp) => new AnalyticsClient($atp)); 129 + } 130 + } 131 + ``` 132 + 133 + ### Step 3: Use Your Extension 134 + 135 + ```php 136 + use SocialDept\AtpClient\Facades\Atp; 137 + 138 + $client = Atp::as('user.bsky.social'); 139 + 140 + $client->analytics->trackEvent('page_view', ['page' => '/feed']); 141 + 142 + $stats = $client->analytics->getEngagementStats('someone.bsky.social'); 143 + ``` 144 + 145 + ## Creating a Request Client 146 + 147 + 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. 148 + 149 + ### Step 1: Create Your Request Client Class 150 + 151 + Extend the base `Request` class to get access to the parent AtpClient: 152 + 153 + ```php 154 + <?php 155 + 156 + namespace App\Atp; 157 + 158 + use SocialDept\AtpClient\Client\Requests\Request; 159 + 160 + class BskyMetricsClient extends Request 161 + { 162 + public function getPostEngagement(string $uri): array 163 + { 164 + $thread = $this->atp->bsky->feed->getPostThread($uri); 165 + $post = $thread->thread['post'] ?? null; 166 + 167 + if (! $post) { 168 + return []; 169 + } 170 + 171 + return [ 172 + 'likes' => $post['likeCount'] ?? 0, 173 + 'reposts' => $post['repostCount'] ?? 0, 174 + 'replies' => $post['replyCount'] ?? 0, 175 + 'quotes' => $post['quoteCount'] ?? 0, 176 + ]; 177 + } 178 + 179 + public function getAuthorMetrics(string $actor): array 180 + { 181 + $feed = $this->atp->bsky->feed->getAuthorFeed($actor, limit: 100); 182 + $posts = $feed->feed; 183 + 184 + $totalLikes = 0; 185 + $totalReposts = 0; 186 + 187 + foreach ($posts as $item) { 188 + $totalLikes += $item['post']['likeCount'] ?? 0; 189 + $totalReposts += $item['post']['repostCount'] ?? 0; 190 + } 191 + 192 + return [ 193 + 'posts_analyzed' => count($posts), 194 + 'total_likes' => $totalLikes, 195 + 'total_reposts' => $totalReposts, 196 + 'avg_likes' => count($posts) > 0 ? $totalLikes / count($posts) : 0, 197 + ]; 198 + } 199 + } 200 + ``` 201 + 202 + ### Step 2: Register the Extension 203 + 204 + ```php 205 + use App\Atp\BskyMetricsClient; 206 + use SocialDept\AtpClient\AtpClient; 207 + 208 + public function boot(): void 209 + { 210 + AtpClient::extendDomain('bsky', 'metrics', fn($bsky) => new BskyMetricsClient($bsky)); 211 + } 212 + ``` 213 + 214 + The callback receives the domain client instance (`BskyClient` in this case), which is passed to your request client's constructor. 215 + 216 + ### Step 3: Use Your Extension 217 + 218 + ```php 219 + $client = Atp::as('user.bsky.social'); 220 + 221 + $engagement = $client->bsky->metrics->getPostEngagement('at://did:plc:.../app.bsky.feed.post/...'); 222 + 223 + $authorMetrics = $client->bsky->metrics->getAuthorMetrics('someone.bsky.social'); 224 + ``` 225 + 226 + ## Public Client Extensions 227 + 228 + The `AtpPublicClient` supports the same extension system for unauthenticated API access: 229 + 230 + ```php 231 + use SocialDept\AtpClient\Client\Public\AtpPublicClient; 232 + 233 + // Domain client extension 234 + AtpPublicClient::extend('discover', fn($atp) => new DiscoverClient($atp)); 235 + 236 + // Request client extension on existing domain 237 + AtpPublicClient::extendDomain('bsky', 'trending', fn($bsky) => new TrendingClient($bsky)); 238 + ``` 239 + 240 + For public request clients, extend `PublicRequest` instead of `Request`: 241 + 242 + ```php 243 + <?php 244 + 245 + namespace App\Atp; 246 + 247 + use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 248 + 249 + class TrendingPublicClient extends PublicRequest 250 + { 251 + public function getPopularFeeds(int $limit = 10): array 252 + { 253 + return $this->atp->bsky->feed->getPopularFeedGenerators($limit)->feeds; 254 + } 255 + } 256 + ``` 257 + 258 + ## Registering Multiple Extensions 259 + 260 + You can register multiple extensions in your service provider: 261 + 262 + ```php 263 + public function boot(): void 264 + { 265 + // Domain clients 266 + AtpClient::extend('analytics', fn($atp) => new AnalyticsClient($atp)); 267 + AtpClient::extend('moderation', fn($atp) => new ModerationClient($atp)); 268 + 269 + // Request clients 270 + AtpClient::extendDomain('bsky', 'metrics', fn($bsky) => new BskyMetricsClient($bsky)); 271 + AtpClient::extendDomain('bsky', 'lists', fn($bsky) => new BskyListsClient($bsky)); 272 + AtpClient::extendDomain('atproto', 'backup', fn($atproto) => new RepoBackupClient($atproto)); 273 + 274 + // Public client extensions 275 + AtpPublicClient::extend('discover', fn($atp) => new DiscoverClient($atp)); 276 + } 277 + ``` 278 + 279 + ## Conditional Registration 280 + 281 + Register extensions conditionally based on environment or configuration: 282 + 283 + ```php 284 + public function boot(): void 285 + { 286 + if (config('services.analytics.enabled')) { 287 + AtpClient::extend('analytics', fn($atp) => new AnalyticsClient($atp)); 288 + } 289 + 290 + if (app()->environment('local')) { 291 + AtpClient::extend('debug', fn($atp) => new DebugClient($atp)); 292 + } 293 + } 294 + ``` 295 + 296 + ## Testing Extensions 297 + 298 + ### Test Isolation 299 + 300 + Use `flushExtensions()` to clear registered extensions between tests: 301 + 302 + ```php 303 + use SocialDept\AtpClient\AtpClient; 304 + use PHPUnit\Framework\TestCase; 305 + 306 + class MyExtensionTest extends TestCase 307 + { 308 + protected function setUp(): void 309 + { 310 + parent::setUp(); 311 + AtpClient::flushExtensions(); 312 + } 313 + 314 + protected function tearDown(): void 315 + { 316 + AtpClient::flushExtensions(); 317 + parent::tearDown(); 318 + } 319 + 320 + public function test_extension_is_registered(): void 321 + { 322 + AtpClient::extend('test', fn($atp) => new TestClient($atp)); 323 + 324 + $this->assertTrue(AtpClient::hasExtension('test')); 325 + } 326 + } 327 + ``` 328 + 329 + ### Checking Registration 330 + 331 + Use the static methods to verify extensions are registered: 332 + 333 + ```php 334 + // Check domain extension 335 + if (AtpClient::hasExtension('analytics')) { 336 + $client->analytics->trackEvent('test'); 337 + } 338 + 339 + // Check request client extension 340 + if (AtpClient::hasDomainExtension('bsky', 'metrics')) { 341 + $metrics = $client->bsky->metrics->getAuthorMetrics($actor); 342 + } 343 + ``` 344 + 345 + ## Advanced Patterns 346 + 347 + ### Accessing the HTTP Client 348 + 349 + Domain client extensions can access the underlying HTTP client for custom API calls: 350 + 351 + ```php 352 + class CustomApiClient 353 + { 354 + protected AtpClient $atp; 355 + 356 + public function __construct(AtpClient $parent) 357 + { 358 + $this->atp = $parent; 359 + } 360 + 361 + public function customEndpoint(array $params): array 362 + { 363 + // Access the authenticated HTTP client 364 + $response = $this->atp->client->get('com.example.customEndpoint', $params); 365 + 366 + return $response->json(); 367 + } 368 + 369 + public function customProcedure(array $data): array 370 + { 371 + $response = $this->atp->client->post('com.example.customProcedure', $data); 372 + 373 + return $response->json(); 374 + } 375 + } 376 + ``` 377 + 378 + ### Using Typed Responses 379 + 380 + Return typed response objects for better IDE support: 381 + 382 + ```php 383 + use SocialDept\AtpClient\Data\Responses\Response; 384 + 385 + class MetricsResponse extends Response 386 + { 387 + public function __construct( 388 + public readonly int $likes, 389 + public readonly int $reposts, 390 + public readonly int $replies, 391 + ) {} 392 + 393 + public static function fromArray(array $data): static 394 + { 395 + return new static( 396 + likes: $data['likes'] ?? 0, 397 + reposts: $data['reposts'] ?? 0, 398 + replies: $data['replies'] ?? 0, 399 + ); 400 + } 401 + } 402 + 403 + class BskyMetricsClient extends Request 404 + { 405 + public function getPostMetrics(string $uri): MetricsResponse 406 + { 407 + $thread = $this->atp->bsky->feed->getPostThread($uri); 408 + $post = $thread->thread['post'] ?? []; 409 + 410 + return MetricsResponse::fromArray([ 411 + 'likes' => $post['likeCount'] ?? 0, 412 + 'reposts' => $post['repostCount'] ?? 0, 413 + 'replies' => $post['replyCount'] ?? 0, 414 + ]); 415 + } 416 + } 417 + ``` 418 + 419 + ### Composing Multiple Clients 420 + 421 + Extensions can use other extensions or built-in clients: 422 + 423 + ```php 424 + class DashboardClient 425 + { 426 + protected AtpClient $atp; 427 + 428 + public function __construct(AtpClient $parent) 429 + { 430 + $this->atp = $parent; 431 + } 432 + 433 + public function getOverview(string $actor): array 434 + { 435 + // Use built-in clients 436 + $profile = $this->atp->bsky->actor->getProfile($actor); 437 + $feed = $this->atp->bsky->feed->getAuthorFeed($actor, limit: 10); 438 + 439 + // Use other extensions (if registered) 440 + $metrics = AtpClient::hasDomainExtension('bsky', 'metrics') 441 + ? $this->atp->bsky->metrics->getAuthorMetrics($actor) 442 + : null; 443 + 444 + return [ 445 + 'profile' => $profile, 446 + 'recent_posts' => $feed->feed, 447 + 'metrics' => $metrics, 448 + ]; 449 + } 450 + } 451 + ``` 452 + 453 + ## Available Domains 454 + 455 + You can extend these built-in domains with `extendDomain()`: 456 + 457 + | Domain | Description | 458 + |--------|-------------| 459 + | `bsky` | Bluesky-specific operations (app.bsky.*) | 460 + | `atproto` | AT Protocol core operations (com.atproto.*) | 461 + | `chat` | Direct messaging operations (chat.bsky.*) | 462 + | `ozone` | Moderation tools (tools.ozone.*) |
+4
src/AtpClientServiceProvider.php
··· 16 16 use SocialDept\AtpClient\Enums\ScopeEnforcementLevel; 17 17 use SocialDept\AtpClient\Http\Middleware\RequiresScopeMiddleware; 18 18 use SocialDept\AtpClient\Console\GenerateOAuthKeyCommand; 19 + use SocialDept\AtpClient\Console\MakeAtpClientCommand; 20 + use SocialDept\AtpClient\Console\MakeAtpRequestCommand; 19 21 use SocialDept\AtpClient\Contracts\CredentialProvider; 20 22 use SocialDept\AtpClient\Contracts\KeyStore; 21 23 use SocialDept\AtpClient\Http\Controllers\ClientMetadataController; ··· 140 142 141 143 $this->commands([ 142 144 GenerateOAuthKeyCommand::class, 145 + MakeAtpClientCommand::class, 146 + MakeAtpRequestCommand::class, 143 147 ]); 144 148 } 145 149
+150
src/Console/MakeAtpClientCommand.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Console; 4 + 5 + use Illuminate\Console\Command; 6 + use Illuminate\Filesystem\Filesystem; 7 + use Illuminate\Support\Str; 8 + 9 + class MakeAtpClientCommand extends Command 10 + { 11 + protected $signature = 'make:atp-client 12 + {name : The name of the client class} 13 + {--public : Generate a public client extension instead of authenticated} 14 + {--force : Overwrite existing file}'; 15 + 16 + protected $description = 'Create a new ATP domain client extension'; 17 + 18 + public function __construct(protected Filesystem $files) 19 + { 20 + parent::__construct(); 21 + } 22 + 23 + public function handle(): int 24 + { 25 + $name = $this->argument('name'); 26 + $isPublic = $this->option('public'); 27 + 28 + if (! Str::endsWith($name, 'Client')) { 29 + $name .= 'Client'; 30 + } 31 + 32 + $path = $this->getPath($name); 33 + 34 + if ($this->files->exists($path) && ! $this->option('force')) { 35 + $this->components->error("Client [{$name}] already exists!"); 36 + 37 + return self::FAILURE; 38 + } 39 + 40 + $this->makeDirectory($path); 41 + 42 + $stub = $isPublic ? $this->getPublicStub() : $this->getStub(); 43 + $content = $this->populateStub($stub, $name); 44 + 45 + $this->files->put($path, $content); 46 + 47 + $this->components->info("Client [{$path}] created successfully."); 48 + 49 + $this->outputRegistrationHint($name, $isPublic); 50 + 51 + return self::SUCCESS; 52 + } 53 + 54 + protected function getPath(string $name): string 55 + { 56 + $basePath = config('client.generators.client_path', 'app/Services/Clients'); 57 + 58 + return base_path($basePath.'/'.$name.'.php'); 59 + } 60 + 61 + protected function makeDirectory(string $path): void 62 + { 63 + if (! $this->files->isDirectory(dirname($path))) { 64 + $this->files->makeDirectory(dirname($path), 0755, true); 65 + } 66 + } 67 + 68 + protected function getNamespace(): string 69 + { 70 + $basePath = config('client.generators.client_path', 'app/Services/Clients'); 71 + 72 + return Str::of($basePath) 73 + ->replace('/', '\\') 74 + ->ucfirst() 75 + ->replace('App', 'App') 76 + ->toString(); 77 + } 78 + 79 + protected function populateStub(string $stub, string $name): string 80 + { 81 + return str_replace( 82 + ['{{ namespace }}', '{{ class }}'], 83 + [$this->getNamespace(), $name], 84 + $stub 85 + ); 86 + } 87 + 88 + protected function outputRegistrationHint(string $name, bool $isPublic): void 89 + { 90 + $this->newLine(); 91 + $this->components->info('Register the extension in your AppServiceProvider:'); 92 + $this->newLine(); 93 + 94 + $namespace = $this->getNamespace(); 95 + $extensionName = Str::of($name)->before('Client')->camel()->toString(); 96 + $clientClass = $isPublic ? 'AtpPublicClient' : 'AtpClient'; 97 + 98 + $this->line("use {$namespace}\\{$name};"); 99 + $this->line("use SocialDept\\AtpClient\\".($isPublic ? 'Client\\Public\\' : '').$clientClass.';'); 100 + $this->newLine(); 101 + $this->line("// In boot() method:"); 102 + $this->line("{$clientClass}::extend('{$extensionName}', fn(\${$clientClass} \$atp) => new {$name}(\$atp));"); 103 + } 104 + 105 + protected function getStub(): string 106 + { 107 + return <<<'STUB' 108 + <?php 109 + 110 + namespace {{ namespace }}; 111 + 112 + use SocialDept\AtpClient\AtpClient; 113 + 114 + class {{ class }} 115 + { 116 + protected AtpClient $atp; 117 + 118 + public function __construct(AtpClient $parent) 119 + { 120 + $this->atp = $parent; 121 + } 122 + 123 + // 124 + } 125 + STUB; 126 + } 127 + 128 + protected function getPublicStub(): string 129 + { 130 + return <<<'STUB' 131 + <?php 132 + 133 + namespace {{ namespace }}; 134 + 135 + use SocialDept\AtpClient\Client\Public\AtpPublicClient; 136 + 137 + class {{ class }} 138 + { 139 + protected AtpPublicClient $atp; 140 + 141 + public function __construct(AtpPublicClient $parent) 142 + { 143 + $this->atp = $parent; 144 + } 145 + 146 + // 147 + } 148 + STUB; 149 + } 150 + }
+146
src/Console/MakeAtpRequestCommand.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Console; 4 + 5 + use Illuminate\Console\Command; 6 + use Illuminate\Filesystem\Filesystem; 7 + use Illuminate\Support\Str; 8 + 9 + class MakeAtpRequestCommand extends Command 10 + { 11 + protected $signature = 'make:atp-request 12 + {name : The name of the request client class} 13 + {--domain=bsky : The domain to extend (bsky, atproto, chat, ozone)} 14 + {--public : Generate a public request client instead of authenticated} 15 + {--force : Overwrite existing file}'; 16 + 17 + protected $description = 'Create a new ATP request client extension for an existing domain'; 18 + 19 + protected array $validDomains = ['bsky', 'atproto', 'chat', 'ozone']; 20 + 21 + public function __construct(protected Filesystem $files) 22 + { 23 + parent::__construct(); 24 + } 25 + 26 + public function handle(): int 27 + { 28 + $name = $this->argument('name'); 29 + $domain = $this->option('domain'); 30 + $isPublic = $this->option('public'); 31 + 32 + if (! in_array($domain, $this->validDomains)) { 33 + $this->components->error("Invalid domain [{$domain}]. Valid domains: ".implode(', ', $this->validDomains)); 34 + 35 + return self::FAILURE; 36 + } 37 + 38 + if (! Str::endsWith($name, 'Client')) { 39 + $name .= 'Client'; 40 + } 41 + 42 + $path = $this->getPath($name); 43 + 44 + if ($this->files->exists($path) && ! $this->option('force')) { 45 + $this->components->error("Request client [{$name}] already exists!"); 46 + 47 + return self::FAILURE; 48 + } 49 + 50 + $this->makeDirectory($path); 51 + 52 + $stub = $isPublic ? $this->getPublicStub() : $this->getStub(); 53 + $content = $this->populateStub($stub, $name); 54 + 55 + $this->files->put($path, $content); 56 + 57 + $this->components->info("Request client [{$path}] created successfully."); 58 + 59 + $this->outputRegistrationHint($name, $domain, $isPublic); 60 + 61 + return self::SUCCESS; 62 + } 63 + 64 + protected function getPath(string $name): string 65 + { 66 + $basePath = config('client.generators.request_path', 'app/Services/Clients/Requests'); 67 + 68 + return base_path($basePath.'/'.$name.'.php'); 69 + } 70 + 71 + protected function makeDirectory(string $path): void 72 + { 73 + if (! $this->files->isDirectory(dirname($path))) { 74 + $this->files->makeDirectory(dirname($path), 0755, true); 75 + } 76 + } 77 + 78 + protected function getNamespace(): string 79 + { 80 + $basePath = config('client.generators.request_path', 'app/Services/Clients/Requests'); 81 + 82 + return Str::of($basePath) 83 + ->replace('/', '\\') 84 + ->ucfirst() 85 + ->replace('App', 'App') 86 + ->toString(); 87 + } 88 + 89 + protected function populateStub(string $stub, string $name): string 90 + { 91 + return str_replace( 92 + ['{{ namespace }}', '{{ class }}'], 93 + [$this->getNamespace(), $name], 94 + $stub 95 + ); 96 + } 97 + 98 + protected function outputRegistrationHint(string $name, string $domain, bool $isPublic): void 99 + { 100 + $this->newLine(); 101 + $this->components->info('Register the extension in your AppServiceProvider:'); 102 + $this->newLine(); 103 + 104 + $namespace = $this->getNamespace(); 105 + $extensionName = Str::of($name)->before('Client')->camel()->toString(); 106 + $clientClass = $isPublic ? 'AtpPublicClient' : 'AtpClient'; 107 + 108 + $this->line("use {$namespace}\\{$name};"); 109 + $this->line("use SocialDept\\AtpClient\\".($isPublic ? 'Client\\Public\\' : '').$clientClass.';'); 110 + $this->newLine(); 111 + $this->line("// In boot() method:"); 112 + $this->line("{$clientClass}::extendDomain('{$domain}', '{$extensionName}', fn(\$domain) => new {$name}(\$domain));"); 113 + } 114 + 115 + protected function getStub(): string 116 + { 117 + return <<<'STUB' 118 + <?php 119 + 120 + namespace {{ namespace }}; 121 + 122 + use SocialDept\AtpClient\Client\Requests\Request; 123 + 124 + class {{ class }} extends Request 125 + { 126 + // 127 + } 128 + STUB; 129 + } 130 + 131 + protected function getPublicStub(): string 132 + { 133 + return <<<'STUB' 134 + <?php 135 + 136 + namespace {{ namespace }}; 137 + 138 + use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 139 + 140 + class {{ class }} extends PublicRequest 141 + { 142 + // 143 + } 144 + STUB; 145 + } 146 + }