Skip to content

Commit b72951c

Browse files
authored
Add ephemeral (in-memory) templates for on-the-fly use without database (#3)
Introduces EphemeralTemplate and EphemeralSection classes that implement the same Renderable and SchemaGeneratable interfaces as the Eloquent models but store everything in memory. Accessible via Schematic::ephemeral() or EphemeralTemplate::make(). Includes 34 new tests and README documentation.
1 parent ddfbef6 commit b72951c

7 files changed

Lines changed: 963 additions & 1 deletion

File tree

README.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
- [Adding Sections](#adding-sections)
1212
- [Defining Fields](#defining-fields)
1313
- [Array & Object Fields](#array-and-object-fields)
14+
- [Ephemeral Templates](#ephemeral-templates)
15+
- [Creating Ephemeral Templates](#creating-ephemeral-templates)
16+
- [Ephemeral Schema & Rendering](#ephemeral-schema-and-rendering)
1417
- [JSON Schema Generation](#json-schema-generation)
1518
- [Template Schemas](#template-schemas)
1619
- [Section Schemas](#section-schemas)
@@ -32,7 +35,7 @@
3235
<a name="introduction"></a>
3336
## Introduction
3437

35-
Schematic is a database-driven templating engine for Laravel that generates JSON Schema definitions from your templates. It is designed for use with LLM structured output APIs such as those provided by OpenAI and Anthropic, allowing you to define templates with typed fields and automatically produce valid JSON Schema for tool use and structured responses.
38+
Schematic is a templating engine for Laravel that generates JSON Schema definitions from your templates. It is designed for use with LLM structured output APIs such as those provided by OpenAI and Anthropic, allowing you to define templates with typed fields and automatically produce valid JSON Schema for tool use and structured responses. Templates can be persisted to the database or created as [ephemeral (in-memory) templates](#ephemeral-templates) for on-the-fly use without any database overhead.
3639

3740
<a name="installation"></a>
3841
## Installation
@@ -195,6 +198,82 @@ TPL,
195198
);
196199
```
197200

201+
<a name="ephemeral-templates"></a>
202+
## Ephemeral Templates
203+
204+
Ephemeral templates are in-memory templates that are **not persisted to the database**. They are useful for one-off or dynamic templates that you build at runtime — no migrations or database queries required.
205+
206+
Ephemeral templates support the same core features as database-backed templates: sections, fields, JSON Schema generation, rendering, and previewing.
207+
208+
<a name="creating-ephemeral-templates"></a>
209+
### Creating Ephemeral Templates
210+
211+
Use the `ephemeral` method on the `Schematic` facade to create an in-memory template:
212+
213+
```php
214+
use Yannelli\Schematic\Facades\Schematic;
215+
216+
$template = Schematic::ephemeral(
217+
slug: 'intake-form',
218+
name: 'Patient Intake Form',
219+
description: 'A quick intake form built on the fly',
220+
);
221+
222+
$template->addSection(
223+
slug: 'demographics',
224+
name: 'Demographics',
225+
content: '{{ patient_name }}, Age: {{ age }}',
226+
fields: [
227+
['name' => 'patient_name', 'type' => 'string', 'description' => 'Full name'],
228+
['name' => 'age', 'type' => 'integer', 'description' => 'Patient age'],
229+
],
230+
examples: ['patient_name' => 'Jane Doe', 'age' => 34],
231+
);
232+
```
233+
234+
You may also create ephemeral templates directly via the `EphemeralTemplate` class:
235+
236+
```php
237+
use Yannelli\Schematic\Ephemeral\EphemeralTemplate;
238+
239+
$template = EphemeralTemplate::make('quick-note', 'Quick Note');
240+
$section = $template->addSection('body', 'Body', content: '{{ note }}');
241+
$section->addField('note', 'string', 'The note content');
242+
```
243+
244+
Sections on ephemeral templates support the same fluent methods as database-backed sections, including `addField`, `removeField`, `enable`, `disable`, and `setExamples`. All mutations happen in memory.
245+
246+
<a name="ephemeral-schema-and-rendering"></a>
247+
### Ephemeral Schema & Rendering
248+
249+
Ephemeral templates generate JSON Schema and render content exactly like their database-backed counterparts:
250+
251+
```php
252+
// JSON Schema generation
253+
$schema = $template->toJsonSchema();
254+
$doc = $template->toJsonSchemaDocument();
255+
$sectionSchema = $template->sectionSchema('demographics');
256+
257+
// Rendering with data
258+
$output = $template->render([
259+
'demographics' => ['patient_name' => 'Alice Smith', 'age' => 28],
260+
]);
261+
262+
// Preview using example data
263+
$preview = $template->preview();
264+
```
265+
266+
Section management works identically — you can iterate, reorder, enable, and disable sections:
267+
268+
```php
269+
$template->section('demographics')->disable();
270+
$template->reorderSections(['body', 'demographics']);
271+
272+
foreach ($template->iterateSections() as $section) {
273+
// Only enabled sections
274+
}
275+
```
276+
198277
<a name="json-schema-generation"></a>
199278
## JSON Schema Generation
200279

src/Ephemeral/EphemeralSection.php

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
namespace Yannelli\Schematic\Ephemeral;
4+
5+
use Yannelli\Schematic\Compiler;
6+
use Yannelli\Schematic\Contracts\Renderable;
7+
use Yannelli\Schematic\Contracts\SchemaGeneratable;
8+
use Yannelli\Schematic\FieldDefinition;
9+
10+
class EphemeralSection implements Renderable, SchemaGeneratable
11+
{
12+
public function __construct(
13+
public string $slug,
14+
public string $name,
15+
public ?string $description = null,
16+
public ?string $content = null,
17+
public array $fields = [],
18+
public array $examples = [],
19+
public int $order = 0,
20+
public bool $is_enabled = true,
21+
) {}
22+
23+
/**
24+
* Named constructor for fluent usage.
25+
*/
26+
public static function make(
27+
string $slug,
28+
string $name,
29+
?string $description = null,
30+
?string $content = null,
31+
array $fields = [],
32+
array $examples = [],
33+
int $order = 0,
34+
bool $enabled = true,
35+
): static {
36+
return new static($slug, $name, $description, $content, $fields, $examples, $order, $enabled);
37+
}
38+
39+
// ---------------------------------------------------------------
40+
// Schema Generation
41+
// ---------------------------------------------------------------
42+
43+
/**
44+
* Parse the fields array into FieldDefinition objects.
45+
*
46+
* @return \Illuminate\Support\Collection<int, FieldDefinition>
47+
*/
48+
public function fieldDefinitions(): \Illuminate\Support\Collection
49+
{
50+
return collect($this->fields)->map(
51+
fn (array $field) => FieldDefinition::fromArray($field)
52+
);
53+
}
54+
55+
public function toJsonSchema(): array
56+
{
57+
$properties = [];
58+
$required = [];
59+
60+
foreach ($this->fieldDefinitions() as $field) {
61+
$properties[$field->name] = $field->toJsonSchemaProperty();
62+
63+
if ($field->required) {
64+
$required[] = $field->name;
65+
}
66+
}
67+
68+
$schema = [
69+
'type' => 'object',
70+
'properties' => $properties,
71+
];
72+
73+
if ($required !== []) {
74+
$schema['required'] = $required;
75+
}
76+
77+
if ($this->description) {
78+
$schema['description'] = $this->description;
79+
}
80+
81+
if (config('schematic.schema.strict', true)) {
82+
$schema['additionalProperties'] = false;
83+
}
84+
85+
return $schema;
86+
}
87+
88+
// ---------------------------------------------------------------
89+
// Rendering
90+
// ---------------------------------------------------------------
91+
92+
public function render(array $data = []): string
93+
{
94+
if (! $this->is_enabled) {
95+
return '';
96+
}
97+
98+
return app(Compiler::class)->compile($this->content ?? '', $data);
99+
}
100+
101+
public function preview(): string
102+
{
103+
return $this->render($this->examples);
104+
}
105+
106+
// ---------------------------------------------------------------
107+
// Fluent Builders
108+
// ---------------------------------------------------------------
109+
110+
public function enable(): static
111+
{
112+
$this->is_enabled = true;
113+
114+
return $this;
115+
}
116+
117+
public function disable(): static
118+
{
119+
$this->is_enabled = false;
120+
121+
return $this;
122+
}
123+
124+
public function addField(
125+
string $name,
126+
string $type = 'string',
127+
string $description = '',
128+
bool $required = true,
129+
bool $nullable = false,
130+
mixed $default = null,
131+
?array $enum = null,
132+
): static {
133+
$this->fields[] = (new FieldDefinition(
134+
name: $name,
135+
type: $type,
136+
description: $description,
137+
required: $required,
138+
nullable: $nullable,
139+
default: $default,
140+
enum: $enum,
141+
))->jsonSerialize();
142+
143+
return $this;
144+
}
145+
146+
public function removeField(string $name): static
147+
{
148+
$this->fields = collect($this->fields)->reject(
149+
fn (array $f) => $f['name'] === $name
150+
)->values()->all();
151+
152+
return $this;
153+
}
154+
155+
public function setExamples(array $examples): static
156+
{
157+
$this->examples = $examples;
158+
159+
return $this;
160+
}
161+
}

0 commit comments

Comments
 (0)