Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wn System Module Laravel Package

winter/wn-system-module

Core system module for Winter CMS, providing the backend foundation, module management, update and migration tooling, settings, and shared services that power the platform and its plugins.

View on GitHub
Deep Wiki
Context7

Getting Started

winter/wn-system-module is not something you composer require into an existing app — it is the split-published copy of modules/system that ships inside Winter CMS itself. So step one is confirming the version you are actually running against, not what's on Packagist:

composer show winter/wn-system-module   # version tracks your Winter release
php artisan winter:version              # build + installed modules

Because the module is already autoloaded by the root composer.json (Winter\System\ → modules/system/), you consume it by extending it, never by editing it. Treat the repo as read-only reference documentation: a git clone (or just browsing GitHub) is more useful than an install.

Where to look first, in this order:

  1. modules/system/classes/PluginManager.php — how plugins are discovered, registered, enabled, and disabled. Every "why isn't my plugin loading" question ends here.
  2. modules/system/classes/PluginBase.php — the lifecycle contract (register() vs boot()) and the full list of register* hooks available to you.
  3. modules/system/models/File.php + modules/system/traits/ — the file attachment and view/asset/config traits that most day-to-day code leans on.
  4. modules/system/behaviors/SettingsModel.php — the canonical pattern for persisting plugin settings.

First use case: scaffold a plugin and wire it to the system module.

php artisan create:plugin Acme.Blog

Open the generated plugins/acme/blog/Plugin.php. You now inherit from System\Classes\PluginBase, which gives you pluginDetails(), registerPermissions(), registerSettings(), registerNavigation(), registerComponents(), registerSchedule(), and the boot() method for runtime hooks. In boot() you'll immediately touch system-module surfaces: Event::listen(), BackendAuth, System\Models\File, System\Classes\MediaLibrary. That's the module's real role — it's the substrate your plugin sits on.

Implementation Patterns

Plugin lifecycle: register() vs boot(). register() runs for every plugin before any boot() runs; use it to bind container entries, register console commands, and declare extension points. boot() runs after all plugins have registered; use it for Event::listen, route/schedule registration, and anything that depends on another plugin's bindings. Mixing these up produces order-dependent bugs that only appear with certain plugin combinations.

Settings that survive deploys. The supported pattern is System\Behaviors\SettingsModel:

class BlogSettings extends Model
{
    public $implement = [\System\Behaviors\SettingsModel::class];
    public $settingsCode = 'acme_blog_settings';
    public $settingsFields = 'fields.yaml';
}

Read with BlogSettings::instance(), write with BlogSettings::set(['key' => 'value']) then ->save(). For ad-hoc key/value with no UI, System\Models\Parameter::get('acme.blog::api_key') / Parameter::set(...) is the lighter-weight alternative. Register the settings page itself with registerSettings() in Plugin.php returning a System\Classes\Settings\SettingItem.

File attachments without reinventing storage. System\Models\File is a polymorphic attachment model backed by system_files. Declare public $attachOne = ['cover' => \System\Models\File::class] (or $attachMany) on your model and you inherit upload handling, cleanup on delete, and image resizing:

$model->cover()->create(['data' => $uploadedFile]);   // or ->fromFile($path)
$model->cover->getThumb(600, 400, ['mode' => 'crop']);
$model->cover->output();       // streams the response
$model->cover->getPathname();  // absolute disk path

In backend forms this is just type: fileupload, mode: image, imageWidth/imageHeight. Prefer this over hand-rolling uploads — it gives you the backend UI, thumbnail cache, and orphan cleanup for free.

Backend CRUD via behaviors, not hand-written controllers. Backend\Classes\Controller plus FormController, ListController, RelationController, and FilterController in $implement, driven by YAML configs. The win is that other plugins can extend your forms/lists through backend.form.extendFields and backend.list.extendColumns without touching your code. Keep $requiredPermissions set — it's the cheapest security control you'll ever write.

AJAX handlers follow the system module's conventions. Handlers are public methods prefixed on; they return partial/update arrays or throw AjaxException. Delegate to behaviors rather than duplicating logic:

public function onSave()
{
    return $this->asExtension('FormController')->update_onSave();
}

In markup: data-request="onSave", data-request-update="{'@form': '#form'}". Flash messages via Flash::success() / Flash::error() render through the module's partials.

Media library for programmatic uploads. System\Classes\MediaLibrary::instance() handles anything that isn't model-attached — shared branding assets, imported images, user-generated content outside a record:

MediaLibrary::instance()->put('/imports/logo.png', $contents);
MediaLibrary::instance()->getThumbnail('/imports/logo.png', 200, 200);
MediaLibrary::instance()->listFolderContents('/imports');

Paths are always relative to the media disk root and should start with /.

Asset pipeline. System\Traits\AssetMaker gives you addJs(), addCss(), addRss() on controllers and widgets. When you need bundling, System\Classes\CombineAssets::combine([...]) returns a single cached URL, and registerFilter() lets you pipe SCSS/LESS through a compiler. php artisan winter:util compile assets should be part of your deploy script so the combiner cache is warm before traffic hits.

Cross-plugin extension. Two mechanisms, both worth knowing: Laravel-style Event::listen('backend.menu.extendItems', ...) for late binding, and PluginBase's $this->extend()-style hooks (FormController::extendFormFields, ListController::extendListColumns, Controller::extendListFilterScopes) for widget-level changes. Prefer events — they're easier to reason about and don't create hard class dependencies.

Versioning and migrations. Plugin schema changes are driven by updates/version.yaml mapping version strings to migration files, executed by System\Classes\VersionManager during winter:update. If you add a migration file but forget to bump the version and reference it in version.yaml, it will silently never run — this is the single most common "my column doesn't exist in production" bug in the ecosystem.

Gotchas and Tips

Never edit modules/system in place. It's core, it's overwritten on the next composer update / winter:update, and if you genuinely need a behavior change the answer is an upstream PR or an event hook. If your team has vendored patches, you have a fork maintenance problem, not a configuration problem.

Low stars, zero dependents is expected — don't read it as neglect. This repo is a subtree split of a monolithic CMS. Its "adoption" shows up as Winter installs, not as a Composer dependency graph. Judge it by the release cadence of wintercms/winter, not by this repo's metrics.

Directory casing is a classic trap. Plugin directories are lowercase (plugins/acme/blog) while namespaces are StudlyCase (Acme\Blog). On case-sensitive Linux this works; on macOS's default filesystem a mis-cased directory will appear to work locally and explode on deploy. PluginManager::instance()->getPlugin('Acme.Blog') also returns null for disabled plugins even when files exist — check exists() before assuming a typo.

Settings cache and stale reads. SettingsModel::instance() caches in memory. If you write via Parameter::set() in a seeder and read via a settings model (or vice versa), you're using two different stores — system_parameters for Parameter, the settings model's own code for the other. Pick one per concern.

Thumbnails go stale after replacing an image. getThumb() writes into a hashed cache path; replacing the underlying file doesn't invalidate it. Purge with php artisan winter:util purge thumbnails in your deploy, or use the System\Classes\ImageResizer interface directly if you need different invalidation semantics. Also confirm GD or Imagick is actually compiled in — the failure mode is a blank 0-byte image, not an exception.

The asset combiner needs writable storage. If storage/app isn't writable by the web user, assets silently serve un-combined or 404 in production while working fine locally. Compile them at deploy time rather than at first request.

Permission string mismatches fail as 403, not as errors. Permission codes are namespaced and lowercased (acme.blog.manage_posts) by convention. A typo in $requiredPermissions or in a hasPermission() call produces a dead-looking backend page with no log entry. When a menu item vanishes for a non-admin user, check the permission name character by character.

backend.form.extendFields fires for every form in the backend. Always guard: if (!$widget->model instanceof MyModel) return; or match on $widget->alias. Unguarded listeners are a leading cause of "why does my plugin break unrelated admin pages."

Log tables grow unbounded. system_event_logs and system_request_logs are convenient (EventLog::add($message, $details)) but nobody prunes them by default. On a busy install, add a scheduled cleanup or cap retention in Backend → Settings → Logs before the table hits a million rows.

Debugging order of operations. trace_log() and Log::debug() write through the module's logging setup; the backend's Event Log screen is the fast path for production issues where you can't tail a file. For plugin loading problems, PluginManager::instance()->getPluginNames(), ->isDisabled($name), and ->getPluginPath($name) will tell you in three calls whether the issue is discovery, enablement, or namespace resolution — which is almost always the whole diagnostic tree.

Extension points to remember when the built-in behavior is almost right: System\Classes\SettingsManager (add settings pages), System\Classes\NavigationManager (sidebar items), System\Classes\MailManager (mail templates and layouts), BackendAuth (user/permission checks), and registerSchedule() for cron-driven work. Reaching for any of these is better than forking a behavior — and it's exactly what the module is designed to support.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource