Something I've wanted in ProcessWire for a long time is full version support for pages. It's one of those things I've been trying to build since ProcessWire 1.0, but never totally got there. Versioning text and number fields (and similar types) is straightforward. But field types in ProcessWire are plugin modules, making any type of data storage possible. That just doesn't mix well with being version friendly, particularly when getting into repeaters and other complex types.
ProDrafts got close, but full version support was dropped from it before the first version was released. It had just become too much to manage, and I wanted it to focus just on doing drafts, and doing them as well as we could. ProDrafts supports repeaters too, though nested repeaters became too complex to officially support, so there are still some inherent limitations.
I tried again to get full version support with a module called PageSnapshots developed a couple years ago, and spent weeks developing it. But by the time I got it fully working with all the core Fieldtypes (including repeaters), I wasn't happy with it. It was functional but had become too complex for comfort. So it was never released. This happens with about 1/2 of the code I write – it gets thrown out or rewritten. It's part of the process.
What I learned from all this is that it's not practical for any single module to effectively support versions across all Fieldtypes in ProcessWire. Instead, the Fieldtypes themselves have to manage versions of their own data, at least in the more complicated cases (repeaters, ProFields and such). The storage systems behind Fieldtypes are sometimes unique to the type, and version management needs to stay internal [to the Fieldtype] in those cases. Repeaters are a good example, as they literally use other pages as storage, in addition to the field_* tables.
For the above reasons, I've been working on a core interface for Fieldtypes to provide their own version support. Alongside that, I've been working on something that vaguely resembles the Snapshots module's API. But rather than trying to manage versions for page field data itself, it delegates to the Fieldtypes when appropriate. If a Fieldtype implements the version interface, it calls upon that Fieldtype to save, get, restore and delete versions of its own data. It breaks the complexity down into smaller chunks, to the point where it's no longer "complexity", and thus reasonable and manageable.
It's a work in progress and I've not committed any of it to the core yet, but some of this is functional already. So far it's going more smoothly than past attempts due to the different approach. My hope is to have core version support so that modules like ProDrafts and others can eventually use that API to handle their versioning needs rather than trying to do it all themselves. I also hope this will enable us to effectively version the repeater types (including nested). I'm not there yet, but it's in sight.
If it all works out as intended, the plan is to have a page versions API, as part of the $pages API. I'll follow up more as work continues. Thanks for reading and have a great weekend!
This week I've continued work on the page versions support that I wrote about last week. While the main PagesVersions module needs more work before it's ready to commit to the dev branch and test externally, it is so far working quite well in internal testing.
I mentioned last week how it will support an interface where Fieldtypes can declare that they will handle their own versions. That interface has been pushed to the dev branch as FieldtypeDoesVersions. I've also implemented it with the Repeater Fieldtype, which is visible starting here. Repeaters are so far working really well with versions!
As far as core Fieldtypes go, Repeater, PageTable and FieldsetPage are likely the only ones that will need custom implementations. For ProFields, RepeaterMatrix already works with the implementation in the core Repeater (already tested). It's possible that other ProFields will not need custom implementations, though not yet positive.
The module that provides version capability is called PagesVersions and the plan so far is to make it a core module that provides API version support for pages. A separate module provides interactive version support in the page editor. I've built this module initially so that I can test versions more easily, but it'll be expanded to provide a lot more. Below is a screenshot if what it looks like in the page editor Settings tab so far:
All of this can be done from the module's API as well. Note that the API is provided by a $pagesVersions API variable that is present when PagesVersions module is installed. The API method names and such are a bit verbose right now but may be simplified before it's final.
// Get page and add a new version of it
$page = $pages->get(1234);
$page->title = 'New title';
$version = $pagesVersions->addPageVersion($page);
echo $version; // i.e. "2"
// Get version 2 of a page
$pageV2 = $pagesVersions->getPageVersion($page, 2);
// Update a version of a page
$pageV2->title = "Updated title";
$pagesVersions->savePageVersion($pageV2);
// Restore version to live page
$pagesVersions->restorePageVersion($pageV2);
// Delete page version
$pagesVersions->deletePageVersion($pageV2);
Thanks for reading! More next week.
If you haven't yet noticed it, @teppo has hit the 500th issue of PW Weekly! That is a ridiculously massive milestone and an amazing achievement, and after some quick maths, also tells me that he's held strong for almost 10 years now! I'm not sure what's more surprising, that he's managed to keep it going continuously for this long, or that I remember when he started it... That's a long darn time. I may not be a hugely active member of the community, but I'm darn proud to be a part of it regardless.
Thank you so much for your devotion to the PW Weekly project, @teppo!!!!
In the last couple of weeks I've been working on the page versions support in ProcessWire (recap here and here). This week the new PagesVersions module was committed to the core. Though please consider it very much "beta" at this stage. Along with this, the core dev branch version was bumped to 3.0.232. The API reference page for PagesVersions is now live here: https://processwire.com/api/ref/pages-versions/. Note that the module is not installed by default, but once running 3.0.232, it can be installed by going in your admin to Modules > Wire > Pages > PagesVersions.
In addition, a related development module named PagesVersionsPro has also been released. This module uses the new API from the core PagesVersions module. This module will eventually be merged with or replace ProDrafts. The new PagesVersionsPro support board and module is currently visible to ProDrafts, ProFields and ProDevTools subscribers here.
Unlike ProDrafts, PagesVersionsPro gets all of its version abilities from the core, and instead just focuses on providing an interactive interface to them in the page editor. To word it another way, the module does not extend the PagesVersions module in the way that ListerPro extends Lister. Instead, it just provides a web interface for it. I think this is a better long term and more sustainable strategy for handling version support.
Core version 3.0.232 also adds version support for nested repeaters and FieldsetPage fields. Support was added in those Fieldtypes directly. Still remaining are PageTable (core) and Table (ProFields), both of which will need their own implementations for versions like Repeater and FieldsetPage needed. But following that, there won't be any unsupported fieldtypes to my knowledge.
ProcessWire Weekly published its 500th issue! Congratulations and big thanks to @teppo for his incredible work with ProcessWire Weekly, it is truly outstanding!
Thanks for reading and have a great weekend!
The core version has been bumped to 3.0.233 this week. While there aren't a lot of commits, there are some major updates to the core PagesVersions module. I also thought a version bump would be helpful as there's also a new PagesVersionsPro version released which requires features only available in 3.0.233.
The PagesVersions module is now pretty much finished in terms of its API and feature set. This week the ability to save and restore partial versions was added, and that was the main remaining thing. By partial versions, I mean the ability to specify what fields are included when a version is saved or restored. Though I think it's primarily useful on the restore side. So if you find you just want to restore one or more particular fields from a past version, rather than all the fields, now you can.
The core PageTable field was also updated to support versions, partially anyway. It supports versioning of items already in the page table, but doesn't handle versioning of items that you might add or remove within a version. It turns out it's going to be a lot of work to do that, so I settled with just partial support for this week. As it is, if you add a new item to the PageTable while in a version, then it'll ask you if you want to import it once you edit the live version. If you delete an item, it'll be deleted from all versions. That's how it works temporarily until it fully supports versions.
ProFields Table now also supports versions. But there is one case where it doesn't: paginated table fields. A future version of Table will add support for that. Until then, the PagesVersionsPro module does make it clear when a paginated table field won't be added to the version. So now all fields in ProcessWire are supported, except for certain scenarios in PageTable and Table fields.
A new version of the PagesVersionsPro module was released as well, and this is posted in the PagesVersionPro support board download thread here. This module made a lot of progress this week and will continue to evolve in the coming weeks. I'll copy/paste the version 2 changelog for it below this post.
This weekend or early next week I also plan to release new versions of ProFields Table and Combo. These versions facilitate versions when doing partial save or restore operations that include file or image fields in Table or Combo fields.
I hope that you and your family have a wonderful Winter/Christmas/Hanukkah/Festivus holiday!
Version 2 changelog for PagesVersionsPro
When doing a restore, it now detects which fields differ between "live" and "version", making it easier for you to choose which fields to restore.
When editing a version the “Delete” tab in the page editor now refers to deleting the version rather than trashing the page.
The “compare” option has now been improved so that it can better detect differences between the live and version page.
During restore, if you “Choose which fields to restore” you now have the option to compare them individually to see what is different between live and version.
Added "page-edit-versions" permission so that you can limit the capabilities of this module to specific user roles.
if($config->ajax) {
// Return search results in JSON format
$q = $sanitizer->selectorValue($input->get->q);
$results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results
header("Content-type: application/json"); // Set header to JSON
echo $results->toJSON(); // Output the results as JSON via the toJSON function
return $this->halt();
// look for a GET variable named 'q' and sanitize it
$q = $sanitizer->selectorValue($input->get->q);
// did $q have anything in it?
if($q) {
// Find pages that match the selector
$matches = $pages->find('search_cache%=' . $q);
// did we find any matches? ...
if($matches->count) {
echo "
We found $matches->count results:
";
echo "
";
foreach($matches as $match) {
echo "- $match->title";
echo "
$match->summary
";
echo "
";
} else { ?>
No results found.
= $files->render('elements/_searchbox'); ?>
} else { ?>
Search:
= $files->render('elements/_searchbox'); ?>
} ?>
Explanation:
This part here at the top of the template handles the requests that are send via ajax. This is the important part for later on.
if($config->ajax) {
// Return search results in JSON format
$q = $sanitizer->selectorValue($input->get->q);
$results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results
header("Content-type: application/json"); // Set header to JSON
echo $results->toJSON(); // Output the results as JSON via the toJSON function
return $this->halt();
What this does: