cwsoft Posted May 19 Share Posted May 19 (edited) Dear all, I am struggling with implementing a before/after hook (tested both, didn't work), which should prevent users from changing the first 3 characters of a string in a specific text_field. Here is what I came up with for the moment in my site/ready.php. The bd() debug calls indicated, that the "new" value is reset to the "old" value inside $event. However when clicking "Save" in the backend, the "new" value will be saved and populated instead. // Prevent user from changing the first three characters of a text_field on a specific page template. $wire->addHookBefore('Page(template=my_template)::changed(text_field)', function (HookEvent $event) { // Ensure we have a valid page to save. $page = $event->object; if ($page->id === 0 || $page->isTrash) return; // Allow changes of the text_field value if the first three characters remain unchanged. $old = $event->arguments(1); $new = $event->arguments(2); if (mb_substr($new, 0, 3) === mb_substr($old, 0, 3)) return; // Revert back user input in case the first three chars differ. bd($event); $event->arguments(2, $old); bd($event); $event->warning('First three chars of text_field can ot be changed. Reset to previous value.'); }); Seems I am missing something here, but I can't figure out what exactly. Could anybody give me a hint why this won't work? Do I need to implement this in the Page:saveReady hook instead? If yes I guess I would need to fetch the old values from the uncached page object to compare old/new values. Thats why I thought the Page:changed hook would make more sense. Any hints or comments are welcome. Cheers Edited May 24 by cwsoft marked solved Link to comment Share on other sites More sharing options...
bernhard Posted May 19 Share Posted May 19 I think the changed hook is not meant to revert any changes but only to listen to changes and trigger actions. But you can do this (not sure this is the best solution) - example using the title field in /site/ready.php: wire()->addHookAfter('Page::changed(title)', function (HookEvent $event) { $old = $event->arguments(1); $new = $event->arguments(2); $oldStart = substr($old, 0, 3); $newStart = substr($new, 0, 3); if ($oldStart === $newStart) return; // save old value to a temporary property that will be used // in the saveReady hook to revert to the old value $page = $event->object; $page->revertTitle = $old; }); wire()->addHookAfter('Pages::saveReady', function (HookEvent $event) { $page = $event->arguments(0); if ($page->revertTitle) $page->title = $page->revertTitle; }); 2 1 Link to comment Share on other sites More sharing options...
Robin S Posted May 19 Share Posted May 19 Assuming that the field would be changed in Page Edit rather than as result of some other API code, you can validate the user input in a hook to InputfieldText::processInput. Example: $wire->addHookBefore('InputfieldText::processInput', function(HookEvent $event) { $inputfield = $event->object; $input = $event->arguments(0); $field = $inputfield->hasField; $page = $inputfield->hasPage; // For a particular field name and template name if($field && $field->name === 'text_1' && $page && $page->template == 'events') { $old_value = $page->getUnformatted('text_1'); // Return early if the old value is empty if(!$old_value) return; $new_value = $input[$inputfield->name]; // If the first three characters have changed if(substr($new_value, 0, 3) !== substr($old_value, 0, 3)) { // Show an error message $inputfield->error('You are not allowed to change the first three characters of the "Text 1" field.'); // Replace the hooked method so the new value won't be saved to the field $event->replace = true; } } }); 5 1 Link to comment Share on other sites More sharing options...
cwsoft Posted May 19 Author Share Posted May 19 (edited) @bernhard @Robin S: Thank you very much for your code suggestions. Looks very promising. Will try both tomorrow and then report back. Giving an additional hint/warning directly to the corresponding inputfield like robin suggested seems a good idea in terms of usability. The idea of a temporary field is also a need trick. Tried with saveReady before, which seems to work, but felt a bit hacky to me, as I needed to fetch the uncached old value to check for potential changes in the text field. Always good to learn something new. Your answers are highly appreciated. Made my day. Edited May 19 by cwsoft 2 Link to comment Share on other sites More sharing options...
TomPich Posted May 20 Share Posted May 20 Interesting topic! I don’t know guys how you acquired this mastering of PW hook. I’d be struggling like @cwsoft 😅. It would be very useful to have some deeper documentation / tutorial about using hooks in PW. 1 Link to comment Share on other sites More sharing options...
cwsoft Posted May 20 Author Share Posted May 20 (edited) As promised some feedback. The easiest thing I came up with was a saveReady Hook, where I fetched the previous page state from the database and compared the fields needed with the actual values. However the code became a bit messy, as I have two templates one with three input fields one with two, ending in a gigantic switch block hard to maintain. What worked too, was the proposal from Robin S. However fetching the actual values from Inputfields of various types (text, datetime, select array, page select) needs some custom tweaks here and there to get the field values. Things can get tricky very fast, when using multiple hooks, which interfere with each other. Here it‘s key to understand what hooks fire first etc. At the end, I managed what I had in mind, but it indeed was a long way with lots of trial and error, reading the tutorial over and over again and inspecting the inputfield classes and with quite a lot of life debugging using Tracy. Without Tracy and the ideas and snippets found in the forum, I wouldn‘t be able to solve what I had in mind. So yes, managing hooks is still on my todo list, even after one year using ProcessWire for two part time projects. Edited May 20 by cwsoft 1 Link to comment Share on other sites More sharing options...
cwsoft Posted May 21 Author Share Posted May 21 (edited) After playing about two evenings with various kinds of hooks, I just used a plain old Pages::saveReady hook, as this allows me to access all page fields of a certain page/template more easily. So my final code looks like this now. // Ensure first three chars of the required field 'location_place' can't be changed. $wire->addHookAfter('Pages::saveReady(template=location)', function (HookEvent $event) { // Ensure we have a valid page. $page = $event->arguments(0); if (!$page || $page->isTrash) return; // Get actual field values (required) entered by the user. $actualPlace = $page->get('location_place'); // Fetch previous page from database so we can check if 'location_place' has changed. $cachedPage = pages()->getById($page->id, array('cache' => false, 'getFromCache' => false, 'getOne' => true)); if ($cachedPage->id > 0) { $oldPlace = $cachedPage->getUnformatted('location_place'); if ($oldPlace && substr($actualPlace, 0, 3) !== substr($oldPlace, 0, 3)) { // Reset location_place to previous value and show an error on the inputfield. $page->location_place = $oldPlace; $inputfield = $page->getInputfield('location_place'); $inputfield->error('First three characters can not be changed. Input reset to previous value.'); // If you prefer less drama, just show a message instead of the error state on the input field itself. // $event->message('First three characters can not be changed. Input reset to previous value.'); } } }); However I may play with this hook too, just to check it's potential use case: https://processwire.com/api/ref/fieldtype-text/save-field-ready/ Cheers and thanks to @bernhard and @Robin S for your valuable feedback and proposals. Pretty sure they will come handy at some time for me. Edited May 23 by cwsoft Fixed $page->get call, thanks to Bernhard Link to comment Share on other sites More sharing options...
bernhard Posted May 21 Share Posted May 21 39 minutes ago, cwsoft said: After playing about two evenings with various kinds of hooks, I just used a plain old Pages::saveReady hook, as this allows me to access all page fields of a certain page/template more easily. So my final code looks like this now. Great you got it working and yeah, if you need to compare multiple values, then having all available in the $page object in saveReady sounds like a good solution! 40 minutes ago, cwsoft said: $cachedPage = pages()->getById($page->id, array('cache' => false, 'getFromCache' => false, 'getOne' => true)); Some notes here: I think this is the same as just using pages()->getFresh($page) ? I think the variable name $cachedPage is not ideal, because it's actually the non-cached page. It's the page fresh from the DB, not the cached copy that lives in memory I don't know this syntax: $actualPlace = $page->get('location_place', 'text', ''); And in hooks I tend to avoid nested IFs, so you might consider refactoring your hook to this (though yours is absolutely fine, of course): <?php // Ensure first three chars of the required field 'location_place' can't be changed. $wire->addHookAfter('Pages::saveReady(template=location)', function (HookEvent $event) { // Ensure we have a valid page. $page = $event->arguments(0); if (!$page) return; if ($page->isTrash) return; // Load old page from DB $dbPage = wire()->pages->getFresh($page); // Get actual field values (required) entered by the user. $newPlace = (string)$page->get('location_place'); $oldPlace = (string)$dbPage->get('location_place'); // if new and old value match --> nothing to do if (substr($newPlace, 0, 3) === substr($oldPlace, 0, 3)) return; // otherwise reset location_place to previous value and show an error on the inputfield. $page->location_place = $oldPlace; $inputfield = $page->getInputfield('location_place'); $inputfield->error('First three characters can not be changed. Input reset to previous value.'); // If you prefer less drama, just show a message instead of the error state on the input field itself. // $event->message('First three characters can not be changed. Input reset to previous value.'); }); But when checking for multiple values it might be better to use if(...) { ... } multiple times as early exits won't work in such a scenario 🙂 Link to comment Share on other sites More sharing options...
cwsoft Posted May 21 Author Share Posted May 21 (edited) @bernhardThanks for your suggestions. Will try the get Fresh call for sure. Fun fact is that my $cachedPage was named this way only in this example code. I used $dbPage and finally $oldPage for my own code. I indeed revert back up to four fields (page select, select option, textfield and datetime) on two different templates split in two separate saveReady hooks via saveReady(template=xy). In addition I overwrite page name and page title to some unique values using parts of some input fields and take care of setting a draft value for the page title when a page is newly created but not yet saved (my page title is an auto field not directly set by the user). So early returns are not always possible in this special case. I usally tend to use short circuit returns instead of nested ifs, whenever possible, which however isn‘t possible in my scenario, as the page name and the final title needs to be set last once all fields which may be part of it where sanitized and maybe reverted back to it‘s previous state. Edited May 21 by cwsoft 1 Link to comment Share on other sites More sharing options...
cwsoft Posted May 23 Author Share Posted May 23 On 5/21/2025 at 10:54 PM, bernhard said: I think this is the same as just using pages()->getFresh($page) ? I don't know this syntax: $actualPlace = $page->get('location_place', 'text', ''); Can confirm, that pages()->getFresh($page) works as well. The $page->get('field', 'sanitizer_method', 'default') was accidentally taken over from an $input->get call. Fixed it in my example code above. Thanks. 1 Link to comment Share on other sites More sharing options...
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now