Documenting your API (Moonlight)


Who this is for

Northwind’s frontend team needs /docs and an OpenAPI file they can trust. You have DeskFlow services with *Action methods and want to document every { service, action } pair without maintaining a separate spec by hand.

What you will learn

  • @moonlight-* PHPDoc tags for task, member, and project actions
  • Generating docs/api/openapi.json with php pionia api:docs
  • Enabling Scalar UI at http://127.0.0.1:8000/docs in development

Before you start

Before you start
  • Services with *Action methods registered on MainSwitch
  • DeskFlow running locally on port 8000

How it works

Moonlight docs scan your services/ classes at boot and when you run api:docs. Each action becomes one OpenAPI operation grouped by service tag — clients still POST to a single /api/v1/ URL.

What Moonlight docs are

Pionia’s HTTP API is not one OpenAPI path per REST resource. Clients POST to a versioned URL (e.g. /api/v1/) with a JSON body:

{
  "service": "task",
  "action": "list",
  "project_id": 1
}

Moonlight documentation describes every { service, action } pair your app exposes: parameters, auth, examples, and the response envelope. The framework scans your service classes and produces:

OutputCommandTypical path
OpenAPI 3.1 specphp pionia api:docsdocs/api/openapi.json
Markdown indexsamedocs/api/index.md
Scalar HTML UIphp pionia api:docs --uidocs/api/index.html
Live browser UIruntime/docs (when enabled)
JSON catalogphp pionia api:catalogstdout or /api/v1/__catalog

Moonlight docs describe your application API, not Pionia framework internals (those use phpDocumentor in the core repo).

Quick start

  1. Add @moonlight-* tags to a service class and its action methods (see below).
  2. Generate committed docs:
php pionia api:docs --ui
  1. In development, open interactive docs:
php pionia serve
open http://127.0.0.1:8000/docs
  1. In CI, fail on drift:
php pionia api:docs --check
# or: composer document:api:check   # in PioniaCore monorepo

Document a service (class level)

Put Moonlight tags on the service class docblock. They apply to every action unless overridden on the method.

<?php

namespace Application\Services;

use Pionia\Http\Services\Service;

/**
 * DeskFlow tasks for Northwind Studio.
 *
 * @moonlight-service task
 * @moonlight-version v1
 * @moonlight-auth partial
 */
class TaskService extends Service
{
    // actions …
}
TagRequiredPurpose
@moonlight-serviceRecommendedService alias in requests ("service": "auth"). Defaults to the switch registry key if omitted.
@moonlight-versionOptionalAPI version label in docs (usually v1).
@moonlight-authOptionalDefault auth for the service: none, optional, partial, or required.
@moonlight-tableOptionalPrimary table for generic CRUD actions.

The service alias must match how the service is registered on your switch (registerServices()), e.g. auth, category, mail.

Document an action (method level)

Each API action is a *Action method on the service. Document it in the method docblock:

use Pionia\Collections\Arrayable;
use Pionia\Http\Response\ApiResponse;

/**
 * List auth records with optional filters.
 *
 * @moonlight-action list_auth
 * @moonlight-summary Returns paginated auth rows
 * @moonlight-auth none
 * @moonlight-perm list_auth
 * @moonlight-param int page Page number, default 1
 * @moonlight-param int limit Page size, default 20
 * @moonlight-return object items array of records, total int count
 * @moonlight-example {"service":"auth","action":"list_auth","page":1,"limit":20}
 */
protected function listAuthAction(Arrayable $data): ApiResponse
{
    // …
}

Tag reference (actions)

TagRequiredPurpose
@moonlight-actionRecommendedThe action string clients send in JSON.
@moonlight-summaryRecommendedOne-line description in OpenAPI and /docs.
@moonlight-authOptionalnone, optional, or required for this action.
@moonlight-permOptionalPermission slug (repeat tag for multiple).
@moonlight-paramOptionaltype name Description — documents a request body field.
@moonlight-returnOptionalShape of returnData in the response envelope.
@moonlight-exampleOptionalFull example JSON payload (include service and action).
@moonlight-deprecatedOptionalMark action deprecated in generated docs.

Parameter syntax: @moonlight-param type name Description

Examples:

 * @moonlight-param string email Recipient address
 * @moonlight-param int id Row id
 * @moonlight-param object data Row payload (name, optional id)

PHP 8 attributes (alternative)

Instead of (or in addition to) PHPDoc, you can use the MoonlightAction attribute:

use Pionia\Documentation\Attributes\MoonlightAction;

#[MoonlightAction(
    name: 'list_auth',
    summary: 'Returns paginated auth rows',
    auth: 'none',
    permissions: ['list_auth'],
)]
protected function listAuthAction(Arrayable $data): ApiResponse
{
    // …
}

PHPDoc tags and attributes merge; attributes win when both define the same field.

What gets inferred without tags

The doc collector still picks up actions when tags are missing:

ItemInference rule
Action methodsPublic/protected methods ending in Action, plus generic service macros.
Action nameSnake_case of the method name without the Action suffix (listAuthActionlist_auth). For generated services, actions follow {verb}_{service}_action (e.g. delete_todo_action).
AuthMerged from $serviceRequiresAuth, $actionsRequiringAuth, $actionPermissions on the service.

Inference is enough for an internal catalog, but you should add @moonlight-summary and @moonlight-example before sharing docs with frontend teams or external consumers.

Response envelope

Every action returns the same JSON shape (see Actions):

{
  "returnCode": 0,
  "returnMessage": "OK",
  "returnData": {},
  "extraData": null
}

returnCode: 0 means success. Document the payload inside returnData with @moonlight-return.

Generate docs (api:docs)

php pionia api:docs
php pionia api:docs --ui
php pionia api:docs --format=openapi,markdown --output=docs/api
php pionia api:docs --check
OptionPurpose
--format=openapi,markdown,htmlOutput formats (comma-separated).
--output=docs/apiDestination directory (default: docs/api/ under app root).
--uiAlso write index.html (Scalar UI bundle).
--checkExit with error if generated files differ from disk (CI).

Aliases: make:api-docs, docs:api

php pionia api:catalog
php pionia api:catalog --json   # pretty-printed

Same source as api:docs, useful for scripts and debugging.

Runtime docs (/docs)

When DEBUG=true or docs are explicitly enabled, the app serves live documentation from registered services at boot (not only committed files).

URLContent
/docsScalar interactive UI
/docs/openapi.jsonOpenAPI spec
/api/v1/__catalogJSON action catalog (debug gate)

Browsing /docs

The Scalar sidebar is organized by service, then action:

auth
  └ list_auth
  └ create_auth
todo
  └ list_todo_action
  └ delete_todo_action

Each action page shows:

  • Summary, auth, and permissions
  • Dispatch table with the real runtime URL (POST /api/v1/)
  • Inline request body (parameters + example)
  • Inline response envelope (returnCode, returnMessage, returnData)

There is no separate Models or Overview section — request and response shapes live on each action page. OpenAPI may list documentation paths like /api/v1/moonlight/todo/delete_todo_action; those are for navigation only. Clients always call the versioned base path with { "service", "action", ...params }.

Enable in staging/production

environment/settings.ini:

[docs]
ENABLED = true

Or .env:

DOCS_ENABLED=true

Optional token (recommended when DEBUG=false):

[docs]
ENABLED = true
TOKEN = your-secret
DOCS_TOKEN=your-secret

Access with ?token=your-secret or header X-Docs-Token: your-secret.

See also Developer stats/stats uses a separate STATS_TOKEN.

OpenAPI shape

Moonlight OpenAPI is optimized for browsing, not for pretending each action is a separate REST URL.

LayerWhat it is
RuntimeOne POST per API version (e.g. /api/v1/) with JSON { "service", "action", ...params }
OpenAPI / ScalarOne documented operation per action, grouped by service tag in the sidebar
Doc pathsVirtual paths such as /api/v1/moonlight/todo/delete_todo_action — reference only
x-pionia-dispatchOn each operation: real URL, service, and action for copy-paste integration

Request and response schemas are inlined on each action operation (not listed in a global Models catalog). Scalar is configured with hideModels: true.

Commit docs/api/openapi.json if your team consumes it in CI or client generators; regenerate after changing @moonlight-* tags.

Full example service

<?php

namespace Application\Services;

use Pionia\Collections\Arrayable;
use Pionia\Http\Response\ApiResponse;
use Pionia\Http\Services\Service;

/**
 * Company categories.
 *
 * @moonlight-service category
 * @moonlight-version v1
 * @moonlight-table company
 */
class CategoryService extends Service
{
    /**
     * @moonlight-action list
     * @moonlight-summary List all companies
     * @moonlight-example {"service":"category","action":"list"}
     */
    protected function listAction(Arrayable $data): ApiResponse
    {
        return response(0, 'OK', ['items' => []]);
    }

    /**
     * @moonlight-action update
     * @moonlight-summary Update a company by id
     * @moonlight-param int id Row id
     * @moonlight-param string name New name
     * @moonlight-example {"service":"category","action":"update","id":1,"name":"Acme"}
     */
    protected function updateAction(Arrayable $data): ApiResponse
    {
        return response(0, 'Updated', ['id' => $data->get('id')]);
    }
}

Register category on your switch, run php pionia api:docs --ui, then open /docs to verify.

Checklist for new actions

  • Service has @moonlight-service (or relies on registry alias).
  • Each public API method has @moonlight-action (or follows *Action naming).
  • @moonlight-summary describes what the action does in one line.
  • @moonlight-example shows a copy-paste JSON body with service and action.
  • Params documented with @moonlight-param when non-obvious.
  • @moonlight-auth set when auth differs from service default.
  • Run php pionia api:docs --check in CI after changing tags.

Common mistakes

  • Documenting REST URLs per action — Moonlight clients always POST to /api/v1/; OpenAPI paths are for browsing only.
  • Skipping @moonlight-example — frontend teams copy-paste from examples; include "service" and "action" keys.
  • Mismatching registry alias@moonlight-service task must match MainSwitch::registerServices() exactly.
  • Leaving /docs open in staging — set DOCS_TOKEN when DEBUG=false.

What’s next

Actions

Write the methods you document.

Moonlight security

@moonlight-auth and switch auth.

Commands

Full api:docs / api:catalog reference.