Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ echo $user->email; // "ada@example.com"
echo $user->id; // "216174"
```

### List organizations

```php
foreach ($forge->organizations()->iterate() as $organization) {
echo $organization->slug.PHP_EOL;
}

// Or grab a single page:
$page = $forge->organizations()->all(new ListOrganizationsOptions(size: 10));
foreach ($page as $organization) {
// ...
}
if ($page->hasMore()) {
$next = $forge->organizations()->all(new ListOrganizationsOptions(cursor: $page->nextCursor));
}
```

### Get one organization

```php
$organization = $forge->organization('acme')->get();

echo $organization->name;
echo $organization->slug;
```

### Testing your own code

The SDK is built on Saloon, so you can fake the Forge API in your own test suite without hitting the network:
Expand Down
31 changes: 31 additions & 0 deletions src/Data/ListOrganizationsOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace PhpDevKits\ForgeSdk\Data;

final readonly class ListOrganizationsOptions
{
public function __construct(
public ?int $size = null,
public ?string $cursor = null,
) {}

/**
* @return array<string, int|string>
*/
public function toQuery(): array
{
$query = [];

if ($this->size !== null) {
$query['page[size]'] = $this->size;
}

if ($this->cursor !== null) {
$query['page[cursor]'] = $this->cursor;
}

return $query;
}
}
89 changes: 89 additions & 0 deletions src/Data/Organization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace PhpDevKits\ForgeSdk\Data;

use DateTimeImmutable;
use InvalidArgumentException;
use JsonSerializable;
use Override;
use Throwable;

final readonly class Organization implements JsonSerializable
{
public function __construct(
public string $id,
public string $name,
public string $slug,
public ?DateTimeImmutable $createdAt,
public ?DateTimeImmutable $updatedAt,
) {}

/**
* @param array<array-key, mixed> $data A JSON:API `OrganizationResource` object
* (the shape under each `data[*]` entry).
*/
public static function from(array $data): self
{
$id = $data['id'] ?? null;
if (! is_string($id)) {
throw new InvalidArgumentException('Organization data is missing the `id` field.');
}

$attributes = $data['attributes'] ?? null;
if (! is_array($attributes)) {
throw new InvalidArgumentException('Organization data is missing the `attributes` object.');
}

$name = $attributes['name'] ?? null;
if (! is_string($name)) {
throw new InvalidArgumentException('Organization `attributes.name` must be a string.');
}

$slug = $attributes['slug'] ?? null;
if (! is_string($slug)) {
throw new InvalidArgumentException('Organization `attributes.slug` must be a string.');
}

return new self(
id: $id,
name: $name,
slug: $slug,
createdAt: self::parseDate($attributes['created_at'] ?? null, 'created_at'),
updatedAt: self::parseDate($attributes['updated_at'] ?? null, 'updated_at'),
);
}

private static function parseDate(mixed $value, string $field): ?DateTimeImmutable
{
if ($value === null) {
return null;
}

if (! is_string($value)) {
throw new InvalidArgumentException(sprintf('Organization `attributes.%s` must be a string or null.', $field));
}

try {
return new DateTimeImmutable($value);
} catch (Throwable $throwable) {
throw new InvalidArgumentException(sprintf('Organization `attributes.%s` is not a valid date-time.', $field), 0, $throwable);
}
}

/**
* @return array{id: string, name: string, slug: string, created_at: ?string, updated_at: ?string}
*/
#[Override]
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'created_at' => $this->createdAt?->format(DATE_ATOM),
'updated_at' => $this->updatedAt?->format(DATE_ATOM),
];
}
}
58 changes: 58 additions & 0 deletions src/Data/Page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace PhpDevKits\ForgeSdk\Data;

use Generator;
use IteratorAggregate;
use JsonSerializable;
use Override;

/**
* @template T
*
* @implements IteratorAggregate<int, T>
*/
final readonly class Page implements IteratorAggregate, JsonSerializable
{
/**
* @param list<T> $data
*/
public function __construct(
public array $data,
public ?string $nextCursor,
public ?string $prevCursor,
public int $size,
) {}

public function hasMore(): bool
{
return $this->nextCursor !== null;
}

/**
* @return Generator<int, T>
*/
#[Override]
public function getIterator(): Generator
{
foreach ($this->data as $key => $item) {
yield $key => $item;
}
}

/**
* @return array{data: list<T>, next_cursor: ?string, prev_cursor: ?string, size: int}
*/
#[Override]
public function jsonSerialize(): array
{
return [
'data' => $this->data,
'next_cursor' => $this->nextCursor,
'prev_cursor' => $this->prevCursor,
'size' => $this->size,
];
}
}
17 changes: 13 additions & 4 deletions src/Forge.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use PhpDevKits\ForgeSdk\Exceptions\UnauthorizedException;
use PhpDevKits\ForgeSdk\Exceptions\ValidationException;
use PhpDevKits\ForgeSdk\Requests\Me\GetMe;
use PhpDevKits\ForgeSdk\Resources\OrganizationResource;
use PhpDevKits\ForgeSdk\Resources\OrganizationsResource;
use RuntimeException;
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Http\Auth\TokenAuthenticator;
Expand All @@ -32,10 +34,7 @@ final class Forge extends Connector
{
use AlwaysThrowOnErrors;

public function __construct(
private readonly string $token,
public readonly ?string $organization = null,
) {}
public function __construct(private readonly string $token, public readonly ?string $defaultOrganization = null) {}

public static function fromConfig(?string $path = null): self
{
Expand Down Expand Up @@ -147,6 +146,16 @@ public function me(): User
return User::from($data);
}

public function organizations(): OrganizationsResource
{
return new OrganizationsResource($this);
}

public function organization(string $slug): OrganizationResource
{
return new OrganizationResource($this, $slug);
}

#[Override]
public function send(
Request $request,
Expand Down
22 changes: 22 additions & 0 deletions src/Requests/Organizations/GetOrganization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace PhpDevKits\ForgeSdk\Requests\Organizations;

use Override;
use Saloon\Enums\Method;
use Saloon\Http\Request;

final class GetOrganization extends Request
{
protected Method $method = Method::GET;

public function __construct(private readonly string $slug) {}

#[Override]
public function resolveEndpoint(): string
{
return '/orgs/'.$this->slug;
}
}
32 changes: 32 additions & 0 deletions src/Requests/Organizations/GetOrganizations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace PhpDevKits\ForgeSdk\Requests\Organizations;

use Override;
use PhpDevKits\ForgeSdk\Data\ListOrganizationsOptions;
use Saloon\Enums\Method;
use Saloon\Http\Request;

final class GetOrganizations extends Request
{
protected Method $method = Method::GET;

public function __construct(private readonly ?ListOrganizationsOptions $options = null) {}

#[Override]
public function resolveEndpoint(): string
{
return '/orgs';
}

/**
* @return array<string, int|string>
*/
#[Override]
protected function defaultQuery(): array
{
return $this->options?->toQuery() ?? [];
}
}
37 changes: 37 additions & 0 deletions src/Resources/OrganizationResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace PhpDevKits\ForgeSdk\Resources;

use PhpDevKits\ForgeSdk\Data\Organization;
use PhpDevKits\ForgeSdk\Requests\Organizations\GetOrganization;
use RuntimeException;
use Saloon\Http\BaseResource;
use Saloon\Http\Connector;

final class OrganizationResource extends BaseResource
{
public function __construct(
Connector $connector,
private readonly string $slug,
) {
parent::__construct($connector);
}

/**
* @throws \Throwable
*/
public function get(): Organization
{
$response = $this->connector->send(new GetOrganization($this->slug));

$data = $response->json('data');

if (! is_array($data)) {
throw new RuntimeException('Forge /orgs/{slug} response did not include a `data` object.');
}

return Organization::from($data);
}
}
Loading
Loading