winter/wn-test-plugin
A test plugin for Winter CMS that provides sample models, controllers, migrations, and backend features for exercising plugin behavior and automated tests. Useful as a reference when building or validating Winter CMS plugins.
This is Winter CMS's own test/playground plugin — the fixture the core team uses to exercise backend forms, lists, relations, components and migrations. You don't "adopt" it; you install it into a development Winter instance and read it.
As of v3.0.0 it requires Winter v1.2.8 or newer. Check your core version first — on older cores the backend create/update/preview pages render empty, because the plugin no longer ships those view files and relies on the defaults added in v1.2.8.
Minimal setup:
composer require winter/wn-test-plugin --dev
php artisan winter:up # runs migrations + update steps
php artisan serve
It installs to plugins/winter/test/ (Winter's composer installer maps the package name to the plugin path), and it should appear in the backend under its own navigation entry.
Look here first, in this order:
Plugin.php — the whole registration surface: registerComponents(), registerNavigation(), registerSettings(), registerMarkupTags(), registerSchedule(). This file is effectively the table of contents for the plugin.models/ + updates/ — model definitions next to the migrations that create them, with $belongsTo/$belongsToMany/$attachMany declared alongside.controllers/ + controllers/<name>/config_form.yaml and config_list.yaml — the YAML-driven backend CRUD pattern, plus $formLayout = 'fancy' on the Pages and Records controllers. Note there are no create.htm/update.htm/preview.htm/reorder.htm or list-toolbar partials anymore; core supplies those.components/ — a component class plus its default.htm partial.First use case: clone it into a fresh project as a working reference while you build your first real plugin. Run php artisan create:plugin Acme\Demo, then keep plugins/winter/test open side by side and copy the shape of each piece rather than inventing it. The second use case is a smoke test: if the test plugin's backend pages render and its migrations run, your Winter install, database and asset pipeline are healthy.
Reference-driven scaffolding. The plugin is most valuable as a canonical example set. Grep for the pattern you need rather than reading docs:
grep -rn "belongsToMany" plugins/winter/test/models
grep -rn "registerComponents" plugins/winter/test/Plugin.php
grep -rn "type: fieldset" plugins/winter/test/controllers/*/config_form.yaml
grep -rn "formLayout" plugins/winter/test/controllers
Every Winter feature the core supports tends to have a minimal, working instance here — relations, file attachments, repeater widgets, list filters/scopes, backend permissions, and (new in v3.0.0) fieldset, url and sensitive form widgets.
Form field widgets as copy-paste references. The Records form is now the best single file to crib from. It demonstrates:
fieldset grouping contact details under one legend, with a nested datepicker — note the fields still save to the model as top-level attributes, the fieldset is purely presentational;url field with suggestion options;sensitive widget inside a repeater (for values you want masked in the UI but still round-trip through the model);taglist relation-mode examples, now grouped on the FormWidgets tab;Complex group in the grouped repeater on the Repeater tab.Copy the YAML shape, then adjust only the field names and tab structure.
Prefer $formLayout over controller overrides. v3.0.0 deleted the per-controller BackendMenu::setContext() constructors, $bodyClass overrides, and the Pages::index()/create()/update() overrides in favour of the declarative $formLayout = 'fancy' property. If your own controllers carry that scaffolding, this is the moment to delete it: set $formLayout on the controller and let core's default views handle context and chrome. Same for the 68 create/update/preview/reorder/toolbar views the plugin used to ship — don't reintroduce them.
Live sandbox for your own code. Because it ships models you can mutate freely, it's a safe target for experiments: attach one of your plugin's models to a test model via a relation, drop one of your components onto a test page, or point a backend widget at its controller. You get real data and real CRUD without polluting a client project.
Component smoke tests in pages/partials. If it registers components, drop one into a CMS page to verify the entire theme → component → partial chain works:
{% component 'testComponent' %}
This is the fastest way to confirm your theme's {% framework %}/{% scripts %} includes and AJAX handler registration are wired correctly.
Forking instead of depending. For day-to-day work, copy the plugin directory into your own repo as plugins/acme/playground and rename the namespace. It's a template, not a library — vendoring gives you a stable reference you control and lets you delete the parts you don't need. Keep in mind you'll now also need Winter v1.2.8+ in the target project, since the fork inherits the same reliance on core default views.
CI sanity fixture. In a pipeline that tests a Winter plugin, install this as a dev dependency and run php artisan winter:up before your own migrations. It exercises the plugin loader, the migration runner and the backend boot path — a cheap integration assertion that your CI environment is actually functional, not just that PHP starts. Pin the combination of plugin version and core version, since v3.0.0 raises the floor.
Upgrade verification. After bumping Winter core, boot an instance with this plugin installed and click through its backend pages. It touches enough of the framework that breakage surfaces here before it surfaces in your own plugin.
composer update, check Winter core version before debugging your own code.create/update/preview/reorder or toolbar partials from an older tag into your project; they were removed because core's defaults now do the job. Any local fork that still carries them will silently shadow the new behaviour..htm to .php. If you patched or extended a partial by path, your include will break. Re-grep for partial names after upgrading.label / label_plural translations. Core's default views use them to title pages, and v3.0.0 filled in the missing ones for the bundled models. Your own models will show raw or empty titles without them.--dev, and pin a version if you're relying on it in CI.updates/ will create real tables. Use a scratch database or SQLite in-memory for local experiments; don't install it against a shared dev database you care about.composer.lock — hence --dev plus a note in the README.Plugin.php's $plugin->name/author, the namespace, the composer package name, and the directory together — Winter will silently load only one of them otherwise.php artisan plugin:refresh Winter.Test to roll back and re-run its migrations, and php artisan cache:clear after touching Plugin.php if registrations seem stale.plugins/. If your project overrides the plugin path, or if allow-plugins blocks the installer, the package ends up in vendor/ and Winter never sees it. Symptom: composer require succeeds, nothing appears in the backend. Check plugins/winter/test exists on disk.composer update. If you extend CustomFile, match the new return type declarations on getPartitionDirectory(), getStorageDirectory(), getPublicPath() and isLocalStorage() or PHP will throw a signature mismatch.debug => true in config/app.php and watch storage/logs/. The YAML configs (config_form.yaml, config_list.yaml) fail loudly on typos in field names but quietly on wrong list column names — a blank column usually means the attribute doesn't exist on the model, not that the widget is broken. Mislabelled or missing partial/hint paths also tend to fail silently; v3.0.0 fixed several of these in the Records form, so compare your paths against it.How can I help you explore Laravel packages today?