Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel

Compare changes

Choose any two refs to compare.

+24
.github/workflows/code-style.yml
···
··· 1 + name: Code Style 2 + 3 + on: 4 + pull_request: 5 + branches: [ main, dev ] 6 + 7 + jobs: 8 + php-cs-fixer: 9 + runs-on: ubuntu-latest 10 + 11 + steps: 12 + - name: Checkout code 13 + uses: actions/checkout@v4 14 + 15 + - name: Setup PHP 16 + uses: shivammathur/setup-php@v2 17 + with: 18 + php-version: 8.3 19 + extensions: gmp, mbstring, json 20 + coverage: none 21 + tools: php-cs-fixer 22 + 23 + - name: Run PHP CS Fixer 24 + run: php-cs-fixer fix --dry-run --diff --verbose
+30
.github/workflows/tests.yml
···
··· 1 + name: Tests 2 + 3 + on: 4 + pull_request: 5 + branches: [ main, dev ] 6 + 7 + jobs: 8 + test: 9 + runs-on: ubuntu-latest 10 + 11 + name: Tests (PHP 8.2 - Laravel 12) 12 + 13 + steps: 14 + - name: Checkout code 15 + uses: actions/checkout@v4 16 + 17 + - name: Setup PHP 18 + uses: shivammathur/setup-php@v2 19 + with: 20 + php-version: 8.2 21 + extensions: gmp, mbstring, json 22 + coverage: none 23 + 24 + - name: Install dependencies 25 + run: | 26 + composer require "laravel/framework:^12.0" "orchestra/testbench:^10.0" --no-interaction --no-update 27 + composer update --prefer-stable --prefer-dist --no-interaction 28 + 29 + - name: Execute tests 30 + run: vendor/bin/phpunit
+6
.gitignore
···
··· 1 + .DS_Store 2 + .phpunit.cache 3 + .phpunit.result.cache 4 + .php-cs-fixer.cache 5 + composer.lock 6 + /vendor
+35
.php-cs-fixer.php
···
··· 1 + <?php 2 + 3 + use PhpCsFixer\Config; 4 + use PhpCsFixer\Finder; 5 + 6 + $finder = Finder::create() 7 + ->in(__DIR__ . '/src') 8 + ->in(__DIR__ . '/tests') 9 + ->name('*.php') 10 + ->notName('*.blade.php') 11 + ->ignoreDotFiles(true) 12 + ->ignoreVCS(true); 13 + 14 + return (new Config()) 15 + ->setRules([ 16 + '@PSR12' => true, 17 + 'array_syntax' => ['syntax' => 'short'], 18 + 'ordered_imports' => ['sort_algorithm' => 'alpha'], 19 + 'no_unused_imports' => true, 20 + 'not_operator_with_successor_space' => true, 21 + 'trailing_comma_in_multiline' => true, 22 + 'phpdoc_scalar' => true, 23 + 'unary_operator_spaces' => true, 24 + 'binary_operator_spaces' => true, 25 + 'blank_line_before_statement' => [ 26 + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 27 + ], 28 + 'phpdoc_single_line_var_spacing' => true, 29 + 'phpdoc_var_without_name' => true, 30 + 'method_argument_space' => [ 31 + 'on_multiline' => 'ensure_fully_multiline', 32 + 'keep_multiple_spaces_after_comma' => true, 33 + ], 34 + ]) 35 + ->setFinder($finder);
+67
CONTRIBUTING.md
···
··· 1 + # Contributing 2 + 3 + Contributions are **welcome** and will be fully **credited**. 4 + 5 + ## Etiquette 6 + 7 + This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. 8 + 9 + Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. 10 + 11 + It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 12 + 13 + ## Viability 14 + 15 + When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. 16 + 17 + ## Procedure 18 + 19 + ### Before Filing an Issue 20 + 21 + - Search existing issues to avoid duplicates 22 + - Check the [documentation](docs/) to ensure it's not a usage question 23 + - Provide a clear title and description 24 + - Include steps to reproduce the issue 25 + - Specify your environment (PHP version, Laravel version, Signal version, mode) 26 + - Include relevant code samples and full error messages 27 + 28 + ### Before Submitting a Pull Request 29 + 30 + - **Discuss non-trivial changes first** by opening an issue 31 + - **Fork the repository** and create a feature branch from `main` 32 + - **Follow all requirements** listed below 33 + - **Write tests** for your changes 34 + - **Update documentation** if behavior changes 35 + - **Run code style checks** with `vendor/bin/php-cs-fixer fix` 36 + - **Ensure all tests pass** with `vendor/bin/phpunit` 37 + - **Write clear commit messages** that explain what and why 38 + 39 + ## Requirements 40 + 41 + - **[PSR-12 Coding Standard](https://www.php-fig.org/psr/psr-12/)** - Run `vendor/bin/php-cs-fixer fix` to automatically fix code style issues. 42 + 43 + - **Add tests** - Your patch won't be accepted if it doesn't have tests. All tests must use [PHPUnit](https://phpunit.de/). 44 + 45 + - **Document any change in behaviour** - Make sure the `README.md`, `docs/`, and any other relevant documentation are kept up-to-date. 46 + 47 + - **Consider our release cycle** - We follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 48 + 49 + - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 50 + 51 + - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 52 + 53 + ## Running Tests 54 + 55 + ```bash 56 + vendor/bin/phpunit 57 + ``` 58 + 59 + ## Code Style 60 + 61 + Signal follows PSR-12 coding standard. Run PHP CS Fixer before submitting: 62 + 63 + ```bash 64 + vendor/bin/php-cs-fixer fix 65 + ``` 66 + 67 + **Happy coding**!
+114 -699
README.md
··· 1 - # Signal 2 3 - **Laravel package for building Signals that respond to AT Protocol events** 4 5 - Signal provides a clean, Laravel-style interface for consuming real-time events from the AT Protocol. Supports both **Jetstream** (simplified JSON events) and **Firehose** (raw CBOR/CAR format) for maximum flexibility. Build reactive applications, AppViews, and custom indexers that respond to posts, likes, follows, and other social interactions on the AT Protocol network. 6 7 --- 8 9 - ## Features 10 - 11 - - ๐Ÿ”„ **Dual-Mode Support** - Choose between Jetstream (JSON) or Firehose (CBOR/CAR) based on your needs 12 - - ๐Ÿ”Œ **WebSocket Connection** - Connect to AT Protocol with automatic reconnection and exponential backoff 13 - - ๐ŸŽฏ **Signal-based Architecture** - Clean, testable event handlers (avoiding Laravel's "listener" naming collision) 14 - - โญ **Wildcard Collection Filtering** - Match multiple collections with patterns like `app.bsky.feed.*` 15 - - ๐Ÿ—๏ธ **AppView Ready** - Full support for custom collections and building AT Protocol AppViews 16 - - ๐Ÿ’พ **Cursor Management** - Resume from last position after disconnections (Database, Redis, or File storage) 17 - - โšก **Queue Integration** - Process events asynchronously with Laravel queues 18 - - ๐Ÿ” **Auto-Discovery** - Automatically find and register Signals in `app/Signals` 19 - - ๐Ÿงช **Testing Tools** - Test your Signals with sample data 20 - - ๐Ÿ› ๏ธ **Artisan Commands** - Full CLI support for managing and testing Signals 21 - 22 - --- 23 - 24 - ## Table of Contents 25 - 26 - <!-- TOC --> 27 - * [Installation](#installation) 28 - * [Quick Start](#quick-start) 29 - * [Jetstream vs Firehose](#jetstream-vs-firehose) 30 - * [Creating Signals](#creating-signals) 31 - * [Filtering Events](#filtering-events) 32 - * [Queue Integration](#queue-integration) 33 - * [Configuration](#configuration-1) 34 - * [Available Commands](#available-commands) 35 - * [Testing](#testing) 36 - * [External Resources](#external-resources) 37 - * [Examples](#examples) 38 - * [Requirements](#requirements) 39 - * [License](#license) 40 - * [Support](#support) 41 - <!-- TOC --> 42 - 43 - --- 44 - 45 - ## Installation 46 47 - Install the package via Composer: 48 49 - ```bash 50 - composer require socialdept/signal 51 - ``` 52 53 - Run the installation command: 54 55 - ```bash 56 - php artisan signal:install 57 - ``` 58 59 - This will: 60 - - Publish the configuration file to `config/signal.php` 61 - - Publish the database migration 62 - - Run migrations (with confirmation) 63 - - Display next steps 64 - 65 - ### Manual Installation 66 - 67 - If you prefer manual installation: 68 - 69 - ```bash 70 - php artisan vendor:publish --tag=signal-config 71 - php artisan vendor:publish --tag=signal-migrations 72 - php artisan migrate 73 - ``` 74 - 75 - --- 76 - 77 - ## Quick Start 78 - 79 - ### 1. Create Your First Signal 80 - 81 - ```bash 82 - php artisan make:signal NewPostSignal 83 - ``` 84 - 85 - This creates `app/Signals/NewPostSignal.php`: 86 87 ```php 88 - <?php 89 - 90 - namespace App\Signals; 91 - 92 - use SocialDept\Signal\Events\SignalEvent; 93 - use SocialDept\Signal\Signals\Signal; 94 95 class NewPostSignal extends Signal 96 { ··· 116 } 117 ``` 118 119 - ### 2. Start Consuming Events 120 - 121 - ```bash 122 - php artisan signal:consume 123 - ``` 124 - 125 - Your Signal will now respond to new posts on the AT Protocol network in real-time! 126 - 127 - --- 128 - 129 - ## Jetstream vs Firehose 130 - 131 - Signal supports two modes for consuming AT Protocol events. Choose based on your use case: 132 133 - ### Jetstream Mode (Default) 134 - 135 - **Best for**: Standard Bluesky collections, production efficiency, lower bandwidth 136 137 ```bash 138 - php artisan signal:consume --mode=jetstream 139 ``` 140 141 - **Characteristics:** 142 - - โœ… Simplified JSON events (easy to work with) 143 - - โœ… Server-side collection filtering (efficient) 144 - - โœ… Lower bandwidth and processing overhead 145 - - โš ๏ธ Only standard `app.bsky.*` collections get create/update operations 146 - - โš ๏ธ Custom collections only receive delete operations 147 148 - **Jetstream URL options:** 149 - - US East: `wss://jetstream2.us-east.bsky.network` (default) 150 - - US West: `wss://jetstream1.us-west.bsky.network` 151 152 - ### Firehose Mode 153 154 - **Best for**: Custom collections, AppViews, comprehensive indexing 155 156 ```bash 157 - php artisan signal:consume --mode=firehose 158 - ``` 159 - 160 - **Characteristics:** 161 - - โœ… **All operations** (create, update, delete) for **all collections** 162 - - โœ… Perfect for custom collections (e.g., `app.yourapp.*.collection`) 163 - - โœ… Full CBOR/CAR decoding with package `revolution/laravel-bluesky` 164 - - โš ๏ธ Client-side filtering only (higher bandwidth) 165 - - โš ๏ธ More processing overhead 166 - 167 - **When to use Firehose:** 168 - - Building an AT Protocol AppView 169 - - Working with custom collections 170 - - Need create/update events for non-standard collections 171 - - Building comprehensive indexes 172 - 173 - ### Configuration 174 - 175 - Set your preferred mode in `.env`: 176 - 177 - ```env 178 - # Use Jetstream (default) 179 - SIGNAL_MODE=jetstream 180 - 181 - # Or use Firehose for custom collections 182 - SIGNAL_MODE=firehose 183 ``` 184 185 - ### Example: Custom Collections 186 - 187 - If you're tracking custom collections like `app.offprint.beta.publication`, you **must** use Firehose mode: 188 189 ```php 190 - class PublicationSignal extends Signal 191 { 192 - public function collections(): ?array 193 - { 194 - return ['app.offprint.beta.publication']; 195 - } 196 - 197 - public function handle(SignalEvent $event): void 198 - { 199 - // With Jetstream: Only sees deletes โŒ 200 - // With Firehose: Sees creates, updates, deletes โœ… 201 - } 202 } 203 ``` 204 205 - --- 206 207 - ## Creating Signals 208 209 - ### Basic Signal Structure 210 211 - Every Signal extends the base `Signal` class and must implement: 212 213 - ```php 214 - use SocialDept\Signal\Enums\SignalEventType; 215 - use SocialDept\Signal\Events\SignalEvent; 216 - use SocialDept\Signal\Signals\Signal; 217 218 - class MySignal extends Signal 219 - { 220 - // Required: Define which event types to listen for 221 - public function eventTypes(): array 222 - { 223 - return [SignalEventType::Commit]; 224 225 - // Or use strings: 226 - // return ['commit']; 227 - } 228 229 - // Required: Handle the event 230 - public function handle(SignalEvent $event): void 231 - { 232 - // Your logic here 233 - } 234 - } 235 - ``` 236 237 - **Enums vs Strings**: Signal supports both typed enums and strings for better IDE support and type safety. Use whichever you prefer! 238 239 - ### Event Types 240 - 241 - Three event types are available: 242 243 - | Enum | String | Description | Use Cases | 244 - |-----------------------------|--------------|--------------------------------------------------|---------------------------------------| 245 - | `SignalEventType::Commit` | `'commit'` | Repository commits (posts, likes, follows, etc.) | Content creation, social interactions | 246 - | `SignalEventType::Identity` | `'identity'` | Identity changes (handle updates) | User profile tracking | 247 - | `SignalEventType::Account` | `'account'` | Account status changes | Account monitoring | 248 - 249 - ### Accessing Event Data 250 - 251 ```php 252 - use SocialDept\Signal\Enums\SignalCommitOperation; 253 - 254 - public function handle(SignalEvent $event): void 255 { 256 - // Common properties 257 - $did = $event->did; // User's DID 258 - $kind = $event->kind; // Event type 259 - $timestamp = $event->timeUs; // Microsecond timestamp 260 - 261 - // Commit events 262 - if ($event->isCommit()) { 263 - $collection = $event->getCollection(); // e.g., 'app.bsky.feed.post' 264 - $operation = $event->getOperation(); // SignalCommitOperation enum 265 - $record = $event->getRecord(); // The actual record data 266 - $rkey = $event->commit->rkey; // Record key 267 - 268 - // Use enum for type-safe comparisons 269 - if ($operation === SignalCommitOperation::Create) { 270 - // Handle new records 271 - } 272 - 273 - // Or get string value 274 - $operationString = $operation->value; // 'create', 'update', or 'delete' 275 - } 276 - 277 - // Identity events 278 - if ($event->isIdentity()) { 279 - $handle = $event->identity->handle; 280 - } 281 - 282 - // Account events 283 - if ($event->isAccount()) { 284 - $active = $event->account->active; 285 - $status = $event->account->status; 286 - } 287 } 288 ``` 289 290 - --- 291 - 292 - ## Filtering Events 293 - 294 - ### Collection Filtering (with Wildcards!) 295 - 296 - Filter events by AT Protocol collection. 297 - 298 - **Important**: 299 - - **Jetstream mode**: Exact collection names are sent as URL parameters for server-side filtering. Wildcards work for client-side filtering only. 300 - - **Firehose mode**: All filtering is client-side. Wildcards work normally. 301 - 302 ```php 303 - // Exact match - only posts 304 - public function collections(): ?array 305 - { 306 - return ['app.bsky.feed.post']; 307 - } 308 - 309 - // Wildcard - all feed events 310 public function collections(): ?array 311 { 312 return ['app.bsky.feed.*']; 313 } 314 315 - // Multiple patterns 316 - public function collections(): ?array 317 { 318 - return [ 319 - 'app.bsky.feed.post', 320 - 'app.bsky.feed.repost', 321 - 'app.bsky.graph.*', // All graph collections 322 - ]; 323 - } 324 - 325 - // No filter - all collections 326 - public function collections(): ?array 327 - { 328 - return null; 329 } 330 ``` 331 332 - ### Common Collection Patterns 333 - 334 - | Pattern | Matches | 335 - |--------------------|-----------------------------| 336 - | `app.bsky.feed.*` | Posts, likes, reposts, etc. | 337 - | `app.bsky.graph.*` | Follows, blocks, mutes | 338 - | `app.bsky.actor.*` | Profile updates | 339 - | `app.bsky.*` | All Bluesky collections | 340 - 341 - ### Operation Filtering 342 - 343 - Filter events by operation type (only applies to `commit` events): 344 - 345 ```php 346 - use SocialDept\Signal\Enums\SignalCommitOperation; 347 - 348 - // Only handle creates (using enum) 349 - public function operations(): ?array 350 { 351 - return [SignalCommitOperation::Create]; 352 - } 353 - 354 - // Only handle creates and updates (using enums) 355 - public function operations(): ?array 356 - { 357 - return [ 358 - SignalCommitOperation::Create, 359 - SignalCommitOperation::Update, 360 - ]; 361 - } 362 - 363 - // Only handle deletes (using string) 364 - public function operations(): ?array 365 - { 366 - return ['delete']; 367 - } 368 - 369 - // No filter - all operations (default) 370 - public function operations(): ?array 371 - { 372 - return null; 373 } 374 ``` 375 376 - **Available operations:** 377 378 - | Enum | String | Description | 379 - |---------------------------------|------------|---------------------------| 380 - | `SignalCommitOperation::Create` | `'create'` | New records created | 381 - | `SignalCommitOperation::Update` | `'update'` | Existing records modified | 382 - | `SignalCommitOperation::Delete` | `'delete'` | Records removed | 383 384 - **Example use cases:** 385 - ```php 386 - use SocialDept\Signal\Enums\SignalCommitOperation; 387 388 - // Signal that only handles new posts (not edits) 389 - class NewPostSignal extends Signal 390 - { 391 - public function collections(): ?array 392 - { 393 - return ['app.bsky.feed.post']; 394 - } 395 396 - public function operations(): ?array 397 - { 398 - return [SignalCommitOperation::Create]; 399 - } 400 - } 401 402 - // Signal that only handles content updates 403 - class ContentUpdateSignal extends Signal 404 - { 405 - public function collections(): ?array 406 - { 407 - return ['app.bsky.feed.post']; 408 - } 409 410 - public function operations(): ?array 411 - { 412 - return [SignalCommitOperation::Update]; 413 - } 414 - } 415 416 - // Signal that handles deletions for cleanup 417 - class CleanupSignal extends Signal 418 - { 419 - public function collections(): ?array 420 - { 421 - return ['app.bsky.feed.*']; 422 - } 423 - 424 - public function operations(): ?array 425 - { 426 - return [SignalCommitOperation::Delete]; 427 - } 428 - } 429 - ``` 430 - 431 - ### DID Filtering 432 - 433 - Filter events by specific users: 434 435 ```php 436 - public function dids(): ?array 437 { 438 return [ 439 - 'did:plc:z72i7hdynmk6r22z27h6tvur', // Specific user 440 - 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // Another user 441 ]; 442 } 443 ``` 444 445 - ### Custom Filtering 446 447 - Add complex filtering logic: 448 449 - ```php 450 - public function shouldHandle(SignalEvent $event): bool 451 - { 452 - // Only handle posts with images 453 - if ($event->isCommit() && $event->commit->collection === 'app.bsky.feed.post') { 454 - $record = $event->getRecord(); 455 - return isset($record->embed); 456 - } 457 - 458 - return true; 459 - } 460 - ``` 461 - 462 - --- 463 - 464 - ## Queue Integration 465 - 466 - Process events asynchronously using Laravel queues: 467 468 ```php 469 - class HeavyProcessingSignal extends Signal 470 { 471 - public function eventTypes(): array 472 - { 473 - return ['commit']; 474 - } 475 - 476 - // Enable queueing 477 - public function shouldQueue(): bool 478 - { 479 - return true; 480 - } 481 - 482 - // Optional: Customize queue 483 - public function queue(): string 484 - { 485 - return 'high-priority'; 486 - } 487 - 488 - // Optional: Customize connection 489 - public function queueConnection(): string 490 - { 491 - return 'redis'; 492 - } 493 - 494 - public function handle(SignalEvent $event): void 495 - { 496 - // This runs in a queue job 497 - $this->performExpensiveOperation($event); 498 - } 499 - 500 - // Handle failures 501 - public function failed(SignalEvent $event, \Throwable $exception): void 502 - { 503 - Log::error('Signal failed', [ 504 - 'event' => $event->toArray(), 505 - 'error' => $exception->getMessage(), 506 - ]); 507 - } 508 } 509 ``` 510 511 - --- 512 - 513 - ## Configuration 514 - 515 - Configuration is stored in `config/signal.php`: 516 - 517 - ### Consumer Mode 518 - 519 - Choose between Jetstream (JSON) or Firehose (CBOR) mode: 520 - 521 - ```php 522 - 'mode' => env('SIGNAL_MODE', 'jetstream'), 523 - ``` 524 - 525 - Options: 526 - - `jetstream` - JSON events, server-side filtering (default) 527 - - `firehose` - CBOR events, client-side filtering (required for custom collections) 528 - 529 - ### Jetstream Configuration 530 - 531 - ```php 532 - 'websocket_url' => env('SIGNAL_JETSTREAM_URL', 'wss://jetstream2.us-east.bsky.network'), 533 - ``` 534 - 535 - Available endpoints: 536 - - **US East**: `wss://jetstream2.us-east.bsky.network` (default) 537 - - **US West**: `wss://jetstream1.us-west.bsky.network` 538 - 539 - ### Firehose Configuration 540 - 541 - ```php 542 - 'firehose' => [ 543 - 'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'), 544 - ], 545 - ``` 546 - 547 - The raw firehose endpoint is: `wss://{host}/xrpc/com.atproto.sync.subscribeRepos` 548 - 549 - ### Cursor Storage 550 - 551 - Choose how to store cursor positions: 552 - 553 - ```php 554 - 'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'), 555 - ``` 556 - 557 - | Driver | Best For | Configuration | 558 - |------------|-------------------------------|--------------------| 559 - | `database` | Production, multi-server | Default connection | 560 - | `redis` | High performance, distributed | Redis connection | 561 - | `file` | Development, single server | Storage path | 562 - 563 - ### Environment Variables 564 - 565 - Add to your `.env`: 566 - 567 - ```env 568 - # Consumer Mode 569 - SIGNAL_MODE=jetstream # or 'firehose' for custom collections 570 - 571 - # Jetstream Configuration 572 - SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 573 - 574 - # Firehose Configuration (only needed if using firehose mode) 575 - SIGNAL_FIREHOSE_HOST=bsky.network 576 - 577 - # Optional Configuration 578 - SIGNAL_CURSOR_STORAGE=database 579 - SIGNAL_QUEUE_CONNECTION=redis 580 - SIGNAL_QUEUE=signal 581 - SIGNAL_BATCH_SIZE=100 582 - SIGNAL_RATE_LIMIT=1000 583 - ``` 584 - 585 - ### Auto-Discovery 586 - 587 - Signals are automatically discovered from `app/Signals`. Disable if needed: 588 - 589 - ```php 590 - 'auto_discovery' => [ 591 - 'enabled' => true, 592 - 'path' => app_path('Signals'), 593 - 'namespace' => 'App\\Signals', 594 - ], 595 - ``` 596 - 597 - Or manually register Signals: 598 - 599 - ```php 600 - 'signals' => [ 601 - \App\Signals\NewPostSignal::class, 602 - \App\Signals\NewFollowSignal::class, 603 - ], 604 - ``` 605 - 606 - --- 607 608 ## Available Commands 609 - 610 - ### `signal:install` 611 - Install the package (publish config, migrations, run migrations) 612 613 ```bash 614 php artisan signal:install 615 - ``` 616 617 - ### `signal:consume` 618 - Start consuming events from AT Protocol 619 - 620 - ```bash 621 - # Use default mode from config 622 - php artisan signal:consume 623 - 624 - # Override mode 625 - php artisan signal:consume --mode=jetstream 626 - php artisan signal:consume --mode=firehose 627 - 628 - # Start from specific cursor 629 - php artisan signal:consume --cursor=123456789 630 - 631 - # Start fresh (ignore stored cursor) 632 - php artisan signal:consume --fresh 633 634 - # Combine options 635 - php artisan signal:consume --mode=firehose --fresh 636 - ``` 637 - 638 - ### `signal:list` 639 - List all registered Signals 640 - 641 - ```bash 642 php artisan signal:list 643 - ``` 644 645 - ### `signal:make` 646 - Create a new Signal class 647 - 648 - ```bash 649 - php artisan make:signal NewPostSignal 650 - 651 - # With options 652 - php artisan make:signal FollowSignal --type=commit --collection=app.bsky.graph.follow 653 - ``` 654 - 655 - ### `signal:test` 656 - Test a Signal with sample data 657 - 658 - ```bash 659 - php artisan signal:test NewPostSignal 660 - ``` 661 - 662 - --- 663 - 664 - ## Testing 665 - 666 - Signal includes a comprehensive test suite. Test your Signals: 667 - 668 - ### Unit Testing 669 - 670 - ```php 671 - use SocialDept\Signal\Events\CommitEvent; 672 - use SocialDept\Signal\Events\SignalEvent; 673 - 674 - class NewPostSignalTest extends TestCase 675 - { 676 - /** @test */ 677 - public function it_handles_new_posts() 678 - { 679 - $signal = new NewPostSignal(); 680 - 681 - $event = new SignalEvent( 682 - did: 'did:plc:test', 683 - timeUs: time() * 1000000, 684 - kind: 'commit', 685 - commit: new CommitEvent( 686 - rev: 'test', 687 - operation: 'create', 688 - collection: 'app.bsky.feed.post', 689 - rkey: 'test', 690 - record: (object) [ 691 - 'text' => 'Hello World!', 692 - 'createdAt' => now()->toIso8601String(), 693 - ], 694 - ), 695 - ); 696 - 697 - $signal->handle($event); 698 699 - // Assert your expected behavior 700 - } 701 - } 702 ``` 703 704 - ### Testing with Artisan 705 706 - ```bash 707 - php artisan signal:test NewPostSignal 708 - ``` 709 710 - --- 711 - 712 - ## External Resources 713 714 - [AT Protocol Documentation](https://atproto.com/) 715 - [Firehose Documentation](https://docs.bsky.app/docs/advanced-guides/firehose) 716 - - [Bluesky Lexicon](https://atproto.com/lexicons) 717 718 - --- 719 720 - ## Examples 721 722 - ### Monitor All Feed Activity 723 724 - ```php 725 - class FeedMonitorSignal extends Signal 726 - { 727 - public function eventTypes(): array 728 - { 729 - return ['commit']; 730 - } 731 732 - public function collections(): ?array 733 - { 734 - return ['app.bsky.feed.*']; 735 - } 736 - 737 - public function handle(SignalEvent $event): void 738 - { 739 - // Handles posts, likes, reposts, etc. 740 - Log::info('Feed activity', [ 741 - 'collection' => $event->getCollection(), 742 - 'operation' => $event->getOperation(), 743 - 'did' => $event->did, 744 - ]); 745 - } 746 - } 747 - ``` 748 - 749 - ### Track New Follows 750 - 751 - ```php 752 - class NewFollowSignal extends Signal 753 - { 754 - public function eventTypes(): array 755 - { 756 - return ['commit']; 757 - } 758 - 759 - public function collections(): ?array 760 - { 761 - return ['app.bsky.graph.follow']; 762 - } 763 - 764 - public function handle(SignalEvent $event): void 765 - { 766 - if ($event->commit->isCreate()) { 767 - $record = $event->getRecord(); 768 - 769 - // Store follow relationship 770 - Follow::create([ 771 - 'follower_did' => $event->did, 772 - 'following_did' => $record->subject, 773 - ]); 774 - } 775 - } 776 - } 777 - ``` 778 - 779 - ### Content Moderation 780 - 781 - ```php 782 - class ModerationSignal extends Signal 783 - { 784 - public function eventTypes(): array 785 - { 786 - return ['commit']; 787 - } 788 - 789 - public function collections(): ?array 790 - { 791 - return ['app.bsky.feed.post']; 792 - } 793 - 794 - public function shouldQueue(): bool 795 - { 796 - return true; 797 - } 798 - 799 - public function handle(SignalEvent $event): void 800 - { 801 - $record = $event->getRecord(); 802 - 803 - if ($this->containsProhibitedContent($record->text)) { 804 - $this->flagForModeration($event->did, $record); 805 - } 806 - } 807 - } 808 - ``` 809 - 810 - --- 811 - 812 - ## Requirements 813 - 814 - - PHP 8.2 or higher 815 - - Laravel 11.0 or higher 816 - - WebSocket support (enabled by default in most environments) 817 - 818 - --- 819 820 ## License 821 822 - The MIT License (MIT). Please see [LICENSE](LICENSE) for more information. 823 824 --- 825 826 - ## Support 827 - 828 - For issues, questions, or feature requests: 829 - - Read the [README.md](./README.md) before opening issues 830 - - Search through existing issues 831 - - Open new issue 832 - 833 - --- 834 - 835 - **Built for the AT Protocol ecosystem** โ€ข Made with โค๏ธ by Social Dept
··· 1 + [![Signal Header](./header.png)](https://github.com/socialdept/atp-signals) 2 3 + <h3 align="center"> 4 + Consume real-time AT Protocol events in your Laravel application. 5 + </h3> 6 7 + <p align="center"> 8 + <br> 9 + <a href="https://packagist.org/packages/socialdept/atp-signals" title="Latest Version on Packagist"><img src="https://img.shields.io/packagist/v/socialdept/atp-signals.svg?style=flat-square"></a> 10 + <a href="https://packagist.org/packages/socialdept/atp-signals" title="Total Downloads"><img src="https://img.shields.io/packagist/dt/socialdept/atp-signals.svg?style=flat-square"></a> 11 + <a href="https://github.com/socialdept/atp-signals/actions/workflows/tests.yml" title="GitHub Tests Action Status"><img src="https://img.shields.io/github/actions/workflow/status/socialdept/atp-signals/tests.yml?branch=main&label=tests&style=flat-square"></a> 12 + <a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-signals?style=flat-square"></a> 13 + </p> 14 15 --- 16 17 + ## What is Signal? 18 19 + **Signal** is a Laravel package that lets you respond to real-time events from the AT Protocol network. Build reactive applications, custom feeds, moderation tools, analytics systems, and AppViews by listening to posts, likes, follows, and other social interactions as they happen across Bluesky and the entire AT Protocol ecosystem. 20 21 + Think of it as Laravel's event listeners, but for the decentralized social web. 22 23 + ## Why use Signal? 24 25 + - **Laravel-style code** - Familiar patterns you already know 26 + - **Real-time processing** - React to events as they happen 27 + - **Dual-mode support** - Choose Jetstream (efficient JSON) or Firehose (comprehensive CBOR) 28 + - **AppView ready** - Full support for custom collections and protocols 29 + - **Production features** - Queue integration, cursor management, auto-reconnection 30 + - **Easy filtering** - Target specific collections, operations, and users with wildcards 31 + - **Built-in testing** - Test your signals with sample data 32 33 + ## Quick Example 34 35 ```php 36 + use SocialDept\AtpSignals\Events\SignalEvent; 37 + use SocialDept\AtpSignals\Signals\Signal; 38 39 class NewPostSignal extends Signal 40 { ··· 60 } 61 ``` 62 63 + Run `php artisan signal:consume` and start responding to every post on Bluesky in real-time. 64 65 + ## Installation 66 67 ```bash 68 + composer require socialdept/atp-signals 69 + php artisan signal:install 70 ``` 71 72 + That's it. [Read the installation docs โ†’](docs/installation.md) 73 74 + ## Getting Started 75 76 + Once installed, you're three steps away from consuming AT Protocol events: 77 78 + ### 1. Create a Signal 79 80 ```bash 81 + php artisan make:signal NewPostSignal 82 ``` 83 84 + ### 2. Define What to Listen For 85 86 ```php 87 + public function collections(): ?array 88 { 89 + return ['app.bsky.feed.post']; 90 } 91 ``` 92 93 + ### 3. Start Consuming 94 95 + ```bash 96 + php artisan signal:consume 97 + ``` 98 99 + Your Signal will now handle every matching event from the network. [Read the quickstart guide โ†’](docs/quickstart.md) 100 101 + ## What can you build? 102 103 + - **Custom feeds** - Curate content based on your own algorithms 104 + - **Moderation tools** - Detect and flag problematic content automatically 105 + - **Analytics platforms** - Track engagement, trends, and network growth 106 + - **Social integrations** - Mirror content to other platforms in real-time 107 + - **Notification systems** - Alert users about relevant activity 108 + - **AppViews** - Build custom AT Protocol applications with your own collections 109 110 + ## Documentation 111 112 + **Getting Started** 113 + - [Installation](docs/installation.md) - Detailed setup instructions 114 + - [Quickstart Guide](docs/quickstart.md) - Build your first Signal 115 + - [Jetstream vs Firehose](docs/modes.md) - Choose the right mode 116 117 + **Building Signals** 118 + - [Creating Signals](docs/signals.md) - Complete Signal reference 119 + - [Filtering Events](docs/filtering.md) - Target specific collections and operations 120 + - [Queue Integration](docs/queues.md) - Process events asynchronously 121 122 + **Advanced** 123 + - [Configuration](docs/configuration.md) - All config options explained 124 + - [Testing](docs/testing.md) - Test your Signals 125 + - [Examples](docs/examples.md) - Real-world use cases 126 127 + ## Example Use Cases 128 129 + ### Track User Growth 130 ```php 131 + public function collections(): ?array 132 { 133 + return ['app.bsky.graph.follow']; 134 } 135 ``` 136 137 + ### Monitor Content Moderation 138 ```php 139 public function collections(): ?array 140 { 141 return ['app.bsky.feed.*']; 142 } 143 144 + public function shouldQueue(): bool 145 { 146 + return true; // Process in background 147 } 148 ``` 149 150 + ### Build Custom Collections (AppView) 151 ```php 152 + public function collections(): ?array 153 { 154 + return ['app.yourapp.custom.collection']; 155 } 156 ``` 157 158 + [See more examples โ†’](docs/examples.md) 159 160 + ## Key Features Explained 161 162 + ### Jetstream vs Firehose 163 164 + Signal supports two modes for consuming AT Protocol events: 165 166 + - **Jetstream** (default) - Simplified JSON events with server-side filtering 167 + - **Firehose** - Raw CBOR/CAR format with client-side filtering 168 169 + [Learn more about modes โ†’](docs/modes.md) 170 171 + ### Wildcard Filtering 172 173 + Match multiple collections with patterns: 174 175 ```php 176 + public function collections(): ?array 177 { 178 return [ 179 + 'app.bsky.feed.*', // All feed events 180 + 'app.bsky.graph.*', // All graph events 181 + 'app.yourapp.*', // All your custom collections 182 ]; 183 } 184 ``` 185 186 + [Learn more about filtering โ†’](docs/filtering.md) 187 188 + ### Queue Integration 189 190 + Process events asynchronously for better performance: 191 192 ```php 193 + public function shouldQueue(): bool 194 { 195 + return true; 196 } 197 ``` 198 199 + [Learn more about queues โ†’](docs/queues.md) 200 201 ## Available Commands 202 203 ```bash 204 + # Install Signal 205 php artisan signal:install 206 207 + # Create a new Signal 208 + php artisan make:signal YourSignal 209 210 + # List all registered Signals 211 php artisan signal:list 212 213 + # Start consuming events 214 + php artisan signal:consume 215 216 + # Test a Signal with sample data 217 + php artisan signal:test YourSignal 218 ``` 219 220 + ## Requirements 221 222 + - PHP 8.2+ 223 + - Laravel 11+ 224 + - WebSocket support (enabled by default) 225 226 + ## Resources 227 228 - [AT Protocol Documentation](https://atproto.com/) 229 + - [Bluesky API Docs](https://docs.bsky.app/) 230 - [Firehose Documentation](https://docs.bsky.app/docs/advanced-guides/firehose) 231 + - [Jetstream Documentation](https://github.com/bluesky-social/jetstream) 232 233 + ## Support & Contributing 234 235 + Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-signals/issues). 236 237 + Want to contribute? We'd love your help! Check out the [contribution guidelines](CONTRIBUTING.md). 238 239 + ## Credits 240 241 + - [Miguel Batres](https://batres.co) - founder & lead maintainer 242 + - [All contributors](https://github.com/socialdept/atp-signals/graphs/contributors) 243 244 ## License 245 246 + Signal is open-source software licensed under the [MIT license](LICENSE). 247 248 --- 249 250 + **Built for the Federation** โ€ข By Social Dept.
+9 -8
composer.json
··· 1 { 2 - "name": "socialdept/signal", 3 "description": "Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel", 4 "type": "library", 5 "license": "MIT", 6 "require": { 7 "php": "^8.2", 8 "illuminate/support": "^11.0|^12.0", 9 "illuminate/console": "^11.0|^12.0", 10 "illuminate/database": "^11.0|^12.0", 11 "ratchet/pawl": "^0.4", 12 - "react/event-loop": "^1.5", 13 - "revolution/laravel-bluesky": "^1.1" 14 }, 15 "require-dev": { 16 "orchestra/testbench": "^9.0", 17 - "phpunit/phpunit": "^11.0" 18 }, 19 "autoload": { 20 "psr-4": { 21 - "SocialDept\\Signal\\": "src/" 22 } 23 }, 24 "autoload-dev": { 25 "psr-4": { 26 - "SocialDept\\Signal\\Tests\\": "tests/" 27 } 28 }, 29 "extra": { 30 "laravel": { 31 "providers": [ 32 - "SocialDept\\Signal\\SignalServiceProvider" 33 ], 34 "aliases": { 35 - "Signal": "SocialDept\\Signal\\Facades\\Signal" 36 } 37 } 38 }
··· 1 { 2 + "name": "socialdept/atp-signals", 3 "description": "Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel", 4 "type": "library", 5 "license": "MIT", 6 "require": { 7 "php": "^8.2", 8 + "ext-gmp": "*", 9 "illuminate/support": "^11.0|^12.0", 10 "illuminate/console": "^11.0|^12.0", 11 "illuminate/database": "^11.0|^12.0", 12 "ratchet/pawl": "^0.4", 13 + "react/event-loop": "^1.5" 14 }, 15 "require-dev": { 16 "orchestra/testbench": "^9.0", 17 + "phpunit/phpunit": "^11.0", 18 + "friendsofphp/php-cs-fixer": "^3.89" 19 }, 20 "autoload": { 21 "psr-4": { 22 + "SocialDept\\AtpSignals\\": "src/" 23 } 24 }, 25 "autoload-dev": { 26 "psr-4": { 27 + "SocialDept\\AtpSignals\\Tests\\": "tests/" 28 } 29 }, 30 "extra": { 31 "laravel": { 32 "providers": [ 33 + "SocialDept\\AtpSignals\\SignalServiceProvider" 34 ], 35 "aliases": { 36 + "Signal": "SocialDept\\AtpSignals\\Facades\\Signal" 37 } 38 } 39 }
-9912
composer.lock
··· 1 - { 2 - "_readme": [ 3 - "This file locks the dependencies of your project to a known state", 4 - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 - "This file is @generated automatically" 6 - ], 7 - "content-hash": "b00f2db2fb718f1b2d56431ebb175d75", 8 - "packages": [ 9 - { 10 - "name": "brick/math", 11 - "version": "0.14.0", 12 - "source": { 13 - "type": "git", 14 - "url": "https://github.com/brick/math.git", 15 - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" 16 - }, 17 - "dist": { 18 - "type": "zip", 19 - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", 20 - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", 21 - "shasum": "" 22 - }, 23 - "require": { 24 - "php": "^8.2" 25 - }, 26 - "require-dev": { 27 - "php-coveralls/php-coveralls": "^2.2", 28 - "phpstan/phpstan": "2.1.22", 29 - "phpunit/phpunit": "^11.5" 30 - }, 31 - "type": "library", 32 - "autoload": { 33 - "psr-4": { 34 - "Brick\\Math\\": "src/" 35 - } 36 - }, 37 - "notification-url": "https://packagist.org/downloads/", 38 - "license": [ 39 - "MIT" 40 - ], 41 - "description": "Arbitrary-precision arithmetic library", 42 - "keywords": [ 43 - "Arbitrary-precision", 44 - "BigInteger", 45 - "BigRational", 46 - "arithmetic", 47 - "bigdecimal", 48 - "bignum", 49 - "bignumber", 50 - "brick", 51 - "decimal", 52 - "integer", 53 - "math", 54 - "mathematics", 55 - "rational" 56 - ], 57 - "support": { 58 - "issues": "https://github.com/brick/math/issues", 59 - "source": "https://github.com/brick/math/tree/0.14.0" 60 - }, 61 - "funding": [ 62 - { 63 - "url": "https://github.com/BenMorel", 64 - "type": "github" 65 - } 66 - ], 67 - "time": "2025-08-29T12:40:03+00:00" 68 - }, 69 - { 70 - "name": "carbonphp/carbon-doctrine-types", 71 - "version": "3.2.0", 72 - "source": { 73 - "type": "git", 74 - "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", 75 - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" 76 - }, 77 - "dist": { 78 - "type": "zip", 79 - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", 80 - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", 81 - "shasum": "" 82 - }, 83 - "require": { 84 - "php": "^8.1" 85 - }, 86 - "conflict": { 87 - "doctrine/dbal": "<4.0.0 || >=5.0.0" 88 - }, 89 - "require-dev": { 90 - "doctrine/dbal": "^4.0.0", 91 - "nesbot/carbon": "^2.71.0 || ^3.0.0", 92 - "phpunit/phpunit": "^10.3" 93 - }, 94 - "type": "library", 95 - "autoload": { 96 - "psr-4": { 97 - "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" 98 - } 99 - }, 100 - "notification-url": "https://packagist.org/downloads/", 101 - "license": [ 102 - "MIT" 103 - ], 104 - "authors": [ 105 - { 106 - "name": "KyleKatarn", 107 - "email": "kylekatarnls@gmail.com" 108 - } 109 - ], 110 - "description": "Types to use Carbon in Doctrine", 111 - "keywords": [ 112 - "carbon", 113 - "date", 114 - "datetime", 115 - "doctrine", 116 - "time" 117 - ], 118 - "support": { 119 - "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", 120 - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" 121 - }, 122 - "funding": [ 123 - { 124 - "url": "https://github.com/kylekatarnls", 125 - "type": "github" 126 - }, 127 - { 128 - "url": "https://opencollective.com/Carbon", 129 - "type": "open_collective" 130 - }, 131 - { 132 - "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", 133 - "type": "tidelift" 134 - } 135 - ], 136 - "time": "2024-02-09T16:56:22+00:00" 137 - }, 138 - { 139 - "name": "dflydev/dot-access-data", 140 - "version": "v3.0.3", 141 - "source": { 142 - "type": "git", 143 - "url": "https://github.com/dflydev/dflydev-dot-access-data.git", 144 - "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" 145 - }, 146 - "dist": { 147 - "type": "zip", 148 - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", 149 - "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", 150 - "shasum": "" 151 - }, 152 - "require": { 153 - "php": "^7.1 || ^8.0" 154 - }, 155 - "require-dev": { 156 - "phpstan/phpstan": "^0.12.42", 157 - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", 158 - "scrutinizer/ocular": "1.6.0", 159 - "squizlabs/php_codesniffer": "^3.5", 160 - "vimeo/psalm": "^4.0.0" 161 - }, 162 - "type": "library", 163 - "extra": { 164 - "branch-alias": { 165 - "dev-main": "3.x-dev" 166 - } 167 - }, 168 - "autoload": { 169 - "psr-4": { 170 - "Dflydev\\DotAccessData\\": "src/" 171 - } 172 - }, 173 - "notification-url": "https://packagist.org/downloads/", 174 - "license": [ 175 - "MIT" 176 - ], 177 - "authors": [ 178 - { 179 - "name": "Dragonfly Development Inc.", 180 - "email": "info@dflydev.com", 181 - "homepage": "http://dflydev.com" 182 - }, 183 - { 184 - "name": "Beau Simensen", 185 - "email": "beau@dflydev.com", 186 - "homepage": "http://beausimensen.com" 187 - }, 188 - { 189 - "name": "Carlos Frutos", 190 - "email": "carlos@kiwing.it", 191 - "homepage": "https://github.com/cfrutos" 192 - }, 193 - { 194 - "name": "Colin O'Dell", 195 - "email": "colinodell@gmail.com", 196 - "homepage": "https://www.colinodell.com" 197 - } 198 - ], 199 - "description": "Given a deep data structure, access data by dot notation.", 200 - "homepage": "https://github.com/dflydev/dflydev-dot-access-data", 201 - "keywords": [ 202 - "access", 203 - "data", 204 - "dot", 205 - "notation" 206 - ], 207 - "support": { 208 - "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", 209 - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" 210 - }, 211 - "time": "2024-07-08T12:26:09+00:00" 212 - }, 213 - { 214 - "name": "doctrine/inflector", 215 - "version": "2.1.0", 216 - "source": { 217 - "type": "git", 218 - "url": "https://github.com/doctrine/inflector.git", 219 - "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" 220 - }, 221 - "dist": { 222 - "type": "zip", 223 - "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", 224 - "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", 225 - "shasum": "" 226 - }, 227 - "require": { 228 - "php": "^7.2 || ^8.0" 229 - }, 230 - "require-dev": { 231 - "doctrine/coding-standard": "^12.0 || ^13.0", 232 - "phpstan/phpstan": "^1.12 || ^2.0", 233 - "phpstan/phpstan-phpunit": "^1.4 || ^2.0", 234 - "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", 235 - "phpunit/phpunit": "^8.5 || ^12.2" 236 - }, 237 - "type": "library", 238 - "autoload": { 239 - "psr-4": { 240 - "Doctrine\\Inflector\\": "src" 241 - } 242 - }, 243 - "notification-url": "https://packagist.org/downloads/", 244 - "license": [ 245 - "MIT" 246 - ], 247 - "authors": [ 248 - { 249 - "name": "Guilherme Blanco", 250 - "email": "guilhermeblanco@gmail.com" 251 - }, 252 - { 253 - "name": "Roman Borschel", 254 - "email": "roman@code-factory.org" 255 - }, 256 - { 257 - "name": "Benjamin Eberlei", 258 - "email": "kontakt@beberlei.de" 259 - }, 260 - { 261 - "name": "Jonathan Wage", 262 - "email": "jonwage@gmail.com" 263 - }, 264 - { 265 - "name": "Johannes Schmitt", 266 - "email": "schmittjoh@gmail.com" 267 - } 268 - ], 269 - "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", 270 - "homepage": "https://www.doctrine-project.org/projects/inflector.html", 271 - "keywords": [ 272 - "inflection", 273 - "inflector", 274 - "lowercase", 275 - "manipulation", 276 - "php", 277 - "plural", 278 - "singular", 279 - "strings", 280 - "uppercase", 281 - "words" 282 - ], 283 - "support": { 284 - "issues": "https://github.com/doctrine/inflector/issues", 285 - "source": "https://github.com/doctrine/inflector/tree/2.1.0" 286 - }, 287 - "funding": [ 288 - { 289 - "url": "https://www.doctrine-project.org/sponsorship.html", 290 - "type": "custom" 291 - }, 292 - { 293 - "url": "https://www.patreon.com/phpdoctrine", 294 - "type": "patreon" 295 - }, 296 - { 297 - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", 298 - "type": "tidelift" 299 - } 300 - ], 301 - "time": "2025-08-10T19:31:58+00:00" 302 - }, 303 - { 304 - "name": "doctrine/lexer", 305 - "version": "3.0.1", 306 - "source": { 307 - "type": "git", 308 - "url": "https://github.com/doctrine/lexer.git", 309 - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" 310 - }, 311 - "dist": { 312 - "type": "zip", 313 - "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", 314 - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", 315 - "shasum": "" 316 - }, 317 - "require": { 318 - "php": "^8.1" 319 - }, 320 - "require-dev": { 321 - "doctrine/coding-standard": "^12", 322 - "phpstan/phpstan": "^1.10", 323 - "phpunit/phpunit": "^10.5", 324 - "psalm/plugin-phpunit": "^0.18.3", 325 - "vimeo/psalm": "^5.21" 326 - }, 327 - "type": "library", 328 - "autoload": { 329 - "psr-4": { 330 - "Doctrine\\Common\\Lexer\\": "src" 331 - } 332 - }, 333 - "notification-url": "https://packagist.org/downloads/", 334 - "license": [ 335 - "MIT" 336 - ], 337 - "authors": [ 338 - { 339 - "name": "Guilherme Blanco", 340 - "email": "guilhermeblanco@gmail.com" 341 - }, 342 - { 343 - "name": "Roman Borschel", 344 - "email": "roman@code-factory.org" 345 - }, 346 - { 347 - "name": "Johannes Schmitt", 348 - "email": "schmittjoh@gmail.com" 349 - } 350 - ], 351 - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", 352 - "homepage": "https://www.doctrine-project.org/projects/lexer.html", 353 - "keywords": [ 354 - "annotations", 355 - "docblock", 356 - "lexer", 357 - "parser", 358 - "php" 359 - ], 360 - "support": { 361 - "issues": "https://github.com/doctrine/lexer/issues", 362 - "source": "https://github.com/doctrine/lexer/tree/3.0.1" 363 - }, 364 - "funding": [ 365 - { 366 - "url": "https://www.doctrine-project.org/sponsorship.html", 367 - "type": "custom" 368 - }, 369 - { 370 - "url": "https://www.patreon.com/phpdoctrine", 371 - "type": "patreon" 372 - }, 373 - { 374 - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", 375 - "type": "tidelift" 376 - } 377 - ], 378 - "time": "2024-02-05T11:56:58+00:00" 379 - }, 380 - { 381 - "name": "dragonmantank/cron-expression", 382 - "version": "v3.4.0", 383 - "source": { 384 - "type": "git", 385 - "url": "https://github.com/dragonmantank/cron-expression.git", 386 - "reference": "8c784d071debd117328803d86b2097615b457500" 387 - }, 388 - "dist": { 389 - "type": "zip", 390 - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", 391 - "reference": "8c784d071debd117328803d86b2097615b457500", 392 - "shasum": "" 393 - }, 394 - "require": { 395 - "php": "^7.2|^8.0", 396 - "webmozart/assert": "^1.0" 397 - }, 398 - "replace": { 399 - "mtdowling/cron-expression": "^1.0" 400 - }, 401 - "require-dev": { 402 - "phpstan/extension-installer": "^1.0", 403 - "phpstan/phpstan": "^1.0", 404 - "phpunit/phpunit": "^7.0|^8.0|^9.0" 405 - }, 406 - "type": "library", 407 - "extra": { 408 - "branch-alias": { 409 - "dev-master": "3.x-dev" 410 - } 411 - }, 412 - "autoload": { 413 - "psr-4": { 414 - "Cron\\": "src/Cron/" 415 - } 416 - }, 417 - "notification-url": "https://packagist.org/downloads/", 418 - "license": [ 419 - "MIT" 420 - ], 421 - "authors": [ 422 - { 423 - "name": "Chris Tankersley", 424 - "email": "chris@ctankersley.com", 425 - "homepage": "https://github.com/dragonmantank" 426 - } 427 - ], 428 - "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", 429 - "keywords": [ 430 - "cron", 431 - "schedule" 432 - ], 433 - "support": { 434 - "issues": "https://github.com/dragonmantank/cron-expression/issues", 435 - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" 436 - }, 437 - "funding": [ 438 - { 439 - "url": "https://github.com/dragonmantank", 440 - "type": "github" 441 - } 442 - ], 443 - "time": "2024-10-09T13:47:03+00:00" 444 - }, 445 - { 446 - "name": "egulias/email-validator", 447 - "version": "4.0.4", 448 - "source": { 449 - "type": "git", 450 - "url": "https://github.com/egulias/EmailValidator.git", 451 - "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" 452 - }, 453 - "dist": { 454 - "type": "zip", 455 - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", 456 - "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", 457 - "shasum": "" 458 - }, 459 - "require": { 460 - "doctrine/lexer": "^2.0 || ^3.0", 461 - "php": ">=8.1", 462 - "symfony/polyfill-intl-idn": "^1.26" 463 - }, 464 - "require-dev": { 465 - "phpunit/phpunit": "^10.2", 466 - "vimeo/psalm": "^5.12" 467 - }, 468 - "suggest": { 469 - "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" 470 - }, 471 - "type": "library", 472 - "extra": { 473 - "branch-alias": { 474 - "dev-master": "4.0.x-dev" 475 - } 476 - }, 477 - "autoload": { 478 - "psr-4": { 479 - "Egulias\\EmailValidator\\": "src" 480 - } 481 - }, 482 - "notification-url": "https://packagist.org/downloads/", 483 - "license": [ 484 - "MIT" 485 - ], 486 - "authors": [ 487 - { 488 - "name": "Eduardo Gulias Davis" 489 - } 490 - ], 491 - "description": "A library for validating emails against several RFCs", 492 - "homepage": "https://github.com/egulias/EmailValidator", 493 - "keywords": [ 494 - "email", 495 - "emailvalidation", 496 - "emailvalidator", 497 - "validation", 498 - "validator" 499 - ], 500 - "support": { 501 - "issues": "https://github.com/egulias/EmailValidator/issues", 502 - "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" 503 - }, 504 - "funding": [ 505 - { 506 - "url": "https://github.com/egulias", 507 - "type": "github" 508 - } 509 - ], 510 - "time": "2025-03-06T22:45:56+00:00" 511 - }, 512 - { 513 - "name": "evenement/evenement", 514 - "version": "v3.0.2", 515 - "source": { 516 - "type": "git", 517 - "url": "https://github.com/igorw/evenement.git", 518 - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" 519 - }, 520 - "dist": { 521 - "type": "zip", 522 - "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", 523 - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", 524 - "shasum": "" 525 - }, 526 - "require": { 527 - "php": ">=7.0" 528 - }, 529 - "require-dev": { 530 - "phpunit/phpunit": "^9 || ^6" 531 - }, 532 - "type": "library", 533 - "autoload": { 534 - "psr-4": { 535 - "Evenement\\": "src/" 536 - } 537 - }, 538 - "notification-url": "https://packagist.org/downloads/", 539 - "license": [ 540 - "MIT" 541 - ], 542 - "authors": [ 543 - { 544 - "name": "Igor Wiedler", 545 - "email": "igor@wiedler.ch" 546 - } 547 - ], 548 - "description": "ร‰vรฉnement is a very simple event dispatching library for PHP", 549 - "keywords": [ 550 - "event-dispatcher", 551 - "event-emitter" 552 - ], 553 - "support": { 554 - "issues": "https://github.com/igorw/evenement/issues", 555 - "source": "https://github.com/igorw/evenement/tree/v3.0.2" 556 - }, 557 - "time": "2023-08-08T05:53:35+00:00" 558 - }, 559 - { 560 - "name": "firebase/php-jwt", 561 - "version": "v6.11.1", 562 - "source": { 563 - "type": "git", 564 - "url": "https://github.com/firebase/php-jwt.git", 565 - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" 566 - }, 567 - "dist": { 568 - "type": "zip", 569 - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", 570 - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", 571 - "shasum": "" 572 - }, 573 - "require": { 574 - "php": "^8.0" 575 - }, 576 - "require-dev": { 577 - "guzzlehttp/guzzle": "^7.4", 578 - "phpspec/prophecy-phpunit": "^2.0", 579 - "phpunit/phpunit": "^9.5", 580 - "psr/cache": "^2.0||^3.0", 581 - "psr/http-client": "^1.0", 582 - "psr/http-factory": "^1.0" 583 - }, 584 - "suggest": { 585 - "ext-sodium": "Support EdDSA (Ed25519) signatures", 586 - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" 587 - }, 588 - "type": "library", 589 - "autoload": { 590 - "psr-4": { 591 - "Firebase\\JWT\\": "src" 592 - } 593 - }, 594 - "notification-url": "https://packagist.org/downloads/", 595 - "license": [ 596 - "BSD-3-Clause" 597 - ], 598 - "authors": [ 599 - { 600 - "name": "Neuman Vong", 601 - "email": "neuman+pear@twilio.com", 602 - "role": "Developer" 603 - }, 604 - { 605 - "name": "Anant Narayanan", 606 - "email": "anant@php.net", 607 - "role": "Developer" 608 - } 609 - ], 610 - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 611 - "homepage": "https://github.com/firebase/php-jwt", 612 - "keywords": [ 613 - "jwt", 614 - "php" 615 - ], 616 - "support": { 617 - "issues": "https://github.com/firebase/php-jwt/issues", 618 - "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" 619 - }, 620 - "time": "2025-04-09T20:32:01+00:00" 621 - }, 622 - { 623 - "name": "fruitcake/php-cors", 624 - "version": "v1.3.0", 625 - "source": { 626 - "type": "git", 627 - "url": "https://github.com/fruitcake/php-cors.git", 628 - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" 629 - }, 630 - "dist": { 631 - "type": "zip", 632 - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", 633 - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", 634 - "shasum": "" 635 - }, 636 - "require": { 637 - "php": "^7.4|^8.0", 638 - "symfony/http-foundation": "^4.4|^5.4|^6|^7" 639 - }, 640 - "require-dev": { 641 - "phpstan/phpstan": "^1.4", 642 - "phpunit/phpunit": "^9", 643 - "squizlabs/php_codesniffer": "^3.5" 644 - }, 645 - "type": "library", 646 - "extra": { 647 - "branch-alias": { 648 - "dev-master": "1.2-dev" 649 - } 650 - }, 651 - "autoload": { 652 - "psr-4": { 653 - "Fruitcake\\Cors\\": "src/" 654 - } 655 - }, 656 - "notification-url": "https://packagist.org/downloads/", 657 - "license": [ 658 - "MIT" 659 - ], 660 - "authors": [ 661 - { 662 - "name": "Fruitcake", 663 - "homepage": "https://fruitcake.nl" 664 - }, 665 - { 666 - "name": "Barryvdh", 667 - "email": "barryvdh@gmail.com" 668 - } 669 - ], 670 - "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", 671 - "homepage": "https://github.com/fruitcake/php-cors", 672 - "keywords": [ 673 - "cors", 674 - "laravel", 675 - "symfony" 676 - ], 677 - "support": { 678 - "issues": "https://github.com/fruitcake/php-cors/issues", 679 - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" 680 - }, 681 - "funding": [ 682 - { 683 - "url": "https://fruitcake.nl", 684 - "type": "custom" 685 - }, 686 - { 687 - "url": "https://github.com/barryvdh", 688 - "type": "github" 689 - } 690 - ], 691 - "time": "2023-10-12T05:21:21+00:00" 692 - }, 693 - { 694 - "name": "graham-campbell/result-type", 695 - "version": "v1.1.3", 696 - "source": { 697 - "type": "git", 698 - "url": "https://github.com/GrahamCampbell/Result-Type.git", 699 - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" 700 - }, 701 - "dist": { 702 - "type": "zip", 703 - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", 704 - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", 705 - "shasum": "" 706 - }, 707 - "require": { 708 - "php": "^7.2.5 || ^8.0", 709 - "phpoption/phpoption": "^1.9.3" 710 - }, 711 - "require-dev": { 712 - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" 713 - }, 714 - "type": "library", 715 - "autoload": { 716 - "psr-4": { 717 - "GrahamCampbell\\ResultType\\": "src/" 718 - } 719 - }, 720 - "notification-url": "https://packagist.org/downloads/", 721 - "license": [ 722 - "MIT" 723 - ], 724 - "authors": [ 725 - { 726 - "name": "Graham Campbell", 727 - "email": "hello@gjcampbell.co.uk", 728 - "homepage": "https://github.com/GrahamCampbell" 729 - } 730 - ], 731 - "description": "An Implementation Of The Result Type", 732 - "keywords": [ 733 - "Graham Campbell", 734 - "GrahamCampbell", 735 - "Result Type", 736 - "Result-Type", 737 - "result" 738 - ], 739 - "support": { 740 - "issues": "https://github.com/GrahamCampbell/Result-Type/issues", 741 - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" 742 - }, 743 - "funding": [ 744 - { 745 - "url": "https://github.com/GrahamCampbell", 746 - "type": "github" 747 - }, 748 - { 749 - "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", 750 - "type": "tidelift" 751 - } 752 - ], 753 - "time": "2024-07-20T21:45:45+00:00" 754 - }, 755 - { 756 - "name": "guzzlehttp/guzzle", 757 - "version": "7.10.0", 758 - "source": { 759 - "type": "git", 760 - "url": "https://github.com/guzzle/guzzle.git", 761 - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" 762 - }, 763 - "dist": { 764 - "type": "zip", 765 - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", 766 - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", 767 - "shasum": "" 768 - }, 769 - "require": { 770 - "ext-json": "*", 771 - "guzzlehttp/promises": "^2.3", 772 - "guzzlehttp/psr7": "^2.8", 773 - "php": "^7.2.5 || ^8.0", 774 - "psr/http-client": "^1.0", 775 - "symfony/deprecation-contracts": "^2.2 || ^3.0" 776 - }, 777 - "provide": { 778 - "psr/http-client-implementation": "1.0" 779 - }, 780 - "require-dev": { 781 - "bamarni/composer-bin-plugin": "^1.8.2", 782 - "ext-curl": "*", 783 - "guzzle/client-integration-tests": "3.0.2", 784 - "php-http/message-factory": "^1.1", 785 - "phpunit/phpunit": "^8.5.39 || ^9.6.20", 786 - "psr/log": "^1.1 || ^2.0 || ^3.0" 787 - }, 788 - "suggest": { 789 - "ext-curl": "Required for CURL handler support", 790 - "ext-intl": "Required for Internationalized Domain Name (IDN) support", 791 - "psr/log": "Required for using the Log middleware" 792 - }, 793 - "type": "library", 794 - "extra": { 795 - "bamarni-bin": { 796 - "bin-links": true, 797 - "forward-command": false 798 - } 799 - }, 800 - "autoload": { 801 - "files": [ 802 - "src/functions_include.php" 803 - ], 804 - "psr-4": { 805 - "GuzzleHttp\\": "src/" 806 - } 807 - }, 808 - "notification-url": "https://packagist.org/downloads/", 809 - "license": [ 810 - "MIT" 811 - ], 812 - "authors": [ 813 - { 814 - "name": "Graham Campbell", 815 - "email": "hello@gjcampbell.co.uk", 816 - "homepage": "https://github.com/GrahamCampbell" 817 - }, 818 - { 819 - "name": "Michael Dowling", 820 - "email": "mtdowling@gmail.com", 821 - "homepage": "https://github.com/mtdowling" 822 - }, 823 - { 824 - "name": "Jeremy Lindblom", 825 - "email": "jeremeamia@gmail.com", 826 - "homepage": "https://github.com/jeremeamia" 827 - }, 828 - { 829 - "name": "George Mponos", 830 - "email": "gmponos@gmail.com", 831 - "homepage": "https://github.com/gmponos" 832 - }, 833 - { 834 - "name": "Tobias Nyholm", 835 - "email": "tobias.nyholm@gmail.com", 836 - "homepage": "https://github.com/Nyholm" 837 - }, 838 - { 839 - "name": "Mรกrk Sรกgi-Kazรกr", 840 - "email": "mark.sagikazar@gmail.com", 841 - "homepage": "https://github.com/sagikazarmark" 842 - }, 843 - { 844 - "name": "Tobias Schultze", 845 - "email": "webmaster@tubo-world.de", 846 - "homepage": "https://github.com/Tobion" 847 - } 848 - ], 849 - "description": "Guzzle is a PHP HTTP client library", 850 - "keywords": [ 851 - "client", 852 - "curl", 853 - "framework", 854 - "http", 855 - "http client", 856 - "psr-18", 857 - "psr-7", 858 - "rest", 859 - "web service" 860 - ], 861 - "support": { 862 - "issues": "https://github.com/guzzle/guzzle/issues", 863 - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" 864 - }, 865 - "funding": [ 866 - { 867 - "url": "https://github.com/GrahamCampbell", 868 - "type": "github" 869 - }, 870 - { 871 - "url": "https://github.com/Nyholm", 872 - "type": "github" 873 - }, 874 - { 875 - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", 876 - "type": "tidelift" 877 - } 878 - ], 879 - "time": "2025-08-23T22:36:01+00:00" 880 - }, 881 - { 882 - "name": "guzzlehttp/promises", 883 - "version": "2.3.0", 884 - "source": { 885 - "type": "git", 886 - "url": "https://github.com/guzzle/promises.git", 887 - "reference": "481557b130ef3790cf82b713667b43030dc9c957" 888 - }, 889 - "dist": { 890 - "type": "zip", 891 - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", 892 - "reference": "481557b130ef3790cf82b713667b43030dc9c957", 893 - "shasum": "" 894 - }, 895 - "require": { 896 - "php": "^7.2.5 || ^8.0" 897 - }, 898 - "require-dev": { 899 - "bamarni/composer-bin-plugin": "^1.8.2", 900 - "phpunit/phpunit": "^8.5.44 || ^9.6.25" 901 - }, 902 - "type": "library", 903 - "extra": { 904 - "bamarni-bin": { 905 - "bin-links": true, 906 - "forward-command": false 907 - } 908 - }, 909 - "autoload": { 910 - "psr-4": { 911 - "GuzzleHttp\\Promise\\": "src/" 912 - } 913 - }, 914 - "notification-url": "https://packagist.org/downloads/", 915 - "license": [ 916 - "MIT" 917 - ], 918 - "authors": [ 919 - { 920 - "name": "Graham Campbell", 921 - "email": "hello@gjcampbell.co.uk", 922 - "homepage": "https://github.com/GrahamCampbell" 923 - }, 924 - { 925 - "name": "Michael Dowling", 926 - "email": "mtdowling@gmail.com", 927 - "homepage": "https://github.com/mtdowling" 928 - }, 929 - { 930 - "name": "Tobias Nyholm", 931 - "email": "tobias.nyholm@gmail.com", 932 - "homepage": "https://github.com/Nyholm" 933 - }, 934 - { 935 - "name": "Tobias Schultze", 936 - "email": "webmaster@tubo-world.de", 937 - "homepage": "https://github.com/Tobion" 938 - } 939 - ], 940 - "description": "Guzzle promises library", 941 - "keywords": [ 942 - "promise" 943 - ], 944 - "support": { 945 - "issues": "https://github.com/guzzle/promises/issues", 946 - "source": "https://github.com/guzzle/promises/tree/2.3.0" 947 - }, 948 - "funding": [ 949 - { 950 - "url": "https://github.com/GrahamCampbell", 951 - "type": "github" 952 - }, 953 - { 954 - "url": "https://github.com/Nyholm", 955 - "type": "github" 956 - }, 957 - { 958 - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", 959 - "type": "tidelift" 960 - } 961 - ], 962 - "time": "2025-08-22T14:34:08+00:00" 963 - }, 964 - { 965 - "name": "guzzlehttp/psr7", 966 - "version": "2.8.0", 967 - "source": { 968 - "type": "git", 969 - "url": "https://github.com/guzzle/psr7.git", 970 - "reference": "21dc724a0583619cd1652f673303492272778051" 971 - }, 972 - "dist": { 973 - "type": "zip", 974 - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", 975 - "reference": "21dc724a0583619cd1652f673303492272778051", 976 - "shasum": "" 977 - }, 978 - "require": { 979 - "php": "^7.2.5 || ^8.0", 980 - "psr/http-factory": "^1.0", 981 - "psr/http-message": "^1.1 || ^2.0", 982 - "ralouphie/getallheaders": "^3.0" 983 - }, 984 - "provide": { 985 - "psr/http-factory-implementation": "1.0", 986 - "psr/http-message-implementation": "1.0" 987 - }, 988 - "require-dev": { 989 - "bamarni/composer-bin-plugin": "^1.8.2", 990 - "http-interop/http-factory-tests": "0.9.0", 991 - "phpunit/phpunit": "^8.5.44 || ^9.6.25" 992 - }, 993 - "suggest": { 994 - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" 995 - }, 996 - "type": "library", 997 - "extra": { 998 - "bamarni-bin": { 999 - "bin-links": true, 1000 - "forward-command": false 1001 - } 1002 - }, 1003 - "autoload": { 1004 - "psr-4": { 1005 - "GuzzleHttp\\Psr7\\": "src/" 1006 - } 1007 - }, 1008 - "notification-url": "https://packagist.org/downloads/", 1009 - "license": [ 1010 - "MIT" 1011 - ], 1012 - "authors": [ 1013 - { 1014 - "name": "Graham Campbell", 1015 - "email": "hello@gjcampbell.co.uk", 1016 - "homepage": "https://github.com/GrahamCampbell" 1017 - }, 1018 - { 1019 - "name": "Michael Dowling", 1020 - "email": "mtdowling@gmail.com", 1021 - "homepage": "https://github.com/mtdowling" 1022 - }, 1023 - { 1024 - "name": "George Mponos", 1025 - "email": "gmponos@gmail.com", 1026 - "homepage": "https://github.com/gmponos" 1027 - }, 1028 - { 1029 - "name": "Tobias Nyholm", 1030 - "email": "tobias.nyholm@gmail.com", 1031 - "homepage": "https://github.com/Nyholm" 1032 - }, 1033 - { 1034 - "name": "Mรกrk Sรกgi-Kazรกr", 1035 - "email": "mark.sagikazar@gmail.com", 1036 - "homepage": "https://github.com/sagikazarmark" 1037 - }, 1038 - { 1039 - "name": "Tobias Schultze", 1040 - "email": "webmaster@tubo-world.de", 1041 - "homepage": "https://github.com/Tobion" 1042 - }, 1043 - { 1044 - "name": "Mรกrk Sรกgi-Kazรกr", 1045 - "email": "mark.sagikazar@gmail.com", 1046 - "homepage": "https://sagikazarmark.hu" 1047 - } 1048 - ], 1049 - "description": "PSR-7 message implementation that also provides common utility methods", 1050 - "keywords": [ 1051 - "http", 1052 - "message", 1053 - "psr-7", 1054 - "request", 1055 - "response", 1056 - "stream", 1057 - "uri", 1058 - "url" 1059 - ], 1060 - "support": { 1061 - "issues": "https://github.com/guzzle/psr7/issues", 1062 - "source": "https://github.com/guzzle/psr7/tree/2.8.0" 1063 - }, 1064 - "funding": [ 1065 - { 1066 - "url": "https://github.com/GrahamCampbell", 1067 - "type": "github" 1068 - }, 1069 - { 1070 - "url": "https://github.com/Nyholm", 1071 - "type": "github" 1072 - }, 1073 - { 1074 - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", 1075 - "type": "tidelift" 1076 - } 1077 - ], 1078 - "time": "2025-08-23T21:21:41+00:00" 1079 - }, 1080 - { 1081 - "name": "guzzlehttp/uri-template", 1082 - "version": "v1.0.5", 1083 - "source": { 1084 - "type": "git", 1085 - "url": "https://github.com/guzzle/uri-template.git", 1086 - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" 1087 - }, 1088 - "dist": { 1089 - "type": "zip", 1090 - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", 1091 - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", 1092 - "shasum": "" 1093 - }, 1094 - "require": { 1095 - "php": "^7.2.5 || ^8.0", 1096 - "symfony/polyfill-php80": "^1.24" 1097 - }, 1098 - "require-dev": { 1099 - "bamarni/composer-bin-plugin": "^1.8.2", 1100 - "phpunit/phpunit": "^8.5.44 || ^9.6.25", 1101 - "uri-template/tests": "1.0.0" 1102 - }, 1103 - "type": "library", 1104 - "extra": { 1105 - "bamarni-bin": { 1106 - "bin-links": true, 1107 - "forward-command": false 1108 - } 1109 - }, 1110 - "autoload": { 1111 - "psr-4": { 1112 - "GuzzleHttp\\UriTemplate\\": "src" 1113 - } 1114 - }, 1115 - "notification-url": "https://packagist.org/downloads/", 1116 - "license": [ 1117 - "MIT" 1118 - ], 1119 - "authors": [ 1120 - { 1121 - "name": "Graham Campbell", 1122 - "email": "hello@gjcampbell.co.uk", 1123 - "homepage": "https://github.com/GrahamCampbell" 1124 - }, 1125 - { 1126 - "name": "Michael Dowling", 1127 - "email": "mtdowling@gmail.com", 1128 - "homepage": "https://github.com/mtdowling" 1129 - }, 1130 - { 1131 - "name": "George Mponos", 1132 - "email": "gmponos@gmail.com", 1133 - "homepage": "https://github.com/gmponos" 1134 - }, 1135 - { 1136 - "name": "Tobias Nyholm", 1137 - "email": "tobias.nyholm@gmail.com", 1138 - "homepage": "https://github.com/Nyholm" 1139 - } 1140 - ], 1141 - "description": "A polyfill class for uri_template of PHP", 1142 - "keywords": [ 1143 - "guzzlehttp", 1144 - "uri-template" 1145 - ], 1146 - "support": { 1147 - "issues": "https://github.com/guzzle/uri-template/issues", 1148 - "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" 1149 - }, 1150 - "funding": [ 1151 - { 1152 - "url": "https://github.com/GrahamCampbell", 1153 - "type": "github" 1154 - }, 1155 - { 1156 - "url": "https://github.com/Nyholm", 1157 - "type": "github" 1158 - }, 1159 - { 1160 - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", 1161 - "type": "tidelift" 1162 - } 1163 - ], 1164 - "time": "2025-08-22T14:27:06+00:00" 1165 - }, 1166 - { 1167 - "name": "laravel/framework", 1168 - "version": "v11.46.1", 1169 - "source": { 1170 - "type": "git", 1171 - "url": "https://github.com/laravel/framework.git", 1172 - "reference": "5fd457f807570a962a53b403b1346efe4cc80bb8" 1173 - }, 1174 - "dist": { 1175 - "type": "zip", 1176 - "url": "https://api.github.com/repos/laravel/framework/zipball/5fd457f807570a962a53b403b1346efe4cc80bb8", 1177 - "reference": "5fd457f807570a962a53b403b1346efe4cc80bb8", 1178 - "shasum": "" 1179 - }, 1180 - "require": { 1181 - "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12|^0.13|^0.14", 1182 - "composer-runtime-api": "^2.2", 1183 - "doctrine/inflector": "^2.0.5", 1184 - "dragonmantank/cron-expression": "^3.4", 1185 - "egulias/email-validator": "^3.2.1|^4.0", 1186 - "ext-ctype": "*", 1187 - "ext-filter": "*", 1188 - "ext-hash": "*", 1189 - "ext-mbstring": "*", 1190 - "ext-openssl": "*", 1191 - "ext-session": "*", 1192 - "ext-tokenizer": "*", 1193 - "fruitcake/php-cors": "^1.3", 1194 - "guzzlehttp/guzzle": "^7.8.2", 1195 - "guzzlehttp/uri-template": "^1.0", 1196 - "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", 1197 - "laravel/serializable-closure": "^1.3|^2.0", 1198 - "league/commonmark": "^2.7", 1199 - "league/flysystem": "^3.25.1", 1200 - "league/flysystem-local": "^3.25.1", 1201 - "league/uri": "^7.5.1", 1202 - "monolog/monolog": "^3.0", 1203 - "nesbot/carbon": "^2.72.6|^3.8.4", 1204 - "nunomaduro/termwind": "^2.0", 1205 - "php": "^8.2", 1206 - "psr/container": "^1.1.1|^2.0.1", 1207 - "psr/log": "^1.0|^2.0|^3.0", 1208 - "psr/simple-cache": "^1.0|^2.0|^3.0", 1209 - "ramsey/uuid": "^4.7", 1210 - "symfony/console": "^7.0.3", 1211 - "symfony/error-handler": "^7.0.3", 1212 - "symfony/finder": "^7.0.3", 1213 - "symfony/http-foundation": "^7.2.0", 1214 - "symfony/http-kernel": "^7.0.3", 1215 - "symfony/mailer": "^7.0.3", 1216 - "symfony/mime": "^7.0.3", 1217 - "symfony/polyfill-php83": "^1.31", 1218 - "symfony/process": "^7.0.3", 1219 - "symfony/routing": "^7.0.3", 1220 - "symfony/uid": "^7.0.3", 1221 - "symfony/var-dumper": "^7.0.3", 1222 - "tijsverkoyen/css-to-inline-styles": "^2.2.5", 1223 - "vlucas/phpdotenv": "^5.6.1", 1224 - "voku/portable-ascii": "^2.0.2" 1225 - }, 1226 - "conflict": { 1227 - "tightenco/collect": "<5.5.33" 1228 - }, 1229 - "provide": { 1230 - "psr/container-implementation": "1.1|2.0", 1231 - "psr/log-implementation": "1.0|2.0|3.0", 1232 - "psr/simple-cache-implementation": "1.0|2.0|3.0" 1233 - }, 1234 - "replace": { 1235 - "illuminate/auth": "self.version", 1236 - "illuminate/broadcasting": "self.version", 1237 - "illuminate/bus": "self.version", 1238 - "illuminate/cache": "self.version", 1239 - "illuminate/collections": "self.version", 1240 - "illuminate/concurrency": "self.version", 1241 - "illuminate/conditionable": "self.version", 1242 - "illuminate/config": "self.version", 1243 - "illuminate/console": "self.version", 1244 - "illuminate/container": "self.version", 1245 - "illuminate/contracts": "self.version", 1246 - "illuminate/cookie": "self.version", 1247 - "illuminate/database": "self.version", 1248 - "illuminate/encryption": "self.version", 1249 - "illuminate/events": "self.version", 1250 - "illuminate/filesystem": "self.version", 1251 - "illuminate/hashing": "self.version", 1252 - "illuminate/http": "self.version", 1253 - "illuminate/log": "self.version", 1254 - "illuminate/macroable": "self.version", 1255 - "illuminate/mail": "self.version", 1256 - "illuminate/notifications": "self.version", 1257 - "illuminate/pagination": "self.version", 1258 - "illuminate/pipeline": "self.version", 1259 - "illuminate/process": "self.version", 1260 - "illuminate/queue": "self.version", 1261 - "illuminate/redis": "self.version", 1262 - "illuminate/routing": "self.version", 1263 - "illuminate/session": "self.version", 1264 - "illuminate/support": "self.version", 1265 - "illuminate/testing": "self.version", 1266 - "illuminate/translation": "self.version", 1267 - "illuminate/validation": "self.version", 1268 - "illuminate/view": "self.version", 1269 - "spatie/once": "*" 1270 - }, 1271 - "require-dev": { 1272 - "ably/ably-php": "^1.0", 1273 - "aws/aws-sdk-php": "^3.322.9", 1274 - "ext-gmp": "*", 1275 - "fakerphp/faker": "^1.24", 1276 - "guzzlehttp/promises": "^2.0.3", 1277 - "guzzlehttp/psr7": "^2.4", 1278 - "laravel/pint": "^1.18", 1279 - "league/flysystem-aws-s3-v3": "^3.25.1", 1280 - "league/flysystem-ftp": "^3.25.1", 1281 - "league/flysystem-path-prefixing": "^3.25.1", 1282 - "league/flysystem-read-only": "^3.25.1", 1283 - "league/flysystem-sftp-v3": "^3.25.1", 1284 - "mockery/mockery": "^1.6.10", 1285 - "orchestra/testbench-core": "^9.16.1", 1286 - "pda/pheanstalk": "^5.0.6", 1287 - "php-http/discovery": "^1.15", 1288 - "phpstan/phpstan": "^2.0", 1289 - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", 1290 - "predis/predis": "^2.3", 1291 - "resend/resend-php": "^0.10.0", 1292 - "symfony/cache": "^7.0.3", 1293 - "symfony/http-client": "^7.0.3", 1294 - "symfony/psr-http-message-bridge": "^7.0.3", 1295 - "symfony/translation": "^7.0.3" 1296 - }, 1297 - "suggest": { 1298 - "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", 1299 - "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", 1300 - "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", 1301 - "ext-apcu": "Required to use the APC cache driver.", 1302 - "ext-fileinfo": "Required to use the Filesystem class.", 1303 - "ext-ftp": "Required to use the Flysystem FTP driver.", 1304 - "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", 1305 - "ext-memcached": "Required to use the memcache cache driver.", 1306 - "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", 1307 - "ext-pdo": "Required to use all database features.", 1308 - "ext-posix": "Required to use all features of the queue worker.", 1309 - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", 1310 - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", 1311 - "filp/whoops": "Required for friendly error pages in development (^2.14.3).", 1312 - "laravel/tinker": "Required to use the tinker console command (^2.0).", 1313 - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", 1314 - "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", 1315 - "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", 1316 - "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", 1317 - "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", 1318 - "mockery/mockery": "Required to use mocking (^1.6).", 1319 - "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", 1320 - "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", 1321 - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).", 1322 - "predis/predis": "Required to use the predis connector (^2.3).", 1323 - "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", 1324 - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", 1325 - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", 1326 - "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", 1327 - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", 1328 - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", 1329 - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", 1330 - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", 1331 - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." 1332 - }, 1333 - "type": "library", 1334 - "extra": { 1335 - "branch-alias": { 1336 - "dev-master": "11.x-dev" 1337 - } 1338 - }, 1339 - "autoload": { 1340 - "files": [ 1341 - "src/Illuminate/Collections/functions.php", 1342 - "src/Illuminate/Collections/helpers.php", 1343 - "src/Illuminate/Events/functions.php", 1344 - "src/Illuminate/Filesystem/functions.php", 1345 - "src/Illuminate/Foundation/helpers.php", 1346 - "src/Illuminate/Log/functions.php", 1347 - "src/Illuminate/Support/functions.php", 1348 - "src/Illuminate/Support/helpers.php" 1349 - ], 1350 - "psr-4": { 1351 - "Illuminate\\": "src/Illuminate/", 1352 - "Illuminate\\Support\\": [ 1353 - "src/Illuminate/Macroable/", 1354 - "src/Illuminate/Collections/", 1355 - "src/Illuminate/Conditionable/" 1356 - ] 1357 - } 1358 - }, 1359 - "notification-url": "https://packagist.org/downloads/", 1360 - "license": [ 1361 - "MIT" 1362 - ], 1363 - "authors": [ 1364 - { 1365 - "name": "Taylor Otwell", 1366 - "email": "taylor@laravel.com" 1367 - } 1368 - ], 1369 - "description": "The Laravel Framework.", 1370 - "homepage": "https://laravel.com", 1371 - "keywords": [ 1372 - "framework", 1373 - "laravel" 1374 - ], 1375 - "support": { 1376 - "issues": "https://github.com/laravel/framework/issues", 1377 - "source": "https://github.com/laravel/framework" 1378 - }, 1379 - "time": "2025-09-30T14:51:32+00:00" 1380 - }, 1381 - { 1382 - "name": "laravel/prompts", 1383 - "version": "v0.3.7", 1384 - "source": { 1385 - "type": "git", 1386 - "url": "https://github.com/laravel/prompts.git", 1387 - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" 1388 - }, 1389 - "dist": { 1390 - "type": "zip", 1391 - "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", 1392 - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", 1393 - "shasum": "" 1394 - }, 1395 - "require": { 1396 - "composer-runtime-api": "^2.2", 1397 - "ext-mbstring": "*", 1398 - "php": "^8.1", 1399 - "symfony/console": "^6.2|^7.0" 1400 - }, 1401 - "conflict": { 1402 - "illuminate/console": ">=10.17.0 <10.25.0", 1403 - "laravel/framework": ">=10.17.0 <10.25.0" 1404 - }, 1405 - "require-dev": { 1406 - "illuminate/collections": "^10.0|^11.0|^12.0", 1407 - "mockery/mockery": "^1.5", 1408 - "pestphp/pest": "^2.3|^3.4", 1409 - "phpstan/phpstan": "^1.12.28", 1410 - "phpstan/phpstan-mockery": "^1.1.3" 1411 - }, 1412 - "suggest": { 1413 - "ext-pcntl": "Required for the spinner to be animated." 1414 - }, 1415 - "type": "library", 1416 - "extra": { 1417 - "branch-alias": { 1418 - "dev-main": "0.3.x-dev" 1419 - } 1420 - }, 1421 - "autoload": { 1422 - "files": [ 1423 - "src/helpers.php" 1424 - ], 1425 - "psr-4": { 1426 - "Laravel\\Prompts\\": "src/" 1427 - } 1428 - }, 1429 - "notification-url": "https://packagist.org/downloads/", 1430 - "license": [ 1431 - "MIT" 1432 - ], 1433 - "description": "Add beautiful and user-friendly forms to your command-line applications.", 1434 - "support": { 1435 - "issues": "https://github.com/laravel/prompts/issues", 1436 - "source": "https://github.com/laravel/prompts/tree/v0.3.7" 1437 - }, 1438 - "time": "2025-09-19T13:47:56+00:00" 1439 - }, 1440 - { 1441 - "name": "laravel/serializable-closure", 1442 - "version": "v2.0.6", 1443 - "source": { 1444 - "type": "git", 1445 - "url": "https://github.com/laravel/serializable-closure.git", 1446 - "reference": "038ce42edee619599a1debb7e81d7b3759492819" 1447 - }, 1448 - "dist": { 1449 - "type": "zip", 1450 - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", 1451 - "reference": "038ce42edee619599a1debb7e81d7b3759492819", 1452 - "shasum": "" 1453 - }, 1454 - "require": { 1455 - "php": "^8.1" 1456 - }, 1457 - "require-dev": { 1458 - "illuminate/support": "^10.0|^11.0|^12.0", 1459 - "nesbot/carbon": "^2.67|^3.0", 1460 - "pestphp/pest": "^2.36|^3.0", 1461 - "phpstan/phpstan": "^2.0", 1462 - "symfony/var-dumper": "^6.2.0|^7.0.0" 1463 - }, 1464 - "type": "library", 1465 - "extra": { 1466 - "branch-alias": { 1467 - "dev-master": "2.x-dev" 1468 - } 1469 - }, 1470 - "autoload": { 1471 - "psr-4": { 1472 - "Laravel\\SerializableClosure\\": "src/" 1473 - } 1474 - }, 1475 - "notification-url": "https://packagist.org/downloads/", 1476 - "license": [ 1477 - "MIT" 1478 - ], 1479 - "authors": [ 1480 - { 1481 - "name": "Taylor Otwell", 1482 - "email": "taylor@laravel.com" 1483 - }, 1484 - { 1485 - "name": "Nuno Maduro", 1486 - "email": "nuno@laravel.com" 1487 - } 1488 - ], 1489 - "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", 1490 - "keywords": [ 1491 - "closure", 1492 - "laravel", 1493 - "serializable" 1494 - ], 1495 - "support": { 1496 - "issues": "https://github.com/laravel/serializable-closure/issues", 1497 - "source": "https://github.com/laravel/serializable-closure" 1498 - }, 1499 - "time": "2025-10-09T13:42:30+00:00" 1500 - }, 1501 - { 1502 - "name": "laravel/socialite", 1503 - "version": "v5.23.1", 1504 - "source": { 1505 - "type": "git", 1506 - "url": "https://github.com/laravel/socialite.git", 1507 - "reference": "83d7523c97c1101d288126948947891319eef800" 1508 - }, 1509 - "dist": { 1510 - "type": "zip", 1511 - "url": "https://api.github.com/repos/laravel/socialite/zipball/83d7523c97c1101d288126948947891319eef800", 1512 - "reference": "83d7523c97c1101d288126948947891319eef800", 1513 - "shasum": "" 1514 - }, 1515 - "require": { 1516 - "ext-json": "*", 1517 - "firebase/php-jwt": "^6.4", 1518 - "guzzlehttp/guzzle": "^6.0|^7.0", 1519 - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 1520 - "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 1521 - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 1522 - "league/oauth1-client": "^1.11", 1523 - "php": "^7.2|^8.0", 1524 - "phpseclib/phpseclib": "^3.0" 1525 - }, 1526 - "require-dev": { 1527 - "mockery/mockery": "^1.0", 1528 - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 1529 - "phpstan/phpstan": "^1.12.23", 1530 - "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" 1531 - }, 1532 - "type": "library", 1533 - "extra": { 1534 - "laravel": { 1535 - "aliases": { 1536 - "Socialite": "Laravel\\Socialite\\Facades\\Socialite" 1537 - }, 1538 - "providers": [ 1539 - "Laravel\\Socialite\\SocialiteServiceProvider" 1540 - ] 1541 - }, 1542 - "branch-alias": { 1543 - "dev-master": "5.x-dev" 1544 - } 1545 - }, 1546 - "autoload": { 1547 - "psr-4": { 1548 - "Laravel\\Socialite\\": "src/" 1549 - } 1550 - }, 1551 - "notification-url": "https://packagist.org/downloads/", 1552 - "license": [ 1553 - "MIT" 1554 - ], 1555 - "authors": [ 1556 - { 1557 - "name": "Taylor Otwell", 1558 - "email": "taylor@laravel.com" 1559 - } 1560 - ], 1561 - "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", 1562 - "homepage": "https://laravel.com", 1563 - "keywords": [ 1564 - "laravel", 1565 - "oauth" 1566 - ], 1567 - "support": { 1568 - "issues": "https://github.com/laravel/socialite/issues", 1569 - "source": "https://github.com/laravel/socialite" 1570 - }, 1571 - "time": "2025-10-27T15:36:41+00:00" 1572 - }, 1573 - { 1574 - "name": "league/commonmark", 1575 - "version": "2.7.1", 1576 - "source": { 1577 - "type": "git", 1578 - "url": "https://github.com/thephpleague/commonmark.git", 1579 - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" 1580 - }, 1581 - "dist": { 1582 - "type": "zip", 1583 - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", 1584 - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", 1585 - "shasum": "" 1586 - }, 1587 - "require": { 1588 - "ext-mbstring": "*", 1589 - "league/config": "^1.1.1", 1590 - "php": "^7.4 || ^8.0", 1591 - "psr/event-dispatcher": "^1.0", 1592 - "symfony/deprecation-contracts": "^2.1 || ^3.0", 1593 - "symfony/polyfill-php80": "^1.16" 1594 - }, 1595 - "require-dev": { 1596 - "cebe/markdown": "^1.0", 1597 - "commonmark/cmark": "0.31.1", 1598 - "commonmark/commonmark.js": "0.31.1", 1599 - "composer/package-versions-deprecated": "^1.8", 1600 - "embed/embed": "^4.4", 1601 - "erusev/parsedown": "^1.0", 1602 - "ext-json": "*", 1603 - "github/gfm": "0.29.0", 1604 - "michelf/php-markdown": "^1.4 || ^2.0", 1605 - "nyholm/psr7": "^1.5", 1606 - "phpstan/phpstan": "^1.8.2", 1607 - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", 1608 - "scrutinizer/ocular": "^1.8.1", 1609 - "symfony/finder": "^5.3 | ^6.0 | ^7.0", 1610 - "symfony/process": "^5.4 | ^6.0 | ^7.0", 1611 - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", 1612 - "unleashedtech/php-coding-standard": "^3.1.1", 1613 - "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" 1614 - }, 1615 - "suggest": { 1616 - "symfony/yaml": "v2.3+ required if using the Front Matter extension" 1617 - }, 1618 - "type": "library", 1619 - "extra": { 1620 - "branch-alias": { 1621 - "dev-main": "2.8-dev" 1622 - } 1623 - }, 1624 - "autoload": { 1625 - "psr-4": { 1626 - "League\\CommonMark\\": "src" 1627 - } 1628 - }, 1629 - "notification-url": "https://packagist.org/downloads/", 1630 - "license": [ 1631 - "BSD-3-Clause" 1632 - ], 1633 - "authors": [ 1634 - { 1635 - "name": "Colin O'Dell", 1636 - "email": "colinodell@gmail.com", 1637 - "homepage": "https://www.colinodell.com", 1638 - "role": "Lead Developer" 1639 - } 1640 - ], 1641 - "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", 1642 - "homepage": "https://commonmark.thephpleague.com", 1643 - "keywords": [ 1644 - "commonmark", 1645 - "flavored", 1646 - "gfm", 1647 - "github", 1648 - "github-flavored", 1649 - "markdown", 1650 - "md", 1651 - "parser" 1652 - ], 1653 - "support": { 1654 - "docs": "https://commonmark.thephpleague.com/", 1655 - "forum": "https://github.com/thephpleague/commonmark/discussions", 1656 - "issues": "https://github.com/thephpleague/commonmark/issues", 1657 - "rss": "https://github.com/thephpleague/commonmark/releases.atom", 1658 - "source": "https://github.com/thephpleague/commonmark" 1659 - }, 1660 - "funding": [ 1661 - { 1662 - "url": "https://www.colinodell.com/sponsor", 1663 - "type": "custom" 1664 - }, 1665 - { 1666 - "url": "https://www.paypal.me/colinpodell/10.00", 1667 - "type": "custom" 1668 - }, 1669 - { 1670 - "url": "https://github.com/colinodell", 1671 - "type": "github" 1672 - }, 1673 - { 1674 - "url": "https://tidelift.com/funding/github/packagist/league/commonmark", 1675 - "type": "tidelift" 1676 - } 1677 - ], 1678 - "time": "2025-07-20T12:47:49+00:00" 1679 - }, 1680 - { 1681 - "name": "league/config", 1682 - "version": "v1.2.0", 1683 - "source": { 1684 - "type": "git", 1685 - "url": "https://github.com/thephpleague/config.git", 1686 - "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" 1687 - }, 1688 - "dist": { 1689 - "type": "zip", 1690 - "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", 1691 - "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", 1692 - "shasum": "" 1693 - }, 1694 - "require": { 1695 - "dflydev/dot-access-data": "^3.0.1", 1696 - "nette/schema": "^1.2", 1697 - "php": "^7.4 || ^8.0" 1698 - }, 1699 - "require-dev": { 1700 - "phpstan/phpstan": "^1.8.2", 1701 - "phpunit/phpunit": "^9.5.5", 1702 - "scrutinizer/ocular": "^1.8.1", 1703 - "unleashedtech/php-coding-standard": "^3.1", 1704 - "vimeo/psalm": "^4.7.3" 1705 - }, 1706 - "type": "library", 1707 - "extra": { 1708 - "branch-alias": { 1709 - "dev-main": "1.2-dev" 1710 - } 1711 - }, 1712 - "autoload": { 1713 - "psr-4": { 1714 - "League\\Config\\": "src" 1715 - } 1716 - }, 1717 - "notification-url": "https://packagist.org/downloads/", 1718 - "license": [ 1719 - "BSD-3-Clause" 1720 - ], 1721 - "authors": [ 1722 - { 1723 - "name": "Colin O'Dell", 1724 - "email": "colinodell@gmail.com", 1725 - "homepage": "https://www.colinodell.com", 1726 - "role": "Lead Developer" 1727 - } 1728 - ], 1729 - "description": "Define configuration arrays with strict schemas and access values with dot notation", 1730 - "homepage": "https://config.thephpleague.com", 1731 - "keywords": [ 1732 - "array", 1733 - "config", 1734 - "configuration", 1735 - "dot", 1736 - "dot-access", 1737 - "nested", 1738 - "schema" 1739 - ], 1740 - "support": { 1741 - "docs": "https://config.thephpleague.com/", 1742 - "issues": "https://github.com/thephpleague/config/issues", 1743 - "rss": "https://github.com/thephpleague/config/releases.atom", 1744 - "source": "https://github.com/thephpleague/config" 1745 - }, 1746 - "funding": [ 1747 - { 1748 - "url": "https://www.colinodell.com/sponsor", 1749 - "type": "custom" 1750 - }, 1751 - { 1752 - "url": "https://www.paypal.me/colinpodell/10.00", 1753 - "type": "custom" 1754 - }, 1755 - { 1756 - "url": "https://github.com/colinodell", 1757 - "type": "github" 1758 - } 1759 - ], 1760 - "time": "2022-12-11T20:36:23+00:00" 1761 - }, 1762 - { 1763 - "name": "league/flysystem", 1764 - "version": "3.30.1", 1765 - "source": { 1766 - "type": "git", 1767 - "url": "https://github.com/thephpleague/flysystem.git", 1768 - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da" 1769 - }, 1770 - "dist": { 1771 - "type": "zip", 1772 - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da", 1773 - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da", 1774 - "shasum": "" 1775 - }, 1776 - "require": { 1777 - "league/flysystem-local": "^3.0.0", 1778 - "league/mime-type-detection": "^1.0.0", 1779 - "php": "^8.0.2" 1780 - }, 1781 - "conflict": { 1782 - "async-aws/core": "<1.19.0", 1783 - "async-aws/s3": "<1.14.0", 1784 - "aws/aws-sdk-php": "3.209.31 || 3.210.0", 1785 - "guzzlehttp/guzzle": "<7.0", 1786 - "guzzlehttp/ringphp": "<1.1.1", 1787 - "phpseclib/phpseclib": "3.0.15", 1788 - "symfony/http-client": "<5.2" 1789 - }, 1790 - "require-dev": { 1791 - "async-aws/s3": "^1.5 || ^2.0", 1792 - "async-aws/simple-s3": "^1.1 || ^2.0", 1793 - "aws/aws-sdk-php": "^3.295.10", 1794 - "composer/semver": "^3.0", 1795 - "ext-fileinfo": "*", 1796 - "ext-ftp": "*", 1797 - "ext-mongodb": "^1.3|^2", 1798 - "ext-zip": "*", 1799 - "friendsofphp/php-cs-fixer": "^3.5", 1800 - "google/cloud-storage": "^1.23", 1801 - "guzzlehttp/psr7": "^2.6", 1802 - "microsoft/azure-storage-blob": "^1.1", 1803 - "mongodb/mongodb": "^1.2|^2", 1804 - "phpseclib/phpseclib": "^3.0.36", 1805 - "phpstan/phpstan": "^1.10", 1806 - "phpunit/phpunit": "^9.5.11|^10.0", 1807 - "sabre/dav": "^4.6.0" 1808 - }, 1809 - "type": "library", 1810 - "autoload": { 1811 - "psr-4": { 1812 - "League\\Flysystem\\": "src" 1813 - } 1814 - }, 1815 - "notification-url": "https://packagist.org/downloads/", 1816 - "license": [ 1817 - "MIT" 1818 - ], 1819 - "authors": [ 1820 - { 1821 - "name": "Frank de Jonge", 1822 - "email": "info@frankdejonge.nl" 1823 - } 1824 - ], 1825 - "description": "File storage abstraction for PHP", 1826 - "keywords": [ 1827 - "WebDAV", 1828 - "aws", 1829 - "cloud", 1830 - "file", 1831 - "files", 1832 - "filesystem", 1833 - "filesystems", 1834 - "ftp", 1835 - "s3", 1836 - "sftp", 1837 - "storage" 1838 - ], 1839 - "support": { 1840 - "issues": "https://github.com/thephpleague/flysystem/issues", 1841 - "source": "https://github.com/thephpleague/flysystem/tree/3.30.1" 1842 - }, 1843 - "time": "2025-10-20T15:35:26+00:00" 1844 - }, 1845 - { 1846 - "name": "league/flysystem-local", 1847 - "version": "3.30.0", 1848 - "source": { 1849 - "type": "git", 1850 - "url": "https://github.com/thephpleague/flysystem-local.git", 1851 - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" 1852 - }, 1853 - "dist": { 1854 - "type": "zip", 1855 - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", 1856 - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", 1857 - "shasum": "" 1858 - }, 1859 - "require": { 1860 - "ext-fileinfo": "*", 1861 - "league/flysystem": "^3.0.0", 1862 - "league/mime-type-detection": "^1.0.0", 1863 - "php": "^8.0.2" 1864 - }, 1865 - "type": "library", 1866 - "autoload": { 1867 - "psr-4": { 1868 - "League\\Flysystem\\Local\\": "" 1869 - } 1870 - }, 1871 - "notification-url": "https://packagist.org/downloads/", 1872 - "license": [ 1873 - "MIT" 1874 - ], 1875 - "authors": [ 1876 - { 1877 - "name": "Frank de Jonge", 1878 - "email": "info@frankdejonge.nl" 1879 - } 1880 - ], 1881 - "description": "Local filesystem adapter for Flysystem.", 1882 - "keywords": [ 1883 - "Flysystem", 1884 - "file", 1885 - "files", 1886 - "filesystem", 1887 - "local" 1888 - ], 1889 - "support": { 1890 - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" 1891 - }, 1892 - "time": "2025-05-21T10:34:19+00:00" 1893 - }, 1894 - { 1895 - "name": "league/mime-type-detection", 1896 - "version": "1.16.0", 1897 - "source": { 1898 - "type": "git", 1899 - "url": "https://github.com/thephpleague/mime-type-detection.git", 1900 - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" 1901 - }, 1902 - "dist": { 1903 - "type": "zip", 1904 - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", 1905 - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", 1906 - "shasum": "" 1907 - }, 1908 - "require": { 1909 - "ext-fileinfo": "*", 1910 - "php": "^7.4 || ^8.0" 1911 - }, 1912 - "require-dev": { 1913 - "friendsofphp/php-cs-fixer": "^3.2", 1914 - "phpstan/phpstan": "^0.12.68", 1915 - "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" 1916 - }, 1917 - "type": "library", 1918 - "autoload": { 1919 - "psr-4": { 1920 - "League\\MimeTypeDetection\\": "src" 1921 - } 1922 - }, 1923 - "notification-url": "https://packagist.org/downloads/", 1924 - "license": [ 1925 - "MIT" 1926 - ], 1927 - "authors": [ 1928 - { 1929 - "name": "Frank de Jonge", 1930 - "email": "info@frankdejonge.nl" 1931 - } 1932 - ], 1933 - "description": "Mime-type detection for Flysystem", 1934 - "support": { 1935 - "issues": "https://github.com/thephpleague/mime-type-detection/issues", 1936 - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" 1937 - }, 1938 - "funding": [ 1939 - { 1940 - "url": "https://github.com/frankdejonge", 1941 - "type": "github" 1942 - }, 1943 - { 1944 - "url": "https://tidelift.com/funding/github/packagist/league/flysystem", 1945 - "type": "tidelift" 1946 - } 1947 - ], 1948 - "time": "2024-09-21T08:32:55+00:00" 1949 - }, 1950 - { 1951 - "name": "league/oauth1-client", 1952 - "version": "v1.11.0", 1953 - "source": { 1954 - "type": "git", 1955 - "url": "https://github.com/thephpleague/oauth1-client.git", 1956 - "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" 1957 - }, 1958 - "dist": { 1959 - "type": "zip", 1960 - "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", 1961 - "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", 1962 - "shasum": "" 1963 - }, 1964 - "require": { 1965 - "ext-json": "*", 1966 - "ext-openssl": "*", 1967 - "guzzlehttp/guzzle": "^6.0|^7.0", 1968 - "guzzlehttp/psr7": "^1.7|^2.0", 1969 - "php": ">=7.1||>=8.0" 1970 - }, 1971 - "require-dev": { 1972 - "ext-simplexml": "*", 1973 - "friendsofphp/php-cs-fixer": "^2.17", 1974 - "mockery/mockery": "^1.3.3", 1975 - "phpstan/phpstan": "^0.12.42", 1976 - "phpunit/phpunit": "^7.5||9.5" 1977 - }, 1978 - "suggest": { 1979 - "ext-simplexml": "For decoding XML-based responses." 1980 - }, 1981 - "type": "library", 1982 - "extra": { 1983 - "branch-alias": { 1984 - "dev-master": "1.0-dev", 1985 - "dev-develop": "2.0-dev" 1986 - } 1987 - }, 1988 - "autoload": { 1989 - "psr-4": { 1990 - "League\\OAuth1\\Client\\": "src/" 1991 - } 1992 - }, 1993 - "notification-url": "https://packagist.org/downloads/", 1994 - "license": [ 1995 - "MIT" 1996 - ], 1997 - "authors": [ 1998 - { 1999 - "name": "Ben Corlett", 2000 - "email": "bencorlett@me.com", 2001 - "homepage": "http://www.webcomm.com.au", 2002 - "role": "Developer" 2003 - } 2004 - ], 2005 - "description": "OAuth 1.0 Client Library", 2006 - "keywords": [ 2007 - "Authentication", 2008 - "SSO", 2009 - "authorization", 2010 - "bitbucket", 2011 - "identity", 2012 - "idp", 2013 - "oauth", 2014 - "oauth1", 2015 - "single sign on", 2016 - "trello", 2017 - "tumblr", 2018 - "twitter" 2019 - ], 2020 - "support": { 2021 - "issues": "https://github.com/thephpleague/oauth1-client/issues", 2022 - "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" 2023 - }, 2024 - "time": "2024-12-10T19:59:05+00:00" 2025 - }, 2026 - { 2027 - "name": "league/uri", 2028 - "version": "7.5.1", 2029 - "source": { 2030 - "type": "git", 2031 - "url": "https://github.com/thephpleague/uri.git", 2032 - "reference": "81fb5145d2644324614cc532b28efd0215bda430" 2033 - }, 2034 - "dist": { 2035 - "type": "zip", 2036 - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", 2037 - "reference": "81fb5145d2644324614cc532b28efd0215bda430", 2038 - "shasum": "" 2039 - }, 2040 - "require": { 2041 - "league/uri-interfaces": "^7.5", 2042 - "php": "^8.1" 2043 - }, 2044 - "conflict": { 2045 - "league/uri-schemes": "^1.0" 2046 - }, 2047 - "suggest": { 2048 - "ext-bcmath": "to improve IPV4 host parsing", 2049 - "ext-fileinfo": "to create Data URI from file contennts", 2050 - "ext-gmp": "to improve IPV4 host parsing", 2051 - "ext-intl": "to handle IDN host with the best performance", 2052 - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", 2053 - "league/uri-components": "Needed to easily manipulate URI objects components", 2054 - "php-64bit": "to improve IPV4 host parsing", 2055 - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" 2056 - }, 2057 - "type": "library", 2058 - "extra": { 2059 - "branch-alias": { 2060 - "dev-master": "7.x-dev" 2061 - } 2062 - }, 2063 - "autoload": { 2064 - "psr-4": { 2065 - "League\\Uri\\": "" 2066 - } 2067 - }, 2068 - "notification-url": "https://packagist.org/downloads/", 2069 - "license": [ 2070 - "MIT" 2071 - ], 2072 - "authors": [ 2073 - { 2074 - "name": "Ignace Nyamagana Butera", 2075 - "email": "nyamsprod@gmail.com", 2076 - "homepage": "https://nyamsprod.com" 2077 - } 2078 - ], 2079 - "description": "URI manipulation library", 2080 - "homepage": "https://uri.thephpleague.com", 2081 - "keywords": [ 2082 - "data-uri", 2083 - "file-uri", 2084 - "ftp", 2085 - "hostname", 2086 - "http", 2087 - "https", 2088 - "middleware", 2089 - "parse_str", 2090 - "parse_url", 2091 - "psr-7", 2092 - "query-string", 2093 - "querystring", 2094 - "rfc3986", 2095 - "rfc3987", 2096 - "rfc6570", 2097 - "uri", 2098 - "uri-template", 2099 - "url", 2100 - "ws" 2101 - ], 2102 - "support": { 2103 - "docs": "https://uri.thephpleague.com", 2104 - "forum": "https://thephpleague.slack.com", 2105 - "issues": "https://github.com/thephpleague/uri-src/issues", 2106 - "source": "https://github.com/thephpleague/uri/tree/7.5.1" 2107 - }, 2108 - "funding": [ 2109 - { 2110 - "url": "https://github.com/sponsors/nyamsprod", 2111 - "type": "github" 2112 - } 2113 - ], 2114 - "time": "2024-12-08T08:40:02+00:00" 2115 - }, 2116 - { 2117 - "name": "league/uri-interfaces", 2118 - "version": "7.5.0", 2119 - "source": { 2120 - "type": "git", 2121 - "url": "https://github.com/thephpleague/uri-interfaces.git", 2122 - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" 2123 - }, 2124 - "dist": { 2125 - "type": "zip", 2126 - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", 2127 - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", 2128 - "shasum": "" 2129 - }, 2130 - "require": { 2131 - "ext-filter": "*", 2132 - "php": "^8.1", 2133 - "psr/http-factory": "^1", 2134 - "psr/http-message": "^1.1 || ^2.0" 2135 - }, 2136 - "suggest": { 2137 - "ext-bcmath": "to improve IPV4 host parsing", 2138 - "ext-gmp": "to improve IPV4 host parsing", 2139 - "ext-intl": "to handle IDN host with the best performance", 2140 - "php-64bit": "to improve IPV4 host parsing", 2141 - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" 2142 - }, 2143 - "type": "library", 2144 - "extra": { 2145 - "branch-alias": { 2146 - "dev-master": "7.x-dev" 2147 - } 2148 - }, 2149 - "autoload": { 2150 - "psr-4": { 2151 - "League\\Uri\\": "" 2152 - } 2153 - }, 2154 - "notification-url": "https://packagist.org/downloads/", 2155 - "license": [ 2156 - "MIT" 2157 - ], 2158 - "authors": [ 2159 - { 2160 - "name": "Ignace Nyamagana Butera", 2161 - "email": "nyamsprod@gmail.com", 2162 - "homepage": "https://nyamsprod.com" 2163 - } 2164 - ], 2165 - "description": "Common interfaces and classes for URI representation and interaction", 2166 - "homepage": "https://uri.thephpleague.com", 2167 - "keywords": [ 2168 - "data-uri", 2169 - "file-uri", 2170 - "ftp", 2171 - "hostname", 2172 - "http", 2173 - "https", 2174 - "parse_str", 2175 - "parse_url", 2176 - "psr-7", 2177 - "query-string", 2178 - "querystring", 2179 - "rfc3986", 2180 - "rfc3987", 2181 - "rfc6570", 2182 - "uri", 2183 - "url", 2184 - "ws" 2185 - ], 2186 - "support": { 2187 - "docs": "https://uri.thephpleague.com", 2188 - "forum": "https://thephpleague.slack.com", 2189 - "issues": "https://github.com/thephpleague/uri-src/issues", 2190 - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" 2191 - }, 2192 - "funding": [ 2193 - { 2194 - "url": "https://github.com/sponsors/nyamsprod", 2195 - "type": "github" 2196 - } 2197 - ], 2198 - "time": "2024-12-08T08:18:47+00:00" 2199 - }, 2200 - { 2201 - "name": "monolog/monolog", 2202 - "version": "3.9.0", 2203 - "source": { 2204 - "type": "git", 2205 - "url": "https://github.com/Seldaek/monolog.git", 2206 - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" 2207 - }, 2208 - "dist": { 2209 - "type": "zip", 2210 - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", 2211 - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", 2212 - "shasum": "" 2213 - }, 2214 - "require": { 2215 - "php": ">=8.1", 2216 - "psr/log": "^2.0 || ^3.0" 2217 - }, 2218 - "provide": { 2219 - "psr/log-implementation": "3.0.0" 2220 - }, 2221 - "require-dev": { 2222 - "aws/aws-sdk-php": "^3.0", 2223 - "doctrine/couchdb": "~1.0@dev", 2224 - "elasticsearch/elasticsearch": "^7 || ^8", 2225 - "ext-json": "*", 2226 - "graylog2/gelf-php": "^1.4.2 || ^2.0", 2227 - "guzzlehttp/guzzle": "^7.4.5", 2228 - "guzzlehttp/psr7": "^2.2", 2229 - "mongodb/mongodb": "^1.8", 2230 - "php-amqplib/php-amqplib": "~2.4 || ^3", 2231 - "php-console/php-console": "^3.1.8", 2232 - "phpstan/phpstan": "^2", 2233 - "phpstan/phpstan-deprecation-rules": "^2", 2234 - "phpstan/phpstan-strict-rules": "^2", 2235 - "phpunit/phpunit": "^10.5.17 || ^11.0.7", 2236 - "predis/predis": "^1.1 || ^2", 2237 - "rollbar/rollbar": "^4.0", 2238 - "ruflin/elastica": "^7 || ^8", 2239 - "symfony/mailer": "^5.4 || ^6", 2240 - "symfony/mime": "^5.4 || ^6" 2241 - }, 2242 - "suggest": { 2243 - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 2244 - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 2245 - "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", 2246 - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 2247 - "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", 2248 - "ext-mbstring": "Allow to work properly with unicode symbols", 2249 - "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", 2250 - "ext-openssl": "Required to send log messages using SSL", 2251 - "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", 2252 - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", 2253 - "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", 2254 - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", 2255 - "rollbar/rollbar": "Allow sending log messages to Rollbar", 2256 - "ruflin/elastica": "Allow sending log messages to an Elastic Search server" 2257 - }, 2258 - "type": "library", 2259 - "extra": { 2260 - "branch-alias": { 2261 - "dev-main": "3.x-dev" 2262 - } 2263 - }, 2264 - "autoload": { 2265 - "psr-4": { 2266 - "Monolog\\": "src/Monolog" 2267 - } 2268 - }, 2269 - "notification-url": "https://packagist.org/downloads/", 2270 - "license": [ 2271 - "MIT" 2272 - ], 2273 - "authors": [ 2274 - { 2275 - "name": "Jordi Boggiano", 2276 - "email": "j.boggiano@seld.be", 2277 - "homepage": "https://seld.be" 2278 - } 2279 - ], 2280 - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 2281 - "homepage": "https://github.com/Seldaek/monolog", 2282 - "keywords": [ 2283 - "log", 2284 - "logging", 2285 - "psr-3" 2286 - ], 2287 - "support": { 2288 - "issues": "https://github.com/Seldaek/monolog/issues", 2289 - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" 2290 - }, 2291 - "funding": [ 2292 - { 2293 - "url": "https://github.com/Seldaek", 2294 - "type": "github" 2295 - }, 2296 - { 2297 - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", 2298 - "type": "tidelift" 2299 - } 2300 - ], 2301 - "time": "2025-03-24T10:02:05+00:00" 2302 - }, 2303 - { 2304 - "name": "nesbot/carbon", 2305 - "version": "3.10.3", 2306 - "source": { 2307 - "type": "git", 2308 - "url": "https://github.com/CarbonPHP/carbon.git", 2309 - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" 2310 - }, 2311 - "dist": { 2312 - "type": "zip", 2313 - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", 2314 - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", 2315 - "shasum": "" 2316 - }, 2317 - "require": { 2318 - "carbonphp/carbon-doctrine-types": "<100.0", 2319 - "ext-json": "*", 2320 - "php": "^8.1", 2321 - "psr/clock": "^1.0", 2322 - "symfony/clock": "^6.3.12 || ^7.0", 2323 - "symfony/polyfill-mbstring": "^1.0", 2324 - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" 2325 - }, 2326 - "provide": { 2327 - "psr/clock-implementation": "1.0" 2328 - }, 2329 - "require-dev": { 2330 - "doctrine/dbal": "^3.6.3 || ^4.0", 2331 - "doctrine/orm": "^2.15.2 || ^3.0", 2332 - "friendsofphp/php-cs-fixer": "^v3.87.1", 2333 - "kylekatarnls/multi-tester": "^2.5.3", 2334 - "phpmd/phpmd": "^2.15.0", 2335 - "phpstan/extension-installer": "^1.4.3", 2336 - "phpstan/phpstan": "^2.1.22", 2337 - "phpunit/phpunit": "^10.5.53", 2338 - "squizlabs/php_codesniffer": "^3.13.4" 2339 - }, 2340 - "bin": [ 2341 - "bin/carbon" 2342 - ], 2343 - "type": "library", 2344 - "extra": { 2345 - "laravel": { 2346 - "providers": [ 2347 - "Carbon\\Laravel\\ServiceProvider" 2348 - ] 2349 - }, 2350 - "phpstan": { 2351 - "includes": [ 2352 - "extension.neon" 2353 - ] 2354 - }, 2355 - "branch-alias": { 2356 - "dev-2.x": "2.x-dev", 2357 - "dev-master": "3.x-dev" 2358 - } 2359 - }, 2360 - "autoload": { 2361 - "psr-4": { 2362 - "Carbon\\": "src/Carbon/" 2363 - } 2364 - }, 2365 - "notification-url": "https://packagist.org/downloads/", 2366 - "license": [ 2367 - "MIT" 2368 - ], 2369 - "authors": [ 2370 - { 2371 - "name": "Brian Nesbitt", 2372 - "email": "brian@nesbot.com", 2373 - "homepage": "https://markido.com" 2374 - }, 2375 - { 2376 - "name": "kylekatarnls", 2377 - "homepage": "https://github.com/kylekatarnls" 2378 - } 2379 - ], 2380 - "description": "An API extension for DateTime that supports 281 different languages.", 2381 - "homepage": "https://carbon.nesbot.com", 2382 - "keywords": [ 2383 - "date", 2384 - "datetime", 2385 - "time" 2386 - ], 2387 - "support": { 2388 - "docs": "https://carbon.nesbot.com/docs", 2389 - "issues": "https://github.com/CarbonPHP/carbon/issues", 2390 - "source": "https://github.com/CarbonPHP/carbon" 2391 - }, 2392 - "funding": [ 2393 - { 2394 - "url": "https://github.com/sponsors/kylekatarnls", 2395 - "type": "github" 2396 - }, 2397 - { 2398 - "url": "https://opencollective.com/Carbon#sponsor", 2399 - "type": "opencollective" 2400 - }, 2401 - { 2402 - "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", 2403 - "type": "tidelift" 2404 - } 2405 - ], 2406 - "time": "2025-09-06T13:39:36+00:00" 2407 - }, 2408 - { 2409 - "name": "nette/schema", 2410 - "version": "v1.3.2", 2411 - "source": { 2412 - "type": "git", 2413 - "url": "https://github.com/nette/schema.git", 2414 - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" 2415 - }, 2416 - "dist": { 2417 - "type": "zip", 2418 - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", 2419 - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", 2420 - "shasum": "" 2421 - }, 2422 - "require": { 2423 - "nette/utils": "^4.0", 2424 - "php": "8.1 - 8.4" 2425 - }, 2426 - "require-dev": { 2427 - "nette/tester": "^2.5.2", 2428 - "phpstan/phpstan-nette": "^1.0", 2429 - "tracy/tracy": "^2.8" 2430 - }, 2431 - "type": "library", 2432 - "extra": { 2433 - "branch-alias": { 2434 - "dev-master": "1.3-dev" 2435 - } 2436 - }, 2437 - "autoload": { 2438 - "classmap": [ 2439 - "src/" 2440 - ] 2441 - }, 2442 - "notification-url": "https://packagist.org/downloads/", 2443 - "license": [ 2444 - "BSD-3-Clause", 2445 - "GPL-2.0-only", 2446 - "GPL-3.0-only" 2447 - ], 2448 - "authors": [ 2449 - { 2450 - "name": "David Grudl", 2451 - "homepage": "https://davidgrudl.com" 2452 - }, 2453 - { 2454 - "name": "Nette Community", 2455 - "homepage": "https://nette.org/contributors" 2456 - } 2457 - ], 2458 - "description": "๐Ÿ“ Nette Schema: validating data structures against a given Schema.", 2459 - "homepage": "https://nette.org", 2460 - "keywords": [ 2461 - "config", 2462 - "nette" 2463 - ], 2464 - "support": { 2465 - "issues": "https://github.com/nette/schema/issues", 2466 - "source": "https://github.com/nette/schema/tree/v1.3.2" 2467 - }, 2468 - "time": "2024-10-06T23:10:23+00:00" 2469 - }, 2470 - { 2471 - "name": "nette/utils", 2472 - "version": "v4.0.8", 2473 - "source": { 2474 - "type": "git", 2475 - "url": "https://github.com/nette/utils.git", 2476 - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" 2477 - }, 2478 - "dist": { 2479 - "type": "zip", 2480 - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", 2481 - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", 2482 - "shasum": "" 2483 - }, 2484 - "require": { 2485 - "php": "8.0 - 8.5" 2486 - }, 2487 - "conflict": { 2488 - "nette/finder": "<3", 2489 - "nette/schema": "<1.2.2" 2490 - }, 2491 - "require-dev": { 2492 - "jetbrains/phpstorm-attributes": "^1.2", 2493 - "nette/tester": "^2.5", 2494 - "phpstan/phpstan-nette": "^2.0@stable", 2495 - "tracy/tracy": "^2.9" 2496 - }, 2497 - "suggest": { 2498 - "ext-gd": "to use Image", 2499 - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", 2500 - "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", 2501 - "ext-json": "to use Nette\\Utils\\Json", 2502 - "ext-mbstring": "to use Strings::lower() etc...", 2503 - "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" 2504 - }, 2505 - "type": "library", 2506 - "extra": { 2507 - "branch-alias": { 2508 - "dev-master": "4.0-dev" 2509 - } 2510 - }, 2511 - "autoload": { 2512 - "psr-4": { 2513 - "Nette\\": "src" 2514 - }, 2515 - "classmap": [ 2516 - "src/" 2517 - ] 2518 - }, 2519 - "notification-url": "https://packagist.org/downloads/", 2520 - "license": [ 2521 - "BSD-3-Clause", 2522 - "GPL-2.0-only", 2523 - "GPL-3.0-only" 2524 - ], 2525 - "authors": [ 2526 - { 2527 - "name": "David Grudl", 2528 - "homepage": "https://davidgrudl.com" 2529 - }, 2530 - { 2531 - "name": "Nette Community", 2532 - "homepage": "https://nette.org/contributors" 2533 - } 2534 - ], 2535 - "description": "๐Ÿ›  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", 2536 - "homepage": "https://nette.org", 2537 - "keywords": [ 2538 - "array", 2539 - "core", 2540 - "datetime", 2541 - "images", 2542 - "json", 2543 - "nette", 2544 - "paginator", 2545 - "password", 2546 - "slugify", 2547 - "string", 2548 - "unicode", 2549 - "utf-8", 2550 - "utility", 2551 - "validation" 2552 - ], 2553 - "support": { 2554 - "issues": "https://github.com/nette/utils/issues", 2555 - "source": "https://github.com/nette/utils/tree/v4.0.8" 2556 - }, 2557 - "time": "2025-08-06T21:43:34+00:00" 2558 - }, 2559 - { 2560 - "name": "nunomaduro/termwind", 2561 - "version": "v2.3.2", 2562 - "source": { 2563 - "type": "git", 2564 - "url": "https://github.com/nunomaduro/termwind.git", 2565 - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" 2566 - }, 2567 - "dist": { 2568 - "type": "zip", 2569 - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", 2570 - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", 2571 - "shasum": "" 2572 - }, 2573 - "require": { 2574 - "ext-mbstring": "*", 2575 - "php": "^8.2", 2576 - "symfony/console": "^7.3.4" 2577 - }, 2578 - "require-dev": { 2579 - "illuminate/console": "^11.46.1", 2580 - "laravel/pint": "^1.25.1", 2581 - "mockery/mockery": "^1.6.12", 2582 - "pestphp/pest": "^2.36.0 || ^3.8.4", 2583 - "phpstan/phpstan": "^1.12.32", 2584 - "phpstan/phpstan-strict-rules": "^1.6.2", 2585 - "symfony/var-dumper": "^7.3.4", 2586 - "thecodingmachine/phpstan-strict-rules": "^1.0.0" 2587 - }, 2588 - "type": "library", 2589 - "extra": { 2590 - "laravel": { 2591 - "providers": [ 2592 - "Termwind\\Laravel\\TermwindServiceProvider" 2593 - ] 2594 - }, 2595 - "branch-alias": { 2596 - "dev-2.x": "2.x-dev" 2597 - } 2598 - }, 2599 - "autoload": { 2600 - "files": [ 2601 - "src/Functions.php" 2602 - ], 2603 - "psr-4": { 2604 - "Termwind\\": "src/" 2605 - } 2606 - }, 2607 - "notification-url": "https://packagist.org/downloads/", 2608 - "license": [ 2609 - "MIT" 2610 - ], 2611 - "authors": [ 2612 - { 2613 - "name": "Nuno Maduro", 2614 - "email": "enunomaduro@gmail.com" 2615 - } 2616 - ], 2617 - "description": "Its like Tailwind CSS, but for the console.", 2618 - "keywords": [ 2619 - "cli", 2620 - "console", 2621 - "css", 2622 - "package", 2623 - "php", 2624 - "style" 2625 - ], 2626 - "support": { 2627 - "issues": "https://github.com/nunomaduro/termwind/issues", 2628 - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" 2629 - }, 2630 - "funding": [ 2631 - { 2632 - "url": "https://www.paypal.com/paypalme/enunomaduro", 2633 - "type": "custom" 2634 - }, 2635 - { 2636 - "url": "https://github.com/nunomaduro", 2637 - "type": "github" 2638 - }, 2639 - { 2640 - "url": "https://github.com/xiCO2k", 2641 - "type": "github" 2642 - } 2643 - ], 2644 - "time": "2025-10-18T11:10:27+00:00" 2645 - }, 2646 - { 2647 - "name": "paragonie/constant_time_encoding", 2648 - "version": "v3.1.3", 2649 - "source": { 2650 - "type": "git", 2651 - "url": "https://github.com/paragonie/constant_time_encoding.git", 2652 - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" 2653 - }, 2654 - "dist": { 2655 - "type": "zip", 2656 - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", 2657 - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", 2658 - "shasum": "" 2659 - }, 2660 - "require": { 2661 - "php": "^8" 2662 - }, 2663 - "require-dev": { 2664 - "infection/infection": "^0", 2665 - "nikic/php-fuzzer": "^0", 2666 - "phpunit/phpunit": "^9|^10|^11", 2667 - "vimeo/psalm": "^4|^5|^6" 2668 - }, 2669 - "type": "library", 2670 - "autoload": { 2671 - "psr-4": { 2672 - "ParagonIE\\ConstantTime\\": "src/" 2673 - } 2674 - }, 2675 - "notification-url": "https://packagist.org/downloads/", 2676 - "license": [ 2677 - "MIT" 2678 - ], 2679 - "authors": [ 2680 - { 2681 - "name": "Paragon Initiative Enterprises", 2682 - "email": "security@paragonie.com", 2683 - "homepage": "https://paragonie.com", 2684 - "role": "Maintainer" 2685 - }, 2686 - { 2687 - "name": "Steve 'Sc00bz' Thomas", 2688 - "email": "steve@tobtu.com", 2689 - "homepage": "https://www.tobtu.com", 2690 - "role": "Original Developer" 2691 - } 2692 - ], 2693 - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", 2694 - "keywords": [ 2695 - "base16", 2696 - "base32", 2697 - "base32_decode", 2698 - "base32_encode", 2699 - "base64", 2700 - "base64_decode", 2701 - "base64_encode", 2702 - "bin2hex", 2703 - "encoding", 2704 - "hex", 2705 - "hex2bin", 2706 - "rfc4648" 2707 - ], 2708 - "support": { 2709 - "email": "info@paragonie.com", 2710 - "issues": "https://github.com/paragonie/constant_time_encoding/issues", 2711 - "source": "https://github.com/paragonie/constant_time_encoding" 2712 - }, 2713 - "time": "2025-09-24T15:06:41+00:00" 2714 - }, 2715 - { 2716 - "name": "paragonie/random_compat", 2717 - "version": "v9.99.100", 2718 - "source": { 2719 - "type": "git", 2720 - "url": "https://github.com/paragonie/random_compat.git", 2721 - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" 2722 - }, 2723 - "dist": { 2724 - "type": "zip", 2725 - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", 2726 - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", 2727 - "shasum": "" 2728 - }, 2729 - "require": { 2730 - "php": ">= 7" 2731 - }, 2732 - "require-dev": { 2733 - "phpunit/phpunit": "4.*|5.*", 2734 - "vimeo/psalm": "^1" 2735 - }, 2736 - "suggest": { 2737 - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." 2738 - }, 2739 - "type": "library", 2740 - "notification-url": "https://packagist.org/downloads/", 2741 - "license": [ 2742 - "MIT" 2743 - ], 2744 - "authors": [ 2745 - { 2746 - "name": "Paragon Initiative Enterprises", 2747 - "email": "security@paragonie.com", 2748 - "homepage": "https://paragonie.com" 2749 - } 2750 - ], 2751 - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", 2752 - "keywords": [ 2753 - "csprng", 2754 - "polyfill", 2755 - "pseudorandom", 2756 - "random" 2757 - ], 2758 - "support": { 2759 - "email": "info@paragonie.com", 2760 - "issues": "https://github.com/paragonie/random_compat/issues", 2761 - "source": "https://github.com/paragonie/random_compat" 2762 - }, 2763 - "time": "2020-10-15T08:29:30+00:00" 2764 - }, 2765 - { 2766 - "name": "phpoption/phpoption", 2767 - "version": "1.9.4", 2768 - "source": { 2769 - "type": "git", 2770 - "url": "https://github.com/schmittjoh/php-option.git", 2771 - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" 2772 - }, 2773 - "dist": { 2774 - "type": "zip", 2775 - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", 2776 - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", 2777 - "shasum": "" 2778 - }, 2779 - "require": { 2780 - "php": "^7.2.5 || ^8.0" 2781 - }, 2782 - "require-dev": { 2783 - "bamarni/composer-bin-plugin": "^1.8.2", 2784 - "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" 2785 - }, 2786 - "type": "library", 2787 - "extra": { 2788 - "bamarni-bin": { 2789 - "bin-links": true, 2790 - "forward-command": false 2791 - }, 2792 - "branch-alias": { 2793 - "dev-master": "1.9-dev" 2794 - } 2795 - }, 2796 - "autoload": { 2797 - "psr-4": { 2798 - "PhpOption\\": "src/PhpOption/" 2799 - } 2800 - }, 2801 - "notification-url": "https://packagist.org/downloads/", 2802 - "license": [ 2803 - "Apache-2.0" 2804 - ], 2805 - "authors": [ 2806 - { 2807 - "name": "Johannes M. Schmitt", 2808 - "email": "schmittjoh@gmail.com", 2809 - "homepage": "https://github.com/schmittjoh" 2810 - }, 2811 - { 2812 - "name": "Graham Campbell", 2813 - "email": "hello@gjcampbell.co.uk", 2814 - "homepage": "https://github.com/GrahamCampbell" 2815 - } 2816 - ], 2817 - "description": "Option Type for PHP", 2818 - "keywords": [ 2819 - "language", 2820 - "option", 2821 - "php", 2822 - "type" 2823 - ], 2824 - "support": { 2825 - "issues": "https://github.com/schmittjoh/php-option/issues", 2826 - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" 2827 - }, 2828 - "funding": [ 2829 - { 2830 - "url": "https://github.com/GrahamCampbell", 2831 - "type": "github" 2832 - }, 2833 - { 2834 - "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", 2835 - "type": "tidelift" 2836 - } 2837 - ], 2838 - "time": "2025-08-21T11:53:16+00:00" 2839 - }, 2840 - { 2841 - "name": "phpseclib/phpseclib", 2842 - "version": "3.0.47", 2843 - "source": { 2844 - "type": "git", 2845 - "url": "https://github.com/phpseclib/phpseclib.git", 2846 - "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" 2847 - }, 2848 - "dist": { 2849 - "type": "zip", 2850 - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", 2851 - "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", 2852 - "shasum": "" 2853 - }, 2854 - "require": { 2855 - "paragonie/constant_time_encoding": "^1|^2|^3", 2856 - "paragonie/random_compat": "^1.4|^2.0|^9.99.99", 2857 - "php": ">=5.6.1" 2858 - }, 2859 - "require-dev": { 2860 - "phpunit/phpunit": "*" 2861 - }, 2862 - "suggest": { 2863 - "ext-dom": "Install the DOM extension to load XML formatted public keys.", 2864 - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", 2865 - "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", 2866 - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", 2867 - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." 2868 - }, 2869 - "type": "library", 2870 - "autoload": { 2871 - "files": [ 2872 - "phpseclib/bootstrap.php" 2873 - ], 2874 - "psr-4": { 2875 - "phpseclib3\\": "phpseclib/" 2876 - } 2877 - }, 2878 - "notification-url": "https://packagist.org/downloads/", 2879 - "license": [ 2880 - "MIT" 2881 - ], 2882 - "authors": [ 2883 - { 2884 - "name": "Jim Wigginton", 2885 - "email": "terrafrost@php.net", 2886 - "role": "Lead Developer" 2887 - }, 2888 - { 2889 - "name": "Patrick Monnerat", 2890 - "email": "pm@datasphere.ch", 2891 - "role": "Developer" 2892 - }, 2893 - { 2894 - "name": "Andreas Fischer", 2895 - "email": "bantu@phpbb.com", 2896 - "role": "Developer" 2897 - }, 2898 - { 2899 - "name": "Hans-Jรผrgen Petrich", 2900 - "email": "petrich@tronic-media.com", 2901 - "role": "Developer" 2902 - }, 2903 - { 2904 - "name": "Graham Campbell", 2905 - "email": "graham@alt-three.com", 2906 - "role": "Developer" 2907 - } 2908 - ], 2909 - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", 2910 - "homepage": "http://phpseclib.sourceforge.net", 2911 - "keywords": [ 2912 - "BigInteger", 2913 - "aes", 2914 - "asn.1", 2915 - "asn1", 2916 - "blowfish", 2917 - "crypto", 2918 - "cryptography", 2919 - "encryption", 2920 - "rsa", 2921 - "security", 2922 - "sftp", 2923 - "signature", 2924 - "signing", 2925 - "ssh", 2926 - "twofish", 2927 - "x.509", 2928 - "x509" 2929 - ], 2930 - "support": { 2931 - "issues": "https://github.com/phpseclib/phpseclib/issues", 2932 - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" 2933 - }, 2934 - "funding": [ 2935 - { 2936 - "url": "https://github.com/terrafrost", 2937 - "type": "github" 2938 - }, 2939 - { 2940 - "url": "https://www.patreon.com/phpseclib", 2941 - "type": "patreon" 2942 - }, 2943 - { 2944 - "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", 2945 - "type": "tidelift" 2946 - } 2947 - ], 2948 - "time": "2025-10-06T01:07:24+00:00" 2949 - }, 2950 - { 2951 - "name": "psr/clock", 2952 - "version": "1.0.0", 2953 - "source": { 2954 - "type": "git", 2955 - "url": "https://github.com/php-fig/clock.git", 2956 - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" 2957 - }, 2958 - "dist": { 2959 - "type": "zip", 2960 - "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", 2961 - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", 2962 - "shasum": "" 2963 - }, 2964 - "require": { 2965 - "php": "^7.0 || ^8.0" 2966 - }, 2967 - "type": "library", 2968 - "autoload": { 2969 - "psr-4": { 2970 - "Psr\\Clock\\": "src/" 2971 - } 2972 - }, 2973 - "notification-url": "https://packagist.org/downloads/", 2974 - "license": [ 2975 - "MIT" 2976 - ], 2977 - "authors": [ 2978 - { 2979 - "name": "PHP-FIG", 2980 - "homepage": "https://www.php-fig.org/" 2981 - } 2982 - ], 2983 - "description": "Common interface for reading the clock.", 2984 - "homepage": "https://github.com/php-fig/clock", 2985 - "keywords": [ 2986 - "clock", 2987 - "now", 2988 - "psr", 2989 - "psr-20", 2990 - "time" 2991 - ], 2992 - "support": { 2993 - "issues": "https://github.com/php-fig/clock/issues", 2994 - "source": "https://github.com/php-fig/clock/tree/1.0.0" 2995 - }, 2996 - "time": "2022-11-25T14:36:26+00:00" 2997 - }, 2998 - { 2999 - "name": "psr/container", 3000 - "version": "2.0.2", 3001 - "source": { 3002 - "type": "git", 3003 - "url": "https://github.com/php-fig/container.git", 3004 - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" 3005 - }, 3006 - "dist": { 3007 - "type": "zip", 3008 - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", 3009 - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", 3010 - "shasum": "" 3011 - }, 3012 - "require": { 3013 - "php": ">=7.4.0" 3014 - }, 3015 - "type": "library", 3016 - "extra": { 3017 - "branch-alias": { 3018 - "dev-master": "2.0.x-dev" 3019 - } 3020 - }, 3021 - "autoload": { 3022 - "psr-4": { 3023 - "Psr\\Container\\": "src/" 3024 - } 3025 - }, 3026 - "notification-url": "https://packagist.org/downloads/", 3027 - "license": [ 3028 - "MIT" 3029 - ], 3030 - "authors": [ 3031 - { 3032 - "name": "PHP-FIG", 3033 - "homepage": "https://www.php-fig.org/" 3034 - } 3035 - ], 3036 - "description": "Common Container Interface (PHP FIG PSR-11)", 3037 - "homepage": "https://github.com/php-fig/container", 3038 - "keywords": [ 3039 - "PSR-11", 3040 - "container", 3041 - "container-interface", 3042 - "container-interop", 3043 - "psr" 3044 - ], 3045 - "support": { 3046 - "issues": "https://github.com/php-fig/container/issues", 3047 - "source": "https://github.com/php-fig/container/tree/2.0.2" 3048 - }, 3049 - "time": "2021-11-05T16:47:00+00:00" 3050 - }, 3051 - { 3052 - "name": "psr/event-dispatcher", 3053 - "version": "1.0.0", 3054 - "source": { 3055 - "type": "git", 3056 - "url": "https://github.com/php-fig/event-dispatcher.git", 3057 - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" 3058 - }, 3059 - "dist": { 3060 - "type": "zip", 3061 - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", 3062 - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", 3063 - "shasum": "" 3064 - }, 3065 - "require": { 3066 - "php": ">=7.2.0" 3067 - }, 3068 - "type": "library", 3069 - "extra": { 3070 - "branch-alias": { 3071 - "dev-master": "1.0.x-dev" 3072 - } 3073 - }, 3074 - "autoload": { 3075 - "psr-4": { 3076 - "Psr\\EventDispatcher\\": "src/" 3077 - } 3078 - }, 3079 - "notification-url": "https://packagist.org/downloads/", 3080 - "license": [ 3081 - "MIT" 3082 - ], 3083 - "authors": [ 3084 - { 3085 - "name": "PHP-FIG", 3086 - "homepage": "http://www.php-fig.org/" 3087 - } 3088 - ], 3089 - "description": "Standard interfaces for event handling.", 3090 - "keywords": [ 3091 - "events", 3092 - "psr", 3093 - "psr-14" 3094 - ], 3095 - "support": { 3096 - "issues": "https://github.com/php-fig/event-dispatcher/issues", 3097 - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" 3098 - }, 3099 - "time": "2019-01-08T18:20:26+00:00" 3100 - }, 3101 - { 3102 - "name": "psr/http-client", 3103 - "version": "1.0.3", 3104 - "source": { 3105 - "type": "git", 3106 - "url": "https://github.com/php-fig/http-client.git", 3107 - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" 3108 - }, 3109 - "dist": { 3110 - "type": "zip", 3111 - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", 3112 - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", 3113 - "shasum": "" 3114 - }, 3115 - "require": { 3116 - "php": "^7.0 || ^8.0", 3117 - "psr/http-message": "^1.0 || ^2.0" 3118 - }, 3119 - "type": "library", 3120 - "extra": { 3121 - "branch-alias": { 3122 - "dev-master": "1.0.x-dev" 3123 - } 3124 - }, 3125 - "autoload": { 3126 - "psr-4": { 3127 - "Psr\\Http\\Client\\": "src/" 3128 - } 3129 - }, 3130 - "notification-url": "https://packagist.org/downloads/", 3131 - "license": [ 3132 - "MIT" 3133 - ], 3134 - "authors": [ 3135 - { 3136 - "name": "PHP-FIG", 3137 - "homepage": "https://www.php-fig.org/" 3138 - } 3139 - ], 3140 - "description": "Common interface for HTTP clients", 3141 - "homepage": "https://github.com/php-fig/http-client", 3142 - "keywords": [ 3143 - "http", 3144 - "http-client", 3145 - "psr", 3146 - "psr-18" 3147 - ], 3148 - "support": { 3149 - "source": "https://github.com/php-fig/http-client" 3150 - }, 3151 - "time": "2023-09-23T14:17:50+00:00" 3152 - }, 3153 - { 3154 - "name": "psr/http-factory", 3155 - "version": "1.1.0", 3156 - "source": { 3157 - "type": "git", 3158 - "url": "https://github.com/php-fig/http-factory.git", 3159 - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" 3160 - }, 3161 - "dist": { 3162 - "type": "zip", 3163 - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", 3164 - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", 3165 - "shasum": "" 3166 - }, 3167 - "require": { 3168 - "php": ">=7.1", 3169 - "psr/http-message": "^1.0 || ^2.0" 3170 - }, 3171 - "type": "library", 3172 - "extra": { 3173 - "branch-alias": { 3174 - "dev-master": "1.0.x-dev" 3175 - } 3176 - }, 3177 - "autoload": { 3178 - "psr-4": { 3179 - "Psr\\Http\\Message\\": "src/" 3180 - } 3181 - }, 3182 - "notification-url": "https://packagist.org/downloads/", 3183 - "license": [ 3184 - "MIT" 3185 - ], 3186 - "authors": [ 3187 - { 3188 - "name": "PHP-FIG", 3189 - "homepage": "https://www.php-fig.org/" 3190 - } 3191 - ], 3192 - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", 3193 - "keywords": [ 3194 - "factory", 3195 - "http", 3196 - "message", 3197 - "psr", 3198 - "psr-17", 3199 - "psr-7", 3200 - "request", 3201 - "response" 3202 - ], 3203 - "support": { 3204 - "source": "https://github.com/php-fig/http-factory" 3205 - }, 3206 - "time": "2024-04-15T12:06:14+00:00" 3207 - }, 3208 - { 3209 - "name": "psr/http-message", 3210 - "version": "2.0", 3211 - "source": { 3212 - "type": "git", 3213 - "url": "https://github.com/php-fig/http-message.git", 3214 - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" 3215 - }, 3216 - "dist": { 3217 - "type": "zip", 3218 - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", 3219 - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", 3220 - "shasum": "" 3221 - }, 3222 - "require": { 3223 - "php": "^7.2 || ^8.0" 3224 - }, 3225 - "type": "library", 3226 - "extra": { 3227 - "branch-alias": { 3228 - "dev-master": "2.0.x-dev" 3229 - } 3230 - }, 3231 - "autoload": { 3232 - "psr-4": { 3233 - "Psr\\Http\\Message\\": "src/" 3234 - } 3235 - }, 3236 - "notification-url": "https://packagist.org/downloads/", 3237 - "license": [ 3238 - "MIT" 3239 - ], 3240 - "authors": [ 3241 - { 3242 - "name": "PHP-FIG", 3243 - "homepage": "https://www.php-fig.org/" 3244 - } 3245 - ], 3246 - "description": "Common interface for HTTP messages", 3247 - "homepage": "https://github.com/php-fig/http-message", 3248 - "keywords": [ 3249 - "http", 3250 - "http-message", 3251 - "psr", 3252 - "psr-7", 3253 - "request", 3254 - "response" 3255 - ], 3256 - "support": { 3257 - "source": "https://github.com/php-fig/http-message/tree/2.0" 3258 - }, 3259 - "time": "2023-04-04T09:54:51+00:00" 3260 - }, 3261 - { 3262 - "name": "psr/log", 3263 - "version": "3.0.2", 3264 - "source": { 3265 - "type": "git", 3266 - "url": "https://github.com/php-fig/log.git", 3267 - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" 3268 - }, 3269 - "dist": { 3270 - "type": "zip", 3271 - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", 3272 - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", 3273 - "shasum": "" 3274 - }, 3275 - "require": { 3276 - "php": ">=8.0.0" 3277 - }, 3278 - "type": "library", 3279 - "extra": { 3280 - "branch-alias": { 3281 - "dev-master": "3.x-dev" 3282 - } 3283 - }, 3284 - "autoload": { 3285 - "psr-4": { 3286 - "Psr\\Log\\": "src" 3287 - } 3288 - }, 3289 - "notification-url": "https://packagist.org/downloads/", 3290 - "license": [ 3291 - "MIT" 3292 - ], 3293 - "authors": [ 3294 - { 3295 - "name": "PHP-FIG", 3296 - "homepage": "https://www.php-fig.org/" 3297 - } 3298 - ], 3299 - "description": "Common interface for logging libraries", 3300 - "homepage": "https://github.com/php-fig/log", 3301 - "keywords": [ 3302 - "log", 3303 - "psr", 3304 - "psr-3" 3305 - ], 3306 - "support": { 3307 - "source": "https://github.com/php-fig/log/tree/3.0.2" 3308 - }, 3309 - "time": "2024-09-11T13:17:53+00:00" 3310 - }, 3311 - { 3312 - "name": "psr/simple-cache", 3313 - "version": "3.0.0", 3314 - "source": { 3315 - "type": "git", 3316 - "url": "https://github.com/php-fig/simple-cache.git", 3317 - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" 3318 - }, 3319 - "dist": { 3320 - "type": "zip", 3321 - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", 3322 - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", 3323 - "shasum": "" 3324 - }, 3325 - "require": { 3326 - "php": ">=8.0.0" 3327 - }, 3328 - "type": "library", 3329 - "extra": { 3330 - "branch-alias": { 3331 - "dev-master": "3.0.x-dev" 3332 - } 3333 - }, 3334 - "autoload": { 3335 - "psr-4": { 3336 - "Psr\\SimpleCache\\": "src/" 3337 - } 3338 - }, 3339 - "notification-url": "https://packagist.org/downloads/", 3340 - "license": [ 3341 - "MIT" 3342 - ], 3343 - "authors": [ 3344 - { 3345 - "name": "PHP-FIG", 3346 - "homepage": "https://www.php-fig.org/" 3347 - } 3348 - ], 3349 - "description": "Common interfaces for simple caching", 3350 - "keywords": [ 3351 - "cache", 3352 - "caching", 3353 - "psr", 3354 - "psr-16", 3355 - "simple-cache" 3356 - ], 3357 - "support": { 3358 - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" 3359 - }, 3360 - "time": "2021-10-29T13:26:27+00:00" 3361 - }, 3362 - { 3363 - "name": "ralouphie/getallheaders", 3364 - "version": "3.0.3", 3365 - "source": { 3366 - "type": "git", 3367 - "url": "https://github.com/ralouphie/getallheaders.git", 3368 - "reference": "120b605dfeb996808c31b6477290a714d356e822" 3369 - }, 3370 - "dist": { 3371 - "type": "zip", 3372 - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", 3373 - "reference": "120b605dfeb996808c31b6477290a714d356e822", 3374 - "shasum": "" 3375 - }, 3376 - "require": { 3377 - "php": ">=5.6" 3378 - }, 3379 - "require-dev": { 3380 - "php-coveralls/php-coveralls": "^2.1", 3381 - "phpunit/phpunit": "^5 || ^6.5" 3382 - }, 3383 - "type": "library", 3384 - "autoload": { 3385 - "files": [ 3386 - "src/getallheaders.php" 3387 - ] 3388 - }, 3389 - "notification-url": "https://packagist.org/downloads/", 3390 - "license": [ 3391 - "MIT" 3392 - ], 3393 - "authors": [ 3394 - { 3395 - "name": "Ralph Khattar", 3396 - "email": "ralph.khattar@gmail.com" 3397 - } 3398 - ], 3399 - "description": "A polyfill for getallheaders.", 3400 - "support": { 3401 - "issues": "https://github.com/ralouphie/getallheaders/issues", 3402 - "source": "https://github.com/ralouphie/getallheaders/tree/develop" 3403 - }, 3404 - "time": "2019-03-08T08:55:37+00:00" 3405 - }, 3406 - { 3407 - "name": "ramsey/collection", 3408 - "version": "2.1.1", 3409 - "source": { 3410 - "type": "git", 3411 - "url": "https://github.com/ramsey/collection.git", 3412 - "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" 3413 - }, 3414 - "dist": { 3415 - "type": "zip", 3416 - "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", 3417 - "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", 3418 - "shasum": "" 3419 - }, 3420 - "require": { 3421 - "php": "^8.1" 3422 - }, 3423 - "require-dev": { 3424 - "captainhook/plugin-composer": "^5.3", 3425 - "ergebnis/composer-normalize": "^2.45", 3426 - "fakerphp/faker": "^1.24", 3427 - "hamcrest/hamcrest-php": "^2.0", 3428 - "jangregor/phpstan-prophecy": "^2.1", 3429 - "mockery/mockery": "^1.6", 3430 - "php-parallel-lint/php-console-highlighter": "^1.0", 3431 - "php-parallel-lint/php-parallel-lint": "^1.4", 3432 - "phpspec/prophecy-phpunit": "^2.3", 3433 - "phpstan/extension-installer": "^1.4", 3434 - "phpstan/phpstan": "^2.1", 3435 - "phpstan/phpstan-mockery": "^2.0", 3436 - "phpstan/phpstan-phpunit": "^2.0", 3437 - "phpunit/phpunit": "^10.5", 3438 - "ramsey/coding-standard": "^2.3", 3439 - "ramsey/conventional-commits": "^1.6", 3440 - "roave/security-advisories": "dev-latest" 3441 - }, 3442 - "type": "library", 3443 - "extra": { 3444 - "captainhook": { 3445 - "force-install": true 3446 - }, 3447 - "ramsey/conventional-commits": { 3448 - "configFile": "conventional-commits.json" 3449 - } 3450 - }, 3451 - "autoload": { 3452 - "psr-4": { 3453 - "Ramsey\\Collection\\": "src/" 3454 - } 3455 - }, 3456 - "notification-url": "https://packagist.org/downloads/", 3457 - "license": [ 3458 - "MIT" 3459 - ], 3460 - "authors": [ 3461 - { 3462 - "name": "Ben Ramsey", 3463 - "email": "ben@benramsey.com", 3464 - "homepage": "https://benramsey.com" 3465 - } 3466 - ], 3467 - "description": "A PHP library for representing and manipulating collections.", 3468 - "keywords": [ 3469 - "array", 3470 - "collection", 3471 - "hash", 3472 - "map", 3473 - "queue", 3474 - "set" 3475 - ], 3476 - "support": { 3477 - "issues": "https://github.com/ramsey/collection/issues", 3478 - "source": "https://github.com/ramsey/collection/tree/2.1.1" 3479 - }, 3480 - "time": "2025-03-22T05:38:12+00:00" 3481 - }, 3482 - { 3483 - "name": "ramsey/uuid", 3484 - "version": "4.9.1", 3485 - "source": { 3486 - "type": "git", 3487 - "url": "https://github.com/ramsey/uuid.git", 3488 - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" 3489 - }, 3490 - "dist": { 3491 - "type": "zip", 3492 - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", 3493 - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", 3494 - "shasum": "" 3495 - }, 3496 - "require": { 3497 - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", 3498 - "php": "^8.0", 3499 - "ramsey/collection": "^1.2 || ^2.0" 3500 - }, 3501 - "replace": { 3502 - "rhumsaa/uuid": "self.version" 3503 - }, 3504 - "require-dev": { 3505 - "captainhook/captainhook": "^5.25", 3506 - "captainhook/plugin-composer": "^5.3", 3507 - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 3508 - "ergebnis/composer-normalize": "^2.47", 3509 - "mockery/mockery": "^1.6", 3510 - "paragonie/random-lib": "^2", 3511 - "php-mock/php-mock": "^2.6", 3512 - "php-mock/php-mock-mockery": "^1.5", 3513 - "php-parallel-lint/php-parallel-lint": "^1.4.0", 3514 - "phpbench/phpbench": "^1.2.14", 3515 - "phpstan/extension-installer": "^1.4", 3516 - "phpstan/phpstan": "^2.1", 3517 - "phpstan/phpstan-mockery": "^2.0", 3518 - "phpstan/phpstan-phpunit": "^2.0", 3519 - "phpunit/phpunit": "^9.6", 3520 - "slevomat/coding-standard": "^8.18", 3521 - "squizlabs/php_codesniffer": "^3.13" 3522 - }, 3523 - "suggest": { 3524 - "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", 3525 - "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", 3526 - "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", 3527 - "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", 3528 - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." 3529 - }, 3530 - "type": "library", 3531 - "extra": { 3532 - "captainhook": { 3533 - "force-install": true 3534 - } 3535 - }, 3536 - "autoload": { 3537 - "files": [ 3538 - "src/functions.php" 3539 - ], 3540 - "psr-4": { 3541 - "Ramsey\\Uuid\\": "src/" 3542 - } 3543 - }, 3544 - "notification-url": "https://packagist.org/downloads/", 3545 - "license": [ 3546 - "MIT" 3547 - ], 3548 - "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", 3549 - "keywords": [ 3550 - "guid", 3551 - "identifier", 3552 - "uuid" 3553 - ], 3554 - "support": { 3555 - "issues": "https://github.com/ramsey/uuid/issues", 3556 - "source": "https://github.com/ramsey/uuid/tree/4.9.1" 3557 - }, 3558 - "time": "2025-09-04T20:59:21+00:00" 3559 - }, 3560 - { 3561 - "name": "ratchet/pawl", 3562 - "version": "v0.4.3", 3563 - "source": { 3564 - "type": "git", 3565 - "url": "https://github.com/ratchetphp/Pawl.git", 3566 - "reference": "2c582373c78271de32cb04c755c4c0db7e09c9c0" 3567 - }, 3568 - "dist": { 3569 - "type": "zip", 3570 - "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/2c582373c78271de32cb04c755c4c0db7e09c9c0", 3571 - "reference": "2c582373c78271de32cb04c755c4c0db7e09c9c0", 3572 - "shasum": "" 3573 - }, 3574 - "require": { 3575 - "evenement/evenement": "^3.0 || ^2.0", 3576 - "guzzlehttp/psr7": "^2.0", 3577 - "php": ">=7.4", 3578 - "ratchet/rfc6455": "^0.3.1 || ^0.4.0", 3579 - "react/socket": "^1.9" 3580 - }, 3581 - "require-dev": { 3582 - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8" 3583 - }, 3584 - "suggest": { 3585 - "reactivex/rxphp": "~2.0" 3586 - }, 3587 - "type": "library", 3588 - "autoload": { 3589 - "files": [ 3590 - "src/functions_include.php" 3591 - ], 3592 - "psr-4": { 3593 - "Ratchet\\Client\\": "src" 3594 - } 3595 - }, 3596 - "notification-url": "https://packagist.org/downloads/", 3597 - "license": [ 3598 - "MIT" 3599 - ], 3600 - "description": "Asynchronous WebSocket client", 3601 - "keywords": [ 3602 - "Ratchet", 3603 - "async", 3604 - "client", 3605 - "websocket", 3606 - "websocket client" 3607 - ], 3608 - "support": { 3609 - "issues": "https://github.com/ratchetphp/Pawl/issues", 3610 - "source": "https://github.com/ratchetphp/Pawl/tree/v0.4.3" 3611 - }, 3612 - "time": "2025-03-19T16:47:38+00:00" 3613 - }, 3614 - { 3615 - "name": "ratchet/rfc6455", 3616 - "version": "v0.4.0", 3617 - "source": { 3618 - "type": "git", 3619 - "url": "https://github.com/ratchetphp/RFC6455.git", 3620 - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" 3621 - }, 3622 - "dist": { 3623 - "type": "zip", 3624 - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", 3625 - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", 3626 - "shasum": "" 3627 - }, 3628 - "require": { 3629 - "php": ">=7.4", 3630 - "psr/http-factory-implementation": "^1.0", 3631 - "symfony/polyfill-php80": "^1.15" 3632 - }, 3633 - "require-dev": { 3634 - "guzzlehttp/psr7": "^2.7", 3635 - "phpunit/phpunit": "^9.5", 3636 - "react/socket": "^1.3" 3637 - }, 3638 - "type": "library", 3639 - "autoload": { 3640 - "psr-4": { 3641 - "Ratchet\\RFC6455\\": "src" 3642 - } 3643 - }, 3644 - "notification-url": "https://packagist.org/downloads/", 3645 - "license": [ 3646 - "MIT" 3647 - ], 3648 - "authors": [ 3649 - { 3650 - "name": "Chris Boden", 3651 - "email": "cboden@gmail.com", 3652 - "role": "Developer" 3653 - }, 3654 - { 3655 - "name": "Matt Bonneau", 3656 - "role": "Developer" 3657 - } 3658 - ], 3659 - "description": "RFC6455 WebSocket protocol handler", 3660 - "homepage": "http://socketo.me", 3661 - "keywords": [ 3662 - "WebSockets", 3663 - "rfc6455", 3664 - "websocket" 3665 - ], 3666 - "support": { 3667 - "chat": "https://gitter.im/reactphp/reactphp", 3668 - "issues": "https://github.com/ratchetphp/RFC6455/issues", 3669 - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" 3670 - }, 3671 - "time": "2025-02-24T01:18:22+00:00" 3672 - }, 3673 - { 3674 - "name": "react/cache", 3675 - "version": "v1.2.0", 3676 - "source": { 3677 - "type": "git", 3678 - "url": "https://github.com/reactphp/cache.git", 3679 - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" 3680 - }, 3681 - "dist": { 3682 - "type": "zip", 3683 - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", 3684 - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", 3685 - "shasum": "" 3686 - }, 3687 - "require": { 3688 - "php": ">=5.3.0", 3689 - "react/promise": "^3.0 || ^2.0 || ^1.1" 3690 - }, 3691 - "require-dev": { 3692 - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" 3693 - }, 3694 - "type": "library", 3695 - "autoload": { 3696 - "psr-4": { 3697 - "React\\Cache\\": "src/" 3698 - } 3699 - }, 3700 - "notification-url": "https://packagist.org/downloads/", 3701 - "license": [ 3702 - "MIT" 3703 - ], 3704 - "authors": [ 3705 - { 3706 - "name": "Christian Lรผck", 3707 - "email": "christian@clue.engineering", 3708 - "homepage": "https://clue.engineering/" 3709 - }, 3710 - { 3711 - "name": "Cees-Jan Kiewiet", 3712 - "email": "reactphp@ceesjankiewiet.nl", 3713 - "homepage": "https://wyrihaximus.net/" 3714 - }, 3715 - { 3716 - "name": "Jan Sorgalla", 3717 - "email": "jsorgalla@gmail.com", 3718 - "homepage": "https://sorgalla.com/" 3719 - }, 3720 - { 3721 - "name": "Chris Boden", 3722 - "email": "cboden@gmail.com", 3723 - "homepage": "https://cboden.dev/" 3724 - } 3725 - ], 3726 - "description": "Async, Promise-based cache interface for ReactPHP", 3727 - "keywords": [ 3728 - "cache", 3729 - "caching", 3730 - "promise", 3731 - "reactphp" 3732 - ], 3733 - "support": { 3734 - "issues": "https://github.com/reactphp/cache/issues", 3735 - "source": "https://github.com/reactphp/cache/tree/v1.2.0" 3736 - }, 3737 - "funding": [ 3738 - { 3739 - "url": "https://opencollective.com/reactphp", 3740 - "type": "open_collective" 3741 - } 3742 - ], 3743 - "time": "2022-11-30T15:59:55+00:00" 3744 - }, 3745 - { 3746 - "name": "react/dns", 3747 - "version": "v1.13.0", 3748 - "source": { 3749 - "type": "git", 3750 - "url": "https://github.com/reactphp/dns.git", 3751 - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" 3752 - }, 3753 - "dist": { 3754 - "type": "zip", 3755 - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", 3756 - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", 3757 - "shasum": "" 3758 - }, 3759 - "require": { 3760 - "php": ">=5.3.0", 3761 - "react/cache": "^1.0 || ^0.6 || ^0.5", 3762 - "react/event-loop": "^1.2", 3763 - "react/promise": "^3.2 || ^2.7 || ^1.2.1" 3764 - }, 3765 - "require-dev": { 3766 - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", 3767 - "react/async": "^4.3 || ^3 || ^2", 3768 - "react/promise-timer": "^1.11" 3769 - }, 3770 - "type": "library", 3771 - "autoload": { 3772 - "psr-4": { 3773 - "React\\Dns\\": "src/" 3774 - } 3775 - }, 3776 - "notification-url": "https://packagist.org/downloads/", 3777 - "license": [ 3778 - "MIT" 3779 - ], 3780 - "authors": [ 3781 - { 3782 - "name": "Christian Lรผck", 3783 - "email": "christian@clue.engineering", 3784 - "homepage": "https://clue.engineering/" 3785 - }, 3786 - { 3787 - "name": "Cees-Jan Kiewiet", 3788 - "email": "reactphp@ceesjankiewiet.nl", 3789 - "homepage": "https://wyrihaximus.net/" 3790 - }, 3791 - { 3792 - "name": "Jan Sorgalla", 3793 - "email": "jsorgalla@gmail.com", 3794 - "homepage": "https://sorgalla.com/" 3795 - }, 3796 - { 3797 - "name": "Chris Boden", 3798 - "email": "cboden@gmail.com", 3799 - "homepage": "https://cboden.dev/" 3800 - } 3801 - ], 3802 - "description": "Async DNS resolver for ReactPHP", 3803 - "keywords": [ 3804 - "async", 3805 - "dns", 3806 - "dns-resolver", 3807 - "reactphp" 3808 - ], 3809 - "support": { 3810 - "issues": "https://github.com/reactphp/dns/issues", 3811 - "source": "https://github.com/reactphp/dns/tree/v1.13.0" 3812 - }, 3813 - "funding": [ 3814 - { 3815 - "url": "https://opencollective.com/reactphp", 3816 - "type": "open_collective" 3817 - } 3818 - ], 3819 - "time": "2024-06-13T14:18:03+00:00" 3820 - }, 3821 - { 3822 - "name": "react/event-loop", 3823 - "version": "v1.5.0", 3824 - "source": { 3825 - "type": "git", 3826 - "url": "https://github.com/reactphp/event-loop.git", 3827 - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" 3828 - }, 3829 - "dist": { 3830 - "type": "zip", 3831 - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", 3832 - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", 3833 - "shasum": "" 3834 - }, 3835 - "require": { 3836 - "php": ">=5.3.0" 3837 - }, 3838 - "require-dev": { 3839 - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 3840 - }, 3841 - "suggest": { 3842 - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" 3843 - }, 3844 - "type": "library", 3845 - "autoload": { 3846 - "psr-4": { 3847 - "React\\EventLoop\\": "src/" 3848 - } 3849 - }, 3850 - "notification-url": "https://packagist.org/downloads/", 3851 - "license": [ 3852 - "MIT" 3853 - ], 3854 - "authors": [ 3855 - { 3856 - "name": "Christian Lรผck", 3857 - "email": "christian@clue.engineering", 3858 - "homepage": "https://clue.engineering/" 3859 - }, 3860 - { 3861 - "name": "Cees-Jan Kiewiet", 3862 - "email": "reactphp@ceesjankiewiet.nl", 3863 - "homepage": "https://wyrihaximus.net/" 3864 - }, 3865 - { 3866 - "name": "Jan Sorgalla", 3867 - "email": "jsorgalla@gmail.com", 3868 - "homepage": "https://sorgalla.com/" 3869 - }, 3870 - { 3871 - "name": "Chris Boden", 3872 - "email": "cboden@gmail.com", 3873 - "homepage": "https://cboden.dev/" 3874 - } 3875 - ], 3876 - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", 3877 - "keywords": [ 3878 - "asynchronous", 3879 - "event-loop" 3880 - ], 3881 - "support": { 3882 - "issues": "https://github.com/reactphp/event-loop/issues", 3883 - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" 3884 - }, 3885 - "funding": [ 3886 - { 3887 - "url": "https://opencollective.com/reactphp", 3888 - "type": "open_collective" 3889 - } 3890 - ], 3891 - "time": "2023-11-13T13:48:05+00:00" 3892 - }, 3893 - { 3894 - "name": "react/promise", 3895 - "version": "v3.3.0", 3896 - "source": { 3897 - "type": "git", 3898 - "url": "https://github.com/reactphp/promise.git", 3899 - "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" 3900 - }, 3901 - "dist": { 3902 - "type": "zip", 3903 - "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", 3904 - "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", 3905 - "shasum": "" 3906 - }, 3907 - "require": { 3908 - "php": ">=7.1.0" 3909 - }, 3910 - "require-dev": { 3911 - "phpstan/phpstan": "1.12.28 || 1.4.10", 3912 - "phpunit/phpunit": "^9.6 || ^7.5" 3913 - }, 3914 - "type": "library", 3915 - "autoload": { 3916 - "files": [ 3917 - "src/functions_include.php" 3918 - ], 3919 - "psr-4": { 3920 - "React\\Promise\\": "src/" 3921 - } 3922 - }, 3923 - "notification-url": "https://packagist.org/downloads/", 3924 - "license": [ 3925 - "MIT" 3926 - ], 3927 - "authors": [ 3928 - { 3929 - "name": "Jan Sorgalla", 3930 - "email": "jsorgalla@gmail.com", 3931 - "homepage": "https://sorgalla.com/" 3932 - }, 3933 - { 3934 - "name": "Christian Lรผck", 3935 - "email": "christian@clue.engineering", 3936 - "homepage": "https://clue.engineering/" 3937 - }, 3938 - { 3939 - "name": "Cees-Jan Kiewiet", 3940 - "email": "reactphp@ceesjankiewiet.nl", 3941 - "homepage": "https://wyrihaximus.net/" 3942 - }, 3943 - { 3944 - "name": "Chris Boden", 3945 - "email": "cboden@gmail.com", 3946 - "homepage": "https://cboden.dev/" 3947 - } 3948 - ], 3949 - "description": "A lightweight implementation of CommonJS Promises/A for PHP", 3950 - "keywords": [ 3951 - "promise", 3952 - "promises" 3953 - ], 3954 - "support": { 3955 - "issues": "https://github.com/reactphp/promise/issues", 3956 - "source": "https://github.com/reactphp/promise/tree/v3.3.0" 3957 - }, 3958 - "funding": [ 3959 - { 3960 - "url": "https://opencollective.com/reactphp", 3961 - "type": "open_collective" 3962 - } 3963 - ], 3964 - "time": "2025-08-19T18:57:03+00:00" 3965 - }, 3966 - { 3967 - "name": "react/socket", 3968 - "version": "v1.16.0", 3969 - "source": { 3970 - "type": "git", 3971 - "url": "https://github.com/reactphp/socket.git", 3972 - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" 3973 - }, 3974 - "dist": { 3975 - "type": "zip", 3976 - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", 3977 - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", 3978 - "shasum": "" 3979 - }, 3980 - "require": { 3981 - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 3982 - "php": ">=5.3.0", 3983 - "react/dns": "^1.13", 3984 - "react/event-loop": "^1.2", 3985 - "react/promise": "^3.2 || ^2.6 || ^1.2.1", 3986 - "react/stream": "^1.4" 3987 - }, 3988 - "require-dev": { 3989 - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", 3990 - "react/async": "^4.3 || ^3.3 || ^2", 3991 - "react/promise-stream": "^1.4", 3992 - "react/promise-timer": "^1.11" 3993 - }, 3994 - "type": "library", 3995 - "autoload": { 3996 - "psr-4": { 3997 - "React\\Socket\\": "src/" 3998 - } 3999 - }, 4000 - "notification-url": "https://packagist.org/downloads/", 4001 - "license": [ 4002 - "MIT" 4003 - ], 4004 - "authors": [ 4005 - { 4006 - "name": "Christian Lรผck", 4007 - "email": "christian@clue.engineering", 4008 - "homepage": "https://clue.engineering/" 4009 - }, 4010 - { 4011 - "name": "Cees-Jan Kiewiet", 4012 - "email": "reactphp@ceesjankiewiet.nl", 4013 - "homepage": "https://wyrihaximus.net/" 4014 - }, 4015 - { 4016 - "name": "Jan Sorgalla", 4017 - "email": "jsorgalla@gmail.com", 4018 - "homepage": "https://sorgalla.com/" 4019 - }, 4020 - { 4021 - "name": "Chris Boden", 4022 - "email": "cboden@gmail.com", 4023 - "homepage": "https://cboden.dev/" 4024 - } 4025 - ], 4026 - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", 4027 - "keywords": [ 4028 - "Connection", 4029 - "Socket", 4030 - "async", 4031 - "reactphp", 4032 - "stream" 4033 - ], 4034 - "support": { 4035 - "issues": "https://github.com/reactphp/socket/issues", 4036 - "source": "https://github.com/reactphp/socket/tree/v1.16.0" 4037 - }, 4038 - "funding": [ 4039 - { 4040 - "url": "https://opencollective.com/reactphp", 4041 - "type": "open_collective" 4042 - } 4043 - ], 4044 - "time": "2024-07-26T10:38:09+00:00" 4045 - }, 4046 - { 4047 - "name": "react/stream", 4048 - "version": "v1.4.0", 4049 - "source": { 4050 - "type": "git", 4051 - "url": "https://github.com/reactphp/stream.git", 4052 - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" 4053 - }, 4054 - "dist": { 4055 - "type": "zip", 4056 - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", 4057 - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", 4058 - "shasum": "" 4059 - }, 4060 - "require": { 4061 - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 4062 - "php": ">=5.3.8", 4063 - "react/event-loop": "^1.2" 4064 - }, 4065 - "require-dev": { 4066 - "clue/stream-filter": "~1.2", 4067 - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 4068 - }, 4069 - "type": "library", 4070 - "autoload": { 4071 - "psr-4": { 4072 - "React\\Stream\\": "src/" 4073 - } 4074 - }, 4075 - "notification-url": "https://packagist.org/downloads/", 4076 - "license": [ 4077 - "MIT" 4078 - ], 4079 - "authors": [ 4080 - { 4081 - "name": "Christian Lรผck", 4082 - "email": "christian@clue.engineering", 4083 - "homepage": "https://clue.engineering/" 4084 - }, 4085 - { 4086 - "name": "Cees-Jan Kiewiet", 4087 - "email": "reactphp@ceesjankiewiet.nl", 4088 - "homepage": "https://wyrihaximus.net/" 4089 - }, 4090 - { 4091 - "name": "Jan Sorgalla", 4092 - "email": "jsorgalla@gmail.com", 4093 - "homepage": "https://sorgalla.com/" 4094 - }, 4095 - { 4096 - "name": "Chris Boden", 4097 - "email": "cboden@gmail.com", 4098 - "homepage": "https://cboden.dev/" 4099 - } 4100 - ], 4101 - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", 4102 - "keywords": [ 4103 - "event-driven", 4104 - "io", 4105 - "non-blocking", 4106 - "pipe", 4107 - "reactphp", 4108 - "readable", 4109 - "stream", 4110 - "writable" 4111 - ], 4112 - "support": { 4113 - "issues": "https://github.com/reactphp/stream/issues", 4114 - "source": "https://github.com/reactphp/stream/tree/v1.4.0" 4115 - }, 4116 - "funding": [ 4117 - { 4118 - "url": "https://opencollective.com/reactphp", 4119 - "type": "open_collective" 4120 - } 4121 - ], 4122 - "time": "2024-06-11T12:45:25+00:00" 4123 - }, 4124 - { 4125 - "name": "revolution/atproto-lexicon-contracts", 4126 - "version": "1.0.82", 4127 - "source": { 4128 - "type": "git", 4129 - "url": "https://github.com/invokable/atproto-lexicon-contracts.git", 4130 - "reference": "07110f78d88ce49547d664140b83165ddfb5b5cf" 4131 - }, 4132 - "dist": { 4133 - "type": "zip", 4134 - "url": "https://api.github.com/repos/invokable/atproto-lexicon-contracts/zipball/07110f78d88ce49547d664140b83165ddfb5b5cf", 4135 - "reference": "07110f78d88ce49547d664140b83165ddfb5b5cf", 4136 - "shasum": "" 4137 - }, 4138 - "require": { 4139 - "php": "^8.2" 4140 - }, 4141 - "require-dev": { 4142 - "guzzlehttp/guzzle": "^7.9", 4143 - "laravel/pint": "^1.22" 4144 - }, 4145 - "type": "library", 4146 - "autoload": { 4147 - "psr-4": { 4148 - "Revolution\\AtProto\\Lexicon\\": "src/" 4149 - } 4150 - }, 4151 - "notification-url": "https://packagist.org/downloads/", 4152 - "license": [ 4153 - "MIT" 4154 - ], 4155 - "authors": [ 4156 - { 4157 - "name": "kawax", 4158 - "email": "kawaxbiz@gmail.com" 4159 - } 4160 - ], 4161 - "description": "Auto generated pure PHP interface and enum", 4162 - "keywords": [ 4163 - "Lexicon", 4164 - "atproto", 4165 - "bluesky", 4166 - "contracts" 4167 - ], 4168 - "support": { 4169 - "source": "https://github.com/invokable/atproto-lexicon-contracts/tree/1.0.82" 4170 - }, 4171 - "funding": [ 4172 - { 4173 - "url": "https://github.com/invokable", 4174 - "type": "github" 4175 - } 4176 - ], 4177 - "time": "2025-09-03T04:11:59+00:00" 4178 - }, 4179 - { 4180 - "name": "revolution/laravel-bluesky", 4181 - "version": "1.1.3", 4182 - "source": { 4183 - "type": "git", 4184 - "url": "https://github.com/invokable/laravel-bluesky.git", 4185 - "reference": "bdee3d69d4b95388696864559e5cd1c506b94b73" 4186 - }, 4187 - "dist": { 4188 - "type": "zip", 4189 - "url": "https://api.github.com/repos/invokable/laravel-bluesky/zipball/bdee3d69d4b95388696864559e5cd1c506b94b73", 4190 - "reference": "bdee3d69d4b95388696864559e5cd1c506b94b73", 4191 - "shasum": "" 4192 - }, 4193 - "require": { 4194 - "firebase/php-jwt": "^6.10", 4195 - "guzzlehttp/guzzle": "^7.8", 4196 - "illuminate/support": "^11.30||^12.0", 4197 - "laravel/socialite": "^5.16", 4198 - "php": "^8.2", 4199 - "phpseclib/phpseclib": "^3.0", 4200 - "revolution/atproto-lexicon-contracts": "1.0.82", 4201 - "yocto/yoclib-multibase": "^1.2" 4202 - }, 4203 - "require-dev": { 4204 - "laravel/pint": "^1.22", 4205 - "orchestra/testbench": "^9.0||^10.0", 4206 - "revolt/event-loop": "^1.0", 4207 - "workerman/workerman": "^5.0" 4208 - }, 4209 - "suggest": { 4210 - "ext-gmp": "*", 4211 - "ext-pcntl": "*", 4212 - "revolt/event-loop": "Required to use WebSocket.", 4213 - "workerman/workerman": "Required to use WebSocket." 4214 - }, 4215 - "type": "library", 4216 - "extra": { 4217 - "laravel": { 4218 - "providers": [ 4219 - "Revolution\\Bluesky\\Providers\\BlueskyServiceProvider" 4220 - ] 4221 - } 4222 - }, 4223 - "autoload": { 4224 - "psr-4": { 4225 - "Revolution\\Bluesky\\": "src/" 4226 - } 4227 - }, 4228 - "notification-url": "https://packagist.org/downloads/", 4229 - "license": [ 4230 - "MIT" 4231 - ], 4232 - "authors": [ 4233 - { 4234 - "name": "kawax", 4235 - "email": "kawaxbiz@gmail.com" 4236 - } 4237 - ], 4238 - "description": "Bluesky(AT Protocol) for Laravel", 4239 - "keywords": [ 4240 - "atproto", 4241 - "bluesky", 4242 - "feed-generator", 4243 - "labeler", 4244 - "laravel", 4245 - "notifications", 4246 - "socialite" 4247 - ], 4248 - "support": { 4249 - "source": "https://github.com/invokable/laravel-bluesky/tree/1.1.3" 4250 - }, 4251 - "funding": [ 4252 - { 4253 - "url": "https://github.com/invokable", 4254 - "type": "github" 4255 - } 4256 - ], 4257 - "time": "2025-09-03T05:23:09+00:00" 4258 - }, 4259 - { 4260 - "name": "symfony/clock", 4261 - "version": "v7.3.0", 4262 - "source": { 4263 - "type": "git", 4264 - "url": "https://github.com/symfony/clock.git", 4265 - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" 4266 - }, 4267 - "dist": { 4268 - "type": "zip", 4269 - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", 4270 - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", 4271 - "shasum": "" 4272 - }, 4273 - "require": { 4274 - "php": ">=8.2", 4275 - "psr/clock": "^1.0", 4276 - "symfony/polyfill-php83": "^1.28" 4277 - }, 4278 - "provide": { 4279 - "psr/clock-implementation": "1.0" 4280 - }, 4281 - "type": "library", 4282 - "autoload": { 4283 - "files": [ 4284 - "Resources/now.php" 4285 - ], 4286 - "psr-4": { 4287 - "Symfony\\Component\\Clock\\": "" 4288 - }, 4289 - "exclude-from-classmap": [ 4290 - "/Tests/" 4291 - ] 4292 - }, 4293 - "notification-url": "https://packagist.org/downloads/", 4294 - "license": [ 4295 - "MIT" 4296 - ], 4297 - "authors": [ 4298 - { 4299 - "name": "Nicolas Grekas", 4300 - "email": "p@tchwork.com" 4301 - }, 4302 - { 4303 - "name": "Symfony Community", 4304 - "homepage": "https://symfony.com/contributors" 4305 - } 4306 - ], 4307 - "description": "Decouples applications from the system clock", 4308 - "homepage": "https://symfony.com", 4309 - "keywords": [ 4310 - "clock", 4311 - "psr20", 4312 - "time" 4313 - ], 4314 - "support": { 4315 - "source": "https://github.com/symfony/clock/tree/v7.3.0" 4316 - }, 4317 - "funding": [ 4318 - { 4319 - "url": "https://symfony.com/sponsor", 4320 - "type": "custom" 4321 - }, 4322 - { 4323 - "url": "https://github.com/fabpot", 4324 - "type": "github" 4325 - }, 4326 - { 4327 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4328 - "type": "tidelift" 4329 - } 4330 - ], 4331 - "time": "2024-09-25T14:21:43+00:00" 4332 - }, 4333 - { 4334 - "name": "symfony/console", 4335 - "version": "v7.3.5", 4336 - "source": { 4337 - "type": "git", 4338 - "url": "https://github.com/symfony/console.git", 4339 - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" 4340 - }, 4341 - "dist": { 4342 - "type": "zip", 4343 - "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", 4344 - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", 4345 - "shasum": "" 4346 - }, 4347 - "require": { 4348 - "php": ">=8.2", 4349 - "symfony/deprecation-contracts": "^2.5|^3", 4350 - "symfony/polyfill-mbstring": "~1.0", 4351 - "symfony/service-contracts": "^2.5|^3", 4352 - "symfony/string": "^7.2" 4353 - }, 4354 - "conflict": { 4355 - "symfony/dependency-injection": "<6.4", 4356 - "symfony/dotenv": "<6.4", 4357 - "symfony/event-dispatcher": "<6.4", 4358 - "symfony/lock": "<6.4", 4359 - "symfony/process": "<6.4" 4360 - }, 4361 - "provide": { 4362 - "psr/log-implementation": "1.0|2.0|3.0" 4363 - }, 4364 - "require-dev": { 4365 - "psr/log": "^1|^2|^3", 4366 - "symfony/config": "^6.4|^7.0", 4367 - "symfony/dependency-injection": "^6.4|^7.0", 4368 - "symfony/event-dispatcher": "^6.4|^7.0", 4369 - "symfony/http-foundation": "^6.4|^7.0", 4370 - "symfony/http-kernel": "^6.4|^7.0", 4371 - "symfony/lock": "^6.4|^7.0", 4372 - "symfony/messenger": "^6.4|^7.0", 4373 - "symfony/process": "^6.4|^7.0", 4374 - "symfony/stopwatch": "^6.4|^7.0", 4375 - "symfony/var-dumper": "^6.4|^7.0" 4376 - }, 4377 - "type": "library", 4378 - "autoload": { 4379 - "psr-4": { 4380 - "Symfony\\Component\\Console\\": "" 4381 - }, 4382 - "exclude-from-classmap": [ 4383 - "/Tests/" 4384 - ] 4385 - }, 4386 - "notification-url": "https://packagist.org/downloads/", 4387 - "license": [ 4388 - "MIT" 4389 - ], 4390 - "authors": [ 4391 - { 4392 - "name": "Fabien Potencier", 4393 - "email": "fabien@symfony.com" 4394 - }, 4395 - { 4396 - "name": "Symfony Community", 4397 - "homepage": "https://symfony.com/contributors" 4398 - } 4399 - ], 4400 - "description": "Eases the creation of beautiful and testable command line interfaces", 4401 - "homepage": "https://symfony.com", 4402 - "keywords": [ 4403 - "cli", 4404 - "command-line", 4405 - "console", 4406 - "terminal" 4407 - ], 4408 - "support": { 4409 - "source": "https://github.com/symfony/console/tree/v7.3.5" 4410 - }, 4411 - "funding": [ 4412 - { 4413 - "url": "https://symfony.com/sponsor", 4414 - "type": "custom" 4415 - }, 4416 - { 4417 - "url": "https://github.com/fabpot", 4418 - "type": "github" 4419 - }, 4420 - { 4421 - "url": "https://github.com/nicolas-grekas", 4422 - "type": "github" 4423 - }, 4424 - { 4425 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4426 - "type": "tidelift" 4427 - } 4428 - ], 4429 - "time": "2025-10-14T15:46:26+00:00" 4430 - }, 4431 - { 4432 - "name": "symfony/css-selector", 4433 - "version": "v7.3.0", 4434 - "source": { 4435 - "type": "git", 4436 - "url": "https://github.com/symfony/css-selector.git", 4437 - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" 4438 - }, 4439 - "dist": { 4440 - "type": "zip", 4441 - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", 4442 - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", 4443 - "shasum": "" 4444 - }, 4445 - "require": { 4446 - "php": ">=8.2" 4447 - }, 4448 - "type": "library", 4449 - "autoload": { 4450 - "psr-4": { 4451 - "Symfony\\Component\\CssSelector\\": "" 4452 - }, 4453 - "exclude-from-classmap": [ 4454 - "/Tests/" 4455 - ] 4456 - }, 4457 - "notification-url": "https://packagist.org/downloads/", 4458 - "license": [ 4459 - "MIT" 4460 - ], 4461 - "authors": [ 4462 - { 4463 - "name": "Fabien Potencier", 4464 - "email": "fabien@symfony.com" 4465 - }, 4466 - { 4467 - "name": "Jean-Franรงois Simon", 4468 - "email": "jeanfrancois.simon@sensiolabs.com" 4469 - }, 4470 - { 4471 - "name": "Symfony Community", 4472 - "homepage": "https://symfony.com/contributors" 4473 - } 4474 - ], 4475 - "description": "Converts CSS selectors to XPath expressions", 4476 - "homepage": "https://symfony.com", 4477 - "support": { 4478 - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" 4479 - }, 4480 - "funding": [ 4481 - { 4482 - "url": "https://symfony.com/sponsor", 4483 - "type": "custom" 4484 - }, 4485 - { 4486 - "url": "https://github.com/fabpot", 4487 - "type": "github" 4488 - }, 4489 - { 4490 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4491 - "type": "tidelift" 4492 - } 4493 - ], 4494 - "time": "2024-09-25T14:21:43+00:00" 4495 - }, 4496 - { 4497 - "name": "symfony/deprecation-contracts", 4498 - "version": "v3.6.0", 4499 - "source": { 4500 - "type": "git", 4501 - "url": "https://github.com/symfony/deprecation-contracts.git", 4502 - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" 4503 - }, 4504 - "dist": { 4505 - "type": "zip", 4506 - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", 4507 - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", 4508 - "shasum": "" 4509 - }, 4510 - "require": { 4511 - "php": ">=8.1" 4512 - }, 4513 - "type": "library", 4514 - "extra": { 4515 - "thanks": { 4516 - "url": "https://github.com/symfony/contracts", 4517 - "name": "symfony/contracts" 4518 - }, 4519 - "branch-alias": { 4520 - "dev-main": "3.6-dev" 4521 - } 4522 - }, 4523 - "autoload": { 4524 - "files": [ 4525 - "function.php" 4526 - ] 4527 - }, 4528 - "notification-url": "https://packagist.org/downloads/", 4529 - "license": [ 4530 - "MIT" 4531 - ], 4532 - "authors": [ 4533 - { 4534 - "name": "Nicolas Grekas", 4535 - "email": "p@tchwork.com" 4536 - }, 4537 - { 4538 - "name": "Symfony Community", 4539 - "homepage": "https://symfony.com/contributors" 4540 - } 4541 - ], 4542 - "description": "A generic function and convention to trigger deprecation notices", 4543 - "homepage": "https://symfony.com", 4544 - "support": { 4545 - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" 4546 - }, 4547 - "funding": [ 4548 - { 4549 - "url": "https://symfony.com/sponsor", 4550 - "type": "custom" 4551 - }, 4552 - { 4553 - "url": "https://github.com/fabpot", 4554 - "type": "github" 4555 - }, 4556 - { 4557 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4558 - "type": "tidelift" 4559 - } 4560 - ], 4561 - "time": "2024-09-25T14:21:43+00:00" 4562 - }, 4563 - { 4564 - "name": "symfony/error-handler", 4565 - "version": "v7.3.4", 4566 - "source": { 4567 - "type": "git", 4568 - "url": "https://github.com/symfony/error-handler.git", 4569 - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" 4570 - }, 4571 - "dist": { 4572 - "type": "zip", 4573 - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", 4574 - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", 4575 - "shasum": "" 4576 - }, 4577 - "require": { 4578 - "php": ">=8.2", 4579 - "psr/log": "^1|^2|^3", 4580 - "symfony/var-dumper": "^6.4|^7.0" 4581 - }, 4582 - "conflict": { 4583 - "symfony/deprecation-contracts": "<2.5", 4584 - "symfony/http-kernel": "<6.4" 4585 - }, 4586 - "require-dev": { 4587 - "symfony/console": "^6.4|^7.0", 4588 - "symfony/deprecation-contracts": "^2.5|^3", 4589 - "symfony/http-kernel": "^6.4|^7.0", 4590 - "symfony/serializer": "^6.4|^7.0", 4591 - "symfony/webpack-encore-bundle": "^1.0|^2.0" 4592 - }, 4593 - "bin": [ 4594 - "Resources/bin/patch-type-declarations" 4595 - ], 4596 - "type": "library", 4597 - "autoload": { 4598 - "psr-4": { 4599 - "Symfony\\Component\\ErrorHandler\\": "" 4600 - }, 4601 - "exclude-from-classmap": [ 4602 - "/Tests/" 4603 - ] 4604 - }, 4605 - "notification-url": "https://packagist.org/downloads/", 4606 - "license": [ 4607 - "MIT" 4608 - ], 4609 - "authors": [ 4610 - { 4611 - "name": "Fabien Potencier", 4612 - "email": "fabien@symfony.com" 4613 - }, 4614 - { 4615 - "name": "Symfony Community", 4616 - "homepage": "https://symfony.com/contributors" 4617 - } 4618 - ], 4619 - "description": "Provides tools to manage errors and ease debugging PHP code", 4620 - "homepage": "https://symfony.com", 4621 - "support": { 4622 - "source": "https://github.com/symfony/error-handler/tree/v7.3.4" 4623 - }, 4624 - "funding": [ 4625 - { 4626 - "url": "https://symfony.com/sponsor", 4627 - "type": "custom" 4628 - }, 4629 - { 4630 - "url": "https://github.com/fabpot", 4631 - "type": "github" 4632 - }, 4633 - { 4634 - "url": "https://github.com/nicolas-grekas", 4635 - "type": "github" 4636 - }, 4637 - { 4638 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4639 - "type": "tidelift" 4640 - } 4641 - ], 4642 - "time": "2025-09-11T10:12:26+00:00" 4643 - }, 4644 - { 4645 - "name": "symfony/event-dispatcher", 4646 - "version": "v7.3.3", 4647 - "source": { 4648 - "type": "git", 4649 - "url": "https://github.com/symfony/event-dispatcher.git", 4650 - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" 4651 - }, 4652 - "dist": { 4653 - "type": "zip", 4654 - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", 4655 - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", 4656 - "shasum": "" 4657 - }, 4658 - "require": { 4659 - "php": ">=8.2", 4660 - "symfony/event-dispatcher-contracts": "^2.5|^3" 4661 - }, 4662 - "conflict": { 4663 - "symfony/dependency-injection": "<6.4", 4664 - "symfony/service-contracts": "<2.5" 4665 - }, 4666 - "provide": { 4667 - "psr/event-dispatcher-implementation": "1.0", 4668 - "symfony/event-dispatcher-implementation": "2.0|3.0" 4669 - }, 4670 - "require-dev": { 4671 - "psr/log": "^1|^2|^3", 4672 - "symfony/config": "^6.4|^7.0", 4673 - "symfony/dependency-injection": "^6.4|^7.0", 4674 - "symfony/error-handler": "^6.4|^7.0", 4675 - "symfony/expression-language": "^6.4|^7.0", 4676 - "symfony/http-foundation": "^6.4|^7.0", 4677 - "symfony/service-contracts": "^2.5|^3", 4678 - "symfony/stopwatch": "^6.4|^7.0" 4679 - }, 4680 - "type": "library", 4681 - "autoload": { 4682 - "psr-4": { 4683 - "Symfony\\Component\\EventDispatcher\\": "" 4684 - }, 4685 - "exclude-from-classmap": [ 4686 - "/Tests/" 4687 - ] 4688 - }, 4689 - "notification-url": "https://packagist.org/downloads/", 4690 - "license": [ 4691 - "MIT" 4692 - ], 4693 - "authors": [ 4694 - { 4695 - "name": "Fabien Potencier", 4696 - "email": "fabien@symfony.com" 4697 - }, 4698 - { 4699 - "name": "Symfony Community", 4700 - "homepage": "https://symfony.com/contributors" 4701 - } 4702 - ], 4703 - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", 4704 - "homepage": "https://symfony.com", 4705 - "support": { 4706 - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" 4707 - }, 4708 - "funding": [ 4709 - { 4710 - "url": "https://symfony.com/sponsor", 4711 - "type": "custom" 4712 - }, 4713 - { 4714 - "url": "https://github.com/fabpot", 4715 - "type": "github" 4716 - }, 4717 - { 4718 - "url": "https://github.com/nicolas-grekas", 4719 - "type": "github" 4720 - }, 4721 - { 4722 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4723 - "type": "tidelift" 4724 - } 4725 - ], 4726 - "time": "2025-08-13T11:49:31+00:00" 4727 - }, 4728 - { 4729 - "name": "symfony/event-dispatcher-contracts", 4730 - "version": "v3.6.0", 4731 - "source": { 4732 - "type": "git", 4733 - "url": "https://github.com/symfony/event-dispatcher-contracts.git", 4734 - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" 4735 - }, 4736 - "dist": { 4737 - "type": "zip", 4738 - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", 4739 - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", 4740 - "shasum": "" 4741 - }, 4742 - "require": { 4743 - "php": ">=8.1", 4744 - "psr/event-dispatcher": "^1" 4745 - }, 4746 - "type": "library", 4747 - "extra": { 4748 - "thanks": { 4749 - "url": "https://github.com/symfony/contracts", 4750 - "name": "symfony/contracts" 4751 - }, 4752 - "branch-alias": { 4753 - "dev-main": "3.6-dev" 4754 - } 4755 - }, 4756 - "autoload": { 4757 - "psr-4": { 4758 - "Symfony\\Contracts\\EventDispatcher\\": "" 4759 - } 4760 - }, 4761 - "notification-url": "https://packagist.org/downloads/", 4762 - "license": [ 4763 - "MIT" 4764 - ], 4765 - "authors": [ 4766 - { 4767 - "name": "Nicolas Grekas", 4768 - "email": "p@tchwork.com" 4769 - }, 4770 - { 4771 - "name": "Symfony Community", 4772 - "homepage": "https://symfony.com/contributors" 4773 - } 4774 - ], 4775 - "description": "Generic abstractions related to dispatching event", 4776 - "homepage": "https://symfony.com", 4777 - "keywords": [ 4778 - "abstractions", 4779 - "contracts", 4780 - "decoupling", 4781 - "interfaces", 4782 - "interoperability", 4783 - "standards" 4784 - ], 4785 - "support": { 4786 - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" 4787 - }, 4788 - "funding": [ 4789 - { 4790 - "url": "https://symfony.com/sponsor", 4791 - "type": "custom" 4792 - }, 4793 - { 4794 - "url": "https://github.com/fabpot", 4795 - "type": "github" 4796 - }, 4797 - { 4798 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4799 - "type": "tidelift" 4800 - } 4801 - ], 4802 - "time": "2024-09-25T14:21:43+00:00" 4803 - }, 4804 - { 4805 - "name": "symfony/finder", 4806 - "version": "v7.3.5", 4807 - "source": { 4808 - "type": "git", 4809 - "url": "https://github.com/symfony/finder.git", 4810 - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" 4811 - }, 4812 - "dist": { 4813 - "type": "zip", 4814 - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", 4815 - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", 4816 - "shasum": "" 4817 - }, 4818 - "require": { 4819 - "php": ">=8.2" 4820 - }, 4821 - "require-dev": { 4822 - "symfony/filesystem": "^6.4|^7.0" 4823 - }, 4824 - "type": "library", 4825 - "autoload": { 4826 - "psr-4": { 4827 - "Symfony\\Component\\Finder\\": "" 4828 - }, 4829 - "exclude-from-classmap": [ 4830 - "/Tests/" 4831 - ] 4832 - }, 4833 - "notification-url": "https://packagist.org/downloads/", 4834 - "license": [ 4835 - "MIT" 4836 - ], 4837 - "authors": [ 4838 - { 4839 - "name": "Fabien Potencier", 4840 - "email": "fabien@symfony.com" 4841 - }, 4842 - { 4843 - "name": "Symfony Community", 4844 - "homepage": "https://symfony.com/contributors" 4845 - } 4846 - ], 4847 - "description": "Finds files and directories via an intuitive fluent interface", 4848 - "homepage": "https://symfony.com", 4849 - "support": { 4850 - "source": "https://github.com/symfony/finder/tree/v7.3.5" 4851 - }, 4852 - "funding": [ 4853 - { 4854 - "url": "https://symfony.com/sponsor", 4855 - "type": "custom" 4856 - }, 4857 - { 4858 - "url": "https://github.com/fabpot", 4859 - "type": "github" 4860 - }, 4861 - { 4862 - "url": "https://github.com/nicolas-grekas", 4863 - "type": "github" 4864 - }, 4865 - { 4866 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4867 - "type": "tidelift" 4868 - } 4869 - ], 4870 - "time": "2025-10-15T18:45:57+00:00" 4871 - }, 4872 - { 4873 - "name": "symfony/http-foundation", 4874 - "version": "v7.3.5", 4875 - "source": { 4876 - "type": "git", 4877 - "url": "https://github.com/symfony/http-foundation.git", 4878 - "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897" 4879 - }, 4880 - "dist": { 4881 - "type": "zip", 4882 - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce31218c7cac92eab280762c4375fb70a6f4f897", 4883 - "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897", 4884 - "shasum": "" 4885 - }, 4886 - "require": { 4887 - "php": ">=8.2", 4888 - "symfony/deprecation-contracts": "^2.5|^3.0", 4889 - "symfony/polyfill-mbstring": "~1.1", 4890 - "symfony/polyfill-php83": "^1.27" 4891 - }, 4892 - "conflict": { 4893 - "doctrine/dbal": "<3.6", 4894 - "symfony/cache": "<6.4.12|>=7.0,<7.1.5" 4895 - }, 4896 - "require-dev": { 4897 - "doctrine/dbal": "^3.6|^4", 4898 - "predis/predis": "^1.1|^2.0", 4899 - "symfony/cache": "^6.4.12|^7.1.5", 4900 - "symfony/clock": "^6.4|^7.0", 4901 - "symfony/dependency-injection": "^6.4|^7.0", 4902 - "symfony/expression-language": "^6.4|^7.0", 4903 - "symfony/http-kernel": "^6.4|^7.0", 4904 - "symfony/mime": "^6.4|^7.0", 4905 - "symfony/rate-limiter": "^6.4|^7.0" 4906 - }, 4907 - "type": "library", 4908 - "autoload": { 4909 - "psr-4": { 4910 - "Symfony\\Component\\HttpFoundation\\": "" 4911 - }, 4912 - "exclude-from-classmap": [ 4913 - "/Tests/" 4914 - ] 4915 - }, 4916 - "notification-url": "https://packagist.org/downloads/", 4917 - "license": [ 4918 - "MIT" 4919 - ], 4920 - "authors": [ 4921 - { 4922 - "name": "Fabien Potencier", 4923 - "email": "fabien@symfony.com" 4924 - }, 4925 - { 4926 - "name": "Symfony Community", 4927 - "homepage": "https://symfony.com/contributors" 4928 - } 4929 - ], 4930 - "description": "Defines an object-oriented layer for the HTTP specification", 4931 - "homepage": "https://symfony.com", 4932 - "support": { 4933 - "source": "https://github.com/symfony/http-foundation/tree/v7.3.5" 4934 - }, 4935 - "funding": [ 4936 - { 4937 - "url": "https://symfony.com/sponsor", 4938 - "type": "custom" 4939 - }, 4940 - { 4941 - "url": "https://github.com/fabpot", 4942 - "type": "github" 4943 - }, 4944 - { 4945 - "url": "https://github.com/nicolas-grekas", 4946 - "type": "github" 4947 - }, 4948 - { 4949 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 4950 - "type": "tidelift" 4951 - } 4952 - ], 4953 - "time": "2025-10-24T21:42:11+00:00" 4954 - }, 4955 - { 4956 - "name": "symfony/http-kernel", 4957 - "version": "v7.3.5", 4958 - "source": { 4959 - "type": "git", 4960 - "url": "https://github.com/symfony/http-kernel.git", 4961 - "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab" 4962 - }, 4963 - "dist": { 4964 - "type": "zip", 4965 - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/24fd3f123532e26025f49f1abefcc01a69ef15ab", 4966 - "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab", 4967 - "shasum": "" 4968 - }, 4969 - "require": { 4970 - "php": ">=8.2", 4971 - "psr/log": "^1|^2|^3", 4972 - "symfony/deprecation-contracts": "^2.5|^3", 4973 - "symfony/error-handler": "^6.4|^7.0", 4974 - "symfony/event-dispatcher": "^7.3", 4975 - "symfony/http-foundation": "^7.3", 4976 - "symfony/polyfill-ctype": "^1.8" 4977 - }, 4978 - "conflict": { 4979 - "symfony/browser-kit": "<6.4", 4980 - "symfony/cache": "<6.4", 4981 - "symfony/config": "<6.4", 4982 - "symfony/console": "<6.4", 4983 - "symfony/dependency-injection": "<6.4", 4984 - "symfony/doctrine-bridge": "<6.4", 4985 - "symfony/form": "<6.4", 4986 - "symfony/http-client": "<6.4", 4987 - "symfony/http-client-contracts": "<2.5", 4988 - "symfony/mailer": "<6.4", 4989 - "symfony/messenger": "<6.4", 4990 - "symfony/translation": "<6.4", 4991 - "symfony/translation-contracts": "<2.5", 4992 - "symfony/twig-bridge": "<6.4", 4993 - "symfony/validator": "<6.4", 4994 - "symfony/var-dumper": "<6.4", 4995 - "twig/twig": "<3.12" 4996 - }, 4997 - "provide": { 4998 - "psr/log-implementation": "1.0|2.0|3.0" 4999 - }, 5000 - "require-dev": { 5001 - "psr/cache": "^1.0|^2.0|^3.0", 5002 - "symfony/browser-kit": "^6.4|^7.0", 5003 - "symfony/clock": "^6.4|^7.0", 5004 - "symfony/config": "^6.4|^7.0", 5005 - "symfony/console": "^6.4|^7.0", 5006 - "symfony/css-selector": "^6.4|^7.0", 5007 - "symfony/dependency-injection": "^6.4|^7.0", 5008 - "symfony/dom-crawler": "^6.4|^7.0", 5009 - "symfony/expression-language": "^6.4|^7.0", 5010 - "symfony/finder": "^6.4|^7.0", 5011 - "symfony/http-client-contracts": "^2.5|^3", 5012 - "symfony/process": "^6.4|^7.0", 5013 - "symfony/property-access": "^7.1", 5014 - "symfony/routing": "^6.4|^7.0", 5015 - "symfony/serializer": "^7.1", 5016 - "symfony/stopwatch": "^6.4|^7.0", 5017 - "symfony/translation": "^6.4|^7.0", 5018 - "symfony/translation-contracts": "^2.5|^3", 5019 - "symfony/uid": "^6.4|^7.0", 5020 - "symfony/validator": "^6.4|^7.0", 5021 - "symfony/var-dumper": "^6.4|^7.0", 5022 - "symfony/var-exporter": "^6.4|^7.0", 5023 - "twig/twig": "^3.12" 5024 - }, 5025 - "type": "library", 5026 - "autoload": { 5027 - "psr-4": { 5028 - "Symfony\\Component\\HttpKernel\\": "" 5029 - }, 5030 - "exclude-from-classmap": [ 5031 - "/Tests/" 5032 - ] 5033 - }, 5034 - "notification-url": "https://packagist.org/downloads/", 5035 - "license": [ 5036 - "MIT" 5037 - ], 5038 - "authors": [ 5039 - { 5040 - "name": "Fabien Potencier", 5041 - "email": "fabien@symfony.com" 5042 - }, 5043 - { 5044 - "name": "Symfony Community", 5045 - "homepage": "https://symfony.com/contributors" 5046 - } 5047 - ], 5048 - "description": "Provides a structured process for converting a Request into a Response", 5049 - "homepage": "https://symfony.com", 5050 - "support": { 5051 - "source": "https://github.com/symfony/http-kernel/tree/v7.3.5" 5052 - }, 5053 - "funding": [ 5054 - { 5055 - "url": "https://symfony.com/sponsor", 5056 - "type": "custom" 5057 - }, 5058 - { 5059 - "url": "https://github.com/fabpot", 5060 - "type": "github" 5061 - }, 5062 - { 5063 - "url": "https://github.com/nicolas-grekas", 5064 - "type": "github" 5065 - }, 5066 - { 5067 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5068 - "type": "tidelift" 5069 - } 5070 - ], 5071 - "time": "2025-10-28T10:19:01+00:00" 5072 - }, 5073 - { 5074 - "name": "symfony/mailer", 5075 - "version": "v7.3.5", 5076 - "source": { 5077 - "type": "git", 5078 - "url": "https://github.com/symfony/mailer.git", 5079 - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba" 5080 - }, 5081 - "dist": { 5082 - "type": "zip", 5083 - "url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba", 5084 - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba", 5085 - "shasum": "" 5086 - }, 5087 - "require": { 5088 - "egulias/email-validator": "^2.1.10|^3|^4", 5089 - "php": ">=8.2", 5090 - "psr/event-dispatcher": "^1", 5091 - "psr/log": "^1|^2|^3", 5092 - "symfony/event-dispatcher": "^6.4|^7.0", 5093 - "symfony/mime": "^7.2", 5094 - "symfony/service-contracts": "^2.5|^3" 5095 - }, 5096 - "conflict": { 5097 - "symfony/http-client-contracts": "<2.5", 5098 - "symfony/http-kernel": "<6.4", 5099 - "symfony/messenger": "<6.4", 5100 - "symfony/mime": "<6.4", 5101 - "symfony/twig-bridge": "<6.4" 5102 - }, 5103 - "require-dev": { 5104 - "symfony/console": "^6.4|^7.0", 5105 - "symfony/http-client": "^6.4|^7.0", 5106 - "symfony/messenger": "^6.4|^7.0", 5107 - "symfony/twig-bridge": "^6.4|^7.0" 5108 - }, 5109 - "type": "library", 5110 - "autoload": { 5111 - "psr-4": { 5112 - "Symfony\\Component\\Mailer\\": "" 5113 - }, 5114 - "exclude-from-classmap": [ 5115 - "/Tests/" 5116 - ] 5117 - }, 5118 - "notification-url": "https://packagist.org/downloads/", 5119 - "license": [ 5120 - "MIT" 5121 - ], 5122 - "authors": [ 5123 - { 5124 - "name": "Fabien Potencier", 5125 - "email": "fabien@symfony.com" 5126 - }, 5127 - { 5128 - "name": "Symfony Community", 5129 - "homepage": "https://symfony.com/contributors" 5130 - } 5131 - ], 5132 - "description": "Helps sending emails", 5133 - "homepage": "https://symfony.com", 5134 - "support": { 5135 - "source": "https://github.com/symfony/mailer/tree/v7.3.5" 5136 - }, 5137 - "funding": [ 5138 - { 5139 - "url": "https://symfony.com/sponsor", 5140 - "type": "custom" 5141 - }, 5142 - { 5143 - "url": "https://github.com/fabpot", 5144 - "type": "github" 5145 - }, 5146 - { 5147 - "url": "https://github.com/nicolas-grekas", 5148 - "type": "github" 5149 - }, 5150 - { 5151 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5152 - "type": "tidelift" 5153 - } 5154 - ], 5155 - "time": "2025-10-24T14:27:20+00:00" 5156 - }, 5157 - { 5158 - "name": "symfony/mime", 5159 - "version": "v7.3.4", 5160 - "source": { 5161 - "type": "git", 5162 - "url": "https://github.com/symfony/mime.git", 5163 - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" 5164 - }, 5165 - "dist": { 5166 - "type": "zip", 5167 - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", 5168 - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", 5169 - "shasum": "" 5170 - }, 5171 - "require": { 5172 - "php": ">=8.2", 5173 - "symfony/polyfill-intl-idn": "^1.10", 5174 - "symfony/polyfill-mbstring": "^1.0" 5175 - }, 5176 - "conflict": { 5177 - "egulias/email-validator": "~3.0.0", 5178 - "phpdocumentor/reflection-docblock": "<3.2.2", 5179 - "phpdocumentor/type-resolver": "<1.4.0", 5180 - "symfony/mailer": "<6.4", 5181 - "symfony/serializer": "<6.4.3|>7.0,<7.0.3" 5182 - }, 5183 - "require-dev": { 5184 - "egulias/email-validator": "^2.1.10|^3.1|^4", 5185 - "league/html-to-markdown": "^5.0", 5186 - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", 5187 - "symfony/dependency-injection": "^6.4|^7.0", 5188 - "symfony/process": "^6.4|^7.0", 5189 - "symfony/property-access": "^6.4|^7.0", 5190 - "symfony/property-info": "^6.4|^7.0", 5191 - "symfony/serializer": "^6.4.3|^7.0.3" 5192 - }, 5193 - "type": "library", 5194 - "autoload": { 5195 - "psr-4": { 5196 - "Symfony\\Component\\Mime\\": "" 5197 - }, 5198 - "exclude-from-classmap": [ 5199 - "/Tests/" 5200 - ] 5201 - }, 5202 - "notification-url": "https://packagist.org/downloads/", 5203 - "license": [ 5204 - "MIT" 5205 - ], 5206 - "authors": [ 5207 - { 5208 - "name": "Fabien Potencier", 5209 - "email": "fabien@symfony.com" 5210 - }, 5211 - { 5212 - "name": "Symfony Community", 5213 - "homepage": "https://symfony.com/contributors" 5214 - } 5215 - ], 5216 - "description": "Allows manipulating MIME messages", 5217 - "homepage": "https://symfony.com", 5218 - "keywords": [ 5219 - "mime", 5220 - "mime-type" 5221 - ], 5222 - "support": { 5223 - "source": "https://github.com/symfony/mime/tree/v7.3.4" 5224 - }, 5225 - "funding": [ 5226 - { 5227 - "url": "https://symfony.com/sponsor", 5228 - "type": "custom" 5229 - }, 5230 - { 5231 - "url": "https://github.com/fabpot", 5232 - "type": "github" 5233 - }, 5234 - { 5235 - "url": "https://github.com/nicolas-grekas", 5236 - "type": "github" 5237 - }, 5238 - { 5239 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5240 - "type": "tidelift" 5241 - } 5242 - ], 5243 - "time": "2025-09-16T08:38:17+00:00" 5244 - }, 5245 - { 5246 - "name": "symfony/polyfill-ctype", 5247 - "version": "v1.33.0", 5248 - "source": { 5249 - "type": "git", 5250 - "url": "https://github.com/symfony/polyfill-ctype.git", 5251 - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" 5252 - }, 5253 - "dist": { 5254 - "type": "zip", 5255 - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", 5256 - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", 5257 - "shasum": "" 5258 - }, 5259 - "require": { 5260 - "php": ">=7.2" 5261 - }, 5262 - "provide": { 5263 - "ext-ctype": "*" 5264 - }, 5265 - "suggest": { 5266 - "ext-ctype": "For best performance" 5267 - }, 5268 - "type": "library", 5269 - "extra": { 5270 - "thanks": { 5271 - "url": "https://github.com/symfony/polyfill", 5272 - "name": "symfony/polyfill" 5273 - } 5274 - }, 5275 - "autoload": { 5276 - "files": [ 5277 - "bootstrap.php" 5278 - ], 5279 - "psr-4": { 5280 - "Symfony\\Polyfill\\Ctype\\": "" 5281 - } 5282 - }, 5283 - "notification-url": "https://packagist.org/downloads/", 5284 - "license": [ 5285 - "MIT" 5286 - ], 5287 - "authors": [ 5288 - { 5289 - "name": "Gert de Pagter", 5290 - "email": "BackEndTea@gmail.com" 5291 - }, 5292 - { 5293 - "name": "Symfony Community", 5294 - "homepage": "https://symfony.com/contributors" 5295 - } 5296 - ], 5297 - "description": "Symfony polyfill for ctype functions", 5298 - "homepage": "https://symfony.com", 5299 - "keywords": [ 5300 - "compatibility", 5301 - "ctype", 5302 - "polyfill", 5303 - "portable" 5304 - ], 5305 - "support": { 5306 - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" 5307 - }, 5308 - "funding": [ 5309 - { 5310 - "url": "https://symfony.com/sponsor", 5311 - "type": "custom" 5312 - }, 5313 - { 5314 - "url": "https://github.com/fabpot", 5315 - "type": "github" 5316 - }, 5317 - { 5318 - "url": "https://github.com/nicolas-grekas", 5319 - "type": "github" 5320 - }, 5321 - { 5322 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5323 - "type": "tidelift" 5324 - } 5325 - ], 5326 - "time": "2024-09-09T11:45:10+00:00" 5327 - }, 5328 - { 5329 - "name": "symfony/polyfill-intl-grapheme", 5330 - "version": "v1.33.0", 5331 - "source": { 5332 - "type": "git", 5333 - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", 5334 - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" 5335 - }, 5336 - "dist": { 5337 - "type": "zip", 5338 - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", 5339 - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", 5340 - "shasum": "" 5341 - }, 5342 - "require": { 5343 - "php": ">=7.2" 5344 - }, 5345 - "suggest": { 5346 - "ext-intl": "For best performance" 5347 - }, 5348 - "type": "library", 5349 - "extra": { 5350 - "thanks": { 5351 - "url": "https://github.com/symfony/polyfill", 5352 - "name": "symfony/polyfill" 5353 - } 5354 - }, 5355 - "autoload": { 5356 - "files": [ 5357 - "bootstrap.php" 5358 - ], 5359 - "psr-4": { 5360 - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" 5361 - } 5362 - }, 5363 - "notification-url": "https://packagist.org/downloads/", 5364 - "license": [ 5365 - "MIT" 5366 - ], 5367 - "authors": [ 5368 - { 5369 - "name": "Nicolas Grekas", 5370 - "email": "p@tchwork.com" 5371 - }, 5372 - { 5373 - "name": "Symfony Community", 5374 - "homepage": "https://symfony.com/contributors" 5375 - } 5376 - ], 5377 - "description": "Symfony polyfill for intl's grapheme_* functions", 5378 - "homepage": "https://symfony.com", 5379 - "keywords": [ 5380 - "compatibility", 5381 - "grapheme", 5382 - "intl", 5383 - "polyfill", 5384 - "portable", 5385 - "shim" 5386 - ], 5387 - "support": { 5388 - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" 5389 - }, 5390 - "funding": [ 5391 - { 5392 - "url": "https://symfony.com/sponsor", 5393 - "type": "custom" 5394 - }, 5395 - { 5396 - "url": "https://github.com/fabpot", 5397 - "type": "github" 5398 - }, 5399 - { 5400 - "url": "https://github.com/nicolas-grekas", 5401 - "type": "github" 5402 - }, 5403 - { 5404 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5405 - "type": "tidelift" 5406 - } 5407 - ], 5408 - "time": "2025-06-27T09:58:17+00:00" 5409 - }, 5410 - { 5411 - "name": "symfony/polyfill-intl-idn", 5412 - "version": "v1.33.0", 5413 - "source": { 5414 - "type": "git", 5415 - "url": "https://github.com/symfony/polyfill-intl-idn.git", 5416 - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" 5417 - }, 5418 - "dist": { 5419 - "type": "zip", 5420 - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", 5421 - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", 5422 - "shasum": "" 5423 - }, 5424 - "require": { 5425 - "php": ">=7.2", 5426 - "symfony/polyfill-intl-normalizer": "^1.10" 5427 - }, 5428 - "suggest": { 5429 - "ext-intl": "For best performance" 5430 - }, 5431 - "type": "library", 5432 - "extra": { 5433 - "thanks": { 5434 - "url": "https://github.com/symfony/polyfill", 5435 - "name": "symfony/polyfill" 5436 - } 5437 - }, 5438 - "autoload": { 5439 - "files": [ 5440 - "bootstrap.php" 5441 - ], 5442 - "psr-4": { 5443 - "Symfony\\Polyfill\\Intl\\Idn\\": "" 5444 - } 5445 - }, 5446 - "notification-url": "https://packagist.org/downloads/", 5447 - "license": [ 5448 - "MIT" 5449 - ], 5450 - "authors": [ 5451 - { 5452 - "name": "Laurent Bassin", 5453 - "email": "laurent@bassin.info" 5454 - }, 5455 - { 5456 - "name": "Trevor Rowbotham", 5457 - "email": "trevor.rowbotham@pm.me" 5458 - }, 5459 - { 5460 - "name": "Symfony Community", 5461 - "homepage": "https://symfony.com/contributors" 5462 - } 5463 - ], 5464 - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", 5465 - "homepage": "https://symfony.com", 5466 - "keywords": [ 5467 - "compatibility", 5468 - "idn", 5469 - "intl", 5470 - "polyfill", 5471 - "portable", 5472 - "shim" 5473 - ], 5474 - "support": { 5475 - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" 5476 - }, 5477 - "funding": [ 5478 - { 5479 - "url": "https://symfony.com/sponsor", 5480 - "type": "custom" 5481 - }, 5482 - { 5483 - "url": "https://github.com/fabpot", 5484 - "type": "github" 5485 - }, 5486 - { 5487 - "url": "https://github.com/nicolas-grekas", 5488 - "type": "github" 5489 - }, 5490 - { 5491 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5492 - "type": "tidelift" 5493 - } 5494 - ], 5495 - "time": "2024-09-10T14:38:51+00:00" 5496 - }, 5497 - { 5498 - "name": "symfony/polyfill-intl-normalizer", 5499 - "version": "v1.33.0", 5500 - "source": { 5501 - "type": "git", 5502 - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", 5503 - "reference": "3833d7255cc303546435cb650316bff708a1c75c" 5504 - }, 5505 - "dist": { 5506 - "type": "zip", 5507 - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", 5508 - "reference": "3833d7255cc303546435cb650316bff708a1c75c", 5509 - "shasum": "" 5510 - }, 5511 - "require": { 5512 - "php": ">=7.2" 5513 - }, 5514 - "suggest": { 5515 - "ext-intl": "For best performance" 5516 - }, 5517 - "type": "library", 5518 - "extra": { 5519 - "thanks": { 5520 - "url": "https://github.com/symfony/polyfill", 5521 - "name": "symfony/polyfill" 5522 - } 5523 - }, 5524 - "autoload": { 5525 - "files": [ 5526 - "bootstrap.php" 5527 - ], 5528 - "psr-4": { 5529 - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" 5530 - }, 5531 - "classmap": [ 5532 - "Resources/stubs" 5533 - ] 5534 - }, 5535 - "notification-url": "https://packagist.org/downloads/", 5536 - "license": [ 5537 - "MIT" 5538 - ], 5539 - "authors": [ 5540 - { 5541 - "name": "Nicolas Grekas", 5542 - "email": "p@tchwork.com" 5543 - }, 5544 - { 5545 - "name": "Symfony Community", 5546 - "homepage": "https://symfony.com/contributors" 5547 - } 5548 - ], 5549 - "description": "Symfony polyfill for intl's Normalizer class and related functions", 5550 - "homepage": "https://symfony.com", 5551 - "keywords": [ 5552 - "compatibility", 5553 - "intl", 5554 - "normalizer", 5555 - "polyfill", 5556 - "portable", 5557 - "shim" 5558 - ], 5559 - "support": { 5560 - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" 5561 - }, 5562 - "funding": [ 5563 - { 5564 - "url": "https://symfony.com/sponsor", 5565 - "type": "custom" 5566 - }, 5567 - { 5568 - "url": "https://github.com/fabpot", 5569 - "type": "github" 5570 - }, 5571 - { 5572 - "url": "https://github.com/nicolas-grekas", 5573 - "type": "github" 5574 - }, 5575 - { 5576 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5577 - "type": "tidelift" 5578 - } 5579 - ], 5580 - "time": "2024-09-09T11:45:10+00:00" 5581 - }, 5582 - { 5583 - "name": "symfony/polyfill-mbstring", 5584 - "version": "v1.33.0", 5585 - "source": { 5586 - "type": "git", 5587 - "url": "https://github.com/symfony/polyfill-mbstring.git", 5588 - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" 5589 - }, 5590 - "dist": { 5591 - "type": "zip", 5592 - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", 5593 - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", 5594 - "shasum": "" 5595 - }, 5596 - "require": { 5597 - "ext-iconv": "*", 5598 - "php": ">=7.2" 5599 - }, 5600 - "provide": { 5601 - "ext-mbstring": "*" 5602 - }, 5603 - "suggest": { 5604 - "ext-mbstring": "For best performance" 5605 - }, 5606 - "type": "library", 5607 - "extra": { 5608 - "thanks": { 5609 - "url": "https://github.com/symfony/polyfill", 5610 - "name": "symfony/polyfill" 5611 - } 5612 - }, 5613 - "autoload": { 5614 - "files": [ 5615 - "bootstrap.php" 5616 - ], 5617 - "psr-4": { 5618 - "Symfony\\Polyfill\\Mbstring\\": "" 5619 - } 5620 - }, 5621 - "notification-url": "https://packagist.org/downloads/", 5622 - "license": [ 5623 - "MIT" 5624 - ], 5625 - "authors": [ 5626 - { 5627 - "name": "Nicolas Grekas", 5628 - "email": "p@tchwork.com" 5629 - }, 5630 - { 5631 - "name": "Symfony Community", 5632 - "homepage": "https://symfony.com/contributors" 5633 - } 5634 - ], 5635 - "description": "Symfony polyfill for the Mbstring extension", 5636 - "homepage": "https://symfony.com", 5637 - "keywords": [ 5638 - "compatibility", 5639 - "mbstring", 5640 - "polyfill", 5641 - "portable", 5642 - "shim" 5643 - ], 5644 - "support": { 5645 - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" 5646 - }, 5647 - "funding": [ 5648 - { 5649 - "url": "https://symfony.com/sponsor", 5650 - "type": "custom" 5651 - }, 5652 - { 5653 - "url": "https://github.com/fabpot", 5654 - "type": "github" 5655 - }, 5656 - { 5657 - "url": "https://github.com/nicolas-grekas", 5658 - "type": "github" 5659 - }, 5660 - { 5661 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5662 - "type": "tidelift" 5663 - } 5664 - ], 5665 - "time": "2024-12-23T08:48:59+00:00" 5666 - }, 5667 - { 5668 - "name": "symfony/polyfill-php80", 5669 - "version": "v1.33.0", 5670 - "source": { 5671 - "type": "git", 5672 - "url": "https://github.com/symfony/polyfill-php80.git", 5673 - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" 5674 - }, 5675 - "dist": { 5676 - "type": "zip", 5677 - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", 5678 - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", 5679 - "shasum": "" 5680 - }, 5681 - "require": { 5682 - "php": ">=7.2" 5683 - }, 5684 - "type": "library", 5685 - "extra": { 5686 - "thanks": { 5687 - "url": "https://github.com/symfony/polyfill", 5688 - "name": "symfony/polyfill" 5689 - } 5690 - }, 5691 - "autoload": { 5692 - "files": [ 5693 - "bootstrap.php" 5694 - ], 5695 - "psr-4": { 5696 - "Symfony\\Polyfill\\Php80\\": "" 5697 - }, 5698 - "classmap": [ 5699 - "Resources/stubs" 5700 - ] 5701 - }, 5702 - "notification-url": "https://packagist.org/downloads/", 5703 - "license": [ 5704 - "MIT" 5705 - ], 5706 - "authors": [ 5707 - { 5708 - "name": "Ion Bazan", 5709 - "email": "ion.bazan@gmail.com" 5710 - }, 5711 - { 5712 - "name": "Nicolas Grekas", 5713 - "email": "p@tchwork.com" 5714 - }, 5715 - { 5716 - "name": "Symfony Community", 5717 - "homepage": "https://symfony.com/contributors" 5718 - } 5719 - ], 5720 - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", 5721 - "homepage": "https://symfony.com", 5722 - "keywords": [ 5723 - "compatibility", 5724 - "polyfill", 5725 - "portable", 5726 - "shim" 5727 - ], 5728 - "support": { 5729 - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" 5730 - }, 5731 - "funding": [ 5732 - { 5733 - "url": "https://symfony.com/sponsor", 5734 - "type": "custom" 5735 - }, 5736 - { 5737 - "url": "https://github.com/fabpot", 5738 - "type": "github" 5739 - }, 5740 - { 5741 - "url": "https://github.com/nicolas-grekas", 5742 - "type": "github" 5743 - }, 5744 - { 5745 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5746 - "type": "tidelift" 5747 - } 5748 - ], 5749 - "time": "2025-01-02T08:10:11+00:00" 5750 - }, 5751 - { 5752 - "name": "symfony/polyfill-php83", 5753 - "version": "v1.33.0", 5754 - "source": { 5755 - "type": "git", 5756 - "url": "https://github.com/symfony/polyfill-php83.git", 5757 - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" 5758 - }, 5759 - "dist": { 5760 - "type": "zip", 5761 - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", 5762 - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", 5763 - "shasum": "" 5764 - }, 5765 - "require": { 5766 - "php": ">=7.2" 5767 - }, 5768 - "type": "library", 5769 - "extra": { 5770 - "thanks": { 5771 - "url": "https://github.com/symfony/polyfill", 5772 - "name": "symfony/polyfill" 5773 - } 5774 - }, 5775 - "autoload": { 5776 - "files": [ 5777 - "bootstrap.php" 5778 - ], 5779 - "psr-4": { 5780 - "Symfony\\Polyfill\\Php83\\": "" 5781 - }, 5782 - "classmap": [ 5783 - "Resources/stubs" 5784 - ] 5785 - }, 5786 - "notification-url": "https://packagist.org/downloads/", 5787 - "license": [ 5788 - "MIT" 5789 - ], 5790 - "authors": [ 5791 - { 5792 - "name": "Nicolas Grekas", 5793 - "email": "p@tchwork.com" 5794 - }, 5795 - { 5796 - "name": "Symfony Community", 5797 - "homepage": "https://symfony.com/contributors" 5798 - } 5799 - ], 5800 - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", 5801 - "homepage": "https://symfony.com", 5802 - "keywords": [ 5803 - "compatibility", 5804 - "polyfill", 5805 - "portable", 5806 - "shim" 5807 - ], 5808 - "support": { 5809 - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" 5810 - }, 5811 - "funding": [ 5812 - { 5813 - "url": "https://symfony.com/sponsor", 5814 - "type": "custom" 5815 - }, 5816 - { 5817 - "url": "https://github.com/fabpot", 5818 - "type": "github" 5819 - }, 5820 - { 5821 - "url": "https://github.com/nicolas-grekas", 5822 - "type": "github" 5823 - }, 5824 - { 5825 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5826 - "type": "tidelift" 5827 - } 5828 - ], 5829 - "time": "2025-07-08T02:45:35+00:00" 5830 - }, 5831 - { 5832 - "name": "symfony/polyfill-uuid", 5833 - "version": "v1.33.0", 5834 - "source": { 5835 - "type": "git", 5836 - "url": "https://github.com/symfony/polyfill-uuid.git", 5837 - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" 5838 - }, 5839 - "dist": { 5840 - "type": "zip", 5841 - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", 5842 - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", 5843 - "shasum": "" 5844 - }, 5845 - "require": { 5846 - "php": ">=7.2" 5847 - }, 5848 - "provide": { 5849 - "ext-uuid": "*" 5850 - }, 5851 - "suggest": { 5852 - "ext-uuid": "For best performance" 5853 - }, 5854 - "type": "library", 5855 - "extra": { 5856 - "thanks": { 5857 - "url": "https://github.com/symfony/polyfill", 5858 - "name": "symfony/polyfill" 5859 - } 5860 - }, 5861 - "autoload": { 5862 - "files": [ 5863 - "bootstrap.php" 5864 - ], 5865 - "psr-4": { 5866 - "Symfony\\Polyfill\\Uuid\\": "" 5867 - } 5868 - }, 5869 - "notification-url": "https://packagist.org/downloads/", 5870 - "license": [ 5871 - "MIT" 5872 - ], 5873 - "authors": [ 5874 - { 5875 - "name": "Grรฉgoire Pineau", 5876 - "email": "lyrixx@lyrixx.info" 5877 - }, 5878 - { 5879 - "name": "Symfony Community", 5880 - "homepage": "https://symfony.com/contributors" 5881 - } 5882 - ], 5883 - "description": "Symfony polyfill for uuid functions", 5884 - "homepage": "https://symfony.com", 5885 - "keywords": [ 5886 - "compatibility", 5887 - "polyfill", 5888 - "portable", 5889 - "uuid" 5890 - ], 5891 - "support": { 5892 - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" 5893 - }, 5894 - "funding": [ 5895 - { 5896 - "url": "https://symfony.com/sponsor", 5897 - "type": "custom" 5898 - }, 5899 - { 5900 - "url": "https://github.com/fabpot", 5901 - "type": "github" 5902 - }, 5903 - { 5904 - "url": "https://github.com/nicolas-grekas", 5905 - "type": "github" 5906 - }, 5907 - { 5908 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5909 - "type": "tidelift" 5910 - } 5911 - ], 5912 - "time": "2024-09-09T11:45:10+00:00" 5913 - }, 5914 - { 5915 - "name": "symfony/process", 5916 - "version": "v7.3.4", 5917 - "source": { 5918 - "type": "git", 5919 - "url": "https://github.com/symfony/process.git", 5920 - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" 5921 - }, 5922 - "dist": { 5923 - "type": "zip", 5924 - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", 5925 - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", 5926 - "shasum": "" 5927 - }, 5928 - "require": { 5929 - "php": ">=8.2" 5930 - }, 5931 - "type": "library", 5932 - "autoload": { 5933 - "psr-4": { 5934 - "Symfony\\Component\\Process\\": "" 5935 - }, 5936 - "exclude-from-classmap": [ 5937 - "/Tests/" 5938 - ] 5939 - }, 5940 - "notification-url": "https://packagist.org/downloads/", 5941 - "license": [ 5942 - "MIT" 5943 - ], 5944 - "authors": [ 5945 - { 5946 - "name": "Fabien Potencier", 5947 - "email": "fabien@symfony.com" 5948 - }, 5949 - { 5950 - "name": "Symfony Community", 5951 - "homepage": "https://symfony.com/contributors" 5952 - } 5953 - ], 5954 - "description": "Executes commands in sub-processes", 5955 - "homepage": "https://symfony.com", 5956 - "support": { 5957 - "source": "https://github.com/symfony/process/tree/v7.3.4" 5958 - }, 5959 - "funding": [ 5960 - { 5961 - "url": "https://symfony.com/sponsor", 5962 - "type": "custom" 5963 - }, 5964 - { 5965 - "url": "https://github.com/fabpot", 5966 - "type": "github" 5967 - }, 5968 - { 5969 - "url": "https://github.com/nicolas-grekas", 5970 - "type": "github" 5971 - }, 5972 - { 5973 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 5974 - "type": "tidelift" 5975 - } 5976 - ], 5977 - "time": "2025-09-11T10:12:26+00:00" 5978 - }, 5979 - { 5980 - "name": "symfony/routing", 5981 - "version": "v7.3.4", 5982 - "source": { 5983 - "type": "git", 5984 - "url": "https://github.com/symfony/routing.git", 5985 - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c" 5986 - }, 5987 - "dist": { 5988 - "type": "zip", 5989 - "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c", 5990 - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c", 5991 - "shasum": "" 5992 - }, 5993 - "require": { 5994 - "php": ">=8.2", 5995 - "symfony/deprecation-contracts": "^2.5|^3" 5996 - }, 5997 - "conflict": { 5998 - "symfony/config": "<6.4", 5999 - "symfony/dependency-injection": "<6.4", 6000 - "symfony/yaml": "<6.4" 6001 - }, 6002 - "require-dev": { 6003 - "psr/log": "^1|^2|^3", 6004 - "symfony/config": "^6.4|^7.0", 6005 - "symfony/dependency-injection": "^6.4|^7.0", 6006 - "symfony/expression-language": "^6.4|^7.0", 6007 - "symfony/http-foundation": "^6.4|^7.0", 6008 - "symfony/yaml": "^6.4|^7.0" 6009 - }, 6010 - "type": "library", 6011 - "autoload": { 6012 - "psr-4": { 6013 - "Symfony\\Component\\Routing\\": "" 6014 - }, 6015 - "exclude-from-classmap": [ 6016 - "/Tests/" 6017 - ] 6018 - }, 6019 - "notification-url": "https://packagist.org/downloads/", 6020 - "license": [ 6021 - "MIT" 6022 - ], 6023 - "authors": [ 6024 - { 6025 - "name": "Fabien Potencier", 6026 - "email": "fabien@symfony.com" 6027 - }, 6028 - { 6029 - "name": "Symfony Community", 6030 - "homepage": "https://symfony.com/contributors" 6031 - } 6032 - ], 6033 - "description": "Maps an HTTP request to a set of configuration variables", 6034 - "homepage": "https://symfony.com", 6035 - "keywords": [ 6036 - "router", 6037 - "routing", 6038 - "uri", 6039 - "url" 6040 - ], 6041 - "support": { 6042 - "source": "https://github.com/symfony/routing/tree/v7.3.4" 6043 - }, 6044 - "funding": [ 6045 - { 6046 - "url": "https://symfony.com/sponsor", 6047 - "type": "custom" 6048 - }, 6049 - { 6050 - "url": "https://github.com/fabpot", 6051 - "type": "github" 6052 - }, 6053 - { 6054 - "url": "https://github.com/nicolas-grekas", 6055 - "type": "github" 6056 - }, 6057 - { 6058 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6059 - "type": "tidelift" 6060 - } 6061 - ], 6062 - "time": "2025-09-11T10:12:26+00:00" 6063 - }, 6064 - { 6065 - "name": "symfony/service-contracts", 6066 - "version": "v3.6.0", 6067 - "source": { 6068 - "type": "git", 6069 - "url": "https://github.com/symfony/service-contracts.git", 6070 - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" 6071 - }, 6072 - "dist": { 6073 - "type": "zip", 6074 - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", 6075 - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", 6076 - "shasum": "" 6077 - }, 6078 - "require": { 6079 - "php": ">=8.1", 6080 - "psr/container": "^1.1|^2.0", 6081 - "symfony/deprecation-contracts": "^2.5|^3" 6082 - }, 6083 - "conflict": { 6084 - "ext-psr": "<1.1|>=2" 6085 - }, 6086 - "type": "library", 6087 - "extra": { 6088 - "thanks": { 6089 - "url": "https://github.com/symfony/contracts", 6090 - "name": "symfony/contracts" 6091 - }, 6092 - "branch-alias": { 6093 - "dev-main": "3.6-dev" 6094 - } 6095 - }, 6096 - "autoload": { 6097 - "psr-4": { 6098 - "Symfony\\Contracts\\Service\\": "" 6099 - }, 6100 - "exclude-from-classmap": [ 6101 - "/Test/" 6102 - ] 6103 - }, 6104 - "notification-url": "https://packagist.org/downloads/", 6105 - "license": [ 6106 - "MIT" 6107 - ], 6108 - "authors": [ 6109 - { 6110 - "name": "Nicolas Grekas", 6111 - "email": "p@tchwork.com" 6112 - }, 6113 - { 6114 - "name": "Symfony Community", 6115 - "homepage": "https://symfony.com/contributors" 6116 - } 6117 - ], 6118 - "description": "Generic abstractions related to writing services", 6119 - "homepage": "https://symfony.com", 6120 - "keywords": [ 6121 - "abstractions", 6122 - "contracts", 6123 - "decoupling", 6124 - "interfaces", 6125 - "interoperability", 6126 - "standards" 6127 - ], 6128 - "support": { 6129 - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" 6130 - }, 6131 - "funding": [ 6132 - { 6133 - "url": "https://symfony.com/sponsor", 6134 - "type": "custom" 6135 - }, 6136 - { 6137 - "url": "https://github.com/fabpot", 6138 - "type": "github" 6139 - }, 6140 - { 6141 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6142 - "type": "tidelift" 6143 - } 6144 - ], 6145 - "time": "2025-04-25T09:37:31+00:00" 6146 - }, 6147 - { 6148 - "name": "symfony/string", 6149 - "version": "v7.3.4", 6150 - "source": { 6151 - "type": "git", 6152 - "url": "https://github.com/symfony/string.git", 6153 - "reference": "f96476035142921000338bad71e5247fbc138872" 6154 - }, 6155 - "dist": { 6156 - "type": "zip", 6157 - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", 6158 - "reference": "f96476035142921000338bad71e5247fbc138872", 6159 - "shasum": "" 6160 - }, 6161 - "require": { 6162 - "php": ">=8.2", 6163 - "symfony/polyfill-ctype": "~1.8", 6164 - "symfony/polyfill-intl-grapheme": "~1.0", 6165 - "symfony/polyfill-intl-normalizer": "~1.0", 6166 - "symfony/polyfill-mbstring": "~1.0" 6167 - }, 6168 - "conflict": { 6169 - "symfony/translation-contracts": "<2.5" 6170 - }, 6171 - "require-dev": { 6172 - "symfony/emoji": "^7.1", 6173 - "symfony/http-client": "^6.4|^7.0", 6174 - "symfony/intl": "^6.4|^7.0", 6175 - "symfony/translation-contracts": "^2.5|^3.0", 6176 - "symfony/var-exporter": "^6.4|^7.0" 6177 - }, 6178 - "type": "library", 6179 - "autoload": { 6180 - "files": [ 6181 - "Resources/functions.php" 6182 - ], 6183 - "psr-4": { 6184 - "Symfony\\Component\\String\\": "" 6185 - }, 6186 - "exclude-from-classmap": [ 6187 - "/Tests/" 6188 - ] 6189 - }, 6190 - "notification-url": "https://packagist.org/downloads/", 6191 - "license": [ 6192 - "MIT" 6193 - ], 6194 - "authors": [ 6195 - { 6196 - "name": "Nicolas Grekas", 6197 - "email": "p@tchwork.com" 6198 - }, 6199 - { 6200 - "name": "Symfony Community", 6201 - "homepage": "https://symfony.com/contributors" 6202 - } 6203 - ], 6204 - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", 6205 - "homepage": "https://symfony.com", 6206 - "keywords": [ 6207 - "grapheme", 6208 - "i18n", 6209 - "string", 6210 - "unicode", 6211 - "utf-8", 6212 - "utf8" 6213 - ], 6214 - "support": { 6215 - "source": "https://github.com/symfony/string/tree/v7.3.4" 6216 - }, 6217 - "funding": [ 6218 - { 6219 - "url": "https://symfony.com/sponsor", 6220 - "type": "custom" 6221 - }, 6222 - { 6223 - "url": "https://github.com/fabpot", 6224 - "type": "github" 6225 - }, 6226 - { 6227 - "url": "https://github.com/nicolas-grekas", 6228 - "type": "github" 6229 - }, 6230 - { 6231 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6232 - "type": "tidelift" 6233 - } 6234 - ], 6235 - "time": "2025-09-11T14:36:48+00:00" 6236 - }, 6237 - { 6238 - "name": "symfony/translation", 6239 - "version": "v7.3.4", 6240 - "source": { 6241 - "type": "git", 6242 - "url": "https://github.com/symfony/translation.git", 6243 - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" 6244 - }, 6245 - "dist": { 6246 - "type": "zip", 6247 - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", 6248 - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", 6249 - "shasum": "" 6250 - }, 6251 - "require": { 6252 - "php": ">=8.2", 6253 - "symfony/deprecation-contracts": "^2.5|^3", 6254 - "symfony/polyfill-mbstring": "~1.0", 6255 - "symfony/translation-contracts": "^2.5|^3.0" 6256 - }, 6257 - "conflict": { 6258 - "nikic/php-parser": "<5.0", 6259 - "symfony/config": "<6.4", 6260 - "symfony/console": "<6.4", 6261 - "symfony/dependency-injection": "<6.4", 6262 - "symfony/http-client-contracts": "<2.5", 6263 - "symfony/http-kernel": "<6.4", 6264 - "symfony/service-contracts": "<2.5", 6265 - "symfony/twig-bundle": "<6.4", 6266 - "symfony/yaml": "<6.4" 6267 - }, 6268 - "provide": { 6269 - "symfony/translation-implementation": "2.3|3.0" 6270 - }, 6271 - "require-dev": { 6272 - "nikic/php-parser": "^5.0", 6273 - "psr/log": "^1|^2|^3", 6274 - "symfony/config": "^6.4|^7.0", 6275 - "symfony/console": "^6.4|^7.0", 6276 - "symfony/dependency-injection": "^6.4|^7.0", 6277 - "symfony/finder": "^6.4|^7.0", 6278 - "symfony/http-client-contracts": "^2.5|^3.0", 6279 - "symfony/http-kernel": "^6.4|^7.0", 6280 - "symfony/intl": "^6.4|^7.0", 6281 - "symfony/polyfill-intl-icu": "^1.21", 6282 - "symfony/routing": "^6.4|^7.0", 6283 - "symfony/service-contracts": "^2.5|^3", 6284 - "symfony/yaml": "^6.4|^7.0" 6285 - }, 6286 - "type": "library", 6287 - "autoload": { 6288 - "files": [ 6289 - "Resources/functions.php" 6290 - ], 6291 - "psr-4": { 6292 - "Symfony\\Component\\Translation\\": "" 6293 - }, 6294 - "exclude-from-classmap": [ 6295 - "/Tests/" 6296 - ] 6297 - }, 6298 - "notification-url": "https://packagist.org/downloads/", 6299 - "license": [ 6300 - "MIT" 6301 - ], 6302 - "authors": [ 6303 - { 6304 - "name": "Fabien Potencier", 6305 - "email": "fabien@symfony.com" 6306 - }, 6307 - { 6308 - "name": "Symfony Community", 6309 - "homepage": "https://symfony.com/contributors" 6310 - } 6311 - ], 6312 - "description": "Provides tools to internationalize your application", 6313 - "homepage": "https://symfony.com", 6314 - "support": { 6315 - "source": "https://github.com/symfony/translation/tree/v7.3.4" 6316 - }, 6317 - "funding": [ 6318 - { 6319 - "url": "https://symfony.com/sponsor", 6320 - "type": "custom" 6321 - }, 6322 - { 6323 - "url": "https://github.com/fabpot", 6324 - "type": "github" 6325 - }, 6326 - { 6327 - "url": "https://github.com/nicolas-grekas", 6328 - "type": "github" 6329 - }, 6330 - { 6331 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6332 - "type": "tidelift" 6333 - } 6334 - ], 6335 - "time": "2025-09-07T11:39:36+00:00" 6336 - }, 6337 - { 6338 - "name": "symfony/translation-contracts", 6339 - "version": "v3.6.0", 6340 - "source": { 6341 - "type": "git", 6342 - "url": "https://github.com/symfony/translation-contracts.git", 6343 - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" 6344 - }, 6345 - "dist": { 6346 - "type": "zip", 6347 - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", 6348 - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", 6349 - "shasum": "" 6350 - }, 6351 - "require": { 6352 - "php": ">=8.1" 6353 - }, 6354 - "type": "library", 6355 - "extra": { 6356 - "thanks": { 6357 - "url": "https://github.com/symfony/contracts", 6358 - "name": "symfony/contracts" 6359 - }, 6360 - "branch-alias": { 6361 - "dev-main": "3.6-dev" 6362 - } 6363 - }, 6364 - "autoload": { 6365 - "psr-4": { 6366 - "Symfony\\Contracts\\Translation\\": "" 6367 - }, 6368 - "exclude-from-classmap": [ 6369 - "/Test/" 6370 - ] 6371 - }, 6372 - "notification-url": "https://packagist.org/downloads/", 6373 - "license": [ 6374 - "MIT" 6375 - ], 6376 - "authors": [ 6377 - { 6378 - "name": "Nicolas Grekas", 6379 - "email": "p@tchwork.com" 6380 - }, 6381 - { 6382 - "name": "Symfony Community", 6383 - "homepage": "https://symfony.com/contributors" 6384 - } 6385 - ], 6386 - "description": "Generic abstractions related to translation", 6387 - "homepage": "https://symfony.com", 6388 - "keywords": [ 6389 - "abstractions", 6390 - "contracts", 6391 - "decoupling", 6392 - "interfaces", 6393 - "interoperability", 6394 - "standards" 6395 - ], 6396 - "support": { 6397 - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" 6398 - }, 6399 - "funding": [ 6400 - { 6401 - "url": "https://symfony.com/sponsor", 6402 - "type": "custom" 6403 - }, 6404 - { 6405 - "url": "https://github.com/fabpot", 6406 - "type": "github" 6407 - }, 6408 - { 6409 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6410 - "type": "tidelift" 6411 - } 6412 - ], 6413 - "time": "2024-09-27T08:32:26+00:00" 6414 - }, 6415 - { 6416 - "name": "symfony/uid", 6417 - "version": "v7.3.1", 6418 - "source": { 6419 - "type": "git", 6420 - "url": "https://github.com/symfony/uid.git", 6421 - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" 6422 - }, 6423 - "dist": { 6424 - "type": "zip", 6425 - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", 6426 - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", 6427 - "shasum": "" 6428 - }, 6429 - "require": { 6430 - "php": ">=8.2", 6431 - "symfony/polyfill-uuid": "^1.15" 6432 - }, 6433 - "require-dev": { 6434 - "symfony/console": "^6.4|^7.0" 6435 - }, 6436 - "type": "library", 6437 - "autoload": { 6438 - "psr-4": { 6439 - "Symfony\\Component\\Uid\\": "" 6440 - }, 6441 - "exclude-from-classmap": [ 6442 - "/Tests/" 6443 - ] 6444 - }, 6445 - "notification-url": "https://packagist.org/downloads/", 6446 - "license": [ 6447 - "MIT" 6448 - ], 6449 - "authors": [ 6450 - { 6451 - "name": "Grรฉgoire Pineau", 6452 - "email": "lyrixx@lyrixx.info" 6453 - }, 6454 - { 6455 - "name": "Nicolas Grekas", 6456 - "email": "p@tchwork.com" 6457 - }, 6458 - { 6459 - "name": "Symfony Community", 6460 - "homepage": "https://symfony.com/contributors" 6461 - } 6462 - ], 6463 - "description": "Provides an object-oriented API to generate and represent UIDs", 6464 - "homepage": "https://symfony.com", 6465 - "keywords": [ 6466 - "UID", 6467 - "ulid", 6468 - "uuid" 6469 - ], 6470 - "support": { 6471 - "source": "https://github.com/symfony/uid/tree/v7.3.1" 6472 - }, 6473 - "funding": [ 6474 - { 6475 - "url": "https://symfony.com/sponsor", 6476 - "type": "custom" 6477 - }, 6478 - { 6479 - "url": "https://github.com/fabpot", 6480 - "type": "github" 6481 - }, 6482 - { 6483 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6484 - "type": "tidelift" 6485 - } 6486 - ], 6487 - "time": "2025-06-27T19:55:54+00:00" 6488 - }, 6489 - { 6490 - "name": "symfony/var-dumper", 6491 - "version": "v7.3.5", 6492 - "source": { 6493 - "type": "git", 6494 - "url": "https://github.com/symfony/var-dumper.git", 6495 - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" 6496 - }, 6497 - "dist": { 6498 - "type": "zip", 6499 - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", 6500 - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", 6501 - "shasum": "" 6502 - }, 6503 - "require": { 6504 - "php": ">=8.2", 6505 - "symfony/deprecation-contracts": "^2.5|^3", 6506 - "symfony/polyfill-mbstring": "~1.0" 6507 - }, 6508 - "conflict": { 6509 - "symfony/console": "<6.4" 6510 - }, 6511 - "require-dev": { 6512 - "symfony/console": "^6.4|^7.0", 6513 - "symfony/http-kernel": "^6.4|^7.0", 6514 - "symfony/process": "^6.4|^7.0", 6515 - "symfony/uid": "^6.4|^7.0", 6516 - "twig/twig": "^3.12" 6517 - }, 6518 - "bin": [ 6519 - "Resources/bin/var-dump-server" 6520 - ], 6521 - "type": "library", 6522 - "autoload": { 6523 - "files": [ 6524 - "Resources/functions/dump.php" 6525 - ], 6526 - "psr-4": { 6527 - "Symfony\\Component\\VarDumper\\": "" 6528 - }, 6529 - "exclude-from-classmap": [ 6530 - "/Tests/" 6531 - ] 6532 - }, 6533 - "notification-url": "https://packagist.org/downloads/", 6534 - "license": [ 6535 - "MIT" 6536 - ], 6537 - "authors": [ 6538 - { 6539 - "name": "Nicolas Grekas", 6540 - "email": "p@tchwork.com" 6541 - }, 6542 - { 6543 - "name": "Symfony Community", 6544 - "homepage": "https://symfony.com/contributors" 6545 - } 6546 - ], 6547 - "description": "Provides mechanisms for walking through any arbitrary PHP variable", 6548 - "homepage": "https://symfony.com", 6549 - "keywords": [ 6550 - "debug", 6551 - "dump" 6552 - ], 6553 - "support": { 6554 - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" 6555 - }, 6556 - "funding": [ 6557 - { 6558 - "url": "https://symfony.com/sponsor", 6559 - "type": "custom" 6560 - }, 6561 - { 6562 - "url": "https://github.com/fabpot", 6563 - "type": "github" 6564 - }, 6565 - { 6566 - "url": "https://github.com/nicolas-grekas", 6567 - "type": "github" 6568 - }, 6569 - { 6570 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 6571 - "type": "tidelift" 6572 - } 6573 - ], 6574 - "time": "2025-09-27T09:00:46+00:00" 6575 - }, 6576 - { 6577 - "name": "tijsverkoyen/css-to-inline-styles", 6578 - "version": "v2.3.0", 6579 - "source": { 6580 - "type": "git", 6581 - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", 6582 - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" 6583 - }, 6584 - "dist": { 6585 - "type": "zip", 6586 - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", 6587 - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", 6588 - "shasum": "" 6589 - }, 6590 - "require": { 6591 - "ext-dom": "*", 6592 - "ext-libxml": "*", 6593 - "php": "^7.4 || ^8.0", 6594 - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" 6595 - }, 6596 - "require-dev": { 6597 - "phpstan/phpstan": "^2.0", 6598 - "phpstan/phpstan-phpunit": "^2.0", 6599 - "phpunit/phpunit": "^8.5.21 || ^9.5.10" 6600 - }, 6601 - "type": "library", 6602 - "extra": { 6603 - "branch-alias": { 6604 - "dev-master": "2.x-dev" 6605 - } 6606 - }, 6607 - "autoload": { 6608 - "psr-4": { 6609 - "TijsVerkoyen\\CssToInlineStyles\\": "src" 6610 - } 6611 - }, 6612 - "notification-url": "https://packagist.org/downloads/", 6613 - "license": [ 6614 - "BSD-3-Clause" 6615 - ], 6616 - "authors": [ 6617 - { 6618 - "name": "Tijs Verkoyen", 6619 - "email": "css_to_inline_styles@verkoyen.eu", 6620 - "role": "Developer" 6621 - } 6622 - ], 6623 - "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", 6624 - "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", 6625 - "support": { 6626 - "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", 6627 - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" 6628 - }, 6629 - "time": "2024-12-21T16:25:41+00:00" 6630 - }, 6631 - { 6632 - "name": "vlucas/phpdotenv", 6633 - "version": "v5.6.2", 6634 - "source": { 6635 - "type": "git", 6636 - "url": "https://github.com/vlucas/phpdotenv.git", 6637 - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" 6638 - }, 6639 - "dist": { 6640 - "type": "zip", 6641 - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", 6642 - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", 6643 - "shasum": "" 6644 - }, 6645 - "require": { 6646 - "ext-pcre": "*", 6647 - "graham-campbell/result-type": "^1.1.3", 6648 - "php": "^7.2.5 || ^8.0", 6649 - "phpoption/phpoption": "^1.9.3", 6650 - "symfony/polyfill-ctype": "^1.24", 6651 - "symfony/polyfill-mbstring": "^1.24", 6652 - "symfony/polyfill-php80": "^1.24" 6653 - }, 6654 - "require-dev": { 6655 - "bamarni/composer-bin-plugin": "^1.8.2", 6656 - "ext-filter": "*", 6657 - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" 6658 - }, 6659 - "suggest": { 6660 - "ext-filter": "Required to use the boolean validator." 6661 - }, 6662 - "type": "library", 6663 - "extra": { 6664 - "bamarni-bin": { 6665 - "bin-links": true, 6666 - "forward-command": false 6667 - }, 6668 - "branch-alias": { 6669 - "dev-master": "5.6-dev" 6670 - } 6671 - }, 6672 - "autoload": { 6673 - "psr-4": { 6674 - "Dotenv\\": "src/" 6675 - } 6676 - }, 6677 - "notification-url": "https://packagist.org/downloads/", 6678 - "license": [ 6679 - "BSD-3-Clause" 6680 - ], 6681 - "authors": [ 6682 - { 6683 - "name": "Graham Campbell", 6684 - "email": "hello@gjcampbell.co.uk", 6685 - "homepage": "https://github.com/GrahamCampbell" 6686 - }, 6687 - { 6688 - "name": "Vance Lucas", 6689 - "email": "vance@vancelucas.com", 6690 - "homepage": "https://github.com/vlucas" 6691 - } 6692 - ], 6693 - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", 6694 - "keywords": [ 6695 - "dotenv", 6696 - "env", 6697 - "environment" 6698 - ], 6699 - "support": { 6700 - "issues": "https://github.com/vlucas/phpdotenv/issues", 6701 - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" 6702 - }, 6703 - "funding": [ 6704 - { 6705 - "url": "https://github.com/GrahamCampbell", 6706 - "type": "github" 6707 - }, 6708 - { 6709 - "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", 6710 - "type": "tidelift" 6711 - } 6712 - ], 6713 - "time": "2025-04-30T23:37:27+00:00" 6714 - }, 6715 - { 6716 - "name": "voku/portable-ascii", 6717 - "version": "2.0.3", 6718 - "source": { 6719 - "type": "git", 6720 - "url": "https://github.com/voku/portable-ascii.git", 6721 - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" 6722 - }, 6723 - "dist": { 6724 - "type": "zip", 6725 - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", 6726 - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", 6727 - "shasum": "" 6728 - }, 6729 - "require": { 6730 - "php": ">=7.0.0" 6731 - }, 6732 - "require-dev": { 6733 - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" 6734 - }, 6735 - "suggest": { 6736 - "ext-intl": "Use Intl for transliterator_transliterate() support" 6737 - }, 6738 - "type": "library", 6739 - "autoload": { 6740 - "psr-4": { 6741 - "voku\\": "src/voku/" 6742 - } 6743 - }, 6744 - "notification-url": "https://packagist.org/downloads/", 6745 - "license": [ 6746 - "MIT" 6747 - ], 6748 - "authors": [ 6749 - { 6750 - "name": "Lars Moelleken", 6751 - "homepage": "https://www.moelleken.org/" 6752 - } 6753 - ], 6754 - "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", 6755 - "homepage": "https://github.com/voku/portable-ascii", 6756 - "keywords": [ 6757 - "ascii", 6758 - "clean", 6759 - "php" 6760 - ], 6761 - "support": { 6762 - "issues": "https://github.com/voku/portable-ascii/issues", 6763 - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" 6764 - }, 6765 - "funding": [ 6766 - { 6767 - "url": "https://www.paypal.me/moelleken", 6768 - "type": "custom" 6769 - }, 6770 - { 6771 - "url": "https://github.com/voku", 6772 - "type": "github" 6773 - }, 6774 - { 6775 - "url": "https://opencollective.com/portable-ascii", 6776 - "type": "open_collective" 6777 - }, 6778 - { 6779 - "url": "https://www.patreon.com/voku", 6780 - "type": "patreon" 6781 - }, 6782 - { 6783 - "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", 6784 - "type": "tidelift" 6785 - } 6786 - ], 6787 - "time": "2024-11-21T01:49:47+00:00" 6788 - }, 6789 - { 6790 - "name": "webmozart/assert", 6791 - "version": "1.12.1", 6792 - "source": { 6793 - "type": "git", 6794 - "url": "https://github.com/webmozarts/assert.git", 6795 - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" 6796 - }, 6797 - "dist": { 6798 - "type": "zip", 6799 - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", 6800 - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", 6801 - "shasum": "" 6802 - }, 6803 - "require": { 6804 - "ext-ctype": "*", 6805 - "ext-date": "*", 6806 - "ext-filter": "*", 6807 - "php": "^7.2 || ^8.0" 6808 - }, 6809 - "suggest": { 6810 - "ext-intl": "", 6811 - "ext-simplexml": "", 6812 - "ext-spl": "" 6813 - }, 6814 - "type": "library", 6815 - "extra": { 6816 - "branch-alias": { 6817 - "dev-master": "1.10-dev" 6818 - } 6819 - }, 6820 - "autoload": { 6821 - "psr-4": { 6822 - "Webmozart\\Assert\\": "src/" 6823 - } 6824 - }, 6825 - "notification-url": "https://packagist.org/downloads/", 6826 - "license": [ 6827 - "MIT" 6828 - ], 6829 - "authors": [ 6830 - { 6831 - "name": "Bernhard Schussek", 6832 - "email": "bschussek@gmail.com" 6833 - } 6834 - ], 6835 - "description": "Assertions to validate method input/output with nice error messages.", 6836 - "keywords": [ 6837 - "assert", 6838 - "check", 6839 - "validate" 6840 - ], 6841 - "support": { 6842 - "issues": "https://github.com/webmozarts/assert/issues", 6843 - "source": "https://github.com/webmozarts/assert/tree/1.12.1" 6844 - }, 6845 - "time": "2025-10-29T15:56:20+00:00" 6846 - }, 6847 - { 6848 - "name": "yocto/yoclib-multibase", 6849 - "version": "v1.2.0", 6850 - "source": { 6851 - "type": "git", 6852 - "url": "https://github.com/yocto/yoclib-multibase-php.git", 6853 - "reference": "c7171897bf61dbc4a4cc6bb3f2fd5c3e62298e13" 6854 - }, 6855 - "dist": { 6856 - "type": "zip", 6857 - "url": "https://api.github.com/repos/yocto/yoclib-multibase-php/zipball/c7171897bf61dbc4a4cc6bb3f2fd5c3e62298e13", 6858 - "reference": "c7171897bf61dbc4a4cc6bb3f2fd5c3e62298e13", 6859 - "shasum": "" 6860 - }, 6861 - "require": { 6862 - "ext-mbstring": "*", 6863 - "php": "^7.4||^8" 6864 - }, 6865 - "require-dev": { 6866 - "phpunit/phpunit": "^7||^8||^9" 6867 - }, 6868 - "type": "library", 6869 - "autoload": { 6870 - "psr-4": { 6871 - "YOCLIB\\Multiformats\\Multibase\\": "src/" 6872 - } 6873 - }, 6874 - "notification-url": "https://packagist.org/downloads/", 6875 - "license": [ 6876 - "GPL-3.0-or-later" 6877 - ], 6878 - "description": "This yocLibrary enables your project to encode and decode Multibases in PHP.", 6879 - "keywords": [ 6880 - "composer", 6881 - "multibase", 6882 - "multiformats", 6883 - "php", 6884 - "yoclib", 6885 - "yocto" 6886 - ], 6887 - "support": { 6888 - "issues": "https://github.com/yocto/yoclib-multibase-php/issues", 6889 - "source": "https://github.com/yocto/yoclib-multibase-php/tree/v1.2.0" 6890 - }, 6891 - "time": "2024-06-05T13:42:01+00:00" 6892 - } 6893 - ], 6894 - "packages-dev": [ 6895 - { 6896 - "name": "composer/semver", 6897 - "version": "3.4.4", 6898 - "source": { 6899 - "type": "git", 6900 - "url": "https://github.com/composer/semver.git", 6901 - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" 6902 - }, 6903 - "dist": { 6904 - "type": "zip", 6905 - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", 6906 - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", 6907 - "shasum": "" 6908 - }, 6909 - "require": { 6910 - "php": "^5.3.2 || ^7.0 || ^8.0" 6911 - }, 6912 - "require-dev": { 6913 - "phpstan/phpstan": "^1.11", 6914 - "symfony/phpunit-bridge": "^3 || ^7" 6915 - }, 6916 - "type": "library", 6917 - "extra": { 6918 - "branch-alias": { 6919 - "dev-main": "3.x-dev" 6920 - } 6921 - }, 6922 - "autoload": { 6923 - "psr-4": { 6924 - "Composer\\Semver\\": "src" 6925 - } 6926 - }, 6927 - "notification-url": "https://packagist.org/downloads/", 6928 - "license": [ 6929 - "MIT" 6930 - ], 6931 - "authors": [ 6932 - { 6933 - "name": "Nils Adermann", 6934 - "email": "naderman@naderman.de", 6935 - "homepage": "http://www.naderman.de" 6936 - }, 6937 - { 6938 - "name": "Jordi Boggiano", 6939 - "email": "j.boggiano@seld.be", 6940 - "homepage": "http://seld.be" 6941 - }, 6942 - { 6943 - "name": "Rob Bast", 6944 - "email": "rob.bast@gmail.com", 6945 - "homepage": "http://robbast.nl" 6946 - } 6947 - ], 6948 - "description": "Semver library that offers utilities, version constraint parsing and validation.", 6949 - "keywords": [ 6950 - "semantic", 6951 - "semver", 6952 - "validation", 6953 - "versioning" 6954 - ], 6955 - "support": { 6956 - "irc": "ircs://irc.libera.chat:6697/composer", 6957 - "issues": "https://github.com/composer/semver/issues", 6958 - "source": "https://github.com/composer/semver/tree/3.4.4" 6959 - }, 6960 - "funding": [ 6961 - { 6962 - "url": "https://packagist.com", 6963 - "type": "custom" 6964 - }, 6965 - { 6966 - "url": "https://github.com/composer", 6967 - "type": "github" 6968 - } 6969 - ], 6970 - "time": "2025-08-20T19:15:30+00:00" 6971 - }, 6972 - { 6973 - "name": "fakerphp/faker", 6974 - "version": "v1.24.1", 6975 - "source": { 6976 - "type": "git", 6977 - "url": "https://github.com/FakerPHP/Faker.git", 6978 - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" 6979 - }, 6980 - "dist": { 6981 - "type": "zip", 6982 - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", 6983 - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", 6984 - "shasum": "" 6985 - }, 6986 - "require": { 6987 - "php": "^7.4 || ^8.0", 6988 - "psr/container": "^1.0 || ^2.0", 6989 - "symfony/deprecation-contracts": "^2.2 || ^3.0" 6990 - }, 6991 - "conflict": { 6992 - "fzaninotto/faker": "*" 6993 - }, 6994 - "require-dev": { 6995 - "bamarni/composer-bin-plugin": "^1.4.1", 6996 - "doctrine/persistence": "^1.3 || ^2.0", 6997 - "ext-intl": "*", 6998 - "phpunit/phpunit": "^9.5.26", 6999 - "symfony/phpunit-bridge": "^5.4.16" 7000 - }, 7001 - "suggest": { 7002 - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", 7003 - "ext-curl": "Required by Faker\\Provider\\Image to download images.", 7004 - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", 7005 - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", 7006 - "ext-mbstring": "Required for multibyte Unicode string functionality." 7007 - }, 7008 - "type": "library", 7009 - "autoload": { 7010 - "psr-4": { 7011 - "Faker\\": "src/Faker/" 7012 - } 7013 - }, 7014 - "notification-url": "https://packagist.org/downloads/", 7015 - "license": [ 7016 - "MIT" 7017 - ], 7018 - "authors": [ 7019 - { 7020 - "name": "Franรงois Zaninotto" 7021 - } 7022 - ], 7023 - "description": "Faker is a PHP library that generates fake data for you.", 7024 - "keywords": [ 7025 - "data", 7026 - "faker", 7027 - "fixtures" 7028 - ], 7029 - "support": { 7030 - "issues": "https://github.com/FakerPHP/Faker/issues", 7031 - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" 7032 - }, 7033 - "time": "2024-11-21T13:46:39+00:00" 7034 - }, 7035 - { 7036 - "name": "filp/whoops", 7037 - "version": "2.18.4", 7038 - "source": { 7039 - "type": "git", 7040 - "url": "https://github.com/filp/whoops.git", 7041 - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" 7042 - }, 7043 - "dist": { 7044 - "type": "zip", 7045 - "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", 7046 - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", 7047 - "shasum": "" 7048 - }, 7049 - "require": { 7050 - "php": "^7.1 || ^8.0", 7051 - "psr/log": "^1.0.1 || ^2.0 || ^3.0" 7052 - }, 7053 - "require-dev": { 7054 - "mockery/mockery": "^1.0", 7055 - "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", 7056 - "symfony/var-dumper": "^4.0 || ^5.0" 7057 - }, 7058 - "suggest": { 7059 - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", 7060 - "whoops/soap": "Formats errors as SOAP responses" 7061 - }, 7062 - "type": "library", 7063 - "extra": { 7064 - "branch-alias": { 7065 - "dev-master": "2.7-dev" 7066 - } 7067 - }, 7068 - "autoload": { 7069 - "psr-4": { 7070 - "Whoops\\": "src/Whoops/" 7071 - } 7072 - }, 7073 - "notification-url": "https://packagist.org/downloads/", 7074 - "license": [ 7075 - "MIT" 7076 - ], 7077 - "authors": [ 7078 - { 7079 - "name": "Filipe Dobreira", 7080 - "homepage": "https://github.com/filp", 7081 - "role": "Developer" 7082 - } 7083 - ], 7084 - "description": "php error handling for cool kids", 7085 - "homepage": "https://filp.github.io/whoops/", 7086 - "keywords": [ 7087 - "error", 7088 - "exception", 7089 - "handling", 7090 - "library", 7091 - "throwable", 7092 - "whoops" 7093 - ], 7094 - "support": { 7095 - "issues": "https://github.com/filp/whoops/issues", 7096 - "source": "https://github.com/filp/whoops/tree/2.18.4" 7097 - }, 7098 - "funding": [ 7099 - { 7100 - "url": "https://github.com/denis-sokolov", 7101 - "type": "github" 7102 - } 7103 - ], 7104 - "time": "2025-08-08T12:00:00+00:00" 7105 - }, 7106 - { 7107 - "name": "hamcrest/hamcrest-php", 7108 - "version": "v2.1.1", 7109 - "source": { 7110 - "type": "git", 7111 - "url": "https://github.com/hamcrest/hamcrest-php.git", 7112 - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" 7113 - }, 7114 - "dist": { 7115 - "type": "zip", 7116 - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", 7117 - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", 7118 - "shasum": "" 7119 - }, 7120 - "require": { 7121 - "php": "^7.4|^8.0" 7122 - }, 7123 - "replace": { 7124 - "cordoval/hamcrest-php": "*", 7125 - "davedevelopment/hamcrest-php": "*", 7126 - "kodova/hamcrest-php": "*" 7127 - }, 7128 - "require-dev": { 7129 - "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", 7130 - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" 7131 - }, 7132 - "type": "library", 7133 - "extra": { 7134 - "branch-alias": { 7135 - "dev-master": "2.1-dev" 7136 - } 7137 - }, 7138 - "autoload": { 7139 - "classmap": [ 7140 - "hamcrest" 7141 - ] 7142 - }, 7143 - "notification-url": "https://packagist.org/downloads/", 7144 - "license": [ 7145 - "BSD-3-Clause" 7146 - ], 7147 - "description": "This is the PHP port of Hamcrest Matchers", 7148 - "keywords": [ 7149 - "test" 7150 - ], 7151 - "support": { 7152 - "issues": "https://github.com/hamcrest/hamcrest-php/issues", 7153 - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" 7154 - }, 7155 - "time": "2025-04-30T06:54:44+00:00" 7156 - }, 7157 - { 7158 - "name": "laravel/pail", 7159 - "version": "v1.2.3", 7160 - "source": { 7161 - "type": "git", 7162 - "url": "https://github.com/laravel/pail.git", 7163 - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" 7164 - }, 7165 - "dist": { 7166 - "type": "zip", 7167 - "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", 7168 - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", 7169 - "shasum": "" 7170 - }, 7171 - "require": { 7172 - "ext-mbstring": "*", 7173 - "illuminate/console": "^10.24|^11.0|^12.0", 7174 - "illuminate/contracts": "^10.24|^11.0|^12.0", 7175 - "illuminate/log": "^10.24|^11.0|^12.0", 7176 - "illuminate/process": "^10.24|^11.0|^12.0", 7177 - "illuminate/support": "^10.24|^11.0|^12.0", 7178 - "nunomaduro/termwind": "^1.15|^2.0", 7179 - "php": "^8.2", 7180 - "symfony/console": "^6.0|^7.0" 7181 - }, 7182 - "require-dev": { 7183 - "laravel/framework": "^10.24|^11.0|^12.0", 7184 - "laravel/pint": "^1.13", 7185 - "orchestra/testbench-core": "^8.13|^9.0|^10.0", 7186 - "pestphp/pest": "^2.20|^3.0", 7187 - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", 7188 - "phpstan/phpstan": "^1.12.27", 7189 - "symfony/var-dumper": "^6.3|^7.0" 7190 - }, 7191 - "type": "library", 7192 - "extra": { 7193 - "laravel": { 7194 - "providers": [ 7195 - "Laravel\\Pail\\PailServiceProvider" 7196 - ] 7197 - }, 7198 - "branch-alias": { 7199 - "dev-main": "1.x-dev" 7200 - } 7201 - }, 7202 - "autoload": { 7203 - "psr-4": { 7204 - "Laravel\\Pail\\": "src/" 7205 - } 7206 - }, 7207 - "notification-url": "https://packagist.org/downloads/", 7208 - "license": [ 7209 - "MIT" 7210 - ], 7211 - "authors": [ 7212 - { 7213 - "name": "Taylor Otwell", 7214 - "email": "taylor@laravel.com" 7215 - }, 7216 - { 7217 - "name": "Nuno Maduro", 7218 - "email": "enunomaduro@gmail.com" 7219 - } 7220 - ], 7221 - "description": "Easily delve into your Laravel application's log files directly from the command line.", 7222 - "homepage": "https://github.com/laravel/pail", 7223 - "keywords": [ 7224 - "dev", 7225 - "laravel", 7226 - "logs", 7227 - "php", 7228 - "tail" 7229 - ], 7230 - "support": { 7231 - "issues": "https://github.com/laravel/pail/issues", 7232 - "source": "https://github.com/laravel/pail" 7233 - }, 7234 - "time": "2025-06-05T13:55:57+00:00" 7235 - }, 7236 - { 7237 - "name": "laravel/tinker", 7238 - "version": "v2.10.1", 7239 - "source": { 7240 - "type": "git", 7241 - "url": "https://github.com/laravel/tinker.git", 7242 - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" 7243 - }, 7244 - "dist": { 7245 - "type": "zip", 7246 - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", 7247 - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", 7248 - "shasum": "" 7249 - }, 7250 - "require": { 7251 - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 7252 - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 7253 - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 7254 - "php": "^7.2.5|^8.0", 7255 - "psy/psysh": "^0.11.1|^0.12.0", 7256 - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" 7257 - }, 7258 - "require-dev": { 7259 - "mockery/mockery": "~1.3.3|^1.4.2", 7260 - "phpstan/phpstan": "^1.10", 7261 - "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" 7262 - }, 7263 - "suggest": { 7264 - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." 7265 - }, 7266 - "type": "library", 7267 - "extra": { 7268 - "laravel": { 7269 - "providers": [ 7270 - "Laravel\\Tinker\\TinkerServiceProvider" 7271 - ] 7272 - } 7273 - }, 7274 - "autoload": { 7275 - "psr-4": { 7276 - "Laravel\\Tinker\\": "src/" 7277 - } 7278 - }, 7279 - "notification-url": "https://packagist.org/downloads/", 7280 - "license": [ 7281 - "MIT" 7282 - ], 7283 - "authors": [ 7284 - { 7285 - "name": "Taylor Otwell", 7286 - "email": "taylor@laravel.com" 7287 - } 7288 - ], 7289 - "description": "Powerful REPL for the Laravel framework.", 7290 - "keywords": [ 7291 - "REPL", 7292 - "Tinker", 7293 - "laravel", 7294 - "psysh" 7295 - ], 7296 - "support": { 7297 - "issues": "https://github.com/laravel/tinker/issues", 7298 - "source": "https://github.com/laravel/tinker/tree/v2.10.1" 7299 - }, 7300 - "time": "2025-01-27T14:24:01+00:00" 7301 - }, 7302 - { 7303 - "name": "mockery/mockery", 7304 - "version": "1.6.12", 7305 - "source": { 7306 - "type": "git", 7307 - "url": "https://github.com/mockery/mockery.git", 7308 - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" 7309 - }, 7310 - "dist": { 7311 - "type": "zip", 7312 - "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", 7313 - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", 7314 - "shasum": "" 7315 - }, 7316 - "require": { 7317 - "hamcrest/hamcrest-php": "^2.0.1", 7318 - "lib-pcre": ">=7.0", 7319 - "php": ">=7.3" 7320 - }, 7321 - "conflict": { 7322 - "phpunit/phpunit": "<8.0" 7323 - }, 7324 - "require-dev": { 7325 - "phpunit/phpunit": "^8.5 || ^9.6.17", 7326 - "symplify/easy-coding-standard": "^12.1.14" 7327 - }, 7328 - "type": "library", 7329 - "autoload": { 7330 - "files": [ 7331 - "library/helpers.php", 7332 - "library/Mockery.php" 7333 - ], 7334 - "psr-4": { 7335 - "Mockery\\": "library/Mockery" 7336 - } 7337 - }, 7338 - "notification-url": "https://packagist.org/downloads/", 7339 - "license": [ 7340 - "BSD-3-Clause" 7341 - ], 7342 - "authors": [ 7343 - { 7344 - "name": "Pรกdraic Brady", 7345 - "email": "padraic.brady@gmail.com", 7346 - "homepage": "https://github.com/padraic", 7347 - "role": "Author" 7348 - }, 7349 - { 7350 - "name": "Dave Marshall", 7351 - "email": "dave.marshall@atstsolutions.co.uk", 7352 - "homepage": "https://davedevelopment.co.uk", 7353 - "role": "Developer" 7354 - }, 7355 - { 7356 - "name": "Nathanael Esayeas", 7357 - "email": "nathanael.esayeas@protonmail.com", 7358 - "homepage": "https://github.com/ghostwriter", 7359 - "role": "Lead Developer" 7360 - } 7361 - ], 7362 - "description": "Mockery is a simple yet flexible PHP mock object framework", 7363 - "homepage": "https://github.com/mockery/mockery", 7364 - "keywords": [ 7365 - "BDD", 7366 - "TDD", 7367 - "library", 7368 - "mock", 7369 - "mock objects", 7370 - "mockery", 7371 - "stub", 7372 - "test", 7373 - "test double", 7374 - "testing" 7375 - ], 7376 - "support": { 7377 - "docs": "https://docs.mockery.io/", 7378 - "issues": "https://github.com/mockery/mockery/issues", 7379 - "rss": "https://github.com/mockery/mockery/releases.atom", 7380 - "security": "https://github.com/mockery/mockery/security/advisories", 7381 - "source": "https://github.com/mockery/mockery" 7382 - }, 7383 - "time": "2024-05-16T03:13:13+00:00" 7384 - }, 7385 - { 7386 - "name": "myclabs/deep-copy", 7387 - "version": "1.13.4", 7388 - "source": { 7389 - "type": "git", 7390 - "url": "https://github.com/myclabs/DeepCopy.git", 7391 - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" 7392 - }, 7393 - "dist": { 7394 - "type": "zip", 7395 - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", 7396 - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", 7397 - "shasum": "" 7398 - }, 7399 - "require": { 7400 - "php": "^7.1 || ^8.0" 7401 - }, 7402 - "conflict": { 7403 - "doctrine/collections": "<1.6.8", 7404 - "doctrine/common": "<2.13.3 || >=3 <3.2.2" 7405 - }, 7406 - "require-dev": { 7407 - "doctrine/collections": "^1.6.8", 7408 - "doctrine/common": "^2.13.3 || ^3.2.2", 7409 - "phpspec/prophecy": "^1.10", 7410 - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 7411 - }, 7412 - "type": "library", 7413 - "autoload": { 7414 - "files": [ 7415 - "src/DeepCopy/deep_copy.php" 7416 - ], 7417 - "psr-4": { 7418 - "DeepCopy\\": "src/DeepCopy/" 7419 - } 7420 - }, 7421 - "notification-url": "https://packagist.org/downloads/", 7422 - "license": [ 7423 - "MIT" 7424 - ], 7425 - "description": "Create deep copies (clones) of your objects", 7426 - "keywords": [ 7427 - "clone", 7428 - "copy", 7429 - "duplicate", 7430 - "object", 7431 - "object graph" 7432 - ], 7433 - "support": { 7434 - "issues": "https://github.com/myclabs/DeepCopy/issues", 7435 - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" 7436 - }, 7437 - "funding": [ 7438 - { 7439 - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 7440 - "type": "tidelift" 7441 - } 7442 - ], 7443 - "time": "2025-08-01T08:46:24+00:00" 7444 - }, 7445 - { 7446 - "name": "nikic/php-parser", 7447 - "version": "v5.6.2", 7448 - "source": { 7449 - "type": "git", 7450 - "url": "https://github.com/nikic/PHP-Parser.git", 7451 - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" 7452 - }, 7453 - "dist": { 7454 - "type": "zip", 7455 - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", 7456 - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", 7457 - "shasum": "" 7458 - }, 7459 - "require": { 7460 - "ext-ctype": "*", 7461 - "ext-json": "*", 7462 - "ext-tokenizer": "*", 7463 - "php": ">=7.4" 7464 - }, 7465 - "require-dev": { 7466 - "ircmaxell/php-yacc": "^0.0.7", 7467 - "phpunit/phpunit": "^9.0" 7468 - }, 7469 - "bin": [ 7470 - "bin/php-parse" 7471 - ], 7472 - "type": "library", 7473 - "extra": { 7474 - "branch-alias": { 7475 - "dev-master": "5.x-dev" 7476 - } 7477 - }, 7478 - "autoload": { 7479 - "psr-4": { 7480 - "PhpParser\\": "lib/PhpParser" 7481 - } 7482 - }, 7483 - "notification-url": "https://packagist.org/downloads/", 7484 - "license": [ 7485 - "BSD-3-Clause" 7486 - ], 7487 - "authors": [ 7488 - { 7489 - "name": "Nikita Popov" 7490 - } 7491 - ], 7492 - "description": "A PHP parser written in PHP", 7493 - "keywords": [ 7494 - "parser", 7495 - "php" 7496 - ], 7497 - "support": { 7498 - "issues": "https://github.com/nikic/PHP-Parser/issues", 7499 - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" 7500 - }, 7501 - "time": "2025-10-21T19:32:17+00:00" 7502 - }, 7503 - { 7504 - "name": "nunomaduro/collision", 7505 - "version": "v8.8.2", 7506 - "source": { 7507 - "type": "git", 7508 - "url": "https://github.com/nunomaduro/collision.git", 7509 - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" 7510 - }, 7511 - "dist": { 7512 - "type": "zip", 7513 - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", 7514 - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", 7515 - "shasum": "" 7516 - }, 7517 - "require": { 7518 - "filp/whoops": "^2.18.1", 7519 - "nunomaduro/termwind": "^2.3.1", 7520 - "php": "^8.2.0", 7521 - "symfony/console": "^7.3.0" 7522 - }, 7523 - "conflict": { 7524 - "laravel/framework": "<11.44.2 || >=13.0.0", 7525 - "phpunit/phpunit": "<11.5.15 || >=13.0.0" 7526 - }, 7527 - "require-dev": { 7528 - "brianium/paratest": "^7.8.3", 7529 - "larastan/larastan": "^3.4.2", 7530 - "laravel/framework": "^11.44.2 || ^12.18", 7531 - "laravel/pint": "^1.22.1", 7532 - "laravel/sail": "^1.43.1", 7533 - "laravel/sanctum": "^4.1.1", 7534 - "laravel/tinker": "^2.10.1", 7535 - "orchestra/testbench-core": "^9.12.0 || ^10.4", 7536 - "pestphp/pest": "^3.8.2", 7537 - "sebastian/environment": "^7.2.1 || ^8.0" 7538 - }, 7539 - "type": "library", 7540 - "extra": { 7541 - "laravel": { 7542 - "providers": [ 7543 - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" 7544 - ] 7545 - }, 7546 - "branch-alias": { 7547 - "dev-8.x": "8.x-dev" 7548 - } 7549 - }, 7550 - "autoload": { 7551 - "files": [ 7552 - "./src/Adapters/Phpunit/Autoload.php" 7553 - ], 7554 - "psr-4": { 7555 - "NunoMaduro\\Collision\\": "src/" 7556 - } 7557 - }, 7558 - "notification-url": "https://packagist.org/downloads/", 7559 - "license": [ 7560 - "MIT" 7561 - ], 7562 - "authors": [ 7563 - { 7564 - "name": "Nuno Maduro", 7565 - "email": "enunomaduro@gmail.com" 7566 - } 7567 - ], 7568 - "description": "Cli error handling for console/command-line PHP applications.", 7569 - "keywords": [ 7570 - "artisan", 7571 - "cli", 7572 - "command-line", 7573 - "console", 7574 - "dev", 7575 - "error", 7576 - "handling", 7577 - "laravel", 7578 - "laravel-zero", 7579 - "php", 7580 - "symfony" 7581 - ], 7582 - "support": { 7583 - "issues": "https://github.com/nunomaduro/collision/issues", 7584 - "source": "https://github.com/nunomaduro/collision" 7585 - }, 7586 - "funding": [ 7587 - { 7588 - "url": "https://www.paypal.com/paypalme/enunomaduro", 7589 - "type": "custom" 7590 - }, 7591 - { 7592 - "url": "https://github.com/nunomaduro", 7593 - "type": "github" 7594 - }, 7595 - { 7596 - "url": "https://www.patreon.com/nunomaduro", 7597 - "type": "patreon" 7598 - } 7599 - ], 7600 - "time": "2025-06-25T02:12:12+00:00" 7601 - }, 7602 - { 7603 - "name": "orchestra/canvas", 7604 - "version": "v9.2.2", 7605 - "source": { 7606 - "type": "git", 7607 - "url": "https://github.com/orchestral/canvas.git", 7608 - "reference": "002d948834c0899e511f5ac0381669363d7881e5" 7609 - }, 7610 - "dist": { 7611 - "type": "zip", 7612 - "url": "https://api.github.com/repos/orchestral/canvas/zipball/002d948834c0899e511f5ac0381669363d7881e5", 7613 - "reference": "002d948834c0899e511f5ac0381669363d7881e5", 7614 - "shasum": "" 7615 - }, 7616 - "require": { 7617 - "composer-runtime-api": "^2.2", 7618 - "composer/semver": "^3.0", 7619 - "illuminate/console": "^11.43.0", 7620 - "illuminate/database": "^11.43.0", 7621 - "illuminate/filesystem": "^11.43.0", 7622 - "illuminate/support": "^11.43.0", 7623 - "orchestra/canvas-core": "^9.1.1", 7624 - "orchestra/sidekick": "^1.0.2", 7625 - "orchestra/testbench-core": "^9.11.0", 7626 - "php": "^8.2", 7627 - "symfony/polyfill-php83": "^1.31", 7628 - "symfony/yaml": "^7.0.3" 7629 - }, 7630 - "require-dev": { 7631 - "laravel/framework": "^11.43.0", 7632 - "laravel/pint": "^1.21", 7633 - "mockery/mockery": "^1.6.10", 7634 - "phpstan/phpstan": "^2.1", 7635 - "phpunit/phpunit": "^11.5.7", 7636 - "spatie/laravel-ray": "^1.39.1" 7637 - }, 7638 - "bin": [ 7639 - "canvas" 7640 - ], 7641 - "type": "library", 7642 - "extra": { 7643 - "laravel": { 7644 - "providers": [ 7645 - "Orchestra\\Canvas\\LaravelServiceProvider" 7646 - ] 7647 - } 7648 - }, 7649 - "autoload": { 7650 - "psr-4": { 7651 - "Orchestra\\Canvas\\": "src/" 7652 - } 7653 - }, 7654 - "notification-url": "https://packagist.org/downloads/", 7655 - "license": [ 7656 - "MIT" 7657 - ], 7658 - "authors": [ 7659 - { 7660 - "name": "Taylor Otwell", 7661 - "email": "taylor@laravel.com" 7662 - }, 7663 - { 7664 - "name": "Mior Muhammad Zaki", 7665 - "email": "crynobone@gmail.com" 7666 - } 7667 - ], 7668 - "description": "Code Generators for Laravel Applications and Packages", 7669 - "support": { 7670 - "issues": "https://github.com/orchestral/canvas/issues", 7671 - "source": "https://github.com/orchestral/canvas/tree/v9.2.2" 7672 - }, 7673 - "time": "2025-02-19T04:27:08+00:00" 7674 - }, 7675 - { 7676 - "name": "orchestra/canvas-core", 7677 - "version": "v9.1.1", 7678 - "source": { 7679 - "type": "git", 7680 - "url": "https://github.com/orchestral/canvas-core.git", 7681 - "reference": "a8ebfa6c2e50f8c6597c489b4dfaf9af6789f62a" 7682 - }, 7683 - "dist": { 7684 - "type": "zip", 7685 - "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/a8ebfa6c2e50f8c6597c489b4dfaf9af6789f62a", 7686 - "reference": "a8ebfa6c2e50f8c6597c489b4dfaf9af6789f62a", 7687 - "shasum": "" 7688 - }, 7689 - "require": { 7690 - "composer-runtime-api": "^2.2", 7691 - "composer/semver": "^3.0", 7692 - "illuminate/console": "^11.43.0", 7693 - "illuminate/support": "^11.43.0", 7694 - "orchestra/sidekick": "^1.0.2", 7695 - "php": "^8.2", 7696 - "symfony/polyfill-php83": "^1.31" 7697 - }, 7698 - "require-dev": { 7699 - "laravel/framework": "^11.43.0", 7700 - "laravel/pint": "^1.20", 7701 - "mockery/mockery": "^1.6.10", 7702 - "orchestra/testbench-core": "^9.11.0", 7703 - "phpstan/phpstan": "^2.1", 7704 - "phpunit/phpunit": "^11.5.7", 7705 - "symfony/yaml": "^7.0.3" 7706 - }, 7707 - "type": "library", 7708 - "extra": { 7709 - "laravel": { 7710 - "providers": [ 7711 - "Orchestra\\Canvas\\Core\\LaravelServiceProvider" 7712 - ] 7713 - } 7714 - }, 7715 - "autoload": { 7716 - "psr-4": { 7717 - "Orchestra\\Canvas\\Core\\": "src/" 7718 - } 7719 - }, 7720 - "notification-url": "https://packagist.org/downloads/", 7721 - "license": [ 7722 - "MIT" 7723 - ], 7724 - "authors": [ 7725 - { 7726 - "name": "Taylor Otwell", 7727 - "email": "taylor@laravel.com" 7728 - }, 7729 - { 7730 - "name": "Mior Muhammad Zaki", 7731 - "email": "crynobone@gmail.com" 7732 - } 7733 - ], 7734 - "description": "Code Generators Builder for Laravel Applications and Packages", 7735 - "support": { 7736 - "issues": "https://github.com/orchestral/canvas/issues", 7737 - "source": "https://github.com/orchestral/canvas-core/tree/v9.1.1" 7738 - }, 7739 - "time": "2025-02-19T04:14:36+00:00" 7740 - }, 7741 - { 7742 - "name": "orchestra/sidekick", 7743 - "version": "v1.2.17", 7744 - "source": { 7745 - "type": "git", 7746 - "url": "https://github.com/orchestral/sidekick.git", 7747 - "reference": "371ce2882ee3f5bf826b36e75d461e51c9cd76c2" 7748 - }, 7749 - "dist": { 7750 - "type": "zip", 7751 - "url": "https://api.github.com/repos/orchestral/sidekick/zipball/371ce2882ee3f5bf826b36e75d461e51c9cd76c2", 7752 - "reference": "371ce2882ee3f5bf826b36e75d461e51c9cd76c2", 7753 - "shasum": "" 7754 - }, 7755 - "require": { 7756 - "composer-runtime-api": "^2.2", 7757 - "php": "^8.1", 7758 - "symfony/polyfill-php83": "^1.32" 7759 - }, 7760 - "require-dev": { 7761 - "fakerphp/faker": "^1.21", 7762 - "laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0", 7763 - "laravel/pint": "^1.4", 7764 - "mockery/mockery": "^1.5.1", 7765 - "orchestra/testbench-core": "^8.37.0|^9.14.0|^10.2.0|^11.0", 7766 - "phpstan/phpstan": "^2.1.14", 7767 - "phpunit/phpunit": "^10.0|^11.0|^12.0", 7768 - "symfony/process": "^6.0|^7.0" 7769 - }, 7770 - "type": "library", 7771 - "autoload": { 7772 - "files": [ 7773 - "src/Eloquent/functions.php", 7774 - "src/Http/functions.php", 7775 - "src/functions.php" 7776 - ], 7777 - "psr-4": { 7778 - "Orchestra\\Sidekick\\": "src/" 7779 - } 7780 - }, 7781 - "notification-url": "https://packagist.org/downloads/", 7782 - "license": [ 7783 - "MIT" 7784 - ], 7785 - "authors": [ 7786 - { 7787 - "name": "Mior Muhammad Zaki", 7788 - "email": "crynobone@gmail.com" 7789 - } 7790 - ], 7791 - "description": "Packages Toolkit Utilities and Helpers for Laravel", 7792 - "support": { 7793 - "issues": "https://github.com/orchestral/sidekick/issues", 7794 - "source": "https://github.com/orchestral/sidekick/tree/v1.2.17" 7795 - }, 7796 - "time": "2025-10-02T11:02:26+00:00" 7797 - }, 7798 - { 7799 - "name": "orchestra/testbench", 7800 - "version": "v9.15.0", 7801 - "source": { 7802 - "type": "git", 7803 - "url": "https://github.com/orchestral/testbench.git", 7804 - "reference": "d0181240f93688448d4ae3b5479ec5ed70a87a47" 7805 - }, 7806 - "dist": { 7807 - "type": "zip", 7808 - "url": "https://api.github.com/repos/orchestral/testbench/zipball/d0181240f93688448d4ae3b5479ec5ed70a87a47", 7809 - "reference": "d0181240f93688448d4ae3b5479ec5ed70a87a47", 7810 - "shasum": "" 7811 - }, 7812 - "require": { 7813 - "composer-runtime-api": "^2.2", 7814 - "fakerphp/faker": "^1.23", 7815 - "laravel/framework": "^11.45.2", 7816 - "mockery/mockery": "^1.6.10", 7817 - "orchestra/testbench-core": "^9.16.0", 7818 - "orchestra/workbench": "^9.13.5", 7819 - "php": "^8.2", 7820 - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", 7821 - "symfony/process": "^7.0.3", 7822 - "symfony/yaml": "^7.0.3", 7823 - "vlucas/phpdotenv": "^5.6.1" 7824 - }, 7825 - "type": "library", 7826 - "notification-url": "https://packagist.org/downloads/", 7827 - "license": [ 7828 - "MIT" 7829 - ], 7830 - "authors": [ 7831 - { 7832 - "name": "Mior Muhammad Zaki", 7833 - "email": "crynobone@gmail.com", 7834 - "homepage": "https://github.com/crynobone" 7835 - } 7836 - ], 7837 - "description": "Laravel Testing Helper for Packages Development", 7838 - "homepage": "https://packages.tools/testbench/", 7839 - "keywords": [ 7840 - "BDD", 7841 - "TDD", 7842 - "dev", 7843 - "laravel", 7844 - "laravel-packages", 7845 - "testing" 7846 - ], 7847 - "support": { 7848 - "issues": "https://github.com/orchestral/testbench/issues", 7849 - "source": "https://github.com/orchestral/testbench/tree/v9.15.0" 7850 - }, 7851 - "time": "2025-08-20T11:42:03+00:00" 7852 - }, 7853 - { 7854 - "name": "orchestra/testbench-core", 7855 - "version": "v9.17.0", 7856 - "source": { 7857 - "type": "git", 7858 - "url": "https://github.com/orchestral/testbench-core.git", 7859 - "reference": "a5b4d56a40536fde50a72e20ce43abaa76f8de2f" 7860 - }, 7861 - "dist": { 7862 - "type": "zip", 7863 - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/a5b4d56a40536fde50a72e20ce43abaa76f8de2f", 7864 - "reference": "a5b4d56a40536fde50a72e20ce43abaa76f8de2f", 7865 - "shasum": "" 7866 - }, 7867 - "require": { 7868 - "composer-runtime-api": "^2.2", 7869 - "orchestra/sidekick": "~1.1.20|~1.2.17", 7870 - "php": "^8.2", 7871 - "symfony/deprecation-contracts": "^2.5|^3.0", 7872 - "symfony/polyfill-php83": "^1.32" 7873 - }, 7874 - "conflict": { 7875 - "brianium/paratest": "<7.3.0|>=8.0.0", 7876 - "laravel/framework": "<11.45.3|>=12.0.0", 7877 - "laravel/serializable-closure": "<1.3.0|>=2.0.0 <2.0.3|>=3.0.0", 7878 - "nunomaduro/collision": "<8.0.0|>=9.0.0", 7879 - "orchestra/testbench-dusk": "<9.10.0|>=10.0.0", 7880 - "phpunit/phpunit": "<10.5.35|>=11.0.0 <11.3.6|>=12.0.0 <12.0.1|>=12.4.0" 7881 - }, 7882 - "require-dev": { 7883 - "fakerphp/faker": "^1.24", 7884 - "laravel/framework": "^11.45.3", 7885 - "laravel/pint": "^1.24", 7886 - "laravel/serializable-closure": "^1.3|^2.0.4", 7887 - "mockery/mockery": "^1.6.10", 7888 - "phpstan/phpstan": "^2.1.19", 7889 - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", 7890 - "spatie/laravel-ray": "^1.40.2", 7891 - "symfony/process": "^7.0.3", 7892 - "symfony/yaml": "^7.0.3", 7893 - "vlucas/phpdotenv": "^5.6.1" 7894 - }, 7895 - "suggest": { 7896 - "brianium/paratest": "Allow using parallel testing (^7.3).", 7897 - "ext-pcntl": "Required to use all features of the console signal trapping.", 7898 - "fakerphp/faker": "Allow using Faker for testing (^1.23).", 7899 - "laravel/framework": "Required for testing (^11.45.3).", 7900 - "mockery/mockery": "Allow using Mockery for testing (^1.6).", 7901 - "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.0).", 7902 - "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^9.10).", 7903 - "phpunit/phpunit": "Allow using PHPUnit for testing (^10.5.35|^11.3.6|^12.0.1).", 7904 - "symfony/process": "Required to use Orchestra\\Testbench\\remote function (^7.0).", 7905 - "symfony/yaml": "Required for Testbench CLI (^7.0).", 7906 - "vlucas/phpdotenv": "Required for Testbench CLI (^5.6.1)." 7907 - }, 7908 - "bin": [ 7909 - "testbench" 7910 - ], 7911 - "type": "library", 7912 - "autoload": { 7913 - "files": [ 7914 - "src/functions.php" 7915 - ], 7916 - "psr-4": { 7917 - "Orchestra\\Testbench\\": "src/" 7918 - } 7919 - }, 7920 - "notification-url": "https://packagist.org/downloads/", 7921 - "license": [ 7922 - "MIT" 7923 - ], 7924 - "authors": [ 7925 - { 7926 - "name": "Mior Muhammad Zaki", 7927 - "email": "crynobone@gmail.com", 7928 - "homepage": "https://github.com/crynobone" 7929 - } 7930 - ], 7931 - "description": "Testing Helper for Laravel Development", 7932 - "homepage": "https://packages.tools/testbench", 7933 - "keywords": [ 7934 - "BDD", 7935 - "TDD", 7936 - "dev", 7937 - "laravel", 7938 - "laravel-packages", 7939 - "testing" 7940 - ], 7941 - "support": { 7942 - "issues": "https://github.com/orchestral/testbench/issues", 7943 - "source": "https://github.com/orchestral/testbench-core" 7944 - }, 7945 - "time": "2025-10-14T12:02:37+00:00" 7946 - }, 7947 - { 7948 - "name": "orchestra/workbench", 7949 - "version": "v9.13.5", 7950 - "source": { 7951 - "type": "git", 7952 - "url": "https://github.com/orchestral/workbench.git", 7953 - "reference": "1da2ea95089ed3516bda6f8e9cd57c81290004bf" 7954 - }, 7955 - "dist": { 7956 - "type": "zip", 7957 - "url": "https://api.github.com/repos/orchestral/workbench/zipball/1da2ea95089ed3516bda6f8e9cd57c81290004bf", 7958 - "reference": "1da2ea95089ed3516bda6f8e9cd57c81290004bf", 7959 - "shasum": "" 7960 - }, 7961 - "require": { 7962 - "composer-runtime-api": "^2.2", 7963 - "fakerphp/faker": "^1.23", 7964 - "laravel/framework": "^11.44.2", 7965 - "laravel/pail": "^1.2", 7966 - "laravel/tinker": "^2.9", 7967 - "nunomaduro/collision": "^8.0", 7968 - "orchestra/canvas": "^9.2.2", 7969 - "orchestra/sidekick": "^1.1.0", 7970 - "orchestra/testbench-core": "^9.12.0", 7971 - "php": "^8.2", 7972 - "symfony/polyfill-php83": "^1.31", 7973 - "symfony/polyfill-php84": "^1.31", 7974 - "symfony/process": "^7.0.3", 7975 - "symfony/yaml": "^7.0.3" 7976 - }, 7977 - "require-dev": { 7978 - "laravel/pint": "^1.21", 7979 - "mockery/mockery": "^1.6.10", 7980 - "phpstan/phpstan": "^2.1", 7981 - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", 7982 - "spatie/laravel-ray": "^1.39.1" 7983 - }, 7984 - "suggest": { 7985 - "ext-pcntl": "Required to use all features of the console signal trapping." 7986 - }, 7987 - "type": "library", 7988 - "autoload": { 7989 - "psr-4": { 7990 - "Orchestra\\Workbench\\": "src/" 7991 - } 7992 - }, 7993 - "notification-url": "https://packagist.org/downloads/", 7994 - "license": [ 7995 - "MIT" 7996 - ], 7997 - "authors": [ 7998 - { 7999 - "name": "Mior Muhammad Zaki", 8000 - "email": "crynobone@gmail.com" 8001 - } 8002 - ], 8003 - "description": "Workbench Companion for Laravel Packages Development", 8004 - "keywords": [ 8005 - "dev", 8006 - "laravel", 8007 - "laravel-packages", 8008 - "testing" 8009 - ], 8010 - "support": { 8011 - "issues": "https://github.com/orchestral/workbench/issues", 8012 - "source": "https://github.com/orchestral/workbench/tree/v9.13.5" 8013 - }, 8014 - "time": "2025-04-06T11:06:19+00:00" 8015 - }, 8016 - { 8017 - "name": "phar-io/manifest", 8018 - "version": "2.0.4", 8019 - "source": { 8020 - "type": "git", 8021 - "url": "https://github.com/phar-io/manifest.git", 8022 - "reference": "54750ef60c58e43759730615a392c31c80e23176" 8023 - }, 8024 - "dist": { 8025 - "type": "zip", 8026 - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", 8027 - "reference": "54750ef60c58e43759730615a392c31c80e23176", 8028 - "shasum": "" 8029 - }, 8030 - "require": { 8031 - "ext-dom": "*", 8032 - "ext-libxml": "*", 8033 - "ext-phar": "*", 8034 - "ext-xmlwriter": "*", 8035 - "phar-io/version": "^3.0.1", 8036 - "php": "^7.2 || ^8.0" 8037 - }, 8038 - "type": "library", 8039 - "extra": { 8040 - "branch-alias": { 8041 - "dev-master": "2.0.x-dev" 8042 - } 8043 - }, 8044 - "autoload": { 8045 - "classmap": [ 8046 - "src/" 8047 - ] 8048 - }, 8049 - "notification-url": "https://packagist.org/downloads/", 8050 - "license": [ 8051 - "BSD-3-Clause" 8052 - ], 8053 - "authors": [ 8054 - { 8055 - "name": "Arne Blankerts", 8056 - "email": "arne@blankerts.de", 8057 - "role": "Developer" 8058 - }, 8059 - { 8060 - "name": "Sebastian Heuer", 8061 - "email": "sebastian@phpeople.de", 8062 - "role": "Developer" 8063 - }, 8064 - { 8065 - "name": "Sebastian Bergmann", 8066 - "email": "sebastian@phpunit.de", 8067 - "role": "Developer" 8068 - } 8069 - ], 8070 - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 8071 - "support": { 8072 - "issues": "https://github.com/phar-io/manifest/issues", 8073 - "source": "https://github.com/phar-io/manifest/tree/2.0.4" 8074 - }, 8075 - "funding": [ 8076 - { 8077 - "url": "https://github.com/theseer", 8078 - "type": "github" 8079 - } 8080 - ], 8081 - "time": "2024-03-03T12:33:53+00:00" 8082 - }, 8083 - { 8084 - "name": "phar-io/version", 8085 - "version": "3.2.1", 8086 - "source": { 8087 - "type": "git", 8088 - "url": "https://github.com/phar-io/version.git", 8089 - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 8090 - }, 8091 - "dist": { 8092 - "type": "zip", 8093 - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 8094 - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 8095 - "shasum": "" 8096 - }, 8097 - "require": { 8098 - "php": "^7.2 || ^8.0" 8099 - }, 8100 - "type": "library", 8101 - "autoload": { 8102 - "classmap": [ 8103 - "src/" 8104 - ] 8105 - }, 8106 - "notification-url": "https://packagist.org/downloads/", 8107 - "license": [ 8108 - "BSD-3-Clause" 8109 - ], 8110 - "authors": [ 8111 - { 8112 - "name": "Arne Blankerts", 8113 - "email": "arne@blankerts.de", 8114 - "role": "Developer" 8115 - }, 8116 - { 8117 - "name": "Sebastian Heuer", 8118 - "email": "sebastian@phpeople.de", 8119 - "role": "Developer" 8120 - }, 8121 - { 8122 - "name": "Sebastian Bergmann", 8123 - "email": "sebastian@phpunit.de", 8124 - "role": "Developer" 8125 - } 8126 - ], 8127 - "description": "Library for handling version information and constraints", 8128 - "support": { 8129 - "issues": "https://github.com/phar-io/version/issues", 8130 - "source": "https://github.com/phar-io/version/tree/3.2.1" 8131 - }, 8132 - "time": "2022-02-21T01:04:05+00:00" 8133 - }, 8134 - { 8135 - "name": "phpunit/php-code-coverage", 8136 - "version": "11.0.11", 8137 - "source": { 8138 - "type": "git", 8139 - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 8140 - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" 8141 - }, 8142 - "dist": { 8143 - "type": "zip", 8144 - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", 8145 - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", 8146 - "shasum": "" 8147 - }, 8148 - "require": { 8149 - "ext-dom": "*", 8150 - "ext-libxml": "*", 8151 - "ext-xmlwriter": "*", 8152 - "nikic/php-parser": "^5.4.0", 8153 - "php": ">=8.2", 8154 - "phpunit/php-file-iterator": "^5.1.0", 8155 - "phpunit/php-text-template": "^4.0.1", 8156 - "sebastian/code-unit-reverse-lookup": "^4.0.1", 8157 - "sebastian/complexity": "^4.0.1", 8158 - "sebastian/environment": "^7.2.0", 8159 - "sebastian/lines-of-code": "^3.0.1", 8160 - "sebastian/version": "^5.0.2", 8161 - "theseer/tokenizer": "^1.2.3" 8162 - }, 8163 - "require-dev": { 8164 - "phpunit/phpunit": "^11.5.2" 8165 - }, 8166 - "suggest": { 8167 - "ext-pcov": "PHP extension that provides line coverage", 8168 - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 8169 - }, 8170 - "type": "library", 8171 - "extra": { 8172 - "branch-alias": { 8173 - "dev-main": "11.0.x-dev" 8174 - } 8175 - }, 8176 - "autoload": { 8177 - "classmap": [ 8178 - "src/" 8179 - ] 8180 - }, 8181 - "notification-url": "https://packagist.org/downloads/", 8182 - "license": [ 8183 - "BSD-3-Clause" 8184 - ], 8185 - "authors": [ 8186 - { 8187 - "name": "Sebastian Bergmann", 8188 - "email": "sebastian@phpunit.de", 8189 - "role": "lead" 8190 - } 8191 - ], 8192 - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 8193 - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 8194 - "keywords": [ 8195 - "coverage", 8196 - "testing", 8197 - "xunit" 8198 - ], 8199 - "support": { 8200 - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 8201 - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 8202 - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" 8203 - }, 8204 - "funding": [ 8205 - { 8206 - "url": "https://github.com/sebastianbergmann", 8207 - "type": "github" 8208 - }, 8209 - { 8210 - "url": "https://liberapay.com/sebastianbergmann", 8211 - "type": "liberapay" 8212 - }, 8213 - { 8214 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 8215 - "type": "thanks_dev" 8216 - }, 8217 - { 8218 - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", 8219 - "type": "tidelift" 8220 - } 8221 - ], 8222 - "time": "2025-08-27T14:37:49+00:00" 8223 - }, 8224 - { 8225 - "name": "phpunit/php-file-iterator", 8226 - "version": "5.1.0", 8227 - "source": { 8228 - "type": "git", 8229 - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 8230 - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" 8231 - }, 8232 - "dist": { 8233 - "type": "zip", 8234 - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", 8235 - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", 8236 - "shasum": "" 8237 - }, 8238 - "require": { 8239 - "php": ">=8.2" 8240 - }, 8241 - "require-dev": { 8242 - "phpunit/phpunit": "^11.0" 8243 - }, 8244 - "type": "library", 8245 - "extra": { 8246 - "branch-alias": { 8247 - "dev-main": "5.0-dev" 8248 - } 8249 - }, 8250 - "autoload": { 8251 - "classmap": [ 8252 - "src/" 8253 - ] 8254 - }, 8255 - "notification-url": "https://packagist.org/downloads/", 8256 - "license": [ 8257 - "BSD-3-Clause" 8258 - ], 8259 - "authors": [ 8260 - { 8261 - "name": "Sebastian Bergmann", 8262 - "email": "sebastian@phpunit.de", 8263 - "role": "lead" 8264 - } 8265 - ], 8266 - "description": "FilterIterator implementation that filters files based on a list of suffixes.", 8267 - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 8268 - "keywords": [ 8269 - "filesystem", 8270 - "iterator" 8271 - ], 8272 - "support": { 8273 - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 8274 - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", 8275 - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" 8276 - }, 8277 - "funding": [ 8278 - { 8279 - "url": "https://github.com/sebastianbergmann", 8280 - "type": "github" 8281 - } 8282 - ], 8283 - "time": "2024-08-27T05:02:59+00:00" 8284 - }, 8285 - { 8286 - "name": "phpunit/php-invoker", 8287 - "version": "5.0.1", 8288 - "source": { 8289 - "type": "git", 8290 - "url": "https://github.com/sebastianbergmann/php-invoker.git", 8291 - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" 8292 - }, 8293 - "dist": { 8294 - "type": "zip", 8295 - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", 8296 - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", 8297 - "shasum": "" 8298 - }, 8299 - "require": { 8300 - "php": ">=8.2" 8301 - }, 8302 - "require-dev": { 8303 - "ext-pcntl": "*", 8304 - "phpunit/phpunit": "^11.0" 8305 - }, 8306 - "suggest": { 8307 - "ext-pcntl": "*" 8308 - }, 8309 - "type": "library", 8310 - "extra": { 8311 - "branch-alias": { 8312 - "dev-main": "5.0-dev" 8313 - } 8314 - }, 8315 - "autoload": { 8316 - "classmap": [ 8317 - "src/" 8318 - ] 8319 - }, 8320 - "notification-url": "https://packagist.org/downloads/", 8321 - "license": [ 8322 - "BSD-3-Clause" 8323 - ], 8324 - "authors": [ 8325 - { 8326 - "name": "Sebastian Bergmann", 8327 - "email": "sebastian@phpunit.de", 8328 - "role": "lead" 8329 - } 8330 - ], 8331 - "description": "Invoke callables with a timeout", 8332 - "homepage": "https://github.com/sebastianbergmann/php-invoker/", 8333 - "keywords": [ 8334 - "process" 8335 - ], 8336 - "support": { 8337 - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 8338 - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", 8339 - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" 8340 - }, 8341 - "funding": [ 8342 - { 8343 - "url": "https://github.com/sebastianbergmann", 8344 - "type": "github" 8345 - } 8346 - ], 8347 - "time": "2024-07-03T05:07:44+00:00" 8348 - }, 8349 - { 8350 - "name": "phpunit/php-text-template", 8351 - "version": "4.0.1", 8352 - "source": { 8353 - "type": "git", 8354 - "url": "https://github.com/sebastianbergmann/php-text-template.git", 8355 - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" 8356 - }, 8357 - "dist": { 8358 - "type": "zip", 8359 - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", 8360 - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", 8361 - "shasum": "" 8362 - }, 8363 - "require": { 8364 - "php": ">=8.2" 8365 - }, 8366 - "require-dev": { 8367 - "phpunit/phpunit": "^11.0" 8368 - }, 8369 - "type": "library", 8370 - "extra": { 8371 - "branch-alias": { 8372 - "dev-main": "4.0-dev" 8373 - } 8374 - }, 8375 - "autoload": { 8376 - "classmap": [ 8377 - "src/" 8378 - ] 8379 - }, 8380 - "notification-url": "https://packagist.org/downloads/", 8381 - "license": [ 8382 - "BSD-3-Clause" 8383 - ], 8384 - "authors": [ 8385 - { 8386 - "name": "Sebastian Bergmann", 8387 - "email": "sebastian@phpunit.de", 8388 - "role": "lead" 8389 - } 8390 - ], 8391 - "description": "Simple template engine.", 8392 - "homepage": "https://github.com/sebastianbergmann/php-text-template/", 8393 - "keywords": [ 8394 - "template" 8395 - ], 8396 - "support": { 8397 - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 8398 - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", 8399 - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" 8400 - }, 8401 - "funding": [ 8402 - { 8403 - "url": "https://github.com/sebastianbergmann", 8404 - "type": "github" 8405 - } 8406 - ], 8407 - "time": "2024-07-03T05:08:43+00:00" 8408 - }, 8409 - { 8410 - "name": "phpunit/php-timer", 8411 - "version": "7.0.1", 8412 - "source": { 8413 - "type": "git", 8414 - "url": "https://github.com/sebastianbergmann/php-timer.git", 8415 - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" 8416 - }, 8417 - "dist": { 8418 - "type": "zip", 8419 - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", 8420 - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", 8421 - "shasum": "" 8422 - }, 8423 - "require": { 8424 - "php": ">=8.2" 8425 - }, 8426 - "require-dev": { 8427 - "phpunit/phpunit": "^11.0" 8428 - }, 8429 - "type": "library", 8430 - "extra": { 8431 - "branch-alias": { 8432 - "dev-main": "7.0-dev" 8433 - } 8434 - }, 8435 - "autoload": { 8436 - "classmap": [ 8437 - "src/" 8438 - ] 8439 - }, 8440 - "notification-url": "https://packagist.org/downloads/", 8441 - "license": [ 8442 - "BSD-3-Clause" 8443 - ], 8444 - "authors": [ 8445 - { 8446 - "name": "Sebastian Bergmann", 8447 - "email": "sebastian@phpunit.de", 8448 - "role": "lead" 8449 - } 8450 - ], 8451 - "description": "Utility class for timing", 8452 - "homepage": "https://github.com/sebastianbergmann/php-timer/", 8453 - "keywords": [ 8454 - "timer" 8455 - ], 8456 - "support": { 8457 - "issues": "https://github.com/sebastianbergmann/php-timer/issues", 8458 - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", 8459 - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" 8460 - }, 8461 - "funding": [ 8462 - { 8463 - "url": "https://github.com/sebastianbergmann", 8464 - "type": "github" 8465 - } 8466 - ], 8467 - "time": "2024-07-03T05:09:35+00:00" 8468 - }, 8469 - { 8470 - "name": "phpunit/phpunit", 8471 - "version": "11.5.42", 8472 - "source": { 8473 - "type": "git", 8474 - "url": "https://github.com/sebastianbergmann/phpunit.git", 8475 - "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c" 8476 - }, 8477 - "dist": { 8478 - "type": "zip", 8479 - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", 8480 - "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", 8481 - "shasum": "" 8482 - }, 8483 - "require": { 8484 - "ext-dom": "*", 8485 - "ext-json": "*", 8486 - "ext-libxml": "*", 8487 - "ext-mbstring": "*", 8488 - "ext-xml": "*", 8489 - "ext-xmlwriter": "*", 8490 - "myclabs/deep-copy": "^1.13.4", 8491 - "phar-io/manifest": "^2.0.4", 8492 - "phar-io/version": "^3.2.1", 8493 - "php": ">=8.2", 8494 - "phpunit/php-code-coverage": "^11.0.11", 8495 - "phpunit/php-file-iterator": "^5.1.0", 8496 - "phpunit/php-invoker": "^5.0.1", 8497 - "phpunit/php-text-template": "^4.0.1", 8498 - "phpunit/php-timer": "^7.0.1", 8499 - "sebastian/cli-parser": "^3.0.2", 8500 - "sebastian/code-unit": "^3.0.3", 8501 - "sebastian/comparator": "^6.3.2", 8502 - "sebastian/diff": "^6.0.2", 8503 - "sebastian/environment": "^7.2.1", 8504 - "sebastian/exporter": "^6.3.2", 8505 - "sebastian/global-state": "^7.0.2", 8506 - "sebastian/object-enumerator": "^6.0.1", 8507 - "sebastian/type": "^5.1.3", 8508 - "sebastian/version": "^5.0.2", 8509 - "staabm/side-effects-detector": "^1.0.5" 8510 - }, 8511 - "suggest": { 8512 - "ext-soap": "To be able to generate mocks based on WSDL files" 8513 - }, 8514 - "bin": [ 8515 - "phpunit" 8516 - ], 8517 - "type": "library", 8518 - "extra": { 8519 - "branch-alias": { 8520 - "dev-main": "11.5-dev" 8521 - } 8522 - }, 8523 - "autoload": { 8524 - "files": [ 8525 - "src/Framework/Assert/Functions.php" 8526 - ], 8527 - "classmap": [ 8528 - "src/" 8529 - ] 8530 - }, 8531 - "notification-url": "https://packagist.org/downloads/", 8532 - "license": [ 8533 - "BSD-3-Clause" 8534 - ], 8535 - "authors": [ 8536 - { 8537 - "name": "Sebastian Bergmann", 8538 - "email": "sebastian@phpunit.de", 8539 - "role": "lead" 8540 - } 8541 - ], 8542 - "description": "The PHP Unit Testing framework.", 8543 - "homepage": "https://phpunit.de/", 8544 - "keywords": [ 8545 - "phpunit", 8546 - "testing", 8547 - "xunit" 8548 - ], 8549 - "support": { 8550 - "issues": "https://github.com/sebastianbergmann/phpunit/issues", 8551 - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 8552 - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.42" 8553 - }, 8554 - "funding": [ 8555 - { 8556 - "url": "https://phpunit.de/sponsors.html", 8557 - "type": "custom" 8558 - }, 8559 - { 8560 - "url": "https://github.com/sebastianbergmann", 8561 - "type": "github" 8562 - }, 8563 - { 8564 - "url": "https://liberapay.com/sebastianbergmann", 8565 - "type": "liberapay" 8566 - }, 8567 - { 8568 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 8569 - "type": "thanks_dev" 8570 - }, 8571 - { 8572 - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 8573 - "type": "tidelift" 8574 - } 8575 - ], 8576 - "time": "2025-09-28T12:09:13+00:00" 8577 - }, 8578 - { 8579 - "name": "psy/psysh", 8580 - "version": "v0.12.14", 8581 - "source": { 8582 - "type": "git", 8583 - "url": "https://github.com/bobthecow/psysh.git", 8584 - "reference": "95c29b3756a23855a30566b745d218bee690bef2" 8585 - }, 8586 - "dist": { 8587 - "type": "zip", 8588 - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", 8589 - "reference": "95c29b3756a23855a30566b745d218bee690bef2", 8590 - "shasum": "" 8591 - }, 8592 - "require": { 8593 - "ext-json": "*", 8594 - "ext-tokenizer": "*", 8595 - "nikic/php-parser": "^5.0 || ^4.0", 8596 - "php": "^8.0 || ^7.4", 8597 - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", 8598 - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" 8599 - }, 8600 - "conflict": { 8601 - "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" 8602 - }, 8603 - "require-dev": { 8604 - "bamarni/composer-bin-plugin": "^1.2", 8605 - "composer/class-map-generator": "^1.6" 8606 - }, 8607 - "suggest": { 8608 - "composer/class-map-generator": "Improved tab completion performance with better class discovery.", 8609 - "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", 8610 - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." 8611 - }, 8612 - "bin": [ 8613 - "bin/psysh" 8614 - ], 8615 - "type": "library", 8616 - "extra": { 8617 - "bamarni-bin": { 8618 - "bin-links": false, 8619 - "forward-command": false 8620 - }, 8621 - "branch-alias": { 8622 - "dev-main": "0.12.x-dev" 8623 - } 8624 - }, 8625 - "autoload": { 8626 - "files": [ 8627 - "src/functions.php" 8628 - ], 8629 - "psr-4": { 8630 - "Psy\\": "src/" 8631 - } 8632 - }, 8633 - "notification-url": "https://packagist.org/downloads/", 8634 - "license": [ 8635 - "MIT" 8636 - ], 8637 - "authors": [ 8638 - { 8639 - "name": "Justin Hileman", 8640 - "email": "justin@justinhileman.info" 8641 - } 8642 - ], 8643 - "description": "An interactive shell for modern PHP.", 8644 - "homepage": "https://psysh.org", 8645 - "keywords": [ 8646 - "REPL", 8647 - "console", 8648 - "interactive", 8649 - "shell" 8650 - ], 8651 - "support": { 8652 - "issues": "https://github.com/bobthecow/psysh/issues", 8653 - "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" 8654 - }, 8655 - "time": "2025-10-27T17:15:31+00:00" 8656 - }, 8657 - { 8658 - "name": "sebastian/cli-parser", 8659 - "version": "3.0.2", 8660 - "source": { 8661 - "type": "git", 8662 - "url": "https://github.com/sebastianbergmann/cli-parser.git", 8663 - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" 8664 - }, 8665 - "dist": { 8666 - "type": "zip", 8667 - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", 8668 - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", 8669 - "shasum": "" 8670 - }, 8671 - "require": { 8672 - "php": ">=8.2" 8673 - }, 8674 - "require-dev": { 8675 - "phpunit/phpunit": "^11.0" 8676 - }, 8677 - "type": "library", 8678 - "extra": { 8679 - "branch-alias": { 8680 - "dev-main": "3.0-dev" 8681 - } 8682 - }, 8683 - "autoload": { 8684 - "classmap": [ 8685 - "src/" 8686 - ] 8687 - }, 8688 - "notification-url": "https://packagist.org/downloads/", 8689 - "license": [ 8690 - "BSD-3-Clause" 8691 - ], 8692 - "authors": [ 8693 - { 8694 - "name": "Sebastian Bergmann", 8695 - "email": "sebastian@phpunit.de", 8696 - "role": "lead" 8697 - } 8698 - ], 8699 - "description": "Library for parsing CLI options", 8700 - "homepage": "https://github.com/sebastianbergmann/cli-parser", 8701 - "support": { 8702 - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 8703 - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", 8704 - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" 8705 - }, 8706 - "funding": [ 8707 - { 8708 - "url": "https://github.com/sebastianbergmann", 8709 - "type": "github" 8710 - } 8711 - ], 8712 - "time": "2024-07-03T04:41:36+00:00" 8713 - }, 8714 - { 8715 - "name": "sebastian/code-unit", 8716 - "version": "3.0.3", 8717 - "source": { 8718 - "type": "git", 8719 - "url": "https://github.com/sebastianbergmann/code-unit.git", 8720 - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" 8721 - }, 8722 - "dist": { 8723 - "type": "zip", 8724 - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", 8725 - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", 8726 - "shasum": "" 8727 - }, 8728 - "require": { 8729 - "php": ">=8.2" 8730 - }, 8731 - "require-dev": { 8732 - "phpunit/phpunit": "^11.5" 8733 - }, 8734 - "type": "library", 8735 - "extra": { 8736 - "branch-alias": { 8737 - "dev-main": "3.0-dev" 8738 - } 8739 - }, 8740 - "autoload": { 8741 - "classmap": [ 8742 - "src/" 8743 - ] 8744 - }, 8745 - "notification-url": "https://packagist.org/downloads/", 8746 - "license": [ 8747 - "BSD-3-Clause" 8748 - ], 8749 - "authors": [ 8750 - { 8751 - "name": "Sebastian Bergmann", 8752 - "email": "sebastian@phpunit.de", 8753 - "role": "lead" 8754 - } 8755 - ], 8756 - "description": "Collection of value objects that represent the PHP code units", 8757 - "homepage": "https://github.com/sebastianbergmann/code-unit", 8758 - "support": { 8759 - "issues": "https://github.com/sebastianbergmann/code-unit/issues", 8760 - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", 8761 - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" 8762 - }, 8763 - "funding": [ 8764 - { 8765 - "url": "https://github.com/sebastianbergmann", 8766 - "type": "github" 8767 - } 8768 - ], 8769 - "time": "2025-03-19T07:56:08+00:00" 8770 - }, 8771 - { 8772 - "name": "sebastian/code-unit-reverse-lookup", 8773 - "version": "4.0.1", 8774 - "source": { 8775 - "type": "git", 8776 - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 8777 - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" 8778 - }, 8779 - "dist": { 8780 - "type": "zip", 8781 - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", 8782 - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", 8783 - "shasum": "" 8784 - }, 8785 - "require": { 8786 - "php": ">=8.2" 8787 - }, 8788 - "require-dev": { 8789 - "phpunit/phpunit": "^11.0" 8790 - }, 8791 - "type": "library", 8792 - "extra": { 8793 - "branch-alias": { 8794 - "dev-main": "4.0-dev" 8795 - } 8796 - }, 8797 - "autoload": { 8798 - "classmap": [ 8799 - "src/" 8800 - ] 8801 - }, 8802 - "notification-url": "https://packagist.org/downloads/", 8803 - "license": [ 8804 - "BSD-3-Clause" 8805 - ], 8806 - "authors": [ 8807 - { 8808 - "name": "Sebastian Bergmann", 8809 - "email": "sebastian@phpunit.de" 8810 - } 8811 - ], 8812 - "description": "Looks up which function or method a line of code belongs to", 8813 - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 8814 - "support": { 8815 - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 8816 - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", 8817 - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" 8818 - }, 8819 - "funding": [ 8820 - { 8821 - "url": "https://github.com/sebastianbergmann", 8822 - "type": "github" 8823 - } 8824 - ], 8825 - "time": "2024-07-03T04:45:54+00:00" 8826 - }, 8827 - { 8828 - "name": "sebastian/comparator", 8829 - "version": "6.3.2", 8830 - "source": { 8831 - "type": "git", 8832 - "url": "https://github.com/sebastianbergmann/comparator.git", 8833 - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" 8834 - }, 8835 - "dist": { 8836 - "type": "zip", 8837 - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", 8838 - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", 8839 - "shasum": "" 8840 - }, 8841 - "require": { 8842 - "ext-dom": "*", 8843 - "ext-mbstring": "*", 8844 - "php": ">=8.2", 8845 - "sebastian/diff": "^6.0", 8846 - "sebastian/exporter": "^6.0" 8847 - }, 8848 - "require-dev": { 8849 - "phpunit/phpunit": "^11.4" 8850 - }, 8851 - "suggest": { 8852 - "ext-bcmath": "For comparing BcMath\\Number objects" 8853 - }, 8854 - "type": "library", 8855 - "extra": { 8856 - "branch-alias": { 8857 - "dev-main": "6.3-dev" 8858 - } 8859 - }, 8860 - "autoload": { 8861 - "classmap": [ 8862 - "src/" 8863 - ] 8864 - }, 8865 - "notification-url": "https://packagist.org/downloads/", 8866 - "license": [ 8867 - "BSD-3-Clause" 8868 - ], 8869 - "authors": [ 8870 - { 8871 - "name": "Sebastian Bergmann", 8872 - "email": "sebastian@phpunit.de" 8873 - }, 8874 - { 8875 - "name": "Jeff Welch", 8876 - "email": "whatthejeff@gmail.com" 8877 - }, 8878 - { 8879 - "name": "Volker Dusch", 8880 - "email": "github@wallbash.com" 8881 - }, 8882 - { 8883 - "name": "Bernhard Schussek", 8884 - "email": "bschussek@2bepublished.at" 8885 - } 8886 - ], 8887 - "description": "Provides the functionality to compare PHP values for equality", 8888 - "homepage": "https://github.com/sebastianbergmann/comparator", 8889 - "keywords": [ 8890 - "comparator", 8891 - "compare", 8892 - "equality" 8893 - ], 8894 - "support": { 8895 - "issues": "https://github.com/sebastianbergmann/comparator/issues", 8896 - "security": "https://github.com/sebastianbergmann/comparator/security/policy", 8897 - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" 8898 - }, 8899 - "funding": [ 8900 - { 8901 - "url": "https://github.com/sebastianbergmann", 8902 - "type": "github" 8903 - }, 8904 - { 8905 - "url": "https://liberapay.com/sebastianbergmann", 8906 - "type": "liberapay" 8907 - }, 8908 - { 8909 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 8910 - "type": "thanks_dev" 8911 - }, 8912 - { 8913 - "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", 8914 - "type": "tidelift" 8915 - } 8916 - ], 8917 - "time": "2025-08-10T08:07:46+00:00" 8918 - }, 8919 - { 8920 - "name": "sebastian/complexity", 8921 - "version": "4.0.1", 8922 - "source": { 8923 - "type": "git", 8924 - "url": "https://github.com/sebastianbergmann/complexity.git", 8925 - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" 8926 - }, 8927 - "dist": { 8928 - "type": "zip", 8929 - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", 8930 - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", 8931 - "shasum": "" 8932 - }, 8933 - "require": { 8934 - "nikic/php-parser": "^5.0", 8935 - "php": ">=8.2" 8936 - }, 8937 - "require-dev": { 8938 - "phpunit/phpunit": "^11.0" 8939 - }, 8940 - "type": "library", 8941 - "extra": { 8942 - "branch-alias": { 8943 - "dev-main": "4.0-dev" 8944 - } 8945 - }, 8946 - "autoload": { 8947 - "classmap": [ 8948 - "src/" 8949 - ] 8950 - }, 8951 - "notification-url": "https://packagist.org/downloads/", 8952 - "license": [ 8953 - "BSD-3-Clause" 8954 - ], 8955 - "authors": [ 8956 - { 8957 - "name": "Sebastian Bergmann", 8958 - "email": "sebastian@phpunit.de", 8959 - "role": "lead" 8960 - } 8961 - ], 8962 - "description": "Library for calculating the complexity of PHP code units", 8963 - "homepage": "https://github.com/sebastianbergmann/complexity", 8964 - "support": { 8965 - "issues": "https://github.com/sebastianbergmann/complexity/issues", 8966 - "security": "https://github.com/sebastianbergmann/complexity/security/policy", 8967 - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" 8968 - }, 8969 - "funding": [ 8970 - { 8971 - "url": "https://github.com/sebastianbergmann", 8972 - "type": "github" 8973 - } 8974 - ], 8975 - "time": "2024-07-03T04:49:50+00:00" 8976 - }, 8977 - { 8978 - "name": "sebastian/diff", 8979 - "version": "6.0.2", 8980 - "source": { 8981 - "type": "git", 8982 - "url": "https://github.com/sebastianbergmann/diff.git", 8983 - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" 8984 - }, 8985 - "dist": { 8986 - "type": "zip", 8987 - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", 8988 - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", 8989 - "shasum": "" 8990 - }, 8991 - "require": { 8992 - "php": ">=8.2" 8993 - }, 8994 - "require-dev": { 8995 - "phpunit/phpunit": "^11.0", 8996 - "symfony/process": "^4.2 || ^5" 8997 - }, 8998 - "type": "library", 8999 - "extra": { 9000 - "branch-alias": { 9001 - "dev-main": "6.0-dev" 9002 - } 9003 - }, 9004 - "autoload": { 9005 - "classmap": [ 9006 - "src/" 9007 - ] 9008 - }, 9009 - "notification-url": "https://packagist.org/downloads/", 9010 - "license": [ 9011 - "BSD-3-Clause" 9012 - ], 9013 - "authors": [ 9014 - { 9015 - "name": "Sebastian Bergmann", 9016 - "email": "sebastian@phpunit.de" 9017 - }, 9018 - { 9019 - "name": "Kore Nordmann", 9020 - "email": "mail@kore-nordmann.de" 9021 - } 9022 - ], 9023 - "description": "Diff implementation", 9024 - "homepage": "https://github.com/sebastianbergmann/diff", 9025 - "keywords": [ 9026 - "diff", 9027 - "udiff", 9028 - "unidiff", 9029 - "unified diff" 9030 - ], 9031 - "support": { 9032 - "issues": "https://github.com/sebastianbergmann/diff/issues", 9033 - "security": "https://github.com/sebastianbergmann/diff/security/policy", 9034 - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" 9035 - }, 9036 - "funding": [ 9037 - { 9038 - "url": "https://github.com/sebastianbergmann", 9039 - "type": "github" 9040 - } 9041 - ], 9042 - "time": "2024-07-03T04:53:05+00:00" 9043 - }, 9044 - { 9045 - "name": "sebastian/environment", 9046 - "version": "7.2.1", 9047 - "source": { 9048 - "type": "git", 9049 - "url": "https://github.com/sebastianbergmann/environment.git", 9050 - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" 9051 - }, 9052 - "dist": { 9053 - "type": "zip", 9054 - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", 9055 - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", 9056 - "shasum": "" 9057 - }, 9058 - "require": { 9059 - "php": ">=8.2" 9060 - }, 9061 - "require-dev": { 9062 - "phpunit/phpunit": "^11.3" 9063 - }, 9064 - "suggest": { 9065 - "ext-posix": "*" 9066 - }, 9067 - "type": "library", 9068 - "extra": { 9069 - "branch-alias": { 9070 - "dev-main": "7.2-dev" 9071 - } 9072 - }, 9073 - "autoload": { 9074 - "classmap": [ 9075 - "src/" 9076 - ] 9077 - }, 9078 - "notification-url": "https://packagist.org/downloads/", 9079 - "license": [ 9080 - "BSD-3-Clause" 9081 - ], 9082 - "authors": [ 9083 - { 9084 - "name": "Sebastian Bergmann", 9085 - "email": "sebastian@phpunit.de" 9086 - } 9087 - ], 9088 - "description": "Provides functionality to handle HHVM/PHP environments", 9089 - "homepage": "https://github.com/sebastianbergmann/environment", 9090 - "keywords": [ 9091 - "Xdebug", 9092 - "environment", 9093 - "hhvm" 9094 - ], 9095 - "support": { 9096 - "issues": "https://github.com/sebastianbergmann/environment/issues", 9097 - "security": "https://github.com/sebastianbergmann/environment/security/policy", 9098 - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" 9099 - }, 9100 - "funding": [ 9101 - { 9102 - "url": "https://github.com/sebastianbergmann", 9103 - "type": "github" 9104 - }, 9105 - { 9106 - "url": "https://liberapay.com/sebastianbergmann", 9107 - "type": "liberapay" 9108 - }, 9109 - { 9110 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 9111 - "type": "thanks_dev" 9112 - }, 9113 - { 9114 - "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", 9115 - "type": "tidelift" 9116 - } 9117 - ], 9118 - "time": "2025-05-21T11:55:47+00:00" 9119 - }, 9120 - { 9121 - "name": "sebastian/exporter", 9122 - "version": "6.3.2", 9123 - "source": { 9124 - "type": "git", 9125 - "url": "https://github.com/sebastianbergmann/exporter.git", 9126 - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" 9127 - }, 9128 - "dist": { 9129 - "type": "zip", 9130 - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", 9131 - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", 9132 - "shasum": "" 9133 - }, 9134 - "require": { 9135 - "ext-mbstring": "*", 9136 - "php": ">=8.2", 9137 - "sebastian/recursion-context": "^6.0" 9138 - }, 9139 - "require-dev": { 9140 - "phpunit/phpunit": "^11.3" 9141 - }, 9142 - "type": "library", 9143 - "extra": { 9144 - "branch-alias": { 9145 - "dev-main": "6.3-dev" 9146 - } 9147 - }, 9148 - "autoload": { 9149 - "classmap": [ 9150 - "src/" 9151 - ] 9152 - }, 9153 - "notification-url": "https://packagist.org/downloads/", 9154 - "license": [ 9155 - "BSD-3-Clause" 9156 - ], 9157 - "authors": [ 9158 - { 9159 - "name": "Sebastian Bergmann", 9160 - "email": "sebastian@phpunit.de" 9161 - }, 9162 - { 9163 - "name": "Jeff Welch", 9164 - "email": "whatthejeff@gmail.com" 9165 - }, 9166 - { 9167 - "name": "Volker Dusch", 9168 - "email": "github@wallbash.com" 9169 - }, 9170 - { 9171 - "name": "Adam Harvey", 9172 - "email": "aharvey@php.net" 9173 - }, 9174 - { 9175 - "name": "Bernhard Schussek", 9176 - "email": "bschussek@gmail.com" 9177 - } 9178 - ], 9179 - "description": "Provides the functionality to export PHP variables for visualization", 9180 - "homepage": "https://www.github.com/sebastianbergmann/exporter", 9181 - "keywords": [ 9182 - "export", 9183 - "exporter" 9184 - ], 9185 - "support": { 9186 - "issues": "https://github.com/sebastianbergmann/exporter/issues", 9187 - "security": "https://github.com/sebastianbergmann/exporter/security/policy", 9188 - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" 9189 - }, 9190 - "funding": [ 9191 - { 9192 - "url": "https://github.com/sebastianbergmann", 9193 - "type": "github" 9194 - }, 9195 - { 9196 - "url": "https://liberapay.com/sebastianbergmann", 9197 - "type": "liberapay" 9198 - }, 9199 - { 9200 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 9201 - "type": "thanks_dev" 9202 - }, 9203 - { 9204 - "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", 9205 - "type": "tidelift" 9206 - } 9207 - ], 9208 - "time": "2025-09-24T06:12:51+00:00" 9209 - }, 9210 - { 9211 - "name": "sebastian/global-state", 9212 - "version": "7.0.2", 9213 - "source": { 9214 - "type": "git", 9215 - "url": "https://github.com/sebastianbergmann/global-state.git", 9216 - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" 9217 - }, 9218 - "dist": { 9219 - "type": "zip", 9220 - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", 9221 - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", 9222 - "shasum": "" 9223 - }, 9224 - "require": { 9225 - "php": ">=8.2", 9226 - "sebastian/object-reflector": "^4.0", 9227 - "sebastian/recursion-context": "^6.0" 9228 - }, 9229 - "require-dev": { 9230 - "ext-dom": "*", 9231 - "phpunit/phpunit": "^11.0" 9232 - }, 9233 - "type": "library", 9234 - "extra": { 9235 - "branch-alias": { 9236 - "dev-main": "7.0-dev" 9237 - } 9238 - }, 9239 - "autoload": { 9240 - "classmap": [ 9241 - "src/" 9242 - ] 9243 - }, 9244 - "notification-url": "https://packagist.org/downloads/", 9245 - "license": [ 9246 - "BSD-3-Clause" 9247 - ], 9248 - "authors": [ 9249 - { 9250 - "name": "Sebastian Bergmann", 9251 - "email": "sebastian@phpunit.de" 9252 - } 9253 - ], 9254 - "description": "Snapshotting of global state", 9255 - "homepage": "https://www.github.com/sebastianbergmann/global-state", 9256 - "keywords": [ 9257 - "global state" 9258 - ], 9259 - "support": { 9260 - "issues": "https://github.com/sebastianbergmann/global-state/issues", 9261 - "security": "https://github.com/sebastianbergmann/global-state/security/policy", 9262 - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" 9263 - }, 9264 - "funding": [ 9265 - { 9266 - "url": "https://github.com/sebastianbergmann", 9267 - "type": "github" 9268 - } 9269 - ], 9270 - "time": "2024-07-03T04:57:36+00:00" 9271 - }, 9272 - { 9273 - "name": "sebastian/lines-of-code", 9274 - "version": "3.0.1", 9275 - "source": { 9276 - "type": "git", 9277 - "url": "https://github.com/sebastianbergmann/lines-of-code.git", 9278 - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" 9279 - }, 9280 - "dist": { 9281 - "type": "zip", 9282 - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", 9283 - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", 9284 - "shasum": "" 9285 - }, 9286 - "require": { 9287 - "nikic/php-parser": "^5.0", 9288 - "php": ">=8.2" 9289 - }, 9290 - "require-dev": { 9291 - "phpunit/phpunit": "^11.0" 9292 - }, 9293 - "type": "library", 9294 - "extra": { 9295 - "branch-alias": { 9296 - "dev-main": "3.0-dev" 9297 - } 9298 - }, 9299 - "autoload": { 9300 - "classmap": [ 9301 - "src/" 9302 - ] 9303 - }, 9304 - "notification-url": "https://packagist.org/downloads/", 9305 - "license": [ 9306 - "BSD-3-Clause" 9307 - ], 9308 - "authors": [ 9309 - { 9310 - "name": "Sebastian Bergmann", 9311 - "email": "sebastian@phpunit.de", 9312 - "role": "lead" 9313 - } 9314 - ], 9315 - "description": "Library for counting the lines of code in PHP source code", 9316 - "homepage": "https://github.com/sebastianbergmann/lines-of-code", 9317 - "support": { 9318 - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 9319 - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", 9320 - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" 9321 - }, 9322 - "funding": [ 9323 - { 9324 - "url": "https://github.com/sebastianbergmann", 9325 - "type": "github" 9326 - } 9327 - ], 9328 - "time": "2024-07-03T04:58:38+00:00" 9329 - }, 9330 - { 9331 - "name": "sebastian/object-enumerator", 9332 - "version": "6.0.1", 9333 - "source": { 9334 - "type": "git", 9335 - "url": "https://github.com/sebastianbergmann/object-enumerator.git", 9336 - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" 9337 - }, 9338 - "dist": { 9339 - "type": "zip", 9340 - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", 9341 - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", 9342 - "shasum": "" 9343 - }, 9344 - "require": { 9345 - "php": ">=8.2", 9346 - "sebastian/object-reflector": "^4.0", 9347 - "sebastian/recursion-context": "^6.0" 9348 - }, 9349 - "require-dev": { 9350 - "phpunit/phpunit": "^11.0" 9351 - }, 9352 - "type": "library", 9353 - "extra": { 9354 - "branch-alias": { 9355 - "dev-main": "6.0-dev" 9356 - } 9357 - }, 9358 - "autoload": { 9359 - "classmap": [ 9360 - "src/" 9361 - ] 9362 - }, 9363 - "notification-url": "https://packagist.org/downloads/", 9364 - "license": [ 9365 - "BSD-3-Clause" 9366 - ], 9367 - "authors": [ 9368 - { 9369 - "name": "Sebastian Bergmann", 9370 - "email": "sebastian@phpunit.de" 9371 - } 9372 - ], 9373 - "description": "Traverses array structures and object graphs to enumerate all referenced objects", 9374 - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 9375 - "support": { 9376 - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 9377 - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", 9378 - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" 9379 - }, 9380 - "funding": [ 9381 - { 9382 - "url": "https://github.com/sebastianbergmann", 9383 - "type": "github" 9384 - } 9385 - ], 9386 - "time": "2024-07-03T05:00:13+00:00" 9387 - }, 9388 - { 9389 - "name": "sebastian/object-reflector", 9390 - "version": "4.0.1", 9391 - "source": { 9392 - "type": "git", 9393 - "url": "https://github.com/sebastianbergmann/object-reflector.git", 9394 - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" 9395 - }, 9396 - "dist": { 9397 - "type": "zip", 9398 - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", 9399 - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", 9400 - "shasum": "" 9401 - }, 9402 - "require": { 9403 - "php": ">=8.2" 9404 - }, 9405 - "require-dev": { 9406 - "phpunit/phpunit": "^11.0" 9407 - }, 9408 - "type": "library", 9409 - "extra": { 9410 - "branch-alias": { 9411 - "dev-main": "4.0-dev" 9412 - } 9413 - }, 9414 - "autoload": { 9415 - "classmap": [ 9416 - "src/" 9417 - ] 9418 - }, 9419 - "notification-url": "https://packagist.org/downloads/", 9420 - "license": [ 9421 - "BSD-3-Clause" 9422 - ], 9423 - "authors": [ 9424 - { 9425 - "name": "Sebastian Bergmann", 9426 - "email": "sebastian@phpunit.de" 9427 - } 9428 - ], 9429 - "description": "Allows reflection of object attributes, including inherited and non-public ones", 9430 - "homepage": "https://github.com/sebastianbergmann/object-reflector/", 9431 - "support": { 9432 - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 9433 - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", 9434 - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" 9435 - }, 9436 - "funding": [ 9437 - { 9438 - "url": "https://github.com/sebastianbergmann", 9439 - "type": "github" 9440 - } 9441 - ], 9442 - "time": "2024-07-03T05:01:32+00:00" 9443 - }, 9444 - { 9445 - "name": "sebastian/recursion-context", 9446 - "version": "6.0.3", 9447 - "source": { 9448 - "type": "git", 9449 - "url": "https://github.com/sebastianbergmann/recursion-context.git", 9450 - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" 9451 - }, 9452 - "dist": { 9453 - "type": "zip", 9454 - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", 9455 - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", 9456 - "shasum": "" 9457 - }, 9458 - "require": { 9459 - "php": ">=8.2" 9460 - }, 9461 - "require-dev": { 9462 - "phpunit/phpunit": "^11.3" 9463 - }, 9464 - "type": "library", 9465 - "extra": { 9466 - "branch-alias": { 9467 - "dev-main": "6.0-dev" 9468 - } 9469 - }, 9470 - "autoload": { 9471 - "classmap": [ 9472 - "src/" 9473 - ] 9474 - }, 9475 - "notification-url": "https://packagist.org/downloads/", 9476 - "license": [ 9477 - "BSD-3-Clause" 9478 - ], 9479 - "authors": [ 9480 - { 9481 - "name": "Sebastian Bergmann", 9482 - "email": "sebastian@phpunit.de" 9483 - }, 9484 - { 9485 - "name": "Jeff Welch", 9486 - "email": "whatthejeff@gmail.com" 9487 - }, 9488 - { 9489 - "name": "Adam Harvey", 9490 - "email": "aharvey@php.net" 9491 - } 9492 - ], 9493 - "description": "Provides functionality to recursively process PHP variables", 9494 - "homepage": "https://github.com/sebastianbergmann/recursion-context", 9495 - "support": { 9496 - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 9497 - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", 9498 - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" 9499 - }, 9500 - "funding": [ 9501 - { 9502 - "url": "https://github.com/sebastianbergmann", 9503 - "type": "github" 9504 - }, 9505 - { 9506 - "url": "https://liberapay.com/sebastianbergmann", 9507 - "type": "liberapay" 9508 - }, 9509 - { 9510 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 9511 - "type": "thanks_dev" 9512 - }, 9513 - { 9514 - "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", 9515 - "type": "tidelift" 9516 - } 9517 - ], 9518 - "time": "2025-08-13T04:42:22+00:00" 9519 - }, 9520 - { 9521 - "name": "sebastian/type", 9522 - "version": "5.1.3", 9523 - "source": { 9524 - "type": "git", 9525 - "url": "https://github.com/sebastianbergmann/type.git", 9526 - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" 9527 - }, 9528 - "dist": { 9529 - "type": "zip", 9530 - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", 9531 - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", 9532 - "shasum": "" 9533 - }, 9534 - "require": { 9535 - "php": ">=8.2" 9536 - }, 9537 - "require-dev": { 9538 - "phpunit/phpunit": "^11.3" 9539 - }, 9540 - "type": "library", 9541 - "extra": { 9542 - "branch-alias": { 9543 - "dev-main": "5.1-dev" 9544 - } 9545 - }, 9546 - "autoload": { 9547 - "classmap": [ 9548 - "src/" 9549 - ] 9550 - }, 9551 - "notification-url": "https://packagist.org/downloads/", 9552 - "license": [ 9553 - "BSD-3-Clause" 9554 - ], 9555 - "authors": [ 9556 - { 9557 - "name": "Sebastian Bergmann", 9558 - "email": "sebastian@phpunit.de", 9559 - "role": "lead" 9560 - } 9561 - ], 9562 - "description": "Collection of value objects that represent the types of the PHP type system", 9563 - "homepage": "https://github.com/sebastianbergmann/type", 9564 - "support": { 9565 - "issues": "https://github.com/sebastianbergmann/type/issues", 9566 - "security": "https://github.com/sebastianbergmann/type/security/policy", 9567 - "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" 9568 - }, 9569 - "funding": [ 9570 - { 9571 - "url": "https://github.com/sebastianbergmann", 9572 - "type": "github" 9573 - }, 9574 - { 9575 - "url": "https://liberapay.com/sebastianbergmann", 9576 - "type": "liberapay" 9577 - }, 9578 - { 9579 - "url": "https://thanks.dev/u/gh/sebastianbergmann", 9580 - "type": "thanks_dev" 9581 - }, 9582 - { 9583 - "url": "https://tidelift.com/funding/github/packagist/sebastian/type", 9584 - "type": "tidelift" 9585 - } 9586 - ], 9587 - "time": "2025-08-09T06:55:48+00:00" 9588 - }, 9589 - { 9590 - "name": "sebastian/version", 9591 - "version": "5.0.2", 9592 - "source": { 9593 - "type": "git", 9594 - "url": "https://github.com/sebastianbergmann/version.git", 9595 - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" 9596 - }, 9597 - "dist": { 9598 - "type": "zip", 9599 - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", 9600 - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", 9601 - "shasum": "" 9602 - }, 9603 - "require": { 9604 - "php": ">=8.2" 9605 - }, 9606 - "type": "library", 9607 - "extra": { 9608 - "branch-alias": { 9609 - "dev-main": "5.0-dev" 9610 - } 9611 - }, 9612 - "autoload": { 9613 - "classmap": [ 9614 - "src/" 9615 - ] 9616 - }, 9617 - "notification-url": "https://packagist.org/downloads/", 9618 - "license": [ 9619 - "BSD-3-Clause" 9620 - ], 9621 - "authors": [ 9622 - { 9623 - "name": "Sebastian Bergmann", 9624 - "email": "sebastian@phpunit.de", 9625 - "role": "lead" 9626 - } 9627 - ], 9628 - "description": "Library that helps with managing the version number of Git-hosted PHP projects", 9629 - "homepage": "https://github.com/sebastianbergmann/version", 9630 - "support": { 9631 - "issues": "https://github.com/sebastianbergmann/version/issues", 9632 - "security": "https://github.com/sebastianbergmann/version/security/policy", 9633 - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" 9634 - }, 9635 - "funding": [ 9636 - { 9637 - "url": "https://github.com/sebastianbergmann", 9638 - "type": "github" 9639 - } 9640 - ], 9641 - "time": "2024-10-09T05:16:32+00:00" 9642 - }, 9643 - { 9644 - "name": "staabm/side-effects-detector", 9645 - "version": "1.0.5", 9646 - "source": { 9647 - "type": "git", 9648 - "url": "https://github.com/staabm/side-effects-detector.git", 9649 - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" 9650 - }, 9651 - "dist": { 9652 - "type": "zip", 9653 - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", 9654 - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", 9655 - "shasum": "" 9656 - }, 9657 - "require": { 9658 - "ext-tokenizer": "*", 9659 - "php": "^7.4 || ^8.0" 9660 - }, 9661 - "require-dev": { 9662 - "phpstan/extension-installer": "^1.4.3", 9663 - "phpstan/phpstan": "^1.12.6", 9664 - "phpunit/phpunit": "^9.6.21", 9665 - "symfony/var-dumper": "^5.4.43", 9666 - "tomasvotruba/type-coverage": "1.0.0", 9667 - "tomasvotruba/unused-public": "1.0.0" 9668 - }, 9669 - "type": "library", 9670 - "autoload": { 9671 - "classmap": [ 9672 - "lib/" 9673 - ] 9674 - }, 9675 - "notification-url": "https://packagist.org/downloads/", 9676 - "license": [ 9677 - "MIT" 9678 - ], 9679 - "description": "A static analysis tool to detect side effects in PHP code", 9680 - "keywords": [ 9681 - "static analysis" 9682 - ], 9683 - "support": { 9684 - "issues": "https://github.com/staabm/side-effects-detector/issues", 9685 - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" 9686 - }, 9687 - "funding": [ 9688 - { 9689 - "url": "https://github.com/staabm", 9690 - "type": "github" 9691 - } 9692 - ], 9693 - "time": "2024-10-20T05:08:20+00:00" 9694 - }, 9695 - { 9696 - "name": "symfony/polyfill-php84", 9697 - "version": "v1.33.0", 9698 - "source": { 9699 - "type": "git", 9700 - "url": "https://github.com/symfony/polyfill-php84.git", 9701 - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" 9702 - }, 9703 - "dist": { 9704 - "type": "zip", 9705 - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", 9706 - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", 9707 - "shasum": "" 9708 - }, 9709 - "require": { 9710 - "php": ">=7.2" 9711 - }, 9712 - "type": "library", 9713 - "extra": { 9714 - "thanks": { 9715 - "url": "https://github.com/symfony/polyfill", 9716 - "name": "symfony/polyfill" 9717 - } 9718 - }, 9719 - "autoload": { 9720 - "files": [ 9721 - "bootstrap.php" 9722 - ], 9723 - "psr-4": { 9724 - "Symfony\\Polyfill\\Php84\\": "" 9725 - }, 9726 - "classmap": [ 9727 - "Resources/stubs" 9728 - ] 9729 - }, 9730 - "notification-url": "https://packagist.org/downloads/", 9731 - "license": [ 9732 - "MIT" 9733 - ], 9734 - "authors": [ 9735 - { 9736 - "name": "Nicolas Grekas", 9737 - "email": "p@tchwork.com" 9738 - }, 9739 - { 9740 - "name": "Symfony Community", 9741 - "homepage": "https://symfony.com/contributors" 9742 - } 9743 - ], 9744 - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", 9745 - "homepage": "https://symfony.com", 9746 - "keywords": [ 9747 - "compatibility", 9748 - "polyfill", 9749 - "portable", 9750 - "shim" 9751 - ], 9752 - "support": { 9753 - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" 9754 - }, 9755 - "funding": [ 9756 - { 9757 - "url": "https://symfony.com/sponsor", 9758 - "type": "custom" 9759 - }, 9760 - { 9761 - "url": "https://github.com/fabpot", 9762 - "type": "github" 9763 - }, 9764 - { 9765 - "url": "https://github.com/nicolas-grekas", 9766 - "type": "github" 9767 - }, 9768 - { 9769 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 9770 - "type": "tidelift" 9771 - } 9772 - ], 9773 - "time": "2025-06-24T13:30:11+00:00" 9774 - }, 9775 - { 9776 - "name": "symfony/yaml", 9777 - "version": "v7.3.5", 9778 - "source": { 9779 - "type": "git", 9780 - "url": "https://github.com/symfony/yaml.git", 9781 - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" 9782 - }, 9783 - "dist": { 9784 - "type": "zip", 9785 - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", 9786 - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", 9787 - "shasum": "" 9788 - }, 9789 - "require": { 9790 - "php": ">=8.2", 9791 - "symfony/deprecation-contracts": "^2.5|^3.0", 9792 - "symfony/polyfill-ctype": "^1.8" 9793 - }, 9794 - "conflict": { 9795 - "symfony/console": "<6.4" 9796 - }, 9797 - "require-dev": { 9798 - "symfony/console": "^6.4|^7.0" 9799 - }, 9800 - "bin": [ 9801 - "Resources/bin/yaml-lint" 9802 - ], 9803 - "type": "library", 9804 - "autoload": { 9805 - "psr-4": { 9806 - "Symfony\\Component\\Yaml\\": "" 9807 - }, 9808 - "exclude-from-classmap": [ 9809 - "/Tests/" 9810 - ] 9811 - }, 9812 - "notification-url": "https://packagist.org/downloads/", 9813 - "license": [ 9814 - "MIT" 9815 - ], 9816 - "authors": [ 9817 - { 9818 - "name": "Fabien Potencier", 9819 - "email": "fabien@symfony.com" 9820 - }, 9821 - { 9822 - "name": "Symfony Community", 9823 - "homepage": "https://symfony.com/contributors" 9824 - } 9825 - ], 9826 - "description": "Loads and dumps YAML files", 9827 - "homepage": "https://symfony.com", 9828 - "support": { 9829 - "source": "https://github.com/symfony/yaml/tree/v7.3.5" 9830 - }, 9831 - "funding": [ 9832 - { 9833 - "url": "https://symfony.com/sponsor", 9834 - "type": "custom" 9835 - }, 9836 - { 9837 - "url": "https://github.com/fabpot", 9838 - "type": "github" 9839 - }, 9840 - { 9841 - "url": "https://github.com/nicolas-grekas", 9842 - "type": "github" 9843 - }, 9844 - { 9845 - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 9846 - "type": "tidelift" 9847 - } 9848 - ], 9849 - "time": "2025-09-27T09:00:46+00:00" 9850 - }, 9851 - { 9852 - "name": "theseer/tokenizer", 9853 - "version": "1.2.3", 9854 - "source": { 9855 - "type": "git", 9856 - "url": "https://github.com/theseer/tokenizer.git", 9857 - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" 9858 - }, 9859 - "dist": { 9860 - "type": "zip", 9861 - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 9862 - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 9863 - "shasum": "" 9864 - }, 9865 - "require": { 9866 - "ext-dom": "*", 9867 - "ext-tokenizer": "*", 9868 - "ext-xmlwriter": "*", 9869 - "php": "^7.2 || ^8.0" 9870 - }, 9871 - "type": "library", 9872 - "autoload": { 9873 - "classmap": [ 9874 - "src/" 9875 - ] 9876 - }, 9877 - "notification-url": "https://packagist.org/downloads/", 9878 - "license": [ 9879 - "BSD-3-Clause" 9880 - ], 9881 - "authors": [ 9882 - { 9883 - "name": "Arne Blankerts", 9884 - "email": "arne@blankerts.de", 9885 - "role": "Developer" 9886 - } 9887 - ], 9888 - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 9889 - "support": { 9890 - "issues": "https://github.com/theseer/tokenizer/issues", 9891 - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" 9892 - }, 9893 - "funding": [ 9894 - { 9895 - "url": "https://github.com/theseer", 9896 - "type": "github" 9897 - } 9898 - ], 9899 - "time": "2024-03-03T12:36:25+00:00" 9900 - } 9901 - ], 9902 - "aliases": [], 9903 - "minimum-stability": "stable", 9904 - "stability-flags": {}, 9905 - "prefer-stable": false, 9906 - "prefer-lowest": false, 9907 - "platform": { 9908 - "php": "^8.2" 9909 - }, 9910 - "platform-dev": {}, 9911 - "plugin-api-version": "2.6.0" 9912 - }
···
+3 -3
config/signal.php
··· 9 | The mode determines which AT Protocol stream to consume: 10 | - 'jetstream': JSON events with server-side collection filtering 11 | (only standard app.bsky.* collections get create/update) 12 - | - 'firehose': Raw CBOR events with client-side filtering 13 - | (all collections including custom ones get all operations) 14 | 15 */ 16 'mode' => env('SIGNAL_MODE', 'jetstream'), ··· 80 | 81 */ 82 'signals' => [ 83 - // App\Signals\NewPostSignal::class, 84 ], 85 86 /*
··· 9 | The mode determines which AT Protocol stream to consume: 10 | - 'jetstream': JSON events with server-side collection filtering 11 | (only standard app.bsky.* collections get create/update) 12 + | - 'firehose': Raw CBOR events with client-side filtering 13 + | (all collections including custom ones get all operations) 14 | 15 */ 16 'mode' => env('SIGNAL_MODE', 'jetstream'), ··· 80 | 81 */ 82 'signals' => [ 83 + // App\Signals\PostCreateSignal::class, 84 ], 85 86 /*
+687
docs/configuration.md
···
··· 1 + # Configuration 2 + 3 + Signal's configuration file provides complete control over how your application consumes AT Protocol events. 4 + 5 + ## Configuration File 6 + 7 + After installation, configuration lives in `config/signal.php`. 8 + 9 + ### Publishing Configuration 10 + 11 + Publish the config file manually if needed: 12 + 13 + ```bash 14 + php artisan vendor:publish --tag=signal-config 15 + ``` 16 + 17 + This creates `config/signal.php` with all available options. 18 + 19 + ## Environment Variables 20 + 21 + Most configuration can be set via `.env` for environment-specific values. 22 + 23 + ### Basic Configuration 24 + 25 + ```env 26 + # Consumer Mode (jetstream or firehose) 27 + SIGNAL_MODE=jetstream 28 + 29 + # Jetstream Configuration 30 + SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 31 + 32 + # Firehose Configuration 33 + SIGNAL_FIREHOSE_HOST=bsky.network 34 + 35 + # Cursor Storage (database, redis, or file) 36 + SIGNAL_CURSOR_STORAGE=database 37 + 38 + # Queue Configuration 39 + SIGNAL_QUEUE_CONNECTION=redis 40 + SIGNAL_QUEUE=signal 41 + ``` 42 + 43 + ## Consumer Mode 44 + 45 + Choose between Jetstream and Firehose mode. 46 + 47 + ### Configuration 48 + 49 + ```php 50 + 'mode' => env('SIGNAL_MODE', 'jetstream'), 51 + ``` 52 + 53 + **Options:** 54 + - `jetstream` - JSON events, server-side filtering (default) 55 + - `firehose` - CBOR/CAR events, client-side filtering 56 + 57 + **Environment Variable:** 58 + ```env 59 + SIGNAL_MODE=jetstream 60 + ``` 61 + 62 + **When to use each:** 63 + - **Jetstream**: Standard Bluesky collections, production efficiency 64 + - **Firehose**: Custom collections, AppViews, comprehensive indexing 65 + 66 + [Learn more about modes โ†’](modes.md) 67 + 68 + ## Jetstream Configuration 69 + 70 + Configuration specific to Jetstream mode. 71 + 72 + ### WebSocket URL 73 + 74 + ```php 75 + 'jetstream' => [ 76 + 'websocket_url' => env( 77 + 'SIGNAL_JETSTREAM_URL', 78 + 'wss://jetstream2.us-east.bsky.network' 79 + ), 80 + ], 81 + ``` 82 + 83 + **Available Endpoints:** 84 + 85 + **US East (Default):** 86 + ```env 87 + SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 88 + ``` 89 + 90 + **US West:** 91 + ```env 92 + SIGNAL_JETSTREAM_URL=wss://jetstream1.us-west.bsky.network 93 + ``` 94 + 95 + Choose the endpoint closest to your server for best latency. 96 + 97 + ## Firehose Configuration 98 + 99 + Configuration specific to Firehose mode. 100 + 101 + ### Host 102 + 103 + ```php 104 + 'firehose' => [ 105 + 'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'), 106 + ], 107 + ``` 108 + 109 + The WebSocket URL is constructed as: 110 + ``` 111 + wss://{host}/xrpc/com.atproto.sync.subscribeRepos 112 + ``` 113 + 114 + **Environment Variable:** 115 + ```env 116 + SIGNAL_FIREHOSE_HOST=bsky.network 117 + ``` 118 + 119 + **Default Host:** `bsky.network` 120 + 121 + **Custom Hosts:** If you're running your own AT Protocol PDS, specify it here: 122 + ```env 123 + SIGNAL_FIREHOSE_HOST=my-pds.example.com 124 + ``` 125 + 126 + ## Cursor Storage 127 + 128 + Configure how Signal stores cursor positions for resuming after disconnections. 129 + 130 + ### Storage Driver 131 + 132 + ```php 133 + 'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'), 134 + ``` 135 + 136 + **Available Drivers:** 137 + - `database` - Store in database (recommended for production) 138 + - `redis` - Store in Redis (high performance) 139 + - `file` - Store in filesystem (development only) 140 + 141 + **Environment Variable:** 142 + ```env 143 + SIGNAL_CURSOR_STORAGE=database 144 + ``` 145 + 146 + ### Database Driver 147 + 148 + Uses Laravel's default database connection. 149 + 150 + **Configuration:** 151 + ```php 152 + 'cursor_storage' => 'database', 153 + ``` 154 + 155 + **Requires:** 156 + - Migration published and run 157 + - Database connection configured 158 + 159 + **Table:** `signal_cursors` 160 + 161 + ### Redis Driver 162 + 163 + Stores cursors in Redis for high performance. 164 + 165 + **Configuration:** 166 + ```php 167 + 'cursor_storage' => 'redis', 168 + 169 + 'redis' => [ 170 + 'connection' => env('SIGNAL_REDIS_CONNECTION', 'default'), 171 + 'key_prefix' => env('SIGNAL_REDIS_PREFIX', 'signal:cursor:'), 172 + ], 173 + ``` 174 + 175 + **Environment Variables:** 176 + ```env 177 + SIGNAL_CURSOR_STORAGE=redis 178 + SIGNAL_REDIS_CONNECTION=default 179 + SIGNAL_REDIS_PREFIX=signal:cursor: 180 + ``` 181 + 182 + **Requires:** 183 + - Redis connection configured in `config/database.php` 184 + - Redis server running 185 + 186 + ### File Driver 187 + 188 + Stores cursors in the filesystem (development only). 189 + 190 + **Configuration:** 191 + ```php 192 + 'cursor_storage' => 'file', 193 + 194 + 'file' => [ 195 + 'path' => env('SIGNAL_FILE_PATH', storage_path('app/signal')), 196 + ], 197 + ``` 198 + 199 + **Environment Variables:** 200 + ```env 201 + SIGNAL_CURSOR_STORAGE=file 202 + SIGNAL_FILE_PATH=/path/to/storage/signal 203 + ``` 204 + 205 + **Not recommended for production:** 206 + - Single server only 207 + - No clustering support 208 + - Filesystem I/O overhead 209 + 210 + ## Queue Configuration 211 + 212 + Configure how Signal dispatches queued jobs. 213 + 214 + ### Queue Connection 215 + 216 + ```php 217 + 'queue' => [ 218 + 'connection' => env('SIGNAL_QUEUE_CONNECTION', null), 219 + 'queue' => env('SIGNAL_QUEUE', 'default'), 220 + ], 221 + ``` 222 + 223 + **Environment Variables:** 224 + ```env 225 + # Queue connection (redis, database, sqs, etc.) 226 + SIGNAL_QUEUE_CONNECTION=redis 227 + 228 + # Queue name 229 + SIGNAL_QUEUE=signal 230 + ``` 231 + 232 + **Defaults:** 233 + - `connection`: Uses Laravel's default queue connection 234 + - `queue`: Uses Laravel's default queue name 235 + 236 + ### Per-Signal Configuration 237 + 238 + Signals can override queue configuration: 239 + 240 + ```php 241 + public function shouldQueue(): bool 242 + { 243 + return true; 244 + } 245 + 246 + public function queueConnection(): string 247 + { 248 + return 'redis'; // Override connection 249 + } 250 + 251 + public function queue(): string 252 + { 253 + return 'high-priority'; // Override queue name 254 + } 255 + ``` 256 + 257 + [Learn more about queue integration โ†’](queues.md) 258 + 259 + ## Auto-Discovery 260 + 261 + Configure automatic Signal discovery. 262 + 263 + ### Enable/Disable 264 + 265 + ```php 266 + 'auto_discovery' => [ 267 + 'enabled' => true, 268 + 'path' => app_path('Signals'), 269 + 'namespace' => 'App\\Signals', 270 + ], 271 + ``` 272 + 273 + **Options:** 274 + - `enabled`: Enable/disable auto-discovery (default: `true`) 275 + - `path`: Directory to scan for Signals (default: `app/Signals`) 276 + - `namespace`: Namespace for discovered Signals (default: `App\Signals`) 277 + 278 + ### Disable Auto-Discovery 279 + 280 + Manually register Signals instead: 281 + 282 + ```php 283 + 'auto_discovery' => [ 284 + 'enabled' => false, 285 + ], 286 + 287 + 'signals' => [ 288 + \App\Signals\NewPostSignal::class, 289 + \App\Signals\NewFollowSignal::class, 290 + ], 291 + ``` 292 + 293 + ### Custom Discovery Path 294 + 295 + Organize Signals in a custom directory: 296 + 297 + ```php 298 + 'auto_discovery' => [ 299 + 'enabled' => true, 300 + 'path' => app_path('Domain/Signals'), 301 + 'namespace' => 'App\\Domain\\Signals', 302 + ], 303 + ``` 304 + 305 + ## Manual Signal Registration 306 + 307 + Register Signals explicitly. 308 + 309 + ### Configuration 310 + 311 + ```php 312 + 'signals' => [ 313 + \App\Signals\NewPostSignal::class, 314 + \App\Signals\NewFollowSignal::class, 315 + \App\Signals\ProfileUpdateSignal::class, 316 + ], 317 + ``` 318 + 319 + **When to use:** 320 + - Auto-discovery disabled 321 + - Signals outside standard directory 322 + - Fine-grained control over which Signals run 323 + 324 + ## Logging 325 + 326 + Signal uses Laravel's logging system. 327 + 328 + ### Configure Logging 329 + 330 + Standard Laravel log configuration applies: 331 + 332 + ```php 333 + // config/logging.php 334 + 'channels' => [ 335 + 'signal' => [ 336 + 'driver' => 'daily', 337 + 'path' => storage_path('logs/signal.log'), 338 + 'level' => env('SIGNAL_LOG_LEVEL', 'debug'), 339 + 'days' => 14, 340 + ], 341 + ], 342 + ``` 343 + 344 + Use in Signals: 345 + 346 + ```php 347 + use Illuminate\Support\Facades\Log; 348 + 349 + public function handle(SignalEvent $event): void 350 + { 351 + Log::channel('signal')->info('Processing event', [ 352 + 'did' => $event->did, 353 + ]); 354 + } 355 + ``` 356 + 357 + ## Complete Configuration Reference 358 + 359 + Here's the full `config/signal.php` with all options: 360 + 361 + ```php 362 + <?php 363 + 364 + return [ 365 + 366 + /* 367 + |-------------------------------------------------------------------------- 368 + | Consumer Mode 369 + |-------------------------------------------------------------------------- 370 + | 371 + | Choose between 'jetstream' (JSON events) or 'firehose' (CBOR/CAR events). 372 + | Jetstream is more efficient for standard Bluesky collections. 373 + | Firehose is required for custom collections. 374 + | 375 + | Options: 'jetstream', 'firehose' 376 + | 377 + */ 378 + 379 + 'mode' => env('SIGNAL_MODE', 'jetstream'), 380 + 381 + /* 382 + |-------------------------------------------------------------------------- 383 + | Jetstream Configuration 384 + |-------------------------------------------------------------------------- 385 + | 386 + | Configuration for Jetstream mode (JSON events). 387 + | 388 + */ 389 + 390 + 'jetstream' => [ 391 + 'websocket_url' => env( 392 + 'SIGNAL_JETSTREAM_URL', 393 + 'wss://jetstream2.us-east.bsky.network' 394 + ), 395 + ], 396 + 397 + /* 398 + |-------------------------------------------------------------------------- 399 + | Firehose Configuration 400 + |-------------------------------------------------------------------------- 401 + | 402 + | Configuration for Firehose mode (CBOR/CAR events). 403 + | 404 + */ 405 + 406 + 'firehose' => [ 407 + 'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'), 408 + ], 409 + 410 + /* 411 + |-------------------------------------------------------------------------- 412 + | Cursor Storage 413 + |-------------------------------------------------------------------------- 414 + | 415 + | Configure how Signal stores cursor positions for resuming after 416 + | disconnections. Options: 'database', 'redis', 'file' 417 + | 418 + */ 419 + 420 + 'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'), 421 + 422 + /* 423 + |-------------------------------------------------------------------------- 424 + | Redis Configuration 425 + |-------------------------------------------------------------------------- 426 + | 427 + | Configuration for Redis cursor storage. 428 + | 429 + */ 430 + 431 + 'redis' => [ 432 + 'connection' => env('SIGNAL_REDIS_CONNECTION', 'default'), 433 + 'key_prefix' => env('SIGNAL_REDIS_PREFIX', 'signal:cursor:'), 434 + ], 435 + 436 + /* 437 + |-------------------------------------------------------------------------- 438 + | File Configuration 439 + |-------------------------------------------------------------------------- 440 + | 441 + | Configuration for file-based cursor storage. 442 + | 443 + */ 444 + 445 + 'file' => [ 446 + 'path' => env('SIGNAL_FILE_PATH', storage_path('app/signal')), 447 + ], 448 + 449 + /* 450 + |-------------------------------------------------------------------------- 451 + | Queue Configuration 452 + |-------------------------------------------------------------------------- 453 + | 454 + | Configure queue connection and name for processing events asynchronously. 455 + | 456 + */ 457 + 458 + 'queue' => [ 459 + 'connection' => env('SIGNAL_QUEUE_CONNECTION', null), 460 + 'queue' => env('SIGNAL_QUEUE', 'default'), 461 + ], 462 + 463 + /* 464 + |-------------------------------------------------------------------------- 465 + | Auto-Discovery 466 + |-------------------------------------------------------------------------- 467 + | 468 + | Automatically discover and register Signals from the specified directory. 469 + | 470 + */ 471 + 472 + 'auto_discovery' => [ 473 + 'enabled' => true, 474 + 'path' => app_path('Signals'), 475 + 'namespace' => 'App\\Signals', 476 + ], 477 + 478 + /* 479 + |-------------------------------------------------------------------------- 480 + | Manual Signal Registration 481 + |-------------------------------------------------------------------------- 482 + | 483 + | Manually register Signals if auto-discovery is disabled. 484 + | 485 + */ 486 + 487 + 'signals' => [ 488 + // \App\Signals\NewPostSignal::class, 489 + ], 490 + 491 + ]; 492 + ``` 493 + 494 + ## Environment-Specific Configuration 495 + 496 + ### Development 497 + 498 + ```env 499 + SIGNAL_MODE=firehose 500 + SIGNAL_CURSOR_STORAGE=file 501 + SIGNAL_QUEUE_CONNECTION=sync 502 + ``` 503 + 504 + **Why:** 505 + - Firehose mode sees all events (comprehensive testing) 506 + - File storage is simple and adequate 507 + - Sync queue processes immediately (easier debugging) 508 + 509 + ### Staging 510 + 511 + ```env 512 + SIGNAL_MODE=jetstream 513 + SIGNAL_CURSOR_STORAGE=redis 514 + SIGNAL_QUEUE_CONNECTION=redis 515 + SIGNAL_QUEUE=signal-staging 516 + ``` 517 + 518 + **Why:** 519 + - Jetstream mode matches production 520 + - Redis for performance testing 521 + - Separate queue for staging isolation 522 + 523 + ### Production 524 + 525 + ```env 526 + SIGNAL_MODE=jetstream 527 + SIGNAL_CURSOR_STORAGE=database 528 + SIGNAL_QUEUE_CONNECTION=redis 529 + SIGNAL_QUEUE=signal 530 + ``` 531 + 532 + **Why:** 533 + - Jetstream mode for efficiency 534 + - Database storage for reliability 535 + - Redis queues for performance 536 + 537 + ## Runtime Configuration 538 + 539 + Change configuration at runtime: 540 + 541 + ```php 542 + use SocialDept\AtpSignals\Facades\Signal; 543 + 544 + // Override mode 545 + config(['signal.mode' => 'firehose']); 546 + 547 + // Override cursor storage 548 + config(['signal.cursor_storage' => 'redis']); 549 + 550 + // Start consumer with new config 551 + Signal::start(); 552 + ``` 553 + 554 + ## Validation 555 + 556 + Signal validates configuration on startup: 557 + 558 + ```bash 559 + php artisan signal:consume 560 + ``` 561 + 562 + **Checks:** 563 + - Mode is valid (`jetstream` or `firehose`) 564 + - Cursor storage driver exists 565 + - Required endpoints are configured 566 + - Queue configuration is valid 567 + 568 + **Validation errors prevent consumer from starting.** 569 + 570 + ## Configuration Helpers 571 + 572 + ### Check Current Mode 573 + 574 + ```php 575 + $mode = config('signal.mode'); // 'jetstream' or 'firehose' 576 + ``` 577 + 578 + Or via Facade: 579 + 580 + ```php 581 + use SocialDept\AtpSignals\Facades\Signal; 582 + 583 + $mode = Signal::getMode(); 584 + ``` 585 + 586 + ### Check Cursor Storage 587 + 588 + ```php 589 + $storage = config('signal.cursor_storage'); // 'database', 'redis', or 'file' 590 + ``` 591 + 592 + ### Check Queue Configuration 593 + 594 + ```php 595 + $connection = config('signal.queue.connection'); 596 + $queue = config('signal.queue.queue'); 597 + ``` 598 + 599 + ## Best Practices 600 + 601 + ### Use Environment Variables 602 + 603 + Don't hardcode values in config file: 604 + 605 + ```php 606 + // Good 607 + 'mode' => env('SIGNAL_MODE', 'jetstream'), 608 + 609 + // Bad 610 + 'mode' => 'jetstream', 611 + ``` 612 + 613 + ### Separate Staging and Production 614 + 615 + Use different queues and storage: 616 + 617 + ```env 618 + # .env.staging 619 + SIGNAL_QUEUE=signal-staging 620 + 621 + # .env.production 622 + SIGNAL_QUEUE=signal-production 623 + ``` 624 + 625 + ### Document Custom Configuration 626 + 627 + If you change defaults, document why: 628 + 629 + ```php 630 + // We use Firehose mode because we have custom collections 631 + 'mode' => env('SIGNAL_MODE', 'firehose'), 632 + ``` 633 + 634 + ### Version Control 635 + 636 + Commit `config/signal.php` but not `.env`: 637 + 638 + ```gitignore 639 + # .gitignore 640 + .env 641 + .env.* 642 + 643 + # Commit 644 + config/signal.php 645 + ``` 646 + 647 + ## Troubleshooting 648 + 649 + ### Configuration Not Loading 650 + 651 + Clear config cache: 652 + 653 + ```bash 654 + php artisan config:clear 655 + php artisan config:cache 656 + ``` 657 + 658 + ### Environment Variables Not Working 659 + 660 + Check `.env` file exists and is readable: 661 + 662 + ```bash 663 + ls -la .env 664 + ``` 665 + 666 + Restart services after changing `.env`: 667 + 668 + ```bash 669 + # If using Supervisor 670 + sudo supervisorctl restart signal-consumer:* 671 + ``` 672 + 673 + ### Invalid Configuration 674 + 675 + Run consumer to see validation errors: 676 + 677 + ```bash 678 + php artisan signal:consume 679 + ``` 680 + 681 + Signal will display specific errors about misconfiguration. 682 + 683 + ## Next Steps 684 + 685 + - **[Learn about testing โ†’](testing.md)** - Test your configuration 686 + - **[See real-world examples โ†’](examples.md)** - Learn from production configurations 687 + - **[Review queue integration โ†’](queues.md)** - Configure queues optimally
+795
docs/examples.md
···
··· 1 + # Real-World Examples 2 + 3 + Learn from production-ready Signal examples covering common use cases. 4 + 5 + ## Social Media Analytics 6 + 7 + Track engagement metrics across Bluesky. 8 + 9 + ```php 10 + <?php 11 + 12 + namespace App\Signals; 13 + 14 + use App\Models\EngagementMetric; 15 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 16 + use SocialDept\AtpSignals\Events\SignalEvent; 17 + use SocialDept\AtpSignals\Signals\Signal; 18 + use Illuminate\Support\Facades\DB; 19 + 20 + class EngagementTrackerSignal extends Signal 21 + { 22 + public function eventTypes(): array 23 + { 24 + return ['commit']; 25 + } 26 + 27 + public function collections(): ?array 28 + { 29 + return [ 30 + 'app.bsky.feed.post', 31 + 'app.bsky.feed.like', 32 + 'app.bsky.feed.repost', 33 + 'app.bsky.graph.follow', 34 + ]; 35 + } 36 + 37 + public function operations(): ?array 38 + { 39 + return [SignalCommitOperation::Create]; 40 + } 41 + 42 + public function shouldQueue(): bool 43 + { 44 + return true; 45 + } 46 + 47 + public function handle(SignalEvent $event): void 48 + { 49 + $collection = $event->getCollection(); 50 + $timestamp = $event->getTimestamp(); 51 + 52 + // Increment counter for this hour 53 + DB::table('engagement_metrics') 54 + ->updateOrInsert( 55 + [ 56 + 'collection' => $collection, 57 + 'hour' => $timestamp->startOfHour(), 58 + ], 59 + [ 60 + 'count' => DB::raw('count + 1'), 61 + 'updated_at' => now(), 62 + ] 63 + ); 64 + } 65 + } 66 + ``` 67 + 68 + **Use case:** Build analytics dashboards showing posts/hour, likes/hour, follows/hour. 69 + 70 + ## Content Moderation 71 + 72 + Automatically flag problematic content. 73 + 74 + ```php 75 + <?php 76 + 77 + namespace App\Signals; 78 + 79 + use App\Models\FlaggedPost; 80 + use App\Services\ModerationService; 81 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 82 + use SocialDept\AtpSignals\Events\SignalEvent; 83 + use SocialDept\AtpSignals\Signals\Signal; 84 + 85 + class ModerationSignal extends Signal 86 + { 87 + public function __construct( 88 + private ModerationService $moderation 89 + ) {} 90 + 91 + public function eventTypes(): array 92 + { 93 + return ['commit']; 94 + } 95 + 96 + public function collections(): ?array 97 + { 98 + return ['app.bsky.feed.post']; 99 + } 100 + 101 + public function operations(): ?array 102 + { 103 + return [SignalCommitOperation::Create]; 104 + } 105 + 106 + public function shouldQueue(): bool 107 + { 108 + return true; 109 + } 110 + 111 + public function queue(): string 112 + { 113 + return 'moderation'; 114 + } 115 + 116 + public function handle(SignalEvent $event): void 117 + { 118 + $record = $event->getRecord(); 119 + 120 + if (!isset($record->text)) { 121 + return; 122 + } 123 + 124 + $result = $this->moderation->analyze($record->text); 125 + 126 + if ($result->needsReview) { 127 + FlaggedPost::create([ 128 + 'did' => $event->did, 129 + 'rkey' => $event->commit->rkey, 130 + 'text' => $record->text, 131 + 'reason' => $result->reason, 132 + 'confidence' => $result->confidence, 133 + 'flagged_at' => now(), 134 + ]); 135 + } 136 + } 137 + 138 + public function failed(SignalEvent $event, \Throwable $exception): void 139 + { 140 + Log::error('Moderation signal failed', [ 141 + 'did' => $event->did, 142 + 'error' => $exception->getMessage(), 143 + ]); 144 + } 145 + } 146 + ``` 147 + 148 + **Use case:** Automated content moderation with human review queue. 149 + 150 + ## User Activity Feed 151 + 152 + Build a personalized activity feed. 153 + 154 + ```php 155 + <?php 156 + 157 + namespace App\Signals; 158 + 159 + use App\Models\Activity; 160 + use App\Models\User; 161 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 162 + use SocialDept\AtpSignals\Events\SignalEvent; 163 + use SocialDept\AtpSignals\Signals\Signal; 164 + 165 + class ActivityFeedSignal extends Signal 166 + { 167 + public function eventTypes(): array 168 + { 169 + return ['commit']; 170 + } 171 + 172 + public function collections(): ?array 173 + { 174 + return [ 175 + 'app.bsky.feed.post', 176 + 'app.bsky.feed.like', 177 + 'app.bsky.feed.repost', 178 + ]; 179 + } 180 + 181 + public function operations(): ?array 182 + { 183 + return [SignalCommitOperation::Create]; 184 + } 185 + 186 + public function shouldQueue(): bool 187 + { 188 + return true; 189 + } 190 + 191 + public function handle(SignalEvent $event): void 192 + { 193 + // Check if we're tracking this user 194 + $user = User::where('did', $event->did)->first(); 195 + 196 + if (!$user) { 197 + return; 198 + } 199 + 200 + // Check if any followers want to see this 201 + $followerIds = $user->followers()->pluck('id'); 202 + 203 + if ($followerIds->isEmpty()) { 204 + return; 205 + } 206 + 207 + $collection = $event->getCollection(); 208 + 209 + // Create activity for each follower's feed 210 + foreach ($followerIds as $followerId) { 211 + Activity::create([ 212 + 'user_id' => $followerId, 213 + 'actor_did' => $event->did, 214 + 'type' => $this->getActivityType($collection), 215 + 'data' => $event->toArray(), 216 + 'created_at' => $event->getTimestamp(), 217 + ]); 218 + } 219 + } 220 + 221 + private function getActivityType(string $collection): string 222 + { 223 + return match ($collection) { 224 + 'app.bsky.feed.post' => 'post', 225 + 'app.bsky.feed.like' => 'like', 226 + 'app.bsky.feed.repost' => 'repost', 227 + default => 'unknown', 228 + }; 229 + } 230 + } 231 + ``` 232 + 233 + **Use case:** Show users activity from people they follow. 234 + 235 + ## Real-Time Notifications 236 + 237 + Send notifications for mentions and interactions. 238 + 239 + ```php 240 + <?php 241 + 242 + namespace App\Signals; 243 + 244 + use App\Models\User; 245 + use App\Notifications\MentionedInPost; 246 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 247 + use SocialDept\AtpSignals\Events\SignalEvent; 248 + use SocialDept\AtpSignals\Signals\Signal; 249 + 250 + class MentionNotificationSignal extends Signal 251 + { 252 + public function eventTypes(): array 253 + { 254 + return ['commit']; 255 + } 256 + 257 + public function collections(): ?array 258 + { 259 + return ['app.bsky.feed.post']; 260 + } 261 + 262 + public function operations(): ?array 263 + { 264 + return [SignalCommitOperation::Create]; 265 + } 266 + 267 + public function shouldQueue(): bool 268 + { 269 + return true; 270 + } 271 + 272 + public function handle(SignalEvent $event): void 273 + { 274 + $record = $event->getRecord(); 275 + 276 + if (!isset($record->facets)) { 277 + return; 278 + } 279 + 280 + // Extract mentions from facets 281 + $mentions = collect($record->facets) 282 + ->filter(fn($facet) => isset($facet->features)) 283 + ->flatMap(fn($facet) => $facet->features) 284 + ->filter(fn($feature) => $feature->{'$type'} === 'app.bsky.richtext.facet#mention') 285 + ->pluck('did') 286 + ->unique(); 287 + 288 + foreach ($mentions as $mentionedDid) { 289 + $user = User::where('did', $mentionedDid)->first(); 290 + 291 + if ($user) { 292 + $user->notify(new MentionedInPost( 293 + authorDid: $event->did, 294 + text: $record->text ?? '', 295 + uri: "at://{$event->did}/app.bsky.feed.post/{$event->commit->rkey}" 296 + )); 297 + } 298 + } 299 + } 300 + } 301 + ``` 302 + 303 + **Use case:** Real-time notifications when users are mentioned. 304 + 305 + ## Follow Tracker 306 + 307 + Track follow relationships and send notifications. 308 + 309 + ```php 310 + <?php 311 + 312 + namespace App\Signals; 313 + 314 + use App\Models\Follow; 315 + use App\Models\User; 316 + use App\Notifications\NewFollower; 317 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 318 + use SocialDept\AtpSignals\Events\SignalEvent; 319 + use SocialDept\AtpSignals\Signals\Signal; 320 + 321 + class FollowTrackerSignal extends Signal 322 + { 323 + public function eventTypes(): array 324 + { 325 + return ['commit']; 326 + } 327 + 328 + public function collections(): ?array 329 + { 330 + return ['app.bsky.graph.follow']; 331 + } 332 + 333 + public function operations(): ?array 334 + { 335 + return [ 336 + SignalCommitOperation::Create, 337 + SignalCommitOperation::Delete, 338 + ]; 339 + } 340 + 341 + public function shouldQueue(): bool 342 + { 343 + return true; 344 + } 345 + 346 + public function handle(SignalEvent $event): void 347 + { 348 + $record = $event->getRecord(); 349 + $operation = $event->getOperation(); 350 + 351 + if ($operation === SignalCommitOperation::Create) { 352 + $this->handleNewFollow($event, $record); 353 + } else { 354 + $this->handleUnfollow($event); 355 + } 356 + } 357 + 358 + private function handleNewFollow(SignalEvent $event, object $record): void 359 + { 360 + Follow::create([ 361 + 'follower_did' => $event->did, 362 + 'following_did' => $record->subject, 363 + 'created_at' => $record->createdAt ?? now(), 364 + ]); 365 + 366 + // Notify the followed user 367 + $followedUser = User::where('did', $record->subject)->first(); 368 + 369 + if ($followedUser) { 370 + $followedUser->notify(new NewFollower($event->did)); 371 + } 372 + } 373 + 374 + private function handleUnfollow(SignalEvent $event): void 375 + { 376 + Follow::where('follower_did', $event->did) 377 + ->where('rkey', $event->commit->rkey) 378 + ->delete(); 379 + } 380 + } 381 + ``` 382 + 383 + **Use case:** Track follows and notify users of new followers. 384 + 385 + ## Search Indexer 386 + 387 + Index posts for full-text search. 388 + 389 + ```php 390 + <?php 391 + 392 + namespace App\Signals; 393 + 394 + use App\Models\Post; 395 + use Laravel\Scout\Searchable; 396 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 397 + use SocialDept\AtpSignals\Events\SignalEvent; 398 + use SocialDept\AtpSignals\Signals\Signal; 399 + 400 + class SearchIndexerSignal extends Signal 401 + { 402 + public function eventTypes(): array 403 + { 404 + return ['commit']; 405 + } 406 + 407 + public function collections(): ?array 408 + { 409 + return ['app.bsky.feed.post']; 410 + } 411 + 412 + public function shouldQueue(): bool 413 + { 414 + return true; 415 + } 416 + 417 + public function queue(): string 418 + { 419 + return 'indexing'; 420 + } 421 + 422 + public function handle(SignalEvent $event): void 423 + { 424 + $operation = $event->getOperation(); 425 + 426 + match ($operation) { 427 + SignalCommitOperation::Create, 428 + SignalCommitOperation::Update => $this->indexPost($event), 429 + SignalCommitOperation::Delete => $this->deletePost($event), 430 + }; 431 + } 432 + 433 + private function indexPost(SignalEvent $event): void 434 + { 435 + $record = $event->getRecord(); 436 + 437 + $post = Post::updateOrCreate( 438 + [ 439 + 'did' => $event->did, 440 + 'rkey' => $event->commit->rkey, 441 + ], 442 + [ 443 + 'text' => $record->text ?? '', 444 + 'created_at' => $record->createdAt ?? now(), 445 + 'indexed_at' => now(), 446 + ] 447 + ); 448 + 449 + // Scout automatically indexes 450 + $post->searchable(); 451 + } 452 + 453 + private function deletePost(SignalEvent $event): void 454 + { 455 + $post = Post::where('did', $event->did) 456 + ->where('rkey', $event->commit->rkey) 457 + ->first(); 458 + 459 + if ($post) { 460 + $post->unsearchable(); 461 + $post->delete(); 462 + } 463 + } 464 + } 465 + ``` 466 + 467 + **Use case:** Full-text search across all Bluesky posts. 468 + 469 + ## Trend Detection 470 + 471 + Identify trending topics and hashtags. 472 + 473 + ```php 474 + <?php 475 + 476 + namespace App\Signals; 477 + 478 + use App\Models\TrendingTopic; 479 + use Illuminate\Support\Facades\Cache; 480 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 481 + use SocialDept\AtpSignals\Events\SignalEvent; 482 + use SocialDept\AtpSignals\Signals\Signal; 483 + 484 + class TrendDetectionSignal extends Signal 485 + { 486 + public function eventTypes(): array 487 + { 488 + return ['commit']; 489 + } 490 + 491 + public function collections(): ?array 492 + { 493 + return ['app.bsky.feed.post']; 494 + } 495 + 496 + public function operations(): ?array 497 + { 498 + return [SignalCommitOperation::Create]; 499 + } 500 + 501 + public function shouldQueue(): bool 502 + { 503 + return true; 504 + } 505 + 506 + public function handle(SignalEvent $event): void 507 + { 508 + $record = $event->getRecord(); 509 + 510 + if (!isset($record->text)) { 511 + return; 512 + } 513 + 514 + // Extract hashtags 515 + preg_match_all('/#(\w+)/', $record->text, $matches); 516 + 517 + foreach ($matches[1] as $hashtag) { 518 + $this->incrementHashtag($hashtag); 519 + } 520 + } 521 + 522 + private function incrementHashtag(string $hashtag): void 523 + { 524 + $key = "trending:hashtag:{$hashtag}"; 525 + 526 + // Increment counter (expires after 1 hour) 527 + $count = Cache::increment($key, 1); 528 + 529 + if (!Cache::has($key)) { 530 + Cache::put($key, 1, now()->addHour()); 531 + } 532 + 533 + // Update trending topics if threshold reached 534 + if ($count > 100) { 535 + TrendingTopic::updateOrCreate( 536 + ['hashtag' => $hashtag], 537 + ['count' => $count, 'updated_at' => now()] 538 + ); 539 + } 540 + } 541 + } 542 + ``` 543 + 544 + **Use case:** Identify trending hashtags and topics in real-time. 545 + 546 + ## Custom AppView 547 + 548 + Index custom collections for your AppView. 549 + 550 + ```php 551 + <?php 552 + 553 + namespace App\Signals; 554 + 555 + use App\Models\Publication; 556 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 557 + use SocialDept\AtpSignals\Events\SignalEvent; 558 + use SocialDept\AtpSignals\Signals\Signal; 559 + 560 + class PublicationIndexerSignal extends Signal 561 + { 562 + public function eventTypes(): array 563 + { 564 + return ['commit']; 565 + } 566 + 567 + public function collections(): ?array 568 + { 569 + return [ 570 + 'app.offprint.beta.publication', 571 + 'app.offprint.beta.post', 572 + ]; 573 + } 574 + 575 + public function shouldQueue(): bool 576 + { 577 + return true; 578 + } 579 + 580 + public function handle(SignalEvent $event): void 581 + { 582 + $collection = $event->getCollection(); 583 + $operation = $event->getOperation(); 584 + 585 + if ($collection === 'app.offprint.beta.publication') { 586 + $this->handlePublication($event, $operation); 587 + } else { 588 + $this->handlePost($event, $operation); 589 + } 590 + } 591 + 592 + private function handlePublication(SignalEvent $event, SignalCommitOperation $operation): void 593 + { 594 + if ($operation === SignalCommitOperation::Delete) { 595 + Publication::where('did', $event->did) 596 + ->where('rkey', $event->commit->rkey) 597 + ->delete(); 598 + return; 599 + } 600 + 601 + $record = $event->getRecord(); 602 + 603 + Publication::updateOrCreate( 604 + [ 605 + 'did' => $event->did, 606 + 'rkey' => $event->commit->rkey, 607 + ], 608 + [ 609 + 'title' => $record->title ?? '', 610 + 'description' => $record->description ?? null, 611 + 'created_at' => $record->createdAt ?? now(), 612 + ] 613 + ); 614 + } 615 + 616 + private function handlePost(SignalEvent $event, SignalCommitOperation $operation): void 617 + { 618 + // Handle custom post records 619 + } 620 + } 621 + ``` 622 + 623 + **Use case:** Build AT Protocol AppViews with custom collections. 624 + 625 + ## Rate-Limited API Integration 626 + 627 + Integrate with external APIs respecting rate limits. 628 + 629 + ```php 630 + <?php 631 + 632 + namespace App\Signals; 633 + 634 + use App\Services\ExternalAPIService; 635 + use Illuminate\Support\Facades\RateLimiter; 636 + use SocialDept\AtpSignals\Events\SignalEvent; 637 + use SocialDept\AtpSignals\Signals\Signal; 638 + 639 + class APIIntegrationSignal extends Signal 640 + { 641 + public function __construct( 642 + private ExternalAPIService $api 643 + ) {} 644 + 645 + public function eventTypes(): array 646 + { 647 + return ['commit']; 648 + } 649 + 650 + public function collections(): ?array 651 + { 652 + return ['app.bsky.feed.post']; 653 + } 654 + 655 + public function shouldQueue(): bool 656 + { 657 + return true; 658 + } 659 + 660 + public function handle(SignalEvent $event): void 661 + { 662 + $record = $event->getRecord(); 663 + 664 + // Rate limit: 100 calls per minute 665 + $executed = RateLimiter::attempt( 666 + 'external-api', 667 + $perMinute = 100, 668 + function () use ($event, $record) { 669 + $this->api->sendPost([ 670 + 'author' => $event->did, 671 + 'text' => $record->text ?? '', 672 + 'timestamp' => $event->getTimestamp(), 673 + ]); 674 + } 675 + ); 676 + 677 + if (!$executed) { 678 + // Re-queue for later 679 + dispatch(fn() => $this->handle($event)) 680 + ->delay(now()->addMinutes(1)); 681 + } 682 + } 683 + } 684 + ``` 685 + 686 + **Use case:** Mirror content to external platforms with rate limiting. 687 + 688 + ## Multi-Collection Analytics 689 + 690 + Track engagement across multiple collection types. 691 + 692 + ```php 693 + <?php 694 + 695 + namespace App\Signals; 696 + 697 + use App\Models\UserMetrics; 698 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 699 + use SocialDept\AtpSignals\Events\SignalEvent; 700 + use SocialDept\AtpSignals\Signals\Signal; 701 + 702 + class UserMetricsSignal extends Signal 703 + { 704 + public function eventTypes(): array 705 + { 706 + return ['commit']; 707 + } 708 + 709 + public function collections(): ?array 710 + { 711 + return ['app.bsky.feed.*', 'app.bsky.graph.*']; 712 + } 713 + 714 + public function operations(): ?array 715 + { 716 + return [SignalCommitOperation::Create]; 717 + } 718 + 719 + public function shouldQueue(): bool 720 + { 721 + return true; 722 + } 723 + 724 + public function handle(SignalEvent $event): void 725 + { 726 + $collection = $event->getCollection(); 727 + 728 + $metrics = UserMetrics::firstOrCreate( 729 + ['did' => $event->did], 730 + ['total_posts' => 0, 'total_likes' => 0, 'total_follows' => 0] 731 + ); 732 + 733 + match ($collection) { 734 + 'app.bsky.feed.post' => $metrics->increment('total_posts'), 735 + 'app.bsky.feed.like' => $metrics->increment('total_likes'), 736 + 'app.bsky.graph.follow' => $metrics->increment('total_follows'), 737 + default => null, 738 + }; 739 + 740 + $metrics->touch('last_activity_at'); 741 + } 742 + } 743 + ``` 744 + 745 + **Use case:** User activity metrics and leaderboards. 746 + 747 + ## Performance Tips 748 + 749 + ### Batch Database Operations 750 + 751 + ```php 752 + public function handle(SignalEvent $event): void 753 + { 754 + // Bad - individual inserts 755 + Post::create([...]); 756 + 757 + // Good - batch inserts 758 + $posts = Cache::get('pending_posts', []); 759 + $posts[] = [...]; 760 + 761 + if (count($posts) >= 100) { 762 + Post::insert($posts); 763 + Cache::forget('pending_posts'); 764 + } else { 765 + Cache::put('pending_posts', $posts, now()->addMinutes(5)); 766 + } 767 + } 768 + ``` 769 + 770 + ### Use Queues for Heavy Operations 771 + 772 + ```php 773 + public function shouldQueue(): bool 774 + { 775 + // Queue if operation takes > 100ms 776 + return true; 777 + } 778 + ``` 779 + 780 + ### Add Indexes for Filtering 781 + 782 + ```php 783 + // Migration for fast lookups 784 + Schema::table('posts', function (Blueprint $table) { 785 + $table->index(['did', 'rkey']); 786 + $table->index('created_at'); 787 + }); 788 + ``` 789 + 790 + ## Next Steps 791 + 792 + - **[Review signal architecture โ†’](signals.md)** - Understand Signal structure 793 + - **[Learn about filtering โ†’](filtering.md)** - Master event filtering 794 + - **[Explore queue integration โ†’](queues.md)** - Build high-performance Signals 795 + - **[Configure your setup โ†’](configuration.md)** - Optimize configuration
+704
docs/filtering.md
···
··· 1 + # Filtering Events 2 + 3 + Filtering is how you control which events your Signals process. Signal provides multiple layers of filtering to help you target exactly the events you care about. 4 + 5 + ## Why Filter? 6 + 7 + The AT Protocol generates millions of events per hour. Without filtering: 8 + 9 + - Your Signals would process every event (slow and expensive) 10 + - Your database would fill with irrelevant data 11 + - Your queues would be overwhelmed 12 + - Your costs would skyrocket 13 + 14 + Filtering lets you focus on what matters. 15 + 16 + ## Filter Layers 17 + 18 + Signal provides four filtering layers, applied in order: 19 + 20 + 1. **Event Type Filtering** - Which kind of events (commit, identity, account) 21 + 2. **Collection Filtering** - Which AT Protocol collections 22 + 3. **Operation Filtering** - Which operations (create, update, delete) 23 + 4. **DID Filtering** - Which users 24 + 5. **Custom Filtering** - Your own logic 25 + 26 + ## Event Type Filtering 27 + 28 + The most basic filter - required for all Signals. 29 + 30 + ### Available Event Types 31 + 32 + ```php 33 + use SocialDept\AtpSignals\Enums\SignalEventType; 34 + 35 + public function eventTypes(): array 36 + { 37 + return [SignalEventType::Commit]; // Most common 38 + // Or: return ['commit']; 39 + } 40 + ``` 41 + 42 + **Three event types:** 43 + 44 + | Type | Description | Use Cases | 45 + |------------|--------------------|----------------------------------------| 46 + | `commit` | Repository changes | Posts, likes, follows, profile updates | 47 + | `identity` | Handle changes | Username updates, account migrations | 48 + | `account` | Account status | Deactivation, suspension | 49 + 50 + ### Multiple Event Types 51 + 52 + Listen to multiple types in one Signal: 53 + 54 + ```php 55 + public function eventTypes(): array 56 + { 57 + return [ 58 + SignalEventType::Commit, 59 + SignalEventType::Identity, 60 + ]; 61 + } 62 + ``` 63 + 64 + Then check the type in your handler: 65 + 66 + ```php 67 + public function handle(SignalEvent $event): void 68 + { 69 + if ($event->isCommit()) { 70 + $this->handleCommit($event); 71 + } 72 + 73 + if ($event->isIdentity()) { 74 + $this->handleIdentity($event); 75 + } 76 + } 77 + ``` 78 + 79 + ## Collection Filtering 80 + 81 + Collections represent different types of data in the AT Protocol. 82 + 83 + ### Basic Collection Filter 84 + 85 + ```php 86 + public function collections(): ?array 87 + { 88 + return ['app.bsky.feed.post']; 89 + } 90 + ``` 91 + 92 + ### No Filter (All Collections) 93 + 94 + Return `null` to process all collections: 95 + 96 + ```php 97 + public function collections(): ?array 98 + { 99 + return null; // Handle everything 100 + } 101 + ``` 102 + 103 + ### Multiple Collections 104 + 105 + ```php 106 + public function collections(): ?array 107 + { 108 + return [ 109 + 'app.bsky.feed.post', 110 + 'app.bsky.feed.like', 111 + 'app.bsky.feed.repost', 112 + ]; 113 + } 114 + ``` 115 + 116 + ### Wildcard Patterns 117 + 118 + Use `*` to match multiple collections: 119 + 120 + ```php 121 + public function collections(): ?array 122 + { 123 + return ['app.bsky.feed.*']; 124 + } 125 + ``` 126 + 127 + **This matches:** 128 + - `app.bsky.feed.post` 129 + - `app.bsky.feed.like` 130 + - `app.bsky.feed.repost` 131 + - Any other `app.bsky.feed.*` collection 132 + 133 + ### Common Collection Patterns 134 + 135 + | Pattern | Matches | Use Case | 136 + |--------------------|-------------------------|------------------------| 137 + | `app.bsky.feed.*` | All feed interactions | Posts, likes, reposts | 138 + | `app.bsky.graph.*` | All social graph | Follows, blocks, mutes | 139 + | `app.bsky.actor.*` | All profile changes | Profile updates | 140 + | `app.bsky.*` | All Bluesky collections | Everything Bluesky | 141 + | `app.yourapp.*` | Your custom collections | Custom AppView | 142 + 143 + ### Mixing Exact and Wildcards 144 + 145 + Combine exact matches with wildcards: 146 + 147 + ```php 148 + public function collections(): ?array 149 + { 150 + return [ 151 + 'app.bsky.feed.post', // Exact: only posts 152 + 'app.bsky.graph.*', // Wildcard: all graph events 153 + 'app.myapp.custom.record', // Exact: custom collection 154 + ]; 155 + } 156 + ``` 157 + 158 + ### Standard Bluesky Collections 159 + 160 + **Feed Collections** (`app.bsky.feed.*`): 161 + - `app.bsky.feed.post` - Posts (text, images, videos) 162 + - `app.bsky.feed.like` - Likes on posts 163 + - `app.bsky.feed.repost` - Reposts (shares) 164 + - `app.bsky.feed.threadgate` - Thread reply controls 165 + - `app.bsky.feed.generator` - Custom feed generators 166 + 167 + **Graph Collections** (`app.bsky.graph.*`): 168 + - `app.bsky.graph.follow` - Follow relationships 169 + - `app.bsky.graph.block` - Blocked users 170 + - `app.bsky.graph.list` - User lists 171 + - `app.bsky.graph.listitem` - List memberships 172 + - `app.bsky.graph.listblock` - List blocks 173 + 174 + **Actor Collections** (`app.bsky.actor.*`): 175 + - `app.bsky.actor.profile` - User profiles 176 + 177 + **Labeler Collections** (`app.bsky.labeler.*`): 178 + - `app.bsky.labeler.service` - Labeler services 179 + 180 + ### Important: Jetstream vs Firehose Filtering 181 + 182 + **Jetstream Mode:** 183 + - Exact collection names are sent to server for filtering (efficient) 184 + - Wildcards work client-side only (you receive more data) 185 + 186 + **Firehose Mode:** 187 + - All filtering is client-side 188 + - Wildcards work normally (no difference in data received) 189 + 190 + [Learn more about modes โ†’](modes.md) 191 + 192 + ### Custom Collections (AppViews) 193 + 194 + Filter your own custom collections: 195 + 196 + ```php 197 + public function collections(): ?array 198 + { 199 + return [ 200 + 'app.offprint.beta.publication', 201 + 'app.offprint.beta.post', 202 + ]; 203 + } 204 + ``` 205 + 206 + ## Operation Filtering 207 + 208 + Filter by operation type (only applies to commit events). 209 + 210 + ### Available Operations 211 + 212 + ```php 213 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 214 + 215 + public function operations(): ?array 216 + { 217 + return [SignalCommitOperation::Create]; 218 + // Or: return ['create']; 219 + } 220 + ``` 221 + 222 + **Three operation types:** 223 + 224 + | Operation | Description | Example | 225 + |-----------|------------------|-----------------| 226 + | `create` | New records | Creating a post | 227 + | `update` | Modified records | Editing a post | 228 + | `delete` | Removed records | Deleting a post | 229 + 230 + ### No Filter (All Operations) 231 + 232 + ```php 233 + public function operations(): ?array 234 + { 235 + return null; // Handle all operations 236 + } 237 + ``` 238 + 239 + ### Multiple Operations 240 + 241 + ```php 242 + public function operations(): ?array 243 + { 244 + return [ 245 + SignalCommitOperation::Create, 246 + SignalCommitOperation::Update, 247 + ]; 248 + // Or: return ['create', 'update']; 249 + } 250 + ``` 251 + 252 + ### Common Patterns 253 + 254 + **Only track new content:** 255 + ```php 256 + public function operations(): ?array 257 + { 258 + return [SignalCommitOperation::Create]; 259 + } 260 + ``` 261 + 262 + **Track modifications:** 263 + ```php 264 + public function operations(): ?array 265 + { 266 + return [SignalCommitOperation::Update]; 267 + } 268 + ``` 269 + 270 + **Cleanup on deletions:** 271 + ```php 272 + public function operations(): ?array 273 + { 274 + return [SignalCommitOperation::Delete]; 275 + } 276 + ``` 277 + 278 + ### Checking Operations in Handler 279 + 280 + You can also check operation type in your handler: 281 + 282 + ```php 283 + public function handle(SignalEvent $event): void 284 + { 285 + $operation = $event->getOperation(); 286 + 287 + // Using enum 288 + if ($operation === SignalCommitOperation::Create) { 289 + $this->createRecord($event); 290 + } 291 + 292 + // Using commit helper 293 + if ($event->commit->isCreate()) { 294 + $this->createRecord($event); 295 + } 296 + 297 + if ($event->commit->isUpdate()) { 298 + $this->updateRecord($event); 299 + } 300 + 301 + if ($event->commit->isDelete()) { 302 + $this->deleteRecord($event); 303 + } 304 + } 305 + ``` 306 + 307 + ## DID Filtering 308 + 309 + Filter events by specific users (DIDs). 310 + 311 + ### Basic DID Filter 312 + 313 + ```php 314 + public function dids(): ?array 315 + { 316 + return [ 317 + 'did:plc:z72i7hdynmk6r22z27h6tvur', 318 + ]; 319 + } 320 + ``` 321 + 322 + ### No Filter (All Users) 323 + 324 + ```php 325 + public function dids(): ?array 326 + { 327 + return null; // Handle all users 328 + } 329 + ``` 330 + 331 + ### Multiple DIDs 332 + 333 + ```php 334 + public function dids(): ?array 335 + { 336 + return [ 337 + 'did:plc:z72i7hdynmk6r22z27h6tvur', 338 + 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', 339 + ]; 340 + } 341 + ``` 342 + 343 + ### Use Cases 344 + 345 + **Monitor specific accounts:** 346 + ```php 347 + // Track posts from specific content creators 348 + public function collections(): ?array 349 + { 350 + return ['app.bsky.feed.post']; 351 + } 352 + 353 + public function dids(): ?array 354 + { 355 + return [ 356 + 'did:plc:z72i7hdynmk6r22z27h6tvur', // Creator 1 357 + 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // Creator 2 358 + ]; 359 + } 360 + ``` 361 + 362 + **Dynamic DID filtering:** 363 + ```php 364 + use App\Models\MonitoredAccount; 365 + 366 + public function dids(): ?array 367 + { 368 + return MonitoredAccount::pluck('did')->toArray(); 369 + } 370 + ``` 371 + 372 + ## Custom Filtering 373 + 374 + Implement complex filtering logic with `shouldHandle()`. 375 + 376 + ### Basic Custom Filter 377 + 378 + ```php 379 + public function shouldHandle(SignalEvent $event): bool 380 + { 381 + // Only handle posts with images 382 + if ($event->isCommit() && $event->getCollection() === 'app.bsky.feed.post') { 383 + $record = $event->getRecord(); 384 + return isset($record->embed); 385 + } 386 + 387 + return true; 388 + } 389 + ``` 390 + 391 + ### Advanced Examples 392 + 393 + **Filter by text content:** 394 + ```php 395 + public function shouldHandle(SignalEvent $event): bool 396 + { 397 + $record = $event->getRecord(); 398 + 399 + if (!isset($record->text)) { 400 + return false; 401 + } 402 + 403 + // Only handle posts mentioning "Laravel" 404 + return str_contains($record->text, 'Laravel'); 405 + } 406 + ``` 407 + 408 + **Filter by language:** 409 + ```php 410 + public function shouldHandle(SignalEvent $event): bool 411 + { 412 + $record = $event->getRecord(); 413 + 414 + // Only handle English posts 415 + return ($record->langs[0] ?? null) === 'en'; 416 + } 417 + ``` 418 + 419 + **Filter by engagement:** 420 + ```php 421 + use App\Services\EngagementCalculator; 422 + 423 + public function shouldHandle(SignalEvent $event): bool 424 + { 425 + $engagement = EngagementCalculator::calculate($event); 426 + 427 + // Only handle high-engagement content 428 + return $engagement > 100; 429 + } 430 + ``` 431 + 432 + **Time-based filtering:** 433 + ```php 434 + public function shouldHandle(SignalEvent $event): bool 435 + { 436 + $timestamp = $event->getTimestamp(); 437 + 438 + // Only handle events from the last hour 439 + return $timestamp->isAfter(now()->subHour()); 440 + } 441 + ``` 442 + 443 + ## Combining Filters 444 + 445 + Stack multiple filter layers for precise targeting: 446 + 447 + ```php 448 + class HighEngagementPostsSignal extends Signal 449 + { 450 + // Layer 1: Event type 451 + public function eventTypes(): array 452 + { 453 + return ['commit']; 454 + } 455 + 456 + // Layer 2: Collection 457 + public function collections(): ?array 458 + { 459 + return ['app.bsky.feed.post']; 460 + } 461 + 462 + // Layer 3: Operation 463 + public function operations(): ?array 464 + { 465 + return [SignalCommitOperation::Create]; 466 + } 467 + 468 + // Layer 4: Custom logic 469 + public function shouldHandle(SignalEvent $event): bool 470 + { 471 + $record = $event->getRecord(); 472 + 473 + // Must have text 474 + if (!isset($record->text)) { 475 + return false; 476 + } 477 + 478 + // Must be longer than 100 characters 479 + if (strlen($record->text) < 100) { 480 + return false; 481 + } 482 + 483 + // Must have media 484 + if (!isset($record->embed)) { 485 + return false; 486 + } 487 + 488 + return true; 489 + } 490 + 491 + public function handle(SignalEvent $event): void 492 + { 493 + // Only high-quality posts make it here 494 + } 495 + } 496 + ``` 497 + 498 + ## Performance Considerations 499 + 500 + ### Server-Side vs Client-Side Filtering 501 + 502 + **Jetstream Mode (Server-Side):** 503 + - Collections filter applied on server (efficient) 504 + - Only receives matching events 505 + - Lower bandwidth usage 506 + 507 + ```php 508 + // These collections are sent to Jetstream server 509 + public function collections(): ?array 510 + { 511 + return ['app.bsky.feed.post', 'app.bsky.feed.like']; 512 + } 513 + ``` 514 + 515 + **Firehose Mode (Client-Side):** 516 + - All filtering happens in your application 517 + - Receives all events (higher bandwidth) 518 + - More control but higher cost 519 + 520 + [Learn more about modes โ†’](modes.md) 521 + 522 + ### Filter Early 523 + 524 + Apply the most restrictive filters first: 525 + 526 + ```php 527 + // Good - filters early 528 + public function eventTypes(): array 529 + { 530 + return ['commit']; // Narrows to commits only 531 + } 532 + 533 + public function collections(): ?array 534 + { 535 + return ['app.bsky.feed.post']; // Further narrows to posts 536 + } 537 + 538 + // Less ideal - too broad 539 + public function eventTypes(): array 540 + { 541 + return ['commit', 'identity', 'account']; // Too many events 542 + } 543 + 544 + public function shouldHandle(SignalEvent $event): bool 545 + { 546 + // Filtering everything in custom logic (expensive) 547 + return $event->isCommit() && $event->getCollection() === 'app.bsky.feed.post'; 548 + } 549 + ``` 550 + 551 + ### Avoid Heavy Logic in shouldHandle() 552 + 553 + Keep custom filtering lightweight: 554 + 555 + ```php 556 + // Good - lightweight checks 557 + public function shouldHandle(SignalEvent $event): bool 558 + { 559 + $record = $event->getRecord(); 560 + return isset($record->text) && strlen($record->text) > 10; 561 + } 562 + 563 + // Less ideal - heavy database queries 564 + public function shouldHandle(SignalEvent $event): bool 565 + { 566 + // Database query on every event (slow!) 567 + return User::where('did', $event->did)->exists(); 568 + } 569 + ``` 570 + 571 + If you need heavy logic, use queues: 572 + 573 + ```php 574 + public function shouldQueue(): bool 575 + { 576 + return true; // Move heavy work to queue 577 + } 578 + ``` 579 + 580 + ## Common Filter Patterns 581 + 582 + ### Track All Activity from Specific Users 583 + 584 + ```php 585 + public function eventTypes(): array 586 + { 587 + return ['commit']; 588 + } 589 + 590 + public function dids(): ?array 591 + { 592 + return [ 593 + 'did:plc:z72i7hdynmk6r22z27h6tvur', 594 + ]; 595 + } 596 + ``` 597 + 598 + ### Monitor All Feed Activity 599 + 600 + ```php 601 + public function eventTypes(): array 602 + { 603 + return ['commit']; 604 + } 605 + 606 + public function collections(): ?array 607 + { 608 + return ['app.bsky.feed.*']; 609 + } 610 + ``` 611 + 612 + ### Track Only New Posts 613 + 614 + ```php 615 + public function eventTypes(): array 616 + { 617 + return ['commit']; 618 + } 619 + 620 + public function collections(): ?array 621 + { 622 + return ['app.bsky.feed.post']; 623 + } 624 + 625 + public function operations(): ?array 626 + { 627 + return [SignalCommitOperation::Create]; 628 + } 629 + ``` 630 + 631 + ### Monitor Content Deletions 632 + 633 + ```php 634 + public function eventTypes(): array 635 + { 636 + return ['commit']; 637 + } 638 + 639 + public function operations(): ?array 640 + { 641 + return [SignalCommitOperation::Delete]; 642 + } 643 + ``` 644 + 645 + ### Track Profile Changes 646 + 647 + ```php 648 + public function eventTypes(): array 649 + { 650 + return ['commit']; 651 + } 652 + 653 + public function collections(): ?array 654 + { 655 + return ['app.bsky.actor.profile']; 656 + } 657 + ``` 658 + 659 + ### Monitor Handle Changes 660 + 661 + ```php 662 + public function eventTypes(): array 663 + { 664 + return ['identity']; 665 + } 666 + ``` 667 + 668 + ## Debugging Filters 669 + 670 + ### Log What's Being Filtered 671 + 672 + ```php 673 + public function shouldHandle(SignalEvent $event): bool 674 + { 675 + $shouldHandle = $this->myCustomLogic($event); 676 + 677 + if (!$shouldHandle) { 678 + Log::debug('Event filtered out', [ 679 + 'signal' => static::class, 680 + 'did' => $event->did, 681 + 'collection' => $event->getCollection(), 682 + 'reason' => 'Failed custom logic', 683 + ]); 684 + } 685 + 686 + return $shouldHandle; 687 + } 688 + ``` 689 + 690 + ### Test Your Filters 691 + 692 + ```bash 693 + php artisan signal:test YourSignal 694 + ``` 695 + 696 + This runs your Signal with sample data to verify filtering works correctly. 697 + 698 + [Learn more about testing โ†’](testing.md) 699 + 700 + ## Next Steps 701 + 702 + - **[Understand Jetstream vs Firehose โ†’](modes.md)** - Choose the right mode for your filters 703 + - **[Learn about queue integration โ†’](queues.md)** - Handle high-volume filtered events 704 + - **[See real-world examples โ†’](examples.md)** - Learn from production filter patterns
+188
docs/installation.md
···
··· 1 + # Installation 2 + 3 + Signal is designed to be installed quickly and easily in any Laravel 11+ application. 4 + 5 + ## Requirements 6 + 7 + Before installing Signal, ensure your environment meets these requirements: 8 + 9 + - **PHP 8.2 or higher** 10 + - **Laravel 11.0 or higher** 11 + - **WebSocket support** (enabled by default in most environments) 12 + - **Database** (for cursor storage) 13 + 14 + ## Composer Installation 15 + 16 + Install Signal via Composer: 17 + 18 + ```bash 19 + composer require socialdept/atp-signals 20 + ``` 21 + 22 + ## Quick Setup 23 + 24 + Run the installation command to set up everything automatically: 25 + 26 + ```bash 27 + php artisan signal:install 28 + ``` 29 + 30 + This interactive command will: 31 + 32 + 1. Publish the configuration file to `config/signal.php` 33 + 2. Publish database migrations for cursor storage 34 + 3. Ask if you'd like to run migrations immediately 35 + 4. Display next steps and helpful information 36 + 37 + ### What Gets Created 38 + 39 + After installation, you'll have: 40 + 41 + - **Configuration file**: `config/signal.php` - All Signal settings 42 + - **Migration**: `database/migrations/2024_01_01_000000_create_signal_cursors_table.php` - Cursor storage 43 + - **Signal directory**: `app/Signals/` - Where your Signals live (created when you make your first Signal) 44 + 45 + ## Manual Installation 46 + 47 + If you prefer more control, you can install manually: 48 + 49 + ### 1. Publish Configuration 50 + 51 + ```bash 52 + php artisan vendor:publish --tag=signal-config 53 + ``` 54 + 55 + This creates `config/signal.php` with all available options. 56 + 57 + ### 2. Publish Migrations 58 + 59 + ```bash 60 + php artisan vendor:publish --tag=signal-migrations 61 + ``` 62 + 63 + This creates the cursor storage migration in `database/migrations/`. 64 + 65 + ### 3. Run Migrations 66 + 67 + ```bash 68 + php artisan migrate 69 + ``` 70 + 71 + This creates the `signal_cursors` table for resuming from last position after disconnections. 72 + 73 + ## Environment Configuration 74 + 75 + Add Signal configuration to your `.env` file: 76 + 77 + ```env 78 + # Consumer Mode (jetstream or firehose) 79 + SIGNAL_MODE=jetstream 80 + 81 + # Jetstream URL (if using jetstream mode) 82 + SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 83 + 84 + # Firehose Host (if using firehose mode) 85 + SIGNAL_FIREHOSE_HOST=bsky.network 86 + 87 + # Optional: Cursor Storage Driver (database, redis, or file) 88 + SIGNAL_CURSOR_STORAGE=database 89 + 90 + # Optional: Queue Configuration 91 + SIGNAL_QUEUE_CONNECTION=redis 92 + SIGNAL_QUEUE=signal 93 + ``` 94 + 95 + ## Choosing Your Mode 96 + 97 + Signal supports two modes for consuming events. Choose based on your use case: 98 + 99 + ### Jetstream Mode (Recommended) 100 + 101 + Best for most applications: 102 + 103 + ```env 104 + SIGNAL_MODE=jetstream 105 + SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 106 + ``` 107 + 108 + **Advantages:** 109 + - Simplified JSON events (easy to work with) 110 + - Server-side collection filtering (efficient) 111 + - Lower bandwidth and processing overhead 112 + 113 + ### Firehose Mode 114 + 115 + Best for comprehensive indexing and raw data access: 116 + 117 + ```env 118 + SIGNAL_MODE=firehose 119 + SIGNAL_FIREHOSE_HOST=bsky.network 120 + ``` 121 + 122 + **Advantages:** 123 + - Access to raw CBOR/CAR data 124 + - Full AT Protocol event stream 125 + - Complete control over event processing 126 + 127 + **Trade-offs:** 128 + - Client-side filtering only (higher bandwidth) 129 + - More processing overhead 130 + 131 + [Learn more about choosing the right mode โ†’](modes.md) 132 + 133 + ## Verify Installation 134 + 135 + Check that Signal is installed correctly: 136 + 137 + ```bash 138 + php artisan signal:list 139 + ``` 140 + 141 + This should display available Signals (initially none until you create them). 142 + 143 + ## Next Steps 144 + 145 + Now that Signal is installed, you're ready to start building: 146 + 147 + 1. **[Create your first Signal โ†’](quickstart.md)** 148 + 2. **[Learn about Signal architecture โ†’](signals.md)** 149 + 3. **[Understand filtering options โ†’](filtering.md)** 150 + 151 + ## Troubleshooting 152 + 153 + ### Migration Already Exists 154 + 155 + If you see "migration already exists" when running `signal:install`, you've likely already installed Signal. You can safely skip this step. 156 + 157 + ### WebSocket Connection Issues 158 + 159 + If you experience WebSocket connection issues: 160 + 161 + 1. Verify your firewall allows WebSocket connections 162 + 2. Check that your hosting environment supports WebSockets 163 + 3. Try switching Jetstream endpoints (US East vs US West) 164 + 165 + ### Permission Errors 166 + 167 + If you encounter permission errors with cursor storage: 168 + 169 + - **Database mode**: Ensure database connection is configured correctly 170 + - **Redis mode**: Verify Redis connection is available 171 + - **File mode**: Check that Laravel has write permissions to `storage/app/signal/` 172 + 173 + ## Uninstallation 174 + 175 + To remove Signal from your application: 176 + 177 + ```bash 178 + # Remove the package 179 + composer remove socialdept/atp-signals 180 + 181 + # Optionally, rollback migrations 182 + php artisan migrate:rollback 183 + ``` 184 + 185 + You can also manually delete: 186 + - `config/signal.php` 187 + - `app/Signals/` directory 188 + - Signal-related migrations
+493
docs/modes.md
···
··· 1 + # Jetstream vs Firehose 2 + 3 + Signal supports two modes for consuming AT Protocol events. Understanding the differences is crucial for building efficient, scalable applications. 4 + 5 + ## Quick Comparison 6 + 7 + | Feature | Jetstream | Firehose | 8 + |------------------|-------------------|------------------------| 9 + | **Event Format** | Simplified JSON | Raw CBOR/CAR | 10 + | **Filtering** | Server-side | Client-side | 11 + | **Bandwidth** | Lower | Higher | 12 + | **Processing** | Lighter | Heavier | 13 + | **Best For** | Most applications | Comprehensive indexing | 14 + 15 + ## Jetstream Mode 16 + 17 + Jetstream is a **simplified, JSON-based event stream** built on top of the AT Protocol Firehose. 18 + 19 + ### When to Use Jetstream 20 + 21 + Choose Jetstream if you're: 22 + 23 + - Building production applications where efficiency matters 24 + - Concerned about bandwidth and server costs 25 + - Processing high volumes of events 26 + - Want server-side filtering for reduced bandwidth 27 + 28 + ### Configuration 29 + 30 + Set Jetstream as your mode in `.env`: 31 + 32 + ```env 33 + SIGNAL_MODE=jetstream 34 + SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 35 + ``` 36 + 37 + ### Available Endpoints 38 + 39 + Jetstream has multiple regional endpoints: 40 + 41 + **US East (Default):** 42 + ```env 43 + SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 44 + ``` 45 + 46 + **US West:** 47 + ```env 48 + SIGNAL_JETSTREAM_URL=wss://jetstream1.us-west.bsky.network 49 + ``` 50 + 51 + Choose the endpoint closest to your server for best performance. 52 + 53 + ### Advantages 54 + 55 + **1. Simplified JSON Format** 56 + 57 + Events arrive as clean JSON objects: 58 + 59 + ```json 60 + { 61 + "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 62 + "time_us": 1234567890, 63 + "kind": "commit", 64 + "commit": { 65 + "rev": "abc123", 66 + "operation": "create", 67 + "collection": "app.bsky.feed.post", 68 + "rkey": "3k2yihcrr2c2a", 69 + "record": { 70 + "text": "Hello World!", 71 + "createdAt": "2024-01-15T10:30:00Z" 72 + } 73 + } 74 + } 75 + ``` 76 + 77 + No complex parsing or decoding required. 78 + 79 + **2. Server-Side Filtering** 80 + 81 + Your collection filters are sent to Jetstream: 82 + 83 + ```php 84 + public function collections(): ?array 85 + { 86 + return ['app.bsky.feed.post', 'app.bsky.feed.like']; 87 + } 88 + ``` 89 + 90 + Jetstream only sends matching events, dramatically reducing bandwidth. 91 + 92 + **3. Lower Bandwidth** 93 + 94 + Only receive the events you care about: 95 + 96 + - **Jetstream**: Receive ~1,000 events/sec for specific collections 97 + - **Firehose**: Receive ~50,000 events/sec for everything 98 + 99 + **4. Lower Processing Overhead** 100 + 101 + JSON parsing is faster than CBOR/CAR decoding: 102 + 103 + - **Jetstream**: Simple JSON deserialization 104 + - **Firehose**: Complex CBOR/CAR decoding with `revolution/laravel-bluesky` 105 + 106 + ### Limitations 107 + 108 + **1. Client-Side Wildcards** 109 + 110 + Wildcard patterns work client-side only: 111 + 112 + ```php 113 + public function collections(): ?array 114 + { 115 + return ['app.bsky.feed.*']; // Still receives all collections 116 + } 117 + ``` 118 + 119 + The wildcard matching happens in your app, not on the server. 120 + 121 + ### Example Configuration 122 + 123 + ```php 124 + // config/signal.php 125 + return [ 126 + 'mode' => env('SIGNAL_MODE', 'jetstream'), 127 + 128 + 'jetstream' => [ 129 + 'websocket_url' => env( 130 + 'SIGNAL_JETSTREAM_URL', 131 + 'wss://jetstream2.us-east.bsky.network' 132 + ), 133 + ], 134 + ]; 135 + ``` 136 + 137 + ## Firehose Mode 138 + 139 + Firehose is the **raw AT Protocol event stream** with comprehensive support for all collections. 140 + 141 + ### When to Use Firehose 142 + 143 + Choose Firehose if you're: 144 + 145 + - Building comprehensive indexing systems 146 + - Developing AT Protocol infrastructure 147 + - Need access to raw CBOR/CAR data 148 + - Prefer client-side filtering control 149 + 150 + ### Configuration 151 + 152 + Set Firehose as your mode in `.env`: 153 + 154 + ```env 155 + SIGNAL_MODE=firehose 156 + SIGNAL_FIREHOSE_HOST=bsky.network 157 + ``` 158 + 159 + ### Advantages 160 + 161 + **1. Raw Event Access** 162 + 163 + Full access to raw AT Protocol data: 164 + 165 + ```php 166 + public function handle(SignalEvent $event): void 167 + { 168 + // Access raw CBOR/CAR data 169 + $cid = $event->commit->cid; 170 + $blocks = $event->commit->blocks; 171 + } 172 + ``` 173 + 174 + **2. Comprehensive Events** 175 + 176 + Every event from the AT Protocol network arrives: 177 + 178 + - All collections (standard and custom) 179 + - All operations (create, update, delete) 180 + - All metadata and context 181 + - Complete repository commits 182 + 183 + **3. Complete Control** 184 + 185 + Full access to raw AT Protocol data: 186 + 187 + - CID (Content Identifiers) 188 + - Block structures 189 + - CAR file data 190 + - Complete repository commits 191 + 192 + ### Trade-offs 193 + 194 + **1. Client-Side Filtering** 195 + 196 + All filtering happens in your application: 197 + 198 + ```php 199 + public function collections(): ?array 200 + { 201 + return ['app.bsky.feed.post']; // Still receives all events 202 + } 203 + ``` 204 + 205 + Your app receives everything and filters locally. 206 + 207 + **2. Higher Bandwidth** 208 + 209 + Receive the full event stream: 210 + 211 + - **~50,000+ events per second** during peak times 212 + - **~10-50 MB/s** of data throughput 213 + - Requires adequate network capacity 214 + 215 + **3. More Processing Overhead** 216 + 217 + Complex CBOR/CAR decoding: 218 + 219 + ```php 220 + // Signal automatically handles decoding using revolution/laravel-bluesky 221 + $record = $event->getRecord(); // Decoded from CBOR/CAR 222 + ``` 223 + 224 + Processing is more CPU-intensive than Jetstream's JSON. 225 + 226 + **4. Requires revolution/laravel-bluesky** 227 + 228 + Firehose mode depends on the `revolution/laravel-bluesky` package for decoding: 229 + 230 + ```bash 231 + composer require revolution/bluesky 232 + ``` 233 + 234 + Signal handles this dependency automatically. 235 + 236 + ### Example Configuration 237 + 238 + ```php 239 + // config/signal.php 240 + return [ 241 + 'mode' => env('SIGNAL_MODE', 'jetstream'), 242 + 243 + 'firehose' => [ 244 + 'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'), 245 + ], 246 + ]; 247 + ``` 248 + 249 + The WebSocket URL is constructed as: 250 + ``` 251 + wss://{host}/xrpc/com.atproto.sync.subscribeRepos 252 + ``` 253 + 254 + ## Choosing the Right Mode 255 + 256 + ### Decision Tree 257 + 258 + ``` 259 + Do you need raw CBOR/CAR access? 260 + โ”œโ”€ Yes โ†’ Use Firehose 261 + โ””โ”€ No 262 + โ”‚ 263 + Do you want server-side filtering? 264 + โ”œโ”€ Yes โ†’ Use Jetstream (recommended) 265 + โ””โ”€ No โ†’ Use Firehose 266 + ``` 267 + 268 + ### Use Case Examples 269 + 270 + **Social Media Analytics (Jetstream)** 271 + 272 + ```php 273 + // Efficient monitoring with server-side filtering 274 + public function collections(): ?array 275 + { 276 + return [ 277 + 'app.bsky.feed.post', 278 + 'app.bsky.feed.like', 279 + 'app.bsky.graph.follow', 280 + ]; 281 + } 282 + ``` 283 + 284 + **Content Moderation (Jetstream)** 285 + 286 + ```php 287 + // Standard content monitoring 288 + public function collections(): ?array 289 + { 290 + return ['app.bsky.feed.*']; 291 + } 292 + ``` 293 + 294 + **Comprehensive Indexer (Firehose)** 295 + 296 + ```php 297 + // Index everything with raw data access 298 + public function collections(): ?array 299 + { 300 + return null; // All collections 301 + } 302 + ``` 303 + 304 + ## Switching Between Modes 305 + 306 + You can switch modes without code changes: 307 + 308 + ### Option 1: Environment Variable 309 + 310 + ```env 311 + # Development - comprehensive testing 312 + SIGNAL_MODE=firehose 313 + 314 + # Production - efficient processing 315 + SIGNAL_MODE=jetstream 316 + ``` 317 + 318 + ### Option 2: Runtime Configuration 319 + 320 + ```php 321 + use SocialDept\AtpSignals\Facades\Signal; 322 + 323 + // Set mode dynamically 324 + config(['signal.mode' => 'jetstream']); 325 + 326 + Signal::start(); 327 + ``` 328 + 329 + ## Performance Comparison 330 + 331 + ### Bandwidth Usage 332 + 333 + **Processing 1 hour of posts:** 334 + 335 + | Mode | Data Received | Bandwidth | 336 + |-----------|-------------------|-----------| 337 + | Jetstream | ~50,000 events | ~10 MB | 338 + | Firehose | ~5,000,000 events | ~500 MB | 339 + 340 + **Savings:** 50x reduction with Jetstream 341 + 342 + ### CPU Usage 343 + 344 + **Processing same events:** 345 + 346 + | Mode | CPU Usage | Processing Time | 347 + |-----------|-----------|-----------------| 348 + | Jetstream | ~5% | 0.1ms per event | 349 + | Firehose | ~20% | 0.4ms per event | 350 + 351 + **Savings:** 4x more efficient with Jetstream 352 + 353 + ### Cost Implications 354 + 355 + For a medium-traffic application: 356 + 357 + | Mode | Monthly Bandwidth | Est. Cost* | 358 + |-----------|-------------------|------------| 359 + | Jetstream | ~20 GB | ~$2 | 360 + | Firehose | ~10 TB | ~$1000 | 361 + 362 + *Estimates vary by provider and usage 363 + 364 + ## Best Practices 365 + 366 + ### Start with Jetstream 367 + 368 + Start with Jetstream for most applications: 369 + 370 + ```env 371 + SIGNAL_MODE=jetstream 372 + ``` 373 + 374 + Switch to Firehose only if you need raw CBOR/CAR access. 375 + 376 + ### Use Firehose for Development 377 + 378 + Test with Firehose in development to see all events: 379 + 380 + ```env 381 + # .env.local 382 + SIGNAL_MODE=firehose 383 + 384 + # .env.production 385 + SIGNAL_MODE=jetstream 386 + ``` 387 + 388 + ### Monitor Performance 389 + 390 + Track your Signal's performance: 391 + 392 + ```php 393 + public function handle(SignalEvent $event): void 394 + { 395 + $start = microtime(true); 396 + 397 + // Your logic 398 + 399 + $duration = microtime(true) - $start; 400 + 401 + if ($duration > 0.1) { 402 + Log::warning('Slow signal processing', [ 403 + 'signal' => static::class, 404 + 'duration' => $duration, 405 + 'mode' => config('signal.mode'), 406 + ]); 407 + } 408 + } 409 + ``` 410 + 411 + ### Use Queues with Firehose 412 + 413 + Firehose generates high volume. Use queues to avoid blocking: 414 + 415 + ```php 416 + public function shouldQueue(): bool 417 + { 418 + // Queue when using Firehose 419 + return config('signal.mode') === 'firehose'; 420 + } 421 + ``` 422 + 423 + [Learn more about queue integration โ†’](queues.md) 424 + 425 + ## Testing Both Modes 426 + 427 + Test your Signals work in both modes: 428 + 429 + ```bash 430 + # Test with Jetstream 431 + SIGNAL_MODE=jetstream php artisan signal:test MySignal 432 + 433 + # Test with Firehose 434 + SIGNAL_MODE=firehose php artisan signal:test MySignal 435 + ``` 436 + 437 + [Learn more about testing โ†’](testing.md) 438 + 439 + ## Common Questions 440 + 441 + ### Can I use both modes simultaneously? 442 + 443 + No, each consumer runs in one mode. However, you can run multiple consumers: 444 + 445 + ```bash 446 + # Terminal 1 - Jetstream consumer 447 + SIGNAL_MODE=jetstream php artisan signal:consume 448 + 449 + # Terminal 2 - Firehose consumer 450 + SIGNAL_MODE=firehose php artisan signal:consume 451 + ``` 452 + 453 + ### Will my Signals break if I switch modes? 454 + 455 + Signals work in both modes without changes. The main difference is: 456 + - Jetstream provides server-side filtering (more efficient) 457 + - Firehose provides raw CBOR/CAR data access (more comprehensive) 458 + 459 + ### How do I know which mode I'm using? 460 + 461 + Check at runtime: 462 + 463 + ```php 464 + $mode = config('signal.mode'); // 'jetstream' or 'firehose' 465 + ``` 466 + 467 + Or via Facade: 468 + 469 + ```php 470 + use SocialDept\AtpSignals\Facades\Signal; 471 + 472 + $mode = Signal::getMode(); 473 + ``` 474 + 475 + ### Can I switch modes while consuming? 476 + 477 + No, you must restart the consumer: 478 + 479 + ```bash 480 + # Stop current consumer (Ctrl+C) 481 + 482 + # Change mode 483 + # Edit .env: SIGNAL_MODE=firehose 484 + 485 + # Start new consumer 486 + php artisan signal:consume 487 + ``` 488 + 489 + ## Next Steps 490 + 491 + - **[Learn about queue integration โ†’](queues.md)** - Handle high-volume events efficiently 492 + - **[Review configuration options โ†’](configuration.md)** - Fine-tune your setup 493 + - **[See real-world examples โ†’](examples.md)** - Learn from production patterns
+672
docs/queues.md
···
··· 1 + # Queue Integration 2 + 3 + Processing AT Protocol events can be resource-intensive. Signal's queue integration lets you handle events asynchronously, preventing bottlenecks and improving performance. 4 + 5 + ## Why Use Queues? 6 + 7 + ### Without Queues (Synchronous) 8 + 9 + ```php 10 + public function handle(SignalEvent $event): void 11 + { 12 + $this->performExpensiveAnalysis($event); // Blocks for 2 seconds 13 + $this->sendNotifications($event); // Blocks for 1 second 14 + $this->updateDatabase($event); // Blocks for 0.5 seconds 15 + } 16 + ``` 17 + 18 + **Problems:** 19 + - Consumer blocks while processing (3.5 seconds per event) 20 + - Events queue up during slow operations 21 + - Risk of disconnection during long processing 22 + - Can't scale horizontally 23 + - Memory issues with long-running processes 24 + 25 + ### With Queues (Asynchronous) 26 + 27 + ```php 28 + public function shouldQueue(): bool 29 + { 30 + return true; 31 + } 32 + 33 + public function handle(SignalEvent $event): void 34 + { 35 + $this->performExpensiveAnalysis($event); // Runs in background 36 + $this->sendNotifications($event); // Runs in background 37 + $this->updateDatabase($event); // Runs in background 38 + } 39 + ``` 40 + 41 + **Benefits:** 42 + - Consumer stays responsive 43 + - Processing happens in parallel 44 + - Scale by adding queue workers 45 + - Better memory management 46 + - Automatic retry on failures 47 + 48 + ## Basic Queue Configuration 49 + 50 + ### Enable Queueing 51 + 52 + Simply return `true` from `shouldQueue()`: 53 + 54 + ```php 55 + class MySignal extends Signal 56 + { 57 + public function eventTypes(): array 58 + { 59 + return ['commit']; 60 + } 61 + 62 + public function shouldQueue(): bool 63 + { 64 + return true; // Enable queuing 65 + } 66 + 67 + public function handle(SignalEvent $event): void 68 + { 69 + // This now runs in a queue job 70 + } 71 + } 72 + ``` 73 + 74 + That's it! Signal automatically: 75 + - Creates a queue job for each event 76 + - Serializes the event data 77 + - Dispatches to Laravel's queue system 78 + - Handles retries and failures 79 + 80 + ### Default Queue Configuration 81 + 82 + Signal uses your Laravel queue configuration: 83 + 84 + ```env 85 + # Default queue connection 86 + QUEUE_CONNECTION=redis 87 + 88 + # Signal-specific queue (optional) 89 + SIGNAL_QUEUE=signal 90 + 91 + # Signal queue connection (optional) 92 + SIGNAL_QUEUE_CONNECTION=redis 93 + ``` 94 + 95 + ## Customizing Queue Behavior 96 + 97 + ### Specify Queue Name 98 + 99 + Send events to a specific queue: 100 + 101 + ```php 102 + public function shouldQueue(): bool 103 + { 104 + return true; 105 + } 106 + 107 + public function queue(): string 108 + { 109 + return 'high-priority'; // Queue name 110 + } 111 + ``` 112 + 113 + Now your events go to the `high-priority` queue: 114 + 115 + ```bash 116 + php artisan queue:work --queue=high-priority 117 + ``` 118 + 119 + ### Specify Queue Connection 120 + 121 + Use a different queue connection: 122 + 123 + ```php 124 + public function shouldQueue(): bool 125 + { 126 + return true; 127 + } 128 + 129 + public function queueConnection(): string 130 + { 131 + return 'redis'; // Connection name 132 + } 133 + ``` 134 + 135 + ### Combine Queue Configuration 136 + 137 + ```php 138 + public function shouldQueue(): bool 139 + { 140 + return true; 141 + } 142 + 143 + public function queueConnection(): string 144 + { 145 + return 'redis'; 146 + } 147 + 148 + public function queue(): string 149 + { 150 + return 'signal-events'; 151 + } 152 + ``` 153 + 154 + ## Running Queue Workers 155 + 156 + ### Start a Worker 157 + 158 + Process queued events: 159 + 160 + ```bash 161 + php artisan queue:work 162 + ``` 163 + 164 + ### Process Specific Queue 165 + 166 + ```bash 167 + php artisan queue:work --queue=signal 168 + ``` 169 + 170 + ### Multiple Queues with Priority 171 + 172 + Process high-priority queue first: 173 + 174 + ```bash 175 + php artisan queue:work --queue=high-priority,default 176 + ``` 177 + 178 + ### Scale with Multiple Workers 179 + 180 + Run multiple workers for throughput: 181 + 182 + ```bash 183 + # Terminal 1 184 + php artisan queue:work --queue=signal 185 + 186 + # Terminal 2 187 + php artisan queue:work --queue=signal 188 + 189 + # Terminal 3 190 + php artisan queue:work --queue=signal 191 + ``` 192 + 193 + ### Supervisor Configuration 194 + 195 + For production, use Supervisor to manage workers: 196 + 197 + ```ini 198 + [program:signal-queue-worker] 199 + process_name=%(program_name)s_%(process_num)02d 200 + command=php /path/to/artisan queue:work --sleep=3 --tries=3 --queue=signal 201 + autostart=true 202 + autorestart=true 203 + stopasgroup=true 204 + killasgroup=true 205 + user=www-data 206 + numprocs=4 207 + redirect_stderr=true 208 + stdout_logfile=/path/to/logs/signal-worker.log 209 + stopwaitsecs=3600 210 + ``` 211 + 212 + This creates 4 workers processing the `signal` queue. 213 + 214 + ## Error Handling 215 + 216 + ### Failed Method 217 + 218 + Handle job failures: 219 + 220 + ```php 221 + public function shouldQueue(): bool 222 + { 223 + return true; 224 + } 225 + 226 + public function handle(SignalEvent $event): void 227 + { 228 + // Your logic that might fail 229 + $this->riskyOperation($event); 230 + } 231 + 232 + public function failed(SignalEvent $event, \Throwable $exception): void 233 + { 234 + Log::error('Signal processing failed', [ 235 + 'signal' => static::class, 236 + 'did' => $event->did, 237 + 'collection' => $event->getCollection(), 238 + 'error' => $exception->getMessage(), 239 + 'trace' => $exception->getTraceAsString(), 240 + ]); 241 + 242 + // Optional: Send alerts 243 + $this->notifyAdmin($exception); 244 + 245 + // Optional: Store for manual review 246 + FailedSignal::create([ 247 + 'event_data' => $event->toArray(), 248 + 'exception' => $exception->getMessage(), 249 + ]); 250 + } 251 + ``` 252 + 253 + ### Automatic Retries 254 + 255 + Laravel automatically retries failed jobs: 256 + 257 + ```bash 258 + # Retry up to 3 times 259 + php artisan queue:work --tries=3 260 + ``` 261 + 262 + Configure retry delay: 263 + 264 + ```php 265 + public function retryAfter(): int 266 + { 267 + return 60; // Wait 60 seconds before retry 268 + } 269 + ``` 270 + 271 + ### Exponential Backoff 272 + 273 + Increase delay between retries: 274 + 275 + ```php 276 + public function backoff(): array 277 + { 278 + return [10, 30, 60]; // 10s, then 30s, then 60s 279 + } 280 + ``` 281 + 282 + ## Performance Optimization 283 + 284 + ### Batch Processing 285 + 286 + Process multiple events at once: 287 + 288 + ```php 289 + use Illuminate\Support\Collection; 290 + 291 + class BatchPostSignal extends Signal 292 + { 293 + public function shouldQueue(): bool 294 + { 295 + return true; 296 + } 297 + 298 + public function handle(SignalEvent $event): void 299 + { 300 + // Collect events in cache 301 + $events = Cache::get('pending_posts', []); 302 + $events[] = $event->toArray(); 303 + 304 + Cache::put('pending_posts', $events, now()->addMinutes(5)); 305 + 306 + // Process in batches of 100 307 + if (count($events) >= 100) { 308 + $this->processBatch($events); 309 + Cache::forget('pending_posts'); 310 + } 311 + } 312 + 313 + private function processBatch(array $events): void 314 + { 315 + // Bulk insert, API calls, etc. 316 + } 317 + } 318 + ``` 319 + 320 + ### Conditional Queuing 321 + 322 + Queue only expensive operations: 323 + 324 + ```php 325 + public function shouldQueue(): bool 326 + { 327 + // Queue during high traffic 328 + return now()->hour >= 9 && now()->hour <= 17; 329 + } 330 + ``` 331 + 332 + Or based on event type: 333 + 334 + ```php 335 + public function handle(SignalEvent $event): void 336 + { 337 + if ($this->isExpensive($event)) { 338 + dispatch(function () use ($event) { 339 + $this->handleExpensive($event); 340 + })->onQueue('slow-operations'); 341 + } else { 342 + $this->handleQuick($event); 343 + } 344 + } 345 + ``` 346 + 347 + ### Rate Limiting 348 + 349 + Prevent overwhelming external APIs: 350 + 351 + ```php 352 + use Illuminate\Support\Facades\RateLimiter; 353 + 354 + public function handle(SignalEvent $event): void 355 + { 356 + RateLimiter::attempt( 357 + 'api-calls', 358 + $perMinute = 100, 359 + function () use ($event) { 360 + $this->callExternalAPI($event); 361 + } 362 + ); 363 + } 364 + ``` 365 + 366 + ## Common Patterns 367 + 368 + ### High-Volume Signal 369 + 370 + Process millions of events efficiently: 371 + 372 + ```php 373 + class HighVolumeSignal extends Signal 374 + { 375 + public function eventTypes(): array 376 + { 377 + return ['commit']; 378 + } 379 + 380 + public function collections(): ?array 381 + { 382 + return ['app.bsky.feed.post']; 383 + } 384 + 385 + public function shouldQueue(): bool 386 + { 387 + return true; 388 + } 389 + 390 + public function queue(): string 391 + { 392 + return 'high-volume'; 393 + } 394 + 395 + public function handle(SignalEvent $event): void 396 + { 397 + // Lightweight processing only 398 + $this->incrementCounter($event); 399 + } 400 + } 401 + ``` 402 + 403 + Run many workers: 404 + 405 + ```bash 406 + # 10 workers on high-volume queue 407 + php artisan queue:work --queue=high-volume --workers=10 408 + ``` 409 + 410 + ### Priority Queues 411 + 412 + Different priorities for different events: 413 + 414 + ```php 415 + class PrioritySignal extends Signal 416 + { 417 + public function shouldQueue(): bool 418 + { 419 + return true; 420 + } 421 + 422 + public function queue(): string 423 + { 424 + // Determine priority based on event 425 + return $this->getQueueForEvent(); 426 + } 427 + 428 + private function getQueueForEvent(): string 429 + { 430 + // Check event attributes 431 + // Return 'high', 'medium', or 'low' 432 + } 433 + } 434 + ``` 435 + 436 + Process high-priority first: 437 + 438 + ```bash 439 + php artisan queue:work --queue=high,medium,low 440 + ``` 441 + 442 + ### Delayed Processing 443 + 444 + Delay event processing: 445 + 446 + ```php 447 + public function handle(SignalEvent $event): void 448 + { 449 + // Dispatch with delay 450 + dispatch(function () use ($event) { 451 + $this->processLater($event); 452 + })->delay(now()->addMinutes(5)); 453 + } 454 + ``` 455 + 456 + ### Scheduled Batch Processing 457 + 458 + Collect events and process on schedule: 459 + 460 + ```php 461 + // Signal collects events 462 + class CollectorSignal extends Signal 463 + { 464 + public function handle(SignalEvent $event): void 465 + { 466 + PendingEvent::create([ 467 + 'data' => $event->toArray(), 468 + ]); 469 + } 470 + } 471 + 472 + // Scheduled command processes batch 473 + // app/Console/Kernel.php 474 + protected function schedule(Schedule $schedule) 475 + { 476 + $schedule->call(function () { 477 + $events = PendingEvent::all(); 478 + $this->processBatch($events); 479 + PendingEvent::truncate(); 480 + })->hourly(); 481 + } 482 + ``` 483 + 484 + ## Monitoring Queues 485 + 486 + ### Check Queue Status 487 + 488 + ```bash 489 + # View failed jobs 490 + php artisan queue:failed 491 + 492 + # Retry failed job 493 + php artisan queue:retry {id} 494 + 495 + # Retry all failed 496 + php artisan queue:retry all 497 + 498 + # Clear failed jobs 499 + php artisan queue:flush 500 + ``` 501 + 502 + ### Queue Metrics 503 + 504 + Track queue performance: 505 + 506 + ```php 507 + use Illuminate\Support\Facades\Queue; 508 + 509 + Queue::after(function ($connection, $job, $data) { 510 + Log::info('Job processed', [ 511 + 'queue' => $job->queue, 512 + 'class' => $job->resolveName(), 513 + 'attempts' => $job->attempts(), 514 + ]); 515 + }); 516 + ``` 517 + 518 + ### Horizon (Recommended) 519 + 520 + Use Laravel Horizon for Redis queues: 521 + 522 + ```bash 523 + composer require laravel/horizon 524 + php artisan horizon:install 525 + php artisan horizon 526 + ``` 527 + 528 + View dashboard at `/horizon`. 529 + 530 + ## Testing Queued Signals 531 + 532 + ### Test with Fake Queue 533 + 534 + ```php 535 + use Illuminate\Support\Facades\Queue; 536 + 537 + /** @test */ 538 + public function it_queues_events() 539 + { 540 + Queue::fake(); 541 + 542 + $signal = new MySignal(); 543 + $event = $this->createSampleEvent(); 544 + 545 + // Assert queue behavior 546 + $this->assertTrue($signal->shouldQueue()); 547 + 548 + // Process would normally queue 549 + $signal->handle($event); 550 + 551 + // Verify job was queued 552 + Queue::assertPushed(SignalJob::class); 553 + } 554 + ``` 555 + 556 + ### Test Synchronously 557 + 558 + Disable queueing for tests: 559 + 560 + ```php 561 + /** @test */ 562 + public function it_processes_events() 563 + { 564 + config(['queue.default' => 'sync']); 565 + 566 + $signal = new MySignal(); 567 + $event = $this->createSampleEvent(); 568 + 569 + $signal->handle($event); 570 + 571 + // Assert processing happened 572 + $this->assertDatabaseHas('posts', [...]); 573 + } 574 + ``` 575 + 576 + [Learn more about testing โ†’](testing.md) 577 + 578 + ## Production Checklist 579 + 580 + ### Infrastructure 581 + 582 + - [ ] Queue driver configured (Redis recommended) 583 + - [ ] Supervisor installed and configured 584 + - [ ] Multiple workers running 585 + - [ ] Worker auto-restart enabled 586 + - [ ] Logs configured and monitored 587 + 588 + ### Configuration 589 + 590 + - [ ] Queue connection set correctly 591 + - [ ] Queue names configured 592 + - [ ] Retry attempts configured 593 + - [ ] Timeout values appropriate 594 + - [ ] Memory limits set 595 + 596 + ### Monitoring 597 + 598 + - [ ] Queue length monitored 599 + - [ ] Failed jobs tracked 600 + - [ ] Worker health checked 601 + - [ ] Processing times measured 602 + - [ ] Horizon installed (if using Redis) 603 + 604 + ### Scaling 605 + 606 + - [ ] Worker count appropriate for volume 607 + - [ ] Priority queues configured 608 + - [ ] Rate limiting implemented 609 + - [ ] Database connection pooling enabled 610 + - [ ] Redis maxmemory policy set 611 + 612 + ## Common Issues 613 + 614 + ### Queue Jobs Not Processing 615 + 616 + **Check worker is running:** 617 + ```bash 618 + php artisan queue:work 619 + ``` 620 + 621 + **Check queue connection:** 622 + ```php 623 + // Should match QUEUE_CONNECTION 624 + config('queue.default') 625 + ``` 626 + 627 + ### Jobs Timing Out 628 + 629 + **Increase timeout:** 630 + ```bash 631 + php artisan queue:work --timeout=300 632 + ``` 633 + 634 + **Or in Signal:** 635 + ```php 636 + public function timeout(): int 637 + { 638 + return 300; // 5 minutes 639 + } 640 + ``` 641 + 642 + ### Memory Leaks 643 + 644 + **Restart workers periodically:** 645 + ```bash 646 + php artisan queue:work --max-jobs=1000 647 + ``` 648 + 649 + Or: 650 + ```bash 651 + php artisan queue:work --max-time=3600 652 + ``` 653 + 654 + ### Failed Jobs Piling Up 655 + 656 + **Review failures:** 657 + ```bash 658 + php artisan queue:failed 659 + ``` 660 + 661 + **Retry or delete:** 662 + ```bash 663 + php artisan queue:retry all 664 + # or 665 + php artisan queue:flush 666 + ``` 667 + 668 + ## Next Steps 669 + 670 + - **[Review configuration options โ†’](configuration.md)** - Fine-tune queue settings 671 + - **[Learn about testing โ†’](testing.md)** - Test queued Signals 672 + - **[See real-world examples โ†’](examples.md)** - Learn from production queue patterns
+391
docs/quickstart.md
···
··· 1 + # Quickstart Guide 2 + 3 + This guide will walk you through building your first Signal and consuming AT Protocol events in under 5 minutes. 4 + 5 + ## Prerequisites 6 + 7 + Before starting, ensure you have: 8 + 9 + - [Installed Signal](installation.md) in your Laravel application 10 + - Run `php artisan signal:install` successfully 11 + - Basic familiarity with Laravel 12 + 13 + ## Your First Signal 14 + 15 + We'll build a Signal that logs every new post created on Bluesky. 16 + 17 + ### Step 1: Generate a Signal 18 + 19 + Use the Artisan command to create a new Signal: 20 + 21 + ```bash 22 + php artisan make:signal NewPostSignal 23 + ``` 24 + 25 + This creates `app/Signals/NewPostSignal.php` with a basic template. 26 + 27 + ### Step 2: Define the Signal 28 + 29 + Open the generated file and update it: 30 + 31 + ```php 32 + <?php 33 + 34 + namespace App\Signals; 35 + 36 + use SocialDept\AtpSignals\Events\SignalEvent; 37 + use SocialDept\AtpSignals\Signals\Signal; 38 + use Illuminate\Support\Facades\Log; 39 + 40 + class NewPostSignal extends Signal 41 + { 42 + /** 43 + * Define which event types to listen for. 44 + */ 45 + public function eventTypes(): array 46 + { 47 + return ['commit']; // Listen for repository commits 48 + } 49 + 50 + /** 51 + * Filter by specific collections. 52 + */ 53 + public function collections(): ?array 54 + { 55 + return ['app.bsky.feed.post']; // Only handle posts 56 + } 57 + 58 + /** 59 + * Handle the event when it arrives. 60 + */ 61 + public function handle(SignalEvent $event): void 62 + { 63 + $record = $event->getRecord(); 64 + 65 + Log::info('New post created', [ 66 + 'author' => $event->did, 67 + 'text' => $record->text ?? null, 68 + 'created_at' => $record->createdAt ?? null, 69 + ]); 70 + } 71 + } 72 + ``` 73 + 74 + ### Step 3: Start Consuming Events 75 + 76 + Run the consumer to start listening: 77 + 78 + ```bash 79 + php artisan signal:consume 80 + ``` 81 + 82 + You should see output like: 83 + 84 + ``` 85 + Starting Signal consumer in jetstream mode... 86 + Connecting to wss://jetstream2.us-east.bsky.network... 87 + Connected! Listening for events... 88 + ``` 89 + 90 + **Congratulations!** Your Signal is now processing every new post on Bluesky in real-time. Check your Laravel logs to see the posts coming in. 91 + 92 + ## Understanding What Just Happened 93 + 94 + Let's break down the Signal you created: 95 + 96 + ### Event Types 97 + 98 + ```php 99 + public function eventTypes(): array 100 + { 101 + return ['commit']; 102 + } 103 + ``` 104 + 105 + This tells Signal you want **commit** events, which represent changes to repositories (like creating posts, likes, follows, etc.). 106 + 107 + Available event types: 108 + - `commit` - Repository commits (most common) 109 + - `identity` - Identity changes (handle updates) 110 + - `account` - Account status changes 111 + 112 + ### Collections 113 + 114 + ```php 115 + public function collections(): ?array 116 + { 117 + return ['app.bsky.feed.post']; 118 + } 119 + ``` 120 + 121 + This filters to only **post** collections. Without this filter, your Signal would receive all commit events for every collection type. 122 + 123 + Common collections: 124 + - `app.bsky.feed.post` - Posts 125 + - `app.bsky.feed.like` - Likes 126 + - `app.bsky.graph.follow` - Follows 127 + - `app.bsky.feed.repost` - Reposts 128 + 129 + [Learn more about filtering โ†’](filtering.md) 130 + 131 + ### Handler Method 132 + 133 + ```php 134 + public function handle(SignalEvent $event): void 135 + { 136 + $record = $event->getRecord(); 137 + // Your logic here 138 + } 139 + ``` 140 + 141 + This is where your code runs for each matching event. The `$event` object contains: 142 + 143 + - `did` - The user's DID (decentralized identifier) 144 + - `timeUs` - Timestamp in microseconds 145 + - `commit` - Commit details (collection, operation, record key) 146 + - `getRecord()` - The actual record data 147 + 148 + ## Next Steps 149 + 150 + Now that you've built your first Signal, let's make it more useful. 151 + 152 + ### Add More Filtering 153 + 154 + Track specific operations only: 155 + 156 + ```php 157 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 158 + 159 + public function operations(): ?array 160 + { 161 + return [SignalCommitOperation::Create]; // Only new posts, not edits 162 + } 163 + ``` 164 + 165 + [Learn more about filtering โ†’](filtering.md) 166 + 167 + ### Process Events Asynchronously 168 + 169 + For expensive operations, use Laravel queues: 170 + 171 + ```php 172 + public function shouldQueue(): bool 173 + { 174 + return true; 175 + } 176 + 177 + public function handle(SignalEvent $event): void 178 + { 179 + // This now runs in a background job 180 + $this->performExpensiveAnalysis($event); 181 + } 182 + ``` 183 + 184 + [Learn more about queues โ†’](queues.md) 185 + 186 + ### Store Data 187 + 188 + Let's store posts in your database: 189 + 190 + ```php 191 + use App\Models\Post; 192 + 193 + public function handle(SignalEvent $event): void 194 + { 195 + $record = $event->getRecord(); 196 + 197 + Post::updateOrCreate( 198 + [ 199 + 'did' => $event->did, 200 + 'rkey' => $event->commit->rkey, 201 + ], 202 + [ 203 + 'text' => $record->text ?? null, 204 + 'created_at' => $record->createdAt, 205 + ] 206 + ); 207 + } 208 + ``` 209 + 210 + ### Handle Multiple Collections 211 + 212 + Use wildcards to match multiple collections: 213 + 214 + ```php 215 + public function collections(): ?array 216 + { 217 + return [ 218 + 'app.bsky.feed.*', // All feed events 219 + ]; 220 + } 221 + 222 + public function handle(SignalEvent $event): void 223 + { 224 + $collection = $event->getCollection(); 225 + 226 + match ($collection) { 227 + 'app.bsky.feed.post' => $this->handlePost($event), 228 + 'app.bsky.feed.like' => $this->handleLike($event), 229 + 'app.bsky.feed.repost' => $this->handleRepost($event), 230 + default => null, 231 + }; 232 + } 233 + ``` 234 + 235 + ## Building Something Real 236 + 237 + Let's build a simple engagement tracker: 238 + 239 + ```php 240 + <?php 241 + 242 + namespace App\Signals; 243 + 244 + use App\Models\EngagementMetric; 245 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 246 + use SocialDept\AtpSignals\Events\SignalEvent; 247 + use SocialDept\AtpSignals\Signals\Signal; 248 + 249 + class EngagementTrackerSignal extends Signal 250 + { 251 + public function eventTypes(): array 252 + { 253 + return ['commit']; 254 + } 255 + 256 + public function collections(): ?array 257 + { 258 + return [ 259 + 'app.bsky.feed.post', 260 + 'app.bsky.feed.like', 261 + 'app.bsky.feed.repost', 262 + ]; 263 + } 264 + 265 + public function operations(): ?array 266 + { 267 + return [SignalCommitOperation::Create]; 268 + } 269 + 270 + public function shouldQueue(): bool 271 + { 272 + return true; // Process in background 273 + } 274 + 275 + public function handle(SignalEvent $event): void 276 + { 277 + EngagementMetric::create([ 278 + 'date' => now()->toDateString(), 279 + 'collection' => $event->getCollection(), 280 + 'event_type' => 'create', 281 + 'count' => 1, 282 + ]); 283 + } 284 + } 285 + ``` 286 + 287 + This Signal tracks all engagement activity (posts, likes, reposts) and stores metrics for analysis. 288 + 289 + ## Testing Your Signal 290 + 291 + Before running in production, test your Signal with sample data: 292 + 293 + ```bash 294 + php artisan signal:test NewPostSignal 295 + ``` 296 + 297 + This will run your Signal with a sample event and show you the output. 298 + 299 + [Learn more about testing โ†’](testing.md) 300 + 301 + ## Common Patterns 302 + 303 + ### Only Process Specific Users 304 + 305 + ```php 306 + public function dids(): ?array 307 + { 308 + return [ 309 + 'did:plc:z72i7hdynmk6r22z27h6tvur', // Specific user 310 + ]; 311 + } 312 + ``` 313 + 314 + ### Add Custom Filtering Logic 315 + 316 + ```php 317 + public function shouldHandle(SignalEvent $event): bool 318 + { 319 + $record = $event->getRecord(); 320 + 321 + // Only handle posts with images 322 + return isset($record->embed); 323 + } 324 + ``` 325 + 326 + ### Handle Failures Gracefully 327 + 328 + ```php 329 + public function failed(SignalEvent $event, \Throwable $exception): void 330 + { 331 + Log::error('Signal processing failed', [ 332 + 'event' => $event->toArray(), 333 + 'error' => $exception->getMessage(), 334 + ]); 335 + 336 + // Optionally notify admins, store for retry, etc. 337 + } 338 + ``` 339 + 340 + ## Running in Production 341 + 342 + ### Using Supervisor 343 + 344 + For production, run Signal under a process monitor like Supervisor: 345 + 346 + ```ini 347 + [program:signal-consumer] 348 + process_name=%(program_name)s 349 + command=php /path/to/artisan signal:consume 350 + autostart=true 351 + autorestart=true 352 + user=www-data 353 + redirect_stderr=true 354 + stdout_logfile=/path/to/logs/signal-consumer.log 355 + ``` 356 + 357 + ### Starting from Last Position 358 + 359 + Signal automatically saves cursor positions, so it resumes from where it left off: 360 + 361 + ```bash 362 + php artisan signal:consume 363 + ``` 364 + 365 + To start fresh and ignore stored position: 366 + 367 + ```bash 368 + php artisan signal:consume --fresh 369 + ``` 370 + 371 + To start from a specific cursor: 372 + 373 + ```bash 374 + php artisan signal:consume --cursor=123456789 375 + ``` 376 + 377 + ## What's Next? 378 + 379 + You now know the basics of building Signals! Explore more advanced topics: 380 + 381 + - **[Signal Architecture](signals.md)** - Deep dive into Signal structure 382 + - **[Advanced Filtering](filtering.md)** - Master collection patterns and wildcards 383 + - **[Jetstream vs Firehose](modes.md)** - Choose the right mode for your use case 384 + - **[Queue Integration](queues.md)** - Build high-performance processors 385 + - **[Real-World Examples](examples.md)** - Learn from production use cases 386 + 387 + ## Getting Help 388 + 389 + - Check the [examples documentation](examples.md) for more patterns 390 + - Review the [configuration guide](configuration.md) for all options 391 + - Open an issue on GitHub if you encounter problems
+702
docs/signals.md
···
··· 1 + # Creating Signals 2 + 3 + Signals are the heart of the Signal package. They define how your application responds to AT Protocol events. 4 + 5 + ## What is a Signal? 6 + 7 + A **Signal** is a PHP class that: 8 + 9 + 1. Listens for specific types of AT Protocol events 10 + 2. Filters those events based on your criteria 11 + 3. Executes custom logic when matching events arrive 12 + 13 + Think of Signals like Laravel event listeners, but specifically designed for the AT Protocol. 14 + 15 + ## Basic Signal Structure 16 + 17 + Every Signal extends the base `Signal` class: 18 + 19 + ```php 20 + <?php 21 + 22 + namespace App\Signals; 23 + 24 + use SocialDept\AtpSignals\Events\SignalEvent; 25 + use SocialDept\AtpSignals\Signals\Signal; 26 + 27 + class MySignal extends Signal 28 + { 29 + /** 30 + * Define which event types to listen for. 31 + * Required. 32 + */ 33 + public function eventTypes(): array 34 + { 35 + return ['commit']; 36 + } 37 + 38 + /** 39 + * Handle the event when it arrives. 40 + * Required. 41 + */ 42 + public function handle(SignalEvent $event): void 43 + { 44 + // Your logic here 45 + } 46 + } 47 + ``` 48 + 49 + Only two methods are required: 50 + - `eventTypes()` - Which event types to listen for 51 + - `handle()` - What to do when events arrive 52 + 53 + ## Creating Signals 54 + 55 + ### Using Artisan (Recommended) 56 + 57 + Generate a new Signal with the make command: 58 + 59 + ```bash 60 + php artisan make:signal MySignal 61 + ``` 62 + 63 + This creates `app/Signals/MySignal.php` with a basic template. 64 + 65 + #### With Options 66 + 67 + Generate a Signal with pre-configured filters: 68 + 69 + ```bash 70 + # Create a Signal for posts only 71 + php artisan make:signal PostSignal --type=commit --collection=app.bsky.feed.post 72 + 73 + # Create a Signal for follows 74 + php artisan make:signal FollowSignal --type=commit --collection=app.bsky.graph.follow 75 + ``` 76 + 77 + ### Manual Creation 78 + 79 + You can also create Signals manually in `app/Signals/`: 80 + 81 + ```php 82 + <?php 83 + 84 + namespace App\Signals; 85 + 86 + use SocialDept\AtpSignals\Events\SignalEvent; 87 + use SocialDept\AtpSignals\Signals\Signal; 88 + 89 + class ManualSignal extends Signal 90 + { 91 + public function eventTypes(): array 92 + { 93 + return ['commit']; 94 + } 95 + 96 + public function handle(SignalEvent $event): void 97 + { 98 + // 99 + } 100 + } 101 + ``` 102 + 103 + Signals are automatically discovered from `app/Signals/` - no registration needed. 104 + 105 + ## Event Types 106 + 107 + Signals can listen for three types of AT Protocol events: 108 + 109 + ### Commit Events 110 + 111 + Repository commits represent changes to user data: 112 + 113 + ```php 114 + use SocialDept\AtpSignals\Enums\SignalEventType; 115 + 116 + public function eventTypes(): array 117 + { 118 + return [SignalEventType::Commit]; 119 + // Or: return ['commit']; 120 + } 121 + ``` 122 + 123 + **Common commit events:** 124 + - Creating posts, likes, follows, reposts 125 + - Updating profile information 126 + - Deleting content 127 + 128 + This is the most common event type and what you'll use 99% of the time. 129 + 130 + ### Identity Events 131 + 132 + Identity changes track handle updates: 133 + 134 + ```php 135 + public function eventTypes(): array 136 + { 137 + return [SignalEventType::Identity]; 138 + // Or: return ['identity']; 139 + } 140 + ``` 141 + 142 + **Use cases:** 143 + - Tracking handle changes 144 + - Updating local user records 145 + - Monitoring account migrations 146 + 147 + ### Account Events 148 + 149 + Account status changes track account state: 150 + 151 + ```php 152 + public function eventTypes(): array 153 + { 154 + return [SignalEventType::Account]; 155 + // Or: return ['account']; 156 + } 157 + ``` 158 + 159 + **Use cases:** 160 + - Detecting account deactivation 161 + - Monitoring account status 162 + - Compliance tracking 163 + 164 + ### Multiple Event Types 165 + 166 + Listen to multiple event types in one Signal: 167 + 168 + ```php 169 + public function eventTypes(): array 170 + { 171 + return [ 172 + SignalEventType::Commit, 173 + SignalEventType::Identity, 174 + ]; 175 + } 176 + 177 + public function handle(SignalEvent $event): void 178 + { 179 + if ($event->isCommit()) { 180 + // Handle commit 181 + } 182 + 183 + if ($event->isIdentity()) { 184 + // Handle identity change 185 + } 186 + } 187 + ``` 188 + 189 + ## The SignalEvent Object 190 + 191 + The `SignalEvent` object contains all event data: 192 + 193 + ### Common Properties 194 + 195 + ```php 196 + public function handle(SignalEvent $event): void 197 + { 198 + // User's DID (decentralized identifier) 199 + $did = $event->did; // "did:plc:z72i7hdynmk6r22z27h6tvur" 200 + 201 + // Event type (commit, identity, account) 202 + $kind = $event->kind; 203 + 204 + // Timestamp in microseconds 205 + $timestamp = $event->timeUs; 206 + 207 + // Convert to Carbon instance 208 + $date = $event->getTimestamp(); 209 + } 210 + ``` 211 + 212 + ### Commit Events 213 + 214 + For commit events, access the `commit` property: 215 + 216 + ```php 217 + public function handle(SignalEvent $event): void 218 + { 219 + if ($event->isCommit()) { 220 + // Collection (e.g., "app.bsky.feed.post") 221 + $collection = $event->commit->collection; 222 + // Or: $collection = $event->getCollection(); 223 + 224 + // Operation (create, update, delete) 225 + $operation = $event->commit->operation; 226 + // Or: $operation = $event->getOperation(); 227 + 228 + // Record key (unique identifier) 229 + $rkey = $event->commit->rkey; 230 + 231 + // Revision 232 + $rev = $event->commit->rev; 233 + 234 + // The actual record data 235 + $record = $event->commit->record; 236 + // Or: $record = $event->getRecord(); 237 + } 238 + } 239 + ``` 240 + 241 + ### Working with Records 242 + 243 + Records contain the actual data (posts, likes, etc.): 244 + 245 + ```php 246 + public function handle(SignalEvent $event): void 247 + { 248 + $record = $event->getRecord(); 249 + 250 + // For posts (app.bsky.feed.post) 251 + $text = $record->text ?? null; 252 + $createdAt = $record->createdAt ?? null; 253 + $embed = $record->embed ?? null; 254 + $facets = $record->facets ?? null; 255 + 256 + // For likes (app.bsky.feed.like) 257 + $subject = $record->subject ?? null; 258 + 259 + // For follows (app.bsky.graph.follow) 260 + $subject = $record->subject ?? null; 261 + } 262 + ``` 263 + 264 + Records are `stdClass` objects, so use null coalescing (`??`) for safety. 265 + 266 + ### Identity Events 267 + 268 + For identity events, access the `identity` property: 269 + 270 + ```php 271 + public function handle(SignalEvent $event): void 272 + { 273 + if ($event->isIdentity()) { 274 + // New handle 275 + $handle = $event->identity->handle; 276 + 277 + // User's DID 278 + $did = $event->did; 279 + 280 + // Sequence number 281 + $seq = $event->identity->seq; 282 + 283 + // Timestamp 284 + $time = $event->identity->time; 285 + } 286 + } 287 + ``` 288 + 289 + ### Account Events 290 + 291 + For account events, access the `account` property: 292 + 293 + ```php 294 + public function handle(SignalEvent $event): void 295 + { 296 + if ($event->isAccount()) { 297 + // Account status 298 + $active = $event->account->active; // true/false 299 + 300 + // Status reason 301 + $status = $event->account->status ?? null; 302 + 303 + // User's DID 304 + $did = $event->did; 305 + 306 + // Sequence number 307 + $seq = $event->account->seq; 308 + 309 + // Timestamp 310 + $time = $event->account->time; 311 + } 312 + } 313 + ``` 314 + 315 + ## Helper Methods 316 + 317 + Signals provide several helper methods for common tasks: 318 + 319 + ### Type Checking 320 + 321 + ```php 322 + public function handle(SignalEvent $event): void 323 + { 324 + // Check event type 325 + if ($event->isCommit()) { 326 + // 327 + } 328 + 329 + if ($event->isIdentity()) { 330 + // 331 + } 332 + 333 + if ($event->isAccount()) { 334 + // 335 + } 336 + } 337 + ``` 338 + 339 + ### Operation Checking (Commit Events) 340 + 341 + ```php 342 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 343 + 344 + public function handle(SignalEvent $event): void 345 + { 346 + $operation = $event->getOperation(); 347 + 348 + // Using enum 349 + if ($operation === SignalCommitOperation::Create) { 350 + // Handle new records 351 + } 352 + 353 + // Using commit helper 354 + if ($event->commit->isCreate()) { 355 + // Handle new records 356 + } 357 + 358 + if ($event->commit->isUpdate()) { 359 + // Handle updates 360 + } 361 + 362 + if ($event->commit->isDelete()) { 363 + // Handle deletions 364 + } 365 + } 366 + ``` 367 + 368 + ### Data Extraction 369 + 370 + ```php 371 + public function handle(SignalEvent $event): void 372 + { 373 + // Get collection (commit events only) 374 + $collection = $event->getCollection(); 375 + 376 + // Get operation (commit events only) 377 + $operation = $event->getOperation(); 378 + 379 + // Get record (commit events only) 380 + $record = $event->getRecord(); 381 + 382 + // Get timestamp as Carbon 383 + $timestamp = $event->getTimestamp(); 384 + 385 + // Convert to array 386 + $array = $event->toArray(); 387 + } 388 + ``` 389 + 390 + ## Optional Signal Methods 391 + 392 + Signals support several optional methods for advanced behavior: 393 + 394 + ### Collections Filter 395 + 396 + Filter by AT Protocol collections: 397 + 398 + ```php 399 + public function collections(): ?array 400 + { 401 + return ['app.bsky.feed.post']; 402 + } 403 + ``` 404 + 405 + Return `null` to handle all collections. 406 + 407 + [Learn more about collection filtering โ†’](filtering.md) 408 + 409 + ### Operations Filter 410 + 411 + Filter by operation type (commit events only): 412 + 413 + ```php 414 + public function operations(): ?array 415 + { 416 + return [SignalCommitOperation::Create]; 417 + } 418 + ``` 419 + 420 + Return `null` to handle all operations. 421 + 422 + [Learn more about operation filtering โ†’](filtering.md) 423 + 424 + ### DIDs Filter 425 + 426 + Filter by specific users: 427 + 428 + ```php 429 + public function dids(): ?array 430 + { 431 + return [ 432 + 'did:plc:z72i7hdynmk6r22z27h6tvur', 433 + ]; 434 + } 435 + ``` 436 + 437 + Return `null` to handle all users. 438 + 439 + [Learn more about DID filtering โ†’](filtering.md) 440 + 441 + ### Custom Filtering 442 + 443 + Add complex filtering logic: 444 + 445 + ```php 446 + public function shouldHandle(SignalEvent $event): bool 447 + { 448 + // Only handle posts with images 449 + if ($event->isCommit() && $event->getCollection() === 'app.bsky.feed.post') { 450 + $record = $event->getRecord(); 451 + return isset($record->embed); 452 + } 453 + 454 + return true; 455 + } 456 + ``` 457 + 458 + ### Queue Configuration 459 + 460 + Process events asynchronously: 461 + 462 + ```php 463 + public function shouldQueue(): bool 464 + { 465 + return true; 466 + } 467 + 468 + public function queue(): string 469 + { 470 + return 'high-priority'; 471 + } 472 + 473 + public function queueConnection(): string 474 + { 475 + return 'redis'; 476 + } 477 + ``` 478 + 479 + [Learn more about queue integration โ†’](queues.md) 480 + 481 + ### Failure Handling 482 + 483 + Handle processing failures: 484 + 485 + ```php 486 + public function failed(SignalEvent $event, \Throwable $exception): void 487 + { 488 + Log::error('Signal failed', [ 489 + 'signal' => static::class, 490 + 'event' => $event->toArray(), 491 + 'error' => $exception->getMessage(), 492 + ]); 493 + } 494 + ``` 495 + 496 + ## Signal Lifecycle 497 + 498 + Understanding the Signal lifecycle helps you write better Signals: 499 + 500 + ### 1. Event Arrives 501 + 502 + An event arrives from the AT Protocol (via Jetstream or Firehose). 503 + 504 + ### 2. Event Type Matching 505 + 506 + Signal checks if the event type matches your `eventTypes()` definition. 507 + 508 + ### 3. Collection Filtering 509 + 510 + If defined, Signal checks if the collection matches your `collections()` definition. 511 + 512 + ### 4. Operation Filtering 513 + 514 + If defined, Signal checks if the operation matches your `operations()` definition. 515 + 516 + ### 5. DID Filtering 517 + 518 + If defined, Signal checks if the DID matches your `dids()` definition. 519 + 520 + ### 6. Custom Filtering 521 + 522 + If defined, Signal calls your `shouldHandle()` method. 523 + 524 + ### 7. Queue Decision 525 + 526 + Signal checks `shouldQueue()` to determine if the event should be queued. 527 + 528 + ### 8. Handler Execution 529 + 530 + Your `handle()` method is called (either synchronously or via queue). 531 + 532 + ### 9. Failure Handling (if applicable) 533 + 534 + If an exception occurs, your `failed()` method is called (if defined). 535 + 536 + ## Best Practices 537 + 538 + ### Keep Handlers Focused 539 + 540 + Each Signal should do one thing well: 541 + 542 + ```php 543 + // Good - focused on one task 544 + class TrackNewPostsSignal extends Signal 545 + { 546 + public function collections(): ?array 547 + { 548 + return ['app.bsky.feed.post']; 549 + } 550 + 551 + public function handle(SignalEvent $event): void 552 + { 553 + $this->storePost($event); 554 + } 555 + } 556 + 557 + // Less ideal - doing too much 558 + class MonitorEverythingSignal extends Signal 559 + { 560 + public function handle(SignalEvent $event): void 561 + { 562 + $this->storePost($event); 563 + $this->sendNotification($event); 564 + $this->updateAnalytics($event); 565 + $this->processRecommendations($event); 566 + } 567 + } 568 + ``` 569 + 570 + ### Use Queues for Heavy Work 571 + 572 + Don't block the consumer with expensive operations: 573 + 574 + ```php 575 + class AnalyzePostSignal extends Signal 576 + { 577 + public function shouldQueue(): bool 578 + { 579 + return true; // Process in background 580 + } 581 + 582 + public function handle(SignalEvent $event): void 583 + { 584 + $this->performExpensiveAnalysis($event); 585 + } 586 + } 587 + ``` 588 + 589 + ### Validate Data Safely 590 + 591 + Records can have missing or unexpected data: 592 + 593 + ```php 594 + public function handle(SignalEvent $event): void 595 + { 596 + $record = $event->getRecord(); 597 + 598 + // Use null coalescing 599 + $text = $record->text ?? ''; 600 + 601 + // Validate before processing 602 + if (empty($text)) { 603 + return; 604 + } 605 + 606 + // Safe to process 607 + $this->processText($text); 608 + } 609 + ``` 610 + 611 + ### Add Logging 612 + 613 + Log important events for debugging: 614 + 615 + ```php 616 + public function handle(SignalEvent $event): void 617 + { 618 + Log::debug('Processing event', [ 619 + 'signal' => static::class, 620 + 'collection' => $event->getCollection(), 621 + 'operation' => $event->getOperation()->value, 622 + ]); 623 + 624 + // Your logic 625 + } 626 + ``` 627 + 628 + ### Handle Failures Gracefully 629 + 630 + Always implement failure handling for queued Signals: 631 + 632 + ```php 633 + public function failed(SignalEvent $event, \Throwable $exception): void 634 + { 635 + Log::error('Signal processing failed', [ 636 + 'signal' => static::class, 637 + 'event_did' => $event->did, 638 + 'error' => $exception->getMessage(), 639 + 'trace' => $exception->getTraceAsString(), 640 + ]); 641 + 642 + // Optionally: send to error tracking service 643 + // report($exception); 644 + } 645 + ``` 646 + 647 + ## Auto-Discovery 648 + 649 + Signals are automatically discovered from `app/Signals/` by default. You can customize discovery in `config/signal.php`: 650 + 651 + ```php 652 + 'auto_discovery' => [ 653 + 'enabled' => true, 654 + 'path' => app_path('Signals'), 655 + 'namespace' => 'App\\Signals', 656 + ], 657 + ``` 658 + 659 + ### Manual Registration 660 + 661 + Disable auto-discovery and register Signals manually: 662 + 663 + ```php 664 + 'auto_discovery' => [ 665 + 'enabled' => false, 666 + ], 667 + 668 + 'signals' => [ 669 + \App\Signals\NewPostSignal::class, 670 + \App\Signals\NewFollowSignal::class, 671 + ], 672 + ``` 673 + 674 + ## Testing Signals 675 + 676 + Test your Signals before deploying: 677 + 678 + ```bash 679 + php artisan signal:test MySignal 680 + ``` 681 + 682 + [Learn more about testing โ†’](testing.md) 683 + 684 + ## Listing Signals 685 + 686 + View all registered Signals: 687 + 688 + ```bash 689 + php artisan signal:list 690 + ``` 691 + 692 + This displays: 693 + - Signal class names 694 + - Event types they listen for 695 + - Collection filters (if any) 696 + - Queue configuration 697 + 698 + ## Next Steps 699 + 700 + - **[Learn about filtering โ†’](filtering.md)** - Master collection patterns and wildcards 701 + - **[Understand queue integration โ†’](queues.md)** - Build high-performance processors 702 + - **[See real-world examples โ†’](examples.md)** - Learn from production use cases
+728
docs/testing.md
···
··· 1 + # Testing Signals 2 + 3 + Testing your Signals ensures they behave correctly before deploying to production. Signal provides tools for both manual and automated testing. 4 + 5 + ## Quick Testing with Artisan 6 + 7 + The fastest way to test a Signal is with the `signal:test` command. 8 + 9 + ### Test a Signal 10 + 11 + ```bash 12 + php artisan signal:test NewPostSignal 13 + ``` 14 + 15 + This runs your Signal with sample event data and displays the output. 16 + 17 + ### What It Does 18 + 19 + 1. Creates a sample `SignalEvent` matching your Signal's filters 20 + 2. Calls your Signal's `handle()` method 21 + 3. Displays output, logs, and any errors 22 + 4. Shows execution time 23 + 24 + ### Example Output 25 + 26 + ``` 27 + Testing Signal: App\Signals\NewPostSignal 28 + 29 + Creating sample commit event for collection: app.bsky.feed.post 30 + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” 31 + 32 + Event Details: 33 + DID: did:plc:test123 34 + Collection: app.bsky.feed.post 35 + Operation: create 36 + Text: Sample post for testing 37 + 38 + โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” 39 + 40 + Processing event... 41 + โœ“ Signal processed successfully 42 + 43 + Execution time: 12ms 44 + ``` 45 + 46 + ### Limitations 47 + 48 + - Uses sample data (not real events) 49 + - Doesn't test filtering logic comprehensively 50 + - Can't test queue behavior 51 + - Limited to basic scenarios 52 + 53 + For comprehensive testing, write automated tests. 54 + 55 + ## Unit Testing 56 + 57 + Test your Signals in isolation. 58 + 59 + ### Basic Test Structure 60 + 61 + ```php 62 + <?php 63 + 64 + namespace Tests\Unit\Signals; 65 + 66 + use App\Signals\NewPostSignal; 67 + use SocialDept\AtpSignals\Events\CommitEvent; 68 + use SocialDept\AtpSignals\Events\SignalEvent; 69 + use Tests\TestCase; 70 + 71 + class NewPostSignalTest extends TestCase 72 + { 73 + /** @test */ 74 + public function it_handles_new_posts() 75 + { 76 + $signal = new NewPostSignal(); 77 + 78 + $event = new SignalEvent( 79 + did: 'did:plc:test123', 80 + timeUs: time() * 1000000, 81 + kind: 'commit', 82 + commit: new CommitEvent( 83 + rev: 'test', 84 + operation: 'create', 85 + collection: 'app.bsky.feed.post', 86 + rkey: 'test123', 87 + record: (object) [ 88 + 'text' => 'Hello World!', 89 + 'createdAt' => now()->toIso8601String(), 90 + ], 91 + ), 92 + ); 93 + 94 + $signal->handle($event); 95 + 96 + // Assert expected behavior 97 + $this->assertDatabaseHas('posts', [ 98 + 'text' => 'Hello World!', 99 + ]); 100 + } 101 + } 102 + ``` 103 + 104 + ### Testing Event Types 105 + 106 + Verify your Signal listens for correct event types: 107 + 108 + ```php 109 + /** @test */ 110 + public function it_listens_for_commit_events() 111 + { 112 + $signal = new NewPostSignal(); 113 + 114 + $eventTypes = $signal->eventTypes(); 115 + 116 + $this->assertContains('commit', $eventTypes); 117 + } 118 + ``` 119 + 120 + ### Testing Filters 121 + 122 + Verify collection filtering: 123 + 124 + ```php 125 + /** @test */ 126 + public function it_filters_to_posts_only() 127 + { 128 + $signal = new NewPostSignal(); 129 + 130 + $collections = $signal->collections(); 131 + 132 + $this->assertEquals(['app.bsky.feed.post'], $collections); 133 + } 134 + ``` 135 + 136 + ### Testing Operation Filtering 137 + 138 + ```php 139 + /** @test */ 140 + public function it_only_handles_creates() 141 + { 142 + $signal = new NewPostSignal(); 143 + 144 + $operations = $signal->operations(); 145 + 146 + $this->assertEquals([SignalCommitOperation::Create], $operations); 147 + } 148 + ``` 149 + 150 + ### Testing Custom Filtering 151 + 152 + ```php 153 + /** @test */ 154 + public function it_filters_posts_with_images() 155 + { 156 + $signal = new ImagePostSignal(); 157 + 158 + // Event with image 159 + $eventWithImage = $this->createEvent([ 160 + 'text' => 'Check this out!', 161 + 'embed' => (object) ['type' => 'image'], 162 + ]); 163 + 164 + $this->assertTrue($signal->shouldHandle($eventWithImage)); 165 + 166 + // Event without image 167 + $eventWithoutImage = $this->createEvent([ 168 + 'text' => 'Just text', 169 + ]); 170 + 171 + $this->assertFalse($signal->shouldHandle($eventWithoutImage)); 172 + } 173 + ``` 174 + 175 + ## Feature Testing 176 + 177 + Test Signals in the context of your application. 178 + 179 + ### Test with Database 180 + 181 + ```php 182 + <?php 183 + 184 + namespace Tests\Feature\Signals; 185 + 186 + use App\Models\Post; 187 + use App\Signals\StorePostSignal; 188 + use Illuminate\Foundation\Testing\RefreshDatabase; 189 + use SocialDept\AtpSignals\Events\CommitEvent; 190 + use SocialDept\AtpSignals\Events\SignalEvent; 191 + use Tests\TestCase; 192 + 193 + class StorePostSignalTest extends TestCase 194 + { 195 + use RefreshDatabase; 196 + 197 + /** @test */ 198 + public function it_stores_posts_in_database() 199 + { 200 + $signal = new StorePostSignal(); 201 + 202 + $event = new SignalEvent( 203 + did: 'did:plc:test123', 204 + timeUs: time() * 1000000, 205 + kind: 'commit', 206 + commit: new CommitEvent( 207 + rev: 'abc', 208 + operation: 'create', 209 + collection: 'app.bsky.feed.post', 210 + rkey: 'test', 211 + record: (object) [ 212 + 'text' => 'Test post', 213 + 'createdAt' => now()->toIso8601String(), 214 + ], 215 + ), 216 + ); 217 + 218 + $signal->handle($event); 219 + 220 + $this->assertDatabaseHas('posts', [ 221 + 'did' => 'did:plc:test123', 222 + 'text' => 'Test post', 223 + ]); 224 + } 225 + 226 + /** @test */ 227 + public function it_updates_existing_posts() 228 + { 229 + Post::create([ 230 + 'did' => 'did:plc:test123', 231 + 'rkey' => 'test', 232 + 'text' => 'Old text', 233 + ]); 234 + 235 + $signal = new StorePostSignal(); 236 + 237 + $event = $this->createUpdateEvent([ 238 + 'text' => 'New text', 239 + ]); 240 + 241 + $signal->handle($event); 242 + 243 + $this->assertDatabaseHas('posts', [ 244 + 'did' => 'did:plc:test123', 245 + 'text' => 'New text', 246 + ]); 247 + 248 + $this->assertEquals(1, Post::count()); 249 + } 250 + 251 + /** @test */ 252 + public function it_deletes_posts() 253 + { 254 + Post::create([ 255 + 'did' => 'did:plc:test123', 256 + 'rkey' => 'test', 257 + 'text' => 'Test post', 258 + ]); 259 + 260 + $signal = new StorePostSignal(); 261 + 262 + $event = $this->createDeleteEvent(); 263 + 264 + $signal->handle($event); 265 + 266 + $this->assertDatabaseMissing('posts', [ 267 + 'did' => 'did:plc:test123', 268 + 'rkey' => 'test', 269 + ]); 270 + } 271 + } 272 + ``` 273 + 274 + ### Test with External APIs 275 + 276 + ```php 277 + use Illuminate\Support\Facades\Http; 278 + 279 + /** @test */ 280 + public function it_sends_notifications() 281 + { 282 + Http::fake(); 283 + 284 + $signal = new NotificationSignal(); 285 + 286 + $event = $this->createEvent(); 287 + 288 + $signal->handle($event); 289 + 290 + Http::assertSent(function ($request) { 291 + return $request->url() === 'https://api.example.com/notify' && 292 + $request['text'] === 'New post created'; 293 + }); 294 + } 295 + ``` 296 + 297 + ## Testing Queued Signals 298 + 299 + Test Signals that use queues. 300 + 301 + ### Test Queue Dispatch 302 + 303 + ```php 304 + use Illuminate\Support\Facades\Queue; 305 + 306 + /** @test */ 307 + public function it_queues_events() 308 + { 309 + Queue::fake(); 310 + 311 + $signal = new QueuedSignal(); 312 + 313 + $this->assertTrue($signal->shouldQueue()); 314 + 315 + // In production, this would queue 316 + // For testing, we verify the intent 317 + } 318 + ``` 319 + 320 + ### Test with Sync Queue 321 + 322 + Process queued jobs synchronously in tests: 323 + 324 + ```php 325 + /** @test */ 326 + public function it_processes_queued_events() 327 + { 328 + // Use sync queue for immediate processing 329 + config(['queue.default' => 'sync']); 330 + 331 + $signal = new QueuedSignal(); 332 + 333 + $event = $this->createEvent(); 334 + 335 + $signal->handle($event); 336 + 337 + // Assert side effects happened 338 + $this->assertDatabaseHas('posts', [...]); 339 + } 340 + ``` 341 + 342 + ### Test Queue Configuration 343 + 344 + ```php 345 + /** @test */ 346 + public function it_uses_high_priority_queue() 347 + { 348 + $signal = new HighPrioritySignal(); 349 + 350 + $this->assertTrue($signal->shouldQueue()); 351 + $this->assertEquals('high-priority', $signal->queue()); 352 + } 353 + 354 + /** @test */ 355 + public function it_uses_redis_connection() 356 + { 357 + $signal = new RedisQueueSignal(); 358 + 359 + $this->assertEquals('redis', $signal->queueConnection()); 360 + } 361 + ``` 362 + 363 + ## Testing Failure Handling 364 + 365 + Test how your Signal handles errors. 366 + 367 + ### Test Failed Method 368 + 369 + ```php 370 + use Illuminate\Support\Facades\Log; 371 + 372 + /** @test */ 373 + public function it_logs_failures() 374 + { 375 + Log::spy(); 376 + 377 + $signal = new FailureHandlingSignal(); 378 + 379 + $event = $this->createEvent(); 380 + $exception = new \Exception('Something went wrong'); 381 + 382 + $signal->failed($event, $exception); 383 + 384 + Log::shouldHaveReceived('error') 385 + ->with('Signal failed', \Mockery::any()); 386 + } 387 + ``` 388 + 389 + ### Test Exception Handling 390 + 391 + ```php 392 + /** @test */ 393 + public function it_handles_invalid_data_gracefully() 394 + { 395 + $signal = new RobustSignal(); 396 + 397 + $event = new SignalEvent( 398 + did: 'did:plc:test', 399 + timeUs: time() * 1000000, 400 + kind: 'commit', 401 + commit: new CommitEvent( 402 + rev: 'test', 403 + operation: 'create', 404 + collection: 'app.bsky.feed.post', 405 + rkey: 'test', 406 + record: (object) [], // Missing required fields 407 + ), 408 + ); 409 + 410 + // Should not throw 411 + $signal->handle($event); 412 + 413 + // Should handle gracefully (e.g., log and skip) 414 + $this->assertDatabaseCount('posts', 0); 415 + } 416 + ``` 417 + 418 + ## Test Helpers 419 + 420 + Create reusable helpers for common test scenarios. 421 + 422 + ### Event Factory Helper 423 + 424 + ```php 425 + trait CreatesSignalEvents 426 + { 427 + protected function createCommitEvent(array $overrides = []): SignalEvent 428 + { 429 + $defaults = [ 430 + 'did' => 'did:plc:test123', 431 + 'timeUs' => time() * 1000000, 432 + 'kind' => 'commit', 433 + 'commit' => new CommitEvent( 434 + rev: 'test', 435 + operation: $overrides['operation'] ?? 'create', 436 + collection: $overrides['collection'] ?? 'app.bsky.feed.post', 437 + rkey: $overrides['rkey'] ?? 'test', 438 + record: (object) array_merge([ 439 + 'text' => 'Test post', 440 + 'createdAt' => now()->toIso8601String(), 441 + ], $overrides['record'] ?? []), 442 + ), 443 + ]; 444 + 445 + return new SignalEvent(...$defaults); 446 + } 447 + 448 + protected function createPostEvent(array $record = []): SignalEvent 449 + { 450 + return $this->createCommitEvent([ 451 + 'collection' => 'app.bsky.feed.post', 452 + 'record' => $record, 453 + ]); 454 + } 455 + 456 + protected function createLikeEvent(array $record = []): SignalEvent 457 + { 458 + return $this->createCommitEvent([ 459 + 'collection' => 'app.bsky.feed.like', 460 + 'record' => array_merge([ 461 + 'subject' => (object) [ 462 + 'uri' => 'at://did:plc:test/app.bsky.feed.post/test', 463 + 'cid' => 'bafytest', 464 + ], 465 + 'createdAt' => now()->toIso8601String(), 466 + ], $record), 467 + ]); 468 + } 469 + 470 + protected function createFollowEvent(array $record = []): SignalEvent 471 + { 472 + return $this->createCommitEvent([ 473 + 'collection' => 'app.bsky.graph.follow', 474 + 'record' => array_merge([ 475 + 'subject' => 'did:plc:target', 476 + 'createdAt' => now()->toIso8601String(), 477 + ], $record), 478 + ]); 479 + } 480 + } 481 + ``` 482 + 483 + Use in tests: 484 + 485 + ```php 486 + class MySignalTest extends TestCase 487 + { 488 + use CreatesSignalEvents; 489 + 490 + /** @test */ 491 + public function it_handles_posts() 492 + { 493 + $event = $this->createPostEvent([ 494 + 'text' => 'Custom text', 495 + ]); 496 + 497 + // Test with event 498 + } 499 + } 500 + ``` 501 + 502 + ### Signal Factory Helper 503 + 504 + ```php 505 + trait CreatesSignals 506 + { 507 + protected function createSignal(string $class, array $config = []) 508 + { 509 + $signal = new $class(); 510 + 511 + // Override configuration for testing 512 + foreach ($config as $method => $value) { 513 + $signal->{$method} = $value; 514 + } 515 + 516 + return $signal; 517 + } 518 + } 519 + ``` 520 + 521 + ## Testing Best Practices 522 + 523 + ### Use Descriptive Test Names 524 + 525 + ```php 526 + // Good 527 + /** @test */ 528 + public function it_stores_posts_with_valid_data() 529 + 530 + /** @test */ 531 + public function it_skips_posts_without_text() 532 + 533 + /** @test */ 534 + public function it_handles_duplicate_posts_gracefully() 535 + 536 + // Less descriptive 537 + /** @test */ 538 + public function test_handle() 539 + ``` 540 + 541 + ### Test Edge Cases 542 + 543 + ```php 544 + /** @test */ 545 + public function it_handles_empty_text() 546 + { 547 + $event = $this->createPostEvent(['text' => '']); 548 + // Test behavior 549 + } 550 + 551 + /** @test */ 552 + public function it_handles_very_long_text() 553 + { 554 + $event = $this->createPostEvent(['text' => str_repeat('a', 10000)]); 555 + // Test behavior 556 + } 557 + 558 + /** @test */ 559 + public function it_handles_missing_created_at() 560 + { 561 + $event = $this->createPostEvent(['createdAt' => null]); 562 + // Test behavior 563 + } 564 + ``` 565 + 566 + ### Test All Operations 567 + 568 + ```php 569 + /** @test */ 570 + public function it_handles_creates() 571 + { 572 + $event = $this->createEvent(['operation' => 'create']); 573 + // Test 574 + } 575 + 576 + /** @test */ 577 + public function it_handles_updates() 578 + { 579 + $event = $this->createEvent(['operation' => 'update']); 580 + // Test 581 + } 582 + 583 + /** @test */ 584 + public function it_handles_deletes() 585 + { 586 + $event = $this->createEvent(['operation' => 'delete']); 587 + // Test 588 + } 589 + ``` 590 + 591 + ### Mock External Dependencies 592 + 593 + ```php 594 + /** @test */ 595 + public function it_calls_external_api() 596 + { 597 + Http::fake([ 598 + 'api.example.com/*' => Http::response(['success' => true]), 599 + ]); 600 + 601 + $signal = new ApiSignal(); 602 + $event = $this->createEvent(); 603 + 604 + $signal->handle($event); 605 + 606 + Http::assertSent(function ($request) { 607 + return $request->url() === 'https://api.example.com/endpoint'; 608 + }); 609 + } 610 + ``` 611 + 612 + ### Test Database State 613 + 614 + ```php 615 + use Illuminate\Foundation\Testing\RefreshDatabase; 616 + 617 + class DatabaseSignalTest extends TestCase 618 + { 619 + use RefreshDatabase; 620 + 621 + /** @test */ 622 + public function it_creates_records() 623 + { 624 + // Fresh database for each test 625 + } 626 + } 627 + ``` 628 + 629 + ## Continuous Integration 630 + 631 + Run tests automatically on every commit. 632 + 633 + ### GitHub Actions 634 + 635 + ```yaml 636 + # .github/workflows/tests.yml 637 + name: Tests 638 + 639 + on: [push, pull_request] 640 + 641 + jobs: 642 + test: 643 + runs-on: ubuntu-latest 644 + 645 + steps: 646 + - uses: actions/checkout@v2 647 + 648 + - name: Setup PHP 649 + uses: shivammathur/setup-php@v2 650 + with: 651 + php-version: 8.2 652 + 653 + - name: Install Dependencies 654 + run: composer install 655 + 656 + - name: Run Tests 657 + run: php artisan test 658 + ``` 659 + 660 + ### Run Signal-Specific Tests 661 + 662 + ```bash 663 + # Run all Signal tests 664 + php artisan test --testsuite=Signals 665 + 666 + # Run specific test file 667 + php artisan test tests/Unit/Signals/NewPostSignalTest.php 668 + 669 + # Run with coverage 670 + php artisan test --coverage 671 + ``` 672 + 673 + ## Debugging Tests 674 + 675 + ### Enable Debug Output 676 + 677 + ```php 678 + /** @test */ 679 + public function it_processes_events() 680 + { 681 + $signal = new NewPostSignal(); 682 + 683 + $event = $this->createEvent(); 684 + 685 + dump($event); // Output event data 686 + 687 + $signal->handle($event); 688 + 689 + dump(Post::all()); // Output results 690 + } 691 + ``` 692 + 693 + ### Use dd() to Stop Execution 694 + 695 + ```php 696 + /** @test */ 697 + public function it_processes_events() 698 + { 699 + $event = $this->createEvent(); 700 + 701 + dd($event); // Dump and die 702 + 703 + // This won't run 704 + } 705 + ``` 706 + 707 + ### Check Logs 708 + 709 + ```php 710 + /** @test */ 711 + public function it_logs_processing() 712 + { 713 + Log::spy(); 714 + 715 + $signal = new LoggingSignal(); 716 + $event = $this->createEvent(); 717 + 718 + $signal->handle($event); 719 + 720 + Log::shouldHaveReceived('info')->once(); 721 + } 722 + ``` 723 + 724 + ## Next Steps 725 + 726 + - **[See real-world examples โ†’](examples.md)** - Learn from production test patterns 727 + - **[Review queue integration โ†’](queues.md)** - Test queued Signals 728 + - **[Review signals documentation โ†’](signals.md)** - Understand Signal structure
header.png

This is a binary file and will not be displayed.

+141
src/Binary/Reader.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Binary; 6 + 7 + use RuntimeException; 8 + 9 + /** 10 + * Binary data reader with position tracking. 11 + * 12 + * Provides stream-like interface for reading from binary strings. 13 + */ 14 + class Reader 15 + { 16 + private int $position = 0; 17 + 18 + public function __construct( 19 + private readonly string $data, 20 + ) { 21 + } 22 + 23 + /** 24 + * Get current position in the data. 25 + */ 26 + public function getPosition(): int 27 + { 28 + return $this->position; 29 + } 30 + 31 + /** 32 + * Get total length of data. 33 + */ 34 + public function getLength(): int 35 + { 36 + return strlen($this->data); 37 + } 38 + 39 + /** 40 + * Check if there's more data to read. 41 + */ 42 + public function hasMore(): bool 43 + { 44 + return $this->position < strlen($this->data); 45 + } 46 + 47 + /** 48 + * Get remaining bytes count. 49 + */ 50 + public function remaining(): int 51 + { 52 + return strlen($this->data) - $this->position; 53 + } 54 + 55 + /** 56 + * Peek at the next byte without advancing position. 57 + * 58 + * @throws RuntimeException If no more data available 59 + */ 60 + public function peek(): int 61 + { 62 + if (! $this->hasMore()) { 63 + throw new RuntimeException('Unexpected end of data'); 64 + } 65 + 66 + return ord($this->data[$this->position]); 67 + } 68 + 69 + /** 70 + * Read a single byte and advance position. 71 + * 72 + * @throws RuntimeException If no more data available 73 + */ 74 + public function readByte(): int 75 + { 76 + $byte = $this->peek(); 77 + $this->position++; 78 + 79 + return $byte; 80 + } 81 + 82 + /** 83 + * Read exactly N bytes and advance position. 84 + * 85 + * @throws RuntimeException If not enough data available 86 + */ 87 + public function readBytes(int $length): string 88 + { 89 + if ($this->remaining() < $length) { 90 + throw new RuntimeException("Cannot read {$length} bytes, only {$this->remaining()} remaining"); 91 + } 92 + 93 + $bytes = substr($this->data, $this->position, $length); 94 + $this->position += $length; 95 + 96 + return $bytes; 97 + } 98 + 99 + /** 100 + * Read a varint (variable-length integer). 101 + * 102 + * @throws RuntimeException If varint is malformed 103 + */ 104 + public function readVarint(): int 105 + { 106 + return Varint::decode($this->data, $this->position); 107 + } 108 + 109 + /** 110 + * Get all remaining data without advancing position. 111 + */ 112 + public function peekRemaining(): string 113 + { 114 + return substr($this->data, $this->position); 115 + } 116 + 117 + /** 118 + * Read all remaining data and advance position to end. 119 + */ 120 + public function readRemaining(): string 121 + { 122 + $remaining = $this->peekRemaining(); 123 + $this->position = strlen($this->data); 124 + 125 + return $remaining; 126 + } 127 + 128 + /** 129 + * Skip N bytes forward. 130 + * 131 + * @throws RuntimeException If trying to skip past end 132 + */ 133 + public function skip(int $bytes): void 134 + { 135 + if ($this->remaining() < $bytes) { 136 + throw new RuntimeException("Cannot skip {$bytes} bytes, only {$this->remaining()} remaining"); 137 + } 138 + 139 + $this->position += $bytes; 140 + } 141 + }
+68
src/Binary/Varint.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Binary; 6 + 7 + use RuntimeException; 8 + 9 + /** 10 + * Varint (Variable-Length Integer) decoder for unsigned integers. 11 + * 12 + * Used in CAR format to encode block lengths. Implements the same 13 + * varint encoding used in Protocol Buffers and other binary formats. 14 + */ 15 + class Varint 16 + { 17 + /** 18 + * Decode a varint from the beginning of the data. 19 + * 20 + * @param string $data Binary data containing varint 21 + * @param int $offset Starting position (will be updated to position after varint) 22 + * @return int Decoded unsigned integer 23 + * @throws RuntimeException If varint is malformed or data ends unexpectedly 24 + */ 25 + public static function decode(string $data, int &$offset = 0): int 26 + { 27 + $result = 0; 28 + $shift = 0; 29 + $length = strlen($data); 30 + 31 + while ($offset < $length) { 32 + $byte = ord($data[$offset]); 33 + $offset++; 34 + 35 + // Take lower 7 bits and shift into result 36 + $result |= ($byte & 0x7F) << $shift; 37 + 38 + // If MSB is 0, we're done 39 + if (($byte & 0x80) === 0) { 40 + return $result; 41 + } 42 + 43 + $shift += 7; 44 + 45 + // Prevent overflow (64-bit max) 46 + if ($shift > 63) { 47 + throw new RuntimeException('Varint too long (max 64 bits)'); 48 + } 49 + } 50 + 51 + throw new RuntimeException('Unexpected end of varint data'); 52 + } 53 + 54 + /** 55 + * Read a varint and return both the value and remaining data. 56 + * 57 + * @param string $data Binary data starting with varint 58 + * @return array{0: int, 1: string} [decoded value, remaining data] 59 + */ 60 + public static function decodeFirst(string $data): array 61 + { 62 + $offset = 0; 63 + $value = self::decode($data, $offset); 64 + $remainder = substr($data, $offset); 65 + 66 + return [$value, $remainder]; 67 + } 68 + }
+132
src/CAR/BlockReader.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\CAR; 6 + 7 + use Generator; 8 + use SocialDept\AtpSignals\Binary\Reader; 9 + use SocialDept\AtpSignals\Core\CID; 10 + 11 + /** 12 + * CAR (Content Addressable aRchive) block reader. 13 + * 14 + * Reads blocks from CAR format data used in AT Protocol commits. 15 + */ 16 + class BlockReader 17 + { 18 + private Reader $reader; 19 + 20 + public function __construct(string $data) 21 + { 22 + $this->reader = new Reader($data); 23 + } 24 + 25 + /** 26 + * Read all blocks from CAR data. 27 + * 28 + * Yields [CID, block data] pairs. 29 + * 30 + * @return Generator<array{0: CID, 1: string}> 31 + */ 32 + public function blocks(): Generator 33 + { 34 + // Skip CAR header (we don't need it for Firehose processing) 35 + $this->skipHeader(); 36 + 37 + // Read blocks until end of data 38 + while ($this->reader->hasMore()) { 39 + $block = $this->readBlock(); 40 + if ($block !== null) { 41 + yield $block; 42 + } 43 + } 44 + } 45 + 46 + /** 47 + * Skip CAR header. 48 + */ 49 + private function skipHeader(): void 50 + { 51 + if (! $this->reader->hasMore()) { 52 + return; 53 + } 54 + 55 + // Read header length (varint) 56 + $headerLength = $this->reader->readVarint(); 57 + 58 + // Skip header data 59 + $this->reader->skip($headerLength); 60 + } 61 + 62 + /** 63 + * Read a single block. 64 + * 65 + * @return array{0: CID, 1: string}|null [CID, block data] or null if no more blocks 66 + */ 67 + private function readBlock(): ?array 68 + { 69 + if (! $this->reader->hasMore()) { 70 + return null; 71 + } 72 + 73 + // Read block length (varint) - this is the total length of CID + data 74 + $blockLength = $this->reader->readVarint(); 75 + 76 + if ($blockLength === 0) { 77 + return null; 78 + } 79 + 80 + // Read entire block data 81 + $blockData = $this->reader->readBytes($blockLength); 82 + 83 + // Parse CID from the beginning of block data 84 + // CIDs in CAR blocks are self-delimiting (no separate length prefix) 85 + // We need to parse the CID to find out its length 86 + $cidReader = new Reader($blockData); 87 + 88 + // Read CID version 89 + $version = $cidReader->readVarint(); 90 + 91 + if ($version === 0x12) { 92 + // CIDv0 - multihash only (starting with 0x12 for SHA-256) 93 + $hashLength = $cidReader->readVarint(); 94 + $cidReader->readBytes($hashLength); // Skip hash bytes 95 + } elseif ($version === 1) { 96 + // CIDv1 - version + codec + multihash 97 + $codec = $cidReader->readVarint(); 98 + $hashType = $cidReader->readVarint(); 99 + $hashLength = $cidReader->readVarint(); 100 + $cidReader->readBytes($hashLength); // Skip hash bytes 101 + } else { 102 + throw new \RuntimeException("Unsupported CID version in CAR block: {$version}"); 103 + } 104 + 105 + // Now we know the CID length 106 + $cidLength = $cidReader->getPosition(); 107 + $cidBytes = substr($blockData, 0, $cidLength); 108 + $cid = CID::fromBinary($cidBytes); 109 + 110 + // Remaining data is the block content 111 + $content = substr($blockData, $cidLength); 112 + 113 + return [$cid, $content]; 114 + } 115 + 116 + /** 117 + * Get all blocks as an associative array. 118 + * 119 + * @return array<string, string> Map of CID string => block data 120 + */ 121 + public function getBlockMap(): array 122 + { 123 + $blocks = []; 124 + 125 + foreach ($this->blocks() as [$cid, $data]) { 126 + $cidString = $cid->toString(); 127 + $blocks[$cidString] = $data; 128 + } 129 + 130 + return $blocks; 131 + } 132 + }
+133
src/CAR/RecordExtractor.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\CAR; 6 + 7 + use Generator; 8 + use SocialDept\AtpSignals\Core\CBOR; 9 + use SocialDept\AtpSignals\Core\CID; 10 + 11 + /** 12 + * Extract records from AT Protocol MST (Merkle Search Tree) blocks. 13 + * 14 + * Walks MST structure to extract collection/rkey records with their values. 15 + */ 16 + class RecordExtractor 17 + { 18 + /** 19 + * @param array<string, string> $blocks Map of CID string => block data 20 + */ 21 + public function __construct( 22 + private readonly array $blocks, 23 + private readonly string $did, 24 + ) { 25 + } 26 + 27 + /** 28 + * Extract all records from blocks. 29 + * 30 + * Yields records in format: "collection/rkey" => record data 31 + * 32 + * @return Generator<string, array> 33 + */ 34 + public function extractRecords(CID $rootCid): Generator 35 + { 36 + yield from $this->walkTree($rootCid, ''); 37 + } 38 + 39 + /** 40 + * Recursively walk MST tree. 41 + * 42 + * @param CID $cid Current node CID 43 + * @param string $prefix Path prefix accumulated from parent nodes 44 + * @return Generator<string, array> 45 + */ 46 + private function walkTree(CID $cid, string $prefix): Generator 47 + { 48 + $cidStr = $cid->toString(); 49 + 50 + // Get block data 51 + if (! isset($this->blocks[$cidStr])) { 52 + // Block not found - might be a pruned tree, skip it 53 + return; 54 + } 55 + 56 + $blockData = $this->blocks[$cidStr]; 57 + 58 + // Decode CBOR block 59 + $node = CBOR::decode($blockData); 60 + 61 + if (! is_array($node)) { 62 + return; 63 + } 64 + 65 + // Process left subtree if exists 66 + if (isset($node['l']) && $node['l'] instanceof CID) { 67 + yield from $this->walkTree($node['l'], $prefix); 68 + } 69 + 70 + // Process entries 71 + if (isset($node['e']) && is_array($node['e'])) { 72 + foreach ($node['e'] as $entry) { 73 + if (! is_array($entry)) { 74 + continue; 75 + } 76 + 77 + // Build full key from prefix + entry key 78 + $entryPrefix = $entry['p'] ?? 0; 79 + $keyPart = $entry['k'] ?? ''; 80 + $fullKey = substr($prefix, 0, $entryPrefix) . $keyPart; 81 + 82 + // If entry has a tree link, walk it 83 + if (isset($entry['t']) && $entry['t'] instanceof CID) { 84 + yield from $this->walkTree($entry['t'], $fullKey); 85 + } 86 + 87 + // If entry has a value (record), yield it 88 + if (isset($entry['v']) && $entry['v'] instanceof CID) { 89 + $recordCid = $entry['v']; 90 + $record = $this->getRecord($recordCid); 91 + 92 + if ($record !== null) { 93 + // Parse collection/rkey from key 94 + $parts = explode('/', $fullKey, 2); 95 + if (count($parts) === 2) { 96 + [$collection, $rkey] = $parts; 97 + $path = "{$collection}/{$rkey}"; 98 + 99 + yield $path => [ 100 + 'uri' => "at://{$this->did}/{$path}", 101 + 'cid' => $recordCid->toString(), 102 + 'value' => $record, 103 + ]; 104 + } else { 105 + // Debug: log when key format doesn't match expected pattern 106 + \Illuminate\Support\Facades\Log::debug('Signal: MST key parse failed', [ 107 + 'fullKey' => $fullKey, 108 + 'parts' => $parts, 109 + 'did' => $this->did, 110 + ]); 111 + } 112 + } 113 + } 114 + } 115 + } 116 + } 117 + 118 + /** 119 + * Get record data from block. 120 + */ 121 + private function getRecord(CID $cid): ?array 122 + { 123 + $cidStr = $cid->toString(); 124 + 125 + if (! isset($this->blocks[$cidStr])) { 126 + return null; 127 + } 128 + 129 + $data = CBOR::decode($this->blocks[$cidStr]); 130 + 131 + return is_array($data) ? $data : null; 132 + } 133 + }
+286
src/CBOR/Decoder.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\CBOR; 6 + 7 + use RuntimeException; 8 + use SocialDept\AtpSignals\Binary\Reader; 9 + use SocialDept\AtpSignals\Core\CID; 10 + 11 + /** 12 + * CBOR (Concise Binary Object Representation) decoder. 13 + * 14 + * Implements RFC 8949 CBOR with DAG-CBOR extensions for IPLD. 15 + * Supports tag 42 for CID links. 16 + */ 17 + class Decoder 18 + { 19 + private const MAJOR_TYPE_UNSIGNED = 0; 20 + 21 + private const MAJOR_TYPE_NEGATIVE = 1; 22 + 23 + private const MAJOR_TYPE_BYTES = 2; 24 + 25 + private const MAJOR_TYPE_TEXT = 3; 26 + 27 + private const MAJOR_TYPE_ARRAY = 4; 28 + 29 + private const MAJOR_TYPE_MAP = 5; 30 + 31 + private const MAJOR_TYPE_TAG = 6; 32 + 33 + private const MAJOR_TYPE_SPECIAL = 7; 34 + 35 + private const TAG_CID = 42; 36 + 37 + private Reader $reader; 38 + 39 + public function __construct(string $data) 40 + { 41 + $this->reader = new Reader($data); 42 + } 43 + 44 + /** 45 + * Decode the next CBOR item. 46 + * 47 + * @return mixed Decoded value 48 + * 49 + * @throws RuntimeException If data is malformed 50 + */ 51 + public function decode(): mixed 52 + { 53 + if (! $this->reader->hasMore()) { 54 + throw new RuntimeException('Unexpected end of CBOR data'); 55 + } 56 + 57 + $initialByte = $this->reader->readByte(); 58 + $majorType = $initialByte >> 5; 59 + $additionalInfo = $initialByte & 0x1F; 60 + 61 + return match ($majorType) { 62 + self::MAJOR_TYPE_UNSIGNED => $this->decodeUnsigned($additionalInfo), 63 + self::MAJOR_TYPE_NEGATIVE => $this->decodeNegative($additionalInfo), 64 + self::MAJOR_TYPE_BYTES => $this->decodeBytes($additionalInfo), 65 + self::MAJOR_TYPE_TEXT => $this->decodeText($additionalInfo), 66 + self::MAJOR_TYPE_ARRAY => $this->decodeArray($additionalInfo), 67 + self::MAJOR_TYPE_MAP => $this->decodeMap($additionalInfo), 68 + self::MAJOR_TYPE_TAG => $this->decodeTag($additionalInfo), 69 + self::MAJOR_TYPE_SPECIAL => $this->decodeSpecial($additionalInfo), 70 + default => throw new RuntimeException("Unknown major type: {$majorType}"), 71 + }; 72 + } 73 + 74 + /** 75 + * Check if there's more data to decode. 76 + */ 77 + public function hasMore(): bool 78 + { 79 + return $this->reader->hasMore(); 80 + } 81 + 82 + /** 83 + * Get current position. 84 + */ 85 + public function getPosition(): int 86 + { 87 + return $this->reader->getPosition(); 88 + } 89 + 90 + /** 91 + * Decode unsigned integer. 92 + */ 93 + private function decodeUnsigned(int $additionalInfo): int 94 + { 95 + return $this->decodeLength($additionalInfo); 96 + } 97 + 98 + /** 99 + * Decode negative integer. 100 + */ 101 + private function decodeNegative(int $additionalInfo): int 102 + { 103 + $value = $this->decodeLength($additionalInfo); 104 + 105 + return -1 - $value; 106 + } 107 + 108 + /** 109 + * Decode byte string. 110 + */ 111 + private function decodeBytes(int $additionalInfo): string 112 + { 113 + $length = $this->decodeLength($additionalInfo); 114 + 115 + return $this->reader->readBytes($length); 116 + } 117 + 118 + /** 119 + * Decode text string. 120 + */ 121 + private function decodeText(int $additionalInfo): string 122 + { 123 + $length = $this->decodeLength($additionalInfo); 124 + 125 + return $this->reader->readBytes($length); 126 + } 127 + 128 + /** 129 + * Decode array. 130 + */ 131 + private function decodeArray(int $additionalInfo): array 132 + { 133 + $length = $this->decodeLength($additionalInfo); 134 + $array = []; 135 + 136 + for ($i = 0; $i < $length; $i++) { 137 + $array[] = $this->decode(); 138 + } 139 + 140 + return $array; 141 + } 142 + 143 + /** 144 + * Decode map (object). 145 + */ 146 + private function decodeMap(int $additionalInfo): array 147 + { 148 + $length = $this->decodeLength($additionalInfo); 149 + $map = []; 150 + 151 + for ($i = 0; $i < $length; $i++) { 152 + $key = $this->decode(); 153 + $value = $this->decode(); 154 + 155 + if (! is_string($key) && ! is_int($key)) { 156 + throw new RuntimeException('Map keys must be strings or integers'); 157 + } 158 + 159 + $map[$key] = $value; 160 + } 161 + 162 + return $map; 163 + } 164 + 165 + /** 166 + * Decode tagged value. 167 + */ 168 + private function decodeTag(int $additionalInfo): mixed 169 + { 170 + $tag = $this->decodeLength($additionalInfo); 171 + 172 + if ($tag === self::TAG_CID) { 173 + // Tag 42 = CID link (DAG-CBOR) 174 + // Next item should be byte string containing CID 175 + $cidBytes = $this->decode(); 176 + 177 + if (! is_string($cidBytes)) { 178 + throw new RuntimeException('CID tag must be followed by byte string'); 179 + } 180 + 181 + // First byte should be 0x00 for CID 182 + if (ord($cidBytes[0]) !== 0x00) { 183 + throw new RuntimeException('Invalid CID byte string prefix'); 184 + } 185 + 186 + return CID::fromBinary(substr($cidBytes, 1)); 187 + } 188 + 189 + // For other tags, just return the tagged value 190 + return $this->decode(); 191 + } 192 + 193 + /** 194 + * Decode special values (bool, null, floats). 195 + */ 196 + private function decodeSpecial(int $additionalInfo): mixed 197 + { 198 + return match ($additionalInfo) { 199 + 20 => false, 200 + 21 => true, 201 + 22 => null, 202 + 23 => throw new RuntimeException('Undefined special value'), 203 + 25 => $this->decodeFloat16(), // IEEE 754 Half-Precision (16-bit) 204 + 26 => $this->decodeFloat32(), // IEEE 754 Single-Precision (32-bit) 205 + 27 => $this->decodeFloat64(), // IEEE 754 Double-Precision (64-bit) 206 + default => throw new RuntimeException("Unsupported special value: {$additionalInfo}"), 207 + }; 208 + } 209 + 210 + /** 211 + * Decode IEEE 754 half-precision float (16-bit). 212 + */ 213 + private function decodeFloat16(): float 214 + { 215 + $bytes = $this->reader->readBytes(2); 216 + $bits = unpack('n', $bytes)[1]; 217 + 218 + // Extract sign, exponent, and mantissa 219 + $sign = ($bits >> 15) & 1; 220 + $exponent = ($bits >> 10) & 0x1F; 221 + $mantissa = $bits & 0x3FF; 222 + 223 + // Handle special cases 224 + if ($exponent === 0) { 225 + // Subnormal or zero 226 + $value = $mantissa / 1024.0 * (2 ** -14); 227 + } elseif ($exponent === 31) { 228 + // Infinity or NaN 229 + return $mantissa === 0 ? ($sign ? -INF : INF) : NAN; 230 + } else { 231 + // Normalized value 232 + $value = (1 + $mantissa / 1024.0) * (2 ** ($exponent - 15)); 233 + } 234 + 235 + return $sign ? -$value : $value; 236 + } 237 + 238 + /** 239 + * Decode IEEE 754 single-precision float (32-bit). 240 + */ 241 + private function decodeFloat32(): float 242 + { 243 + $bytes = $this->reader->readBytes(4); 244 + 245 + return unpack('G', $bytes)[1]; // Big-endian float 246 + } 247 + 248 + /** 249 + * Decode IEEE 754 double-precision float (64-bit). 250 + */ 251 + private function decodeFloat64(): float 252 + { 253 + $bytes = $this->reader->readBytes(8); 254 + 255 + return unpack('E', $bytes)[1]; // Big-endian double 256 + } 257 + 258 + /** 259 + * Decode length/value from additional info. 260 + */ 261 + private function decodeLength(int $additionalInfo): int 262 + { 263 + if ($additionalInfo < 24) { 264 + return $additionalInfo; 265 + } 266 + 267 + return match ($additionalInfo) { 268 + 24 => $this->reader->readByte(), 269 + 25 => unpack('n', $this->reader->readBytes(2))[1], 270 + 26 => unpack('N', $this->reader->readBytes(4))[1], 271 + 27 => $this->readUint64(), 272 + default => throw new RuntimeException("Invalid additional info: {$additionalInfo}"), 273 + }; 274 + } 275 + 276 + /** 277 + * Read 64-bit unsigned integer. 278 + */ 279 + private function readUint64(): int 280 + { 281 + $bytes = $this->reader->readBytes(8); 282 + $unpacked = unpack('J', $bytes)[1]; 283 + 284 + return $unpacked; 285 + } 286 + }
+64 -20
src/Commands/ConsumeCommand.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Commands; 4 5 use Illuminate\Console\Command; 6 - use SocialDept\Signal\Services\FirehoseConsumer; 7 - use SocialDept\Signal\Services\JetstreamConsumer; 8 - use SocialDept\Signal\Services\SignalRegistry; 9 10 class ConsumeCommand extends Command 11 { ··· 18 19 public function handle(SignalRegistry $registry): int 20 { 21 - // Determine mode 22 $mode = $this->option('mode') ?? config('signal.mode', 'jetstream'); 23 24 if (! in_array($mode, ['jetstream', 'firehose'])) { 25 $this->error("Invalid mode: {$mode}. Must be 'jetstream' or 'firehose'."); 26 27 - return self::FAILURE; 28 } 29 30 - $this->info("Signal: Initializing {$mode} consumer..."); 31 32 - // Discover signals 33 $registry->discover(); 34 35 $signalCount = $registry->all()->count(); 36 37 if ($signalCount === 0) { 38 - $this->warn('No signals registered. Create signals in app/Signals or register them in config/signal.php'); 39 40 - return self::FAILURE; 41 } 42 43 - // List registered signals with a prettier display 44 $this->newLine(); 45 $this->components->info("Found {$signalCount} ".str('signal')->plural($signalCount)); 46 $this->newLine(); 47 48 - $normalizeValue = fn ($value) => $value instanceof \BackedEnum ? $value->value : $value; 49 50 foreach ($registry->all() as $signal) { 51 $className = class_basename($signal); ··· 66 } 67 68 $this->newLine(); 69 70 - // Show mode-specific information 71 if ($mode === 'jetstream') { 72 $this->components->warn('Jetstream Mode: Server-side filtering (custom collections may not receive create/update)'); 73 } else { ··· 75 } 76 77 $this->newLine(); 78 79 - // Determine cursor 80 - $cursor = null; 81 if ($this->option('fresh')) { 82 $this->info('Starting fresh from the beginning'); 83 - } elseif ($this->option('cursor')) { 84 $cursor = (int) $this->option('cursor'); 85 $this->info("Starting from cursor: {$cursor}"); 86 - } else { 87 - $this->info('Resuming from stored cursor position'); 88 } 89 90 - // Resolve and start the appropriate consumer 91 $consumer = $mode === 'firehose' 92 ? app(FirehoseConsumer::class) 93 : app(JetstreamConsumer::class); ··· 96 97 try { 98 $consumer->start($cursor); 99 - } catch (\Exception $e) { 100 $this->error('Error: '.$e->getMessage()); 101 102 return self::FAILURE;
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Commands; 4 5 + use BackedEnum; 6 + use Exception; 7 use Illuminate\Console\Command; 8 + use SocialDept\AtpSignals\Services\FirehoseConsumer; 9 + use SocialDept\AtpSignals\Services\JetstreamConsumer; 10 + use SocialDept\AtpSignals\Services\SignalRegistry; 11 12 class ConsumeCommand extends Command 13 { ··· 20 21 public function handle(SignalRegistry $registry): int 22 { 23 + $mode = $this->determineMode(); 24 + 25 + if ($mode === null) { 26 + return self::FAILURE; 27 + } 28 + 29 + $this->info("Signal: Initializing {$mode} consumer..."); 30 + 31 + if (! $this->discoverAndValidateSignals($registry)) { 32 + return self::FAILURE; 33 + } 34 + 35 + $this->displayRegisteredSignals($registry); 36 + $this->displayModeInformation($mode); 37 + 38 + $cursor = $this->determineCursor(); 39 + 40 + return $this->startConsumer($mode, $cursor); 41 + } 42 + 43 + private function determineMode(): ?string 44 + { 45 $mode = $this->option('mode') ?? config('signal.mode', 'jetstream'); 46 47 if (! in_array($mode, ['jetstream', 'firehose'])) { 48 $this->error("Invalid mode: {$mode}. Must be 'jetstream' or 'firehose'."); 49 50 + return null; 51 } 52 53 + return $mode; 54 + } 55 56 + private function discoverAndValidateSignals(SignalRegistry $registry): bool 57 + { 58 $registry->discover(); 59 60 $signalCount = $registry->all()->count(); 61 62 if ($signalCount === 0) { 63 + $this->warn('No signals registered. Create signals in `app/Signals` or register them in `config/signal.php`.'); 64 65 + return false; 66 } 67 68 + return true; 69 + } 70 + 71 + private function displayRegisteredSignals(SignalRegistry $registry): void 72 + { 73 + $signalCount = $registry->all()->count(); 74 + 75 $this->newLine(); 76 $this->components->info("Found {$signalCount} ".str('signal')->plural($signalCount)); 77 $this->newLine(); 78 79 + $normalizeValue = fn ($value) => $value instanceof BackedEnum ? $value->value : $value; 80 81 foreach ($registry->all() as $signal) { 82 $className = class_basename($signal); ··· 97 } 98 99 $this->newLine(); 100 + } 101 102 + private function displayModeInformation(string $mode): void 103 + { 104 if ($mode === 'jetstream') { 105 $this->components->warn('Jetstream Mode: Server-side filtering (custom collections may not receive create/update)'); 106 } else { ··· 108 } 109 110 $this->newLine(); 111 + } 112 113 + private function determineCursor(): ?int 114 + { 115 if ($this->option('fresh')) { 116 $this->info('Starting fresh from the beginning'); 117 + 118 + return 0; // Explicitly 0 means "start fresh, no cursor" 119 + } 120 + 121 + if ($this->option('cursor')) { 122 $cursor = (int) $this->option('cursor'); 123 $this->info("Starting from cursor: {$cursor}"); 124 + 125 + return $cursor; 126 } 127 128 + $this->info('Resuming from stored cursor position'); 129 + 130 + return null; // null means "use stored cursor" 131 + } 132 + 133 + private function startConsumer(string $mode, ?int $cursor): int 134 + { 135 $consumer = $mode === 'firehose' 136 ? app(FirehoseConsumer::class) 137 : app(JetstreamConsumer::class); ··· 140 141 try { 142 $consumer->start($cursor); 143 + } catch (Exception $e) { 144 $this->error('Error: '.$e->getMessage()); 145 146 return self::FAILURE;
+25 -7
src/Commands/InstallCommand.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Commands; 4 5 use Illuminate\Console\Command; 6 ··· 15 $this->info('Installing Signal package...'); 16 $this->newLine(); 17 18 - // Publish config 19 $this->comment('Publishing configuration...'); 20 $this->call('vendor:publish', [ 21 '--tag' => 'signal-config', 22 ]); 23 $this->info('โœ“ Configuration published'); 24 25 - // Publish migrations 26 $this->comment('Publishing migrations...'); 27 $this->call('vendor:publish', [ 28 '--tag' => 'signal-migrations', 29 ]); 30 $this->info('โœ“ Migrations published'); 31 32 - // Run migrations 33 $this->newLine(); 34 $this->comment('Running migrations...'); 35 ··· 39 } else { 40 $this->warn('โš  Skipped migrations. Run "php artisan migrate" manually when ready.'); 41 } 42 43 $this->newLine(); 44 $this->info('Signal package installed successfully!'); 45 $this->newLine(); 46 47 - // Show next steps 48 $this->line('Next steps:'); 49 $this->line('1. Review the config file: config/signal.php'); 50 $this->line('2. Create your first signal: php artisan make:signal NewPostSignal'); 51 $this->line('3. Start consuming events: php artisan signal:consume'); 52 - 53 - return self::SUCCESS; 54 } 55 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Commands; 4 5 use Illuminate\Console\Command; 6 ··· 15 $this->info('Installing Signal package...'); 16 $this->newLine(); 17 18 + $this->publishConfiguration(); 19 + $this->publishMigrations(); 20 + $this->runMigrations(); 21 + 22 + $this->displaySuccessMessage(); 23 + $this->displayNextSteps(); 24 + 25 + return self::SUCCESS; 26 + } 27 + 28 + private function publishConfiguration(): void 29 + { 30 $this->comment('Publishing configuration...'); 31 $this->call('vendor:publish', [ 32 '--tag' => 'signal-config', 33 ]); 34 $this->info('โœ“ Configuration published'); 35 + } 36 37 + private function publishMigrations(): void 38 + { 39 $this->comment('Publishing migrations...'); 40 $this->call('vendor:publish', [ 41 '--tag' => 'signal-migrations', 42 ]); 43 $this->info('โœ“ Migrations published'); 44 + } 45 46 + private function runMigrations(): void 47 + { 48 $this->newLine(); 49 $this->comment('Running migrations...'); 50 ··· 54 } else { 55 $this->warn('โš  Skipped migrations. Run "php artisan migrate" manually when ready.'); 56 } 57 + } 58 59 + private function displaySuccessMessage(): void 60 + { 61 $this->newLine(); 62 $this->info('Signal package installed successfully!'); 63 $this->newLine(); 64 + } 65 66 + private function displayNextSteps(): void 67 + { 68 $this->line('Next steps:'); 69 $this->line('1. Review the config file: config/signal.php'); 70 $this->line('2. Create your first signal: php artisan make:signal NewPostSignal'); 71 $this->line('3. Start consuming events: php artisan signal:consume'); 72 } 73 }
+80 -29
src/Commands/ListSignalsCommand.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Commands; 4 5 use Illuminate\Console\Command; 6 - use SocialDept\Signal\Services\SignalRegistry; 7 8 class ListSignalsCommand extends Command 9 { ··· 13 14 public function handle(SignalRegistry $registry): int 15 { 16 - $registry->discover(); 17 - 18 - $signals = $registry->all(); 19 20 if ($signals->isEmpty()) { 21 - $this->warn('No signals registered.'); 22 - $this->info('Create signals in app/Signals or register them in config/signal.php'); 23 return self::SUCCESS; 24 } 25 26 - $this->components->info("Found {$signals->count()} " . str('signal')->plural($signals->count())); 27 $this->newLine(); 28 29 - foreach ($signals as $signal) { 30 - $className = class_basename($signal); 31 - $fullClassName = get_class($signal); 32 33 - $this->line(" <fg=green>โ€ข</> <options=bold>{$className}</>"); 34 - $this->line(" <fg=gray>Class:</> {$fullClassName}"); 35 36 - $eventTypes = collect($signal->eventTypes())->map(fn($type) => "<fg=cyan>{$type}</>")->join(', '); 37 - $this->line(" <fg=gray>Events:</> {$eventTypes}"); 38 39 - $collections = $signal->collections() 40 - ? collect($signal->collections())->map(fn($col) => "<fg=yellow>{$col}</>")->join(', ') 41 - : '<fg=gray>All collections</>'; 42 - $this->line(" <fg=gray>Collections:</> {$collections}"); 43 44 - if ($signal->dids()) { 45 - $dids = collect($signal->dids())->map(fn($did) => "<fg=magenta>{$did}</>")->join(', '); 46 - $this->line(" <fg=gray>DIDs:</> {$dids}"); 47 - } 48 49 - if ($signal->shouldQueue()) { 50 - $queue = $signal->queue() ?? 'default'; 51 - $this->line(" <fg=gray>Queue:</> <fg=blue>{$queue}</>"); 52 - } 53 54 - $this->newLine(); 55 } 56 57 - return self::SUCCESS; 58 } 59 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Commands; 4 5 use Illuminate\Console\Command; 6 + use Illuminate\Support\Collection; 7 + use SocialDept\AtpSignals\Services\SignalRegistry; 8 9 class ListSignalsCommand extends Command 10 { ··· 14 15 public function handle(SignalRegistry $registry): int 16 { 17 + $signals = $this->discoverSignals($registry); 18 19 if ($signals->isEmpty()) { 20 + $this->displayNoSignalsWarning(); 21 + 22 return self::SUCCESS; 23 } 24 25 + $this->displaySignalCount($signals->count()); 26 + 27 + foreach ($signals as $signal) { 28 + $this->displaySignalDetails($signal); 29 + } 30 + 31 + return self::SUCCESS; 32 + } 33 + 34 + private function discoverSignals(SignalRegistry $registry): Collection 35 + { 36 + $registry->discover(); 37 + 38 + return $registry->all(); 39 + } 40 + 41 + private function displayNoSignalsWarning(): void 42 + { 43 + $this->warn('No signals registered.'); 44 + $this->info('Create signals in app/Signals or register them in config/signal.php'); 45 + } 46 + 47 + private function displaySignalCount(int $count): void 48 + { 49 + $this->components->info("Found {$count} ".str('signal')->plural($count)); 50 $this->newLine(); 51 + } 52 53 + private function displaySignalDetails(object $signal): void 54 + { 55 + $className = class_basename($signal); 56 + $fullClassName = get_class($signal); 57 + 58 + $this->line(" <fg=green>โ€ข</> <options=bold>{$className}</>"); 59 + $this->line(" <fg=gray>Class:</> {$fullClassName}"); 60 + 61 + $this->displayEventTypes($signal); 62 + $this->displayCollections($signal); 63 + $this->displayDids($signal); 64 + $this->displayQueueInfo($signal); 65 + 66 + $this->newLine(); 67 + } 68 69 + private function displayEventTypes(object $signal): void 70 + { 71 + $eventTypes = collect($signal->eventTypes()) 72 + ->map(fn ($type) => "<fg=cyan>{$type}</>") 73 + ->join(', '); 74 75 + $this->line(" <fg=gray>Events:</> {$eventTypes}"); 76 + } 77 78 + private function displayCollections(object $signal): void 79 + { 80 + $collections = $signal->collections() 81 + ? collect($signal->collections())->map(fn ($col) => "<fg=yellow>{$col}</>")->join(', ') 82 + : '<fg=gray>All collections</>'; 83 84 + $this->line(" <fg=gray>Collections:</> {$collections}"); 85 + } 86 87 + private function displayDids(object $signal): void 88 + { 89 + if (! $signal->dids()) { 90 + return; 91 + } 92 + 93 + $dids = collect($signal->dids()) 94 + ->map(fn ($did) => "<fg=magenta>{$did}</>") 95 + ->join(', '); 96 + 97 + $this->line(" <fg=gray>DIDs:</> {$dids}"); 98 + } 99 100 + private function displayQueueInfo(object $signal): void 101 + { 102 + if (! $signal->shouldQueue()) { 103 + return; 104 } 105 106 + $queue = $signal->queue() ?? 'default'; 107 + 108 + $this->line(" <fg=gray>Queue:</> <fg=blue>{$queue}</>"); 109 } 110 }
+1 -1
src/Commands/MakeSignalCommand.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Commands; 4 5 use Illuminate\Console\GeneratorCommand; 6 use Symfony\Component\Console\Input\InputOption;
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Commands; 4 5 use Illuminate\Console\GeneratorCommand; 6 use Symfony\Component\Console\Input\InputOption;
+59 -33
src/Commands/TestSignalCommand.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Commands; 4 5 use Illuminate\Console\Command; 6 - use SocialDept\Signal\Events\SignalEvent; 7 - use SocialDept\Signal\Events\CommitEvent; 8 9 class TestSignalCommand extends Command 10 { ··· 16 17 public function handle(): int 18 { 19 $signalClass = $this->argument('signal'); 20 21 - // Try to resolve the class 22 - if (!class_exists($signalClass)) { 23 - $signalClass = 'App\\Signals\\' . $signalClass; 24 } 25 26 - if (!class_exists($signalClass)) { 27 $this->error("Signal class not found: {$signalClass}"); 28 - return self::FAILURE; 29 } 30 31 - $signal = app($signalClass); 32 33 $this->info("Testing signal: {$signalClass}"); 34 $this->newLine(); 35 36 - // Create sample event 37 $event = $this->createSampleEvent($this->option('sample')); 38 39 $this->info('Sample event created:'); 40 $this->line(json_encode($event->toArray(), JSON_PRETTY_PRINT)); 41 $this->newLine(); 42 43 try { 44 if ($signal->shouldHandle($event)) { 45 $this->info('Calling signal->handle()...'); ··· 49 $this->warn('Signal->shouldHandle() returned false'); 50 } 51 } catch (\Exception $e) { 52 - $this->error('Error executing signal: ' . $e->getMessage()); 53 return self::FAILURE; 54 } 55 ··· 58 59 protected function createSampleEvent(string $type): SignalEvent 60 { 61 - switch ($type) { 62 - case 'commit': 63 - return new SignalEvent( 64 - did: 'did:plc:sample123456789', 65 - timeUs: time() * 1000000, 66 - kind: 'commit', 67 - commit: new CommitEvent( 68 - rev: 'sample-rev', 69 - operation: 'create', 70 - collection: 'app.bsky.feed.post', 71 - rkey: 'sample-rkey', 72 - record: (object) [ 73 - 'text' => 'This is a sample post for testing', 74 - 'createdAt' => now()->toIso8601String(), 75 - ], 76 - cid: 'sample-cid', 77 - ), 78 - ); 79 - 80 - default: 81 - throw new \InvalidArgumentException("Unknown sample type: {$type}"); 82 - } 83 } 84 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Commands; 4 5 use Illuminate\Console\Command; 6 + use InvalidArgumentException; 7 + use SocialDept\AtpSignals\Events\CommitEvent; 8 + use SocialDept\AtpSignals\Events\SignalEvent; 9 10 class TestSignalCommand extends Command 11 { ··· 17 18 public function handle(): int 19 { 20 + $signalClass = $this->resolveSignalClass(); 21 + 22 + if ($signalClass === null) { 23 + return self::FAILURE; 24 + } 25 + 26 + $signal = app($signalClass); 27 + 28 + $this->displayTestHeader($signalClass); 29 + 30 + $event = $this->createAndDisplaySampleEvent(); 31 + 32 + return $this->executeSignal($signal, $event); 33 + } 34 + 35 + private function resolveSignalClass(): ?string 36 + { 37 $signalClass = $this->argument('signal'); 38 39 + if (! class_exists($signalClass)) { 40 + $signalClass = 'App\\Signals\\'.$signalClass; 41 } 42 43 + if (! class_exists($signalClass)) { 44 $this->error("Signal class not found: {$signalClass}"); 45 + 46 + return null; 47 } 48 49 + return $signalClass; 50 + } 51 52 + private function displayTestHeader(string $signalClass): void 53 + { 54 $this->info("Testing signal: {$signalClass}"); 55 $this->newLine(); 56 + } 57 58 + private function createAndDisplaySampleEvent(): SignalEvent 59 + { 60 $event = $this->createSampleEvent($this->option('sample')); 61 62 $this->info('Sample event created:'); 63 $this->line(json_encode($event->toArray(), JSON_PRETTY_PRINT)); 64 $this->newLine(); 65 66 + return $event; 67 + } 68 + 69 + private function executeSignal(object $signal, SignalEvent $event): int 70 + { 71 try { 72 if ($signal->shouldHandle($event)) { 73 $this->info('Calling signal->handle()...'); ··· 77 $this->warn('Signal->shouldHandle() returned false'); 78 } 79 } catch (\Exception $e) { 80 + $this->error('Error executing signal: '.$e->getMessage()); 81 + 82 return self::FAILURE; 83 } 84 ··· 87 88 protected function createSampleEvent(string $type): SignalEvent 89 { 90 + return match ($type) { 91 + 'commit' => new SignalEvent( 92 + did: 'did:plc:sample123456789', 93 + timeUs: time() * 1000000, 94 + kind: 'commit', 95 + commit: new CommitEvent( 96 + rev: 'sample-rev', 97 + operation: 'create', 98 + collection: 'app.bsky.feed.post', 99 + rkey: 'sample-rkey', 100 + record: (object) [ 101 + 'text' => 'This is a sample post for testing', 102 + 'createdAt' => now()->toIso8601String(), 103 + ], 104 + cid: 'sample-cid', 105 + ), 106 + ), 107 + default => throw new InvalidArgumentException("Unknown sample type: {$type}"), 108 + }; 109 } 110 }
+1 -1
src/Contracts/CursorStore.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Contracts; 4 5 interface CursorStore 6 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Contracts; 4 5 interface CursorStore 6 {
+16
src/Contracts/EventContract.php
···
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpSignals\Contracts; 4 + 5 + interface EventContract 6 + { 7 + /** 8 + * Create an instance from an array. 9 + */ 10 + public static function fromArray(array $data): self; 11 + 12 + /** 13 + * Convert the instance to an array. 14 + */ 15 + public function toArray(): array; 16 + }
+85
src/Core/CAR.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Core; 6 + 7 + use SocialDept\AtpSignals\CAR\BlockReader; 8 + 9 + /** 10 + * CAR (Content Addressable aRchive) facade. 11 + * 12 + * Provides static methods for parsing CAR data from AT Protocol Firehose. 13 + */ 14 + class CAR 15 + { 16 + /** 17 + * Parse CAR blocks. 18 + * 19 + * Returns array of blocks keyed by CID string. 20 + * The blocks contain raw CBOR data, not decoded. 21 + * 22 + * @param string $data Binary CAR data 23 + * @param string|null $did DID for constructing URIs (not used, kept for compatibility) 24 + * @return array<string, string> Map of CID string => block data 25 + */ 26 + public static function blockMap(string $data, ?string $did = null): array 27 + { 28 + // Read all blocks from CAR 29 + $blockReader = new BlockReader($data); 30 + 31 + return $blockReader->getBlockMap(); 32 + } 33 + 34 + /** 35 + * Extract DID from commit block. 36 + */ 37 + private static function extractDidFromBlocks(array $blocks): ?string 38 + { 39 + // The first block is typically the commit 40 + $firstBlock = reset($blocks); 41 + 42 + if ($firstBlock === false) { 43 + return null; 44 + } 45 + 46 + $decoded = CBOR::decode($firstBlock); 47 + 48 + if (! is_array($decoded)) { 49 + return null; 50 + } 51 + 52 + return $decoded['did'] ?? null; 53 + } 54 + 55 + /** 56 + * Find MST root CID from blocks. 57 + */ 58 + private static function findMstRoot(array $blocks, array $cids): ?CID 59 + { 60 + // Try to parse commit block to get data CID 61 + $firstBlock = reset($blocks); 62 + 63 + if ($firstBlock === false) { 64 + return null; 65 + } 66 + 67 + $commit = CBOR::decode($firstBlock); 68 + 69 + if (! is_array($commit)) { 70 + return null; 71 + } 72 + 73 + // MST root is in the 'data' field of commit 74 + if (isset($commit['data']) && $commit['data'] instanceof CID) { 75 + return $commit['data']; 76 + } 77 + 78 + // Fallback: second block is often the MST root 79 + if (count($cids) >= 2) { 80 + return CID::fromString($cids[1]); 81 + } 82 + 83 + return null; 84 + } 85 + }
+64
src/Core/CBOR.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Core; 6 + 7 + use SocialDept\AtpSignals\CBOR\Decoder; 8 + 9 + /** 10 + * CBOR facade for simple decoding operations. 11 + * 12 + * Provides static methods matching the interface needed by FirehoseConsumer. 13 + */ 14 + class CBOR 15 + { 16 + /** 17 + * Decode first CBOR item and return remainder. 18 + * 19 + * @param string $data Binary CBOR data 20 + * @return array{0: mixed, 1: string} [decoded value, remaining data] 21 + */ 22 + public static function decodeFirst(string $data): array 23 + { 24 + $decoder = new Decoder($data); 25 + $value = $decoder->decode(); 26 + 27 + // Calculate remaining data based on decoder position 28 + $position = $decoder->getPosition(); 29 + $remainder = substr($data, $position); 30 + 31 + return [$value, $remainder]; 32 + } 33 + 34 + /** 35 + * Decode complete CBOR data. 36 + * 37 + * @param string $data Binary CBOR data 38 + * @return mixed Decoded value 39 + */ 40 + public static function decode(string $data): mixed 41 + { 42 + $decoder = new Decoder($data); 43 + 44 + return $decoder->decode(); 45 + } 46 + 47 + /** 48 + * Decode all CBOR items from data. 49 + * 50 + * @param string $data Binary CBOR data 51 + * @return array All decoded values 52 + */ 53 + public static function decodeAll(string $data): array 54 + { 55 + $decoder = new Decoder($data); 56 + $items = []; 57 + 58 + while ($decoder->hasMore()) { 59 + $items[] = $decoder->decode(); 60 + } 61 + 62 + return $items; 63 + } 64 + }
+272
src/Core/CID.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Core; 6 + 7 + use RuntimeException; 8 + use SocialDept\AtpSignals\Binary\Reader; 9 + use SocialDept\AtpSignals\Binary\Varint; 10 + 11 + /** 12 + * Content Identifier (CID) parser for IPLD. 13 + * 14 + * Supports CIDv0 (base58btc) and CIDv1 (multibase). 15 + * Minimal implementation for reading CIDs from CBOR and CAR data. 16 + */ 17 + class CID 18 + { 19 + private const BASE32_CHARSET = 'abcdefghijklmnopqrstuvwxyz234567'; 20 + private const BASE58BTC_CHARSET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 21 + 22 + public function __construct( 23 + public readonly int $version, 24 + public readonly int $codec, 25 + public readonly string $hash, 26 + ) { 27 + } 28 + 29 + /** 30 + * Parse CID from binary data. 31 + * 32 + * @param string $data Binary CID data 33 + * @return self 34 + * @throws RuntimeException If CID is malformed 35 + */ 36 + public static function fromBinary(string $data): self 37 + { 38 + $reader = new Reader($data); 39 + 40 + // Read version 41 + $version = $reader->readVarint(); 42 + 43 + if ($version === 0x12) { 44 + // CIDv0 (legacy format - starts with multihash directly) 45 + // Reset and read as v0 46 + $reader = new Reader($data); 47 + $hashType = $reader->readVarint(); // 0x12 = sha256 48 + $hashLength = $reader->readVarint(); // typically 32 49 + $hash = $reader->readBytes($hashLength); 50 + 51 + return new self( 52 + version: 0, 53 + codec: 0x70, // dag-pb 54 + hash: chr($hashType) . chr($hashLength) . $hash, 55 + ); 56 + } 57 + 58 + if ($version !== 1) { 59 + throw new RuntimeException("Unsupported CID version: {$version}"); 60 + } 61 + 62 + // Read codec 63 + $codec = $reader->readVarint(); 64 + 65 + // Read multihash (hash type + length + hash bytes) 66 + $hashType = $reader->readVarint(); 67 + $hashLength = $reader->readVarint(); 68 + $hashBytes = $reader->readBytes($hashLength); 69 + 70 + // Store complete multihash 71 + $hash = chr($hashType) . chr($hashLength) . $hashBytes; 72 + 73 + return new self( 74 + version: $version, 75 + codec: $codec, 76 + hash: $hash, 77 + ); 78 + } 79 + 80 + /** 81 + * Parse CID from string (base32 or base58btc). 82 + * 83 + * @param string $str CID string 84 + * @return self 85 + * @throws RuntimeException If CID string is invalid 86 + */ 87 + public static function fromString(string $str): self 88 + { 89 + if (empty($str)) { 90 + throw new RuntimeException('Empty CID string'); 91 + } 92 + 93 + // Check multibase prefix 94 + $prefix = $str[0]; 95 + 96 + if ($prefix === 'b') { 97 + // base32 (CIDv1) 98 + $binary = self::decodeBase32(substr($str, 1)); 99 + 100 + return self::fromBinary($binary); 101 + } 102 + 103 + if ($prefix === 'Q' || $prefix === '1') { 104 + // base58btc (likely CIDv0) 105 + $binary = self::decodeBase58($str); 106 + 107 + return self::fromBinary($binary); 108 + } 109 + 110 + throw new RuntimeException("Unsupported multibase prefix: {$prefix}"); 111 + } 112 + 113 + /** 114 + * Convert CID to string representation. 115 + */ 116 + public function toString(): string 117 + { 118 + if ($this->version === 0) { 119 + // CIDv0 is always base58btc without prefix 120 + return self::encodeBase58($this->hash); 121 + } 122 + 123 + // CIDv1 uses base32 with 'b' prefix 124 + $binary = chr($this->version) . $this->encodeVarint($this->codec) . $this->hash; 125 + 126 + return 'b' . self::encodeBase32($binary); 127 + } 128 + 129 + /** 130 + * Get binary representation. 131 + */ 132 + public function toBinary(): string 133 + { 134 + if ($this->version === 0) { 135 + return $this->hash; 136 + } 137 + 138 + return chr($this->version) . $this->encodeVarint($this->codec) . $this->hash; 139 + } 140 + 141 + /** 142 + * Decode base32 string to binary. 143 + */ 144 + private static function decodeBase32(string $str): string 145 + { 146 + $str = strtolower($str); 147 + $result = ''; 148 + $bits = 0; 149 + $value = 0; 150 + 151 + for ($i = 0; $i < strlen($str); $i++) { 152 + $char = $str[$i]; 153 + $pos = strpos(self::BASE32_CHARSET, $char); 154 + 155 + if ($pos === false) { 156 + throw new RuntimeException("Invalid base32 character: {$char}"); 157 + } 158 + 159 + $value = ($value << 5) | $pos; 160 + $bits += 5; 161 + 162 + if ($bits >= 8) { 163 + $result .= chr(($value >> ($bits - 8)) & 0xFF); 164 + $bits -= 8; 165 + } 166 + } 167 + 168 + return $result; 169 + } 170 + 171 + /** 172 + * Encode binary to base32 string. 173 + */ 174 + private static function encodeBase32(string $data): string 175 + { 176 + $result = ''; 177 + $bits = 0; 178 + $value = 0; 179 + 180 + for ($i = 0; $i < strlen($data); $i++) { 181 + $value = ($value << 8) | ord($data[$i]); 182 + $bits += 8; 183 + 184 + while ($bits >= 5) { 185 + $result .= self::BASE32_CHARSET[($value >> ($bits - 5)) & 0x1F]; 186 + $bits -= 5; 187 + } 188 + } 189 + 190 + if ($bits > 0) { 191 + $result .= self::BASE32_CHARSET[($value << (5 - $bits)) & 0x1F]; 192 + } 193 + 194 + return $result; 195 + } 196 + 197 + /** 198 + * Decode base58btc string to binary. 199 + */ 200 + private static function decodeBase58(string $str): string 201 + { 202 + $decoded = gmp_init(0); 203 + $base = gmp_init(58); 204 + 205 + for ($i = 0; $i < strlen($str); $i++) { 206 + $char = $str[$i]; 207 + $pos = strpos(self::BASE58BTC_CHARSET, $char); 208 + 209 + if ($pos === false) { 210 + throw new RuntimeException("Invalid base58 character: {$char}"); 211 + } 212 + 213 + $decoded = gmp_add(gmp_mul($decoded, $base), gmp_init($pos)); 214 + } 215 + 216 + $hex = gmp_strval($decoded, 16); 217 + if (strlen($hex) % 2) { 218 + $hex = '0' . $hex; 219 + } 220 + 221 + // Add leading zeros 222 + for ($i = 0; $i < strlen($str) && $str[$i] === '1'; $i++) { 223 + $hex = '00' . $hex; 224 + } 225 + 226 + return hex2bin($hex); 227 + } 228 + 229 + /** 230 + * Encode binary to base58btc string. 231 + */ 232 + private static function encodeBase58(string $data): string 233 + { 234 + $num = gmp_init('0x' . bin2hex($data)); 235 + $base = gmp_init(58); 236 + $result = ''; 237 + 238 + while (gmp_cmp($num, 0) > 0) { 239 + [$num, $remainder] = gmp_div_qr($num, $base); 240 + $result = self::BASE58BTC_CHARSET[gmp_intval($remainder)] . $result; 241 + } 242 + 243 + // Add leading '1's for leading zero bytes 244 + for ($i = 0; $i < strlen($data) && ord($data[$i]) === 0; $i++) { 245 + $result = '1' . $result; 246 + } 247 + 248 + return $result; 249 + } 250 + 251 + /** 252 + * Encode varint for CID binary format. 253 + */ 254 + private function encodeVarint(int $value): string 255 + { 256 + $result = ''; 257 + 258 + while ($value >= 0x80) { 259 + $result .= chr(($value & 0x7F) | 0x80); 260 + $value >>= 7; 261 + } 262 + 263 + $result .= chr($value); 264 + 265 + return $result; 266 + } 267 + 268 + public function __toString(): string 269 + { 270 + return $this->toString(); 271 + } 272 + }
+1 -1
src/Enums/SignalCommitOperation.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Enums; 4 5 enum SignalCommitOperation: string 6 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Enums; 4 5 enum SignalCommitOperation: string 6 {
+1 -1
src/Enums/SignalEventType.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Enums; 4 5 enum SignalEventType: string 6 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Enums; 4 5 enum SignalEventType: string 6 {
+6 -3
src/Events/AccountEvent.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Events; 4 5 - class AccountEvent 6 { 7 public function __construct( 8 public string $did, ··· 10 public ?string $status = null, 11 public int $seq = 0, 12 public ?string $time = null, 13 - ) {} 14 15 public static function fromArray(array $data): self 16 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Events; 4 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 + 7 + class AccountEvent implements EventContract 8 { 9 public function __construct( 10 public string $did, ··· 12 public ?string $status = null, 13 public int $seq = 0, 14 public ?string $time = null, 15 + ) { 16 + } 17 18 public static function fromArray(array $data): self 19 {
+4 -3
src/Events/CommitEvent.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Events; 4 5 - use SocialDept\Signal\Enums\SignalCommitOperation; 6 7 - class CommitEvent 8 { 9 public SignalCommitOperation $operation; 10
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Events; 4 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 7 8 + class CommitEvent implements EventContract 9 { 10 public SignalCommitOperation $operation; 11
+6 -3
src/Events/IdentityEvent.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Events; 4 5 - class IdentityEvent 6 { 7 public function __construct( 8 public string $did, 9 public ?string $handle = null, 10 public int $seq = 0, 11 public ?string $time = null, 12 - ) {} 13 14 public static function fromArray(array $data): self 15 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Events; 4 + 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 7 + class IdentityEvent implements EventContract 8 { 9 public function __construct( 10 public string $did, 11 public ?string $handle = null, 12 public int $seq = 0, 13 public ?string $time = null, 14 + ) { 15 + } 16 17 public static function fromArray(array $data): self 18 {
+7 -4
src/Events/SignalEvent.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Events; 4 5 - class SignalEvent 6 { 7 public function __construct( 8 public string $did, ··· 11 public ?CommitEvent $commit = null, 12 public ?IdentityEvent $identity = null, 13 public ?AccountEvent $account = null, 14 - ) {} 15 16 public function isCommit(): bool 17 { ··· 38 return $this->commit?->record; 39 } 40 41 - public function getOperation(): ?\SocialDept\Signal\Enums\SignalCommitOperation 42 { 43 return $this->commit?->operation; 44 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Events; 4 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 + 7 + class SignalEvent implements EventContract 8 { 9 public function __construct( 10 public string $did, ··· 13 public ?CommitEvent $commit = null, 14 public ?IdentityEvent $identity = null, 15 public ?AccountEvent $account = null, 16 + ) { 17 + } 18 19 public function isCommit(): bool 20 { ··· 41 return $this->commit?->record; 42 } 43 44 + public function getOperation(): ?\SocialDept\AtpSignals\Enums\SignalCommitOperation 45 { 46 return $this->commit?->operation; 47 }
+1 -1
src/Exceptions/ConnectionException.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Exceptions; 4 5 class ConnectionException extends \Exception 6 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Exceptions; 4 5 class ConnectionException extends \Exception 6 {
+1 -1
src/Exceptions/SignalException.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Exceptions; 4 5 class SignalException extends \Exception 6 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Exceptions; 4 5 class SignalException extends \Exception 6 {
+5 -4
src/Facades/Signal.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Facades; 4 5 use Illuminate\Support\Facades\Facade; 6 - use SocialDept\Signal\Services\JetstreamConsumer; 7 8 /** 9 * @method static void start(?int $cursor = null) 10 * @method static void stop() 11 * 12 - * @see \SocialDept\Signal\Services\JetstreamConsumer 13 */ 14 class Signal extends Facade 15 { 16 protected static function getFacadeAccessor(): string 17 { 18 - return JetstreamConsumer::class; 19 } 20 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Facades; 4 5 use Illuminate\Support\Facades\Facade; 6 + use SocialDept\AtpSignals\Services\SignalManager; 7 8 /** 9 * @method static void start(?int $cursor = null) 10 * @method static void stop() 11 + * @method static string getMode() 12 * 13 + * @see \SocialDept\AtpSignals\Services\SignalManager 14 */ 15 class Signal extends Facade 16 { 17 protected static function getFacadeAccessor(): string 18 { 19 + return SignalManager::class; 20 } 21 }
+9 -5
src/Jobs/ProcessSignalJob.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Jobs; 4 5 use Illuminate\Bus\Queueable; 6 use Illuminate\Contracts\Queue\ShouldQueue; 7 use Illuminate\Foundation\Bus\Dispatchable; 8 use Illuminate\Queue\InteractsWithQueue; 9 use Illuminate\Queue\SerializesModels; 10 - use SocialDept\Signal\Events\SignalEvent; 11 - use SocialDept\Signal\Signals\Signal; 12 13 class ProcessSignalJob implements ShouldQueue 14 { 15 - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; 16 17 public function __construct( 18 protected Signal $signal, 19 protected SignalEvent $event, 20 - ) {} 21 22 public function handle(): void 23 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Jobs; 4 5 use Illuminate\Bus\Queueable; 6 use Illuminate\Contracts\Queue\ShouldQueue; 7 use Illuminate\Foundation\Bus\Dispatchable; 8 use Illuminate\Queue\InteractsWithQueue; 9 use Illuminate\Queue\SerializesModels; 10 + use SocialDept\AtpSignals\Events\SignalEvent; 11 + use SocialDept\AtpSignals\Signals\Signal; 12 13 class ProcessSignalJob implements ShouldQueue 14 { 15 + use Dispatchable; 16 + use InteractsWithQueue; 17 + use Queueable; 18 + use SerializesModels; 19 20 public function __construct( 21 protected Signal $signal, 22 protected SignalEvent $event, 23 + ) { 24 + } 25 26 public function handle(): void 27 {
+3 -3
src/Services/EventDispatcher.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Services; 4 5 use Illuminate\Support\Facades\Log; 6 use Illuminate\Support\Facades\Queue; 7 - use SocialDept\Signal\Events\SignalEvent; 8 - use SocialDept\Signal\Jobs\ProcessSignalJob; 9 10 class EventDispatcher 11 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Services; 4 5 use Illuminate\Support\Facades\Log; 6 use Illuminate\Support\Facades\Queue; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Jobs\ProcessSignalJob; 9 10 class EventDispatcher 11 {
+139 -47
src/Services/FirehoseConsumer.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Services; 4 5 use Illuminate\Support\Arr; 6 use Illuminate\Support\Facades\Log; 7 - use Revolution\Bluesky\Core\CAR; 8 - use Revolution\Bluesky\Core\CBOR; 9 - use SocialDept\Signal\Contracts\CursorStore; 10 - use SocialDept\Signal\Events\CommitEvent; 11 - use SocialDept\Signal\Events\SignalEvent; 12 - use SocialDept\Signal\Exceptions\ConnectionException; 13 - use SocialDept\Signal\Support\WebSocketConnection; 14 15 class FirehoseConsumer 16 { ··· 43 { 44 $this->shouldStop = false; 45 46 - // Get cursor from storage if not provided 47 if ($cursor === null) { 48 $cursor = $this->cursorStore->get(); 49 } 50 51 - $url = $this->buildWebSocketUrl($cursor); 52 53 Log::info('Signal: Starting Firehose consumer', [ 54 'url' => $url, 55 - 'cursor' => $cursor, 56 'mode' => 'firehose', 57 ]); 58 ··· 78 */ 79 protected function connect(string $url): void 80 { 81 - $this->connection = new WebSocketConnection; 82 83 // Set up event handlers 84 $this->connection ··· 174 $time = $payload['time']; 175 $timeUs = $payload['seq'] ?? 0; // Use seq as time_us equivalent 176 177 - // Parse CAR blocks 178 $records = $payload['blocks']; 179 $blocks = []; 180 if (! empty($records)) { 181 - $blocks = rescue(fn () => iterator_to_array(CAR::blockMap($records)), []); 182 } 183 184 // Process operations ··· 200 $rkey = ''; 201 202 if (str_contains($path, '/')) { 203 - [$collection, $rkey] = explode('/', $path); 204 } 205 206 - $record = $blocks[$path] ?? []; 207 208 // Convert to SignalEvent format for compatibility 209 - $event = $this->buildSignalEvent($did, $timeUs, $action, $collection, $rkey, $rev, $cid, $record); 210 - 211 - // Update cursor 212 - $this->cursorStore->set($timeUs); 213 - 214 - // Check if any signals match this event 215 - $matchingSignals = $this->signalRegistry->getMatchingSignals($event); 216 - 217 - if ($matchingSignals->isNotEmpty()) { 218 - Log::info('Signal: Event matched', [ 219 - 'collection' => $collection, 220 - 'operation' => $action, 221 - 'matched_signals' => $matchingSignals->count(), 222 - 'signal_names' => $matchingSignals->map(fn ($s) => class_basename($s))->join(', '), 223 - ]); 224 - } 225 226 - // Dispatch to matching signals 227 - $this->eventDispatcher->dispatch($event); 228 } 229 } 230 ··· 241 ?string $cid, 242 array $record 243 ): SignalEvent { 244 - $recordValue = $record['value'] ?? null; 245 246 $commitEvent = new CommitEvent( 247 rev: $rev, 248 operation: $operation, 249 collection: $collection, 250 rkey: $rkey, 251 - record: $recordValue ? (object) $recordValue : null, 252 cid: $cid 253 ); 254 ··· 261 } 262 263 /** 264 * Handle identity event from Firehose. 265 */ 266 protected function handleIdentity(array $payload): void 267 { 268 - // Identity events are received but not currently processed 269 } 270 271 /** ··· 273 */ 274 protected function handleAccount(array $payload): void 275 { 276 - // Account events are received but not currently processed 277 } 278 279 /** ··· 313 314 if ($this->reconnectAttempts >= $maxAttempts) { 315 Log::error('Signal: Max reconnection attempts reached'); 316 throw new ConnectionException('Failed to reconnect to Firehose after '.$maxAttempts.' attempts'); 317 } 318 ··· 360 if (! empty($params)) { 361 $url .= '?'.implode('&', $params); 362 } 363 - 364 - Log::warning('Signal: Firehose mode - NO server-side collection filtering', [ 365 - 'note' => 'All events will be received and filtered client-side', 366 - 'registered_collections' => $this->signalRegistry->all() 367 - ->flatMap(fn ($signal) => $signal->collections() ?? []) 368 - ->unique() 369 - ->values() 370 - ->toArray(), 371 - ]); 372 373 return $url; 374 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Services; 4 5 use Illuminate\Support\Arr; 6 use Illuminate\Support\Facades\Log; 7 + use SocialDept\AtpSignals\Contracts\CursorStore; 8 + use SocialDept\AtpSignals\Core\CAR; 9 + use SocialDept\AtpSignals\Core\CBOR; 10 + use SocialDept\AtpSignals\Core\CID; 11 + use SocialDept\AtpSignals\Events\AccountEvent; 12 + use SocialDept\AtpSignals\Events\CommitEvent; 13 + use SocialDept\AtpSignals\Events\IdentityEvent; 14 + use SocialDept\AtpSignals\Events\SignalEvent; 15 + use SocialDept\AtpSignals\Exceptions\ConnectionException; 16 + use SocialDept\AtpSignals\Support\WebSocketConnection; 17 18 class FirehoseConsumer 19 { ··· 46 { 47 $this->shouldStop = false; 48 49 + // Get cursor from storage if not explicitly provided 50 + // null = use stored cursor, 0 = start fresh (no cursor), >0 = specific cursor 51 if ($cursor === null) { 52 $cursor = $this->cursorStore->get(); 53 } 54 55 + // If cursor is explicitly 0, don't send it (fresh start) 56 + $url = $this->buildWebSocketUrl($cursor > 0 ? $cursor : null); 57 58 Log::info('Signal: Starting Firehose consumer', [ 59 'url' => $url, 60 + 'cursor' => $cursor > 0 ? $cursor : 'none (fresh start)', 61 'mode' => 'firehose', 62 ]); 63 ··· 83 */ 84 protected function connect(string $url): void 85 { 86 + $this->connection = new WebSocketConnection(); 87 88 // Set up event handlers 89 $this->connection ··· 179 $time = $payload['time']; 180 $timeUs = $payload['seq'] ?? 0; // Use seq as time_us equivalent 181 182 + // Parse CAR blocks (returns CID => block data map) 183 $records = $payload['blocks']; 184 + 185 $blocks = []; 186 if (! empty($records)) { 187 + $blocks = rescue(fn () => CAR::blockMap($records, $did), [], function (\Throwable $e) { 188 + Log::warning('Signal: Failed to parse CAR blocks', [ 189 + 'error' => $e->getMessage(), 190 + 'trace' => $e->getTraceAsString(), 191 + ]); 192 + }); 193 } 194 195 // Process operations ··· 211 $rkey = ''; 212 213 if (str_contains($path, '/')) { 214 + [$collection, $rkey] = explode('/', $path, 2); 215 } 216 217 + // Get record data from blocks using the op CID 218 + // Convert CID to string if it's an object 219 + $cidStr = $cid instanceof CID ? $cid->toString() : $cid; 220 + 221 + // For delete operations, there won't be a record 222 + $record = []; 223 + if ($action !== 'delete' && isset($blocks[$cidStr])) { 224 + // Decode the CBOR block to get the record data 225 + $decoded = rescue(fn () => CBOR::decode($blocks[$cidStr])); 226 + if (is_array($decoded)) { 227 + $record = $this->normalizeCids($decoded); 228 + } 229 + } 230 231 // Convert to SignalEvent format for compatibility 232 + $event = $this->buildSignalEvent($did, $timeUs, $action, $collection, $rkey, $rev, $cidStr, $record); 233 234 + // Dispatch event with cursor update 235 + $this->dispatchSignalEvent($event); 236 } 237 } 238 ··· 249 ?string $cid, 250 array $record 251 ): SignalEvent { 252 + // Record is already the decoded data, or empty array for deletes 253 + $recordValue = ! empty($record) ? (object) $record : null; 254 255 $commitEvent = new CommitEvent( 256 rev: $rev, 257 operation: $operation, 258 collection: $collection, 259 rkey: $rkey, 260 + record: $recordValue, 261 cid: $cid 262 ); 263 ··· 270 } 271 272 /** 273 + * Normalize CID objects to AT Protocol link format. 274 + */ 275 + protected function normalizeCids(array $data): array 276 + { 277 + foreach ($data as $key => $value) { 278 + if ($value instanceof CID) { 279 + // Convert CID to AT Protocol link format 280 + $data[$key] = ['$link' => $value->toString()]; 281 + } elseif (is_array($value)) { 282 + $data[$key] = $this->normalizeCids($value); 283 + } 284 + } 285 + 286 + return $data; 287 + } 288 + 289 + /** 290 + * Dispatch a SignalEvent with cursor update. 291 + */ 292 + protected function dispatchSignalEvent(SignalEvent $event): void 293 + { 294 + // Update cursor 295 + $this->cursorStore->set($event->timeUs); 296 + 297 + // Dispatch to matching signals 298 + $this->eventDispatcher->dispatch($event); 299 + } 300 + 301 + /** 302 * Handle identity event from Firehose. 303 */ 304 protected function handleIdentity(array $payload): void 305 { 306 + // Validate required fields 307 + if (! isset($payload['did'])) { 308 + Log::debug('Signal: Invalid identity payload - missing did', ['payload' => $payload]); 309 + 310 + return; 311 + } 312 + 313 + $did = $payload['did']; 314 + $handle = $payload['handle'] ?? null; 315 + $seq = $payload['seq'] ?? 0; 316 + $time = $payload['time'] ?? null; 317 + $timeUs = $seq; // Use seq as timeUs equivalent for cursor management 318 + 319 + // Create IdentityEvent 320 + $identityEvent = new IdentityEvent( 321 + did: $did, 322 + handle: $handle, 323 + seq: $seq, 324 + time: $time 325 + ); 326 + 327 + // Create SignalEvent wrapper 328 + $event = new SignalEvent( 329 + did: $did, 330 + timeUs: $timeUs, 331 + kind: 'identity', 332 + identity: $identityEvent 333 + ); 334 + 335 + // Dispatch event with cursor update 336 + $this->dispatchSignalEvent($event); 337 } 338 339 /** ··· 341 */ 342 protected function handleAccount(array $payload): void 343 { 344 + // Validate required fields 345 + if (! isset($payload['did'], $payload['active'])) { 346 + Log::debug('Signal: Invalid account payload - missing required fields', ['payload' => $payload]); 347 + 348 + return; 349 + } 350 + 351 + $did = $payload['did']; 352 + $active = (bool) $payload['active']; 353 + $status = $payload['status'] ?? null; 354 + $seq = $payload['seq'] ?? 0; 355 + $time = $payload['time'] ?? null; 356 + $timeUs = $seq; // Use seq as timeUs equivalent for cursor management 357 + 358 + // Create AccountEvent 359 + $accountEvent = new AccountEvent( 360 + did: $did, 361 + active: $active, 362 + status: $status, 363 + seq: $seq, 364 + time: $time 365 + ); 366 + 367 + // Create SignalEvent wrapper 368 + $event = new SignalEvent( 369 + did: $did, 370 + timeUs: $timeUs, 371 + kind: 'account', 372 + account: $accountEvent 373 + ); 374 + 375 + // Dispatch event with cursor update 376 + $this->dispatchSignalEvent($event); 377 } 378 379 /** ··· 413 414 if ($this->reconnectAttempts >= $maxAttempts) { 415 Log::error('Signal: Max reconnection attempts reached'); 416 + 417 throw new ConnectionException('Failed to reconnect to Firehose after '.$maxAttempts.' attempts'); 418 } 419 ··· 461 if (! empty($params)) { 462 $url .= '?'.implode('&', $params); 463 } 464 465 return $url; 466 }
+13 -30
src/Services/JetstreamConsumer.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Services; 4 5 use Illuminate\Support\Facades\Log; 6 - use SocialDept\Signal\Contracts\CursorStore; 7 - use SocialDept\Signal\Events\SignalEvent; 8 - use SocialDept\Signal\Exceptions\ConnectionException; 9 - use SocialDept\Signal\Support\WebSocketConnection; 10 11 class JetstreamConsumer 12 { ··· 39 { 40 $this->shouldStop = false; 41 42 - // Get cursor from storage if not provided 43 if ($cursor === null) { 44 $cursor = $this->cursorStore->get(); 45 } 46 47 - $url = $this->buildWebSocketUrl($cursor); 48 49 Log::info('Signal: Starting Jetstream consumer', [ 50 'url' => $url, 51 - 'cursor' => $cursor, 52 ]); 53 54 $this->connect($url); ··· 73 */ 74 protected function connect(string $url): void 75 { 76 - $this->connection = new WebSocketConnection; 77 78 // Set up event handlers 79 $this->connection ··· 127 // Update cursor 128 $this->cursorStore->set($event->timeUs); 129 130 - // Check if any signals match this event 131 - $matchingSignals = $this->signalRegistry->getMatchingSignals($event); 132 - 133 - if ($matchingSignals->isNotEmpty()) { 134 - $collection = $event->getCollection() ?? $event->kind; 135 - $operation = $event->getOperation() ?? 'event'; 136 - 137 - Log::info('Signal: Event matched', [ 138 - 'collection' => $collection, 139 - 'operation' => $operation, 140 - 'matched_signals' => $matchingSignals->count(), 141 - 'signal_names' => $matchingSignals->map(fn ($s) => class_basename($s))->join(', '), 142 - ]); 143 - } 144 - 145 // Dispatch to matching signals 146 $this->eventDispatcher->dispatch($event); 147 ··· 190 191 if ($this->reconnectAttempts >= $maxAttempts) { 192 Log::error('Signal: Max reconnection attempts reached'); 193 throw new ConnectionException('Failed to reconnect to Jetstream after '.$maxAttempts.' attempts'); 194 } 195 ··· 244 foreach ($collections as $collection) { 245 $params[] = 'wantedCollections='.urlencode($collection); 246 } 247 - 248 - Log::info('Signal: Collection filters applied', [ 249 - 'collections' => $collections->toArray(), 250 - ]); 251 - } else { 252 - Log::warning('Signal: No collection filters - will receive ALL events'); 253 } 254 255 if (! empty($params)) {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Services; 4 5 use Illuminate\Support\Facades\Log; 6 + use SocialDept\AtpSignals\Contracts\CursorStore; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Exceptions\ConnectionException; 9 + use SocialDept\AtpSignals\Support\WebSocketConnection; 10 11 class JetstreamConsumer 12 { ··· 39 { 40 $this->shouldStop = false; 41 42 + // Get cursor from storage if not explicitly provided 43 + // null = use stored cursor, 0 = start fresh (no cursor), >0 = specific cursor 44 if ($cursor === null) { 45 $cursor = $this->cursorStore->get(); 46 } 47 48 + // If cursor is explicitly 0, don't send it (fresh start) 49 + $url = $this->buildWebSocketUrl($cursor > 0 ? $cursor : null); 50 51 Log::info('Signal: Starting Jetstream consumer', [ 52 'url' => $url, 53 + 'cursor' => $cursor > 0 ? $cursor : 'none (fresh start)', 54 + 'mode' => 'firehose', 55 ]); 56 57 $this->connect($url); ··· 76 */ 77 protected function connect(string $url): void 78 { 79 + $this->connection = new WebSocketConnection(); 80 81 // Set up event handlers 82 $this->connection ··· 130 // Update cursor 131 $this->cursorStore->set($event->timeUs); 132 133 // Dispatch to matching signals 134 $this->eventDispatcher->dispatch($event); 135 ··· 178 179 if ($this->reconnectAttempts >= $maxAttempts) { 180 Log::error('Signal: Max reconnection attempts reached'); 181 + 182 throw new ConnectionException('Failed to reconnect to Jetstream after '.$maxAttempts.' attempts'); 183 } 184 ··· 233 foreach ($collections as $collection) { 234 $params[] = 'wantedCollections='.urlencode($collection); 235 } 236 } 237 238 if (! empty($params)) {
+52
src/Services/SignalManager.php
···
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpSignals\Services; 4 + 5 + use InvalidArgumentException; 6 + 7 + class SignalManager 8 + { 9 + public function __construct( 10 + protected FirehoseConsumer $firehoseConsumer, 11 + protected JetstreamConsumer $jetstreamConsumer, 12 + ) { 13 + } 14 + 15 + /** 16 + * Start consuming events from the AT Protocol. 17 + */ 18 + public function start(?int $cursor = null): void 19 + { 20 + $this->resolveConsumer()->start($cursor); 21 + } 22 + 23 + /** 24 + * Stop consuming events. 25 + */ 26 + public function stop(): void 27 + { 28 + $this->resolveConsumer()->stop(); 29 + } 30 + 31 + /** 32 + * Get the current consumer mode. 33 + */ 34 + public function getMode(): string 35 + { 36 + return config('signal.mode', 'jetstream'); 37 + } 38 + 39 + /** 40 + * Resolve the appropriate consumer based on configuration. 41 + */ 42 + protected function resolveConsumer(): FirehoseConsumer|JetstreamConsumer 43 + { 44 + $mode = $this->getMode(); 45 + 46 + return match ($mode) { 47 + 'firehose' => $this->firehoseConsumer, 48 + 'jetstream' => $this->jetstreamConsumer, 49 + default => throw new InvalidArgumentException("Invalid signal mode: {$mode}. Must be 'jetstream' or 'firehose'."), 50 + }; 51 + } 52 + }
+4 -3
src/Services/SignalRegistry.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Services; 4 5 use Illuminate\Support\Collection; 6 use Illuminate\Support\Facades\File; 7 - use SocialDept\Signal\Signals\Signal; 8 9 class SignalRegistry 10 { ··· 21 public function register(string $signalClass): void 22 { 23 if (! is_subclass_of($signalClass, Signal::class)) { 24 - throw new \InvalidArgumentException( 25 'Signal class must extend '.Signal::class 26 ); 27 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Services; 4 5 use Illuminate\Support\Collection; 6 use Illuminate\Support\Facades\File; 7 + use InvalidArgumentException; 8 + use SocialDept\AtpSignals\Signals\Signal; 9 10 class SignalRegistry 11 { ··· 22 public function register(string $signalClass): void 23 { 24 if (! is_subclass_of($signalClass, Signal::class)) { 25 + throw new InvalidArgumentException( 26 'Signal class must extend '.Signal::class 27 ); 28 }
+36 -17
src/SignalServiceProvider.php
··· 1 <?php 2 3 - namespace SocialDept\Signal; 4 5 use Illuminate\Support\ServiceProvider; 6 - use SocialDept\Signal\Commands\ConsumeCommand; 7 - use SocialDept\Signal\Commands\InstallCommand; 8 - use SocialDept\Signal\Commands\ListSignalsCommand; 9 - use SocialDept\Signal\Commands\MakeSignalCommand; 10 - use SocialDept\Signal\Commands\TestSignalCommand; 11 - use SocialDept\Signal\Contracts\CursorStore; 12 - use SocialDept\Signal\Services\EventDispatcher; 13 - use SocialDept\Signal\Services\JetstreamConsumer; 14 - use SocialDept\Signal\Services\SignalRegistry; 15 - use SocialDept\Signal\Storage\DatabaseCursorStore; 16 - use SocialDept\Signal\Storage\FileCursorStore; 17 - use SocialDept\Signal\Storage\RedisCursorStore; 18 19 class SignalServiceProvider extends ServiceProvider 20 { 21 public function register(): void 22 { 23 - $this->mergeConfigFrom(__DIR__ . '/../config/signal.php', 'signal'); 24 25 // Register cursor store 26 $this->app->singleton(CursorStore::class, function ($app) { ··· 56 $app->make(EventDispatcher::class), 57 ); 58 }); 59 } 60 61 public function boot(): void ··· 63 if ($this->app->runningInConsole()) { 64 // Publish config 65 $this->publishes([ 66 - __DIR__ . '/../config/signal.php' => config_path('signal.php'), 67 ], 'signal-config'); 68 69 // Publish migrations 70 $this->publishes([ 71 - __DIR__ . '/../database/migrations' => database_path('migrations'), 72 ], 'signal-migrations'); 73 74 // Register commands ··· 82 } 83 84 // Load migrations 85 - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 86 } 87 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals; 4 5 use Illuminate\Support\ServiceProvider; 6 + use SocialDept\AtpSignals\Commands\ConsumeCommand; 7 + use SocialDept\AtpSignals\Commands\InstallCommand; 8 + use SocialDept\AtpSignals\Commands\ListSignalsCommand; 9 + use SocialDept\AtpSignals\Commands\MakeSignalCommand; 10 + use SocialDept\AtpSignals\Commands\TestSignalCommand; 11 + use SocialDept\AtpSignals\Contracts\CursorStore; 12 + use SocialDept\AtpSignals\Services\EventDispatcher; 13 + use SocialDept\AtpSignals\Services\FirehoseConsumer; 14 + use SocialDept\AtpSignals\Services\JetstreamConsumer; 15 + use SocialDept\AtpSignals\Services\SignalManager; 16 + use SocialDept\AtpSignals\Services\SignalRegistry; 17 + use SocialDept\AtpSignals\Storage\DatabaseCursorStore; 18 + use SocialDept\AtpSignals\Storage\FileCursorStore; 19 + use SocialDept\AtpSignals\Storage\RedisCursorStore; 20 21 class SignalServiceProvider extends ServiceProvider 22 { 23 public function register(): void 24 { 25 + $this->mergeConfigFrom(__DIR__.'/../config/signal.php', 'signal'); 26 27 // Register cursor store 28 $this->app->singleton(CursorStore::class, function ($app) { ··· 58 $app->make(EventDispatcher::class), 59 ); 60 }); 61 + 62 + // Register Firehose consumer 63 + $this->app->singleton(FirehoseConsumer::class, function ($app) { 64 + return new FirehoseConsumer( 65 + $app->make(CursorStore::class), 66 + $app->make(SignalRegistry::class), 67 + $app->make(EventDispatcher::class), 68 + ); 69 + }); 70 + 71 + // Register Signal manager 72 + $this->app->singleton(SignalManager::class, function ($app) { 73 + return new SignalManager( 74 + $app->make(FirehoseConsumer::class), 75 + $app->make(JetstreamConsumer::class), 76 + ); 77 + }); 78 } 79 80 public function boot(): void ··· 82 if ($this->app->runningInConsole()) { 83 // Publish config 84 $this->publishes([ 85 + __DIR__.'/../config/signal.php' => config_path('signal.php'), 86 ], 'signal-config'); 87 88 // Publish migrations 89 $this->publishes([ 90 + __DIR__.'/../database/migrations' => database_path('migrations'), 91 ], 'signal-migrations'); 92 93 // Register commands ··· 101 } 102 103 // Load migrations 104 + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 105 } 106 }
+4 -4
src/Signals/Signal.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Signals; 4 5 - use SocialDept\Signal\Events\SignalEvent; 6 7 abstract class Signal 8 { 9 /** 10 * Define which event types to listen for. 11 * 12 - * @return array<string|\SocialDept\Signal\Enums\SignalEventType> 13 */ 14 abstract public function eventTypes(): array; 15 ··· 42 * - [SignalCommitOperation::Delete] - Only handle deletes 43 * - null - Handle all operations (default) 44 * 45 - * @return array<string|\SocialDept\Signal\Enums\SignalCommitOperation>|null 46 */ 47 public function operations(): ?array 48 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Signals; 4 5 + use SocialDept\AtpSignals\Events\SignalEvent; 6 7 abstract class Signal 8 { 9 /** 10 * Define which event types to listen for. 11 * 12 + * @return array<string|\SocialDept\AtpSignals\Enums\SignalEventType> 13 */ 14 abstract public function eventTypes(): array; 15 ··· 42 * - [SignalCommitOperation::Delete] - Only handle deletes 43 * - null - Handle all operations (default) 44 * 45 + * @return array<string|\SocialDept\AtpSignals\Enums\SignalCommitOperation>|null 46 */ 47 public function operations(): ?array 48 {
+4 -3
src/Storage/DatabaseCursorStore.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Storage; 4 5 use Illuminate\Support\Facades\DB; 6 - use SocialDept\Signal\Contracts\CursorStore; 7 8 class DatabaseCursorStore implements CursorStore 9 { ··· 44 ->delete(); 45 } 46 47 - protected function query() 48 { 49 return DB::connection($this->connection) 50 ->table($this->table);
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Storage; 4 5 + use Illuminate\Database\Query\Builder; 6 use Illuminate\Support\Facades\DB; 7 + use SocialDept\AtpSignals\Contracts\CursorStore; 8 9 class DatabaseCursorStore implements CursorStore 10 { ··· 45 ->delete(); 46 } 47 48 + protected function query(): Builder 49 { 50 return DB::connection($this->connection) 51 ->table($this->table);
+4 -4
src/Storage/FileCursorStore.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Storage; 4 5 use Illuminate\Support\Facades\File; 6 - use SocialDept\Signal\Contracts\CursorStore; 7 8 class FileCursorStore implements CursorStore 9 { ··· 15 16 // Ensure directory exists 17 $directory = dirname($this->path); 18 - if (!File::exists($directory)) { 19 File::makeDirectory($directory, 0755, true); 20 } 21 } 22 23 public function get(): ?int 24 { 25 - if (!File::exists($this->path)) { 26 return null; 27 } 28
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Storage; 4 5 use Illuminate\Support\Facades\File; 6 + use SocialDept\AtpSignals\Contracts\CursorStore; 7 8 class FileCursorStore implements CursorStore 9 { ··· 15 16 // Ensure directory exists 17 $directory = dirname($this->path); 18 + if (! File::exists($directory)) { 19 File::makeDirectory($directory, 0755, true); 20 } 21 } 22 23 public function get(): ?int 24 { 25 + if (! File::exists($this->path)) { 26 return null; 27 } 28
+2 -2
src/Storage/RedisCursorStore.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Storage; 4 5 use Illuminate\Support\Facades\Redis; 6 - use SocialDept\Signal\Contracts\CursorStore; 7 8 class RedisCursorStore implements CursorStore 9 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Storage; 4 5 use Illuminate\Support\Facades\Redis; 6 + use SocialDept\AtpSignals\Contracts\CursorStore; 7 8 class RedisCursorStore implements CursorStore 9 {
+8 -2
src/Support/WebSocketConnection.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Support; 4 5 use Ratchet\Client\Connector; 6 use Ratchet\Client\WebSocket; ··· 64 if ($this->onError) { 65 ($this->onError)($e); 66 } 67 throw $e; 68 } 69 ); ··· 74 */ 75 public function send(string $message): bool 76 { 77 - if (!$this->connected || !$this->connection) { 78 return false; 79 } 80 81 try { 82 $this->connection->send($message); 83 return true; 84 } catch (\Exception $e) { 85 if ($this->onError) { 86 ($this->onError)($e); 87 } 88 return false; 89 } 90 } ··· 114 public function onMessage(callable $callback): self 115 { 116 $this->onMessage = $callback(...); 117 return $this; 118 } 119 ··· 123 public function onClose(callable $callback): self 124 { 125 $this->onClose = $callback(...); 126 return $this; 127 } 128 ··· 132 public function onError(callable $callback): self 133 { 134 $this->onError = $callback(...); 135 return $this; 136 } 137
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Support; 4 5 use Ratchet\Client\Connector; 6 use Ratchet\Client\WebSocket; ··· 64 if ($this->onError) { 65 ($this->onError)($e); 66 } 67 + 68 throw $e; 69 } 70 ); ··· 75 */ 76 public function send(string $message): bool 77 { 78 + if (! $this->connected || ! $this->connection) { 79 return false; 80 } 81 82 try { 83 $this->connection->send($message); 84 + 85 return true; 86 } catch (\Exception $e) { 87 if ($this->onError) { 88 ($this->onError)($e); 89 } 90 + 91 return false; 92 } 93 } ··· 117 public function onMessage(callable $callback): self 118 { 119 $this->onMessage = $callback(...); 120 + 121 return $this; 122 } 123 ··· 127 public function onClose(callable $callback): self 128 { 129 $this->onClose = $callback(...); 130 + 131 return $this; 132 } 133 ··· 137 public function onError(callable $callback): self 138 { 139 $this->onError = $callback(...); 140 + 141 return $this; 142 } 143
+2 -2
stubs/signal.stub
··· 2 3 namespace {{ namespace }}; 4 5 - use SocialDept\Signal\Events\SignalEvent; 6 - use SocialDept\Signal\Signals\Signal; 7 8 class {{ class }} extends Signal 9 {
··· 2 3 namespace {{ namespace }}; 4 5 + use SocialDept\AtpSignals\Events\SignalEvent; 6 + use SocialDept\AtpSignals\Signals\Signal; 7 8 class {{ class }} extends Signal 9 {
+225
tests/Integration/FirehoseConsumerTest.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Tests\Integration; 6 + 7 + use Orchestra\Testbench\TestCase; 8 + use SocialDept\AtpSignals\Core\CAR; 9 + use SocialDept\AtpSignals\Core\CBOR; 10 + use SocialDept\AtpSignals\Core\CID; 11 + 12 + class FirehoseConsumerTest extends TestCase 13 + { 14 + public function test_cbor_can_decode_firehose_message_header(): void 15 + { 16 + // Simulate a Firehose message header 17 + // Map with 't' => '#commit', 'op' => 1 18 + $header = [ 19 + 't' => '#commit', 20 + 'op' => 1, 21 + ]; 22 + 23 + // Encode it manually for testing 24 + $cbor = "\xA2"; // Map with 2 items 25 + $cbor .= "\x61t"; // Text string 't' 26 + $cbor .= "\x67#commit"; // Text string '#commit' 27 + $cbor .= "\x62op"; // Text string 'op' 28 + $cbor .= "\x01"; // Integer 1 29 + 30 + [$decoded, $remainder] = CBOR::decodeFirst($cbor); 31 + 32 + $this->assertIsArray($decoded); 33 + $this->assertArrayHasKey('t', $decoded); 34 + $this->assertArrayHasKey('op', $decoded); 35 + $this->assertSame('#commit', $decoded['t']); 36 + $this->assertSame(1, $decoded['op']); 37 + } 38 + 39 + public function test_cbor_can_decode_commit_payload(): void 40 + { 41 + // Simplified commit payload structure 42 + $payload = [ 43 + 'repo' => 'did:plc:test123', 44 + 'rev' => 'test-rev', 45 + 'seq' => 12345, 46 + 'time' => '2024-01-01T00:00:00Z', 47 + 'ops' => [], 48 + ]; 49 + 50 + // Encode a simple payload 51 + $cbor = "\xA5"; // Map with 5 items 52 + 53 + // 'repo' key 54 + $cbor .= "\x64repo"; // Text string 'repo' 55 + $cbor .= "\x6Fdid:plc:test123"; // Text string 'did:plc:test123' 56 + 57 + // 'rev' key 58 + $cbor .= "\x63rev"; // Text string 'rev' 59 + $cbor .= "\x68test-rev"; // Text string 'test-rev' 60 + 61 + // 'seq' key 62 + $cbor .= "\x63seq"; // Text string 'seq' 63 + $cbor .= "\x19\x30\x39"; // Integer 12345 64 + 65 + // 'time' key 66 + $cbor .= "\x64time"; // Text string 'time' 67 + $cbor .= "\x74" . "2024-01-01T00:00:00Z"; // Text string (length 20) 68 + 69 + // 'ops' key 70 + $cbor .= "\x63ops"; // Text string 'ops' 71 + $cbor .= "\x80"; // Empty array 72 + 73 + $decoded = CBOR::decode($cbor); 74 + 75 + $this->assertIsArray($decoded); 76 + $this->assertSame('did:plc:test123', $decoded['repo']); 77 + $this->assertSame('test-rev', $decoded['rev']); 78 + $this->assertSame(12345, $decoded['seq']); 79 + } 80 + 81 + public function test_cid_can_be_decoded_from_cbor_tag(): void 82 + { 83 + // Create a CID and encode it as CBOR tag 42 84 + $hash = hash('sha256', 'test-content', true); 85 + $cidBinary = "\x01\x71\x12\x20" . $hash; // CIDv1, dag-cbor, sha256 86 + $cidBytes = "\x00" . $cidBinary; // Add 0x00 prefix 87 + 88 + // CBOR tag 42 + byte string 89 + $length = strlen($cidBytes); 90 + $cbor = "\xD8\x2A\x58" . chr($length) . $cidBytes; 91 + 92 + $decoded = CBOR::decode($cbor); 93 + 94 + $this->assertInstanceOf(CID::class, $decoded); 95 + $this->assertSame(1, $decoded->version); 96 + $this->assertSame(0x71, $decoded->codec); 97 + } 98 + 99 + public function test_car_can_extract_blocks(): void 100 + { 101 + // Create a minimal CAR with header and one block 102 + $car = ''; 103 + 104 + // CAR header (minimal) - {version: 1} 105 + $headerCbor = "\xA1\x67version\x01"; 106 + $headerLength = strlen($headerCbor); 107 + $car .= chr($headerLength) . $headerCbor; 108 + 109 + // Create a block with CID and data 110 + // Block data: {test: "value"} 111 + $blockData = "\xA1\x64test\x65value"; 112 + 113 + // Create CID: version 1, codec 0x71 (dag-cbor), sha256 hash 114 + $cid = CID::fromBinary("\x01\x71\x12\x20" . str_repeat("\x00", 32)); 115 + $cidBinary = $cid->toBinary(); 116 + 117 + // In CAR format: varint(cid_length + data_length), CID bytes, data bytes 118 + $totalLength = strlen($cidBinary) + strlen($blockData); 119 + $car .= chr($totalLength) . $cidBinary . $blockData; 120 + 121 + // Parse blocks 122 + $blocks = CAR::blockMap($car, 'did:plc:test'); 123 + 124 + $this->assertIsArray($blocks); 125 + $this->assertNotEmpty($blocks); 126 + } 127 + 128 + public function test_firehose_consumer_message_structure(): void 129 + { 130 + // Test the exact structure FirehoseConsumer expects 131 + 132 + // 1. Create CBOR header 133 + $headerMap = [ 134 + 't' => '#commit', 135 + 'op' => 1, 136 + ]; 137 + 138 + $header = "\xA2"; // Map with 2 items 139 + $header .= "\x61t\x67#commit"; // 't' => '#commit' 140 + $header .= "\x62op\x01"; // 'op' => 1 141 + 142 + // 2. Create CBOR payload 143 + $payload = "\xA6"; // Map with 6 items 144 + $payload .= "\x63seq\x19\x30\x39"; // 'seq' => 12345 145 + $payload .= "\x66rebase\xF4"; // 'rebase' => false 146 + $payload .= "\x64repo\x6Fdid:plc:test123"; // 'repo' => 'did:plc:test123' 147 + $payload .= "\x66commit\xA0"; // 'commit' => {} 148 + $payload .= "\x63rev\x68test-rev"; // 'rev' => 'test-rev' 149 + $payload .= "\x65since\x66origin"; // 'since' => 'origin' 150 + 151 + // Add required fields 152 + $payload .= "\x66blocks\x40"; // 'blocks' => empty byte string 153 + $payload .= "\x63ops\x80"; // 'ops' => [] 154 + $payload .= "\x64time\x74" . "2024-01-01T00:00:00Z"; // 'time' => timestamp 155 + 156 + // Combine header + payload 157 + $message = $header . $payload; 158 + 159 + // Test decoding header 160 + [$decodedHeader, $remainder] = CBOR::decodeFirst($message); 161 + 162 + $this->assertIsArray($decodedHeader); 163 + $this->assertSame('#commit', $decodedHeader['t']); 164 + $this->assertSame(1, $decodedHeader['op']); 165 + 166 + // Test decoding payload 167 + $decodedPayload = CBOR::decode($remainder); 168 + 169 + $this->assertIsArray($decodedPayload); 170 + $this->assertArrayHasKey('seq', $decodedPayload); 171 + $this->assertArrayHasKey('repo', $decodedPayload); 172 + $this->assertArrayHasKey('rev', $decodedPayload); 173 + } 174 + 175 + public function test_complete_firehose_message_flow(): void 176 + { 177 + // This test simulates the complete flow that FirehoseConsumer::handleMessage() uses 178 + 179 + // Step 1: CBOR header 180 + $header = "\xA2\x61t\x67#commit\x62op\x01"; 181 + 182 + // Step 2: CBOR payload with all required fields 183 + $payload = "\xA9"; // Map with 9 items 184 + $payload .= "\x63seq\x19\x30\x39"; // seq: 12345 185 + $payload .= "\x66rebase\xF4"; // rebase: false 186 + $payload .= "\x64repo\x6Fdid:plc:test123"; // repo: "did:plc:test123" 187 + $payload .= "\x66commit\xA0"; // commit: {} 188 + $payload .= "\x63rev\x68test-rev"; // rev: "test-rev" 189 + $payload .= "\x65since\x66origin"; // since: "origin" 190 + $payload .= "\x66blocks\x40"; // blocks: b'' 191 + $payload .= "\x63ops\x80"; // ops: [] 192 + $payload .= "\x64time\x74" . "2024-01-01T00:00:00Z"; // time: "2024-01-01T00:00:00Z" 193 + 194 + $message = $header . $payload; 195 + 196 + // Simulate FirehoseConsumer::handleMessage() logic 197 + 198 + // 1. Decode CBOR header 199 + [$decodedHeader, $remainder] = CBOR::decodeFirst($message); 200 + 201 + $this->assertArrayHasKey('t', $decodedHeader); 202 + $this->assertArrayHasKey('op', $decodedHeader); 203 + 204 + // 2. Check operation 205 + $this->assertSame(1, $decodedHeader['op']); 206 + 207 + // 3. Decode payload 208 + $decodedPayload = CBOR::decode($remainder); 209 + 210 + // 4. Verify required fields exist 211 + $requiredFields = ['seq', 'rebase', 'repo', 'commit', 'rev', 'since', 'blocks', 'ops', 'time']; 212 + foreach ($requiredFields as $field) { 213 + $this->assertArrayHasKey($field, $decodedPayload); 214 + } 215 + 216 + // 5. Verify data types 217 + $this->assertIsInt($decodedPayload['seq']); 218 + $this->assertIsBool($decodedPayload['rebase']); 219 + $this->assertIsString($decodedPayload['repo']); 220 + $this->assertIsArray($decodedPayload['ops']); 221 + 222 + // Success! The message structure is valid 223 + $this->assertTrue(true); 224 + } 225 + }
+135
tests/Unit/CBORTest.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Tests\Unit; 6 + 7 + use PHPUnit\Framework\TestCase; 8 + use SocialDept\AtpSignals\Core\CBOR; 9 + use SocialDept\AtpSignals\Core\CID; 10 + 11 + class CBORTest extends TestCase 12 + { 13 + public function test_decode_unsigned_integers(): void 14 + { 15 + // Small value (0-23) 16 + $this->assertSame(0, CBOR::decode("\x00")); 17 + $this->assertSame(1, CBOR::decode("\x01")); 18 + $this->assertSame(23, CBOR::decode("\x17")); 19 + 20 + // 1-byte value 21 + $this->assertSame(24, CBOR::decode("\x18\x18")); 22 + $this->assertSame(255, CBOR::decode("\x18\xFF")); 23 + 24 + // 2-byte value 25 + $this->assertSame(256, CBOR::decode("\x19\x01\x00")); 26 + $this->assertSame(1000, CBOR::decode("\x19\x03\xE8")); 27 + } 28 + 29 + public function test_decode_negative_integers(): void 30 + { 31 + // -1 is encoded as 0x20 (major type 1, value 0) 32 + $this->assertSame(-1, CBOR::decode("\x20")); 33 + 34 + // -10 is encoded as 0x29 (major type 1, value 9) 35 + $this->assertSame(-10, CBOR::decode("\x29")); 36 + 37 + // -100 is encoded as 0x38 0x63 (major type 1, 1-byte value 99) 38 + $this->assertSame(-100, CBOR::decode("\x38\x63")); 39 + } 40 + 41 + public function test_decode_byte_strings(): void 42 + { 43 + // Empty byte string 44 + $this->assertSame('', CBOR::decode("\x40")); 45 + 46 + // 4-byte string 47 + $this->assertSame("\x01\x02\x03\x04", CBOR::decode("\x44\x01\x02\x03\x04")); 48 + } 49 + 50 + public function test_decode_text_strings(): void 51 + { 52 + // Empty text string 53 + $this->assertSame('', CBOR::decode("\x60")); 54 + 55 + // "hello" 56 + $this->assertSame('hello', CBOR::decode("\x65hello")); 57 + 58 + // "IETF" 59 + $this->assertSame('IETF', CBOR::decode("\x64IETF")); 60 + } 61 + 62 + public function test_decode_arrays(): void 63 + { 64 + // Empty array 65 + $this->assertSame([], CBOR::decode("\x80")); 66 + 67 + // [1, 2, 3] 68 + $this->assertSame([1, 2, 3], CBOR::decode("\x83\x01\x02\x03")); 69 + 70 + // Mixed array [1, "two", 3] 71 + $result = CBOR::decode("\x83\x01\x63two\x03"); 72 + $this->assertSame([1, 'two', 3], $result); 73 + } 74 + 75 + public function test_decode_maps(): void 76 + { 77 + // Empty map 78 + $this->assertSame([], CBOR::decode("\xA0")); 79 + 80 + // {"a": 1, "b": 2} 81 + $result = CBOR::decode("\xA2\x61a\x01\x61b\x02"); 82 + $this->assertSame(['a' => 1, 'b' => 2], $result); 83 + } 84 + 85 + public function test_decode_special_values(): void 86 + { 87 + // false 88 + $this->assertFalse(CBOR::decode("\xF4")); 89 + 90 + // true 91 + $this->assertTrue(CBOR::decode("\xF5")); 92 + 93 + // null 94 + $this->assertNull(CBOR::decode("\xF6")); 95 + } 96 + 97 + public function test_decode_first_returns_value_and_remainder(): void 98 + { 99 + [$value, $remainder] = CBOR::decodeFirst("\x01\x02\x03"); 100 + 101 + $this->assertSame(1, $value); 102 + $this->assertSame("\x02\x03", $remainder); 103 + } 104 + 105 + public function test_decode_nested_structures(): void 106 + { 107 + // {"key": [1, 2, {"inner": true}]} 108 + $cbor = "\xA1\x63key\x83\x01\x02\xA1\x65inner\xF5"; 109 + $result = CBOR::decode($cbor); 110 + 111 + $expected = [ 112 + 'key' => [1, 2, ['inner' => true]], 113 + ]; 114 + 115 + $this->assertSame($expected, $result); 116 + } 117 + 118 + public function test_decode_cid_tag(): void 119 + { 120 + // Tag 42 (CID) followed by byte string with CID data 121 + // CID bytes: 0x00 prefix + version + codec + multihash 122 + $cidBinary = "\x01\x71\x12\x20" . str_repeat("\x00", 32); // version 1, codec 0x71, sha256, 32 zero bytes 123 + $cidBytes = "\x00" . $cidBinary; // Add 0x00 prefix for CBOR tag 42 124 + 125 + // CBOR: tag 42 (0xD8 0x2A) + byte string with 1-byte length (0x58 = major type 2, additional info 24) 126 + $length = strlen($cidBytes); 127 + $cbor = "\xD8\x2A\x58" . chr($length) . $cidBytes; 128 + 129 + $result = CBOR::decode($cbor); 130 + 131 + $this->assertInstanceOf(CID::class, $result); 132 + $this->assertSame(1, $result->version); 133 + $this->assertSame(0x71, $result->codec); 134 + } 135 + }
+100
tests/Unit/CIDTest.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Tests\Unit; 6 + 7 + use PHPUnit\Framework\TestCase; 8 + use SocialDept\AtpSignals\Core\CID; 9 + 10 + class CIDTest extends TestCase 11 + { 12 + public function test_parse_binary_cidv1(): void 13 + { 14 + // CIDv1: version=1, codec=0x71 (dag-cbor), sha256 hash 15 + $hash = str_repeat("\x00", 32); 16 + $binary = "\x01\x71\x12\x20" . $hash; 17 + 18 + $cid = CID::fromBinary($binary); 19 + 20 + $this->assertSame(1, $cid->version); 21 + $this->assertSame(0x71, $cid->codec); 22 + $this->assertSame("\x12\x20" . $hash, $cid->hash); 23 + } 24 + 25 + public function test_parse_binary_cidv0(): void 26 + { 27 + // CIDv0: starts with 0x12 (sha256) 0x20 (32 bytes) 28 + $hash = str_repeat("\x00", 32); 29 + $binary = "\x12\x20" . $hash; 30 + 31 + $cid = CID::fromBinary($binary); 32 + 33 + $this->assertSame(0, $cid->version); 34 + $this->assertSame(0x70, $cid->codec); // dag-pb 35 + $this->assertSame("\x12\x20" . $hash, $cid->hash); 36 + } 37 + 38 + public function test_to_string_cidv1(): void 39 + { 40 + $hash = str_repeat("\x00", 32); 41 + $binary = "\x01\x71\x12\x20" . $hash; 42 + $cid = CID::fromBinary($binary); 43 + 44 + $str = $cid->toString(); 45 + 46 + // Should start with 'b' (base32 prefix) 47 + $this->assertStringStartsWith('b', $str); 48 + 49 + // Should be able to parse it back 50 + $parsed = CID::fromString($str); 51 + $this->assertSame($cid->version, $parsed->version); 52 + $this->assertSame($cid->codec, $parsed->codec); 53 + } 54 + 55 + public function test_to_binary_cidv1(): void 56 + { 57 + $hash = str_repeat("\x00", 32); 58 + $binary = "\x01\x71\x12\x20" . $hash; 59 + $cid = CID::fromBinary($binary); 60 + 61 + $this->assertSame($binary, $cid->toBinary()); 62 + } 63 + 64 + public function test_round_trip_binary(): void 65 + { 66 + $hash = hash('sha256', 'test', true); 67 + $binary = "\x01\x71\x12\x20" . $hash; 68 + 69 + $cid = CID::fromBinary($binary); 70 + $encoded = $cid->toBinary(); 71 + $decoded = CID::fromBinary($encoded); 72 + 73 + $this->assertSame($cid->version, $decoded->version); 74 + $this->assertSame($cid->codec, $decoded->codec); 75 + $this->assertSame($cid->hash, $decoded->hash); 76 + } 77 + 78 + public function test_round_trip_string(): void 79 + { 80 + $hash = hash('sha256', 'test', true); 81 + $binary = "\x01\x71\x12\x20" . $hash; 82 + $cid = CID::fromBinary($binary); 83 + 84 + $str = $cid->toString(); 85 + $parsed = CID::fromString($str); 86 + 87 + $this->assertSame($cid->version, $parsed->version); 88 + $this->assertSame($cid->codec, $parsed->codec); 89 + $this->assertSame($cid->hash, $parsed->hash); 90 + } 91 + 92 + public function test_to_string_magic_method(): void 93 + { 94 + $hash = str_repeat("\x00", 32); 95 + $binary = "\x01\x71\x12\x20" . $hash; 96 + $cid = CID::fromBinary($binary); 97 + 98 + $this->assertSame($cid->toString(), (string) $cid); 99 + } 100 + }
+4 -5
tests/Unit/SignalRegistryTest.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Tests\Unit; 4 5 use Orchestra\Testbench\TestCase; 6 - use SocialDept\Signal\Events\CommitEvent; 7 - use SocialDept\Signal\Events\SignalEvent; 8 - use SocialDept\Signal\Services\SignalRegistry; 9 - use SocialDept\Signal\Signals\Signal; 10 11 class SignalRegistryTest extends TestCase 12 {
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Tests\Unit; 4 5 use Orchestra\Testbench\TestCase; 6 + use SocialDept\AtpSignals\Events\CommitEvent; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Services\SignalRegistry; 9 10 class SignalRegistryTest extends TestCase 11 {
+17 -10
tests/Unit/SignalTest.php
··· 1 <?php 2 3 - namespace SocialDept\Signal\Tests\Unit; 4 5 use Orchestra\Testbench\TestCase; 6 - use SocialDept\Signal\Events\CommitEvent; 7 - use SocialDept\Signal\Events\SignalEvent; 8 - use SocialDept\Signal\Signals\Signal; 9 10 class SignalTest extends TestCase 11 { 12 /** @test */ 13 public function it_can_create_a_signal() 14 { 15 - $signal = new class extends Signal { 16 public function eventTypes(): array 17 { 18 return ['commit']; ··· 31 /** @test */ 32 public function it_can_filter_by_exact_collection() 33 { 34 - $signal = new class extends Signal { 35 public function eventTypes(): array 36 { 37 return ['commit']; ··· 66 /** @test */ 67 public function it_can_filter_by_wildcard_collection() 68 { 69 - $signal = new class extends Signal { 70 public function eventTypes(): array 71 { 72 return ['commit']; ··· 83 } 84 }; 85 86 // Test that it matches app.bsky.feed.post 87 $postEvent = new SignalEvent( 88 did: 'did:plc:test', ··· 96 ), 97 ); 98 99 - $this->assertTrue($signal->shouldHandle($postEvent)); 100 101 // Test that it matches app.bsky.feed.like 102 $likeEvent = new SignalEvent( ··· 111 ), 112 ); 113 114 - $this->assertTrue($signal->shouldHandle($likeEvent)); 115 116 // Test that it does NOT match app.bsky.graph.follow 117 $followEvent = new SignalEvent( ··· 126 ), 127 ); 128 129 - $this->assertFalse($signal->shouldHandle($followEvent)); 130 } 131 }
··· 1 <?php 2 3 + namespace SocialDept\AtpSignals\Tests\Unit; 4 5 use Orchestra\Testbench\TestCase; 6 + use SocialDept\AtpSignals\Events\CommitEvent; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Signals\Signal; 9 10 class SignalTest extends TestCase 11 { 12 /** @test */ 13 public function it_can_create_a_signal() 14 { 15 + $signal = new class () extends Signal { 16 public function eventTypes(): array 17 { 18 return ['commit']; ··· 31 /** @test */ 32 public function it_can_filter_by_exact_collection() 33 { 34 + $signal = new class () extends Signal { 35 public function eventTypes(): array 36 { 37 return ['commit']; ··· 66 /** @test */ 67 public function it_can_filter_by_wildcard_collection() 68 { 69 + $signalClass = new class () extends Signal { 70 public function eventTypes(): array 71 { 72 return ['commit']; ··· 83 } 84 }; 85 86 + // Create registry and register the signal 87 + $registry = new \SocialDept\AtpSignals\Services\SignalRegistry(); 88 + $registry->register($signalClass::class); 89 + 90 // Test that it matches app.bsky.feed.post 91 $postEvent = new SignalEvent( 92 did: 'did:plc:test', ··· 100 ), 101 ); 102 103 + $matchingSignals = $registry->getMatchingSignals($postEvent); 104 + $this->assertCount(1, $matchingSignals); 105 106 // Test that it matches app.bsky.feed.like 107 $likeEvent = new SignalEvent( ··· 116 ), 117 ); 118 119 + $matchingSignals = $registry->getMatchingSignals($likeEvent); 120 + $this->assertCount(1, $matchingSignals); 121 122 // Test that it does NOT match app.bsky.graph.follow 123 $followEvent = new SignalEvent( ··· 132 ), 133 ); 134 135 + $matchingSignals = $registry->getMatchingSignals($followEvent); 136 + $this->assertCount(0, $matchingSignals); 137 } 138 }
+75
tests/Unit/VarintTest.php
···
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\AtpSignals\Tests\Unit; 6 + 7 + use PHPUnit\Framework\TestCase; 8 + use RuntimeException; 9 + use SocialDept\AtpSignals\Binary\Varint; 10 + 11 + class VarintTest extends TestCase 12 + { 13 + public function test_decode_single_byte_values(): void 14 + { 15 + $this->assertSame(0, Varint::decode("\x00")); 16 + $this->assertSame(1, Varint::decode("\x01")); 17 + $this->assertSame(127, Varint::decode("\x7F")); 18 + } 19 + 20 + public function test_decode_multi_byte_values(): void 21 + { 22 + // 128 = 0x80 0x01 23 + $this->assertSame(128, Varint::decode("\x80\x01")); 24 + 25 + // 300 = 0xAC 0x02 26 + $this->assertSame(300, Varint::decode("\xAC\x02")); 27 + 28 + // 16384 = 0x80 0x80 0x01 29 + $this->assertSame(16384, Varint::decode("\x80\x80\x01")); 30 + } 31 + 32 + public function test_decode_with_offset(): void 33 + { 34 + $data = "\x00\x01\x7F\x80\x01"; 35 + $offset = 0; 36 + 37 + $this->assertSame(0, Varint::decode($data, $offset)); 38 + $this->assertSame(1, $offset); 39 + 40 + $this->assertSame(1, Varint::decode($data, $offset)); 41 + $this->assertSame(2, $offset); 42 + 43 + $this->assertSame(127, Varint::decode($data, $offset)); 44 + $this->assertSame(3, $offset); 45 + 46 + $this->assertSame(128, Varint::decode($data, $offset)); 47 + $this->assertSame(5, $offset); 48 + } 49 + 50 + public function test_decode_first_returns_value_and_remainder(): void 51 + { 52 + [$value, $remainder] = Varint::decodeFirst("\x7F\x01\x02\x03"); 53 + 54 + $this->assertSame(127, $value); 55 + $this->assertSame("\x01\x02\x03", $remainder); 56 + } 57 + 58 + public function test_decode_throws_on_unexpected_end(): void 59 + { 60 + $this->expectException(RuntimeException::class); 61 + $this->expectExceptionMessage('Unexpected end of varint data'); 62 + 63 + Varint::decode("\x80"); 64 + } 65 + 66 + public function test_decode_throws_on_too_long_varint(): void 67 + { 68 + $this->expectException(RuntimeException::class); 69 + $this->expectExceptionMessage('Varint too long (max 64 bits)'); 70 + 71 + // Create a varint that would be longer than 64 bits (10 bytes with continuation bits) 72 + $tooLong = str_repeat("\xFF", 10); 73 + Varint::decode($tooLong); 74 + } 75 + }