fradeet / hypothesis-is-sdk
Requires
- php: ^8.4
- saloonphp/pagination-plugin: ^2.3
- saloonphp/saloon: ^4.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- pestphp/pest: ^5.1
- phpstan/phpstan: ^2.2
- squizlabs/php_codesniffer: ^4.0
README
A vibe-coding project.
A PHP SDK for the Hypothesis API, built on Saloon 4.x. It organizes annotation, group, membership, profile, and user endpoints into resources and uses typed payloads, queries, and DTOs for requests and responses.
For endpoint-specific examples and API behavior, see the complete usage guide.
Requirements
- PHP 8.4 or later
- Composer
- A Hypothesis API token or authority client credentials, depending on the endpoint
Installation
composer require fradeet/hypothesis-is-sdk
Creating a Client
Personal API Token
Use a Bearer token for most operations performed as the current user:
<?php use Fradeet\HypothesisIsSdk\Hypothesis; $hypothesis = Hypothesis::withToken($_ENV['HYPOTHESIS_TOKEN']);
Authority Client Credentials
Authority-level endpoints, such as user management, use HTTP Basic Authentication:
$hypothesis = Hypothesis::withClientCredentials( clientId: $_ENV['HYPOTHESIS_CLIENT_ID'], clientSecret: $_ENV['HYPOTHESIS_CLIENT_SECRET'], );
To send requests on behalf of a user, provide an X-Forwarded-User value:
$hypothesis = Hypothesis::withClientCredentials( clientId: $_ENV['HYPOTHESIS_CLIENT_ID'], clientSecret: $_ENV['HYPOTHESIS_CLIENT_SECRET'], forwardedUser: 'acct:alice@example.com', );
Anonymous Access and Self-Hosted Instances
// Anonymous access to public endpoints $hypothesis = new Hypothesis(); // Self-hosted Hypothesis instance $hypothesis = Hypothesis::withToken( token: $_ENV['HYPOTHESIS_TOKEN'], baseUrl: 'https://hypothesis.example.com/api', );
The client connects to https://hypothes.is/api by default and automatically sends the Hypothesis v1 Accept header.
Quick Start
Create and Retrieve an Annotation
use Fradeet\HypothesisIsSdk\Payloads\CreateAnnotation; $annotation = $hypothesis->annotations()->create(new CreateAnnotation( uri: 'https://example.com/article', text: 'This is an annotation', tags: ['php', 'saloon'], group: 'group-id', )); echo $annotation->id; echo $annotation->text; $sameAnnotation = $hypothesis->annotations()->get($annotation->id);
The returned value is a Fradeet\HypothesisIsSdk\Data\Annotation. Date fields are converted to DateTimeImmutable instances.
Search Annotations
use Fradeet\HypothesisIsSdk\Enums\SearchOrder; use Fradeet\HypothesisIsSdk\Enums\SearchSort; use Fradeet\HypothesisIsSdk\Queries\SearchAnnotations; $result = $hypothesis->annotations()->search(new SearchAnnotations( limit: 50, sort: SearchSort::Updated, order: SearchOrder::Descending, uri: 'https://example.com/article', groups: ['group-a', 'group-b'], tags: ['php', 'sdk'], text: 'keyword', )); echo $result->total; foreach ($result->rows as $annotation) { echo $annotation->text; }
SearchAnnotations also supports offset, searchAfter, url, uriParts, wildcardUri, user, tag, any, quote, and references.
Update, Delete, and Moderate Annotations
use Fradeet\HypothesisIsSdk\Enums\ModerationStatus; use Fradeet\HypothesisIsSdk\Payloads\UpdateAnnotation; $updated = $hypothesis->annotations()->update( id: $annotation->id, annotation: new UpdateAnnotation( text: 'Updated content', tags: ['updated'], ), ); $hypothesis->annotations()->flag($annotation->id); $hypothesis->annotations()->hide($annotation->id); $hypothesis->annotations()->show($annotation->id); $moderated = $hypothesis->annotations()->moderate( id: $annotation->id, status: ModerationStatus::Approved, ); $receipt = $hypothesis->annotations()->delete($annotation->id); echo $receipt->deleted ? 'deleted' : 'not deleted';
UpdateAnnotation only sends fields that were explicitly provided. Values such as text: '' and tags: [] are preserved, allowing existing values to be cleared.
Groups
use Fradeet\HypothesisIsSdk\Payloads\CreateGroup; use Fradeet\HypothesisIsSdk\Payloads\UpdateGroup; use Fradeet\HypothesisIsSdk\Queries\ListGroups; $groups = $hypothesis->groups()->all(new ListGroups( authority: 'example.com', documentUri: 'https://example.com/article', expand: ['organization', 'scopes'], )); $group = $hypothesis->groups()->create(new CreateGroup( name: 'SDK Group', description: 'Created through the SDK', )); $group = $hypothesis->groups()->get( id: $group->id, expand: ['organization', 'scopes'], ); $group = $hypothesis->groups()->update( id: $group->id, group: new UpdateGroup(description: 'Updated description'), );
Retrieve annotations from a group:
use Fradeet\HypothesisIsSdk\Enums\ModerationStatus; use Fradeet\HypothesisIsSdk\Queries\ListGroupAnnotations; $page = $hypothesis->groups()->annotations( id: $group->id, query: new ListGroupAnnotations( pageSize: 50, moderationStatus: ModerationStatus::Approved, ), ); foreach ($page->data as $annotation) { echo $annotation->id; }
To retrieve the next page, pass its timestamp cursor as pageAfter: new DateTimeImmutable(...).
Group Memberships
use Fradeet\HypothesisIsSdk\Payloads\ChangeMembership; use Fradeet\HypothesisIsSdk\Queries\ListMemberships; $membership = $hypothesis->memberships()->add( groupId: $group->id, userId: 'acct:alice@example.com', membership: new ChangeMembership(roles: ['member']), ); $membership = $hypothesis->memberships()->get( groupId: $group->id, userId: 'acct:alice@example.com', ); $membership = $hypothesis->memberships()->update( groupId: $group->id, userId: 'acct:alice@example.com', membership: new ChangeMembership(roles: ['moderator']), ); $page = $hypothesis->memberships()->all( groupId: $group->id, query: new ListMemberships(pageNumber: 1, pageSize: 25), ); $hypothesis->memberships()->remove( groupId: $group->id, userId: 'acct:alice@example.com', );
Without pagination parameters, all() supports the legacy Hypothesis response and returns list<Membership>. When pageNumber is provided, it returns a MembershipPage; memberships are available through $page->data, and pagination metadata through $page->meta.
Profile and User Management
$profile = $hypothesis->profile()->get(); $myGroups = $hypothesis->profile()->groups();
User management usually requires authority client credentials:
use Fradeet\HypothesisIsSdk\Payloads\CreateUser; use Fradeet\HypothesisIsSdk\Payloads\UpdateUser; $user = $hypothesis->users()->create(new CreateUser( authority: 'example.com', username: 'alice', email: 'alice@example.com', displayName: 'Alice', password: 'a-secure-password', )); $user = $hypothesis->users()->get('acct:alice@example.com'); $user = $hypothesis->users()->update( username: 'alice', user: new UpdateUser(displayName: 'Alice Smith'), );
Error Handling
The client uses Saloon's AlwaysThrowOnErrors trait, so non-2xx responses throw a HypothesisApiException:
use Fradeet\HypothesisIsSdk\Exceptions\HypothesisApiException; try { $annotation = $hypothesis->annotations()->get('annotation-id'); } catch (HypothesisApiException $exception) { echo $exception->statusCode; echo $exception->reason ?? 'Hypothesis did not provide an error reason'; if ($exception->mayBePermissionDenied) { // Hypothesis v1 may use 404 to hide inaccessible private resources. } $response = $exception->getResponse(); }
Using Saloon Directly
Hypothesis is a Saloon Connector. In addition to the resource convenience methods, you can send the SDK's request classes directly when you need access to the raw Response:
use Fradeet\HypothesisIsSdk\Requests\Annotations\GetAnnotationRequest; $response = $hypothesis->send(new GetAnnotationRequest('annotation-id')); echo $response->status(); echo $response->body(); $annotation = $response->dtoOrFail();
You can therefore continue to use Saloon features such as authenticators, request middleware, retries, concurrency, logging, and debugging.
Mocking the API in Tests
use Fradeet\HypothesisIsSdk\Hypothesis; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; $mockClient = new MockClient([ MockResponse::make([ 'total' => 0, 'rows' => [], ]), ]); $hypothesis = Hypothesis::withToken('test-token') ->withMockClient($mockClient); $result = $hypothesis->annotations()->search();
API Overview
| Resource | Methods |
|---|---|
$hypothesis->annotations() |
create, search, get, update, delete, flag, hide, show, moderate |
$hypothesis->groups() |
all, create, get, update, annotations |
$hypothesis->memberships() |
all, get, add, update, remove |
$hypothesis->profile() |
get, groups |
$hypothesis->users() |
create, get, update |
PHP named arguments are recommended for payload and query constructors. List and search methods with optional queries provide defaults, so they can also be called directly:
$result = $hypothesis->annotations()->search(); $groups = $hypothesis->groups()->all(); $memberships = $hypothesis->memberships()->all('group-id');
Development
composer install vendor/bin/pest vendor/bin/phpstan analyse --no-progress