This is Laravel WAHA - a PHP Laravel package that provides an elegant API client for the WAHA (WhatsApp HTTP API). The package uses Saloon PHP as its HTTP client foundation.
php artisan boost:add-skill njoguamos/laravel-waha
Save this content to: AGENTS.md
# AI Development Instructions
## Project Overview
This is **Laravel WAHA** - a PHP Laravel package that provides an elegant API client for the [WAHA (WhatsApp HTTP API)](https://waha.example.com). The package uses [Saloon PHP](https://docs.saloon.dev/) as its HTTP client foundation.
- **Package Name:** `njoguamos/laravel-waha`
- **PHP Version:** ^8.4
- **Laravel Version:** ^12.x
- **License:** MIT
- **Author:** Njogu Amos ([email protected])
## Architecture
### Directory Structure
```text
├── config/ # Package configuration
├── docs/ # Documentation (VitePress)
├── src/ # Source code
│ ├── Dto/ # Data Transfer Objects
│ ├── Endpoints/ # API endpoint classes (Status, etc.)
│ ├── Facades/ # Laravel facades
│ └── Requests/ # Saloon request classes
├── tests/ # Pest PHP tests
│ ├── Feature/ # Feature tests
│ ├── Unit/ # Unit tests
│ ├── Pest.php # Pest configuration
│ └── TestCase.php # Base test case (not used for test files)
```
### Core Patterns
1. **Saloon HTTP:** `WahaConnector` (auth & headers) → `Request` classes (per endpoint) → DTOs
2. **Endpoints:** Extend `Waha` base class, return `Saloon\Http\Response`, accessible via facades. Use `dtoOrFail()` to transform the response into a typed DTO.
3. **DTOs:** Have `toArray()` serialization. `fromArray()` is allowed only when needed. `createDtoFromResponse()` in `Request` classes is used for `dtoOrFail()`.
4. **Testing:** All tests MUST use **Pest PHP** syntax (`test()` or `it()`). PHPUnit class-based tests are prohibited.
5. **Service Provider:** Binds `WahaConnector` to container, publishes config
6. **Facades:** Static access via `Status::sendText()` instead of `app(Status::class)->sendText()`
## Coding Standards
### PHP Style (Pint Configuration)
Run formatting with: `composer format`
Key rules from `pint.json`:
- **Preset:** PSR-12
- **Strict types:** Required (`declare(strict_types=1);`)
- **Array syntax:** Short `[]`
- **Imports:** Sorted by length
- **Class elements:** Ordered (traits, constants, properties, constructor, methods)
- **Binary operators:** Aligned (`=>`)
- **Date/Time immutability:** Encourages immutable datetime objects (`date_time_immutable`)
- **Multibyte strings:** Uses `mb_` string functions (`mb_str_functions`)
- **Modern type casting:** Modernizes type casting syntax (`modernize_types_casting`)
- **Global imports:** Imports classes, constants, and functions globally (`global_namespace_import`)
- **Access modifiers:** Converts `protected` to `private` where possible (`protected_to_private`)
### Code Style Examples
```php
<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Requests\Status;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
use NjoguAmos\Waha\Dto\TextStatusData;
class SendTextStatusRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $session,
protected TextStatusData $data
) {
}
public function resolveEndpoint(): string
{
return '/api/'.rawurlencode($this->session).'/status/text';
}
public function defaultBody(): array
{
return $this->data->toArray();
}
}
```
### DTO Example
```php
<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Dto;
class TextStatusData
{
public function __construct(
public string $text,
public ?string $backgroundColor = null,
public int $font = 1,
public ?array $contact = null,
) {
}
public function toArray(): array
{
$array = [
'text' => $this->text,
'backgroundColor' => $this->backgroundColor,
'font' => $this->font,
];
if ($this->contact !== null) {
$array['contacts'] = $this->contact;
}
return $array;
}
}
```
## Testing
Run tests: `composer test -- --parallel`
- **`tests/Feature/`** - Integration tests (endpoint methods, HTTP cycles)
- **`tests/Unit/`** - Unit tests (DTOs, support classes)
### Key Patterns
```php
// Mock HTTP requests
MockClient::global([
SendTextStatusRequest::class => MockResponse::make(['text' => 'Hello'])
]);
// Test DTOs
$statusData = new TextStatusData(text: 'Hello', backgroundColor: '#38b42f', font: 1);
expect($statusData->text)->toBe('Hello');
```
- Configure in `tests/Pest.php` with `Config::preventStrayRequests()` - all HTTP requests must be mocked
- Use `test()` or `it()` functions with descriptive names
## Environment Variables
```bash
WAHA_API_KEY=your-api-key
WAHA_BASE_URL=https://waha.example.com
```
## Service Provider & Configuration
The `WahaServiceProvider` (in `src/WahaServiceProvider.php`):
```php
public function register(): void
{
$this->app->bind(
abstract: WahaConnector::class,
concrete: function (): WahaConnector {
return new WahaConnector(
baseUrl: config()->string(key: 'waha.base_url'),
apiKey: config()->string(key: 'waha.api_key'),
);
}
);
}
public function boot(): void
{
$this->mergeConfigFrom(path: __DIR__ . '/../config/waha.php', key: 'waha');
$this->publishes(paths: [
__DIR__ . '/../config/waha.php' => config_path('waha.php'),
], groups: 'config');
}
```
Publish config: `php artisan vendor:publish --tag=config --provider="NjoguAmos\Waha\WahaServiceProvider"`
## Using Facades
```php
use NjoguAmos\Waha\Facades\Status;
use NjoguAmos\Waha\Dto\TextStatusData;
$statusData = new TextStatusData(
text: 'Have a look! https://waha.example.com/',
backgroundColor: '#38b42f',
font: 1
);
$result = Status::sendText(session: 'default', data: $statusData);
```
Each facade implements `getFacadeAccessor()` pointing to its endpoint class.
## Available Commands
| Command | Description |
|-------------------|--------------------------------|
| `composer test -- --parallel` | Run Pest tests (use --parallel for speed) |
| `composer format` | Run Laravel Pint (format code) |
| `composer lint` | Check code style with Pint |
## Adding New Features
### Adding a New API Endpoint
1. **Create the Request class** in `src/Requests/` (implement `HasBody` if needed):
```php
<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Requests\Something;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
use NjoguAmos\Waha\Dto\SomethingData;
class GetSomething extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $session,
protected SomethingData $data
) {
}
public function resolveEndpoint(): string
{
return '/api/'.rawurlencode($this->session).'/something';
}
public function defaultBody(): array
{
return $this->data->toArray();
}
}
```
2. **Create the DTO** in `src/Dto/`:
```php
<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Dto;
class SomethingData
{
public function __construct(public int $id, public string $name) {}
}
```
3. **Add to Endpoint** in `src/Endpoints/`, return `Saloon\Http\Response`, and write tests in `tests/Feature/`. Use `dtoOrFail()` on the response to access DTO data:
```php
// In Endpoint class
public function getSomething(string $session, SomethingData $data): Response
{
return $this->connector->send(new GetSomething($session, $data));
}
// In usage/tests
$response = Status::getSomething(session: 'default', data: $somethingData);
$dto = $response->dtoOrFail();
```
## Base Class (Waha)
All endpoint classes extend the `NjoguAmos\Waha\Waha` base class, which provides internal utility methods:
- **`humanDelay(int $min = 1, int $max = 10): void`**: Adds a random sleep between `$min` and `$max` seconds to mimic human behavior.
- **`sendPresenceStatus(string $session, string $chatId): void`**: Sends a sequence of Online → Typing → Paused presence statuses with random delays to mimic human-like messaging behavior.
## Documentation Standards (refine-doc)
When updating documentation in `docs/`:
1. **Title and Subtitle:** `#` for main title, `##` for sections like `Usage`, `Response`, `Engines`, and `References`.
2. **Code Blocks:** Use `::: code-group` for multiple implementation examples (e.g., `[Saloon Response]` vs `[DTO]`).
3. **Response Section:** Describe the response type (`Saloon\Http\Response`) and show how to handle it using `json()` and `dtoOrFail()`.
4. **References:** Always include a `## References` section linking to official WAHA docs and relevant DTO references using site-absolute paths (`/reference/dto/<slug>.md`).
5. **Engines Table:** Include a compatibility table for different WAHA engines (WEBJS, WPP, NOWEB, GOWS).
## Important Notes
1. **Always use `declare(strict_types=1);`** at the top of every PHP file
2. **Type hint everything** - properties, parameters, return types
3. **Use constructor property promotion** where appropriate
4. **Prefer named arguments** for DTO constructors
5. **Handle null values** with null coalescing operator `??` in DTOs
6. **Use `dtoOrFail()`** when expecting DTOs from responses
7. **Mock all HTTP requests** in tests - stray requests are prevented
8. **Follow PSR-12** and run `composer format` before committing
# Use Semantic Commit Messages
- `feat`: (new feature for the user, not a new feature for a build script)
- `fix`: (bug fix for the user, not a fix to a build script)
- `docs`: (changes to the documentation)
- `style`: (formatting, missing semicolons, etc.; no production code change)
- `refactor`: (refactoring production code, e.g. renaming a variable)
- `test`: (adding missing tests, refactoring tests; no production code change)
- `chore`: (updating grunt tasks etc.; no production code change)
## Documentation
Full documentation is in `docs/` and can be built with VitePress.
This is Laravel WAHA - a PHP Laravel package that provides an elegant API client for the WAHA (WhatsApp HTTP API). The package uses Saloon PHP as its HTTP client foundation.
njoguamos/laravel-waha├── config/ # Package configuration
├── docs/ # Documentation (VitePress)
├── src/ # Source code
│ ├── Dto/ # Data Transfer Objects
│ ├── Endpoints/ # API endpoint classes (Status, etc.)
│ ├── Facades/ # Laravel facades
│ └── Requests/ # Saloon request classes
├── tests/ # Pest PHP tests
│ ├── Feature/ # Feature tests
│ ├── Unit/ # Unit tests
│ ├── Pest.php # Pest configuration
│ └── TestCase.php # Base test case (not used for test files)
WahaConnector (auth & headers) → Request classes (per endpoint) → DTOsWaha base class, return Saloon\Http\Response, accessible via facades. Use dtoOrFail() to transform the response into a typed DTO.toArray() serialization. fromArray() is allowed only when needed. createDtoFromResponse() in Request classes is used for dtoOrFail().test() or it()). PHPUnit class-based tests are prohibited.WahaConnector to container, publishes configStatus::sendText() instead of app(Status::class)->sendText()Run formatting with: composer format
Key rules from pint.json:
declare(strict_types=1);)[]=>)date_time_immutable)mb_ string functions (mb_str_functions)modernize_types_casting)global_namespace_import)protected to private where possible (protected_to_private)<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Requests\Status;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
use NjoguAmos\Waha\Dto\TextStatusData;
class SendTextStatusRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $session,
protected TextStatusData $data
) {
}
public function resolveEndpoint(): string
{
return '/api/'.rawurlencode($this->session).'/status/text';
}
public function defaultBody(): array
{
return $this->data->toArray();
}
}
<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Dto;
class TextStatusData
{
public function __construct(
public string $text,
public ?string $backgroundColor = null,
public int $font = 1,
public ?array $contact = null,
) {
}
public function toArray(): array
{
$array = [
'text' => $this->text,
'backgroundColor' => $this->backgroundColor,
'font' => $this->font,
];
if ($this->contact !== null) {
$array['contacts'] = $this->contact;
}
return $array;
}
}
Run tests: composer test -- --parallel
tests/Feature/ - Integration tests (endpoint methods, HTTP cycles)tests/Unit/ - Unit tests (DTOs, support classes)// Mock HTTP requests
MockClient::global([
SendTextStatusRequest::class => MockResponse::make(['text' => 'Hello'])
]);
// Test DTOs
$statusData = new TextStatusData(text: 'Hello', backgroundColor: '#38b42f', font: 1);
expect($statusData->text)->toBe('Hello');
tests/Pest.php with Config::preventStrayRequests() - all HTTP requests must be mockedtest() or it() functions with descriptive namesWAHA_API_KEY=your-api-key
WAHA_BASE_URL=https://waha.example.com
The WahaServiceProvider (in src/WahaServiceProvider.php):
public function register(): void
{
$this->app->bind(
abstract: WahaConnector::class,
concrete: function (): WahaConnector {
return new WahaConnector(
baseUrl: config()->string(key: 'waha.base_url'),
apiKey: config()->string(key: 'waha.api_key'),
);
}
);
}
public function boot(): void
{
$this->mergeConfigFrom(path: __DIR__ . '/../config/waha.php', key: 'waha');
$this->publishes(paths: [
__DIR__ . '/../config/waha.php' => config_path('waha.php'),
], groups: 'config');
}
Publish config: php artisan vendor:publish --tag=config --provider="NjoguAmos\Waha\WahaServiceProvider"
use NjoguAmos\Waha\Facades\Status;
use NjoguAmos\Waha\Dto\TextStatusData;
$statusData = new TextStatusData(
text: 'Have a look! https://waha.example.com/',
backgroundColor: '#38b42f',
font: 1
);
$result = Status::sendText(session: 'default', data: $statusData);
Each facade implements getFacadeAccessor() pointing to its endpoint class.
| Command | Description |
|---|---|
composer test -- --parallel |
Run Pest tests (use --parallel for speed) |
composer format |
Run Laravel Pint (format code) |
composer lint |
Check code style with Pint |
src/Requests/ (implement HasBody if needed):<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Requests\Something;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
use NjoguAmos\Waha\Dto\SomethingData;
class GetSomething extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $session,
protected SomethingData $data
) {
}
public function resolveEndpoint(): string
{
return '/api/'.rawurlencode($this->session).'/something';
}
public function defaultBody(): array
{
return $this->data->toArray();
}
}
src/Dto/:<?php
declare(strict_types=1);
namespace NjoguAmos\Waha\Dto;
class SomethingData
{
public function __construct(public int $id, public string $name) {}
}
src/Endpoints/, return Saloon\Http\Response, and write tests in tests/Feature/. Use dtoOrFail() on the response to access DTO data:// In Endpoint class
public function getSomething(string $session, SomethingData $data): Response
{
return $this->connector->send(new GetSomething($session, $data));
}
// In usage/tests
$response = Status::getSomething(session: 'default', data: $somethingData);
$dto = $response->dtoOrFail();
All endpoint classes extend the NjoguAmos\Waha\Waha base class, which provides internal utility methods:
humanDelay(int $min = 1, int $max = 10): void: Adds a random sleep between $min and $max seconds to mimic human behavior.sendPresenceStatus(string $session, string $chatId): void: Sends a sequence of Online → Typing → Paused presence statuses with random delays to mimic human-like messaging behavior.When updating documentation in docs/:
# for main title, ## for sections like Usage, Response, Engines, and References.::: code-group for multiple implementation examples (e.g., [Saloon Response] vs [DTO]).Saloon\Http\Response) and show how to handle it using json() and dtoOrFail().## References section linking to official WAHA docs and relevant DTO references using site-absolute paths (/reference/dto/<slug>.md).declare(strict_types=1); at the top of every PHP file?? in DTOsdtoOrFail() when expecting DTOs from responsescomposer format before committingfeat: (new feature for the user, not a new feature for a build script)fix: (bug fix for the user, not a fix to a build script)docs: (changes to the documentation)style: (formatting, missing semicolons, etc.; no production code change)refactor: (refactoring production code, e.g. renaming a variable)test: (adding missing tests, refactoring tests; no production code change)chore: (updating grunt tasks etc.; no production code change)Full documentation is in docs/ and can be built with VitePress.
How can I help you explore Laravel packages today?