Leaderboard
Popular Content
Showing content with the highest reputation on 11/22/2021 in all areas
-
Fieldtype modules Not really a tutorial, but a bunch of stuff I learnt in building a fieldtype module (FieldtypeMeasurement). That module is used as a starting point for many of the examples. Happy to take corrections and improvements ? Basics For a (full-featured) module, you actually need two module files: FieldtypeModuleName.module; and InputfieldModuleName.module The Fieldtype module defines the general settings for the fieldtype (how it appears in the setup->field page), together with how it interacts with the database, while the Inputfield module defines how the field appears when editing it in a page. In addition, for complex fields, you can define a class to hold your field values in an object. This allows you to provide custom methods for use in the API. Otherwise you can store field values as any existing type or ProcessWire object The chart below summarises the interactions of these elements and the subsequent sections describe the methods in more detail. Fieldtype Module The important methods are described below. __construct() Generally not much is required here (apart from parent::__construct();). If you have a php script with the field object then include it (require_once() ). SQL database interaction getDatabaseSchema(Field $field) This is essential. It states what data will be saved to the SQL database (via the sleepValue function - see below). An example is: public function getDatabaseSchema(Field $field) { $schema = parent::getDatabaseSchema($field); $schema['data'] = 'double NOT NULL'; // value in base units $schema['magnitude'] = 'varchar(64)'; // value in current units - needs to be text to store composite values $schema['unit'] = 'text NOT NULL'; $schema['quantity'] = 'text NOT NULL'; return $schema; } 'data' is required and is a primary key field which means that 'text' cannot be used, although varchar(64) is OK. Often (as here) it would be a numeric field of some type. Other items can be defined as required. ___sleepValue(Page $page, Field $field, $value) This determines how the ProcesssWire field object is mapped to the SQL schema. You need to return an array where the keys are the elements defined in the schema, e.g.: $sleepValue = array( 'data' => $data, 'magnitude' => $magnitude, 'unit' => $value->unit, 'quantity' => $value->quantity ); return $sleepValue; ___wakeupValue(Page $page, Field $field, $value) This is basically the inverse of sleepValue - mapping the array from the database into the field object. In the example below, the field object (Measurement) extends WireData. Properties in WireData objects can be stored in the 'data' property via 'get' and 'set' methods. getBlankValue() (see next section) performs the initial setting of these - config values for the field (see below) can be set, but otherwise the settings are just placeholders. Properties set to a WireData object can also be set or accessed directly, like $item->property or using array access like $item[$property] public function ___wakeupValue(Page $page, Field $field, $value) { // if for some reason we already get a valid value, then just return it if($value instanceof Measurement) return $value; // start a blank value to be populated $measurement = $this->getBlankValue($page, $field); // if we were given a blank value, then we've got nothing to do: just return a blank Measurement object if(empty($value) || !is_array($value)) return $measurement; // create new Measurement object $measurement->quantity = $value['quantity']; // ... custom processing ... $measurement->baseMagnitude = $value['data']; if($value['unit']) { $measurement->unit = $value['unit']; $units = $measurement->getUnits(); if(array_key_exists($value['unit'], $units) && isset($value['magnitude'])) { $measurement->magnitude = explode('|', $value['magnitude']); } else { $measurement->magnitude = $measurement->baseMagnitude; $measurement->unit = $measurement->units->base; $this->error('... error msg ...'); } } else { //... } if(!is_array($measurement->magnitude)) $measurement->magnitude = [$measurement->magnitude]; return $measurement; } getBlankValue(Page $page, Field $field) This should return an empty item of the appropriate type. For instance, if your field object is an array, just return array(); If the field type is an object then you will need to return a 'new ObjectClassName()'. Pre-fill any config values from the Fieldtype settings but leave blank those which are set in the Inputfield, In the above example, the field object data was set as follows: public function getBlankValue(Page $page, Field $field) { /* @var $field FieldtypeMeasurement */ $measurement = new Measurement($field->quantity, null, []); if ($field->quantity) $measurement->set('quantity', $field->quantity); $measurement->set('magnitude', []); $measurement->set('shortLabel', null); $measurement->set('plural', null); return $measurement; } If your object has configurable fields that can be modified according to context (as defined in getConfigAllowContext() - see below), then you will need to deal with this in getBlankValue too, e.g. : public function getBlankValue(Page $page, Field $field): Measurement { //NB Field details may differ between templates so we need to get the field in context $context = ($page && $page->id) ? $field->getContext($page->template) : $field; ... $measurement = new Measurement($context->quantity, null, []); ... return $measurement; } But note that this does not completely deal with the situation where the field is in repeater matrix items where the types might have different contexts - there you might need: public function getBlankValue(Page $page, Field $field): Measurement { //NB Field details may differ between templates so we need to get the field in context $context = ($page && $page->id) ? $field->getContext($page->template) : $field; ... // if($page instanceof RepeaterMatrixPage) { // This does not always work - see edit note if($page->template->pageClass == 'RepeaterMatrixPage') { if($page->getField($field->name)) { $context = $page->fieldgroup->getFieldContext($field, "matrix$page->repeater_matrix_type"); } } ... $measurement = new Measurement($context->quantity, null, []); ... return $measurement; } See this post for more details. Configuration ___getConfigInputfields(Field $field) This defines how the Details tab in the field setup page will look. The best thing to do here is to find a fieldtype module that is similar to the one you want if you are uncertain. Broadly the process is: define the config object - $inputfields = parent::___getConfigInputfields($field); for each config item, use the appropriate input field, e.g . $f = $this->modules->get("InputfieldSelect"); assign the relevant attributes. $f->name = $f_name; is important as it enables the item to be subsequently referred to as $field->f_name in, for example, getBlankValue(). append each item ($inputfields->append($f);) and return $inputfields; ___getConfigAllowContext(Field $field) This determines if the above input fields are allowed to have unique values per Fieldgroup assignment enabling the user to configure them independently per template in the admin, rather than sharing the same setting globally. E.g. public function ___getConfigAllowContext(Field $field) { $a = array('quantity', 'units', 'hide_quantity', 'show_update'); return array_merge(parent::___getConfigAllowContext($field), $a); } In this example the settings 'quantity', 'units', 'hide_quantity' and 'show_update' can be varied in different template contexts. Link with the Inputfield module This is done with getInputfield(Page $page, Field $field) e.g.: public function getInputfield(Page $page, Field $field) { $inputfield = $this->wire('modules')->get('InputfieldMeasurement'); $inputfield->setField($field); return $inputfield; } If you want to reference the current page in the inputfield, you will also need to include $inputfield->setPage($page); If your fieldtype is an object and you want full context flexibility including for different repeater matrix item types, then you may need to use this: public function getInputfield(Page $page, Field $field): Inputfield { $inputfield = $this->wire('modules')->get('InputfieldMeasurement'); if($page->template->pageClass == 'RepeaterMatrixPage') { if($page->getField($field->name)) { $field_in_context = $page->fieldgroup->getFieldContext($field, "matrix$page->repeater_matrix_type"); if($field_in_context) { $field = $field_in_context; } } } $inputfield->setField($field); $inputfield->setPage($page); return $inputfield; } Inputfield Module __construct() Generally not much is required here (apart from parent::__construct();). If you have a php script with the field object then include it (require_once() ). Configuration ___getConfigInputfields() This is pretty much exactly the same construction as the similar method in the Fieldtype class. The only difference is that these settings will appear in the 'input' tab of the fieldtype settings, rather than the 'details' tab. ___getConfigAllowContext(Field $field) This is the equivalent to the Fieldtype::getConfigAllowContext() method, but for the "Input" tab rather than the "Details" tab. Input and output The key methods for this module are to render it from the fieldtype and database and to process the user inputs. ___render() $field = $this->field will have the field config settings from the fieldtype module. $this->attr('value') will have the current values for the field. If there is no current values then, if using a field object, you will need to create a new object, e.g.: if($this->attr('value')) $value = $this->attr('value'); // Measurement object else { $value = new Measurement(); } You can then use $field and $value to display the inputfield (which might be a fieldset) as required using the appropriate pre-existing inputfield modules. (Again, find an existing module that is similar, if you are uncertain). renderValue() This is required where the field is locked (not editable) and therefore render() does not apply. Get the value with $value = $this->attr('value'); and then apply the required formatting, returning the output string. ___processInput(WireInputData $input) Here you take the inputs and update the field values. As in render(), set $value = $this->attr('value') ; and then modify $value for the inputs. For example, set $name = $this->attr('name'); and then assign the inputs thus: $input_names = array( 'magnitude' => "{$name}_magnitude", 'unit' => "{$name}_unit", 'quantity' => "{$name}_quantity", 'update' => "{$name}_update" ); You can then loop through the inputs and carry out the required updates. The example below is slightly convoluted but illustrates this: foreach($input_names as $key => $name) { if(isset($input->$name) && $value->$key != $input->$name) { if($key == 'magnitude') { $input->$name = trim($input->$name); if(!is_numeric($input->$name)) { $magnitude = explode('|', $input->$name); $magnitude = array_filter($magnitude, 'is_numeric'); $value->set($key, $magnitude); } else { $value->set($key, [$input->$name]); } } else { $value->set($key, $input->$name); } $this->trackChange('value'); } } When all is done, return $this; Custom classes As mentioned earlier, for complex field types it may be useful to set up custom classes to hold the data. Typically a custom class would extend WireData, which is ProcessWire's class designed for runtime data storage. It provides this primarily through the built-in get() and set() methods for getting and setting named properties to WireData objects. The most common example of a WireData object is Page, the type used for all pages in ProcessWire. Properties set to a WireData object can also be set or accessed directly, like $item->property or using array access like $item[$property]. If you foreach() a WireData object, the default behaviour is to iterate all of the properties/values present within it. Do not declare any such properties in your class (or declare properties with the same name) otherwise you will end up with two properties, one in the 'data' array and one outside it and endless confusion will result. It is advisable to put any such classes in your own namespace. In that case, you will need to include 'use' statements in your script - e.g. use ProcessWire\{FieldtypeMeasurement, WireData}; use function ProcessWire\{wire, __}; and also include use statements in your module scripts, e.g. use MetaTunes\MeasurementClasses\Measurement;7 points
-
Nice overview! For full fledged field types, you'll often also want to implement Fieldtype::getConfigAllowContext and Inputfield::getConfigAllowContext. They return an array with the names of those config fields defined in their respective getConfigInputfields method that may be overridden in template or fieldgroup context. The Inputfield class by default returns these config fields: return array( 'visibility', 'collapsed', 'columnWidth', 'required', 'requiredIf', 'requiredAttr', 'showIf' ); So make sure to call parent::getConfigInputfields first, then add your own config inputs. A good example is how InputfieldText does this: public function ___getConfigAllowContext($field) { $a = array('initValue', 'pattern', 'placeholder', 'maxlength', 'minlength', 'required', 'requiredAttr'); return array_merge(parent::___getConfigAllowContext($field), $a); } Now the initValue, pattern, placeholder, maxlength, required and requiredAttr settings can be overridden in each template the field is used in.3 points
-
2 points
-
That was the original intention but now I don't think it will happen. I will provide a 'getting started' frontend code that testers can use ?.2 points
-
1 point
-
1 point
-
You can set your own path to your data directory ? https://processwire.com/api/ref/paths/ or you can use the root directory: $urls->root Site root: / (or subdirectory if site not at domain root)1 point
-
Thanks @Robin S. I'll add those settings/permissions to my modules later on. Just in case. For 3rd party modules I might to keep it a bit more future-proof. Thanks @BitPoet. Your untested but working code was the solution for my upcoming problems here. That it's safe for future updates is one of the best parts.1 point
-
Yes, right now it lists all fields and then anonymizes the selected fields on every page, regardless of the template. If more people are interested in this module, I could add more options. You are also free to modify the code or contribute additions to the module @wbmnfktr.1 point
-
I have indeed expressed interest! ? Yup! Haha, mad. I've uploaded an example of the amount of colours for one T-shirt and the various sizes available, for your enjoyment ha. Insane. So those have all been added. Yeah so, I've not made it clear that I meant when editing the page like a donut. See screenshot with red arrows. I have added hook so that when the page is saved and a quantity has been entered for that row it grabs the price from the related product (col 1) and then adds it to the price field and calculated the line total. It would be awesome if there were a way to either output the available variant sizes and colours as separate selects say, or as one join column called variations. I applaud your efforts, patience and skill! It looks and sounds phenomenal based on the ongoing discussion in the PadLoper thread. Imagine if you drop it on Christmas... We could call you Santa? ?1 point
-
For a customer I needed a bunch of pictures to make a mosaic in Photoshop (cropped as a square) ? 20 lines of ProcessWire "et voila": a folder full of first pictures of every page from a certain template. <?php namespace ProcessWire; // boot api include('index.php'); // find estates from web database $estates = $pages->find("template=archief-pand"); // if estates count not zero if(count($estates)){ // loop over estates ($e is single estate) foreach($estates as $e){ // if images not null if(count($e->images)){ // get first image of estate gallery $firstPic = $e->images->first(); // resize and crop image to square 800x800px $firstPic = $firstPic->size(800,800); // if picture not null then copy picture to folder propics if(!empty($firstPic)) copy($_SERVER['DOCUMENT_ROOT'].$firstPic->url, $_SERVER['DOCUMENT_ROOT'].'/propics/'.$firstPic); } } }1 point
-
Hello ! First of all thanks a lot for this post, as a beginner in php and PW I was very happy to find it. But I was wondering if you could direct me to a solution in my case. Because I have applied your code and it works well, but I have one issue with "Confirm Form Resubmission" dialog. What I want: -possibility to add new pages to database of certain template (and it works) Frontend: Code example (top of the page): <?php if ($input->post('title_doc')) { $p = new Page(); $p->template = 'Document'; $p->parent = wire('pages')->get('/Documents/'); $p->title = $input->post->name('title_doc'); $title_format = str_replace("_", " ", $p->title); $p->title = $title_format; $p->save(); } if ($input->post('title_org')) { $p = new Page(); $p->template = 'Organisation'; $p->parent = wire('pages')->get('/Organisations/'); $p->title = $input->post->name('title_org'); $title_format = str_replace("_", " ", $p->title); $p->title = $title_format; $p->save(); } Form example: <div class="pf-sub-column doc_c_light"> <h2 class="pf-title "> Documents <input class="add_document doc" onClick="$('.doc_add').toggleClass('display')" type=button value="+"/> <form class="doc_add add_document_form " method="POST" action="<?php echo $page->url; ?>"> <label for="title_doc">Title</label> <input name="title_doc" type="title" /> <input type="submit" value="add"/> </form> </h2> <?php include('./checkbox_pf_docs.php')?> </div> My issue is not the creation of the page, but that if you open the page and want to press "back" , it shows this window, because my form is not cleaned or refreshed somehow. I was wondering if there is a simple solution I am not aware of due to my nubeeness. Best, Ksenia1 point
-
My opinion: Great work. I was impressed ? Only thing was after I clicked skip intro I got a strange sound and was not able to turn that off. No mute button or the like... a page refresh made the annoying sound go away.1 point
-
1 point
-
I had quite a bit of fun watching the intro video. It didn't really seem to (for me) add anything of value in terms of purpose or content, but it was fun - and different. If all websites started doing this I'd definitely be turned off, but in a case where it's a unique scenario and having some free time, it worked well. I'd be curious - if you're willing - to hear more about how you're handling the cookie consent to only allow scripts if/when a user allows it? It sounds like it's a fairly simple, yet robust solution, and since there are many various solutions to this, I'd be curious to hear yours (again, if you're willing to share). A well done, clean design overall! I might pop a link to this over to Jack McDade (yes, Statamic's Jack McDade) since he loves 80's/90's style designs, though his are more in the pop-fashion that SciFi, he does love everything from that decade genre.1 point
-
Hi, welcome to the ProcessWire forums! The questions in your recent threads are kind of broad and it’s difficult to tell what exactly you’re struggling with and what you already know. I’m assuming you have ProcessWire installed and working, and you want to save some user inputs from the frontend into the fields you have created in PW’s admin area. Saving fields using the ProcessWire API First of all, you need a page to hold the data. Perhaps this page already exists, then you just get it using selectors, or maybe it’s the page you’re already on. Likely you want to save a new page for every time a user fills out the form, so let’s create a page: $p = new Page(); $p->template = 'form-submission'; //or whatever your template is called $p->parent = wire('pages')->get('/submissions/'); //or whatever the parent page is called Nice. Now, to get the information from the form you’re going to need ProcessWire’s input capabilities. You can find out all about it at the link and you’ll need to make some decisions about validation/sanitization depending on the data you’re dealing with, but the gist is to get stuff from the POST request sent by your user and put it into fields. Let’s say your user submits their e-mail address and you want to save it to a field called email: /* You see the name “email” three times: * 1. The first is the field you set up in the admin area. * 2. The second is the sanitizer that makes sure the thing the * user sent you actually looks like an email address. * 3. The third is the name of the input from from the form the * user filled out and submitted. */ $p->email = $input->post->email('email'); //Now you will probably want to fill out some more fields the same way: $p->comment = $input->post->textarea('comment'); $p->title = 'Form submission from ' . date('Y-m-d H:i'); Then you save the page and you’re done. You can go to the admin backend and check out the newly created page. $p->save(); However, we haven’t talked about the frontend bits yet. Creating a frontend form and sending submissions to ProcessWire Once again much depends on what you actually want to do, but for simplicity’s sake, let’s say this all happens on the same page. You have a ProcessWire template associated with a template file and all the above code is in it. Now we put the form into the same file. Basically the form is sent to the same page it’s coming from, but as you’ve seen above, you can still create the new page wherever you want. So here’s a simple HTML form that submits to the same page: <form method="POST" action="<?php echo $page->url; ?>"> <label for="email">Your e-mail address</label> <input name="email" type="email" /> <label for="comment">Your comment (no swearing!!!)</label> <textarea name="comment" rows="5"></textarea> <input type="submit" value="Send"/> </form> Note how the names of the input fields match the names we accessed earlier using $input->post(). Putting it together If you simply copy all my code blocks into your template file, you’ll probably notice that it tries to create a new page ever time the page is loaded. You’ll need to figure out if there is a form submission at all before you deal with it, and otherwise just skip that part and show the blank form. You may just check if there is anything in the comment variable. So here is your complete template file: <?php namespace ProcessWire; $thanks = ""; if ($input->post('comment')) { $p = new Page(); $p->template = 'form-submission'; //or whatever your template is called $p->parent = wire('pages')->get('/submissions/'); //or whatever the parent page is called $p->email = $input->post->email('email'); $p->comment = $input->post->textarea('comment'); $p->title = 'Form submission from ' . date('Y-m-d H:i'); $p->save(); $thanks = "Thanks a bunch, we value your feedback!"; } ?> <!doctype html> <html> <head> <title>Post a comment</title> </head> <body> <?php if ($thanks !== "") { echo "<h1>{$thanks}</h1>"; } ?> <form method="POST" action="<?php echo $page->url; ?>"> <label for="email">Your e-mail address</label> <input name="email" type="email" /> <label for="comment">Your comment (no swearing!!!)</label> <textarea name="comment" rows="5"></textarea> <input type="submit" value="Send"/> </form> </body> </html> Now I’m not saying you should put this exact code on your website, but it’s a demonstration of the most bare-bones things you’ll need to get input from users: A HTML form that generates a POST request and some PHP code to receive it. It doesn’t matter where these things are or what they’re called or how they’re generated. In this example we’ve written the form by hand because it’s easy, but you could just as well generate it from ProcessWire’s fields.1 point
-
New module: OneTimeOnlyCode https://github.com/benbyford/OneTimeOnlyCode Use this module to create codes with urls (or any of string data) and later check a code and its data e.g. to get one time access to content. I built this to send out specific "exclusive" users codes at the end of URLs to new articles where noramlly they would have to log in -> now they can see the content only once and then have to login if they navigate away. I made it only to show the content not any user profile infomation etc when using the code, be warned not to use it to show user information unless you know what you're doing! Codes can have an expiry and uses lazy-cron to cull codes out of day at a set interval (both in checking and elapsed interval for the codes). Check it out!1 point
-
We have a hook for that by now: https://processwire.com/api/ref/pages/published/ <?php $wire->addHookAfter("Pages::published(template=yourpagetemplate)", function(HookEvent $event) { $page = $event->arguments(0); $mail = new WireMail(); $mail->subject("Page {$page->title} has been published..."); ... $mail->send(); }); Not sure if that is really what you want though... Maybe you want to send an email when the page is created? Then Pages::added is for you: https://processwire.com/api/ref/pages/added/1 point
-
Thunder Client — lightweight alternative to Postman Thunder Client is a lightweight Rest API Client Extension for Visual Studio Code (it is not open-source). Launch article Quick demo1 point
-
The module doesn't work with latest PW Version. Please update or remove from directory.1 point