Search the Community
Showing results for tags 'hooks'.
-
I didn't know where the right place to share this was on the forums, so I'll post it here since it may be helpful to those getting started with ProcessWire hooks, or some experienced ProcessWire developers who might find them useful. Either way, dear reader, If someone already wrote it, why write it again? If you're someone with experience, feedback is welcome! If there are better or more efficient ways to do something, I would love being a student. Some of these may either address challenges that others have experienced as well or were inspired by the awesome community sharing their solutions. Kudos to all of the people out there helping all of us. If someone sees something that was solved elsewhere please share it in the comments to give credit. I have to make a disclaimer- these have worked for me and while most of them are ready to copy/paste, a few of them are going to need customization or tweaking to make them work for your use case. I don't currently have the resources (time) to provide a lot of support. Some of these were slightly rewritten or adapted for the examples. If you run into issues, the best thing to do is research the solution so that you know exactly what is happening in your application. If you adapt something, fix a bug, or address an edge case, it would be great if you can come back and share that. Be smart, if you're going to run hooks that modify or create data, run a DB backup first. This is the part where I say "I'm not responsible if your site blows up". I don't think that's possible, but do the right thing. There are dozens of hooks in the project I am sharing these from, and to manage that I created a file structure to handle this because there were far too many to put in one file and keeping the init.php and ready.php files clean really makes a huge difference in maintainability. Being able to jump between files by filename is a supremely efficient way to work as well. The filenames don't matter, they're there to identify the files and make it easy to locate/switch between them. Here's my approach to directory organization: /site - hooks -- HookUtils -- init -- lazy_cron -- ready init.php ready.php The ready.php file contents: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) die(); /** @var ProcessWire $wire */ // Import all ready hooks foreach (glob(__DIR__ . '/hooks/ready/*.php') as $hook) { require_once $hook; } The init.php file contents: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) die(); /** @var ProcessWire $wire */ // Import all init hooks foreach (glob(__DIR__ . '/hooks/init/*.php') as $hook) { require_once $hook; } // Import all LazyCron hooks, import after init hooks as there may be dependencies foreach (glob(__DIR__ . '/hooks/lazy_cron/*.php') as $hook) { require_once $hook; } Operational Hooks Here are some favorites. Sort items in a Repeater matrix when a page is saved This one helped sort RM items by a date subfield to help the user experience when editing pages. This implementation is configured to only fire on a specific template but can be modified to fire everywhere if modified to check that a field exists on the page being saved first. This was adapted from an answer here in the PW forum but can't find the original post, so I'm going to include it. If you're having issues getting items to sort the way you want, check out this post about natural sorting, which also works elsewhere in ProcessWire. Github Gist Automatically add a new child page under a page having a specific template when created This automatically creates a new child page and saves it when a page having a specific template is created. This also has the ability to show a message to the user in the admin when new page(s) have been created by this hook. It is also error safe by catching any potential exceptions which will show an informative error to the admin user and log the exception message. The messaging/logging operation is abstracted to a separate object to allow reuse if creating multiple pages. Github Gist Conditionally show the user a message while editing a page This one shows a message on a page with a specific template under specific conditions. May be page status, field value, type of user, etc. Visual feedback when editing complex pages can be very helpful, especially when an operation may or may not take place depending on factors like the values of multiple fields. This can reduce the amount of explanations needed on each field or training required for users to use a ProcessWire application. In my case, a message is shown if the state of a page indicates that another operation that is triggered by other hooks will or will not run, which is something that the user doesn't directly trigger or may not be aware of. Github Gist Show the user a message when viewing the page tree This is intended to display a message, warning, or error when the page tree is viewed, such as on login, but in this case executes any time the main page tree is viewed to provide consistent communication and awareness. In my case it displays if there is an activity page located under an "Uncategorized" page for an event. This is something that may be buried in the page hierarchy and not noticeable, but if an activity isn't categorized, then is isn't visible on the website, and if it's not visible on the website, people aren't seeing it or buying tickets. So having a persistent message can bring visibility to important but otherwise potentially unnoticed issues. Or you can just say hi and something nice. Github Gist Hook Enhancement - Fast user switching Hooks can run on triggers that vary widely. Some can and should be identified as those that are triggered by the current user, others may be more autonomous like executing via cron. There may be other hooks that are executed by a user that isn't logged in. Depending on the type of action and your need to identify or track it, switching from the current user to another user created specifically to handle certain tasks can be very helpful. ProcessWire tracks a number of things that are attributed to users- log entries note the user, the user that creates pages is stored, the user that last updated the page is stored, etc. You may want to know who did what when, or only take action if the last user that touched something was X and not Y. I created a separate user that has been provided only the specific permissions it needs to complete jobs that are triggered by hooks or crons. Creating a user with less permissions may also help prevent accidental behaviors, or at least help you be very intentional in determining what actions are delegated. Creating custom permissions is also useful. With a dedicated user I can see explicitly that the last update on some pages were made by an autonomous script that syncs information between the ProcessWire application and a third party platform. Github Gist - Fast user switcher Github Gist - Example of switching users in a hook Fast, powerful, and very (very) easy custom admin buttons I needed a way to add custom interactive buttons that had some specific requirements. Needs to be a button that can be clicked by the user and does something Can be conditionally shown to the user with an alternate message if that action is not available Needs to do something on the server and interact with ProcessWire Here's what that looked like for my application. The green "Refresh Activity" button in the top right. That's a custom button and you don't have to author an Inputfield module to get it. When a user clicks that button, it sends a request to the server with GET variables that are recognized in a hook, actions are taken, then a nice message indicating success or failure is shown to the user. To do this you'll need to install FieldtypeRuntimeOnly and create a new field. Following the documentation for that field, create a button with a URL to the current page with GET variables appended. Then create a hook that watches for the specific GET variable that executes if it's present. Shoutout to @Robin S for helping make short work of a potentially complex task. Note that the field code contains JS that handles the URL on page load. Since the hook is looking for a GET variable in the URL, using the back button or refreshing the page will cause the action to run twice. The JS in that example removes the entry from the browser history and also removes the GET parameter after the page loads if it's present. Github Gist - An example gist for the hook that handles the action Github Gist - An example of the FieldtypeRuntimeOnly code that is displayed and interacted with by the user. Automatically convert logged object or array data to JSON If you're using the outstanding Logs JSON Viewer (yet another great one by @Robin S module, then this hook makes for a thoroughly enjoyable logging experience. Using array or stdClass data when logging your values helps store additional information in an organized way Github Gist <?php $log->save('log_name_here', 'Regular string message'); // Remains a string $log->save('log_name_here', ['gets' => 'converted', 'to' => 'json']); $log->save('log_name_here', (object) ['is' => 'stdClass', 'object' => 'friendly']); Use a separate field to store address data for a FieldtypeMapMarker field This one is really simple, more just sharing an implementation and idea, but proved valuable for reducing data redundancy. I have a FieldtypeMapMarker field but the way that I needed to store address data was much better suited to using multiple fields for things like street, city, state, and zip code. I wanted those fields to be the "controlling" fields for the map marker field to prevent needing to edit 2 fields to keep updated, or accidental content diversion between them. On page save the value from the address fields are pulled and converted into a single string that is added to the FieldtypeMapMarker field's "address" property. I used a Custom Field (ProFields) for my address fields but this can be modified to suit your use case very easily. Github Gist You might also consider hiding the address input on the FieldtypeMapMarker field itself to reduce confusion since the values will be updated automatically anyway. You'll need to have this in a file that is appended to the Admin styles /* You can find the appropriate class for the template you are applying this to in the <body> element when editing a page You can omit that if you want to apply this everywhere */ .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerAddress, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerToggle, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerLat, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerLng { display: none !important; } <?php // Add this to your ready.php file or ready-firing hook to insert the file containing that CSS to your admin. $config->styles->add("/path/to/your/custom/admin/css/file.css"); Not-A-Hook Bonus - Here's code for an interactive Google Map Renders a Google Map using a FieldtypeMapMarker field, a separate address field, Alpine.js, and Tailwind. You'll need a Google Maps API key, a styled map ID from your Google Developer account, and the aforementioned fields. I wrote it using the latest Google Maps API. Saved you some time. You'll probably need to tweak it. I adapted this so if you find a bug please let me know and I'll update the gist. Note- this makes use of the AlpineJS Intersect plugin to improve performance by only loading/initializing the map when a user scrolls close enough to it. If you don't want that, remove the x-intersect directive. If you want to see it in action, you can check it out here. Github Gist Hook Support Class - A static method class to translate a field into all languages automatically If you use the Fluency translation module, this is a class that will help out with translating a field into all languages programmatically. Sharing this here because the next hook uses this as a dependency. I keep this in the HookUtils directory noted in the file structure above. Usage is demonstrated in the next hook. Github Gist Translate all translatable fields using Fluency on page save whether from UI or API. This is useful for instances where you want a page translated automatically and especially helpful when you are creating pages programmatically. This requires the above hook support class, as well as Fluency connected to an API account. Here are things that must be kept in mind. Please read them, the code for the hook, and the code for the support class to ensure that it works to your needs. You should modify Fluency before using this, really. Change the value of CACHE_EXPIRY on line 19 in the TranslationCache file to WireCache::expireNever. Do this to prevent chewing through your API usage from month to month on repeat translations. This will become standard in the next release of Fluency. This is an expensive operation in terms of API usage, which is why you very much should modify the caching behavior. This hook does not make an effort to determine which fields have changed before translating because it doesn't really matter if the translation is already cached. First time translations of pages with a significant amount of fields/content may be slow, like noticeably slower first time page save because this operation is only as fast as the speed of the request/response loop between ProcessWire and the translation API. Later page saves will be much faster thanks to cached translations. This will not attempt to translate empty fields, so those won't cause any delays. This works with multi-language text/textarea/TinyMCE/CKEditor fields, RepeaterMatrix fields, and the newer Custom Fields (ProFields). Other fields haven't been tested, but it's definitely possible to adapt this to those needs. I prefer to target specific templates with hooks, you can add multiple but be mindful of your use case. Consider adding excluded fields to the array in the hook if it makes sense Consider adding a field to enable/disable translations from the UI, a checkbox field or something This hook is probably one of the uglier ones, sorry. If you run out of API usage on your account, you're going to see a big ugly exception error on screen. This is due to Fluency not handling an account overage error properly because the return type was not as expected. Will be fixed in the next version of the module This is one that may be tailored to my PW application, I think it's general enough to use as-is for your project, but testing is definitely required. Read all the code please. Github Gist ProcessWire Object Method & Property Hooks The following are custom methods that add functionality to native ProcessWire objects. Add a getMatrixChildren() method to RepeaterMatrixPage objects RepeaterMatrix fields represent nesting depth as an integer on each RepeaterMatrixPage item. So top level is 0, first nested level is 1, second 2, etc. When looping through RM items, determining nesting requires working with that integer. It works, but adding adding some functionality helps out. This is infinitely nestable, so accessing children, children of children, children of children of children, and so on works. Fun for the whole family. This was inspired by a forum post, another one I can't find... Github Gist <?php // Access nested RepeaterMatrix items as child PageArray objects $page->repeater_matrix_field->first()->getMatrixChildren(); // => PageArray ?> <!-- Assists with rendering nested RM items in templates Sponsors are nested under sponsorship levels in the RM field --> <div> <?php foreach ($page->sponsors as $sponsorshipLevel): ?> <h2><?=$sponsorshipLevel->title?></h2> <?php if ($sponsorshipLevel->getMatrixChildren()->count()): ?> <ul> <?php foreach ($sponsorshipLevel->getMatrixChildren() as $sponsor): ?> <li> <img src="<?=$sponsor->image->url?>" alt="<?=$sponsor->image->description?>"> <?=$sponsor->title?> </li> <?php endforeach ?> </ul> <?php endif ?> <?php endforeach ?> </div> Add a resizeAspectRatio() method to PageImage objects Adds a simple way to quickly resize an image to a specific aspect ratio. Use cases include sizing images for Google Structured Data and formatting images for consistency in image carousels. Could be improved by accepting second argument to specify an image width, but didn't fit my use case. Github Gist <?php $page->image_field->resizeAspectRatio('square')->url; // Alias for 1:1 $page->image_field->resizeAspectRatio('video')->url; // Alias for 16:9 $page->image_field->resizeAspectRatio('17:10')->url; // Arbitrary values accepted Add a responsiveAttributes() method to PageImage objects Adds a very helpful method to generate image variations and accompanying 'srcset' and 'sizes' attributes for any image. Designed to be very flexible and is Tailwind ready. Responsive sizing can be as simple or complex as your needs require. Includes an optional 'mobile' Tailwind breakpoint that matches a custom tailwind.config.js value: screens: { 'mobile': '320px'}. I added this breakpoint largely to further optimize images for small screens. The array of Tailwind breakpoints and size definitions can be edited to suit your specific setup if there are customizations When sizing for Tailwind, the last media query generated will automatically be switched to "min-width" rather than "max-width" to prevent problems arising from restricting widths. Example, you can specify values only for 'sm' and 'md' and the 'md' size will have the media query correctly adjusted so that it applies to all breakpoints above it. Github Gist <-- The responsiveAttributes() returns a renderable attribute string: srcset="{generated values}" sizes="{generated values}" --> <-- Create responsive images with arbitrary width and height at breakpoints --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ [240, 125, '(max-width: 300px)'], [225, 125, '(max-width: 600px)'], [280, 165, '(max-width: 900px)'], [210, 125, '(max-width: 1200px)'], [260, 155, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Heights can be selectively ommitted by setting the height value to null --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ [240, 125, '(max-width: 300px)'], [225, null, '(max-width: 600px)'], [280, 165, '(max-width: 900px)'], [210, null, '(max-width: 1200px)'], [260, null, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Create responsive images with only widths at breakpoints --> <img src="<?=$page->image->url?>" <?=page->image->responsiveAttributes([ [240, '(max-width: 300px)'], [225, '(max-width: 600px)'], [280, '(max-width: 900px)'], [210, '(max-width: 1200px)'], [260, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Create custom sizes matched to Tailwind breakpoints --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ 'mobile' => [240, 125], <!-- Custom tailwind directive --> 'sm' => [225, 125], 'md' => [280, 165], 'lg' => [210, 125], 'xl' => [260, 155], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <!-- Resizes width of image to fit Tailwind breakpoints, useful for full width images such as hero images, doesn't change height. Also accepts 'tw' as an alias for 'tailwind' --> <img src="<?=$page->image->url?>" <?=$heroImage->responsiveAttributes('tailwind')?> width="240" height="125" alt="<?=$page->image->description?>" > Add PHP higher-order function methods to WireArray and WireArray derived objects WireArray objects are incredibly powerful and have tons of utility, but there are situations where I find myself needing to work with plain PHP arrays. I'm a very big fan of PHP's array functions that are efficient and make for clean readable code. I found myself often reaching for $wireArrayThing->getArray() to work with data then using functions like array_map, array_filter, and array_reduce. These return arrays, but could easily be modified to return WireArray objects if that is more helpful. Github Gist <?php // The EventPage page class has a method that determines sold out status from more than one source of data/page fields // which means that it isn't queryable using a ProcessWire selector. This returns a single integer calculated from ticket availability // of all events from non-queryable data. $totalEventsAvailable = $eventPages->reduce( fn ($total, $eventPage) => $count = $eventPage->isActive() ? $total++ : $total, 0 ); // Requires using a page class to determine status reliant on multiple data points not queryable via a selector. Knowing what the event // page is for an activity can't be determined using a selector for activity pages. $displayableActivities = $matches->filterToArray( fn ($activityPage) => $activityPage->eventPage()->isPublic() && $activityPage->isActive() ); // Iterating over each Page in a PageArray and processing data for sorting/ordering before rendering on a search results page // Executed within a page class $results = $pages->mapToArray(function($page) { return (object) [ 'page' => $page, 'summary' => $this->createResultSummary(page: $page, maxLength: 750), 'keywordMatchCount' => $this->getQueryMatchCount(page: $page), ]; }); Add an image orientation method/property to PageImage objects Get the portrait or landscape orientation of a PageImage. Github Gist <?php $page->image->orientation; $page->image->orientation(); Add the ability to get all related pages for Page objects at once Gets all of the related pages to a page at once by both page reference fields and links in fields. Transparently passes native arguments to Page methods for native behavior Github Gist <?php $page->allPageReferences(); $page->allPageReferences(true); // Optionally include all pages regardless of status $page->allPageReferences('your_selector=here', 'field_name'); // Use with native Page::references() and Page::links() arguments Add a saveWithoutHooks() convenience method to Page objects The number of hooks in my most recent project was... a lot. There were many that hooked into the page save event and a lot of operations that happen in the background where pages needed to be modified and saved quietly to prevent clearing ProCache files or excessive DB operations through chained hooks. Being able to use a method to do this rather than passing options felt more deliberate and clear when working across hundreds of files and in critical areas of very expensive operations. This method also accepts page save options, but in a way that hooks will always be disabled even if an option is accidentally passed enabling them. Furthermore, it also accepts a string as the first argument that, if debug mode is enabled, will dump a message to the bar via Tracy. Github Gist <?php // Adding a message can be very helpful during testing, especially when saving a page with/without hooks is conditionally based // where the result of another operation determines how a page is saved $page->saveWithoutHooks('updated event sync data hash, saved without hooks'); $page->saveWithoutHooks(['resetTrackChanges' => true]); $page->saveWithoutHooks('message and options', ['resetTrackChanges' => true]); These are a few that I've used to show some diversity in application. Hooking to ProcessWire events makes it possible to build beyond simple websites and implement truly custom behavior. Hope these may be useful to others. If you have any favorite hooks of your own, have corrections of those I've shared, or improvements, sharing them in the comments would be stellar. Cheers!
-
Hi everyone, I'm having trouble correctly targeting and displaying a button within InputfieldRepeater and InputfieldRepeaterMatrix fields in ProcessWire. My current approach involves hooking into ProcessPageEdit::buildFormContent and attempting to attach a custom button to InputfieldText and InputfieldTextarea within these repeaters. Despite my efforts, the button is not displayed in the page that contains the reference to the Repeaters and Repeater Matrix. Issue: The button fails to appear for InputfieldText or InputfieldTextarea within InputfieldRepeater or InputfieldRepeaterMatrix. Am I missing something in the hook or targeting logic? Any guidance on how to ensure the button displays correctly would be greatly appreciated. When the field is Text (InputfieldText) the button Test Button is attached/add without a problem, and I could even use appendMarkup and the button will be added (without the need of a Hook): Here's my code: public function init() { parent::init(); // Hook after the form is built to modify fields $this->wire->addHookAfter('ProcessPageEdit::buildFormContent', function (HookEvent $event) { $form = $event->return; $pageId = $this->input->get->int('id'); // The page being edited // Iterate over each form field foreach ($form->children as $f) { if ($f instanceof InputfieldRepeaterMatrix || $f instanceof InputfieldRepeater) { // Handle repeaters and their items foreach ($f->value as $item) { // Process each repeater item $this->processRepeaterFields($item, $pageId); } } elseif ($f instanceof InputfieldText || $f instanceof InputfieldTextarea) { // Directly handle text and textarea fields $this->addCustomMarkupHook($f, $pageId, null, $f->name); } } }); } private function processRepeaterFields($page, $pageId) { foreach ($page->fields as $field) { $inputField = $page->getInputfield($field->name); if ($inputField instanceof InputfieldRepeater || $inputField instanceof InputfieldRepeaterMatrix) { // Process nested repeaters using recursion foreach ($inputField->value as $repeaterItem) { $this->processRepeaterFields($repeaterItem, $pageId); } } elseif ($inputField instanceof InputfieldText || $inputField instanceof InputfieldTextarea) { if (!empty($inputField->value)) { $this->addCustomMarkupHook($inputField, $pageId, $page->id, $inputField->name); } } } } private function addCustomMarkupHook($inputField, $pageId, $repeaterPageId, $inputFieldName) { // Hook to modify the render output of the field // Is this the correct approach to add a custom button to input fields? $this->wire->addHookAfter('Inputfield::render', function (HookEvent $event) use ($inputField, $pageId, $repeaterPageId, $inputFieldName) { $field = $event->object; if ($field->name === $inputFieldName && !$this->isButtonAppended($event->return)) { $return = $event->return; $pageRefId = $repeaterPageId ?: $pageId; $buttonHtml = "<a href='/processwire/specificpage/?field={$inputFieldName}&page={$pageRefId}' data-field-name='{$inputFieldName}' data-page-id='{$pageRefId}' class='pw-panel my-action-btn'>Test Button</a>"; $event->return = $return . $buttonHtml; } }); } // Check if the button is already appended private function isButtonAppended($markup) { return strpos($markup, 'my-action-btn') !== false; } Appending a button without hooking (worked for InputfieldText/InputfieldTextarea but not Repeaters). $f->appendMarkup("<a href='/processwire/specificpage/?field={$inputFieldName}&page={$repeaterPageId}' data-field-name='{$inputFieldName}' data-page-id='{$repeaterPageId}' class='pw-panel my-action-btn'>Test Button</a>"); Has anyone else worked with InputfieldRepeater or InputfieldRepeaterMatrix fields and successfully targeted inner fields for custom hooks? Thank you very much for any advice.
- 2 replies
-
- repeater matrix
- repeater
-
(and 3 more)
Tagged with:
-
I took Ryan's old Page Edit Per User module and modified it to: Allow users to assign access on pages they have edit access on (and pages they create) Check a field on the page, and current user, and make sure there is a matching value (ex: multiselect field on both the user account and the page) Allow adding pages so long as they have access to a parent page Setup: Assign a role that has "page-edit" granted, but no templates are explicitly assigned to this access. A multiselect text tag field is assigned to both the user profile template, and any templates of pages that I want the ability for access levels to be controlled. (example values of tags: HR, PR, IT, Administration, ...etc...) When a user has a tag value assigned that matches a page's tag value, access to edit (and/or add) is granted. It's mostly working (from the page tree) (I thought it was, the add button is visible, but the same error is displayed when clicking it); I have one instance where a parent isn't displaying the add button for children, but I'll look at that shortly... What definitely is not working: the "Add New" button is (surprisingly) properly displaying templates of pages that users have access to add based on my changes, but once a user tries to use that button, it displays the following error: This makes sense since the role doesn't assign any explicit templates, but now I'm trying to override that. I suspect once I fix this that there will be another area I'll also need to hook, but... This message is part of ProcessPageAdd::___execute(). I tried to hook into `ProcessPageAdd::getAllowedTemplates` because in `execute`, it throws that error only if the template of the page to add isn't already defined in ProcessPageAdd::allowedTemplates. I might be going about this all wrong, but in the init() method of my adjusted PageEditPerUser, I have the following: wire()->addHookBefore('ProcessPageAdd::getAllowedTemplates', $this, 'hookAllowedTemplates'); ...and then the method: /** * ProcessPageAdd::getAllowedTemplates hook * */ public function hookAllowedTemplates($event) { // bd('hookAllowedTemplates'); // bd($event, 'event'); // bd($event->object, 'event->object'); // bd($event->arguments(), 'event->arguments'); $event->replace = true; $allowedTemplates = []; $allowedTemplates[input()->get->template_id] = templates()->get(input()->get->template_id); return $allowedTemplates; } One thing I struggle with, without others' examples, is what return value (and format) is expected from pre/post hooks. In this case I tried to completely replace the call to ProcessPageAdd::getAllowedTemplates because the value of $event->object->allowedTemplates in the post-hook is similar to a numerically indexed (by template ID) array of a template object (but contains a few more properties), but I still received the error as shown above. (I know I'll need to do more checks on the values assigned in the above method call depending on scenario; right now my tests all have valid parent and template IDs, so I just want to get that working first.) Any potential thoughts, either at what to look at, or where to further debug? (...or more code to share?)
-
I am using FormBuilder to collect data and save it to pages. The forms (and the page templates used) use file fields to collect PDF files. The uploaded files should automatically be renamed to match the name of the fields. This should happen every time a page is saved. The first time after the form is saved to the page, but also on every other page save, because editors could have added or exchanged some of the files and then they should be renamed again. I tried to solve this in different ways, but nothing really worked as expected. What I tried: $this->wire->addHookBefore("Pages::saveReady(template^=application_entry)", function(HookEvent $event) { $page = $event->arguments(0); // get all fields of fieldtype file $fileFields = $page->fields->find('type=FieldtypeFile'); // loop through all file fields foreach($fileFields as $field){ $fieldname = $field->name; $field = $page->get($field->name); $index = 1; foreach($field as $item){ $newFilename = $fieldname; // if there are multiple files, add index to filename if(count($field) > 1){ $newFilename .= "-".$index; $index++; } $newFilename .= '.'.$item->ext; // bd($item->basename()." -> ".$newFilename); // only rename if filename is not already set if($item->basename() != $newFilename){ $item->rename($newFilename); } } } $event->arguments(0, $page); }); When I save an existing page, the files are renamed, but I get the following error message and the filename change is not reflected in the database: If the page has not existed before save (got created by FormBuilder) I get a message that it was created, but it does not get created and FormBuilder shows a Not yet created in the entries list. Another approach was this: $this->wire->addHookAfter("Pages::saved(template^=application_entry)", function(HookEvent $event) { $page = $event->arguments(0); // get all fields of fieldtype file $fileFields = $page->fields->find('type=FieldtypeFile'); $page->of(false); // loop through all file fields foreach($fileFields as $field){ $fieldname = $field->name; $field = $page->get($field->name); $index = 1; foreach($field as $item){ $newFilename = $fieldname; // if there are multiple files, add index to filename if(count($field) > 1){ $newFilename .= "-".$index; $index++; } $newFilename .= '.'.$item->ext; bd($item->basename()." -> ".$newFilename); // only rename if filename is not already set if($item->basename() != $newFilename){ $item->rename($newFilename); $page->save(); } } } }); This also renames the file on save, but the filename in the field is still the old name. I am using pagefileSecure on this template (if that matters). Any help his highly appreciated! Thanks in advance, Flo
-
I would like to run this via a hook: $page->of(false); foreach($page->table as $row) { if($row->pass == "") { $pass = new Password(); $string = $pass->randomAlnum(22); $row->pass = $string; } } $page->save('table'); and tried this: $this->addHookAfter('Pages::save', function($event) { $p = $event->arguments('page'); if($p->id == 1599) return; // my page $p->of(false); foreach($p->table as $row) { if($row->pass == "") { $pass = new Password(); $string = $pass->randomAlnum(22); $row->pass = $string; } } $p->save('table'); }); But I get a 500 error. Is it an infinite loop? SOLVED
-
I need to show (in my backend) since when a page special type of page has been published (not created, as there may be a long delay between creation and publishing). So I made a datefield in my template and added following code to my site\ready.php $wire->addHookBefore('Page(template=templateName)::published', function($event){ $page = $event->arguments(0); $this->message("the Hook has been called"); if($page->hasField("myDateField")) { $newDate = date("d.m.Y"); $page->myDateField = $newDate; } }); But for some reason the hook is not called when I publish the page. Is there something I'm missing?
-
Preventing a page from being moved is not necessarily a straightforward matter of trapping the change in a save hook in the same way as a page edit. A problem occurs if the page is only to be 'blocked' conditionally - when the page path is in some given array, for example. In hooking before page save, the page path is the new page path after the move, rather than before it, so you need to do something like this: // .... In the before page:saveReady hook // $page is the page :) The code below operates when (isset($page->parentPrevious) && $object->parentPrevious != $object->parent) $name = $page->parentPrevious->path . $page->name . '/'; // need the path before it was moved if(in_array($name, $blockedNames)) { // $blockedNames are the ones where we don't want moves // Because this hook operates after the move, we need to reverse the move $page->parent = $page->parentPrevious; // Alternatively, to completely stop the save //$event->replace = true; //$event->return = false; } //... rest of hook This prevents the page being moved, both in the page hierarchy (by dragging) or in the page editor by changing the parent in the settings tab. It is also possible to hook before Page:moveable with something like this /** @var Page $page */ $page = $event->object; $moveable = $event->return; // ... code to set $moveable to false if conditions met $event->return = $moveable; Interestingly, the (not documented?) moveable method is created as a hook by PagePermissions and so is hookable. However, this method appears to catch only the situation where the move is effected by dragging the page in the tree, not when the parent is changed in the settings.
-
$wire->addHookAfter('Pages::added(template=booking)', function(HookEvent $event) { $page = $event->arguments(0); $selectable_pages = $page->getInputfield('booking_group')->getSelectablePages($page); $current_group = $selectable_pages->find("template=booking_price, parent.booking_current_group=1"); $first = $current_group->eq(0); $second = $current_group->eq(1); if ($page->booking_numberofpeople->id == 1 ) { $page->setAndSave('booking_group', $first); } else if ($page->booking_numberofpeople->id !== 1 ) { $page->setAndSave('booking_group', $second); } }); I have this code in ready.php It works when i change to (Pages::save) but when (Pages::added) it doesn´t work? It sets the value to $second regardless.. Any clues? booking_group = page reference field booking_numberofpeople = select option field the options in booking_numberofpeople is 1=1 2=2 3=3 4=4 5=5 Also i have set the required value to 1. The pages are created from the module (Simple Contact Form). Please help!!
-
I'm trying to do something I thought would be simple, but maybe not!... I'm trying to hook ProcessPageEdit to change the label/text displayed on the page save button in certain circumstances based on fields in that page. But can't figure if it's possible to target the button. I can get to the button actions in the drop down with ProcessPageEdit::getSubmitActions but this array doesn't contain the top level label only the drop down values. Can anyone point me in the right direction here please? TIA ?
-
I have a couple of pages that has a specific template. In the template i have a page reference field (Activator). What im trying to achive is when i select an option in the dropdown (Activator) in admin and save, then the specific option is disabled (greyed out) in all of the other pages that has the page reference field (Activator). So basically you can only select one specific option once. But when i unselect the same one i want it to be reset and can be selected again in other pages. Please help!
- 3 replies
-
- select options
- hooks
-
(and 1 more)
Tagged with:
-
Hey all, I have pages that can be created two ways, in the PW admin and via an endpoint where JSON data is sent. I have a hook that checks field data on page save and errors if it is not correct. I would like this to create an error in the PW admin using $this->errors, but if the page is created via the Page API in the script that handles JSON requests I want to throw an exception I can catch and handle accordingly for the JSON response. This also allows for setting a user friendly error message for the PW admin, and machine friendly data for API use. Trying to figure out how to detect which context that the page is being created in. A script, or in the PW admin and act accordingly. Is this possible?
-
Hey all, I've been building a new feature on our PW site and the URL hooks are exactly what the doctor ordered. I am developing a RESTful web API to allow external platforms to integrate with our site. The API endpoints are namespaced at /api/v1. I am having trouble making use of the named route parameter using regex. I have a hook for /api/v1/offline-events that gets all Offline Event pages. The next route to be defined is /api/v1/offline-events/{event ID}. The event ID is using a custom string (not the Page ID) that is a 15 character alphanumeric string like this: E4LxQRXZadgjstw. However when I use a PCRE compliant regular expression (tested on regexr.com) I get an error. Here is my hook code for reference and the error produced: <?php namespace ProcessWire; $wire->addHook('/api/v1/offline-events/{eventId:#(?=.{15}$)([A-Za-z0-9])\w+#}/?', function($e) { // ommitted for brevity }); // Error thrown: Warning: preg_match(): Unknown modifier '(' in /wire/core/WireHooks.php on line 1170 I've tried using all delimiters in the documentation (!@#%) but no change in the outcome. The second question is how do I create a wildcard capture for all 404s that occur after the /api endpoint? Currently it is loading the website 404 page. Many thanks!
-
While this is working perfectly: public function init() { $this->addHookAfter('Page::render', $this, 'replaceEverything'); } This is not, nothing happens: public function init() { $this->addHookBefore('Page::render', $this, 'replaceEverything'); } replaceEverything looks simply like this: public function replaceEverything($event) { $event->return = "REPLACED, YO!"; } I want to replace the rendering entirely, so a hook after would not be efficient, since I don't need the previous rendering. What's wrong?
-
Hi I'm working with PW for some time now, but – please don't laugh – never used hooks. I have 200 – 300 subpages with the template 'artist', the title of each page represents the full name, sometimes John Doe, sometimes John Emmet Brown Doe. I want to auto-populate the field 'artist_last_name' with the word that is most likely the last name, the last word of the page title. If there are more names, it can be changed manually. I need this for easier and quicker alphabetical sorting. What I put together is this, in ready.php. Doesn't work. Again, I'm new to hooks.. <?php $wire->addHookBefore('Pages::saved', function(HookEvent $event) { $p = $event->arguments('page'); if($p->template != 'artist') return; if($p->artist_last_name) return; $ln = end(explode(' ', $p->title)); $p->set('artist_last_name',$ln); }); ?> In the best case this would be done once globally without having to open and save every page. But new pages should populate the field on page save. Thanks for your help, I think it's easy, I just need a push.. Nuél
-
Hi, is there a hook after the current (active) page got created? Or which method got called in the Page class after the Constructor of the current page got initialized? Thanks.
-
I need to restrict editing access based on both the user role, and the value of a sub-field in a page reference field. eg $page->competition->closingDate There's some discussion about restricting editing access based on role, and that works, but when I try to access the page via the method @Robin S suggests, the page returned is an admin page rather than the page being edited, so I don't have access to the edited page's fields.
-
So I thought of hooking up with hooks. I installed that "hello world" module, and now, when I save a page, I see "Hello world..." message on top -- which makes sense. To play with it further, I copied the hook code from the module to my header.php file. The code is: $pages->addHookAfter('save', $this, 'example1'); function example1($event) { $page = $event->arguments[0]; $this->message("Hello World 2 ! You saved {$page->path}."); } It is a verbatim copy of the code from the module, and it is placed in a file that gets called first via prependTemplateFile in config.php. However, now when I save a page, I don't see the message from my hook function. What could I be missing? thanks,
-
so i am trying to put CustomHooksForVariations.module, a custom module, i am placing it into site/modules direcotry yet my modules page in admin panel gives me errors so this is the screen show when i refresh modules, i dont know why the shole hook is written on top of the page :|| and this next image is when i try to install it, i saw that it is not defiuned modules.php but it shouldnt need to be ?, any ways i dont want to edit site's core just to make one moulde work there has to be a way
-
Am I right in thinking that multiple hooks that are added for the same event can be prioritised by doing something like this... $session->addHookAfter( 'login', $this, 'myLoginHook', array('priority'=>xyz) ); Where xyz is the relative priority for the hook and the lower the value, the earlier it will be called as the hooks are executed?
-
Continuing my journey into PW hooks, I'm trying to find a way to retrieve all images from a page that explicitly *do not* have a certain tag (or tags) attached to them. Found this post from 2015 But I'm wondering if there's a more elegant way to go about this. Let's say I have a multi-image field called "images_header" and instead of $page->images_header->findTag('mytag'); I would like to do this: $page->images_header->excludeTag('mytag'); So I'd be able to do // find images that don't have the tag "mytag" $images = $page->images_header->excludeTag('mytag'); // check if there's any images if (count($images)>0) { // do something.. } Would this be possible by hooking into Pagefiles somehow? There's this bit in /wire/core/Pagefiles.php Line 626 that I'd basically just need to reverse (or at least in my mind ? ) public function findTag($tag) { $items = $this->makeNew(); foreach($this as $pagefile) { if($pagefile->hasTag($tag)) $items->add($pagefile); } return $items; } Any ideas on how this could be done in a graceful manner? Thanks in advance!
-
Getting a little deeper into the ProcessWire state-of-mind here. I seriously think I wouldn't have come back to webdev if it wasn't for this wonderful little gem of a CMS. I have an "Options" field added to all users on a site. If the user has anything other then "default" selected, I would like to show a permanent message in the admin like the one in the screenshot, only so that the user can't close it. As a friendly reminder that he changed that option from default to something crazy ? I've read up on how to send messages to users, but where would I hook into to make this show up all the time in the backend? https://processwire.com/api/ref/wire/message/ Thanks in advance!
-
Hello, Here's what I'm trying to achieve : I have a textarea field that is frontend editable on any page I want to. If a user with a specific role updates it, I want to tick a checkbox field on the parent page where the textarea resides. So I tried to hook as follows in my ready.php file : $this->addHookBefore('Fieldtype::savePageField', function(HookEvent $event) { $page = $event->argument[0]; $field = $event->argument[1]; if($this->user->hasRole('teacher')) { $page->alertBox = 1; } // DEBUG HERE (bd($page), l($field)... ???? }); Of course (!) it doesn't work (yet !) but the thing is I have no idea how to debug this since my bd() never triggers. I've tried the 'Event interceptor' of Tracy debugger which helped me setting up my hook. I guess my $page and $field are correct... but how could I go any further ? The road is long to become a dev (when you're not one ? )... For further information in case someone wonders : I'd like to set up a textarea that is modified by a user (having a 'player' role, ie 'student'). When front-end modified, I wish it would automatically alert me (the teacher) by ticking a box. So when I log in with my teacher role, I get a list of all textareas I have to read over. When I read/correct/update them (front-end), I would like my hook to automatically untick the box to remove it from my list. In other words, the 'textarea' status should go back and forth according to who modified it last. For the time being, I have managed to make it work with a checkbox that the user has to manually tick/untick, but I've noticed many kids forget to tick the box so they edit their text and I don't get a notice ? Thanks if anyone takes time to try and help me on that one !
-
Hello, I'm struggling with hooks to try and do this : adding user page to a page field when visiting a particular page on my website. In fact, I want to have a list of connected users on a particular page (called meeting-hall). I've tried all kinds of things (which I don't really understand, sorry) but the latest is this : $wire->addHookBefore('Page::render', function($e) { $page = $e->arguments(0); if ($page->is("name=meeting-hall")) { $page->connected->add($user); // connected is a Page field $page->save(); } else { $meeting = wire("pages")->get("name=meeting-hall"); $meeting->connected->remove($user); $meeting->save(); } }); I put this in my _init.php file but nothing works... The best I managed was updating my 'connected' field when the user loads the 'meeting hall' page by having this on the page template : if ($user->isLoggedin()) { $page->connected->add($user); $page->of(false); $page->save(); $page->of(true); } But I wanted to remove the user when he or she leaves the page... hence my thought about using hooks... If you can give me a hint, I'd greatly appreciate !
-
Hi guys (didn't see any gals here), I have a module which has some custom helper classes. One of these classes has methods which are hookable. The class is derived from WireData. An additional complication is that this class has its own namespace. What would be the correct way to provide the hookable methods via API? Here is a very simplified code sample of the class: <?php namespace SnipWire\Services; use ProcessWire\WireData; use ProcessWire\WireException; class Webhooks extends WireData { ... public function ___handleOrderCompleted() { if ($this->debug) $this->wire('log')->save( self::snipWireWebhooksLogName, '[DEBUG] Webhooks request: handleOrderCompleted' ); $this->responseStatus = 202; // Accepted } public function ___handleOrderStatusChanged() { if ($this->debug) $this->wire('log')->save( self::snipWireWebhooksLogName, '[DEBUG] Webhooks request: handleOrderStatusChanged' ); $this->responseStatus = 202; // Accepted } ... } The class itself is invoked/used by a hook from within the main module: Depending of the event, one of the methods of the Webhooks class above is triggered. A developer should now be able to use one of the ___handle* methods to do further things. /** * Check for webohook request and process them. * (Method triggered before ProcessPageView::execute) * */ public function checkWebhookRequest(HookEvent $event) { if ($webhooksEndpoint = $this->get('webhooks_endpoint')) { if ($this->sanitizer->url($this->input->url) == $webhooksEndpoint) { /** @var Webhooks $webhooks Custom ProcessWire API variable */ $this->wire('webhooks', new Webhooks()); $this->wire('webhooks')->process(); $event->replace = true; // @note: Tracy Debug won't work from here on as normal page rendering is omitted! } } } Should I provide a custom API variable e.g. $webhooks? Or how is the best way to do this?
-
Hi there, I'd like to prevent duplicate values in a specific page field (FieldtypeText) across all pages. The hook should also prevent saving of the page if a duplicate value is detected. Therefore I created two hook methods but I can't get it to work properly. Here are both hooks placed in init() method of an autoloader module: The first hook is to initially preset the field with the page id. The field value can be customized by the admin before saving. The second hook should check if the value is unique. If I create a new page and try to save, the WireException is already triggered in first step of page creation, where you enter the page title: SKU [] is already in use If I check the value of $sku (with Tracy) it's always empty! Any idea what could be wrong? EDIT: if I remove the second hook, the field value is correctly preset with the page id! $this->addHookAfter('Pages::added', $this, 'presetProductFields', ['priority' => 99]); $this->addHookAfter('Pages::saveReady', $this, 'checkSKUUnique', ['priority' => 101]); And this are the hook methods: public function presetProductFields(HookEvent $event) { $snipwire = $this->wire('snipwire'); if (!$snipwire) return; $page = $event->arguments(0); if ($snipwire->isProductTemplate($page->template)) { if ($page->hasfield('snipcart_item_id')) $page->setAndSave('snipcart_item_id', $page->id); } } /** * Check if the SKU value is unique across all product pages. * (Method triggered after Pages saveReady -> just before page is saved) * * @throws WireException * */ public function checkSKUUnique(HookEvent $event) { $snipwire = $this->wire('snipwire'); if (!$snipwire) return; $page = $event->arguments(0); if ($snipwire->isProductTemplate($page->template)) { $field = $page->getField('snipcart_item_id'); $sku = $page->snipcart_item_id; // SKU field value bd($sku); if ($page->isChanged('snipcart_item_id')) { $exists = $this->wire('pages')->get("snipcart_item_id=$sku"); if ($exists->id) { // value is not unique! $error = $this->_('SKU must be unique'); $exception = sprintf( $this->_('SKU [%s] is already in use'), $sku ); $inputfield = $page->getInputfield($field); $inputfield->error($error); throw new WireException($exception); // Prevent saving of non-unique value! } } } }