Leaderboard
Popular Content
Showing content with the highest reputation on 06/01/2026 in all areas
-
It's a rainy Sunday where I'm at. The monsoon is hitting hard. Perfect time to write a love letter. Addressed to a module, my favorite PW module of all times. And its creator @bernhard, of course. A Love Letter to RockMigrations by a long-time user and contributor Dear RockMigrations, I’ve been using you for years. Built sites with you, shipped modules that lean on you, even chipped in a few pull requests along the way. And honestly, I took you for granted. You just worked. Fields appeared. Templates materialized. Modules shipped their own schema like little self-contained suitcases. I never wrote down why you’re so good. I just kept using you. Recently I found myself comparing you to other approaches, side by side, line by line. And I realized: I should write this down. Not because I learned something new, but because seeing the alternatives made your elegance impossible to ignore. Let me count the ways. The Declaration of Truth Most migration systems hand me a blank file and say “write code.” Fair enough. But you, RM, let me declare what a field or template is: // site/RockMigrations/fields/subtitle.php return [ 'type' => 'text', 'label' => 'Subtitle', 'columnWidth' => 50, ]; That’s it. No $fields->save(), no $field->type = wire('modules')->get(...). Just the truth. The current, desired state. You figure out the rest. This means the file is my source of truth, not a historical log of what someone did at 9:14 AM on a Tuesday. If I want to know what the subtitle field looks like right now, I open one file. Not a trail of timestamped breadcrumbs. Circular References? You Solved Them. Elegantly. Every migration system hits the same wall: Template A needs to know about Template B, and Template B needs to know about Template A. Who goes first? Most systems tell me to manage this manually. Order my timestamps. Cross my fingers. You do something way smarter: two passes. Pass 1: Create everything. All fields, all templates, all roles. They exist as empty vessels. Pass 2: Configure everything. Wire up parent-child relationships, attach fields, set permissions. Everyone knows everyone now. Circular dependencies become non-circular across passes. No timestamp juggling. No depends_on arrays. It just works. And if I need to inject imperative code at the right moment, you give me four lifecycle hooks: beforeAssets, afterAssets, beforeData, afterData. Escape hatches at exactly the right places. Not too early, not too late. I’ve used these to install modules, create reference pages, and detach stale fields, all in the right order, every time. You’re Non-Destructive by Default If I remove a field from a template’s config array, you don’t rip it out. You leave it alone. Because you assume I might still need it, or that removing it accidentally would be catastrophic. If I actually want to detach a field, I tell you explicitly: a removeFieldFromTemplate() call, right there in the same config file or in the relevant hook. Destruction is a conscious choice, not a side effect. This alone has saved me from myself more times than I’d like to admit. Modules Ship Their Own Schema A module can drop a RockMigrations/fields/ folder into its own directory, and you just… discover it. No registration. No global config. No “please add this module to your migration scan path.” site/modules/Foo/ ├── Foo.module.php └── RockMigrations/ ├── fields/bar.php → foo_bar └── templates/baz.php → foo_baz You auto-prefix field names with the module name. You auto-tag them. When I look at a field in the admin, I can see which module owns it. And when the module uninstalls, its ___uninstall() method can call a migration script that cleans everything up, fields, templates, the lot. Optionally guarded behind a checkbox in the module config so the site admin decides whether to keep the data. No manual cleanup. No orphaned fields haunting the database for years. A module is a self-contained package: code and schema. And you respect that boundary. No sprawl into a shared site/migrations/. No “did file 47 or module X create this field?” The answer is in the file path on disk. This design choice makes every module I’ve built feel cleaner and more portable. Free Constants, Free IDE Support By just creating a marker file (RockMigrationsConstants.php), you auto-generate class constants for every field and template: // Auto-generated. Do not edit. class RockMigrationsConstants { const field_subtitle = 'subtitle'; const template_award = 'award'; } Now my IDE autocompletes RockMigrationsConstants::field_... and I never type "subtitle" as a raw string again. No typos. No silent breakage when I rename a field. You generate this from the config files, the same single source of truth. Silly bugs prevented. You’ve Grown Without Bloating You’re not a proof of concept. You’re not a shiny new thing I’m nervously trying in production. You’ve grown organically across versions, picking up MagicPages, deploy helpers, auto-release workflows, and about forty other things. You’re the Swiss Army knife that somehow doesn’t feel bloated. I’ve watched you mature over the years. Every feature felt like it earned its place. Nothing feels bolted on. Things that did at one time got removed. Other solutions are ... not bad Look, I need to say this clearly: the other migration approaches out there are not bad. They’re genuinely good work by talented people, and I’m grateful we have choices. A ProcessWire ecosystem with multiple migration approaches is a healthy ecosystem. Every approach has its place and its audience. But every time I sit down and compare feature to feature, RockMigrations comes out uniquely complete. Declarative config? Check. Two-pass circular dependency resolution? Check. Module self-containment with auto-discovery? Check. Non-destructive by default? Check. Auto-generated constants? Check. Battle-tested over years? Check. Not all in one package anywhere else. So if you’re on the fence, give it a spin. Create a site/RockMigrations/fields/ folder, drop in a config array, refresh modules, and watch it work. You might just stick around for years too. Yours declaratively, a long-time user12 points
-
Core updates A new WireApiDocs class was added that provides an API... for APIs. It’s a tool that can parse ProcessWire’s API.md files to provide various methods for finding and pulling documentation. It also includes the ability to get certain parts of an API.md (chapters). This new tool is accessible through $wire->docs, which returns an instance of WireApiDocs. Here’s a few usage examples: $api = $wire->docs; $api->get(); // get summary array of all documented classes $api->get('Pages'); // get docs for Pages class $api->get('Fieldtype*'); // get names of all Fieldtype classes Lots more examples here: https://processwire.com/api/ref/wire-api-docs/get/ and the entire class reference can be found here: https://processwire.com/api/ref/wire-api-docs/ WireApiDocs also comes with a full CLI command set, which can be viewed by typing: php index.php docs WireApiDocs: ============ php index.php docs list List classes with API docs as string php index.php docs list 'Class*' List classes matching wildcard pattern as string php index.php docs list-json List classes with API docs in JSON php index.php docs list-json 'Class*' List classes matches wildcard pattern in JSON php index.php docs list-json-verbose List classes with API docs in JSON verbose mode php index.php docs list-json-verbose 'Class*' List classes matching pattern in verbose JSON php index.php docs get Class Get API docs for given Class php index.php docs get-json Class Get JSON API docs for given Class php index.php docs toc Class Get table of contents for given Class (string) php index.php docs toc-json Class Get table of contents for given Class (json) php index.php docs body Class num Get body for given Class and chapter number php index.php docs body Class 'title' Get body for given Class and chapter title New API.md files Speaking of API docs, several new API.md files were added to the core this week. You can view any of them by clicking the links below: WireArray: Covers the base collection class for all PW iterable sets. WireData: Covers the base data-storage class used throughout ProcessWire. LanguageSupport: Covers all things multi-language in ProcessWire. This is probably our most comprehensive API.md yet. WireHooks: Comprehensive guide to ProcessWire's entire hooks system. Fields: the API variable that manages all custom fields in ProcessWire. Templates: The API variable that manages all Template instances. FieldtypeOptions: Field that stores one or more selected options from a predefined list. New LanguagePorter class for exporting and importing translations This new class provides a simple "export CSV" and "import CSV" for language static translations. It was added so that agents and developers could have an easy way to pull all text to translate, translate it, then push it back into the system. It is used by the new "static phrase translation" task that appears in this week's new version of AgentTools. The LanguagePorter can be accessed for any language from the $language->porter() method. WireTests in the core Tests for the WireTests module are now being placed directly in the core. We are slowly migrating the existing tests, while new tests are being committed to the core rather than in the WireTests module. This week tests were added for the following (click links to view tests file): Sanitizer, WireData, WireArray, WireDateTime, LanguageSupport (covers all multi-language features). AgentTools updates New “run in background” option for Site Engineer and Page Engineer AgentTools now has the ability to queue and run AI agent requests in the background on the server, rather than through a web request. This is especially helpful with long running tasks, as they can run without Apache/server http timeouts. The AgentTools module configuration screen tells you what you need to do to enable background jobs. But primarily it’s just a matter of enabling a cron job for AgentTools. When the background job is finished, you can view the agent’s response in the admin. But it also gets emailed to you. Background jobs are available for Site Engineer, Page Engineer as well as the built-in and custom tasks. New built-in task: Static phrase translation and language pack creation Per last week’s update, AgentTools now comes with several built-in AI tasks that you can use. Added this week is yet another new task for multi-language sites: you can now have all the phrases in your site, in modules, or even the entire core, translated to another language by your AI agents. Not only does the task handle all of the translation, but it also handles creating and installing the language pack(s) once it is finished. Please note that this particular task requires that you 1) have ProcessWire 3.0.264 or newer; and 2) have multi-language support enabled on your installation. Also note that mass translation of phrases can take some time, so the new background mode can come in especially handy when doing lots of automatic translations. New engineer preview / dry-run mode If you want to know what the agent would do to accomplish something, without actually committing the changes now, check the new “Preview” checkbox before submitting an Engineer request. The agent will run in a read-only mode to describe what it would do before you commit to making it do the real thing.4 points
-
Suggestion: user-definable collection groups (free-text, self-learning) Hi Maxim — first, thanks for Collections; it's becoming central to how I'm managing a few thousand records in a client’s ProcessWire installation. One small limitation I ran into: the Group field is restricted to the three built-in values (`content`, `taxonomy`, `custom`). The sidebar renderer already handles arbitrary group names gracefully (it appends any non-blessed groups after the three known ones), so the only thing standing between users and custom groups is the form control plus the matching server-side whitelist. Relaxing both makes custom groups work end to end, with no storage, rendering, or migration changes — `Collection::toArray()` / `fromArray()` already round-trip an arbitrary `group` string. Here's what I changed locally (against v1.9.3). Three small edits: 1. `views/configure.php` — Group control: `<select>` → text input with datalist combo This keeps the three defaults as type-ahead suggestions while allowing any new value: <div> <label class="uk-form-label">Group</label> <input type="text" name="group" id="field-group" class="uk-input" list="group-options" autocomplete="off" placeholder="content, taxonomy, custom, or a new name"> <?php // Group suggestions: the three blessed defaults, plus any // groups already used by existing collections ("learned"). $groupSuggestions = ['content', 'taxonomy', 'custom']; foreach ($config->getCollections() as $existingCol) { $g = $existingCol->group; if ($g !== '' && !in_array($g, $groupSuggestions, true)) { $groupSuggestions[] = $g; } } ?> <datalist id="group-options"> <?php foreach ($groupSuggestions as $g): ?> <option value="<?= $wire->sanitizer->entities($g) ?>"></option> <?php endforeach; ?> </datalist> </div> The datalist is seeded with the three defaults and then "learns" any group already in use across existing collections, so previously-coined groups are offered as suggestions going forward. (Naturally, a brand-new group name only becomes a suggestion after its first save — there's nothing persisted to learn from until then.) 2. `src/ProcessCollections.module.php` — `handleSaveCollection()`: drop the server-side whitelist This is the matched pair to the dropdown — without it, any non-blessed value is silently coerced back to `content` on save. Change: 'group' => in_array($post->text('group'), ['content', 'taxonomy', 'custom']) ? $post->text('group') : 'content', to: 'group' => $san->name($post->text('group')) ?: 'content', I used `$san->name()` to keep the value safe as both an array key (it's the grouping key in `layout.php`) and a sidebar heading — lowercase, `a–z 0–9 _ -`, consistent with the convention the Key field already advertises. It still falls back to `content` when the field is empty, preserving the original default behavior. (If you'd rather allow spaces/capitals in headings, `$san->text()` would do it, at the cost of looser grouping keys.) Result With those two edits, custom groups work the whole way through: type a new group, save, and it appears as its own sidebar section (sorting after `content` / `taxonomy` / `custom`, per the existing `$groupOrder` logic in `layout.php` and `dashboard.php`). The datalist addition just makes existing groups discoverable as you go. Happy to adjust naming/sanitizer choices to match your preferences — and thanks again for the module and for being so responsive in this thread. [In the interests of full disclosure, Claude Opus 4.8 helped me identify the spots that needed updating.]1 point
-
CliModules The "native" way to do CLI with ProcessWire as of April 2026 Forum post Developer: @ryan processwire-console Based on Symfony CLI. Installed via composer local to project / in composer.json. Very deep functionality and the most developed; also includes Scheduling, Testing, Queues, Migrations, Seeding; released May 2026 Forum Post ; GitHub Developer: @ukyo RockShell Based on Symfony CLI. Installed via placing RockShell dir in root of PW; released 2022 Forum Post ; GitHub Developer: @bernhard WireCLI Based on Symfony CLI. Installed via composer globally; released 2023 Forum Post ; GitHub Developer: @flydev Wire-Shell Based on Symfony CLI. This project died a while ago. --- I suppose the reason I'm posting this is to start a discussion. Should there ultimately be "one true way"? If CliModules approach "wins", does the fact that it's dependency free (ie, not based on Symfony CLI, as ProcessWire is more of a unified system) pose any potential limitations? Note: I'm personally going to be using some combination of CliModules and processwire-console moving forward.1 point