feat(organizations): add Organizations resource, Page<T>, and list-options pattern#16
Merged
Conversation
Closes #3. Establishes the resource/DTO/pagination shape that #4–#9 will reuse. Code: - `Data\Page<T>` — generic readonly Page with `data`, `nextCursor`, `prevCursor`, `size`. Implements `IteratorAggregate` (yields items) and `JsonSerializable`. - `Data\Organization` — readonly DTO, JSON:API hydration via `::from()` with unknown-key/missing-key throws, dates parsed to `DateTimeImmutable`. - `Data\ListOrganizationsOptions` — readonly DTO, `toQuery()` emits Forge's `page[size]` / `page[cursor]` shape. - `Resources\OrganizationsResource` — extends `Saloon\Http\BaseResource`, exposes `all()` returning `Page<Organization>` and `iterate()` returning a `Generator<Organization>` that follows `meta.next_cursor` until null. - `Resources\OrganizationResource` — `get()` returning a hydrated Organization. - `Requests\Organizations\GetOrganizations` (GET /orgs) and `GetOrganization` (GET /orgs/{slug}). - `Forge::organizations()` and `Forge::organization($slug)` accessors. The default-org property is renamed `defaultOrganization` to avoid collision with the new `organization($slug)` method. Tests (one file per source class, mirroring `src/`, ortto-sdk style): - Per-DTO `tests/Unit/Data/{Page,Organization,ListOrganizationsOptions}Test.php` - Per-Request `tests/Unit/Requests/Organizations/{GetOrganization,GetOrganizations}Test.php` - Per-Resource `tests/Unit/Resources/{Organization,Organizations}ResourceTest.php` - Forge facade tests refactored to use `defaultOrganization` and assert the resource accessor types. - Existing per-Exception tests stay; the giant ForgeTest split into smaller per-source-class files (`Requests/Me/GetMeTest`, `Exceptions/{Validation,RateLimit}ExceptionTest`). Fixtures recorded against the real Forge API and committed under `tests/Fixtures/Saloon/organizations/`. `ForgeFixture` now redacts org slugs in both `attributes.slug` (JSON field pass) and `links.self.href` (regex pass that handles both raw and JSON-escaped slashes), mapping consistently so e.g. `attributes.slug=test-org-1` always lines up with `.../orgs/test-org-1`. 76/76 tests pass at 100% line + type coverage, Pint, Rector, PHPStan max clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fbarrento
added a commit
that referenced
this pull request
May 19, 2026
Captures the four spec mismatches surfaced while building the Servers slice (#18 + #19) plus the open-strings-not-enums decision from the Providers slice (#16 + #17). Append future findings here so the next debugging session can find the answer without re-running live-fire investigations. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3.
Summary
First paginated resource. Establishes the
Page<T>+List{Resource}Options+ SaloonBaseResourcepatterns that every subsequent resource (#4–#9) will reuse.Generic infrastructure
Data\Page<T>— generic readonly page withdata,nextCursor,prevCursor,size. ImplementsIteratorAggregate(yields items) andJsonSerializable.hasMore()reflectsnextCursor !== null.Data\Organization— readonly DTO, hydrated from a JSON:API resource object viaOrganization::from(). Throws on missing required keys; dates parsed toDateTimeImmutable. ImplementsJsonSerializable.Data\ListOrganizationsOptions— readonly DTO withsize+cursor.toQuery()emits Forge'spage[size]/page[cursor]shape. Spec doesn't definesortorfilter[]on/orgs, so the DTO matches.Saloon layer
Resources\OrganizationsResource(extendsSaloon\Http\BaseResource) —all(?Options): Page<Organization>for one HTTP call;iterate(?Options): Generator<Organization>for lazy auto-pagination followingmeta.next_cursoruntil null.Resources\OrganizationResource—get(): Organizationfor a single org by slug.Requests\Organizations\GetOrganizations(GET/orgs) andGetOrganization(GET/orgs/{slug}).Forge facade
$forge->organizations()→OrganizationsResource$forge->organization($slug)→OrganizationResourcedefaultOrganizationto free theorganization()method name. README + Forge tests updated.Tests (ortto-sdk style)
One test file per source class, mirroring
src/, all undertests/Unit/:Data/{Page,Organization,ListOrganizationsOptions}Test.phpRequests/Organizations/{GetOrganization,GetOrganizations}Test.phpResources/{Organization,Organizations}ResourceTest.phpForgeTest.phpfrom the previous PR is split into smaller per-source-class files:tests/Unit/ForgeTest.php(constructor + factories + accessor types, ~16 tests)tests/Unit/Requests/Me/GetMeTest.php(5 happy-path + 13 error-mapping)tests/Unit/Exceptions/ValidationExceptionTest.php(3 tests)tests/Unit/Exceptions/RateLimitExceptionTest.php(2 tests)Fixtures
Recorded against the real Forge API (no hand-crafted bodies), committed under
tests/Fixtures/Saloon/organizations/{list,get}.json.ForgeFixturenow redacts org slugs in bothattributes.slug(JSON-key pass) andlinks.self.hrefURLs (regex pass that handles both/and JSON-escaped\/), mapping consistently soattributes.slug=test-org-1always aligns with.../orgs/test-org-1.Notes
iterate()pagination control-flow test uses a sequence ofMockResponse::make()(the only deterministic way to test cursor-following without making the test depend on the user's exact org count). All shape assertions still go through the real recorded fixture.Test plan
composer testgreen locally (Pint, Rector, PHPStan max, Pest at--exactly=100, 76/76 tests)P{8.4,8.5}× {ubuntu, macos, windows} × {prefer-lowest, prefer-stable}🤖 Generated with Claude Code