anomaly/multiple-field_type
Anomaly Multiple Field Type adds a repeatable, multi-value field type to the Streams Platform for Laravel. It lets developers collect multiple entries of a chosen field type, enabling flexible content models and cleaner admin forms without custom code.
anomaly/multiple-field_type is an addon for the Streams Platform (the data layer behind PyroCMS) that lets you bundle several field types under a single field slug — an "address" field made of street, city, postcode, country, for example. It is not a standalone Laravel package: it is one field type among many, and it only does anything inside a Streams-enabled app.
Minimal setup:
composer require anomaly/multiple-field_type
php artisan addon:install anomaly/multiple_field_type
You need anomaly/streams-platform already installed and a working streams migration set. The addon auto-registers its service provider in PyroCMS; in a bare Laravel + Streams app, check config/app.php or the addon manifest (anomaly/multiple-field_type in the addon registry) and run php artisan addon:publish anomaly.multiple_field_type if you want to override views.
Where to look first, in this order:
src/MultipleFieldType.php — the field type class; confirm the $name, $category, and how it reads its configuration.resources/config/configuration.php — the config schema you'll be filling in when you assign the field.resources/views/ — the form input and the presenter/display view; this tells you exactly what shape the stored value has.resources/lang/ — labels and instructions, useful for knowing what the CP form expects.The first use case to build: a profile or address field on an existing stream that groups three or four scalar sub-fields. Assign it via the control panel (Streams → Fields → New Field, type multiple) or in a migration, then render it in a view. That exercises the whole path — config → form → storage → presenter — before you lean on it for anything structural.
Assigning via migration is the pattern you'll actually live with, because CP assignments don't survive environment promotion:
$this->assignments()->create([
'stream_id' => $stream->id,
'namespace' => 'anomaly.module.users',
'slug' => 'address',
'field_type' => 'anomaly.field_type.multiple',
'config' => [
'fields' => [
['slug' => 'street', 'field_type' => 'anomaly.field_type.text'],
['slug' => 'city', 'field_type' => 'anomaly.field_type.text'],
['slug' => 'country', 'field_type' => 'anomaly.field_type.select',
'config' => ['options' => ['us' => 'US', 'uk' => 'UK']]],
['slug' => 'postcode', 'field_type' => 'anomaly.field_type.text',
'required' => true],
],
],
]);
The fields key is an array of assignments, not just slugs — each entry can carry its own config, required, instructions, and rules. Treat this array as the schema. If you're building a reusable addon, define it in a config/fields.php-style array and loop.
Reading values. The field returns a structured value keyed by sub-field slug, so day-to-day access looks like:
$entry->address; // array/collection of sub-values
$entry->address['city'] ?? null;
$entry->address->get('postcode');
Confirm the exact return shape in the field type's getValue() / presenter before you write accessors — some versions return an AssignmentCollection-backed array, others a plain array. Wrap it once in a model accessor or presenter method so the rest of your app doesn't care:
public function getFullAddressAttribute()
{
$a = $this->address ?: [];
return trim(implode(', ', array_filter([
$a['street'] ?? null, $a['city'] ?? null, $a['postcode'] ?? null,
])));
}
Form integration. Use it like any other field in a Streams form: $form->addForm('entry', 'anomaly.module.x::entries.form') with the assignment present, or drop {{ form.field('address').input|raw }} into a custom view. The sub-fields render inside the parent's markup, so style them by targeting the nested input names (address[city]) rather than assuming flat name="city".
Pairing with repeaters. The most valuable composition is multiple-field-type inside a repeater (or repeater-inside-multiple) for things like line items. Keep the nesting one level deep; deeper combinations get brittle fast and the CP form becomes hard to validate.
Validation. Per-sub-field rules in the assignment config are enforced on the parent field. For anything conditional ("postcode required if country is US"), add a form request or a FormBuilder event listener that reaches into the array — don't try to express it in the config.
API/resource output. Because the value is already structured, serialise it directly rather than flattening: 'address' => $entry->address survives client-side changes better than a concatenated string.
composer.json, commit your composer.lock, and be prepared to fork. If you're starting fresh on Laravel 10/11, audit whether it even installs — the Streams Platform core is the real compatibility constraint, not this addon.config column is your schema, and it's JSON. Renaming a sub-field slug silently orphans the stored data. Add and deprecate; don't rename. Write a migration for any slug change that copies old keys to new ones across the stream's entries table.null, [], or an array of nulls depending on code path. Always ?: [] before array access; don't rely on isset($entry->address['city']) alone.required on a sub-field generally means the parent is required if any sub-field is filled, not that every sub-field must be filled. Test the empty-form submission explicitly.streams_fields (is field_type correct and is config valid JSON?), the addon registration (php artisan addon:list), and whether the view is cached (php artisan view:clear). Nine times out of ten it's a malformed fields array in config.resources/views/vendor/... for markup changes; the package's own views are the display contract and you'll want them intact for reference.name/instructions, not from this addon's lang files. If you're shipping a multi-language CP, set those on the nested config, or the CP falls back to humanised slugs.multiple when the set of sub-fields is variable or when you're building a generic addon that composes other people's field types — that's the problem it was designed for.How can I help you explore Laravel packages today?