-
Posts
16,762 -
Joined
-
Last visited
-
Days Won
1,529
Everything posted by ryan
-
@MrSnoozles Yes you can do this, but not directly. There aren't any file/image functions in comments, but you can use $comment->setMeta('name', 'value'); to store whatever you'd like. So in my case, when someone submits a review with photo attachments, I create a directory /site/assets/comments/[page-id]/[comment-id]/ and then copy the files into it. To do this, take the saveForm() hook code I posted above and append this into it. Note that 'photos' refers to the name if my files field in FormBuilder, but it's also the name I'm using to store an array of photo files (basenames) in my comment meta data: $entry = $processor->getEntry(); if($comment->id && !empty($entry['photos']) && $entry['id']) { $meta = $comment->getMeta(); $path = $processor->entries()->getFilesPath($entry['id']); $meta['photos'] = addCommentPhotos($comment, $entry, $path); $field->updateComment($page, $comment, [ 'meta' => $meta ]); } The snippet above got the meta['photos'] from an addCommentPhotos() function. Here's an example of that function below. It copies the photo files out of the entry, validates them, and places them into the /site/assets/ directory I mentioned above. Note that it requires the FileValidatorImage module. function addCommentPhotos(Comment $comment, array $entry, $sourcePath) { $files = wire()->files; $sanitizer = wire()->sanitizer; $validator = wire()->modules->get('FileValidatorImage'); /** @var FileValidatorImage $validator */ $page = $comment->getPage(); $targetPath = wire()->config->paths->assets . "comments/$page->id/$comment->id/"; $photos = []; // create target directory for files if(!is_dir($targetPath)) $files->mkdir($targetPath, true); foreach($entry['photos'] as $filename) { $sourceName = basename($filename); $sourceFile = $sourcePath . $sourceName; if(!is_file($sourceFile)) continue; // file not found if(!$validator->isValid($sourceFile)) { // photo not valid $processor->addWarning("Error adding photo $sourceName - " . $validator->getReason()); continue; } $sourceExt = pathinfo($sourceFile, PATHINFO_EXTENSION); $sourceName = pathinfo($sourceFile, PATHINFO_FILENAME); $n = 0; // ensure filename is unique do { $targetName = $sanitizer->fieldName($sourceName); if(empty($targetName)) $targetName = 'photo'; $targetName .= ($n ? "-$n" : "") . ".$sourceExt"; $targetFile = $targetPath . $targetName; $n++; } while(is_file($targetFile)); // copy file to target location if($files->copy($sourceFile, $targetFile)) { // populate file basename in $photos $photos[] = $targetName; } } return $photos; } That takes care of getting the photos to the right place. When you want to output then with your comments, here's roughly how you might do it: foreach($page->comments as $comment) { $cite = $comment->getFormatted('cite'); $text = $comment->getFormatted('text'); $when = wireRelativeTimeStr($comment->created); echo "<div class='comment'>"; echo "<p>Posted by: $cite ($when)</p>"; echo "<p>$text</p>"; $photos = $comment->getMeta('photos'); if(!empty($photos)) { $dir = "comments/$page->id/$comment->id/"; $url = $config->urls->assets . $dir; $path = $config->paths->assets . $dir; echo "<ul class='comment-photos'>"; foreach($photos as $basename) { $fileName = $path . $basename; $fileUrl = $sanitizer->entities($url . $basename); if(file_exists($fileName)) { echo "<li><img src='$fileUrl' alt='' /></li>"; } } echo "</ul>"; } echo "</div>"; } But wait, what about when you do things like delete a page, or an individual comment, and want to make sure the photos are deleted too? You can do this by adding hooks into your /site/ready.php file: /** * After page deleted, delete comment files * */ $pages->addHookAfter('deleted', function(HookEvent $event) { $page = $event->arguments(0); /** @var Page $page */ $path = $event->wire()->config->paths->assets . "comments/$page->id/"; if(is_dir($path)) { $event->wire()->files->rmdir($path, true); $event->message("Deleted page comments files path: $path"); } }); /** * After individual comment deleted, delete comment files * */ $wire->addHookAfter('FieldtypeComments::commentDeleted', function(HookEvent $event) { $page = $event->arguments(0); /** @var Page $page */ $comment = $event->arguments(2); /** @var Comment $comment */ $path = $event->wire()->config->paths->assets . "comments/$page->id/$comment->id/"; if(is_dir($path)) { $event->wire()->files->rmdir($path, true); $event->message("Deleted comment files path: $path"); } });
-
@Kiwi Chris Let's say you've got a FormBuilder form named "review" and it has these fields, all of which are required: reviewed_page (Page) where user selects what page they are reviewing reviewer_name (Text) reviewer_email (Email) review_text (Textarea) star_rating (Integer) recommend (Toggle Yes/No) Your comments field is named "comments". We have a template named "form-reviews" where we output this form using FormBuilder embed method C. Above the embed C output code, in that file we have a hook which takes the submitted review and converts it to a pending comment: $forms->addHookAfter('FormBuilderProcessor::saveForm', function(HookEvent $event) { $processor = $event->object; /** @var FormBuilderProcessor $processor */ $form = $event->arguments(0); /** @var InputfieldForm $form */ $field = $event->wire()->fields->get('comments'); /** @var CommentField $field */ $page = $form->getValueByName('reviewed_page'); /** @var Page $page */ $errors = $processor->getErrors(); if(count($errors)) return; // if there were errors, do not create a comment yet $comment = new Comment(); $comment->status = Comment::statusPending; $comment->cite = $form->getValueByName('reviewer_name'); $comment->email = $form->getValueByName('reviewer_email'); $comment->text = $form->getValueByName('review_text'); $comment->stars = $form->getValueByName('star_rating'); // example of using meta data $comment->setMeta('recommended', $form->getValueByName('recommended')); if($field->addComment($page, $comment, true)) { // pending comment added successfully } else { // failed to add comment $processor->addError("Error adding review/comment"); } }); That's basically it. In my case, I have a lot more "meta" fields, so my hook is longer than this. This hook could also be to FormBuilderProcessor::formSubmitSuccess but I choose the saveForm instead because at this point I can still add my own error messages that will be displayed to the user. This gives you the opportunity to do additional validation, should you want to. By the time the formSubmitSuccess method is reached, it's already been determined there were no errors, so at that point you couldn't refuse the form submission if you can't convert it to a Comment for one reason or another. Let's say your Comments field is named "comments", then $page->comments->stars() will return the average of all star ratings on $page (floating point number between 0 and 5.0). For more details, arguments, options see: https://processwire.com/api/ref/comment-array/stars/ https://processwire.com/api/ref/comment-array/render-stars/ https://processwire.com/api/ref/comment-stars/ For rendering stars it'll use ★ but if you want it to use a different icon you can use CommentStars::setDefault() i.e. CommentStars::setDefault('star', '<i class="fa fa-star"></i>')); There are lots of other things you can specify to setDefault() so it's worth looking directly in the file if you decide to use it.
-
Following up on my July 15th post, I've been working on converting ~1600 reviews from a 3rd party service (BazaarVoice) into ProcessWire for a client's website. That conversion is pretty much done now, here's an example page that has a few reviews (click on the Reviews tab). As you can see, we're using the ProcessWire comments field to store not just the review, but also 15 other details and up to 6 photos, per review. This was all done using the new "meta" features of ProcessWire's comments, mentioned in that post linked above. Though I didn't have an example to show you at the time, so wanted to link it here. Here's another example of how someone can submit a review here. This form is built in FormBuilder and uses an updated FormBuilderFile Inputfield module which will be bundled in the next version of FormBuilder. This new version of InputfieldFormBuilderFile adds a few useful features such as: The ability to show a preview image (when uploading photos). The ability to show just one file input at a time, and reveal additional ones as needed. The ability to include a text or textarea description with each file upload. These are all optional and can be enabled or disabled in the field settings. If none of the options are enabled when the output is exactly the same as before, meaning the upgrade should be seamless for existing forms already using the files input. The way that a submitted review goes from FormBuilder into a comment is automated with a hook to FormBuilder's "saveForm" method. When the form is submitted, it creates a new pending comment, translating most of the fields into 'meta' data in the comment, there's really not much to it. If you ever have a similar need, be sure to let me know and I'm happy to share the hook code that worked for me. In addition to those updates for FormBuilder, this week I've also worked on some related updates to ProcessWire's comments field, especially with improvements to the ProcessCommentsManager module. The module now has a separate dedicated comment editor where you can edit existing comments, and add new comments or replies. Previously you could not add new comments/replies in the comments manager. In the case of the site linked above, they use this particular feature for adding staff replies to reviews. I think this comments stuff is largely done now so next week I'll likely be back to working on other parts of the core and modules. Thanks for reading and have a great weekend!
- 8 replies
-
- 18
-
@Kiwi Chris @MarkE I think that probably could be achieved with a Fieldtype+Inputfield module pair. It sounds kind of like what FieldtypeCombo does except that you define the table structure in Combo, rather than Combo reading an existing table structure. No doubt it would be simpler than Combo since it would just read the table for its settings. Something like translating MySQL VARCHAR column to an InputfieldText, and a TEXT/MEDIUMTEXT column to a Textarea or CKE, and perhaps ENUM/SET columns to InputfieldRadios/InputfieldCheckboxes... that sort of thing. Could be fun to build!
-
In addition to site development work and ProcessWire core development, this week I upgraded my OS X version (from Mojave to Monterey), my PhpStorm version (from 2018 to 2022), and my Apache/PHP/MySQL environment versions. I tend to wait a bit too long between upgrades on my computer and so I ended up with a lot of things to resolve, and a still few remaining. While waiting for upgrades to install, I randomly came across one of the first sites I ever designed/developed out of college, working for Grafik in the year 2000: https://americanhistory.si.edu/subs/ ... I can't believe it's still there. It looks really dated now (it's 22 years old), but reminded me of how much things have changed in web design/development. While I'd been developing sites for a few years before this, it was just a hobby, and it wasn't until this time that it was my job. Back in 2000 there weren't a lot of people that knew how to create websites and it always felt like you were breaking new ground. Internet Explorer was king (and nobody liked it then either), Google was just a small startup, but AltaVista, InfoSeek and Yahoo were big. Sites were developed in a way that would make you cringe now. I don't think we used CSS hardly at all, but we did use tables for everything layout related. There was no such thing as "mobile" (the iPhone didn't come till 7 years later). There was no such thing as "responsive" layout and accessibility was pretty much an unknown. Most of the time we used images for a lot more than was appropriate (headlines and much more) because HTML and HTML fonts were so limited. It all seems so primitive now but what's the same is that it was fun then and it's fun now, actually it's more fun now. I don't have any point here, just that it's funny to look back at how much as changed. Last week I mentioned that we're likely to upgrade CKEditor 4 to CKEditor 5 sometime in the next year. There were several comments about that and so just wanted to talk a little more about it. First off, I really like CKEditor 4, it's been a great fit for ProcessWire. If the company behind it was going to continue building and supporting version 4 after 2023 then we'd likely stick with it. But the CKEditor 4 end-of-life is sometime in 2023 and I don't think it's an option to stick with it (in the core) after the developer of it is no longer updating or supporting it... at least not long term. While CKEditor 5 is a different animal than CKEditor 4, it's also still the closest upgrade path from CKEditor 4, and I'm hopeful it will be able to serve as a near drop-in replacement from the perspective of users (only). My hope is that by the time we've completed the upgrade to CKE 5, our clients won't necessarily know the difference or have to re-learn anything, unless they want to take advantage of something new exclusive to CKE 5. From my perspective as a developer integrating CKEditor 5 into ProcessWire, the development side is not a drop in replacement for CKE 4 (not even close), as all supporting code will have to be redeveloped. By supporting code, I mean things like the code in the InputfieldCKEditor.module file, the code for our custom CKE plugins (pwimage and pwlink), as well as anything else development related that is referring to CKEditor. There's no doubt it'll be a lot of work. But the rich text editor is one of the most important input types in the ProcessWire admin, so it's fine, it's worth putting a lot of time into. As for CKEditor 5 being bloated relative to CKEditor 4, I very much doubt that's the case. It was a complete rewrite, the folks at CKEditor know what they are doing, and it's safe to assume it's even more optimized and streamlined than CKE 4. In terms of size, the download for CKE 4 and CKE 5 are both 1.7 megabytes. As I understand it, they started with a new codebase for CKEditor 5 in part to start fresh and avoid legacy bloat. So I see this upgrade as being the opposite of bloat. So what happens with CKEditor 4 in ProcessWire when it likely is replaced with CKEditor 5? So long as CKE 5 can be a near drop in replacement for our users, and for the markup it generates, then the CKE 4 module will move out of the core and into an optional module you can install from the modules directory, when/if someone wants it. On the other hand, if the transition is not completely clean between versions then we may end up supporting both in the core for a short period of time. Though I'm hopeful this won't be necessary. There are some other interesting looking editors out there that have been mentioned, and it'd be nice to have more input options available. I see something like Imperavi's Article as a good option for some but not a replacement for our current rich text editor. At least I know my clients would not be happy to have that big of a change occur from a PW version upgrade. Likewise, something like the Easy Markdown Editor is a great option for some, and I'd like to be able to install a module for that and use it in some cases. But folks used to using CKEditor 4 in their work would not be happy to have that big of a change either. CKEditor 4 works really well for what it does, and I think the goal has to be that clients using CKEditor 4 now should be able to continue doing what they are doing with as few changes to their workflow as possible. I'm hopeful we'll be able to get there with CKEditor 5, while also gaining some benefits in the process. Where other input options make a lot of sense is when building a new site where there aren't already users depending on one input method or another. And it may be that at some point (sooner or later) it will make sense for ProcessWire to have another textarea input option that's different enough from CKE to make it worthwhile. But in my opinion, that would be a potential additional option, not a replacement, as CKE is pretty well established and expected in PW.
- 10 replies
-
- 25
-
This week on the dev branch are several updates to the comments system including support for custom fields on comments (which we're calling comment meta data). I'm currently working on a site that uses a reviews system powered by BazaarVoice. It's pretty nice but it's also very expensive (I think at least $500/month in this case). The system powers their reviews which include not just a rating and review text, but also a bunch of other criteria that the user can provide. See an example here — click the "Reviews" tab and what you see there now is currently coming from BazaarVoice. But in a couple of weeks you should see the same thing powered by ProcessWire. Think of this like a comments system with custom fields. That's not something that ProcessWire has supported before, but now this week it does. Though I know most don't need this, so have kept it pretty simple, focusing just on adding API methods to make it possible to get and set custom field values for any comment. These include: $comment->getMeta('name'); $comment->setMeta('name', $value); $comment->removeMeta('name'); The name and $value can be whatever you decide. There's also a bonus $comment->meta() method which combines the methods, letting you get or set, kind of like the meta() method on Page objects. If you want to use comment meta data like this, it's exclusively an API feature. So if you are looking to collect custom fields from users, you'll want to implement your own comment form rather than the default. In our case, we'll be using FormBuilder to implement the comment form that includes the review and custom fields. But you could just as easily use a homegrown form. When it comes to outputting that custom data, you'll want to handle it more like you would outputting a repeater, iterating through the $page->comments and then using $comment->getMeta('name'); for each of the custom properties you want to output. Worth noting is that output formatting doesn't come into play here, so if some text in the meta data needs to be entity encoded for output (for example) then that's your responsibility. How about later editing of the meta data? Should you need it, the ProcessCommentsManager module (Setup > Comments) has been upgraded to support editing of comment meta data. Next to each comment is now a "Meta" link, and if you click it, you'll get a modal window on top of the comment enabling you to edit the meta data as JSON. That might seem a little unconventional, but I'm trying to keep it simple and flexible. Most will probably use this to view meta data rather than edit it, but the ability is there when/if needed. I didn't think it would be worth spending the significant time building a full-blown page-editor like environment for editing comment meta data, but also felt like I needed something like this for occasional editing needs. The InputfieldCommentsAdmin module was also updated to have meta data links for each comment. But the reality is if you have ProcessCommentsManager installed, chances are you aren't editing comments in the page editor anyway. So a new option has been added in the comments field configuration (Input tab) enabling you to disable comments in the page editor and instead link to the editor in the comments manager. When enabled, your Comments field in the page editor would instead look like this: This is worthwhile because the comments manager is just a better place to view/edit comments in the admin since it is paginated, supports editing of all properties, and lets you sort/filter as you see fit. Whereas a big list of comments in the page editor just slows it down. This week the CKEditor version has been upgraded to 4.19.0 (see release notes). I'm also emailing with the people at CKEditor about getting us a license to use CKEditor 5 with ProcessWire. As you may or may not know, the CKEditor 5 license (LGPL) isn't compatible with ProcessWire's license (MPL2 and MIT), so we are working on a license agreement to make it possible. Since CKEditor 4 will reach EOL in 2023 it's a good time to start planning for CKEditor 5 and I'm excited to work with it. Thanks for reading and have a great weekend!
- 15 replies
-
- 21
-
Just a quick update this week. The dev branch version is now at 3.0.202 as of today. Relative to 3.0.201 this includes 22 commits of various fixes, issue resolutions, improvements and additions. While there's nothing major that's new to report in this version, there is nonetheless quite a bit there, and if you are already running on the dev branch it's a worthwhile upgrade. For full details be sure to see the dev branch commit log. Thanks for reading and have a great weekend!
-
Understanding markup region and possibly looking for alternatives
ryan replied to joe_g's topic in General Support
@joe_g That's right, it doesn't look like there's any reason to provide #maincontent for your situation. When populating markup regions, specify just the region you want to replace, append, prepend, or delete; not a structure of markup regions. Though you did have that case above where you were trying to a direct child to it: <div class="p-8"> outside of your _main.php, so in that case you would, or you could do a <div class="p-8" pw-append="maincontent">...</div> or <div id="maincontent"><div class="p-8">...</div></div>. I recommend adding <!--PW-REGION-DEBUG--> somewhere in your _main.php markup (like at the bottom), and that'll be automatically replaced with some markup that tells you everything your Markup Regions are doing and when it can't find where to place something, etc. Most of my sites have something like this below, and it's handy to look at when you aren't sure why something isn't working, etc. <?php if($config->debug && $user->name === 'ryan'): <div class"uk-container uk-margin"> <!--PW-REGION-DEBUG--> </div> <?php endif; ?> Another tip is that if you ever find Markup Regions are having trouble finding the closing tag for some specific element, you can tell it specially by marking it with a hint comment <!--#maincontent-->. Though I also just find it visually helpful, so I tend to do it whether for markup regions or not. <div id="maincontent"> ... </div><!--#maincontent--> -
This week core updates are focused on resolving issue reports. Nearly all of the 10 commits this week resolve one issue or another. Though all are minor, so I'm not bumping the version number just yet, as I'd like to get a little more in the core before bumping the version to 3.0.202. This week I've also continued development on this WP-to-PW site conversion. On this site hundreds of pages are used to represent certain types of vacations, each with a lot of details and fields. Several pages in the site let you list, search and filter these things. When rendering a list of these (which a lot of pages do), it might list 10, 20, 100 or more of them at once on a page (which is to say, there can be a few, or there can be a lot). Each of the items has a lot of markup, compiled from about a dozen fields in each list item. They are kind of expensive to render in terms of time, so caching comes to mind. These pages aren't good candidates for full-page caches (like ProCache, etc.) since they will typically be unique according to a user's query and filters. So using the $cache API var seems like an obvious choice (or MarkupCache). But I didn't really want to spawn a new DB query for each item (as there might be hundreds), plus I had a specific need for when the cache should be reset — I needed it to re-create the cache for each rendered item whenever the cache for it was older than the last modified date of the page it represents. There's a really simple way to do this and it makes a huge difference in performance (for this case at least). Here's a quick recipe for how to make this sort of rendering very fast. But first, let's take a look at the uncached version: // find items matching query $items = $pages->find('...'); // iterate and render each item foreach($items as $item) { echo " <!-- expensive to render markup here ---> <div class='item'> <a href='$item->url'>$item->title</a> ...and so on... </div> "; } That looks simple, but what you don't see is everything that goes in the <div class="item">...</div> which is a lot more than what you see here. (If we were to take the above code literally, just outputting url and title, then there would be little point in caching.) But within each .item element more than a dozen fields are being accessed and output, including images, repeatable items, etc. It takes some time to render. When there's 100 or more of them to render at once, it literally takes 5 seconds. But after adding caching to it, now the same thing takes under 100 milliseconds. Here's the same code as above, but hundreds of times faster, thanks to a little caching: // determine where we want to store cache files for each list item $cachePath = $config->paths->cache . 'my-list-items/'; if(!is_dir($cachePath)) wireMkdir($cachePath); // find items matching query $items = $pages->find('...'); // iterate and render each item foreach($items as $item) { $file = $cachePath . "$item->id.html"; // item's cache file if(file_exists($file) && filemtime($file) > $page->modified) { // cache is newer than page's last mod time, so use the cache echo file_get_contents($file); continue; } $out = " <!-- expensive to render markup here ---> <div class='item'> <a href='$item->url'>$item->title</a> ...and so on... </div> "; echo $out; // save item to cache file so we can use it next time file_put_contents($file, $out, LOCK_EX); } This is a kind of cache that rarely needs to be completely cleared because each item in the cache stays consistent with the modification time of the page it represents. But at least during development, we'll need to occasionally clear all of the items in the cache when we make changes to the markup used for each item. So it's good to have a simple option to clear the cache. In this case, I just have it display a "clear cache" link before or after the list, and it only appears to the superuser: if($user->isSuperuser()) { if($input->get('clear')) wireRmdir($cachePath, true); echo "<a href='./?clear=1'>Clear cache</a>"; } I found this simple cache solution to be very effective and efficient on this site. I'll probably add a file() method to the $cache API var that does the same thing. But as you can see, it's hardly necessary since this sort of cache can be implemented so easily on its own, just using plain PHP file functions. Give it a try sometime if you haven't already. Thanks for reading and have a great weekend!
- 2 replies
-
- 30
-
Understanding markup region and possibly looking for alternatives
ryan replied to joe_g's topic in General Support
@joe_g No, you can specify this for each template if you want to. Edit a template and see the "files" tab. Or you could just use your _main.php as a controller that loads different layout files depending on some field value or condition that you determine. Sure you can. Use PHP include(), $files->include() or $files->render() with whatever files or order you want. Regions can be replaced, appended, prepended, removed, etc. as many times as they are output. You can do it individually for templates, see my response for #1. Though I usually structure my templates in a way that the _main.php is the "base" that's generic enough to not have ever needed to define alternate files. I think markup regions are the best way myself. Once you start using them a lot, I suspect you'll find you don't want to use anything else. That's been my experience at least. I don't know exactly what you mean by nested regions. You'd have to provide more details on what you are doing before we could say if you are using something in an intended way or not. -
This week I've been busy developing a site. It's the same one as the last few weeks, which is an established site moving out of WordPress and into ProcessWire. Next week the whole thing is getting uploaded to its final server for collaboration and for client preview. I've been pretty focused on getting that ready and don't have any core updates to report this week, though should next week. One thing about the prior version of this site (and perhaps many WordPress sites) is that there wasn't much structure to the pages used in the site, and hundreds of unrelated pages in the site confusingly live off the root, such as /some-product/ and /some-press-release/ and /some-other-thing/. There's no apparent structure or order to it. And those pages that do have some loose structure in the wp-admin have URLs that don't represent that structure on the front-end. There's very little relation between the structure one sees in the wp-admin and the structure that one sees in the URLs, or in the front-end navigation. They all seem to be completely unrelated. That's one thing that I've tried to fix, so that there is some logic and structure rather than having a bunch of unrelated pages all in the same bucket (is this common in WordPress?) But there's one big caveat. We didn't want to change anything about the actual URLs that are used on the site. This is a site with a long history, a lot of incoming links, and a lot of search traffic. The current URLs have been in place a long time and we didn't want to introduce more redirects into the site (there are already a ton of 301 redirects accumulated over time). So we wanted to make sure the existing URLs in the new ProcessWire-powered site are identical to what they were in the WordPress site. That might seem difficult to do, going from an unstructured WordPress site into a highly structured ProcessWire site... but actually it's very simple. Here's how: Making ProcessWire render pages from WordPress URLs We created a new URL field named "href" and added it to most of the site's templates. For established pages that came in from the old WP site, this "href" field contains the WordPress URL for the page. Depending on the page, it contains values like /some-product/ or /some-press-release/, /some-country/some-town/, etc. In most cases this is different from the actual ProcessWire URL, since the page structure is now much more orderly in the back-end. And for newly added pages, we'll be using the more logical ProcessWire URL. But for all the old WordPress pages, we'll make them render from their original URL. This is certainly preferable from an SEO standpoint but also helps to limit the redirects in the site. In order to make $page->url return the old/original WordPress URL (value in $page->href), a hook was added to the /site/init.php file: /** * Update page paths and URLs to use the 'href' field value on pages that have it * */ $wire->addHookBefore('Page::path', function(HookEvent $event) { $page = $event->object; /** @var Page $page */ $href = $page->get('href'); if(!$href) return; // skip if page has no 'href' value $event->return = $href; $event->replace = true; }); Now we've got $page->url calls returning the URLs we want, but how do we get ProcessWire to accept those URLs for rendering pages? The first thing we'll need to do is enable URL segments for the homepage template. We do this by going to: Setup > Templates > home > URLs > and check the box to enable URL segments. Save. Then we need to edit our /site/templates/home.php to identify when URL segments are present and render the appropriate page, rather than the homepage: $href = $input->urlSegmentStr; if($href) { $href = '/' . trim($href, '/') . '/'; $page = $pages->get("href=$href"); if(!$page->id || !$page->viewable()) wire404(); $wire->wire('page', $page); // set new $page API var include($page->template->filename); // include page's template file } else { // render homepage output } As you can see from the above, when URL segments are present, we find a page that has an "href" field value matching those URL segments ($input->urlSegmentStr). If we don't find one, we stop with a 404. But if we do find one, then we set the $page API variable to it and then include its template file, making that page render rather than the homepage. If there is no $input->urlSegmentStr present then of course we just render the homepage. That's it! By using these little bits of code snippets to replace ProcessWire's URL routing, now all the URLs of the old WordPress site are handled by ProcessWire. Like most things in ProcessWire, there's more than one way to accomplish this… We could have used URL/path hooks, or we could have hooked before Page::render to take over homepage requests with URL segments, before the homepage even got involved. Or perhaps we could have hooked in even earlier, to something in the ProcessPageView module or PagesRequest class. Or we could have used an existing module. Any of these might be equally good (or even better) solutions, but I just went with what seemed like the simplest route, one that I could easily see and adjust. Plus, it'll work in any version of ProcessWire. The actual solution I used is a little more than what's presented here, as it has a few fallbacks for finding pages and scanning redirect lists, plus passes along remaining pagination/URL segments to rendered pages. I'm guessing most don't need that stuff, and it adds a decent chunk of code, so I left that out. But there are a couple of optional additions that I would recommend adding in a lot of cases: Forcing pages to only render from their "href" URL and not their ProcessWire URL (optional) Our existing hooks ensure that any URLs output for pages having "href" values are always based on that "href" value. But what if someone accesses a given page at its ProcessWire path/url? The page would render normally. We might want to prevent that from happening, ensuring that it only ever renders at its defined "href" URL instead. If the page is requested at its ProcessWire URL, then we redirect to its "href" URL. We can do that by adding the following code to our /site/templates/_init.php file: if($page->id > 1) { // ensure that we are rendering from the 'href' URL // rather than native PW url, when href is populated $href = $page->get('href'); $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; if($href && $requestUrl && strpos($requestUrl, $href) === false) { // href value is not present in request URL, so redirect to it $session->redirect($config->urls->root . ltrim($href, '/')); } } Enforcing uniqueness and slashes in "href" values (optional) It's worthwhile to enforce our "href" values being consistent in having both a leading and trailing slash, as well as making sure they are always unique, so no two pages can have the same "href" value. To do that, I added this hook to my /site/ready.php file (/site/init.php would also work): /** * Ensure that any saved 'href' values are unique and have leading/trailing slashes * */ $wire->addHookAfter('Pages::savePageOrFieldReady', function(HookEvent $event) { $page = $event->arguments(0); /** @var Page $page */ $fieldName = $event->arguments(1); /** @var string $fieldName */ if($fieldName === 'href' || in_array('href', $page->getChanges())) { // the href field is being saved $href = $page->get('href'); if(!strlen($href)) return; // make sure value has leading and trailing slashes if(strpos($href, '/') === 0 && substr($href, -1) === '/') { // already has slashes } else { $href = '/' . trim($href, '/') . '/'; $page->set('href', $href); } // make sure that the 'href' value is unique $pages = $event->object; /** @var Pages $pages */ $p = $pages->get("id!=$page->id, href=$href"); if($p->id && !$p->isTrash()) { $page->set('href', ''); $page->error( "Path of '$href' is already in use by page $p->id “{$p->title}” - " . "Please enter a different “href” path and save again" ); } } }); That's all for this week. Thanks for reading, have a great weekend!
- 3 replies
-
- 20
-
When we released ProcessWire 3.0.200 one of the biggest differences from prior master/main versions was that PW now only includes 1 profile: the site-blank profile. This means there's probably going to be more people starting with it than before. Yet it is so minimal that I thought it deserved a bit of a walkthrough in how you take it from nearly-nothing to something you can start to build a site around. Everyone will have a little bit different approach here, which is one reason why the blank profile is a useful starting point. In this new post, here are some steps you might take when starting a new site with the blank profile— https://processwire.com/blog/posts/starting-with-the-blank-profile/
- 3 replies
-
- 20
-
The ProcessPageList module now has a configuration setting where you can select pages that should not be shown in the page list. For example, maybe once you've set up your 404 page, you don't really need it to display there anymore, except maybe in debug mode. Or maybe you don't ever need the "Admin" page to display in the page list. This new feature lets you do that, and for any page that you decide. Next, a "Usage" fields has been added to the "Basics" tab in the Field editor, just like the one in the Template editor (requested feature #445). This shows you what fields are using the template. It's essentially the same information that you'll find in the "Actions > Add or remove from templates" feature, but easier to find, especially for people newer to PW. That's all for this week, I hope you have a great weekend!
- 5 replies
-
- 28
-
@bernhard That's correct, a $page->extract() wouldn't be possible as the extract() function has a unique ability to create variables, as far as I understand it. The issue with extract() is that any variables it creates are unknown to your editor/IDE, and often times to the caller too. Whereas list() is defining variables in a visible manner that is readable and known to the IDE, clearly identified in the code as $variables. I rarely like extract() in code because it's too ambiguous, like tipping over a box of an unknown items and quantity. But there are times that's exactly what's intended, like in the TemplateFile class, where it has to pass an unknown set and quantity of variables to a php template file and make them directly accessible by $variables. This is fine because the php template file knows what variables to expect even if the class that provided them (TemplateFile) doesn't. So I think extract is good for cases like that, but otherwise I think it's preferable to have some kind of variable definition whether $name = 'value'; or list($name) = [ 'value' ]; etc.
-
This week we have ProcessWire 3.0.201 on the dev branch which includes a couple minor fixes but also a couple of useful additions: There are new "Tab" field visibility options available for any field in the page editor. This makes the field display in its own page editor tab. It saves you from having to create a new/separate Tab field just to wrap around an existing field to make it display in a tab. So far I've found it particularly useful with ProFields (Combo, Table, Textareas, RepeaterMatrix) as well as the core Repeater, FieldsetPage and Images fields. For instance, I have a Combo field labeled "SEO" that lets me edit browser_title, meta_description, canonical_url, etc., and now I can add that field to any template and pages using that template have their own "SEO" tab. Sure you could do this before by creating a separate tab field with an "SEO" label, but now it's a lot simpler/faster, as any field can now display as a tab on its own. Like with the other visibility modes, also included are options to make the tab load dynamically on click with ajax or make the value non-editable. You can also optionally specify a label for the tab independently of the field label. This is because usually you want tab labels to be very short (1 word is best) whereas field labels have no such need. Please note that this new Tab option is exclusive to the page editor, and in order to work the field must not already be in a tab. Also added this week is a new $page->getMultiple() method. This method works the same as the $page->get() method except that it lets you retrieve multiple page property/field values at once and returns them in an array. For example: $a = $page->getMultiple([ 'foo', 'bar', 'baz' ]); list($foo, $bar, $baz) = $a; It also accepts a CSV string: $a = $page->getMultiple('foo,bar,baz'); By default it returns a regular PHP array, suitable for using in list() like the first example. But if you want it to return an associative array instead, then specify true as the 2nd argument: $a = $page->getMultiple('foo,bar,baz', true); echo $a['foo']; echo $a['bar']; echo $a['baz']; I find this method useful in reducing the amount of code/lines necessary in many cases, and most often I use it in combination with a PHP list() call, i.e. list($title,$subtitle,$body) = $page->getMultiple('title,subtitle,body'); That's all for this week. Thanks for reading and have a great weekend!
- 4 replies
-
- 27
-
I always start from scratch when it comes to creating the fields and templates, and basic site structure, but rarely when it comes to the pages/content. On previous conversions I've gone into WP’s database and exports to migrate content. This new case is a little bit unique in that WordPress couldn't do everything they needed, at least not in a manner that was affordable or reasonable to implement, from what I understand. While some of the content is in WP, other content is coming from authenticated web services. Lots of the pages have some content, custom fields and repeatable blocks in WP, but there are also custom fields with IDs for external services that it pulls other content from at runtime. They are paying a couple other companies quite a bit of money every month to host their content through these web services and make it searchable. They pull from those services at runtime and output it all from the WP templates. It strikes me as really inefficient, but it all works fine and you'd never know it from the front-end. But it's also expensive for them and fragile to have these kinds of external dependencies, not to mention a pain to have to manage content (even for the same page) across different services. This is all stuff that ProcessWire does well on its own, so none of those external services are needed on the new site. Since the front-end of the site is already compiling them all into single-page output, I built a scraper to pull it directly from the existing site and put it into the right places in ProcessWire. Scraping might seem like a last resort, but in this case it was the fastest, most direct way to pull everything out since the content is split among different databases and services. Basically using WP as an adaptor for the services it is pulling from. Luckily I can edit the WP templates to modify the markup (adding wrapping tags or custom html id attributes, etc.) where needed make the scraper relatively simple. @MarkE
-
We had a smooth rollout of the new main/master version 3.0.200 last week (read all about it here). If you haven't upgraded yet, consider doing so, this new version is a great upgrade. I'm going to add the 3.0.200 tag shortly after I finish this post, which should trigger other services (like packagist) to upgrade. I've had a new client project in the pipeline that I've been waiting to start till the new main/master version was out, so this week I started that project. Pete and I are working together on it, like we've worked on others before. It involves taking a popular WordPress site and rebuilding it completely in ProcessWire. I've done this a couple times before, but this time it's bigger and broader in scope. I always find the large site conversions to be great learning experiences, as well as great opportunities to show how ProcessWire can achieve many things relative to WordPress, in this case. At this stage, I'm having to spend a lot of time in WordPress just to get familiar with the content, fields, etc., as well as in the theme files (php and twig). The more time I spend in these, the more excited I get about moving it into ProcessWire. For this particular site, moving from WordPress into ProcessWire is going to result in a big boost in efficiency, maintainability, and performance. Part of that is just the nature of PW relative to the nature of WP. But part of it is also that the WP version of the site is kind of a disorganized patchwork of plugins, code files, and 3rd party services, all kind of duct taped together in an undeniably confused, undisciplined and fragile manner. (Though you'd never know it by looking at the front-end of the site, which is quite nice). This has been a common theme among WordPress sites I've dug into. Though to be fair I don't think that's necessarily the fault of WordPress itself. I always enjoy taking a hodgepodge and turning it into an efficient, performant and secure ProcessWire site. I love seeing the difference it makes to clients and their future by taking something perceived as a "necessary liability to run the business", and then turning it into the most trusted asset. I think the same is true for a lot of us here. We love to develop sites because it's an opportunity to make a big difference to our clients… and it's fun. Ironically, if past history is any indicator, I seem to get the most done on the core (and modules) when I'm actively developing a site. Needs just pop up a lot more. I don't know if that'll be the case this time or not, but I do expect to have weeks with lots of core updates and some weeks with no core updates, just depending on where we are in the project. This particular project has to launch phase 1 by sometime in July, which is kind of a tight schedule, and that may slow core updates temporarily, but who knows. I'll share more on this project and what we learn in this WP-to-PW conversion in the coming weeks. Thanks for reading and have a great weekend!
- 4 replies
-
- 25
-
Local copy of PW site loads home page but all other pages are missing
ryan replied to creativeguy's topic in General Support
@creativeguy Usually this behavior indicates that the site is missing an .htaccess file. Check your /Applications/MAMP/htdocs/giftfunds/ directory to make sure there's an .htaccess file in there. I'm guessing there isn't. If you don't have a copy of it, you can get it from here and then rename it to ".htaccess" (with the period in front). -
@bernhard Just tried to setup that situation and duplicate it but not seeing it. My best guess is that something is later overriding your $config->noHTTPS. But I would suggest not using $config->httpHost at all and instead changing it to: $config->httpHosts = [ 'localhost:8080', 'localhost' ]; …just in case the the port isn't getting communicated in the host name. You can also set your $config->noHTTPS to be conditional for the host names: $config->noHTTPS = [ 'localhost:8080', 'localhost' ]; Another thing to look at is to use your browser view-source to view those page editor view links to see if they really are https links in the source, or if something is changing them after the fact (or browser interpreting them as https due to a current or cached former .htaccess rule). Try manually setting $config->https = false; in your /site/config.php to see if that makes a difference. Do a bd($_SERVER['HTTP_HOST']) to see how the http host is communicated to PHP, just in case it's different from what you expect.
-
@olafgleba Edit your /site/config.php file and look for: $config->httpHost and/or $config->httpHosts. In most cases the $config->httpHost should not be present (since PW will set it automatically at runtime), but the $config->httpHosts (plural) should be present, and it should be an array of http hosts ProcessWire is allowed to auto-detect and use. It will pick one of the hosts in that array to use for the request, so long as it matches the $_SERVER[HTTP_HOST]. If it can't find one that matches the $_SERVER[HTTP_HOST] then it will simply use the first one in your httpHosts array: $config->httpHosts = [ 'www.company.com', 'dev.company.com', 'localhost:8888' ]; If it just contains one host then it'll always use that one, regardless of what's in $_SERVER[HTTP_HOSTS] … $config->httpHosts = [ 'www.company.com' ]; If you do have the non-plural $config->httpHost present (like below), then it forces use of that hostname, rather than letting PW auto-detect from your httpHosts array. Though if there's only one host in your httpHosts array (like above) then of course the effect would be the same. $config->httpHost = 'www.company.com'; My best guess is that you either have $config->httpHost or httpHosts set with '127.0.0.1', or that you have httpHost/httpHosts undefined in your /site/config.php file. To fix it you need to add your www.host.tld to a $config->httpHosts array and preferably as the first item in it. If you do see a $config->httpHost (non-plural) in your config.php them I would remove it.
-
Prevent 504 Gateway Time-out while importing large dataset
ryan replied to ttttim's topic in General Support
@ttttim In my experience a 504 gateway timeout comes prior to PHP max_execution_time and may not even be related to it (?). Which is to say, I think it's a server timeout setting that is probably specified somewhere in Apache or prior, rather than PHP. But my experience with 504s is mostly limited to this server we are typing on, and in the case of this server, I will get a 504 timeout on a long running process and the 504 comes from the load balancer rather than the actual node that is running the script. So when a 504 occurs here, it just breaks the connection with the browser but the script continues running till it finishes, interestingly. I have no idea if your case is similar, but one thing I can suggest in your script is that you call PHP's set_time_limit(600); within your long-running loop to reset PHP's timer on each iteration (600=10 minutes, or a different time if it suits you), otherwise you likely will be subject to PHP's max_execution_time as well. -
@JerryDi I don't think there's a right answer as to what would be the best structure for your case. It really comes down to your needs, which I don't have enough familiarity with this to give an ideal answer. You'll be able to build your search for championships by club, player, etc., either way, as PW will make that simple. You can have any kind of parent/child relationship for those championships such as… /years/year-num/championship-name/ /clubs/club-name/championship-name/ …and so on. But another route to take (and what I might recommend here) would be to have a /championships/ parent and have all the championships under there, perhaps auto-sorted by year. Each with fields for "year", "club", "players", etc. /championships/championship-name/ And perhaps fields like "club" and "players" on each championship page would be page references to other pages setup in a similar structure: /clubs/club-name/ /players/player-name/ …and so on. Taking this route (where all championships share a common parent) may not be perfect for any one need, but will likely be a good fit for a diverse range of needs, a solid choice overall. So without knowing the details of your needs that's the route I'd probably take.
-
@Manaus This is a case where you should do a 302 redirect $session->location($article->url); rather than render different articles at the same URL. Otherwise folks won't be able to accurately bookmark it, and it'll confuse search engines, so it'd be an SEO problem. But I'll assume you know that and you want to do it anyway. What Zeka mentioned above is a good way to go, and probably the one I'd choose. But here's also another option below. It assumes you have a "latest-article" template and it is used by the page /latest-article/. /site/templates/latest-article.php $article = $pages->findOne("parent=/articles, sort=-created"); if(!$article->id) wire404(); echo $article->render(); If you are using prepend/append template files, then you'd also want to check the box to disable them in Setup > Templates > latest-article > Files (tab).
-
@Robin S @bernhard @teppo I would say it's less important to limit how many fields you create than it was before, but it's still worthwhile to make efficient use of fields for other reasons (at least for the way I work). But the technical reasons for doing so appear to be much less than before. If you look here at the performance numbers on a system with 1000 fields you can see that prior to lazy loading, it took 0.5167 ms (half second) to load the fields. This might sound reasonable, but note the fields were created with an automated script and not as diverse as they would be in a real environment, so I'd expect the numbers to be higher/slower in real life. After lazy loading that part of the boot process was reduce to 0.0062 ms. Even without lazy loading enabled, the field loading was reduced to 0.0322 ms (in that 1000 field environment) just due to the other optimizations made in support of lazy loading. So there's some question as to whether lazy loading is even necessary anymore now that everything else more optimized. But using less time and energy is always good thing, even if not felt on individual requests, small differences add up to significant numbers over thousands of requests. The way lazy loading works is that everything in the database table (for fields, templates, fieldgroups) still gets selected and loaded, but into an array rather than into objects. That data doesn't get loaded into and converted to actual Field (or Template, Fieldgroup) objects until that specific object is requested. TheTuningSpoon found that overhead came from actually creating and loading the objects rather than selecting them from the DB, and I was able to confirm that. But it also means that the more fields you have, the more memory they will consume. But the memory consumed by the raw field data is very little and I think you'd have to have many thousands of them for it to even be a consideration. Lazy loading fields don't change the way I use fields, at least. I like to have as few fields as necessary just because it's easier for me to keep track of, fewer things to remember when writing code, fewer things to manage during changes, etc. But if your workflow is like the one Bernhard describes above then that seems like a case where more fields would appear to be a benefit.
- 12 replies
-
- 13