Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
- [Adding Sections](#adding-sections)
- [Defining Fields](#defining-fields)
- [Array & Object Fields](#array-and-object-fields)
- [Ephemeral Templates](#ephemeral-templates)
- [Creating Ephemeral Templates](#creating-ephemeral-templates)
- [Ephemeral Schema & Rendering](#ephemeral-schema-and-rendering)
- [JSON Schema Generation](#json-schema-generation)
- [Template Schemas](#template-schemas)
- [Section Schemas](#section-schemas)
Expand All @@ -32,7 +35,7 @@
<a name="introduction"></a>
## Introduction

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.
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.

<a name="installation"></a>
## Installation
Expand Down Expand Up @@ -195,6 +198,82 @@ TPL,
);
```

<a name="ephemeral-templates"></a>
## Ephemeral Templates

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.

Ephemeral templates support the same core features as database-backed templates: sections, fields, JSON Schema generation, rendering, and previewing.

<a name="creating-ephemeral-templates"></a>
### Creating Ephemeral Templates

Use the `ephemeral` method on the `Schematic` facade to create an in-memory template:

```php
use Yannelli\Schematic\Facades\Schematic;

$template = Schematic::ephemeral(
slug: 'intake-form',
name: 'Patient Intake Form',
description: 'A quick intake form built on the fly',
);

$template->addSection(
slug: 'demographics',
name: 'Demographics',
content: '{{ patient_name }}, Age: {{ age }}',
fields: [
['name' => 'patient_name', 'type' => 'string', 'description' => 'Full name'],
['name' => 'age', 'type' => 'integer', 'description' => 'Patient age'],
],
examples: ['patient_name' => 'Jane Doe', 'age' => 34],
);
```

You may also create ephemeral templates directly via the `EphemeralTemplate` class:

```php
use Yannelli\Schematic\Ephemeral\EphemeralTemplate;

$template = EphemeralTemplate::make('quick-note', 'Quick Note');
$section = $template->addSection('body', 'Body', content: '{{ note }}');
$section->addField('note', 'string', 'The note content');
```

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.

<a name="ephemeral-schema-and-rendering"></a>
### Ephemeral Schema & Rendering

Ephemeral templates generate JSON Schema and render content exactly like their database-backed counterparts:

```php
// JSON Schema generation
$schema = $template->toJsonSchema();
$doc = $template->toJsonSchemaDocument();
$sectionSchema = $template->sectionSchema('demographics');

// Rendering with data
$output = $template->render([
'demographics' => ['patient_name' => 'Alice Smith', 'age' => 28],
]);

// Preview using example data
$preview = $template->preview();
```

Section management works identically — you can iterate, reorder, enable, and disable sections:

```php
$template->section('demographics')->disable();
$template->reorderSections(['body', 'demographics']);
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation example reorders sections using a 'body' slug that isn’t created in the snippet. That makes the example misleading (and, with the current implementation, will just skip the unknown slug and set 'demographics' to order 1). Update the example to only include existing section slugs, or add creation of the missing section before reordering.

Suggested change
$template->reorderSections(['body', 'demographics']);
$template->reorderSections(['demographics']);

Copilot uses AI. Check for mistakes.

foreach ($template->iterateSections() as $section) {
// Only enabled sections
}
```

<a name="json-schema-generation"></a>
## JSON Schema Generation

Expand Down
161 changes: 161 additions & 0 deletions src/Ephemeral/EphemeralSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

namespace Yannelli\Schematic\Ephemeral;

use Yannelli\Schematic\Compiler;
use Yannelli\Schematic\Contracts\Renderable;
use Yannelli\Schematic\Contracts\SchemaGeneratable;
use Yannelli\Schematic\FieldDefinition;

class EphemeralSection implements Renderable, SchemaGeneratable
{
public function __construct(
public string $slug,
public string $name,
public ?string $description = null,
public ?string $content = null,
public array $fields = [],
public array $examples = [],
public int $order = 0,
public bool $is_enabled = true,
) {}

/**
* Named constructor for fluent usage.
*/
public static function make(
string $slug,
string $name,
?string $description = null,
?string $content = null,
array $fields = [],
array $examples = [],
int $order = 0,
bool $enabled = true,
): static {
return new static($slug, $name, $description, $content, $fields, $examples, $order, $enabled);
}

// ---------------------------------------------------------------
// Schema Generation
// ---------------------------------------------------------------

/**
* Parse the fields array into FieldDefinition objects.
*
* @return \Illuminate\Support\Collection<int, FieldDefinition>
*/
public function fieldDefinitions(): \Illuminate\Support\Collection
{
return collect($this->fields)->map(
fn (array $field) => FieldDefinition::fromArray($field)
);
}

public function toJsonSchema(): array
{
$properties = [];
$required = [];

foreach ($this->fieldDefinitions() as $field) {
$properties[$field->name] = $field->toJsonSchemaProperty();

if ($field->required) {
$required[] = $field->name;
}
}

$schema = [
'type' => 'object',
'properties' => $properties,
];

if ($required !== []) {
$schema['required'] = $required;
}

if ($this->description) {
$schema['description'] = $this->description;
}

if (config('schematic.schema.strict', true)) {
$schema['additionalProperties'] = false;
}

return $schema;
}

// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------

public function render(array $data = []): string
{
if (! $this->is_enabled) {
return '';
}

return app(Compiler::class)->compile($this->content ?? '', $data);
}

public function preview(): string
{
return $this->render($this->examples);
}

// ---------------------------------------------------------------
// Fluent Builders
// ---------------------------------------------------------------

public function enable(): static
{
$this->is_enabled = true;

return $this;
}

public function disable(): static
{
$this->is_enabled = false;

return $this;
}

public function addField(
string $name,
string $type = 'string',
string $description = '',
bool $required = true,
bool $nullable = false,
mixed $default = null,
?array $enum = null,
): static {
$this->fields[] = (new FieldDefinition(
name: $name,
type: $type,
description: $description,
required: $required,
nullable: $nullable,
default: $default,
enum: $enum,
))->jsonSerialize();

return $this;
}

public function removeField(string $name): static
{
$this->fields = collect($this->fields)->reject(
fn (array $f) => $f['name'] === $name
)->values()->all();

return $this;
}

public function setExamples(array $examples): static
{
$this->examples = $examples;

return $this;
}
}
Loading
Loading