Search by

nvl / metafields

nicolas_vls

Typed, polymorphic and translatable metafields for Laravel

v7.0.0 2026-11-03 15:10 UTC

This package is auto-updated.

Last update: 2026-11-04 13:03:43 UTC


README

Typed, optionally localized custom fields for registered Eloquent owners on Laravel 13 and PHP 8.4. This package provides native services and model results; hosts own their HTTP and UI layers.

Permanent owner deletion is an explicit host lifecycle. Grant metafields.purge, authorize PurgeOwnerValues and retain MutateOwner/DeleteOwnerValue policy. Inside an existing transaction on the exact owner/value connection, call MetafieldValues::purgeBeforeOwnerDeletion($owner, $actor), then permanently delete that owner before committing. The service locks the canonical owner, purges active and cleared values/locales in bounded batches, preserves definitions and other owners, and publishes its synchronization fact after the outer commit. A connection alias cannot substitute for the same transaction. Normal soft deletion retains values; no destructive trait hooks run automatically. Raw deletion and key reuse bypass this lifecycle.

Doctor's owner-reference sample requires a host MetafieldDiagnosticAccess binding explicitly approving SystemActor::Background and current Ownership admission when declared. Its default denies and reports inspection skipped. This dedicated read policy never routes a background enum through unrelated global Gate hooks. It inspects at most 100 visible value references, detects additional rows with one sentinel, and retains configured host scopes while including soft-deleted owners. Unresolved references may be scoped out or unregistered; this is a bounded diagnostic, not a global orphan inventory or deletion authority. Previously orphaned identities require reviewed host recovery before reuse.

Install and register owners

Install nvl/metafields:^7.0 with its declared nvl/core and nvl/translatable dependencies. Publish the package configuration with Laravel vendor publishing when overrides are needed, choose one migration owner, and run the package migrations. Configure authorization explicitly before enabling operations.

Configure stable aliases in nvl-metafields.owners and nvl-metafields.reference_models:

'owners' => [
    'products' => [
        'model' => App\Models\Product::class,
        'label' => 'Products',
        'sections' => ['general'],
        'supported_types' => ['string', 'integer', 'reference'],
    ],
],
'reference_models' => [
    'categories' => App\Models\Category::class,
],

Owners and reference targets must be persisted, registered models. Values store the host model's native morph identity and exact string representation of its key. Configure native Ownership resources when isolating definitions or owners; unscoped hosts can use ordinary models.

Authorize native operations

Every public operation requires an explicit persisted Authenticatable actor or SystemActor::Background. Host Gate abilities named metafields.create, metafields.update, metafields.archive, metafields.delete, metafields.grant, metafields.revoke, metafields.import, metafields.write, and metafields.read authorize the respective operations. Undefined abilities deny both people and background work. The package does not infer an actor from a guard.

Definition creation, listing, updating, archiving, and deletion additionally call MetafieldAuthorization::authorizeDefinition with the freshly loaded actor. Update, archive, and delete receive the canonical definition locked inside their transaction. The default adapter uses nvl-metafields.authorization.definition_ability when configured, passing the capability string and optional definition to that Gate. Otherwise it uses the native metafields.create, metafields.read, metafields.update, or metafields.delete Gate with the optional definition; archive uses UpdateDefinition in addition to its coarse metafields.archive Gate. Catalog grants and imports retain their independent MetafieldCatalogAccess boundary.

Owner reads and writes also use MetafieldAuthorization::authorizeOwner with the canonical persisted owner. Writes authorize MutateOwner inside the owner lock; clearing a value additionally authorizes DeleteOwnerValue with its persisted definition. Configure nvl-metafields.authorization.owner_ability or bind that contract; the default requires the host's update policy for the owner. Reference writes use MetafieldReferenceAuthorization and deny unless the host configures reference_ability or binds that contract. MetafieldBatchAuthorization is required for nonempty multi-owner reads and supplies SQL visibility for assignments, values, and targets.

Definitions

use Nvl\Metafields\Services\MetafieldDefinitions;

$definition = app(MetafieldDefinitions::class)->create([
    'namespace' => 'product',
    'key' => 'headline',
    'type' => 'string',
    'assignment' => ['ownerType' => 'products', 'section' => 'general'],
    'translations' => ['en' => ['title' => 'Headline']],
], $actor);

$updated = app(MetafieldDefinitions::class)->update(
    $definition,
    ['translations' => ['en' => ['hint' => 'Short headline']]],
    $definition->revision,
    $actor,
);

MetafieldDefinitions exposes create(array $attributes, $actor), update($definition, array $attributes, int $expectedRevision, $actor), archive($definition, bool $archived, int $expectedRevision, $actor), delete($definition, int $expectedRevision, $actor, bool $deleteValues = false), grant(string $definitionId, int $sourceRevision, $actor), revoke(string $grantId, int $expectedRevision, $actor), and import(array $request, $actor). Definition and assignment input is allowlisted, and owner type, section, reference target, locale, default, and rules are validated against registered capabilities. delete requires explicit cascading when live values exist. Import uses an exact granted catalog snapshot with expected revisions and an idempotency key.

Values and reads

use Nvl\Metafields\Services\MetafieldValues;
use Nvl\Metafields\Services\MetafieldReader;

$value = app(MetafieldValues::class)->set($product, 'product.headline', 'Hello', $actor);
$fields = app(MetafieldReader::class)->forOwner($product, $actor, 'en');

This example creates the initial value. Subsequent set calls must supply expectedRevision: $value->revision from the last canonical read; existing localized values also retain their locale-specific input rules.

MetafieldValues exposes set(Model $owner, string $handle, mixed $value, $actor, ?string $locale = null, ?int $expectedRevision = null), delete(Model $owner, MetafieldDefinition|string $definition, int $expectedRevision, $actor), and sync(Model $owner, array $items, $actor). Each sync item has definitionId and exactly one of value, translations, or a true clear; an existing value requires expected_revision in that item. Omission never writes null; use an explicit nullable value or clear. Localized writes accept translation_mode as TranslationSyncMode::Patch or Replace only alongside translations; omitted locales are preserved by patch and explicit null locale values are rejected. A sync validates every item against its persisted definition and commits atomically.

MetafieldReader exposes definitions($actor), forOwner(Model $owner, $actor, ?string $locale = null), and forOwners(array $owners, $actor, ?string $locale = null). The first two return collections of native models or arrays containing definition, assignment, and nullable value models. The batch result contains owners as a nested JSON object keyed by morph type and exact owner key, plus a separate order list. Empty input returns an empty object map. Nonempty batches admit canonical owners under host scopes, apply SQL visibility, and reject unavailable or denied stored and default references. Batches have bounded owner, definition, value, reference, and locale limits.

For stored scalar comparisons on active filterable definitions, use whereNvlMetafield($handle, $value, $actor, $batchPolicy, $operator = '=') on a registered owner query. Missing and default-only values do not match.

Events and diagnostics

MetafieldDefinitionCatalogGrantAudited, MetafieldSet, and MetafieldsSynced contain scalar identities and revisions and dispatch after the source transaction commits. See events. Run php artisan nvl:metafields:doctor --strict --format=json for package diagnostics. Core does not register a shared nvl:doctor by default.

Definition CLI mutations use SystemActor::Background. Grant the appropriate metafields.* capability and native definition/owner policy explicitly; running a command supplies no background bypass.

Verify

From the suite root, run the owning Pest suite with its package configuration:

vendor/bin/pest --test-directory=packages/nvl/metafields/tests --configuration=packages/nvl/metafields/phpunit.xml.dist --bootstrap=vendor/autoload.php

The suite's tests/Historical directory preserves retired HTTP, Action, DTO, and Tenancy bridge tests outside active discovery. Native service, ownership, localization, reference, and batch tests are active.

Shared service contracts, model extensions and composition apply alongside the package-specific rules above.