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 -739
README.md
··· 1 - # Signal 1 + [![Signal Header](./header.png)](https://github.com/socialdept/atp-signals) 2 2 3 - **Laravel package for building Signals that respond to AT Protocol events** 3 + <h3 align="center"> 4 + Consume real-time AT Protocol events in your Laravel application. 5 + </h3> 4 6 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. 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> 6 14 7 15 --- 8 16 9 - ## Features 17 + ## What is Signal? 10 18 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 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. 21 20 22 - --- 21 + Think of it as Laravel's event listeners, but for the decentralized social web. 23 22 24 - ## Table of Contents 23 + ## Why use Signal? 25 24 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 - * [Programmatic Usage](#programmatic-usage) 35 - * [Available Commands](#available-commands) 36 - * [Testing](#testing) 37 - * [External Resources](#external-resources) 38 - * [Examples](#examples) 39 - * [Requirements](#requirements) 40 - * [License](#license) 41 - * [Support](#support) 42 - <!-- TOC --> 43 - 44 - --- 45 - 46 - ## Installation 47 - 48 - Install the package via Composer: 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 49 32 50 - ```bash 51 - composer require socialdept/signal 52 - ``` 53 - 54 - Run the installation command: 55 - 56 - ```bash 57 - php artisan signal:install 58 - ``` 59 - 60 - This will: 61 - - Publish the configuration file to `config/signal.php` 62 - - Publish the database migration 63 - - Run migrations (with confirmation) 64 - - Display next steps 65 - 66 - ### Manual Installation 67 - 68 - If you prefer manual installation: 69 - 70 - ```bash 71 - php artisan vendor:publish --tag=signal-config 72 - php artisan vendor:publish --tag=signal-migrations 73 - php artisan migrate 74 - ``` 75 - 76 - --- 77 - 78 - ## Quick Start 79 - 80 - ### 1. Create Your First Signal 81 - 82 - ```bash 83 - php artisan make:signal NewPostSignal 84 - ``` 85 - 86 - This creates `app/Signals/NewPostSignal.php`: 33 + ## Quick Example 87 34 88 35 ```php 89 - <?php 90 - 91 - namespace App\Signals; 92 - 93 - use SocialDept\Signal\Events\SignalEvent; 94 - use SocialDept\Signal\Signals\Signal; 36 + use SocialDept\AtpSignals\Events\SignalEvent; 37 + use SocialDept\AtpSignals\Signals\Signal; 95 38 96 39 class NewPostSignal extends Signal 97 40 { ··· 117 60 } 118 61 ``` 119 62 120 - ### 2. Start Consuming Events 121 - 122 - ```bash 123 - php artisan signal:consume 124 - ``` 125 - 126 - Your Signal will now respond to new posts on the AT Protocol network in real-time! 127 - 128 - --- 63 + Run `php artisan signal:consume` and start responding to every post on Bluesky in real-time. 129 64 130 - ## Jetstream vs Firehose 131 - 132 - Signal supports two modes for consuming AT Protocol events. Choose based on your use case: 133 - 134 - ### Jetstream Mode (Default) 135 - 136 - **Best for**: Standard Bluesky collections, production efficiency, lower bandwidth 65 + ## Installation 137 66 138 67 ```bash 139 - php artisan signal:consume --mode=jetstream 68 + composer require socialdept/atp-signals 69 + php artisan signal:install 140 70 ``` 141 71 142 - **Characteristics:** 143 - - โœ… Simplified JSON events (easy to work with) 144 - - โœ… Server-side collection filtering (efficient) 145 - - โœ… Lower bandwidth and processing overhead 146 - - โš ๏ธ Only standard `app.bsky.*` collections get create/update operations 147 - - โš ๏ธ Custom collections only receive delete operations 72 + That's it. [Read the installation docs โ†’](docs/installation.md) 148 73 149 - **Jetstream URL options:** 150 - - US East: `wss://jetstream2.us-east.bsky.network` (default) 151 - - US West: `wss://jetstream1.us-west.bsky.network` 74 + ## Getting Started 152 75 153 - ### Firehose Mode 76 + Once installed, you're three steps away from consuming AT Protocol events: 154 77 155 - **Best for**: Custom collections, AppViews, comprehensive indexing 78 + ### 1. Create a Signal 156 79 157 80 ```bash 158 - php artisan signal:consume --mode=firehose 81 + php artisan make:signal NewPostSignal 159 82 ``` 160 83 161 - **Characteristics:** 162 - - โœ… **All operations** (create, update, delete) for **all collections** 163 - - โœ… Perfect for custom collections (e.g., `app.yourapp.*.collection`) 164 - - โœ… Full CBOR/CAR decoding with package `revolution/laravel-bluesky` 165 - - โš ๏ธ Client-side filtering only (higher bandwidth) 166 - - โš ๏ธ More processing overhead 167 - 168 - **When to use Firehose:** 169 - - Building an AT Protocol AppView 170 - - Working with custom collections 171 - - Need create/update events for non-standard collections 172 - - Building comprehensive indexes 173 - 174 - ### Configuration 175 - 176 - Set your preferred mode in `.env`: 177 - 178 - ```env 179 - # Use Jetstream (default) 180 - SIGNAL_MODE=jetstream 181 - 182 - # Or use Firehose for custom collections 183 - SIGNAL_MODE=firehose 184 - ``` 185 - 186 - ### Example: Custom Collections 187 - 188 - If you're tracking custom collections like `app.offprint.beta.publication`, you **must** use Firehose mode: 84 + ### 2. Define What to Listen For 189 85 190 86 ```php 191 - class PublicationSignal extends Signal 87 + public function collections(): ?array 192 88 { 193 - public function collections(): ?array 194 - { 195 - return ['app.offprint.beta.publication']; 196 - } 197 - 198 - public function handle(SignalEvent $event): void 199 - { 200 - // With Jetstream: Only sees deletes โŒ 201 - // With Firehose: Sees creates, updates, deletes โœ… 202 - } 89 + return ['app.bsky.feed.post']; 203 90 } 204 91 ``` 205 92 206 - --- 93 + ### 3. Start Consuming 207 94 208 - ## Creating Signals 95 + ```bash 96 + php artisan signal:consume 97 + ``` 209 98 210 - ### Basic Signal Structure 99 + Your Signal will now handle every matching event from the network. [Read the quickstart guide โ†’](docs/quickstart.md) 211 100 212 - Every Signal extends the base `Signal` class and must implement: 101 + ## What can you build? 213 102 214 - ```php 215 - use SocialDept\Signal\Enums\SignalEventType; 216 - use SocialDept\Signal\Events\SignalEvent; 217 - use SocialDept\Signal\Signals\Signal; 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 218 109 219 - class MySignal extends Signal 220 - { 221 - // Required: Define which event types to listen for 222 - public function eventTypes(): array 223 - { 224 - return [SignalEventType::Commit]; 110 + ## Documentation 225 111 226 - // Or use strings: 227 - // return ['commit']; 228 - } 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 229 116 230 - // Required: Handle the event 231 - public function handle(SignalEvent $event): void 232 - { 233 - // Your logic here 234 - } 235 - } 236 - ``` 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 237 121 238 - **Enums vs Strings**: Signal supports both typed enums and strings for better IDE support and type safety. Use whichever you prefer! 239 - 240 - ### Event Types 241 - 242 - Three event types are available: 243 - 244 - | Enum | String | Description | Use Cases | 245 - |-----------------------------|--------------|--------------------------------------------------|---------------------------------------| 246 - | `SignalEventType::Commit` | `'commit'` | Repository commits (posts, likes, follows, etc.) | Content creation, social interactions | 247 - | `SignalEventType::Identity` | `'identity'` | Identity changes (handle updates) | User profile tracking | 248 - | `SignalEventType::Account` | `'account'` | Account status changes | Account monitoring | 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 249 126 250 - ### Accessing Event Data 127 + ## Example Use Cases 251 128 129 + ### Track User Growth 252 130 ```php 253 - use SocialDept\Signal\Enums\SignalCommitOperation; 254 - 255 - public function handle(SignalEvent $event): void 131 + public function collections(): ?array 256 132 { 257 - // Common properties 258 - $did = $event->did; // User's DID 259 - $kind = $event->kind; // Event type 260 - $timestamp = $event->timeUs; // Microsecond timestamp 261 - 262 - // Commit events 263 - if ($event->isCommit()) { 264 - $collection = $event->getCollection(); // e.g., 'app.bsky.feed.post' 265 - $operation = $event->getOperation(); // SignalCommitOperation enum 266 - $record = $event->getRecord(); // The actual record data 267 - $rkey = $event->commit->rkey; // Record key 268 - 269 - // Use enum for type-safe comparisons 270 - if ($operation === SignalCommitOperation::Create) { 271 - // Handle new records 272 - } 273 - 274 - // Or get string value 275 - $operationString = $operation->value; // 'create', 'update', or 'delete' 276 - } 277 - 278 - // Identity events 279 - if ($event->isIdentity()) { 280 - $handle = $event->identity->handle; 281 - } 282 - 283 - // Account events 284 - if ($event->isAccount()) { 285 - $active = $event->account->active; 286 - $status = $event->account->status; 287 - } 133 + return ['app.bsky.graph.follow']; 288 134 } 289 135 ``` 290 136 291 - --- 292 - 293 - ## Filtering Events 294 - 295 - ### Collection Filtering (with Wildcards!) 296 - 297 - Filter events by AT Protocol collection. 298 - 299 - **Important**: 300 - - **Jetstream mode**: Exact collection names are sent as URL parameters for server-side filtering. Wildcards work for client-side filtering only. 301 - - **Firehose mode**: All filtering is client-side. Wildcards work normally. 302 - 137 + ### Monitor Content Moderation 303 138 ```php 304 - // Exact match - only posts 305 - public function collections(): ?array 306 - { 307 - return ['app.bsky.feed.post']; 308 - } 309 - 310 - // Wildcard - all feed events 311 139 public function collections(): ?array 312 140 { 313 141 return ['app.bsky.feed.*']; 314 142 } 315 143 316 - // Multiple patterns 317 - public function collections(): ?array 144 + public function shouldQueue(): bool 318 145 { 319 - return [ 320 - 'app.bsky.feed.post', 321 - 'app.bsky.feed.repost', 322 - 'app.bsky.graph.*', // All graph collections 323 - ]; 324 - } 325 - 326 - // No filter - all collections 327 - public function collections(): ?array 328 - { 329 - return null; 146 + return true; // Process in background 330 147 } 331 148 ``` 332 149 333 - ### Common Collection Patterns 334 - 335 - | Pattern | Matches | 336 - |--------------------|-----------------------------| 337 - | `app.bsky.feed.*` | Posts, likes, reposts, etc. | 338 - | `app.bsky.graph.*` | Follows, blocks, mutes | 339 - | `app.bsky.actor.*` | Profile updates | 340 - | `app.bsky.*` | All Bluesky collections | 341 - 342 - ### Operation Filtering 343 - 344 - Filter events by operation type (only applies to `commit` events): 345 - 150 + ### Build Custom Collections (AppView) 346 151 ```php 347 - use SocialDept\Signal\Enums\SignalCommitOperation; 348 - 349 - // Only handle creates (using enum) 350 - public function operations(): ?array 152 + public function collections(): ?array 351 153 { 352 - return [SignalCommitOperation::Create]; 353 - } 354 - 355 - // Only handle creates and updates (using enums) 356 - public function operations(): ?array 357 - { 358 - return [ 359 - SignalCommitOperation::Create, 360 - SignalCommitOperation::Update, 361 - ]; 362 - } 363 - 364 - // Only handle deletes (using string) 365 - public function operations(): ?array 366 - { 367 - return ['delete']; 368 - } 369 - 370 - // No filter - all operations (default) 371 - public function operations(): ?array 372 - { 373 - return null; 154 + return ['app.yourapp.custom.collection']; 374 155 } 375 156 ``` 376 157 377 - **Available operations:** 378 - 379 - | Enum | String | Description | 380 - |---------------------------------|------------|---------------------------| 381 - | `SignalCommitOperation::Create` | `'create'` | New records created | 382 - | `SignalCommitOperation::Update` | `'update'` | Existing records modified | 383 - | `SignalCommitOperation::Delete` | `'delete'` | Records removed | 384 - 385 - **Example use cases:** 386 - ```php 387 - use SocialDept\Signal\Enums\SignalCommitOperation; 158 + [See more examples โ†’](docs/examples.md) 388 159 389 - // Signal that only handles new posts (not edits) 390 - class NewPostSignal extends Signal 391 - { 392 - public function collections(): ?array 393 - { 394 - return ['app.bsky.feed.post']; 395 - } 160 + ## Key Features Explained 396 161 397 - public function operations(): ?array 398 - { 399 - return [SignalCommitOperation::Create]; 400 - } 401 - } 162 + ### Jetstream vs Firehose 402 163 403 - // Signal that only handles content updates 404 - class ContentUpdateSignal extends Signal 405 - { 406 - public function collections(): ?array 407 - { 408 - return ['app.bsky.feed.post']; 409 - } 164 + Signal supports two modes for consuming AT Protocol events: 410 165 411 - public function operations(): ?array 412 - { 413 - return [SignalCommitOperation::Update]; 414 - } 415 - } 166 + - **Jetstream** (default) - Simplified JSON events with server-side filtering 167 + - **Firehose** - Raw CBOR/CAR format with client-side filtering 416 168 417 - // Signal that handles deletions for cleanup 418 - class CleanupSignal extends Signal 419 - { 420 - public function collections(): ?array 421 - { 422 - return ['app.bsky.feed.*']; 423 - } 424 - 425 - public function operations(): ?array 426 - { 427 - return [SignalCommitOperation::Delete]; 428 - } 429 - } 430 - ``` 169 + [Learn more about modes โ†’](docs/modes.md) 431 170 432 - ### DID Filtering 171 + ### Wildcard Filtering 433 172 434 - Filter events by specific users: 173 + Match multiple collections with patterns: 435 174 436 175 ```php 437 - public function dids(): ?array 176 + public function collections(): ?array 438 177 { 439 178 return [ 440 - 'did:plc:z72i7hdynmk6r22z27h6tvur', // Specific user 441 - 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // Another user 179 + 'app.bsky.feed.*', // All feed events 180 + 'app.bsky.graph.*', // All graph events 181 + 'app.yourapp.*', // All your custom collections 442 182 ]; 443 183 } 444 184 ``` 445 185 446 - ### Custom Filtering 186 + [Learn more about filtering โ†’](docs/filtering.md) 447 187 448 - Add complex filtering logic: 188 + ### Queue Integration 189 + 190 + Process events asynchronously for better performance: 449 191 450 192 ```php 451 - public function shouldHandle(SignalEvent $event): bool 193 + public function shouldQueue(): bool 452 194 { 453 - // Only handle posts with images 454 - if ($event->isCommit() && $event->commit->collection === 'app.bsky.feed.post') { 455 - $record = $event->getRecord(); 456 - return isset($record->embed); 457 - } 458 - 459 195 return true; 460 196 } 461 197 ``` 462 198 463 - --- 464 - 465 - ## Queue Integration 466 - 467 - Process events asynchronously using Laravel queues: 468 - 469 - ```php 470 - class HeavyProcessingSignal extends Signal 471 - { 472 - public function eventTypes(): array 473 - { 474 - return ['commit']; 475 - } 476 - 477 - // Enable queueing 478 - public function shouldQueue(): bool 479 - { 480 - return true; 481 - } 482 - 483 - // Optional: Customize queue 484 - public function queue(): string 485 - { 486 - return 'high-priority'; 487 - } 488 - 489 - // Optional: Customize connection 490 - public function queueConnection(): string 491 - { 492 - return 'redis'; 493 - } 494 - 495 - public function handle(SignalEvent $event): void 496 - { 497 - // This runs in a queue job 498 - $this->performExpensiveOperation($event); 499 - } 500 - 501 - // Handle failures 502 - public function failed(SignalEvent $event, \Throwable $exception): void 503 - { 504 - Log::error('Signal failed', [ 505 - 'event' => $event->toArray(), 506 - 'error' => $exception->getMessage(), 507 - ]); 508 - } 509 - } 510 - ``` 511 - 512 - --- 513 - 514 - ## Configuration 515 - 516 - Configuration is stored in `config/signal.php`: 517 - 518 - ### Consumer Mode 519 - 520 - Choose between Jetstream (JSON) or Firehose (CBOR) mode: 521 - 522 - ```php 523 - 'mode' => env('SIGNAL_MODE', 'jetstream'), 524 - ``` 525 - 526 - Options: 527 - - `jetstream` - JSON events, server-side filtering (default) 528 - - `firehose` - CBOR events, client-side filtering (required for custom collections) 529 - 530 - ### Jetstream Configuration 531 - 532 - ```php 533 - 'websocket_url' => env('SIGNAL_JETSTREAM_URL', 'wss://jetstream2.us-east.bsky.network'), 534 - ``` 535 - 536 - Available endpoints: 537 - - **US East**: `wss://jetstream2.us-east.bsky.network` (default) 538 - - **US West**: `wss://jetstream1.us-west.bsky.network` 539 - 540 - ### Firehose Configuration 541 - 542 - ```php 543 - 'firehose' => [ 544 - 'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'), 545 - ], 546 - ``` 547 - 548 - The raw firehose endpoint is: `wss://{host}/xrpc/com.atproto.sync.subscribeRepos` 549 - 550 - ### Cursor Storage 551 - 552 - Choose how to store cursor positions: 553 - 554 - ```php 555 - 'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'), 556 - ``` 557 - 558 - | Driver | Best For | Configuration | 559 - |------------|-------------------------------|--------------------| 560 - | `database` | Production, multi-server | Default connection | 561 - | `redis` | High performance, distributed | Redis connection | 562 - | `file` | Development, single server | Storage path | 563 - 564 - ### Environment Variables 565 - 566 - Add to your `.env`: 567 - 568 - ```env 569 - # Consumer Mode 570 - SIGNAL_MODE=jetstream # or 'firehose' for custom collections 571 - 572 - # Jetstream Configuration 573 - SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network 574 - 575 - # Firehose Configuration (only needed if using firehose mode) 576 - SIGNAL_FIREHOSE_HOST=bsky.network 577 - 578 - # Optional Configuration 579 - SIGNAL_CURSOR_STORAGE=database 580 - SIGNAL_QUEUE_CONNECTION=redis 581 - SIGNAL_QUEUE=signal 582 - SIGNAL_BATCH_SIZE=100 583 - SIGNAL_RATE_LIMIT=1000 584 - ``` 585 - 586 - ### Auto-Discovery 587 - 588 - Signals are automatically discovered from `app/Signals`. Disable if needed: 589 - 590 - ```php 591 - 'auto_discovery' => [ 592 - 'enabled' => true, 593 - 'path' => app_path('Signals'), 594 - 'namespace' => 'App\\Signals', 595 - ], 596 - ``` 597 - 598 - Or manually register Signals: 599 - 600 - ```php 601 - 'signals' => [ 602 - \App\Signals\NewPostSignal::class, 603 - \App\Signals\NewFollowSignal::class, 604 - ], 605 - ``` 606 - 607 - --- 608 - 609 - ## Programmatic Usage 610 - 611 - You can start and stop the consumer programmatically using the `Signal` facade: 612 - 613 - ```php 614 - use SocialDept\Signal\Facades\Signal; 615 - 616 - // Start consuming events (uses mode from config) 617 - Signal::start(); 618 - 619 - // Start from a specific cursor 620 - Signal::start(cursor: 123456789); 621 - 622 - // Check which mode is active 623 - $mode = Signal::getMode(); // Returns 'jetstream' or 'firehose' 624 - 625 - // Stop consuming events 626 - Signal::stop(); 627 - ``` 628 - 629 - The facade automatically resolves the correct consumer (Jetstream or Firehose) based on your `config('signal.mode')` setting. This allows you to: 630 - 631 - - Switch between modes by changing configuration 632 - - Start consumers from application code (e.g., in a custom command) 633 - - Integrate Signal into existing application workflows 634 - 635 - ```php 636 - // Example: Start consumer based on environment 637 - if (app()->environment('production')) { 638 - config(['signal.mode' => 'jetstream']); // Use efficient Jetstream 639 - } else { 640 - config(['signal.mode' => 'firehose']); // Use comprehensive Firehose for testing 641 - } 642 - 643 - Signal::start(); 644 - ``` 645 - 646 - --- 199 + [Learn more about queues โ†’](docs/queues.md) 647 200 648 201 ## Available Commands 649 202 650 - ### `signal:install` 651 - Install the package (publish config, migrations, run migrations) 652 - 653 203 ```bash 204 + # Install Signal 654 205 php artisan signal:install 655 - ``` 656 206 657 - ### `signal:consume` 658 - Start consuming events from AT Protocol 659 - 660 - ```bash 661 - # Use default mode from config 662 - php artisan signal:consume 663 - 664 - # Override mode 665 - php artisan signal:consume --mode=jetstream 666 - php artisan signal:consume --mode=firehose 667 - 668 - # Start from specific cursor 669 - php artisan signal:consume --cursor=123456789 207 + # Create a new Signal 208 + php artisan make:signal YourSignal 670 209 671 - # Start fresh (ignore stored cursor) 672 - php artisan signal:consume --fresh 673 - 674 - # Combine options 675 - php artisan signal:consume --mode=firehose --fresh 676 - ``` 677 - 678 - ### `signal:list` 679 - List all registered Signals 680 - 681 - ```bash 210 + # List all registered Signals 682 211 php artisan signal:list 683 - ``` 684 212 685 - ### `signal:make` 686 - Create a new Signal class 687 - 688 - ```bash 689 - php artisan make:signal NewPostSignal 213 + # Start consuming events 214 + php artisan signal:consume 690 215 691 - # With options 692 - php artisan make:signal FollowSignal --type=commit --collection=app.bsky.graph.follow 216 + # Test a Signal with sample data 217 + php artisan signal:test YourSignal 693 218 ``` 694 219 695 - ### `signal:test` 696 - Test a Signal with sample data 220 + ## Requirements 697 221 698 - ```bash 699 - php artisan signal:test NewPostSignal 700 - ``` 222 + - PHP 8.2+ 223 + - Laravel 11+ 224 + - WebSocket support (enabled by default) 701 225 702 - --- 703 - 704 - ## Testing 705 - 706 - Signal includes a comprehensive test suite. Test your Signals: 707 - 708 - ### Unit Testing 709 - 710 - ```php 711 - use SocialDept\Signal\Events\CommitEvent; 712 - use SocialDept\Signal\Events\SignalEvent; 713 - 714 - class NewPostSignalTest extends TestCase 715 - { 716 - /** @test */ 717 - public function it_handles_new_posts() 718 - { 719 - $signal = new NewPostSignal(); 720 - 721 - $event = new SignalEvent( 722 - did: 'did:plc:test', 723 - timeUs: time() * 1000000, 724 - kind: 'commit', 725 - commit: new CommitEvent( 726 - rev: 'test', 727 - operation: 'create', 728 - collection: 'app.bsky.feed.post', 729 - rkey: 'test', 730 - record: (object) [ 731 - 'text' => 'Hello World!', 732 - 'createdAt' => now()->toIso8601String(), 733 - ], 734 - ), 735 - ); 736 - 737 - $signal->handle($event); 738 - 739 - // Assert your expected behavior 740 - } 741 - } 742 - ``` 743 - 744 - ### Testing with Artisan 745 - 746 - ```bash 747 - php artisan signal:test NewPostSignal 748 - ``` 749 - 750 - --- 751 - 752 - ## External Resources 226 + ## Resources 753 227 754 228 - [AT Protocol Documentation](https://atproto.com/) 229 + - [Bluesky API Docs](https://docs.bsky.app/) 755 230 - [Firehose Documentation](https://docs.bsky.app/docs/advanced-guides/firehose) 756 - - [Bluesky Lexicon](https://atproto.com/lexicons) 231 + - [Jetstream Documentation](https://github.com/bluesky-social/jetstream) 757 232 758 - --- 233 + ## Support & Contributing 759 234 760 - ## Examples 235 + Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-signals/issues). 761 236 762 - ### Monitor All Feed Activity 237 + Want to contribute? We'd love your help! Check out the [contribution guidelines](CONTRIBUTING.md). 763 238 764 - ```php 765 - class FeedMonitorSignal extends Signal 766 - { 767 - public function eventTypes(): array 768 - { 769 - return ['commit']; 770 - } 771 - 772 - public function collections(): ?array 773 - { 774 - return ['app.bsky.feed.*']; 775 - } 776 - 777 - public function handle(SignalEvent $event): void 778 - { 779 - // Handles posts, likes, reposts, etc. 780 - Log::info('Feed activity', [ 781 - 'collection' => $event->getCollection(), 782 - 'operation' => $event->getOperation(), 783 - 'did' => $event->did, 784 - ]); 785 - } 786 - } 787 - ``` 788 - 789 - ### Track New Follows 790 - 791 - ```php 792 - class NewFollowSignal extends Signal 793 - { 794 - public function eventTypes(): array 795 - { 796 - return ['commit']; 797 - } 798 - 799 - public function collections(): ?array 800 - { 801 - return ['app.bsky.graph.follow']; 802 - } 803 - 804 - public function handle(SignalEvent $event): void 805 - { 806 - if ($event->commit->isCreate()) { 807 - $record = $event->getRecord(); 808 - 809 - // Store follow relationship 810 - Follow::create([ 811 - 'follower_did' => $event->did, 812 - 'following_did' => $record->subject, 813 - ]); 814 - } 815 - } 816 - } 817 - ``` 818 - 819 - ### Content Moderation 239 + ## Credits 820 240 821 - ```php 822 - class ModerationSignal extends Signal 823 - { 824 - public function eventTypes(): array 825 - { 826 - return ['commit']; 827 - } 828 - 829 - public function collections(): ?array 830 - { 831 - return ['app.bsky.feed.post']; 832 - } 833 - 834 - public function shouldQueue(): bool 835 - { 836 - return true; 837 - } 838 - 839 - public function handle(SignalEvent $event): void 840 - { 841 - $record = $event->getRecord(); 842 - 843 - if ($this->containsProhibitedContent($record->text)) { 844 - $this->flagForModeration($event->did, $record); 845 - } 846 - } 847 - } 848 - ``` 849 - 850 - --- 851 - 852 - ## Requirements 853 - 854 - - PHP 8.2 or higher 855 - - Laravel 11.0 or higher 856 - - WebSocket support (enabled by default in most environments) 857 - 858 - --- 241 + - [Miguel Batres](https://batres.co) - founder & lead maintainer 242 + - [All contributors](https://github.com/socialdept/atp-signals/graphs/contributors) 859 243 860 244 ## License 861 245 862 - The MIT License (MIT). Please see [LICENSE](LICENSE) for more information. 246 + Signal is open-source software licensed under the [MIT license](LICENSE). 863 247 864 248 --- 865 249 866 - ## Support 867 - 868 - For issues, questions, or feature requests: 869 - - Read the [README.md](./README.md) before opening issues 870 - - Search through existing issues 871 - - Open new issue 872 - 873 - --- 874 - 875 - **Built for the AT Protocol ecosystem** โ€ข Made with โค๏ธ by Social Dept 250 + **Built for the Federation** โ€ข By Social Dept.
+9 -8
composer.json
··· 1 1 { 2 - "name": "socialdept/signal", 2 + "name": "socialdept/atp-signals", 3 3 "description": "Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel", 4 4 "type": "library", 5 5 "license": "MIT", 6 6 "require": { 7 7 "php": "^8.2", 8 + "ext-gmp": "*", 8 9 "illuminate/support": "^11.0|^12.0", 9 10 "illuminate/console": "^11.0|^12.0", 10 11 "illuminate/database": "^11.0|^12.0", 11 12 "ratchet/pawl": "^0.4", 12 - "react/event-loop": "^1.5", 13 - "revolution/laravel-bluesky": "^1.1" 13 + "react/event-loop": "^1.5" 14 14 }, 15 15 "require-dev": { 16 16 "orchestra/testbench": "^9.0", 17 - "phpunit/phpunit": "^11.0" 17 + "phpunit/phpunit": "^11.0", 18 + "friendsofphp/php-cs-fixer": "^3.89" 18 19 }, 19 20 "autoload": { 20 21 "psr-4": { 21 - "SocialDept\\Signal\\": "src/" 22 + "SocialDept\\AtpSignals\\": "src/" 22 23 } 23 24 }, 24 25 "autoload-dev": { 25 26 "psr-4": { 26 - "SocialDept\\Signal\\Tests\\": "tests/" 27 + "SocialDept\\AtpSignals\\Tests\\": "tests/" 27 28 } 28 29 }, 29 30 "extra": { 30 31 "laravel": { 31 32 "providers": [ 32 - "SocialDept\\Signal\\SignalServiceProvider" 33 + "SocialDept\\AtpSignals\\SignalServiceProvider" 33 34 ], 34 35 "aliases": { 35 - "Signal": "SocialDept\\Signal\\Facades\\Signal" 36 + "Signal": "SocialDept\\AtpSignals\\Facades\\Signal" 36 37 } 37 38 } 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 - }
+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 + }
+6 -6
src/Commands/ConsumeCommand.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Commands; 3 + namespace SocialDept\AtpSignals\Commands; 4 4 5 5 use BackedEnum; 6 6 use Exception; 7 7 use Illuminate\Console\Command; 8 - use SocialDept\Signal\Services\FirehoseConsumer; 9 - use SocialDept\Signal\Services\JetstreamConsumer; 10 - use SocialDept\Signal\Services\SignalRegistry; 8 + use SocialDept\AtpSignals\Services\FirehoseConsumer; 9 + use SocialDept\AtpSignals\Services\JetstreamConsumer; 10 + use SocialDept\AtpSignals\Services\SignalRegistry; 11 11 12 12 class ConsumeCommand extends Command 13 13 { ··· 115 115 if ($this->option('fresh')) { 116 116 $this->info('Starting fresh from the beginning'); 117 117 118 - return null; 118 + return 0; // Explicitly 0 means "start fresh, no cursor" 119 119 } 120 120 121 121 if ($this->option('cursor')) { ··· 127 127 128 128 $this->info('Resuming from stored cursor position'); 129 129 130 - return null; 130 + return null; // null means "use stored cursor" 131 131 } 132 132 133 133 private function startConsumer(string $mode, ?int $cursor): int
+1 -1
src/Commands/InstallCommand.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Commands; 3 + namespace SocialDept\AtpSignals\Commands; 4 4 5 5 use Illuminate\Console\Command; 6 6
+2 -2
src/Commands/ListSignalsCommand.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Commands; 3 + namespace SocialDept\AtpSignals\Commands; 4 4 5 5 use Illuminate\Console\Command; 6 6 use Illuminate\Support\Collection; 7 - use SocialDept\Signal\Services\SignalRegistry; 7 + use SocialDept\AtpSignals\Services\SignalRegistry; 8 8 9 9 class ListSignalsCommand extends Command 10 10 {
+1 -1
src/Commands/MakeSignalCommand.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Commands; 3 + namespace SocialDept\AtpSignals\Commands; 4 4 5 5 use Illuminate\Console\GeneratorCommand; 6 6 use Symfony\Component\Console\Input\InputOption;
+3 -3
src/Commands/TestSignalCommand.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Commands; 3 + namespace SocialDept\AtpSignals\Commands; 4 4 5 5 use Illuminate\Console\Command; 6 6 use InvalidArgumentException; 7 - use SocialDept\Signal\Events\CommitEvent; 8 - use SocialDept\Signal\Events\SignalEvent; 7 + use SocialDept\AtpSignals\Events\CommitEvent; 8 + use SocialDept\AtpSignals\Events\SignalEvent; 9 9 10 10 class TestSignalCommand extends Command 11 11 {
+1 -1
src/Contracts/CursorStore.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Contracts; 3 + namespace SocialDept\AtpSignals\Contracts; 4 4 5 5 interface CursorStore 6 6 {
+1 -1
src/Contracts/EventContract.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Contracts; 3 + namespace SocialDept\AtpSignals\Contracts; 4 4 5 5 interface EventContract 6 6 {
+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 1 <?php 2 2 3 - namespace SocialDept\Signal\Enums; 3 + namespace SocialDept\AtpSignals\Enums; 4 4 5 5 enum SignalCommitOperation: string 6 6 {
+1 -1
src/Enums/SignalEventType.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Enums; 3 + namespace SocialDept\AtpSignals\Enums; 4 4 5 5 enum SignalEventType: string 6 6 {
+4 -3
src/Events/AccountEvent.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Events; 3 + namespace SocialDept\AtpSignals\Events; 4 4 5 - use SocialDept\Signal\Contracts\EventContract; 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 6 7 7 class AccountEvent implements EventContract 8 8 { ··· 12 12 public ?string $status = null, 13 13 public int $seq = 0, 14 14 public ?string $time = null, 15 - ) {} 15 + ) { 16 + } 16 17 17 18 public static function fromArray(array $data): self 18 19 {
+3 -3
src/Events/CommitEvent.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Events; 3 + namespace SocialDept\AtpSignals\Events; 4 4 5 - use SocialDept\Signal\Contracts\EventContract; 6 - use SocialDept\Signal\Enums\SignalCommitOperation; 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 + use SocialDept\AtpSignals\Enums\SignalCommitOperation; 7 7 8 8 class CommitEvent implements EventContract 9 9 {
+4 -3
src/Events/IdentityEvent.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Events; 3 + namespace SocialDept\AtpSignals\Events; 4 4 5 - use SocialDept\Signal\Contracts\EventContract; 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 6 7 7 class IdentityEvent implements EventContract 8 8 { ··· 11 11 public ?string $handle = null, 12 12 public int $seq = 0, 13 13 public ?string $time = null, 14 - ) {} 14 + ) { 15 + } 15 16 16 17 public static function fromArray(array $data): self 17 18 {
+5 -4
src/Events/SignalEvent.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Events; 3 + namespace SocialDept\AtpSignals\Events; 4 4 5 - use SocialDept\Signal\Contracts\EventContract; 5 + use SocialDept\AtpSignals\Contracts\EventContract; 6 6 7 7 class SignalEvent implements EventContract 8 8 { ··· 13 13 public ?CommitEvent $commit = null, 14 14 public ?IdentityEvent $identity = null, 15 15 public ?AccountEvent $account = null, 16 - ) {} 16 + ) { 17 + } 17 18 18 19 public function isCommit(): bool 19 20 { ··· 40 41 return $this->commit?->record; 41 42 } 42 43 43 - public function getOperation(): ?\SocialDept\Signal\Enums\SignalCommitOperation 44 + public function getOperation(): ?\SocialDept\AtpSignals\Enums\SignalCommitOperation 44 45 { 45 46 return $this->commit?->operation; 46 47 }
+1 -1
src/Exceptions/ConnectionException.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Exceptions; 3 + namespace SocialDept\AtpSignals\Exceptions; 4 4 5 5 class ConnectionException extends \Exception 6 6 {
+1 -1
src/Exceptions/SignalException.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Exceptions; 3 + namespace SocialDept\AtpSignals\Exceptions; 4 4 5 5 class SignalException extends \Exception 6 6 {
+3 -3
src/Facades/Signal.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Facades; 3 + namespace SocialDept\AtpSignals\Facades; 4 4 5 5 use Illuminate\Support\Facades\Facade; 6 - use SocialDept\Signal\Services\SignalManager; 6 + use SocialDept\AtpSignals\Services\SignalManager; 7 7 8 8 /** 9 9 * @method static void start(?int $cursor = null) 10 10 * @method static void stop() 11 11 * @method static string getMode() 12 12 * 13 - * @see \SocialDept\Signal\Services\SignalManager 13 + * @see \SocialDept\AtpSignals\Services\SignalManager 14 14 */ 15 15 class Signal extends Facade 16 16 {
+9 -5
src/Jobs/ProcessSignalJob.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Jobs; 3 + namespace SocialDept\AtpSignals\Jobs; 4 4 5 5 use Illuminate\Bus\Queueable; 6 6 use Illuminate\Contracts\Queue\ShouldQueue; 7 7 use Illuminate\Foundation\Bus\Dispatchable; 8 8 use Illuminate\Queue\InteractsWithQueue; 9 9 use Illuminate\Queue\SerializesModels; 10 - use SocialDept\Signal\Events\SignalEvent; 11 - use SocialDept\Signal\Signals\Signal; 10 + use SocialDept\AtpSignals\Events\SignalEvent; 11 + use SocialDept\AtpSignals\Signals\Signal; 12 12 13 13 class ProcessSignalJob implements ShouldQueue 14 14 { 15 - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; 15 + use Dispatchable; 16 + use InteractsWithQueue; 17 + use Queueable; 18 + use SerializesModels; 16 19 17 20 public function __construct( 18 21 protected Signal $signal, 19 22 protected SignalEvent $event, 20 - ) {} 23 + ) { 24 + } 21 25 22 26 public function handle(): void 23 27 {
+3 -3
src/Services/EventDispatcher.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Services; 3 + namespace SocialDept\AtpSignals\Services; 4 4 5 5 use Illuminate\Support\Facades\Log; 6 6 use Illuminate\Support\Facades\Queue; 7 - use SocialDept\Signal\Events\SignalEvent; 8 - use SocialDept\Signal\Jobs\ProcessSignalJob; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Jobs\ProcessSignalJob; 9 9 10 10 class EventDispatcher 11 11 {
+69 -58
src/Services/FirehoseConsumer.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Services; 3 + namespace SocialDept\AtpSignals\Services; 4 4 5 5 use Illuminate\Support\Arr; 6 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\AccountEvent; 11 - use SocialDept\Signal\Events\CommitEvent; 12 - use SocialDept\Signal\Events\IdentityEvent; 13 - use SocialDept\Signal\Events\SignalEvent; 14 - use SocialDept\Signal\Exceptions\ConnectionException; 15 - use SocialDept\Signal\Support\WebSocketConnection; 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; 16 17 17 18 class FirehoseConsumer 18 19 { ··· 45 46 { 46 47 $this->shouldStop = false; 47 48 48 - // Get cursor from storage if not provided 49 + // Get cursor from storage if not explicitly provided 50 + // null = use stored cursor, 0 = start fresh (no cursor), >0 = specific cursor 49 51 if ($cursor === null) { 50 52 $cursor = $this->cursorStore->get(); 51 53 } 52 54 53 - $url = $this->buildWebSocketUrl($cursor); 55 + // If cursor is explicitly 0, don't send it (fresh start) 56 + $url = $this->buildWebSocketUrl($cursor > 0 ? $cursor : null); 54 57 55 58 Log::info('Signal: Starting Firehose consumer', [ 56 59 'url' => $url, 57 - 'cursor' => $cursor, 60 + 'cursor' => $cursor > 0 ? $cursor : 'none (fresh start)', 58 61 'mode' => 'firehose', 59 62 ]); 60 63 ··· 80 83 */ 81 84 protected function connect(string $url): void 82 85 { 83 - $this->connection = new WebSocketConnection; 86 + $this->connection = new WebSocketConnection(); 84 87 85 88 // Set up event handlers 86 89 $this->connection ··· 176 179 $time = $payload['time']; 177 180 $timeUs = $payload['seq'] ?? 0; // Use seq as time_us equivalent 178 181 179 - // Parse CAR blocks 182 + // Parse CAR blocks (returns CID => block data map) 180 183 $records = $payload['blocks']; 184 + 181 185 $blocks = []; 182 186 if (! empty($records)) { 183 - $blocks = rescue(fn () => iterator_to_array(CAR::blockMap($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 + }); 184 193 } 185 194 186 195 // Process operations ··· 202 211 $rkey = ''; 203 212 204 213 if (str_contains($path, '/')) { 205 - [$collection, $rkey] = explode('/', $path); 214 + [$collection, $rkey] = explode('/', $path, 2); 206 215 } 207 216 208 - $record = $blocks[$path] ?? []; 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 + } 209 230 210 231 // Convert to SignalEvent format for compatibility 211 - $event = $this->buildSignalEvent($did, $timeUs, $action, $collection, $rkey, $rev, $cid, $record); 232 + $event = $this->buildSignalEvent($did, $timeUs, $action, $collection, $rkey, $rev, $cidStr, $record); 212 233 213 - // Dispatch event with cursor update and logging 214 - $this->dispatchSignalEvent($event, 'Commit', [ 215 - 'collection' => $collection, 216 - 'operation' => $action, 217 - ]); 234 + // Dispatch event with cursor update 235 + $this->dispatchSignalEvent($event); 218 236 } 219 237 } 220 238 ··· 231 249 ?string $cid, 232 250 array $record 233 251 ): SignalEvent { 234 - $recordValue = $record['value'] ?? null; 252 + // Record is already the decoded data, or empty array for deletes 253 + $recordValue = ! empty($record) ? (object) $record : null; 235 254 236 255 $commitEvent = new CommitEvent( 237 256 rev: $rev, 238 257 operation: $operation, 239 258 collection: $collection, 240 259 rkey: $rkey, 241 - record: $recordValue ? (object) $recordValue : null, 260 + record: $recordValue, 242 261 cid: $cid 243 262 ); 244 263 ··· 251 270 } 252 271 253 272 /** 254 - * Dispatch a SignalEvent with cursor update and logging. 273 + * Normalize CID objects to AT Protocol link format. 255 274 */ 256 - protected function dispatchSignalEvent(SignalEvent $event, string $eventType, array $context = []): void 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 257 293 { 258 294 // Update cursor 259 295 $this->cursorStore->set($event->timeUs); 260 296 261 - // Check if any signals match this event 262 - $matchingSignals = $this->signalRegistry->getMatchingSignals($event); 263 - 264 - if ($matchingSignals->isNotEmpty()) { 265 - Log::info("Signal: {$eventType} event matched", array_merge([ 266 - 'matched_signals' => $matchingSignals->count(), 267 - 'signal_names' => $matchingSignals->map(fn ($s) => class_basename($s))->join(', '), 268 - ], $context)); 269 - } 270 - 271 297 // Dispatch to matching signals 272 298 $this->eventDispatcher->dispatch($event); 273 299 } ··· 306 332 identity: $identityEvent 307 333 ); 308 334 309 - // Dispatch event with cursor update and logging 310 - $this->dispatchSignalEvent($event, 'Identity', [ 311 - 'did' => $did, 312 - 'handle' => $handle, 313 - ]); 335 + // Dispatch event with cursor update 336 + $this->dispatchSignalEvent($event); 314 337 } 315 338 316 339 /** ··· 349 372 account: $accountEvent 350 373 ); 351 374 352 - // Dispatch event with cursor update and logging 353 - $this->dispatchSignalEvent($event, 'Account', [ 354 - 'did' => $did, 355 - 'active' => $active, 356 - 'status' => $status, 357 - ]); 375 + // Dispatch event with cursor update 376 + $this->dispatchSignalEvent($event); 358 377 } 359 378 360 379 /** ··· 394 413 395 414 if ($this->reconnectAttempts >= $maxAttempts) { 396 415 Log::error('Signal: Max reconnection attempts reached'); 416 + 397 417 throw new ConnectionException('Failed to reconnect to Firehose after '.$maxAttempts.' attempts'); 398 418 } 399 419 ··· 441 461 if (! empty($params)) { 442 462 $url .= '?'.implode('&', $params); 443 463 } 444 - 445 - Log::warning('Signal: Firehose mode - NO server-side collection filtering', [ 446 - 'note' => 'All events will be received and filtered client-side', 447 - 'registered_collections' => $this->signalRegistry->all() 448 - ->flatMap(fn ($signal) => $signal->collections() ?? []) 449 - ->unique() 450 - ->values() 451 - ->toArray(), 452 - ]); 453 464 454 465 return $url; 455 466 }
+13 -30
src/Services/JetstreamConsumer.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Services; 3 + namespace SocialDept\AtpSignals\Services; 4 4 5 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; 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 10 11 11 class JetstreamConsumer 12 12 { ··· 39 39 { 40 40 $this->shouldStop = false; 41 41 42 - // Get cursor from storage if not provided 42 + // Get cursor from storage if not explicitly provided 43 + // null = use stored cursor, 0 = start fresh (no cursor), >0 = specific cursor 43 44 if ($cursor === null) { 44 45 $cursor = $this->cursorStore->get(); 45 46 } 46 47 47 - $url = $this->buildWebSocketUrl($cursor); 48 + // If cursor is explicitly 0, don't send it (fresh start) 49 + $url = $this->buildWebSocketUrl($cursor > 0 ? $cursor : null); 48 50 49 51 Log::info('Signal: Starting Jetstream consumer', [ 50 52 'url' => $url, 51 - 'cursor' => $cursor, 53 + 'cursor' => $cursor > 0 ? $cursor : 'none (fresh start)', 54 + 'mode' => 'firehose', 52 55 ]); 53 56 54 57 $this->connect($url); ··· 73 76 */ 74 77 protected function connect(string $url): void 75 78 { 76 - $this->connection = new WebSocketConnection; 79 + $this->connection = new WebSocketConnection(); 77 80 78 81 // Set up event handlers 79 82 $this->connection ··· 127 130 // Update cursor 128 131 $this->cursorStore->set($event->timeUs); 129 132 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 133 // Dispatch to matching signals 146 134 $this->eventDispatcher->dispatch($event); 147 135 ··· 190 178 191 179 if ($this->reconnectAttempts >= $maxAttempts) { 192 180 Log::error('Signal: Max reconnection attempts reached'); 181 + 193 182 throw new ConnectionException('Failed to reconnect to Jetstream after '.$maxAttempts.' attempts'); 194 183 } 195 184 ··· 244 233 foreach ($collections as $collection) { 245 234 $params[] = 'wantedCollections='.urlencode($collection); 246 235 } 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 236 } 254 237 255 238 if (! empty($params)) {
+3 -2
src/Services/SignalManager.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Services; 3 + namespace SocialDept\AtpSignals\Services; 4 4 5 5 use InvalidArgumentException; 6 6 ··· 9 9 public function __construct( 10 10 protected FirehoseConsumer $firehoseConsumer, 11 11 protected JetstreamConsumer $jetstreamConsumer, 12 - ) {} 12 + ) { 13 + } 13 14 14 15 /** 15 16 * Start consuming events from the AT Protocol.
+2 -2
src/Services/SignalRegistry.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Services; 3 + namespace SocialDept\AtpSignals\Services; 4 4 5 5 use Illuminate\Support\Collection; 6 6 use Illuminate\Support\Facades\File; 7 7 use InvalidArgumentException; 8 - use SocialDept\Signal\Signals\Signal; 8 + use SocialDept\AtpSignals\Signals\Signal; 9 9 10 10 class SignalRegistry 11 11 {
+19 -19
src/SignalServiceProvider.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal; 3 + namespace SocialDept\AtpSignals; 4 4 5 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\FirehoseConsumer; 14 - use SocialDept\Signal\Services\JetstreamConsumer; 15 - use SocialDept\Signal\Services\SignalManager; 16 - use SocialDept\Signal\Services\SignalRegistry; 17 - use SocialDept\Signal\Storage\DatabaseCursorStore; 18 - use SocialDept\Signal\Storage\FileCursorStore; 19 - use SocialDept\Signal\Storage\RedisCursorStore; 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 20 21 21 class SignalServiceProvider extends ServiceProvider 22 22 { ··· 27 27 // Register cursor store 28 28 $this->app->singleton(CursorStore::class, function ($app) { 29 29 return match (config('signal.cursor_storage')) { 30 - 'redis' => new RedisCursorStore, 31 - 'file' => new FileCursorStore, 32 - default => new DatabaseCursorStore, 30 + 'redis' => new RedisCursorStore(), 31 + 'file' => new FileCursorStore(), 32 + default => new DatabaseCursorStore(), 33 33 }; 34 34 }); 35 35 36 36 // Register signal registry 37 37 $this->app->singleton(SignalRegistry::class, function ($app) { 38 - $registry = new SignalRegistry; 38 + $registry = new SignalRegistry(); 39 39 40 40 // Register configured signals 41 41 foreach (config('signal.signals', []) as $signal) {
+4 -4
src/Signals/Signal.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Signals; 3 + namespace SocialDept\AtpSignals\Signals; 4 4 5 - use SocialDept\Signal\Events\SignalEvent; 5 + use SocialDept\AtpSignals\Events\SignalEvent; 6 6 7 7 abstract class Signal 8 8 { 9 9 /** 10 10 * Define which event types to listen for. 11 11 * 12 - * @return array<string|\SocialDept\Signal\Enums\SignalEventType> 12 + * @return array<string|\SocialDept\AtpSignals\Enums\SignalEventType> 13 13 */ 14 14 abstract public function eventTypes(): array; 15 15 ··· 42 42 * - [SignalCommitOperation::Delete] - Only handle deletes 43 43 * - null - Handle all operations (default) 44 44 * 45 - * @return array<string|\SocialDept\Signal\Enums\SignalCommitOperation>|null 45 + * @return array<string|\SocialDept\AtpSignals\Enums\SignalCommitOperation>|null 46 46 */ 47 47 public function operations(): ?array 48 48 {
+2 -2
src/Storage/DatabaseCursorStore.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Storage; 3 + namespace SocialDept\AtpSignals\Storage; 4 4 5 5 use Illuminate\Database\Query\Builder; 6 6 use Illuminate\Support\Facades\DB; 7 - use SocialDept\Signal\Contracts\CursorStore; 7 + use SocialDept\AtpSignals\Contracts\CursorStore; 8 8 9 9 class DatabaseCursorStore implements CursorStore 10 10 {
+4 -4
src/Storage/FileCursorStore.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Storage; 3 + namespace SocialDept\AtpSignals\Storage; 4 4 5 5 use Illuminate\Support\Facades\File; 6 - use SocialDept\Signal\Contracts\CursorStore; 6 + use SocialDept\AtpSignals\Contracts\CursorStore; 7 7 8 8 class FileCursorStore implements CursorStore 9 9 { ··· 15 15 16 16 // Ensure directory exists 17 17 $directory = dirname($this->path); 18 - if (!File::exists($directory)) { 18 + if (! File::exists($directory)) { 19 19 File::makeDirectory($directory, 0755, true); 20 20 } 21 21 } 22 22 23 23 public function get(): ?int 24 24 { 25 - if (!File::exists($this->path)) { 25 + if (! File::exists($this->path)) { 26 26 return null; 27 27 } 28 28
+2 -2
src/Storage/RedisCursorStore.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Storage; 3 + namespace SocialDept\AtpSignals\Storage; 4 4 5 5 use Illuminate\Support\Facades\Redis; 6 - use SocialDept\Signal\Contracts\CursorStore; 6 + use SocialDept\AtpSignals\Contracts\CursorStore; 7 7 8 8 class RedisCursorStore implements CursorStore 9 9 {
+8 -2
src/Support/WebSocketConnection.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Support; 3 + namespace SocialDept\AtpSignals\Support; 4 4 5 5 use Ratchet\Client\Connector; 6 6 use Ratchet\Client\WebSocket; ··· 64 64 if ($this->onError) { 65 65 ($this->onError)($e); 66 66 } 67 + 67 68 throw $e; 68 69 } 69 70 ); ··· 74 75 */ 75 76 public function send(string $message): bool 76 77 { 77 - if (!$this->connected || !$this->connection) { 78 + if (! $this->connected || ! $this->connection) { 78 79 return false; 79 80 } 80 81 81 82 try { 82 83 $this->connection->send($message); 84 + 83 85 return true; 84 86 } catch (\Exception $e) { 85 87 if ($this->onError) { 86 88 ($this->onError)($e); 87 89 } 90 + 88 91 return false; 89 92 } 90 93 } ··· 114 117 public function onMessage(callable $callback): self 115 118 { 116 119 $this->onMessage = $callback(...); 120 + 117 121 return $this; 118 122 } 119 123 ··· 123 127 public function onClose(callable $callback): self 124 128 { 125 129 $this->onClose = $callback(...); 130 + 126 131 return $this; 127 132 } 128 133 ··· 132 137 public function onError(callable $callback): self 133 138 { 134 139 $this->onError = $callback(...); 140 + 135 141 return $this; 136 142 } 137 143
+2 -2
stubs/signal.stub
··· 2 2 3 3 namespace {{ namespace }}; 4 4 5 - use SocialDept\Signal\Events\SignalEvent; 6 - use SocialDept\Signal\Signals\Signal; 5 + use SocialDept\AtpSignals\Events\SignalEvent; 6 + use SocialDept\AtpSignals\Signals\Signal; 7 7 8 8 class {{ class }} extends Signal 9 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 1 <?php 2 2 3 - namespace SocialDept\Signal\Tests\Unit; 3 + namespace SocialDept\AtpSignals\Tests\Unit; 4 4 5 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; 6 + use SocialDept\AtpSignals\Events\CommitEvent; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Services\SignalRegistry; 10 9 11 10 class SignalRegistryTest extends TestCase 12 11 {
+17 -10
tests/Unit/SignalTest.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\Signal\Tests\Unit; 3 + namespace SocialDept\AtpSignals\Tests\Unit; 4 4 5 5 use Orchestra\Testbench\TestCase; 6 - use SocialDept\Signal\Events\CommitEvent; 7 - use SocialDept\Signal\Events\SignalEvent; 8 - use SocialDept\Signal\Signals\Signal; 6 + use SocialDept\AtpSignals\Events\CommitEvent; 7 + use SocialDept\AtpSignals\Events\SignalEvent; 8 + use SocialDept\AtpSignals\Signals\Signal; 9 9 10 10 class SignalTest extends TestCase 11 11 { 12 12 /** @test */ 13 13 public function it_can_create_a_signal() 14 14 { 15 - $signal = new class extends Signal { 15 + $signal = new class () extends Signal { 16 16 public function eventTypes(): array 17 17 { 18 18 return ['commit']; ··· 31 31 /** @test */ 32 32 public function it_can_filter_by_exact_collection() 33 33 { 34 - $signal = new class extends Signal { 34 + $signal = new class () extends Signal { 35 35 public function eventTypes(): array 36 36 { 37 37 return ['commit']; ··· 66 66 /** @test */ 67 67 public function it_can_filter_by_wildcard_collection() 68 68 { 69 - $signal = new class extends Signal { 69 + $signalClass = new class () extends Signal { 70 70 public function eventTypes(): array 71 71 { 72 72 return ['commit']; ··· 83 83 } 84 84 }; 85 85 86 + // Create registry and register the signal 87 + $registry = new \SocialDept\AtpSignals\Services\SignalRegistry(); 88 + $registry->register($signalClass::class); 89 + 86 90 // Test that it matches app.bsky.feed.post 87 91 $postEvent = new SignalEvent( 88 92 did: 'did:plc:test', ··· 96 100 ), 97 101 ); 98 102 99 - $this->assertTrue($signal->shouldHandle($postEvent)); 103 + $matchingSignals = $registry->getMatchingSignals($postEvent); 104 + $this->assertCount(1, $matchingSignals); 100 105 101 106 // Test that it matches app.bsky.feed.like 102 107 $likeEvent = new SignalEvent( ··· 111 116 ), 112 117 ); 113 118 114 - $this->assertTrue($signal->shouldHandle($likeEvent)); 119 + $matchingSignals = $registry->getMatchingSignals($likeEvent); 120 + $this->assertCount(1, $matchingSignals); 115 121 116 122 // Test that it does NOT match app.bsky.graph.follow 117 123 $followEvent = new SignalEvent( ··· 126 132 ), 127 133 ); 128 134 129 - $this->assertFalse($signal->shouldHandle($followEvent)); 135 + $matchingSignals = $registry->getMatchingSignals($followEvent); 136 + $this->assertCount(0, $matchingSignals); 130 137 } 131 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 + }