emange/list-field_type
A Laravel/PHP field type add-on for the Streams Platform that adds configurable list fields to forms and models. It makes it easy to define, store, validate, and render ordered options or values in PyroCMS/Streams-based applications, with a lightweight setup and straightforward Composer installation...
This is a PyroCMS 3.x / Streams Platform field type addon — not a standalone Laravel package. It only makes sense inside an application that already requires anomaly/streams-platform. Its job is to give you a repeatable array of text or textarea inputs on a stream entry without creating a second stream and a relationship just to hold "features", "specs", "bullet points", or "FAQs".
1. Pull it in via a VCS repository (it is not on Packagist):
"repositories": [
{ "type": "vcs", "url": "https://bitbucket.org/edsters/list-field_type.git" }
],
"require": {
"emange/list-field_type": "dev-master"
}
composer update emange/list-field_type
composer dump-autoload
php artisan addon:install emange.list_field_type
php artisan addon:list # confirm it shows as installed + enabled
2. Read these files first, in this order:
| File | Why |
|---|---|
composer.json |
Confirm "type": "streams-addon", the PSR-4 map (Emange\ListFieldType\ → src/), and the real slug/namespace. |
src/ListFieldTypeServiceProvider.php |
Registration; anything bound here is an extension point. |
src/ListFieldType.php |
The actual field type. Look for $inputView, getSlug(), getConfig(), and any getValue()/setValue() overrides — those tell you how the array is persisted. |
resources/views/input.blade.php |
The markup that produces name="field[]". Publish it before you customize anything. |
3. First use case — a Page "Features" list. In the control panel: Content → Pages → Fields → New Field → choose List. Set the type to text, then on the page form you get a repeating row of inputs. In your page view:
{% if entry.features is not empty %}
<ul class="features">
{% for feature in entry.features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
{% endif %}
Before you trust that, dump the value once ({{ entry.features|json_encode }}) to confirm whether the field type decodes for you or hands back a raw JSON/serialized string.
Never rely on clicking through the CP for production schema. Configure it in code:
use Anomaly\Streams\Platform\Database\Migration\Migration;
class AddFeaturesToPages extends Migration
{
protected $stream = ['slug' => 'pages'];
public function up()
{
if (!$field = $this->fields()->findBySlugAndNamespace('features', 'pages')) {
$field = $this->fields()->create([
'namespace' => 'pages',
'slug' => 'features',
'type' => 'emange.list_field_type', // verify against addon:list
'name' => 'Features',
'config' => [
'mode' => 'text', // or 'textarea'
'placeholder' => 'Add a feature…',
'rows' => 1,
],
]);
}
if (!$this->assignments()->findByFieldIdAndStreamId($field->getId(), $this->stream->getId())) {
$this->assignments()->create([
'stream_id' => $this->stream->getId(),
'field_id' => $field->getId(),
]);
}
}
}
The string in type is addon-namespace.field_type-slug. If emange.list_field_type fails validation, run php artisan tinker and inspect the fields table or the addon listing to get the exact slug the class exposes.
Don't trust the raw value to be an array everywhere (API responses, older rows, exports). Add one accessor and use it consistently:
class PagePresenter extends Presenter
{
public function features()
{
$value = $this->object->features;
if (is_string($value)) {
$value = json_decode($value, true) ?: [];
}
return collect((array) $value)
->map(fn ($row) => is_array($row) ? ($row['value'] ?? '') : $row)
->map(fn ($row) => trim((string) $row))
->filter()
->values();
}
}
Streams search and WHERE clauses will not look inside the array. If editors must find entries by keyword, keep a plain text field in sync via an observer or the repository's save hook, or index externally:
$page->search_blob = $page->features()->implode(' ');
Streams stores the field value per locale. That means a translation can have a different number of rows than the default — which is usually what you want, but it also means the CP translation tab must be re-saved for new rows to appear. Check your templates don't assume count($en) === count($fr).
Publish the view, then:
php artisan addon:publish emange.list_field_type
Rewrite resources/views/input.blade.php keeping the name="{{ $fieldType->getFormName() }}[]" convention, and wire Sortable.js to the container. The posted order is the stored order — there is no separate sort_order column.
Field types don't validate arrays deeply out of the box. Put rules in the form request or the field's config:
'features' => 'array|max:20',
'features.*' => 'nullable|string|max:255',
Then trim and de-duplicate in a saving observer so the stored array never contains blanks.
composer require fails. Add the repositories VCS block and pin a tag or commit hash — with zero stars and zero dependents there is no upstream churn signal, so dev-master is a liability.php artisan addon:install emange.list_field_type), autoloader stale (composer dump-autoload), or the addon class not being discovered because "type" in composer.json isn't streams-addon. Reinstall with php artisan addon:reinstall emange.list_field_type after touching the service provider.VARCHAR(255) will truncate mid-JSON and give you null on decode with no error. Use TEXT on creation, and MEDIUMTEXT/JSON if editors paste long blocks. On MySQL non-strict mode this truncates without warning — check SHOW WARNINGS if data "disappears".foreach over null. {% for x in entry.field %} on an empty or null value can throw depending on the Twig version and how the field type returns empties. Always guard with is not empty or ?? [].implode(', ') output gets stray commas and your count() is wrong.[] suffix is load-bearing. If you customize the view, a missing [] on the name attribute means only the last row posts. Same for hidden "row count" inputs — keep them in sync with the DOM or you'll drop rows on save.$value = is_array($value) ? $value : [$value];.$this->getConfig('mode'), not $this->mode. Assignment-level config overrides field-level config, which is genuinely useful for having the same field behave as text on one stream and textarea on another — but it's a common source of "why is my rows setting ignored".php artisan view:clear. Blade compiles to storage/framework/views; without clearing you'll swear your change wasn't saved.getValue() in a subclass (or bind your own in the service provider) to force collect(), and use a Streams observer on the entry model for normalization and companion-field sync. Avoid overriding setValue() unless you've read the parent — the platform's locale/save pipeline is easy to break there.input.blade.php published so a future upstream change (or disappearance) can't take your admin UI with it.How can I help you explore Laravel packages today?