winter/base
Winter/base is the core foundation package of Winter CMS, a free, open-source Laravel-based content management system. It provides the essential modules, helpers, and framework glue that power the platform, giving developers a solid base for building fast, extensible websites and applications.
modules/ and plugins/ — composer create-project wintercms/winter my-site (or clone the repo for core work), run php artisan winter:install, then php artisan winter:up. Everything you extend lives in plugins/<author>/<name>/; everything you inherit lives in modules/{system,backend,cms} and vendor/winter/storm.vendor/winter/storm/src/<Module>/README.md — each module catalogues its public API. grep -rl 'function <thing>' vendor/winter/storm/src/ vendor/laravel/framework/src/ vendor/symfony/ is a ten-second answer.php artisan create:plugin Acme.Blog and php artisan create:controller Acme.Blog Posts, then php artisan create:component Acme.Blog PostList. This exercises the three things you'll do daily: plugin registration (Plugin.php), backend CRUD via Backend\Classes\Controller + config_form.yaml/config_list.yaml, and CMS components with components/PostList.php + default.htm.themes_path(), plugins_path(), media_path(), uploads_path(), temp_path() are autoloaded by Storm. Use them instead of base_path('themes'). They're the single most common source of "works locally, breaks in a different layout" bugs.php artisan winter:mirror public --relative after adding any plugin (assets 404 otherwise), and vendor/bin/phpcs -nq --report=full --extensions=php <changed files> before every push — CI's PHP style job fails the whole PR in ~15s and is trivially reproducible locally.config_list.yaml and config_form.yaml drive list columns, filters, form fields, and relations. Reach for $widget->addTabFields() / getListColumns() overrides only when YAML can't express it; the widget system will fight you if you try to render forms by hand.Winter\Storm\Argon\Database\Model with behaviors declared in $implement — Backend\Behaviors\FormController, ListController, RelationController, Winter\Storm\Database\Behaviors\Purgeable, Sortable, Sluggable. Validation lives in $rules, attribute transformation in getAttribute()/setAttribute() mutators or $jsonable/$dates casts.[postList] in the template's config block), and let it own its AJAX handlers via onRun()/onHandle(). Prefer $this->page['posts'] = ... and a partial include over embedding query logic in Twig.Plugin.php — registerComponents(), registerNavigation(), registerMarkupTags(), registerSchedule(), boot(). Keep boot() cheap; it runs on every request. Defer heavy work into service providers registered through register() or into Event::listen closures.PathResolver::resolve()/within()/join()/standardize() for path math (boundary-safe, unlike str_starts_with), Str::/Arr::/collect() for data munging, Symfony\Component\Finder\Finder::create()->files()->name('*.less')->in($dir) for directory walks, Symfony\Component\Filesystem\Filesystem::dumpFile() for atomic writes, Symfony\Component\Process\Process for shelling out, Http::get() for outbound HTTP.assets/css, assets/js + $this->addCss()), Mix, and Vite (vite:install/vite:compile <Plugin>). For new plugins use Vite; for a two-line CSS tweak in a legacy backend, edit the source and re-mirror.Event::listen on the backend's documented events, Backend\Widgets\* extend/extendFormFields closures in a plugin's boot(), ListController::extendListColumns, FormController::extendFormFields, MarkupTag registration for Twig helpers, and System\Classes\PluginManager for cross-plugin discovery. Never edit modules/ in a deployed site.Queue::push, php artisan queue:work) or the scheduler in Plugin::registerSchedule() — not synchronous loops in a controller. Winter installs default to the sync driver, which will silently make a "background" job block the request; switch to database or redis early.System\Tests\Bootstrap\PluginTestCase when the test needs plugins and auth, System\Tests\Bootstrap\TestCase when it only needs the framework. Temp files that flow through the asset combiner must live under base_path() — use base_path('storage/framework/cache/<unique>') and File::deleteDirectory() in tearDown().Winter\Storm\Support\ClassLoader lowercases every namespace directory segment while the filename keeps PascalCase: System\Twig\Node\Foo → modules/system/twig/node/Foo.php. A capitalized directory works on macOS/Windows and fails on Linux CI with Class not found. When Windows CI is green and every Ubuntu job is red, suspect directory case before anything else.php artisan winter:mirror public --relative — and a 404'd JS module served as text/html gets ORB-blocked, so the UI renders blank with no console error; check the network tab first. (2) vite:compile resolves the plugin's real path, so Node can't find the project's node_modules — symlink them in. (3) node_modules/.vite caching means a source edit can produce identical output; rm -rf it. (4) Prefer PathResolver::within()/resolve() over realpath() + str_starts_with when touching asset resolution — the within check is separator-boundary safe.vite:watch writes an unreachable hot-file URL on IPv6 hosts — laravel-vite-plugin writes http://[::]:5173 into assets/dist/hot, which browsers can't reach, and the backend loads unstyled. Fix with echo -n 'http://localhost:5173' > <plugin>/assets/dist/hot, reload once, and make sure the file is removed when you stop the watcher or the site keeps pointing at a dead server.<?php if ($x): ?>attr="…"<?php endif ?> on one line fails "Newline required after opening brace". Put if:, the body, and endif on their own lines. Run phpcs on changed files before pushing — it's the fastest failing CI job.vendor/ drift causes irreproducible test failures. A test that fails locally but passes on a fresh clone is usually a stale vendor/winter/storm; composer update in Storm (or reinstall) before blaming the environment. Same for symlinked dev setups — verify vendor/winter/storm points where you think it does.modules/; generic filesystem, path, parser, and network primitives belong in Storm. If Storm needs CMS-specific policy (e.g. which directories count as theme asset roots), add a public setter on the Storm class and let the module supply it — never reach into module constants from Storm. This is what keeps forks mergable.php artisan config:cache during deploy will freeze config/app.php and any env-derived plugin settings; remember config:clear when a config change "doesn't take". Similarly, plugin Plugin.php registration is cached in the plugin registry — php artisan plugin:refresh <Author.Plugin> when a newly added component or navigation item doesn't appear.sync. Anything you push to a queue runs inline until you change QUEUE_CONNECTION. This turns "background" image processing into a request timeout that only shows up under load.winter:up after pulling. Migrations, asset mirrors, and version bumps are not applied by git pull or composer update alone. A site that loads a half-migrated schema is a confusing place to debug.How can I help you explore Laravel packages today?