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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
## [3.0.0] — unreleased

### Added
- `RoleRepository` / `RoleDTO` for `/api/v1/roles`, accessible via `$client->role()` — resolve role names to the IDs needed for `UserDTO::$role_ids`; `permission_ids` and `group_ids` are writable on create and update
- PSR-18 / PSR-17 compliant HTTP layer (`RequestHandler`, `RetryAfterMiddleware`)
- Typed DTOs for all 10 resources (`Ticket`, `User`, `Organization`, `Group`, `TicketArticle`, `TicketState`, `TicketPriority`, `Tag`, `TextModule`, `Link`)
- Typed DTOs for all 11 resources (`Ticket`, `User`, `Organization`, `Group`, `Role`, `TicketArticle`, `TicketState`, `TicketPriority`, `Tag`, `TextModule`, `Link`)
- Repository pattern with generator-based pagination (`AbstractRepository`)
- `patch()` method for partial updates via `array` or `TicketUpdateDTO`
- Proper exception hierarchy: `AuthenticationException`, `NotFoundException`, `ValidationException`, `RateLimitException`, `ServerErrorException`, `NetworkException`
Expand Down
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ This library offers three interaction styles. Choose based on your use case:
| **Stateful Resource** | `$client->ticket()->resource($id)->save()` / `destroy()` | Interactive editing — mutate properties step by step, only changes are sent. |
| **Raw HTTP** | `$client->getHandler()->get()`, `delete()`, etc. | Calling endpoints that have no dedicated repository. Escape hatch. |

Repositories are accessed via typed accessors: `$client->ticket()`, `$client->user()`, `$client->organization()`, `$client->group()`, `$client->ticketArticle()`, `$client->ticketState()`, `$client->ticketPriority()`, `$client->tag()`, `$client->textModule()`, `$client->link()`. The underlying `repo()` method is internal.
Repositories are accessed via typed accessors: `$client->ticket()`, `$client->user()`, `$client->organization()`, `$client->group()`, `$client->role()`, `$client->ticketArticle()`, `$client->ticketState()`, `$client->ticketPriority()`, `$client->tag()`, `$client->textModule()`, `$client->link()`. The underlying `repo()` method is internal.

### Connecting

Expand Down Expand Up @@ -215,6 +215,7 @@ All repositories expose a `delete()` method. Repositories implementing `Deletabl
| `TicketArticleRepository` | exception | Zammad API does not allow article deletion |
| `TicketStateRepository` | exception | System resource, read-only |
| `TicketPriorityRepository` | exception | System resource, read-only |
| `RoleRepository` | exception | Zammad exposes no DELETE route for roles; patch `active` to `false` |

```php
$client->ticket()->delete(1);
Expand Down Expand Up @@ -294,6 +295,44 @@ foreach ($repo->all(['object' => 'Ticket', 'o_id' => $ticketId]) as $tag) {
$results = $repo->tagSearch('urg'); // Autocomplete
```

### Roles

Roles bundle permissions. A user carries them via `role_ids`, so the usual task is
resolving a role name to its numeric ID before creating or updating a user:

```php
$roles = [];
foreach ($client->role()->all() as $role) {
$roles[$role->name] = $role->id; // 'Admin' => 1, 'Agent' => 2, 'Customer' => 3
}

$client->user()->create(new UserDTO(
email: 'agent@example.com',
firstname: 'New',
lastname: 'Agent',
role_ids: [$roles['Agent']],
));
```

Permissions and group access come along as IDs, and are writable on create and update:

```php
$client->role()->create(new RoleDTO(
name: 'Supervisor',
permission_ids: [10, 11],
group_ids: [1 => 'full'], // map of group ID to access level, agent roles only
));
```

`all()` and `find()` work with agent, admin and customer tokens. A customer however
only sees `id`, `active`, `permission_ids` and `group_ids` — Zammad replaces `name`
with the placeholder `Role_<id>` — so resolving a role by name needs an agent or admin
token. `search()`, `totalCount()`, `create()` and `patch()` require `admin.role` and
otherwise raise a `ForbiddenException`.

Zammad exposes no DELETE route for roles; deactivate one with
`$client->role()->patch($id, ['active' => false])` instead.

### CSV import

```php
Expand Down Expand Up @@ -530,7 +569,7 @@ $client->ticket()->patch(42, new TicketUpdateDTO(
| `phone` | `?string` | — | |
| `organization_id` | `?int` | — | Primary organization |
| `organization_ids` | `?array` | — | Array of secondary organization IDs |
| `role_ids` | `?array` | — | Array of role IDs (e.g. `[2]` for Agent) |
| `role_ids` | `?array` | — | Array of role IDs (e.g. `[2]` for Agent); resolve by name via `$client->role()` |
| `active` | `?bool` | — | Whether the user account is active |
| `id` | `?int` | — | Server-assigned |
| `created_at` | `?DateTimeImmutable` | — | Read-only |
Expand Down Expand Up @@ -561,6 +600,20 @@ $client->ticket()->patch(42, new TicketUpdateDTO(
| `updated_at` | `?DateTimeImmutable` | — | Read-only |
| `customFields` | `array` | — | |

### RoleDTO

| Field | Type | Required | Notes |
|-------|------|:--------:|-------|
| `name` | `string` | yes | Display label (e.g. `'Agent'`, `'Customer'`); masked as `Role_<id>` for customer tokens |
| `note` | `?string` | — | |
| `active` | `?bool` | — | |
| `default_at_signup` | `?bool` | — | Role assigned to users who sign up themselves |
| `permission_ids` | `?array` | — | IDs of the granted permissions; writable |
| `group_ids` | `?array` | — | Map of group ID to access level (`[1 => 'full']`); agent roles only, writable |
| `id` | `?int` | — | Server-assigned; used in `UserDTO::$role_ids` |
| `created_at` | `?DateTimeImmutable` | — | Read-only |
| `updated_at` | `?DateTimeImmutable` | — | Read-only |

### TicketArticleDTO

| Field | Type | Notes |
Expand Down Expand Up @@ -689,6 +742,9 @@ These require a running Zammad instance and authentication credentials:

\* Either `ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_TOKEN` or `USERNAME`+`PASSWORD` must be set.

The credentials need admin permissions: some suites touch admin-only endpoints
(e.g. `RoleIntegrationTest` calls `totalCount()`, which goes through `/api/v1/roles/search`).

## Migration from v2

See [docs/migration-v3-examples.md](docs/migration-v3-examples.md) for side-by-side code examples.
Expand Down
1 change: 1 addition & 0 deletions docs/migration-v3-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ $client->ticket()->find(1);
$client->user()->find(1);
$client->organization()->find(1);
$client->group()->find(1);
$client->role()->all();
$client->ticketArticle()->getForTicket(1);
$client->ticketState()->all();
$client->ticketPriority()->all();
Expand Down
3 changes: 3 additions & 0 deletions src/Core/Repository/RepositoryRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use ZammadAPIClient\Endpoints\Groups\GroupRepository;
use ZammadAPIClient\Endpoints\Links\LinkDTO;
use ZammadAPIClient\Endpoints\Links\LinkRepository;
use ZammadAPIClient\Endpoints\Roles\RoleDTO;
use ZammadAPIClient\Endpoints\Roles\RoleRepository;
use ZammadAPIClient\Endpoints\Tags\TagDTO;
use ZammadAPIClient\Endpoints\Tags\TagRepository;
use ZammadAPIClient\Endpoints\TextModules\TextModuleDTO;
Expand Down Expand Up @@ -45,6 +47,7 @@ final class RepositoryRegistry
OrganizationRepository::class => ['path' => 'organizations', 'dto' => OrganizationDTO::class],
GroupRepository::class => ['path' => 'groups', 'dto' => GroupDTO::class],
LinkRepository::class => ['path' => 'links', 'dto' => LinkDTO::class],
RoleRepository::class => ['path' => 'roles', 'dto' => RoleDTO::class],
TicketArticleRepository::class => ['path' => 'ticket_articles', 'dto' => TicketArticleDTO::class],
TicketStateRepository::class => ['path' => 'ticket_states', 'dto' => TicketStateDTO::class],
TicketPriorityRepository::class => ['path' => 'ticket_priorities', 'dto' => TicketPriorityDTO::class],
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Traits/RepositoryAccessors.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ZammadAPIClient\Endpoints\Groups\GroupRepository;
use ZammadAPIClient\Endpoints\Links\LinkRepository;
use ZammadAPIClient\Endpoints\Organizations\OrganizationRepository;
use ZammadAPIClient\Endpoints\Roles\RoleRepository;
use ZammadAPIClient\Endpoints\Tags\TagRepository;
use ZammadAPIClient\Endpoints\TextModules\TextModuleRepository;
use ZammadAPIClient\Endpoints\TicketArticles\TicketArticleRepository;
Expand Down Expand Up @@ -43,6 +44,11 @@ public function group(): GroupRepository
return $this->repo(GroupRepository::class);
}

public function role(): RoleRepository
{
return $this->repo(RoleRepository::class);
}

public function ticketArticle(): TicketArticleRepository
{
return $this->repo(TicketArticleRepository::class);
Expand Down
57 changes: 57 additions & 0 deletions src/Endpoints/Roles/RoleDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace ZammadAPIClient\Endpoints\Roles;

use ZammadAPIClient\Core\Contracts\DTOInterface;
use ZammadAPIClient\Core\Traits\HasTimestamps;
use ZammadAPIClient\Core\Traits\HydratesFromArray;
use ZammadAPIClient\Core\Traits\SerializesToArray;

/**
* Represents a Zammad role resource (`/api/v1/roles`).
*
* A role is a named bundle of permissions. The default Zammad installation
* ships with "Admin", "Agent" and "Customer"; administrators can add more.
*
* The `name` field is the display label; Zammad uses the numeric `id` when
* assigning roles to a user via the `role_ids` field on
* {@see \ZammadAPIClient\Endpoints\Users\UserDTO}.
*
* `permission_ids` and `group_ids` are writable on create and update: Zammad
* applies them as associations, so a role can be created with its permissions
* in a single request.
*
* Note that a customer token sees a reduced view of a role — Zammad replaces
* `name` with the placeholder `"Role_<id>"` and omits `note` and
* `default_at_signup`. Resolving a role by name therefore requires an agent or
* admin token.
*
* Timestamp fields (`created_at`, `updated_at`) are provided by
* {@see \ZammadAPIClient\Core\Traits\HasTimestamps}.
*/
final class RoleDTO implements DTOInterface
{
use HasTimestamps;
use HydratesFromArray;
use SerializesToArray;

/**
* @param array<int>|null $permission_ids IDs of the permissions this role grants.
* @param array<int|string, string|array<string>>|null $group_ids Map of group ID to
* access level (e.g. `[1 => 'full', 42 => ['read', 'change']]`). Only
* meaningful for roles that carry the `ticket.agent` permission —
* Zammad clears it for all others.
*/
public function __construct(
public readonly string $name,
public readonly ?string $note = null,
public readonly ?bool $active = null,
public readonly ?bool $default_at_signup = null,
public readonly ?array $permission_ids = null,
public readonly ?array $group_ids = null,
public readonly ?int $id = null,
) {
}
}
45 changes: 45 additions & 0 deletions src/Endpoints/Roles/RoleRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace ZammadAPIClient\Endpoints\Roles;

use ZammadAPIClient\Core\Repository\AbstractRepository;

/**
* Repository for the `/api/v1/roles` endpoint.
*
* Roles bundle permissions in Zammad. Every user carries one or more roles via
* the `role_ids` field on {@see \ZammadAPIClient\Endpoints\Users\UserDTO}; the
* default installation ships with "Admin", "Agent" and "Customer".
*
* The typical use case is resolving a role name to its numeric ID before
* creating or updating a user:
*
* $roles = [];
* foreach ($client->role()->all() as $role) {
* $roles[$role->name] = $role->id;
* }
*
* $client->user()->create(new UserDTO(
* email: 'agent@example.com',
* role_ids: [$roles['Agent']],
* ));
*
* Permissions differ per method. `all()` and `find()` are open to agent,
* admin and customer tokens; a customer however only sees `id`, `active`,
* `permission_ids` and `group_ids`, with `name` replaced by the placeholder
* `"Role_<id>"`. `search()`, `searchList()`, `totalCount()`, `create()` and
* `patch()` require `admin.role` and otherwise raise
* {@see \ZammadAPIClient\Exceptions\ForbiddenException}.
*
* Zammad exposes no DELETE route for roles, so this repository does not
* implement {@see \ZammadAPIClient\Core\Contracts\DeletableInterface}:
* `delete()` throws a `BadMethodCallException`. Deactivate a role by patching
* `active` to `false` instead.
*
* @extends AbstractRepository<RoleDTO>
*/
final class RoleRepository extends AbstractRepository
{
}
Loading
Loading