Documenting your API (Moonlight)

Prerequisites: You have a service with *Action methods registered on a switch. This guide covers documenting those actions for humans and API consumers — not writing the business logic itself.

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": "auth",
  "action": "list_auth",
  "page": 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:8003/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;

/**
 * Authentication — demo CRUD for auth records.
 *
 * @moonlight-service auth
 * @moonlight-version v1
 * @moonlight-auth partial
 */
class AuthService 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).
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)

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 uses one POST path per API version (e.g. /api/v1/) with a oneOf schema per {service}.{action} — not separate REST paths per endpoint. This matches how clients actually call your API.

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.