skir-php-generator 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,21 @@
1
1
  # Skir PHP Generator
2
2
 
3
- Generates framework-agnostic PHP data objects for Skir schemas.
3
+ [![Tests](https://github.com/php-skir/skir-php-generator/actions/workflows/tests.yml/badge.svg)](https://github.com/php-skir/skir-php-generator/actions/workflows/tests.yml)
4
+ [![Coverage](https://raw.githubusercontent.com/php-skir/skir-php-generator/badges/coverage.svg)](https://github.com/php-skir/skir-php-generator/actions/workflows/tests.yml)
5
+ [![npm](https://img.shields.io/npm/v/skir-php-generator?label=npm&logo=npm)](https://www.npmjs.com/package/skir-php-generator)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-22%20%7C%2024-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
7
+ [![License](https://img.shields.io/github/license/php-skir/skir-php-generator)](LICENSE)
4
8
 
5
- Generated PHP code uses `php-skir/runtime` for dense JSON serialization. If you use generated RPC clients, install `php-skir/client` as well.
9
+ Generates framework-agnostic PHP data objects, RPC clients, and server procedure contracts from Skir schemas.
10
+
11
+ Generated PHP uses `php-skir/runtime` for dense JSON serialization. Install `php-skir/client` when using generated RPC clients and `php-skir/server` when using generated server contracts.
12
+
13
+ The npm package uses [`@php-skir/generator-core`](https://github.com/php-skir/generator-core) at generation time for schema normalization, PHP naming and imports, shared enum and RPC rendering, server manifests, and Composer configuration. This package remains the framework-agnostic adapter: it owns the standard PHP data-object representation and conversions, while `php-skir/runtime` remains the Composer dependency used by the generated PHP at runtime.
6
14
 
7
15
  ## Installation
8
16
 
9
17
  ```bash
10
- npm install --save-dev skir-php-generator
18
+ npm install --save-dev skir skir-php-generator
11
19
  composer require php-skir/runtime
12
20
  ```
13
21
 
@@ -17,24 +25,104 @@ For generated typed RPC clients:
17
25
  composer require php-skir/client
18
26
  ```
19
27
 
20
- ## Releasing
28
+ For generated server contracts:
21
29
 
22
- Create a GitHub release for the version in `package.json`. The release workflow reruns type checks, build, package validation, and tests before publishing to npm with provenance. It expects an `NPM_TOKEN` repository secret.
30
+ ```bash
31
+ composer require php-skir/server
32
+ ```
23
33
 
24
- ## Usage with Skir
34
+ ## Configure Skir
25
35
 
26
- Add the generator to `skir.yml`:
36
+ Add the PHP generator to the root `skir.yml`:
27
37
 
28
38
  ```yaml
29
39
  generators:
30
- php:
31
- package: skir-php-generator
32
- output: generated/php
40
+ - mod: skir-php-generator
41
+ outDir: generated/php/skirout
33
42
  config:
34
- namespace: App\Skir
43
+ namespace: Skir
44
+ ```
45
+
46
+ The `generators` value uses Skir's current array syntax. Skir owns the configured `outDir` and may replace generated files there, so do not store handwritten PHP in that directory. Keep each generator in a dedicated output directory whose path ends in `/skirout`; the suffix identifies it as generator-owned output.
47
+
48
+ The root namespace defaults to `Skir`, so the `config` block can be omitted when that default is suitable. Schema directories become PHP subnamespaces and output directories:
49
+
50
+ ```text
51
+ skir-src/health/health.skir -> generated/php/skirout/Health/HealthRequest.php
52
+ -> Skir\Health\HealthRequest
53
+ ```
54
+
55
+ Only source directories become namespace segments. The `.skir` filename itself does not add a namespace segment, so `health.skir` does not produce another `Health` level.
56
+
57
+ A separate `Generated` namespace segment is unnecessary. The dedicated `generated/php/skirout` directory already separates generated code from handwritten application code, while the short `Skir` namespace keeps imports clear.
58
+
59
+ Every emitted PHP file contains a `DO NOT EDIT` banner. Change the `.skir` source or generator configuration and regenerate instead of editing generated PHP.
60
+
61
+ ## Generate and configure Composer
62
+
63
+ Run generation before configuring Composer because the configurator verifies that every output directory already exists:
64
+
65
+ ```bash
66
+ npx skir gen
67
+ npx skir-php-generator configure-composer
68
+ composer dump-autoload
35
69
  ```
36
70
 
37
- Then run the Skir generator command for your project.
71
+ `configure-composer` reads `skir.yml` and `composer.json` from the current project root. It finds the exact `mod: skir-php-generator` entry, normalizes the namespace and output path, and adds the matching Composer PSR-4 mapping. It prints whether the mapping was added or already unchanged.
72
+
73
+ The command only updates `composer.json`. It does not run Composer, so run `composer dump-autoload` afterward. Use `--root <dir>` for another project root or `--mod <module>` when the generator's `mod` is an exact alternative identifier such as a local file URL:
74
+
75
+ ```bash
76
+ npx skir-php-generator configure-composer --root ../api --mod file:///path/to/skir-php-generator/dist/index.js
77
+ ```
78
+
79
+ The operation is idempotent. An existing equivalent mapping is left byte-for-byte unchanged. If the namespace prefix already points somewhere else, the command exits with an error and does not modify `composer.json`. It also stops with a useful error when `skir.yml` or `composer.json` is missing or invalid, the matching generator configuration is missing or invalid, an output path escapes the project root, or a generated output directory does not exist.
80
+
81
+ When `outDir` is an ordered array, the configurator preserves that order in Composer's array-valued PSR-4 mapping. An existing Composer path array must match the entire configured array in the same order. Any difference is a conflict: the configurator fails without appending, merging, or modifying the existing mapping.
82
+
83
+ ### Package script automation
84
+
85
+ The workflow can be kept in `package.json`:
86
+
87
+ ```json
88
+ {
89
+ "scripts": {
90
+ "skir:generate": "skir gen && skir-php-generator configure-composer"
91
+ }
92
+ }
93
+ ```
94
+
95
+ Then run:
96
+
97
+ ```bash
98
+ npm run skir:generate
99
+ ```
100
+
101
+ Refresh Composer's autoloader as a separate follow-up step:
102
+
103
+ ```bash
104
+ composer dump-autoload
105
+ ```
106
+
107
+ ### Manual Composer mapping
108
+
109
+ To configure Composer manually, map the root namespace to the same generator-owned `outDir` and include trailing slashes:
110
+
111
+ ```json
112
+ {
113
+ "autoload": {
114
+ "psr-4": {
115
+ "Skir\\": "generated/php/skirout/"
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ After editing `composer.json`, run:
122
+
123
+ ```bash
124
+ composer dump-autoload
125
+ ```
38
126
 
39
127
  ## Generated PHP
40
128
 
@@ -45,68 +133,72 @@ The generator emits readonly PHP classes for Skir structs and enum wrapper class
45
133
  - `toDenseJson()` and `fromDenseJson()` for dense JSON payloads.
46
134
  - `toSkirValue()` and `fromSkirValue()` on generated enum classes.
47
135
 
48
- SkirRPC methods are emitted in `SkirMethods.php` as `MethodDescriptor` instances. The generator also emits a module-scoped method enum such as `AdminSkirMethod.php` for attribute-based server routing.
136
+ SkirRPC methods are emitted in `SkirMethods.php` as `MethodDescriptor` instances. The generator also emits a module-scoped method enum such as `AdminSkirMethod.php` for typed server routing.
49
137
 
50
- When a module defines SkirRPC methods, the generator also emits `SkirRpcClient.php`. It wraps `Skir\Client\SkirClient` and exposes typed methods:
138
+ When a module defines SkirRPC methods, the generator emits `SkirRpcClient.php`. It wraps `Skir\Client\SkirClient` and exposes typed methods:
51
139
 
52
140
  ```php
53
- use App\Skir\Admin\SkirRpcClient;
141
+ use Skir\Admin\SkirRpcClient;
54
142
  use Skir\Client\SkirClient as TransportSkirClient;
55
143
 
56
144
  $client = new SkirRpcClient(new TransportSkirClient('https://example.com/skir'));
57
145
  $user = $client->getUser($request);
58
146
  ```
59
147
 
60
- For servers, the generator emits `AdminSkirMethod.php`, `AbstractSkirProcedures.php`, `SkirProcedures.php`, and `SkirProcedureProvider.php`.
148
+ For servers, the generator emits a module method enum, `AbstractSkirProcedures.php`, `SkirProcedures.php`, and `SkirProcedureProvider.php`.
61
149
 
62
- The recommended Laravel server path is to use the generated method enum with `php-skir/server` controller routing:
150
+ When two generated records would otherwise use the same PHP class name in one namespace, the generator prefixes each class with its module basename to keep output deterministic.
63
151
 
64
- ```php
65
- use App\Skir\Admin\AdminSkirMethod;
66
- use App\Skir\Admin\GetUserRequest;
67
- use App\Skir\Admin\User;
68
- use Skir\Server\Attributes\SkirMethod;
69
- use Skir\Server\SkirContext;
152
+ ## Validation ownership
70
153
 
71
- final class UserController
72
- {
73
- #[SkirMethod(AdminSkirMethod::GetUser)]
74
- public function get(GetUserRequest $request, SkirContext $context): User
75
- {
76
- return new User(
77
- userId: $request->userId,
78
- name: 'Maxim',
79
- );
80
- }
81
- }
82
- ```
154
+ This generator intentionally performs no application validation. In Laravel, validate untrusted input with a handwritten Form Request before constructing generated objects. In other frameworks, use that framework's validator or an application-owned validation boundary.
83
155
 
84
- Register the controller on a Skir endpoint:
156
+ The generated classes describe and convert the Skir wire format; they are not an input-validation boundary. Keep validation code outside `skirout`, because Skir owns that directory and regeneration overwrites its contents.
85
157
 
86
- ```php
87
- use App\Skir\Controllers\UserController;
88
- use Illuminate\Support\Facades\Route;
89
- use Skir\Server\Facades\Skir;
158
+ ## Server scaffolding manifest
90
159
 
91
- Route::skirRpc('/api/skir', [
92
- Skir::controller(UserController::class),
93
- ]);
94
- ```
160
+ Each generation run writes `skir-server-manifest.json` at the root of the configured `outDir`. For the configuration above, the path is:
95
161
 
96
- The method enum resolves back to `SkirMethods::getUser()`, so the Skir schema remains the source of truth while your IDE can autocomplete enum cases.
162
+ ```text
163
+ generated/php/skirout/skir-server-manifest.json
164
+ ```
97
165
 
98
- The abstract procedure and interface/provider pair remain available as lower-level compatibility APIs.
166
+ The manifest is consumed by the [`php-skir/server`](https://github.com/php-skir/server) Laravel scaffolding commands. Keep the manifest path in the server package configuration aligned with the generator `outDir`.
99
167
 
100
- ## Namespaces and modules
168
+ Manifest module names use the generated PHP namespace segments joined with dots: `user-profile/` becomes `UserProfile`, nested `admin/users/` becomes `Admin.Users`, and a literal `admin.users/` directory becomes `AdminUsers`. Root-level methods use the reserved `_Root` module, while a real `Root/` directory remains `Root`. These names prefix exact server method IDs such as `_Root.Health` and `UserProfile.GetProfile`. Generation fails when distinct source directories normalize to the same PHP module instead of merging them.
101
169
 
102
- The configured namespace defaults to `App\Skir`. Module directories become PHP subnamespaces and output directories:
170
+ The current schema version is `1`:
103
171
 
104
- ```text
105
- admin/users.skir -> App\Skir\Admin
172
+ ```json
173
+ {
174
+ "version": 1,
175
+ "generator": "skir-php-generator",
176
+ "modules": [
177
+ {
178
+ "name": "Admin",
179
+ "methodEnum": "Skir\\Admin\\AdminSkirMethod",
180
+ "methods": [
181
+ {
182
+ "name": "GetUser",
183
+ "enumCase": "GetUser",
184
+ "phpMethod": "getUser",
185
+ "requestType": "Skir\\Admin\\GetUserRequest",
186
+ "requestClass": "Skir\\Admin\\GetUserRequest",
187
+ "responseType": "Skir\\Admin\\GetUserResponse",
188
+ "responseClass": "Skir\\Admin\\GetUserResponse"
189
+ }
190
+ ]
191
+ }
192
+ ]
193
+ }
106
194
  ```
107
195
 
108
- When two generated records would otherwise use the same PHP class name in the same namespace, the generator prefixes the class with the module basename to keep output deterministic.
196
+ `methodEnum` and record request and response types are fully qualified PHP names without a leading backslash. `requestClass` is non-null only when the request is a generated object/struct DTO that can be hydrated through a Form Request; enum and other value requests use `null` while `requestType` retains their PHP type. `responseClass` identifies a generated response class that can be imported. Scalar and union types use PHP type syntax and have a `null` class field. Optional object/struct requests keep their underlying DTO in `requestClass`. Names are derived from the actual generated record location, including nested and cross-module references.
197
+
198
+ Run `npx skir gen` after changing a schema. No extra generator option is required to emit the manifest.
199
+
200
+ ## Releasing
109
201
 
110
- ## Current scope
202
+ `@php-skir/generator-core` is a normal semver dependency of this package. Publish the required core version first, replace any local development link with that published version, update the lockfile, and verify a clean `npm ci` before releasing this adapter.
111
203
 
112
- This package generates framework-agnostic PHP DTOs, method descriptors, typed client adapters, and Laravel Skir server procedure adapters. Laravel-specific data objects and server routing live in separate packages.
204
+ Create a GitHub release for the version in `package.json`. The release workflow reruns type checks, build, package validation, and tests before publishing to npm with provenance. Configure this GitHub repository and its `release.yml` workflow as an npm trusted publisher; the workflow authenticates through OIDC and does not use a long-lived npm token.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { runConfigureComposerCli } from "@php-skir/generator-core";
3
+ import { GeneratorConfig, GENERATOR_MODULE } from "./config.js";
4
+ async function main() {
5
+ try {
6
+ await runConfigureComposerCli({
7
+ argv: process.argv.slice(2),
8
+ bin: "skir-php-generator",
9
+ module: GENERATOR_MODULE,
10
+ parseConfig: (value) => GeneratorConfig.parse(value),
11
+ namespace: (config) => config.namespace,
12
+ });
13
+ }
14
+ catch (error) {
15
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
16
+ process.exitCode = 1;
17
+ }
18
+ }
19
+ void main();
@@ -0,0 +1,2 @@
1
+ export { ComposerAutoloadConflictError, ensureComposerPsr4Mapping, MalformedComposerPsr4PrefixError, } from "@php-skir/generator-core";
2
+ export type { ComposerAutoloadPaths, ComposerPsr4MappingResult, } from "@php-skir/generator-core";
@@ -0,0 +1 @@
1
+ export { ComposerAutoloadConflictError, ensureComposerPsr4Mapping, MalformedComposerPsr4PrefixError, } from "@php-skir/generator-core";
@@ -0,0 +1,9 @@
1
+ import { DEFAULT_NAMESPACE, PhpNamespace } from "@php-skir/generator-core";
2
+ import { z } from "zod";
3
+ export declare const GENERATOR_MODULE = "skir-php-generator";
4
+ export { DEFAULT_NAMESPACE, PhpNamespace };
5
+ export declare const GeneratorConfig: z.ZodObject<{
6
+ namespace: z.ZodDefault<z.ZodString>;
7
+ }, z.core.$strict>;
8
+ export type GeneratorConfig = z.infer<typeof GeneratorConfig>;
9
+ export type GeneratorConfigInput = z.input<typeof GeneratorConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,7 @@
1
+ import { DEFAULT_NAMESPACE, PhpNamespace, } from "@php-skir/generator-core";
2
+ import { z } from "zod";
3
+ export const GENERATOR_MODULE = "skir-php-generator";
4
+ export { DEFAULT_NAMESPACE, PhpNamespace };
5
+ export const GeneratorConfig = z.strictObject({
6
+ namespace: PhpNamespace.default(DEFAULT_NAMESPACE),
7
+ });
@@ -0,0 +1,6 @@
1
+ import { type ComposerPsr4MappingResult } from "@php-skir/generator-core";
2
+ export interface ConfigureComposerOptions {
3
+ readonly mod?: string;
4
+ readonly root?: string;
5
+ }
6
+ export declare function configureComposer(options?: ConfigureComposerOptions): Promise<ComposerPsr4MappingResult>;
@@ -0,0 +1,10 @@
1
+ import { configureComposer as configureCoreComposer, } from "@php-skir/generator-core";
2
+ import { GeneratorConfig, GENERATOR_MODULE } from "./config.js";
3
+ export function configureComposer(options = {}) {
4
+ return configureCoreComposer({
5
+ module: options.mod ?? GENERATOR_MODULE,
6
+ ...(options.root === undefined ? {} : { root: options.root }),
7
+ parseConfig: (value) => GeneratorConfig.parse(value),
8
+ namespace: (config) => config.namespace,
9
+ });
10
+ }
@@ -1,65 +1,9 @@
1
- export interface PhpGeneratorConfig {
2
- readonly namespace?: string;
3
- }
4
- export interface PhpGeneratorInput {
1
+ import { PHP_FILE_HEADER, type CoreGeneratorInput, type GeneratedFile } from "@php-skir/generator-core";
2
+ import { type GeneratorConfigInput } from "./config.js";
3
+ export { PHP_FILE_HEADER };
4
+ export type PhpGeneratorConfig = GeneratorConfigInput;
5
+ export interface PhpGeneratorInput extends CoreGeneratorInput {
5
6
  readonly config?: PhpGeneratorConfig;
6
- readonly modules: readonly SkirModule[];
7
- readonly recordMap?: ReadonlyMap<string, SkirRecordLocation>;
8
- }
9
- export interface SkirModule {
10
- readonly path: string;
11
- readonly records?: readonly (SkirRecord | SkirRecordLocation)[];
12
- readonly methods?: readonly SkirMethod[];
13
- }
14
- export interface SkirRecordLocation {
15
- readonly kind: "record-location";
16
- readonly record: SkirRecord;
17
- readonly recordAncestors?: readonly SkirRecord[];
18
- readonly modulePath?: string;
19
- }
20
- export interface SkirRecord {
21
- readonly kind?: string;
22
- readonly key?: unknown;
23
- readonly name: string | SkirToken;
24
- readonly recordType?: "struct" | "enum";
25
- readonly fields?: readonly SkirField[];
26
- readonly removedNumbers?: readonly number[];
27
- readonly phpClassName?: string;
28
- }
29
- export type SkirField = {
30
- readonly kind: "field";
31
- readonly name: string | SkirToken;
32
- readonly number: number;
33
- readonly type?: SkirType;
34
- } | {
35
- readonly kind: "removed";
36
- readonly number: number;
37
- };
38
- export type SkirType = string | {
39
- readonly kind: string;
40
- readonly primitive?: string;
41
- readonly item?: SkirType;
42
- readonly other?: SkirType;
43
- readonly key?: unknown;
44
- readonly name?: string | SkirToken;
45
- readonly nameParts?: readonly SkirRecordNamePart[];
46
- readonly recordType?: "struct" | "enum";
47
- };
48
- export interface SkirToken {
49
- readonly text: string;
50
- }
51
- export type SkirRecordNamePart = string | SkirToken | {
52
- readonly token: string | SkirToken;
53
- };
54
- export interface SkirMethod {
55
- readonly kind: "method";
56
- readonly name: string | SkirToken;
57
- readonly number: number;
58
- readonly requestType?: SkirType;
59
- readonly responseType?: SkirType;
60
- }
61
- export interface GeneratedFile {
62
- readonly path: string;
63
- readonly code: string;
64
7
  }
65
8
  export declare function generatePhpFiles(input: PhpGeneratorInput): GeneratedFile[];
9
+ export type { GeneratedFile, SkirField, SkirMethod, SkirModule, SkirRecord, SkirRecordLocation, SkirRecordNamePart, SkirToken, SkirType, } from "@php-skir/generator-core";