Skip to content

Commit 951e03c

Browse files
committed
first commit
0 parents  commit 951e03c

60 files changed

Lines changed: 3019 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
check:
9+
name: PHP ${{ matrix.php }}
10+
runs-on: ubuntu-latest
11+
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
php:
16+
- '8.2'
17+
- '8.3'
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup PHP
24+
uses: shivammathur/setup-php@v2
25+
with:
26+
php-version: ${{ matrix.php }}
27+
coverage: none
28+
tools: composer:v2
29+
30+
- name: Install dependencies
31+
run: composer update --no-interaction --no-progress --prefer-dist
32+
33+
- name: Run checks
34+
run: composer check

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/.idea/
2+
/.phpunit.cache/
3+
/.phpunit.result.cache
4+
/composer.lock
5+
/coverage/
6+
/vendor/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 php-n8n
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# PHP n8n Client
2+
3+
A lightweight, strongly typed PHP client for triggering n8n webhooks and tracking workflow executions.
4+
5+
The package is intentionally small today. It focuses on the workflow path most PHP apps need first:
6+
7+
- trigger an n8n webhook from application code;
8+
- read a typed webhook response;
9+
- extract an execution reference when n8n returns one;
10+
- fetch and poll execution status through the n8n API;
11+
- attach lifecycle hooks for logging, metrics, persistence, tracing, or framework integration.
12+
13+
## Requirements
14+
15+
- PHP 8.2 or newer
16+
- A PSR-18 HTTP client
17+
- PSR-17 request and stream factories
18+
- An n8n webhook URL
19+
- An n8n API key only when fetching or polling executions
20+
21+
This package depends on PSR interfaces instead of a concrete HTTP client. Install implementations that match your application stack.
22+
URLs accepted by the package are PSR-7 `UriInterface` instances; create them with your PSR-17 URI factory or your framework's PSR-7 URI implementation.
23+
24+
## Installation
25+
26+
```bash
27+
composer require php-n8n/client guzzlehttp/guzzle nyholm/psr7
28+
```
29+
30+
## Quick Start
31+
32+
```php
33+
<?php
34+
35+
declare(strict_types=1);
36+
37+
use GuzzleHttp\Client as GuzzleClient;
38+
use Nyholm\Psr7\Factory\Psr17Factory;
39+
use PhpN8n\Client\Config\ApiConfig;
40+
use PhpN8n\Client\N8nClient;
41+
use PhpN8n\Client\Webhooks\Webhook;
42+
use PhpN8n\Client\Webhooks\WebhookRequest;
43+
44+
$httpClient = new GuzzleClient();
45+
$psr17Factory = new Psr17Factory();
46+
47+
$client = new N8nClient(
48+
httpClient: $httpClient,
49+
requestFactory: $psr17Factory,
50+
streamFactory: $psr17Factory,
51+
apiConfig: new ApiConfig(
52+
apiUri: $psr17Factory->createUri('https://n8n.example.com/api/v1'),
53+
apiKey: $_ENV['N8N_API_KEY'] ?? null,
54+
),
55+
);
56+
57+
$webhook = Webhook::fromUri(
58+
$psr17Factory->createUri('https://n8n.example.com/webhook/order-created')
59+
);
60+
61+
$response = $client->workflow()->trigger(
62+
$webhook,
63+
WebhookRequest::create([
64+
'orderId' => 'ORD-1001',
65+
'total' => 129.50,
66+
]),
67+
);
68+
69+
$executionReference = $response->executionReference();
70+
71+
if ($executionReference !== null) {
72+
$result = $client->execution($executionReference)->wait();
73+
74+
if ($result->status()->isSuccessful()) {
75+
// Continue after n8n finishes successfully.
76+
}
77+
}
78+
```
79+
80+
## Triggering Webhooks
81+
82+
Create a `Webhook` from the n8n webhook URL, then send an optional `WebhookRequest`.
83+
84+
```php
85+
use PhpN8n\Client\Webhooks\Webhook;
86+
use PhpN8n\Client\Webhooks\WebhookMethod;
87+
use PhpN8n\Client\Webhooks\WebhookRequest;
88+
89+
$webhook = Webhook::fromUri($psr17Factory->createUri('https://n8n.example.com/webhook/sync-customer'))
90+
->withMethod(WebhookMethod::Post)
91+
->withHeader('X-App-Source', 'billing')
92+
->withQueryParameter('tenant', 'acme');
93+
94+
$request = WebhookRequest::create(['customerId' => 123])
95+
->withHeader('X-Request-ID', 'req_123')
96+
->withQueryParameter('dryRun', false);
97+
98+
$response = $client->workflow()->trigger($webhook, $request);
99+
```
100+
101+
Request bodies may be arrays, scalar values, strings, or PSR-7 streams. Non-string, non-stream bodies are JSON encoded and receive `Content-Type: application/json` unless you already set a content type.
102+
103+
## Webhook Responses
104+
105+
`WebhookResponse` exposes:
106+
107+
- `raw()` for the original PSR-7 response;
108+
- `statusCode()` for the HTTP status code;
109+
- `type()` as `Json`, `Text`, or `None`;
110+
- `body()` as decoded JSON, raw text, or `null`;
111+
- `executionReference()` when a response contains `executionId`, `execution_id`, `executionReference.id`, or `execution.id`;
112+
- `status()` when a response contains an n8n execution status.
113+
114+
Invalid JSON returned with a JSON content type throws `InvalidWebhookResponseException`.
115+
116+
## Tracking Executions
117+
118+
Execution tracking uses the n8n API. Configure `ApiConfig` with the n8n API base URI and API key.
119+
120+
```php
121+
use PhpN8n\Client\Config\ExecutionFetchOptions;
122+
use PhpN8n\Client\Config\PollingConfig;
123+
use PhpN8n\Client\Executions\ExecutionReference;
124+
125+
$reference = ExecutionReference::fromId('123');
126+
127+
$result = $client->executions()->get(
128+
$reference,
129+
ExecutionFetchOptions::withData(redactExecutionData: false),
130+
);
131+
132+
$result = $client->executions()->wait(
133+
$reference,
134+
new PollingConfig(
135+
timeoutSeconds: 60,
136+
intervalMilliseconds: 1000,
137+
fetchOptions: ExecutionFetchOptions::withData(),
138+
),
139+
);
140+
```
141+
142+
Execution statuses are normalized to `ExecutionStatus` values: `New`, `Running`, `Success`, `Failed`, `Canceled`, `Waiting`, or `Unknown`.
143+
144+
## Lifecycle Hooks
145+
146+
Hooks are synchronous callbacks that receive a `HookContext`. They are useful for observability and application-specific side effects without coupling the package to a framework.
147+
`HookRegistry` is immutable, so `on()` returns a new registry with the callback registered.
148+
149+
```php
150+
use PhpN8n\Client\Hooks\HookContext;
151+
use PhpN8n\Client\Hooks\HookRegistry;
152+
use PhpN8n\Client\Hooks\LifecycleHook;
153+
154+
$hooks = (new HookRegistry())
155+
->on(LifecycleHook::BeforeWebhookRequest, function (HookContext $context): void {
156+
// Log request metadata or start a trace span.
157+
})
158+
->on(LifecycleHook::ExecutionStatusChanged, function (HookContext $context): void {
159+
// Persist status changes in your application.
160+
})
161+
->on(LifecycleHook::ExceptionThrown, function (HookContext $context): void {
162+
$exception = $context->exception();
163+
// Report the exception to your monitoring system.
164+
});
165+
166+
$client = new N8nClient(
167+
httpClient: $httpClient,
168+
requestFactory: $psr17Factory,
169+
streamFactory: $psr17Factory,
170+
apiConfig: $apiConfig,
171+
hooks: $hooks,
172+
);
173+
```
174+
175+
Available hooks:
176+
177+
- `BeforeWebhookRequest`
178+
- `AfterWebhookResponse`
179+
- `ExecutionReferenceCreated`
180+
- `BeforeExecutionRefresh`
181+
- `AfterExecutionRefresh`
182+
- `BeforePollingStarts`
183+
- `PollingAttempt`
184+
- `ExecutionStatusChanged`
185+
- `ExecutionSucceeded`
186+
- `ExecutionFailed`
187+
- `ExecutionTimedOut`
188+
- `ExceptionThrown`
189+
190+
## Errors
191+
192+
Package exceptions extend `PhpN8n\Client\Exceptions\N8nException`.
193+
194+
- `AuthenticationException` for 401 responses;
195+
- `RequestException` for failed HTTP requests, non-2xx n8n responses, and invalid execution API payloads;
196+
- `InvalidWebhookResponseException` for malformed webhook JSON responses;
197+
- `ExecutionTimeoutException` when polling exceeds the configured timeout.
198+
199+
## Quality Checks
200+
201+
```bash
202+
composer check
203+
```
204+
205+
This runs Composer validation, PHPUnit, PSR-12 checks, and static analysis.
206+
207+
## Current Scope
208+
209+
This is not a full n8n API SDK yet. The current public scope is webhook execution and execution status tracking. The package is designed so additional n8n API areas can be added without replacing the webhook and hook APIs.

composer.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "php-n8n/client",
3+
"description": "A lightweight, strongly typed PHP client for executing and tracking n8n workflows.",
4+
"type": "library",
5+
"license": "MIT",
6+
"require": {
7+
"php": ">=8.2",
8+
"psr/http-client": "^1.0",
9+
"psr/http-factory": "^1.0",
10+
"psr/http-message": "^1.0 || ^2.0"
11+
},
12+
"require-dev": {
13+
"phpstan/phpstan": "^2.1",
14+
"phpunit/phpunit": "^11.5 || ^12.0",
15+
"squizlabs/php_codesniffer": "^3.10"
16+
},
17+
"autoload": {
18+
"psr-4": {
19+
"PhpN8n\\Client\\": "src/"
20+
}
21+
},
22+
"autoload-dev": {
23+
"psr-4": {
24+
"PhpN8n\\Client\\Tests\\": "tests/"
25+
}
26+
},
27+
"scripts": {
28+
"analyse": "phpstan analyse",
29+
"check": [
30+
"composer validate --strict",
31+
"@test",
32+
"@cs",
33+
"@analyse"
34+
],
35+
"cs": "phpcs",
36+
"test": "phpunit"
37+
},
38+
"config": {
39+
"sort-packages": true
40+
}
41+
}

phpcs.xml.dist

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="php-n8n-client">
3+
<description>PSR-12 coding standard for php-n8n/client.</description>
4+
5+
<arg name="extensions" value="php"/>
6+
7+
<file>src</file>
8+
<file>tests</file>
9+
10+
<rule ref="PSR12">
11+
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
12+
</rule>
13+
</ruleset>

phpstan.neon.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
level: 6
3+
paths:
4+
- src

phpunit.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit bootstrap="vendor/autoload.php" colors="true">
3+
<testsuites>
4+
<testsuite name="php-n8n/client">
5+
<directory>tests</directory>
6+
</testsuite>
7+
</testsuites>
8+
<source>
9+
<include>
10+
<directory>src</directory>
11+
</include>
12+
</source>
13+
</phpunit>

src/Config/ApiConfig.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpN8n\Client\Config;
6+
7+
use InvalidArgumentException;
8+
use Psr\Http\Message\UriInterface;
9+
10+
final readonly class ApiConfig
11+
{
12+
public function __construct(
13+
private UriInterface $apiUri,
14+
private ?string $apiKey = null,
15+
) {
16+
if (trim((string) $apiUri) === '') {
17+
throw new InvalidArgumentException('The n8n API URI cannot be empty.');
18+
}
19+
}
20+
21+
public static function fromUri(UriInterface $apiUri, ?string $apiKey = null): self
22+
{
23+
return new self($apiUri, $apiKey);
24+
}
25+
26+
public function uri(): UriInterface
27+
{
28+
return $this->apiUri;
29+
}
30+
31+
public function apiKey(): ?string
32+
{
33+
return $this->apiKey;
34+
}
35+
}

0 commit comments

Comments
 (0)