Skip to content

WIP - Web/Service/Rest/TRestService - Service provider with TRestResources - #1155

Draft
belisoful wants to merge 7 commits into
pradosoft:masterfrom
belisoful:rest-service
Draft

WIP - Web/Service/Rest/TRestService - Service provider with TRestResources#1155
belisoful wants to merge 7 commits into
pradosoft:masterfrom
belisoful:rest-service

Conversation

@belisoful

Copy link
Copy Markdown
Member

This branch introduces 4 TRest* classes, a self-contained REST API layer for PRADO intended as the backend for single-page applications. It is a standalone TService that runs alongside the existing TPageService / TJsonService / TRpcService family.

The new namespace contains four classes:

  • TRestService — compiles and declarations into a route table, dispatches matched requests to convention methods on TRestResource subclasses, and writes the JSON response.
  • TRestResource — abstract base; subclasses override doIndex / doShow / doStore / doUpdate / doPatch / doDestroy. The base provides body parsing, a validation DSL, status helpers, and exception helpers.
  • TRestException — RFC 7807 problem-details envelope with static factories per status.
  • TRestPagination — page / per_page query helper producing offset / limit and a {data, meta} envelope.
    Configuration accepts both XML and PHP-array forms. Inline entries can be grouped under a prefix, with enabled accepting boolean-ish values plus the special string "Debug" (active only in TApplicationMode::Debug). A groupfile splits one group's resources into its own file; a service-level configfile moves the entire tree into a separate file, with inline entries appended afterwards.

TRestService

TRestService maintains its own route table independent of TUrlMapping.

Each compiles to a named-capture regular expression — {name} placeholders become (?P(?:constraint)), where the inner non-capturing group lets user-supplied alternations like \d+|new coexist safely with the named capture. Per-parameter regex constraints are declared with attributes such as parameters.id="\d+", defaulting to [^/]+.

The lifecycle through run() is short: CORS headers (and a short-circuit on OPTIONS preflight) → strip BasePath from PATH_INFO → match against the compiled table in declaration order → instantiate the TRestResource subclass, apply any extra XML attributes as properties, inject path parameters → call authorize() → dispatch to the do…() method picked from the verb and route shape → JSON-encode the return value at the resource's chosen status code. HEAD and 204 responses suppress the body; any TRestException (or other Throwable) is converted to a JSON error envelope.

<service id="rest" class="Prado\Web\Services\Rest\TRestService"
         BasePath="api/" EnableCors="true"
         AllowOrigin="https://myapp.example.com">

  <resource pattern="users"      class="App.Api.UsersResource" />
  <resource pattern="users/{id}" class="App.Api.UsersResource" parameters.id="\d+" />

  <group prefix="v2/" groupfile="Application.config.rest-v2" />
  <group prefix="debug/" enabled="Debug">
    <resource pattern="dump" class="App.Api.Debug.DumpResource" />
  </group>
</service>

TRestResource

TRestResource is the abstract base every concrete resource extends.

It uses __call rather than typed stubs for the six convention methods so subclasses can declare any signature they need — path parameters become method parameters by name (doShow(string $id)), and any convention method the subclass does not override automatically yields 405 Method Not Allowed. Request bodies are parsed lazily on first access: JSON for application/json, $_POST for form-encoded POST, and php://input via parse_str for form-encoded PUT / PATCH (PHP does not populate $_POST for non-POST verbs).

The validation DSL accepts pipe-delimited rule strings — required, nullable, string, integer, float, numeric, boolean, array, email, url, min:N, max:N, in:a,b,c — applies type coercion where appropriate, and throws 422 Unprocessable Entity with field-level errors on failure.

Status helpers (created() / accepted() / noContent()) set the response code while still returning a value, and exception helpers (notFound() / unauthorized() / forbidden() / conflict() / unprocessable() / abort()) raise a properly-typed TRestException.

namespace App\Api;

use Prado\Web\Services\Rest\TRestResource;

class UsersResource extends TRestResource
{
    public function authorize(string $method): void
    {
        if (!in_array($method, ['doIndex', 'doShow'], true)
            && $this->getApplication()->getUser()->getIsGuest()) {
            $this->unauthorized('Authentication required.');
        }
    }

    public function doShow(string $id): array
    {
        return UserDao::find((int) $id)
            ?? $this->notFound("User {$id} not found.");
    }

    public function doStore(): array
    {
        $data = $this->validateBody([
            'name'  => 'required|string|max:255',
            'email' => 'required|email',
            'role'  => 'nullable|string|in:admin,editor,viewer',
        ]);
        return $this->created(UserDao::create($data));
    }
}

Routing classifies routes as item vs. collection based on whether the final path segment is a {param} and pairs that shape with the HTTP verb to pick the resource method; path parameters are injected by name via PHP reflection.

CORS is built in — preflight OPTIONS returns 204, credentialed wildcard origins reflect the request Origin, and Vary: Origin is emitted on non-wildcard origins. Every error — routing failures, method mismatches, thrown TRestException instances, and uncaught exceptions — is serialised as {status, title, detail?, errors?}; ExposeErrors (defaulting on in Debug) controls whether 500 details leak.

@belisoful belisoful changed the title Service/Rest/TRestService - Service provider with TRestResources Web/Service/Rest/TRestService - Service provider with TRestResources May 29, 2026
@belisoful
belisoful marked this pull request as ready for review May 29, 2026 08:32
@belisoful

belisoful commented May 29, 2026

Copy link
Copy Markdown
Member Author

There needs to be some Strong critique of this before agreeing to merge.

Anyone who is interested should see how this implements Rest in the code and comment

@belisoful
belisoful marked this pull request as draft June 1, 2026 04:06
@belisoful belisoful changed the title Web/Service/Rest/TRestService - Service provider with TRestResources WIP - Web/Service/Rest/TRestService - Service provider with TRestResources Jun 5, 2026
belisoful and others added 2 commits June 11, 2026 23:37
- TRestService rejects AllowCredentials combined with the wildcard
  AllowOrigin instead of reflecting arbitrary request origins; validated
  at init() and again in sendCorsHeaders().
- Requests outside BasePath now return 404 instead of matching routes
  without the prefix; the bare base path resolves to the root path.
- 405 responses carry the RFC 7231 Allow header listing the verbs the
  matched resource supports.
- Preflight-only CORS headers (Allow-Methods, Allow-Headers, Max-Age)
  are emitted only for OPTIONS requests.
- TRestResource::validate() throws TConfigurationException for unknown
  rule names instead of silently skipping them.
- TRestResource::query()/input()/hasInput() read true query-string
  parameters via a getQueryParams() seam instead of THttpRequest's
  merged parameter map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@belisoful

Copy link
Copy Markdown
Member Author

requires a58ce2c in #1229

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant