nvl / pages
Hierarchical, translatable, content-composed pages and dynamic page resources for Laravel
Requires
- php: ^8.4
- ext-filter: *
- ext-json: *
- ext-mbstring: *
- laravel/framework: ^13.0
- nesbot/carbon: ^3.8
- nvl/core: ^7.0
- nvl/translatable: ^7.0
- symfony/http-kernel: ^7.2 || ^8.0
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.27
- mockery/mockery: ^1.6
- nvl/content: ^7.0
- nvl/filterable: ^7.0
- nvl/media: ^7.0
- nvl/metafields: ^7.0
- nvl/ownership: ^7.0
- nvl/seo: ^7.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- spatie/laravel-data: ^4.23
Suggests
- nvl/content: Enables explicitly selected Page content composition.
- nvl/metafields: Adds optional Page metafield relationships for consumer-owned composition.
- nvl/ownership: Adds optional model-declared ownership, isolation, adoption and captured worker contexts.
- nvl/seo: Enables explicitly selected SEO profiles and sitemaps.
Provides
None
Conflicts
- nvl/content: <7.0 || >=8.0
- nvl/metafields: <7.0 || >=8.0
- nvl/ownership: <7.0 || >=8.0
- nvl/seo: <7.0 || >=8.0
Replaces
None
README
Localized hierarchical Pages for PHP 8.4 and Laravel 13. Core and Translatable are required. Plain Pages load without Content, SEO, Filterable, Metafields or application DTO libraries. Lifecycle operations and authenticated reads reload Eloquent actors from their class-defined canonical storage before host policies inspect them. Dirty keys, altered storage, detached actors and unavailable rows reject; caller attributes and loaded relations do not carry policy privileges. External identity providers retain their own freshness contract. Private previews require a persisted Eloquent actor. Explicit system actors remain subject to host policy.
Pages owns structural identities, translations, hierarchy, publication windows and native resource resolution. The consuming application owns Actions, input/output data, editor assembly, routes, controllers and preview HTTP responses.
Native services
Inject services from Nvl\Pages\Services:
| Service | Operations | Results |
|---|---|---|
PageLifecycle |
create, update, move, delete, restore |
Refreshed Page models; deletion returns bool |
PageReader |
get, paginate, children, navigation, keyAvailable |
Models, Laravel collections/paginators or documented arrays |
PageResolver |
resolve, preview |
['page' => Page, 'resource' => ?array] |
$page = $pages->create([ 'key' => 'about', 'slug' => 'about', 'site' => 'default', 'status' => 'published', 'translations' => ['en' => ['title' => 'About us']], ], $actor); $page = $pages->update($page, [ 'slug' => 'about', 'kind' => 'static', 'status' => 'draft', 'translations' => ['en' => ['title' => 'About us']], ], $page->revision, $actor); $page = $pages->move($page, $parentId, $position, $page->revision, $actor); $pages->delete($page, $page->revision, $actor); $page = $pages->restore($page, $page->revision, $actor); $result = $resolver->resolve($path, $site, $locale); $preview = $resolver->preview($path, $site, $locale, $persistedActor);
Creation and replacement accept the documented native attribute arrays. Replacement retains the original complete-replacement semantics; omitted defaulted editorial fields reset to their defaults. Key, site, parent identity and ownership cannot be changed through replacement. Move owns parent changes. Expected revisions are separate arguments. Unknown/protected fields are rejected inside the service.
Page keys are globally unique across sites in an unowned installation; use site-prefixed keys when sites share storage. Native Ownership adoption replaces that identity with the documented owner partition. Slugs and paths are site scoped.
Mutations preserve serialized site-tree locks, cycle/depth checks, descendant path rebuilding, unique key/slug constraints, source-connection transactions and committed events. Locale replacement uses Translatable's supported related-row store inside the Page transaction. An enclosing host transaction remains authoritative.
Tree locks and lifecycle transactions use the configured Page model's actual connection. Existing Page inputs supply their identity; the persisted site determines the lock scope, even if the caller edited its in-memory attributes.
Authorization and public resolution
Bind Nvl\Pages\Contracts\PageAuthorization for application policy. It receives PageAbility, an explicit Laravel authenticatable, SystemActor or PublicActor, the canonical Page where applicable, and documented native context facts. Background identity is supplied only by trusted application code and requires explicit consumer authorization for management. The default adapter grants eligible public reads; it does not grant background management authority.
Public resolution always authorizes PublicActor::Anonymous; preview requires a persisted authenticatable and separate Preview authorization. The default policy does not grant private previews. Dynamic resolution first authorizes the matched Page with resourceId => null, before handler validation or data access, then authorizes the resolved resource identity before returning it. Policies must allow the page-level check independently of resource-level permission. Withdrawal from Published or Scheduled to Draft requires Publish authority. Public matching preserves the original not-found failures, publication/expiration windows, exact site, locale and owner isolation. Caller context cannot replace canonical site/path/resource facts.
For isolated configured Page models, enter native Ownership using a verified public owner/site mapping before resolution. PublicOwnerSiteRunner::run($canonicalOrigin, $callback) accepts the canonical origin string. The application owns request parsing. A supplied site must match the verified site; a public admission grants no management rights.
PageReader::children() and navigation() apply public visibility. get() returns a canonical model with localized copy and accepts an explicit PublicActor::Anonymous for public reads: publication/expiration windows and any verified public site constrain its native query before View authorization. Authenticated and trusted Background reads retain their explicit host policy. paginate() authorizes management reads. Pagination accepts fixed native filters for status, kind, parent, navigable and sort; it does not require Filterable. Consumers select safe fields and loaded translations for their response projections.
ConfiguredPageAuthorization is a supported public policy for composition with a host's PageAuthorization adapter. It reads canonical Page storage for public View decisions, retains the verified site, permits navigation and denies other capabilities. Hosts can delegate these public capabilities to it and implement their own management policy without importing an internal class or copying native publication rules.
Dynamic resources
Register stable aliases under nvl-pages.resources. PageResourceHandler retains route patterns, exact route-parameter rules, constrained queries/fetching, sanitized presentation and optional sitemap entries. Request records are array{pageId:string,site:string,locale:string,parameters:array<string,string>}. Presentation records are array{type:string,id:string,payload:array<string,mixed>}.
After a page matches its route and rules, a missing dynamic resource raises NotFoundHttpException; resolution does not try lower-priority pages. Authenticated get resolves a canonical row before its per-page policy, so hosts choose their own public error mapping if existence must be concealed.
Owned handlers implement OwnershipSafePageResourceHandler and declare the exact native model. Pages checks both query and returned model identity under the admitted resource. It does not infer ownership from route parameters or presentation IDs.
Optional composition
nvl-pages.integrations.content, seo and metafields accept null for loaded-provider detection, false to disable, or true to require the provider. Explicitly enabled missing providers fail during boot. The base Page has no hard Content or SEO interface/trait dependency.
When selected, the existing adapters register the configured Page identity, Content group content, scoped SEO profiles/sitemaps and optional model relations. Hard deletion retains Content's atomic owner cleanup. Consumers compose Content rendering and SEO output through those packages' native services; Pages returns its own canonical model and resource record.
Installation and verification
Configure nvl-pages.models.page, connection, table identities, authorization, URL generation and resource handlers. Select one owner for migrations: vendor loading or host-published copies. Preserve existing IDs, revision history and reviewed native Ownership adoption. The legacy Tenancy bridge and package HTTP routes are removed.
Publish canonical nvl-pages-config, nvl-pages-migrations and nvl-pages-skills resources as needed. Run nvl:pages:doctor --strict --format=json for schema, resource, optional-adapter and tree diagnostics. When Ownership is declared, its adoption step provisions the separate owner tree-lock table; doctor verifies that table as part of readiness. nvl-pages.limits exposes maximum_path_bytes (768), maximum_resource_parameters (8), maximum_per_page (100) and maximum_public_children (100). Hierarchy depth is configurable from one to four levels. Host configuration/cache/worker deployment steps remain application-owned.
Shared service contracts, model extensions and composition apply alongside the package-specific rules above.