themusicdev / cakephp-eventbrite
CakePHP 5 plugin for Eventbrite: OAuth connections with encrypted token storage, webhook intake and a per-token API client
Package info
github.com/TheMusicDev/cakephp-eventbrite
Type:cakephp-plugin
pkg:composer/themusicdev/cakephp-eventbrite
Requires
- php: >=8.2
- cakephp/cakephp: ^5.2
- themusicdev/eventbrite: ^0.2
Requires (Dev)
- cakephp/cakephp-codesniffer: ^5.3
- cakephp/migrations: ^5.0
- captainhook/captainhook: ^5.29
- captainhook/plugin-composer: ^5.3
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11.5.3 || ^12.1.3 || ^13.0
- ramsey/conventional-commits: ^1.7
Suggests
- cakephp/migrations: Needed to create the connections table (bin/cake migrations migrate -p TheMusicDev/CakeEventbrite)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-07 16:49:28 UTC
README
Eventbrite for CakePHP 5 apps: connect Eventbrite accounts through OAuth, keep their tokens encrypted, receive webhooks as CakePHP events, and get an API client for any connection. The plugin owns connections, routes and webhook intake; your app owns the screens and decides what to do with the data.
It is built on themusicdev/eventbrite, the framework-agnostic API client.
Requires PHP 8.2+ and CakePHP 5.2+.
Why this plugin
The API client knows nothing about CakePHP, so every app would otherwise rebuild the same parts: the OAuth routes, token
storage, the webhook receiver, and the wiring to CakePHP's HTTP client, cache and logger. This plugin is that shared
layer, and it gets the security-sensitive parts right once. Design and decisions:
docs/eventbrite-plugin-design.md.
Install
composer require themusicdev/cakephp-eventbrite bin/cake plugin load TheMusicDev/CakeEventbrite bin/cake migrations migrate -p TheMusicDev/CakeEventbrite
This creates the eventbrite_connections table in the app's default connection (the plugin owns the schema, the app
owns the storage engine).
Use
Connecting accounts
The plugin ships the routes /eventbrite/oauth/login and /eventbrite/oauth/callback — loading the plugin mounts
them. Two things must be configured or the routes refuse every request (connecting fails closed):
CakeEventbrite.ownerResolver— afn (ServerRequest $request): ?stringreturning the owner key to connect (for example'user:' . $user->id). Set it from your authentication context; when it returns null, no connection happens. Plugin controllers don't inherit yourAppController, so its authentication does not protect these routes.CakeEventbrite.encryptionKey— the key tokens are encrypted with.
The callback redirects to CakeEventbrite.redirects.connected (?error= + a code on failure), and stores the row only
after the new token has been proven against /users/me/.
Using a connection's API access
use TheMusicDev\CakeEventbrite\EventbriteClientFactory; // Inject the factory; there is one client per token, not one per app. $client = $factory->forConnection($connection); foreach ($client->organizationEvents($orgId) as $event) { // ... }
forToken($token) builds a client for an ad-hoc token; forPrivateToken() for apps that skip OAuth entirely
(CakeEventbrite.privateToken, i.e. EVENTBRITE_TOKEN). It wires Cake\Http\Client (PSR-18), Cake's PSR-17 factories,
an optional PSR-16 cache and PSR-3 logger. No 429 retries outside the CLI, because the client's backoff sleeps — a
webhook failure answers 502 instead, and Eventbrite retries.
Receiving webhooks (no registration work)
Deliveries arrive at POST /eventbrite/webhook/{secret}, where {secret} is the connection's private path secret —
Eventbrite's deliveries carry no signature, so the unpublished per-connection URL is the authentication. The controller
fetches the pointed object through fetchUrl() (never trusting the payload, always bypassing the cache) and fires:
EventManager::instance()->on('CakeEventbrite.Webhook.received', function (EventInterface $event): void { // $event->getData(): connection, kind ('event', 'order', 'attendee', ...), apiUrl, object // Store the object here. Idempotent: non-2xx answers make Eventbrite retry. });
Non-2xx makes Eventbrite retry every ten minutes (ten attempts); an exception in a listener counts as failure (500). The plugin never returns 410, which would unsubscribe the endpoint.
Registering webhooks on an organization
use TheMusicDev\CakeEventbrite\Service\WebhookRegistrar; $registrar = new WebhookRegistrar($factory); $registrar->ensure($connection, $orgId); // idempotent; one webhook per action $registrar->endpointUrl($connection); // the URL Eventbrite should POST to $registrar->disable($connection, $orgId); // remove this connection's webhooks
The endpoint URL comes from CakeEventbrite.webhook.baseUrl plus the connection's secret. Default actions are
attendee.updated, barcode.* (check-in), order.* and event.* — overridable in config.
Commands
bin/cake eventbrite me # who the token belongs to bin/cake eventbrite events <orgId> [--status=live] [--paginate] bin/cake eventbrite event <id> [--attendees] bin/cake eventbrite webhooks <orgId> # list what is registered bin/cake eventbrite webhooks <orgId> --owner=<key> [--ensure] [--disable]
--owner=key operates on a stored connection; without it the commands use the private token. Commands never print
tokens. See examples/ for copyable wiring.
Configure (host config/app.php)
Host values always win over the plugin's defaults. Every knob below can also come from the environment; .env.example
in this repo lists the variable names.
| Key | Purpose |
|---|---|
CakeEventbrite.oauth.clientId / .clientSecret / .redirectUri |
From the API key page. The redirect URI must match exactly. |
CakeEventbrite.encryptionKey |
Any non-empty string; required, with no fallback (EVENTBRITE_ENCRYPTION_KEY). Rotate it and stored tokens become unreadable — those accounts reconnect. |
CakeEventbrite.ownerResolver |
fn (ServerRequest): ?string. Says who may connect, and as which owner. Unset or null refuses every connection. |
CakeEventbrite.redirects.connected / .failed |
Where the browser goes after the OAuth callback. |
CakeEventbrite.webhook.baseUrl / .actions |
Public base URL for webhook endpoints (tunnels and proxies), and which actions to register (EVENTBRITE_WEBHOOK_BASE_URL). |
CakeEventbrite.privateToken |
Optional, for single-account apps that skip OAuth. |
CakeEventbrite.cache |
Optional PSR-16 cache config name for GET responses. Off by default: responses contain attendee personal data, so hosts choose where those are stored. |
CakeEventbrite.logger |
Optional PSR-3 channel name (Cake\Log\Log::engine()) for 429 backoff warnings. |
Gotchas
- Exempt the webhook route from CSRF. Your app owns the CSRF middleware, and Eventbrite's POSTs carry no token. Skip
the check for paths starting with
/eventbrite/webhook/. Apply the same exemption to any global authentication middleware. - The redirect back from Eventbrite needs the session cookie. Use
SameSite=Lax, which is CakePHP's default, notStrict. ASession::renew()between the redirect and the callback, such as logging in mid-flow, makes the OAuth state fail (state is bound to the session id and expires after 10 minutes). - Webhooks are delivered more than once. A non-2xx response makes Eventbrite retry, so listeners must be idempotent. An exception in a listener counts as a failed delivery.
- Organization id ≠ user id.
eventbrite_user_idon a connection is the user from/users/me/; webhook registration needs an organization id. An account without an organization returns 404s using the user id instead — create one in the Eventbrite dashboard. - Plugin controllers don't inherit your
AppController. Authentication set up there doesn't protect plugin routes, which is whyownerResolverexists.
Development
docker compose up -d dbtest
composer install
composer check # tests, then coding standard, then static analysis
vendor/bin/phpunit --filter=SomeTest