Services
Who this is for
You scaffolded DeskFlow and need to create and register service classes — the PHP home for task, member, and project business logic.
What you will learn
- How
make:servicescaffoldsTaskServiceunderservices/ - Registering aliases on
MainSwitchso JSON"service": "task"resolves - Choosing Basic vs Generic services for Northwind tables
Before you start
- Completed DeskFlow tutorial Step 1 or read Moonlight overview
- A running DeskFlow app on port 8000
How it works
What is a service?
Services are plain PHP classes under services/ that extend Pionia\Http\Services\Service. Each action is a public method named somethingAction() — Pionia maps "action": "list" to listAction().
In DeskFlow, Northwind Studio uses three services:
| Registered alias | Class | Role |
|---|---|---|
task | TaskService | Tasks for client projects |
member | MemberService | Login and profiles |
project | ProjectService | Group tasks by client |
Clients POST lowercase keys:
{ "service": "task", "action": "list", "project_id": 1 }Create a service
Generate a scaffold from your app root:
php pionia make:service taskChoose Basic for hand-written actions, or Generic for CRUD over a Porm table — see Generic services.
The CLI creates services/TaskService.php:
namespace Application\Services;
use Pionia\Http\Services\Service;
use Pionia\Collections\Arrayable;
class TaskService extends Service
{
public function listAction(Arrayable $data)
{
return response(0, 'OK', ['tasks' => []]);
}
}Register the alias on your switch (usually Application\Switches\MainSwitch):
protected function registerServices(): array
{
return [
'task' => TaskService::class,
];
}curl -s -X POST http://127.0.0.1:8000/api/v1/ \
-H "Content-Type: application/json" \
-d '{"service":"task","action":"list"}'Expected: HTTP 200 with "returnCode": 0 and a tasks array in returnData.
When you run make:service, the CLI offers two paths:
Remember generic services target a base table.
Therefore, you shall be asked the database table name you want to target. This is required.
However, starting from version 1.1.7, you can target relationships too!
You can read more about this in the Generic Services section.
Service registration
Register services in your switch — usually Application\Switches\MainSwitch:
protected function registerServices(): array
{
return [
'task' => TaskService::class,
'member' => MemberService::class,
'project' => ProjectService::class,
];
}The array keys are the service names in JSON requests. They must be unique within a switch.
Register the same service class in v1 and v2 switches when Northwind ships a breaking API version — see API versioning.
Targeting a service in the request
In the request, target a service with the lowercase service key:
{
"service": "task",
"action": "list"
}For envelopes and HTTP status codes, see Requests and responses.
Actions
You can read more about actions in the actions section.
Service Security
You can mark an entire service as requiring authentication by setting the $serviceRequiresAuth parameter to true.
class TodoService extends Service
{
public bool $serviceRequiresAuth = true; // all actions in this service require authentication.
// your other actions here
}If the flag is set to true, all actions in the service will require authentication. This means that only authenticated users will be able to access the service.
Specific actions
You can also mark specific actions in a service as requiring authentication. Use the $actionsRequiringAuth parameter and add action names of actions that should be reached by authenticated users only.
This, unlike $serviceRequiresAuth, will only protect the actions listed in the array not the entire service.
class TodoService extends Service
{
public bool $actionsRequiringAuth = ['getTodo'];
// your other actions here
}Error Handling
According to Moonlight architecture, all requests should return a 200 Ok status code. This is because the client should
be able to know if the request was successful or not by checking the returnCode in the response body.
All normal responses set this internally and are always returning a 200 status code. By convention and by default, all requests
that are successful return 0 as the returnCode. This implies that the server can define multiple other return codes
for other scenarios.
In Pionia, we have a global exception handler that catches all exceptions thrown anywhere in the code. This is to ensure that the client always gets the same response format.
Uncaught throwables flow through the exception pipeline. Status codes depend on the exception type:
| Exception | HTTP status |
|---|---|
ValidationException | 422 |
ResourceNotFoundException | 404 |
HttpException | As defined on the exception |
| Other | 500 (message hidden in production) |
Throw ValidationException for client input errors; use plain Exception only for unexpected server faults. Prefer rules() or #[Validated] on actions — see Validations.
use Pionia\Collections\Arrayable;
use Pionia\Http\Response\ApiResponse;
use Pionia\Validations\Attributes\Validated;
#[Validated(rules: ['id' => 'required|integer'])]
protected function getTodoAction(Arrayable $data): ApiResponse
{
$this->mustAuthenticate();
$id = $data->get('id');
// rest of action logic
return response(0, 'Todo fetched successfully', $todo);
}Uncaught throwables flow through the exception pipeline — use clear exception messages for clients.
Common mistakes
- Wrong service alias — the JSON key must match
registerServices()exactly (task, notTaskService). - Forgetting to register after
make:service— the CLI creates the class but does not editMainSwitchfor you. - Using
$serviceRequiresAuthwithout JWT configured — set upmember.loginfirst; see Authentication. - Expecting every error to be HTTP 200 — validation uses 422, auth failures 401; see Requests & responses.
What’s next
Actions
Request data, responses, and auth helpers.
Validation
422 when Alex omits task title.
Generic services
CRUD for project rows.