Winter core sits on top of Winter Storm (installed at vendor/winter/storm/), which sits on top of Laravel, which sits on top of Symfony components. The helper you want almost certainly already exists at one of those layers. Search Storm → Laravel → Symfony before writing any "small utility", often with safer edge-case handling than a fresh implementation would have.
-
Storm itself — each
vendor/winter/storm/src/<Module>/README.mdcatalogues that module's public API:Filesystem/—PathResolver(resolve,within,join,standardize),Filesystem(extends Illuminate's; addsisAbsolutePath,symbolizePath,existsInsensitive,chmodRecursive)Support/— strings, arrays, class loadingNetwork/,Html/,Parse/,Database/,Halcyon/,Auth/, etc.- Path helpers (always loaded):
themes_path(),plugins_path(),media_path(),uploads_path(),temp_path()— use these instead ofbase_path('themes')etc.
-
Laravel (Illuminate) — everything Laravel ships is available:
Illuminate\Support\Str—Str::startsWith/endsWith/contains/before/after/between/slug/camel/snake/kebab/studly/random/uuid/limit/mask/finish/start/of/headline/title. Use instead of regex one-liners.Illuminate\Support\Arr—Arr::get/set/has/forget/only/except/dot/undot/flatten/pluck/wrap/first/last/where. Use instead of nested foreach.Illuminate\Support\Collection(viacollect()) — chainable map/filter/reduce.Illuminate\Filesystem\Filesystem—deleteDirectory(),cleanDirectory(),copyDirectory(),moveDirectory(),allFiles(),glob(),isDirectory(),prepend(),append(),replace(),hash().- Global helpers:
data_get/set/fill,value,tap,optional,transform,head,last,class_basename,now,today,e,__/trans,cache,config,env,app,resolve,route,url,report,rescue,retry,throw_if/unless,abort/abort_if/unless. - Facades:
Cache,Config,DB,Event,File,Hash,Http,Lang,Log,Mail,Queue,Redis,Route,Schema,Session,Storage,URL,Validator,View.
-
Symfony components at
vendor/symfony/:console,css-selector,error-handler,event-dispatcher,finder,http-foundation,http-kernel,mailer,mime,process,routing,string,translation,uid,var-dumper,yaml. Most commonly reached for:Symfony\Component\Finder\Finder—Finder::create()->files()->name('*.less')->in($dir)replaces RecursiveIteratorIterator chains.Symfony\Component\Filesystem\Filesystem—dumpFile()(atomic write),mirror(),mkdir()(idempotent),remove(),symlink().Symfony\Component\Process\Process— safe external command execution instead ofexec()/shell_exec().Symfony\Component\Yaml\Yaml— strict YAML.Symfony\Component\String\— Unicode-aware strings.Symfony\Component\Uid\Uuid/Ulid— UUID/ULID generation.
grep -rl 'function <thing>' vendor/winter/storm/src/ vendor/laravel/framework/src/ vendor/symfony/ is a 10-second check.
Paths and filesystem:
| If you reach for… | Use this instead |
|---|---|
realpath() + null-check + slash-trim |
\Winter\Storm\Filesystem\PathResolver::resolve() |
str_starts_with($path, $root) to gate file access |
PathResolver::within($path, $root) — separator-boundary safe |
Manual base_path('themes') / base_path('plugins') |
themes_path() / plugins_path() (Storm's autoloaded helpers) |
Recursive rmrf in tests |
\File::deleteDirectory($path) (Laravel facade) |
| Detect absolute path | (new \Winter\Storm\Filesystem\Filesystem())->isAbsolutePath($path) |
Custom path-symbol resolution (~/...) |
(new \Winter\Storm\Filesystem\Filesystem())->symbolizePath($path) |
str_replace('\\', '/', $path) (cross-platform comparison) |
(new \Winter\Storm\Filesystem\Filesystem())->normalizePath($path) |
str_replace('/', DIRECTORY_SEPARATOR, $path) (handing to OS API) |
\Winter\Storm\Filesystem\PathResolver::standardize($path) |
| Atomic file write (avoid partial-write races) | (new \Symfony\Component\Filesystem\Filesystem())->dumpFile($path, $contents) |
| Find files matching a pattern | \Symfony\Component\Finder\Finder::create()->files()->name('*.ext')->in($dir) |
Strings and arrays:
| If you reach for… | Use this instead |
|---|---|
preg_match('/^prefix/', $s) |
Str::startsWith($s, 'prefix') (accepts array of prefixes) |
Manual strpos !== false |
Str::contains($s, $needle) |
strtolower-then-replace slug generation |
Str::slug($s) |
| Random hex/string for tmp paths, tokens | Str::random() / Str::uuid() |
| Deep array key access with null safety | data_get($array, 'a.b.c', $default) |
| Pulling subset of array keys | Arr::only($array, [...]) / Arr::except($array, [...]) |
| Chained map / filter / reduce on array | collect($array)->filter(...)->map(...)->values()->all() |
Other:
| If you reach for… | Use this instead |
|---|---|
exec() / shell_exec() / backticks |
(new \Symfony\Component\Process\Process([$cmd, ...$args]))->mustRun() |
| Manual YAML parsing | \Symfony\Component\Yaml\Yaml::parse() |
| JSON parsing without strict error handling | json_decode($s, true, 512, JSON_THROW_ON_ERROR) |
UUID generation by random_bytes + hex shuffle |
Str::uuid() or \Symfony\Component\Uid\Uuid::v7() |
| HTTP request to external service | Laravel's \Http::get(...) facade |
Storm depends on Laravel + Symfony pieces. It must not depend on Winter modules. Conversely, Winter core modules are free to use Storm. So:
- CMS / theme / plugin / system concerns live in
modules/(backend, cms, system). - Generic filesystem, path, parser, network primitives live in
vendor/winter/storm/. - When a Storm class needs a policy that's CMS-specific (e.g. "which directories count as theme asset roots"), expose a public setter on the Storm class and have the module-level caller supply the policy. Don't reach into module-level constants from Storm.
- Backend/CMS/system tests usually extend
System\Tests\Bootstrap\PluginTestCase(boots Laravel, plugins, auth) orSystem\Tests\Bootstrap\TestCase(boots the framework but not plugins). - Fixtures that flow through
Assetic\Asset\FileAsset(e.g.CombineAssets::combineToFile()) must live underbase_path().sys_get_temp_dir()will fail with "source is not in the root directory". Usebase_path('storage/framework/cache/<unique>')for temp dirs and clean up with\File::deleteDirectory()intearDown(). - A failing test that doesn't reproduce on a fresh clone is almost always a stale local
vendor/. Runcomposer updatein Storm (~/Repositories/WinterCMS/Core/stormor wherever you check it out) before claiming "environment issue".
When a change touches both, work in the actual local checkouts (e.g. ~/Repositories/WinterCMS/Core/storm and ~/Repositories/WinterCMS/Core/winter) on parallel branches, and symlink vendor/winter/storm to the local Storm checkout so changes are visible in the live install. Avoid /tmp worktrees — the user can't test what they can't see.
Open both PRs concurrently; the maintainer handles merge order and Storm release tagging. The Winter core PR's composer.json constraint bump waits for the Storm tag.