Skip to content

Commit edb9680

Browse files
committed
Give Laravel docs their own top-level nav section
The flat sidebar array was shown on every page regardless of section, and kept growing as Laravel-specific pages (Eloquent casting, Livewire, #[WhenLoaded]) got added under "Features" alongside framework-agnostic content — Laravel content ended up scattered across three different places (Features, Integrations, Attributes) with no single home. Moved features/laravel.md, features/eloquent-casting.md, and features/livewire.md into docs/laravel/, added a top-level "Laravel" nav entry, and converted the sidebar config from a flat array into a path-keyed object so /laravel/* pages get their own compact 4-item sidebar (Overview, Eloquent Attribute Casting, Livewire Integration, #[WhenLoaded]) instead of the full ~40-entry list. #[WhenLoaded] is now an explicit sidebar entry in that section, not just a prose link buried in the fromModel() docs. Fixed a resulting break in the custom Breadcrumb.vue component, which assumed theme.sidebar was always a flat array — it now resolves the current page's group via the same longest-prefix-match VitePress itself uses for multi-sidebars. Folded the Octane/long-running-workers note from integrations/laravel.md into the new laravel/index.md, since that page's content is otherwise now fully covered by the consolidated section; integrations/laravel.md stays as the short per-framework quickstart entry (parallel to Plain PHP/Symfony/PSR-7) and points into /laravel/ for the full reference. Updated every internal link that pointed at the old features/laravel.md path (guide/quick-start.md, attributes/when-loaded.md, integrations/laravel.md, and two published-docs URLs in CHANGELOG.md).
1 parent f458641 commit edb9680

9 files changed

Lines changed: 110 additions & 79 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
1919
`#[Discriminator]` class as the cast target. Fully decoupled, like
2020
`HasLaravelIntegration` and `WireableData`: no dependency on
2121
`illuminate/database`. See
22-
[Eloquent Attribute Casting](https://std-out.github.io/simple-data-objects/features/eloquent-casting).
22+
[Eloquent Attribute Casting](https://std-out.github.io/simple-data-objects/laravel/eloquent-casting).
2323
- **`#[WhenLoaded]`.** `fromModel()` now hydrates from `$model->attributesToArray()`
2424
(no relations) and adds a relation only when its property is marked
2525
`#[WhenLoaded('relationName')]` and the relation is actually loaded;
@@ -39,7 +39,7 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
3939
so the trait's methods already satisfy it structurally. The consuming
4040
class adds `implements \Livewire\Wireable` itself, which is the only
4141
place `livewire/livewire` needs to be installed. See
42-
[Livewire Integration](https://std-out.github.io/simple-data-objects/features/livewire).
42+
[Livewire Integration](https://std-out.github.io/simple-data-objects/laravel/livewire).
4343

4444
## [1.14.0] — 2026-07-25
4545

‎docs/.vitepress/config.mjs‎

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -37,83 +37,95 @@ export default defineConfig({
3737
nav: [
3838
{ text: 'Guide', link: '/guide/installation' },
3939
{ text: 'Features', link: '/features/hydration' },
40+
{ text: 'Laravel', link: '/laravel/' },
4041
{ text: 'Casts', link: '/casts/' },
4142
{
4243
text: 'GitHub',
4344
link: 'https://github.com/std-out/simple-data-objects',
4445
},
4546
],
4647

47-
sidebar: [
48-
{
49-
text: 'Getting Started',
50-
items: [
51-
{ text: 'Introduction', link: '/guide/introduction' },
52-
{ text: 'Installation', link: '/guide/installation' },
53-
{ text: 'Quick Start', link: '/guide/quick-start' },
54-
{ text: 'Performance', link: '/guide/performance' },
55-
],
56-
},
57-
{
58-
text: 'Features',
59-
items: [
60-
{ text: 'Hydration', link: '/features/hydration' },
61-
{ text: 'Serialization', link: '/features/serialization' },
62-
{ text: 'Validation', link: '/features/validation' },
63-
{ text: 'DataPipe — Preprocessing', link: '/features/pipes' },
64-
{ text: 'Immutable Copies — with()', link: '/features/with' },
65-
{ text: 'Comparison — equals() & diff()', link: '/features/comparison' },
66-
{ text: 'Collections', link: '/features/collections' },
67-
{ text: 'Laravel Integration', link: '/features/laravel' },
68-
{ text: 'Eloquent Attribute Casting', link: '/features/eloquent-casting' },
69-
{ text: 'Livewire Integration', link: '/features/livewire' },
70-
{ text: 'Metadata Cache', link: '/features/cache' },
71-
],
72-
},
73-
{
74-
text: 'Integrations',
75-
items: [
76-
{ text: 'Plain PHP', link: '/integrations/plain-php' },
77-
{ text: 'Laravel', link: '/integrations/laravel' },
78-
{ text: 'Symfony', link: '/integrations/symfony' },
79-
{ text: 'Slim & PSR-7', link: '/integrations/psr-7' },
80-
],
81-
},
82-
{
83-
text: 'Attributes',
84-
items: [
85-
{ text: 'Overview', link: '/attributes/' },
86-
{ text: '#[Cast]', link: '/attributes/cast' },
87-
{ text: '#[Rules]', link: '/attributes/rules' },
88-
{ text: '#[Pipe]', link: '/attributes/pipe' },
89-
{ text: '#[Flatten]', link: '/attributes/flatten' },
90-
{ text: '#[Hidden]', link: '/attributes/hidden' },
91-
{ text: '#[IgnoreIfNull]', link: '/attributes/ignore-if-null' },
92-
{ text: '#[MapPropertyName]', link: '/attributes/map-property-name' },
93-
{ text: '#[TransformKeys]', link: '/attributes/transform-keys' },
94-
{ text: '#[Discriminator]', link: '/attributes/discriminator' },
95-
{ text: '#[DataCollection]', link: '/attributes/data-collection' },
96-
{ text: '#[WhenLoaded]', link: '/attributes/when-loaded' },
97-
],
98-
},
99-
{
100-
text: 'Built-in Casts',
101-
items: [
102-
{ text: 'Overview', link: '/casts/' },
103-
{ text: 'DateTimeCast', link: '/casts/date-time' },
104-
{ text: 'EnumCast', link: '/casts/enum' },
105-
{ text: 'BooleanCast', link: '/casts/boolean' },
106-
{ text: 'IntegerCast & FloatCast', link: '/casts/numeric' },
107-
{ text: 'TrimCast', link: '/casts/trim' },
108-
{ text: 'JsonCast', link: '/casts/json' },
109-
{ text: 'EncryptedCast', link: '/casts/encrypted' },
110-
{ text: 'UuidCast', link: '/casts/uuid' },
111-
{ text: 'CommaSeparatedCast', link: '/casts/comma-separated' },
112-
{ text: 'MoneyCast', link: '/casts/money' },
113-
{ text: 'Custom Casts', link: '/casts/custom' },
114-
],
115-
},
116-
],
48+
sidebar: {
49+
'/laravel/': [
50+
{
51+
text: 'Laravel',
52+
items: [
53+
{ text: 'Overview', link: '/laravel/' },
54+
{ text: 'Eloquent Attribute Casting', link: '/laravel/eloquent-casting' },
55+
{ text: 'Livewire Integration', link: '/laravel/livewire' },
56+
{ text: '#[WhenLoaded]', link: '/attributes/when-loaded' },
57+
],
58+
},
59+
],
60+
61+
'/': [
62+
{
63+
text: 'Getting Started',
64+
items: [
65+
{ text: 'Introduction', link: '/guide/introduction' },
66+
{ text: 'Installation', link: '/guide/installation' },
67+
{ text: 'Quick Start', link: '/guide/quick-start' },
68+
{ text: 'Performance', link: '/guide/performance' },
69+
],
70+
},
71+
{
72+
text: 'Features',
73+
items: [
74+
{ text: 'Hydration', link: '/features/hydration' },
75+
{ text: 'Serialization', link: '/features/serialization' },
76+
{ text: 'Validation', link: '/features/validation' },
77+
{ text: 'DataPipe — Preprocessing', link: '/features/pipes' },
78+
{ text: 'Immutable Copies — with()', link: '/features/with' },
79+
{ text: 'Comparison — equals() & diff()', link: '/features/comparison' },
80+
{ text: 'Collections', link: '/features/collections' },
81+
{ text: 'Metadata Cache', link: '/features/cache' },
82+
],
83+
},
84+
{
85+
text: 'Integrations',
86+
items: [
87+
{ text: 'Plain PHP', link: '/integrations/plain-php' },
88+
{ text: 'Laravel', link: '/integrations/laravel' },
89+
{ text: 'Symfony', link: '/integrations/symfony' },
90+
{ text: 'Slim & PSR-7', link: '/integrations/psr-7' },
91+
],
92+
},
93+
{
94+
text: 'Attributes',
95+
items: [
96+
{ text: 'Overview', link: '/attributes/' },
97+
{ text: '#[Cast]', link: '/attributes/cast' },
98+
{ text: '#[Rules]', link: '/attributes/rules' },
99+
{ text: '#[Pipe]', link: '/attributes/pipe' },
100+
{ text: '#[Flatten]', link: '/attributes/flatten' },
101+
{ text: '#[Hidden]', link: '/attributes/hidden' },
102+
{ text: '#[IgnoreIfNull]', link: '/attributes/ignore-if-null' },
103+
{ text: '#[MapPropertyName]', link: '/attributes/map-property-name' },
104+
{ text: '#[TransformKeys]', link: '/attributes/transform-keys' },
105+
{ text: '#[Discriminator]', link: '/attributes/discriminator' },
106+
{ text: '#[DataCollection]', link: '/attributes/data-collection' },
107+
{ text: '#[WhenLoaded]', link: '/attributes/when-loaded' },
108+
],
109+
},
110+
{
111+
text: 'Built-in Casts',
112+
items: [
113+
{ text: 'Overview', link: '/casts/' },
114+
{ text: 'DateTimeCast', link: '/casts/date-time' },
115+
{ text: 'EnumCast', link: '/casts/enum' },
116+
{ text: 'BooleanCast', link: '/casts/boolean' },
117+
{ text: 'IntegerCast & FloatCast', link: '/casts/numeric' },
118+
{ text: 'TrimCast', link: '/casts/trim' },
119+
{ text: 'JsonCast', link: '/casts/json' },
120+
{ text: 'EncryptedCast', link: '/casts/encrypted' },
121+
{ text: 'UuidCast', link: '/casts/uuid' },
122+
{ text: 'CommaSeparatedCast', link: '/casts/comma-separated' },
123+
{ text: 'MoneyCast', link: '/casts/money' },
124+
{ text: 'Custom Casts', link: '/casts/custom' },
125+
],
126+
},
127+
],
128+
},
117129

118130
socialLinks: [
119131
{ icon: 'github', link: 'https://github.com/std-out/simple-data-objects' },

‎docs/.vitepress/theme/Breadcrumb.vue‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@ const trail = computed(() => {
1818
const section = segment.charAt(0).toUpperCase() + segment.slice(1)
1919
2020
const link = '/' + path.replace(/(index)?\.md$/, '')
21-
const group = (theme.value.sidebar ?? []).find((g) =>
21+
22+
// sidebar may be a flat array or a multi-sidebar object keyed by path
23+
// prefix — resolve it the same way VitePress does: the most specific
24+
// (longest) matching prefix wins.
25+
const rawSidebar = theme.value.sidebar
26+
const groups = Array.isArray(rawSidebar)
27+
? rawSidebar
28+
: Object.keys(rawSidebar ?? {})
29+
.filter((prefix) => link.startsWith(prefix))
30+
.sort((a, b) => b.length - a.length)
31+
.flatMap((prefix) => rawSidebar[prefix] ?? [])
32+
33+
const group = groups.find((g) =>
2234
g.items?.some((item) => item.link === link || item.link + '/' === link),
2335
)?.text
2436

‎docs/attributes/when-loaded.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# #[WhenLoaded]
22

3-
Includes an Eloquent relation in `fromModel()` hydration only when that relation is actually loaded on the model. Requires [`HasLaravelIntegration`](../features/laravel.md).
3+
Includes an Eloquent relation in `fromModel()` hydration only when that relation is actually loaded on the model. Requires [`HasLaravelIntegration`](../laravel/index.md).
44

55
## Syntax
66

‎docs/guide/quick-start.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,4 @@ $updated->email; // 'newemail@example.com'
134134
- [Hydration →](../features/hydration.md) — all input formats, enums, nested DTOs
135135
- [Cast System →](../casts/index.md) — built-in and custom casts
136136
- [Validation →](../features/validation.md) — full Laravel rule support
137-
- [Laravel Integration →](../features/laravel.md) — `fromRequest()`, `fromModel()`, `toResponse()`
137+
- [Laravel Integration →](../laravel/index.md) — `fromRequest()`, `fromModel()`, `toResponse()`

‎docs/integrations/laravel.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ $order = OrderData::fromModel($orderModel);
3939
$order = OrderData::from($orderModel); // same thing — from() understands models
4040
```
4141

42-
See [Laravel Integration](../features/laravel.md) for the full trait reference and [Validation](../features/validation.md) for `#[Rules]`.
42+
See the [Laravel section](../laravel/index.md) for the full trait reference, [Eloquent attribute casting](../laravel/eloquent-casting.md), and [Livewire integration](../laravel/livewire.md), and [Validation](../features/validation.md) for `#[Rules]`.
4343

4444
## Deploy
4545

File renamed without changes.
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ Add it wherever your other build-time steps live — a Forge/Envoyer deploy
149149
script, a Vapor build hook, or a CI job — right before workers restart, so
150150
every worker starts from an already-compiled cache instead of building it
151151
on the first request it happens to serve. See [Metadata
152-
Cache](./cache.md#pre-warming-on-deploy) for what the command actually
153-
scans and writes, and [opcache.preload](./cache.md#going-further-opcachepreload)
152+
Cache](../features/cache.md#pre-warming-on-deploy) for what the command actually
153+
scans and writes, and [opcache.preload](../features/cache.md#going-further-opcachepreload)
154154
to skip the file-read cost too.
155155

156156
### 3. Clear it on rollback
@@ -161,3 +161,10 @@ php artisan tinker --execute="StdOut\SimpleDataObjects\Support\MetadataRegistry:
161161

162162
Run this whenever DTO classes or their attributes change between deploys —
163163
a stale cache entry keeps serving the old compiled shape otherwise.
164+
165+
## Octane / Long-Running Workers
166+
167+
Nothing to configure: metadata and compiled closures live in per-worker
168+
static caches, so after the first request each worker runs entirely from
169+
memory. The validator factory is resolved from the container per call, so
170+
container rebinds between requests are picked up correctly.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class Counter extends Component
5656

5757
## Combining with Laravel Integration
5858

59-
`WireableData` and [`HasLaravelIntegration`](./laravel.md) are independent traits — use either, both, or neither on a given class:
59+
`WireableData` and [`HasLaravelIntegration`](./index.md) are independent traits — use either, both, or neither on a given class:
6060

6161
```php
6262
class OrderData extends BaseData implements \Livewire\Wireable

0 commit comments

Comments
 (0)