Leaderboard
Popular Content
Showing content with the highest reputation on 08/01/2022 in all areas
-
@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"); } });4 points
-
I feel bad bumping this topic, but I think some people might've missed, or misunderstood the importance of the reasoning for looking towards moving away from CKE4. The codebase of CKE version 4 will essentially be abandoned as of sometime in 2023 by the organization that maintains it. Considering the large amount of security issues that are consistently discovered in heavily used input/output handling JavaScript packages (whether due to custom add-ons or its core), and the fact that Ryan strives for a very secure system... The need to provide an alternative WYSIWYG module is evident. CKE5 seems like the least intrusive option (for users/clients). It appears as though he hopes to make it fairly seamless for site administrators and developers, but the integration changes aren't a 1:1 swap, so he'll have quite a bit to consider. It's likely that you'd still be able to use CKE4 as a module, just like you could continue using TinyMCE when it was removed from core -- as a separately installable module. If you wish to use standard textarea fields or Markdown-powered fields, there are add-ons for those too (already). You will still have choice, and that's one of the excellent things about ProcessWire. The only issue here is that Ryan wants to make sure that you and your clients have a stable, secure, and usable system. As always, you will still have control over your own system(s). ♥2 points
-
I don't suppose there's any chance of getting these juicy forum board solutions added to related module documentation as Examples, could we? These are great starting points to common scenarios! The downside would be maintaining them for compatibility, but perhaps explicit notices with dates that the examples were created? So often I end up searching the website for answers to questions, but they're all over the place - the blog, documentation, api, forum (and not always the module forum). ? Just a thought. That said, loving these examples you're providing from actual client work you're doing. Thanks, Ryan!2 points
-
You don't know what to use when because ProcessWire does some magic here to make both versions work most of the time. For example the method to get children of the current page is $page->children() (https://processwire.com/api/ref/page/children/) but $page->children will work as well. ProcessWire does that using magic methods (https://www.php.net/manual/en/language.oop5.magic.php) so when you request $page->children it will check if a method "children" exists and will call that method for you even though you are accessing it as a property and not as a method. So requesting the method directly might be slightly more efficient, but in most cases I guess it will not matter. And imho using the "property syntax" like $this->wire->files->render() is cleaner than always using methods. $this->wire()->files()->render()...2 points
-
And a few macros as well: <?php use Latte\Compiler; use Latte\MacroNode; use Latte\Macros\MacroSet; use Latte\PhpWriter; /** * Latte macro provider * */ final class Macros extends MacroSet { /** * Install available macros. */ public static function install(Compiler $compiler): void { $me = new static($compiler); $me->addMacro('ifispage', [$me, 'macroIsPage'], '}'); $me->addMacro('icon', [$me, 'macroSvgIcon']); $me->addMacro('trim', '', [$me, 'macroTrimEnd'], null); $me->addMacro('minify', [$me, 'macroMinifyHtml'], [$me, 'macroMinifyHtmlEnd'], null, self::ALLOWED_IN_HEAD); } /** * {ifispage $page} */ public function macroIsPage(MacroNode $node, PhpWriter $writer) { return $writer->write(' $isObj = %node.word && is_object(%node.word); $isCustomPage = $isObj && is_a(%node.word, "ProcessWire\DefaultPage"); $isCorePage = $isObj && get_class(%node.word) === "ProcessWire\Page"; $isPage = ($isCustomPage || $isCorePage) && %node.word->id; if ($isPage) { '); } /** * {icon 'search'} */ public function macroSvgIcon(MacroNode $node, PhpWriter $writer) { return $writer->write(' $icon = %node.word; $ratio = "xMinYMid"; $ratioAttr = $ratio ? "preserveAspectRatio=\"{$ratio}\"" : ""; $html = "<svg class=\'icon icon-{$icon}\' {$ratioAttr} aria-hidden=\'true\'><use xlink:href=\'#icon-{$icon}\' /></svg>"; echo $html; '); } /** * n:trim */ public function macroTrimEnd(MacroNode $node, PhpWriter $writer) { $node->validate(false); $node->openingCode = "<?php ob_start(); ?>"; $node->closingCode = '<?php $__output = ob_get_clean(); ?>'; if ($node->prefix === MacroNode::PREFIX_INNER) { // Simple case: n:inner-trim -> just trim content $node->closingCode .= '<?php $__output = trim($__output); ?>'; } else { // Trickier case: n:trim // remove whitespace *inside* outer tags // while preserving whitespace *outside* of outer tags // Before: " <h1> Title </h1> " // After: " <h1>Title</h1> " $node->closingCode = $node->closingCode . // Remove whitespace after opening tag '<?php $__output = preg_replace(\'~^(\s*<[^>]+>)\s+~s\', "\$1", $__output); ?>' . // Remove whitespace before closing tag '<?php $__output = preg_replace(\'~\s+(</[^>]+>\s*)$~s\', "\$1", $__output); ?>'; } $node->closingCode .= '<?php echo $__output; ?>'; } /** * {minify} */ public function macroMinifyHtml(MacroNode $node, PhpWriter $writer) { return $writer->write(' ob_start(function ($s, $phase) { // 0: replace newlines //$html = preg_replace("/\r\n|\r|\n/", "", $s); // 1: remove whitespace from between tags that are not on the same line. $html=preg_replace(\'~>\s*\n\s*<~\', \'><\', $s); // 2: replace all repeated whitespace with a single space. static $strip = true; $html=LR\Filters::spacelessHtml($html, $phase, $strip); return $html; }, 4096); '); } /** * {/minify} */ public function macroMinifyHtmlEnd(MacroNode $node, PhpWriter $writer) { return $writer->write(' ob_end_flush(); '); } }2 points
-
Sure! Here's a few that might be of use to ProcessWire sites: <?php use Latte\Engine; /** * Latte filter provider * */ final class Filters { /** * Install available filters. */ public static function install(Engine $latte) { foreach ((new static())->provide() as $name => $callback) { $latte->addFilter($name, $callback); } } /** * @return array<string, callable> */ public function provide(): array { return [ // Sanitize values using ProcessWire's sanitizer API variable 'sanitize' => function ($value, $sanitizer, $options = null) { if (!$options) { return sanitizer()->$sanitizer($value); } else { $args = func_get_args(); unset($args[1]); // remove $sanitizer arg return call_user_func_array([sanitizer(), $sanitizer], array_values($args)); } }, // Render FormBuilder form, but allow prepending and appending fields 'form' => function ($form, $options = []) { if (!$form) { return ''; } $output = $form->render(); if ($options['append'] ?? false) { $output = str_ireplace('</form>', $options['append'], $output); } if ($options['prepend'] ?? false) { if (stripos($output, '<form') !== false) { $output = preg_replace('#(<form[^>]*>)#i', '\\1' . $options['prepend'], $output); } else { $output = $output . $options['prepend']; } } return $output; }, // URL slug / page name 'slug' => function ($str, $length = 128) { $str = sanitizer()->unentities($str); $str = sanitizer()->pageNameTranslate($str, $length); return $str; }, // Truncate string 'truncate' => function ($str, $length = 200) { return sanitizer()->truncate($str, $length, ['visible' => true]); }, // Render Markdown 'markdown' => function ($str) { $str = "{$str}"; modules()->get("TextformatterMarkdownExtra")->formatValue(new Page(), new Field(), $str); return $str; }, // Unwrap ProcessWire value objects 'value' => function ($object) { if (is_object($object)) { if ($object instanceof SelectableOptionArray) { return $object->implode(' ', 'value'); } else { return $object->value ?? ''; } } elseif (is_array($object)) { return implode(' ', $object); } else { return (string) $object; } }, // Join a string with a custom separator at the end 'join' => function ($list, $separator = ', ', $lastSeparator = ' & ') { if (count($list) > 1) { $last = array_pop($list); return implode($separator, $list) . $lastSeparator . $last; } else { return implode($separator, $list); } }, // Prettify URL by removing protocol and www 'prettyUrl' => function ($url, $options) { if (null === $url) { return false; } $url = trim($url); if ($options['www'] ?? true) { $url = str_replace('www.', '', $url); } if ($options['http'] ?? true) { $url = str_replace(array('https://', 'http://'), '', $url); } if ($options['slash'] ?? true) { $url = rtrim($url, '/'); } return $url; }, ]; } }2 points
-
Also note that modern IDEs do that automatically for you once you start writing "new Client" in your code ?2 points
-
Find Merge Adds a Pages::findMerge() method that allows multiple PageFinder selectors to be merged into an efficient paginated set of results. This can be useful when you need more sophisticated sorting of results than what would be possible using only the sort value in a single $pages->find(). Details $results = $pages->findMerge($selectors, $options); $selectors is required and must be an array of selectors. Each selector can be in string format or array format. The findMerge() method will loop over the selectors in the order supplied, adding matching pages to the final results. $options is an optional associative array of options. limit (int) Limit for pagination. start (int) Manually override the start value rather than have it be automatically calculated from the current page number. excludeExisting (bool) Whether or not to exclude pages in each selector that have already been matched by a previous selector. Default is true. keepFirst (bool) When excludeExisting is false then a page might match more than one selector in the supplied array. But each page can only appear once in the results and if keepFirst is true then the page will appear in its earliest position in the results, whereas if keepFirst is false it will appear in its latest position in the results. Default is true. As a shortcut you can supply an integer as the second argument and it will be treated as the limit for pagination. Basic usage For most use cases only a limit will be needed for the $options argument. $selectors = [ 'title%=yellow', // title contains "yellow" 'title^=z', // title starts with "z" 'title=elephant', // title equals "elephant" 'template=colour, sort=-title, limit=3', // 3 colours in reverse alphabetical order 'template=country, sort=title, limit=40', // 40 countries in alphabetical order ]; $results = $pages->findMerge($selectors, 10); if($results->count) { echo "<p>Showing results {$results->getPaginationString()}</p>"; echo "<ul>"; foreach($results as $result) { echo "<li><a href='$result->url'>$result->title</a></li>"; } echo "</ul>"; echo $results->renderPager(); } Advanced usage The following notes are only relevant to rare cases and most users can safely skip this section. In the demo example the colour page Yellow will potentially match both the 1st selector and the 4th selector. Because of this the excludeExisting and keepFirst options will have an effect on the results. excludeExisting option false Note that the 4th selector asks for 3 colour pages (limit=3). By default excludeExisting is true, which means that when the 4th selector is processed it is interpreted as saying "find 3 colour pages in reverse alphabetical order that have not already been matched in an earlier selector". We can see that there are 3 pages in the results from that selector: Violet, Red, Orange. But if excludeExisting is set to false then the results are different. The matches of the 1st selector (Yellow, Yellow Warbler) are not excluded from consideration by the 4th selector (the 4th selector matches will be Yellow, Violet, Red), and because each page can only appear once in the results this means that the 4th selector ends up only adding 2 more pages to the results. $selectors = [ 'title%=yellow', // title contains "yellow" 'title^=z', // title starts with "z" 'title=elephant', // title equals "elephant" 'template=colour, sort=-title, limit=3', // 3 colours in reverse alphabetical order 'template=country, sort=title, limit=40', // 40 countries in alphabetical order ]; $options = [ 'limit' => 10, 'excludeExisting' => false, ]; $results = $pages->findMerge($selectors, $options); keepFirst option false As described above, the Yellow page potentially matches both the 1st and 4th selector. By default Yellow will appear in its earliest position within the results, i.e. the position resulting from it being matched by the 1st selector. But if keepFirst is set to false (and excludeExisting is false) then it will appear in its latest position within the results, i.e. the position resulting from it being matched by the 4th selector. $selectors = [ 'title%=yellow', // title contains "yellow" 'title^=z', // title starts with "z" 'title=elephant', // title equals "elephant" 'template=colour, sort=-title, limit=3', // 3 colours in reverse alphabetical order 'template=country, sort=title, limit=40', // 40 countries in alphabetical order ]; $options = [ 'limit' => 10, 'excludeExisting' => false, 'keepFirst' => false, ]; $results = $pages->findMerge($selectors, $options); keepFirst has no effect when excludeExisting is true. https://github.com/Toutouwai/FindMerge https://processwire.com/modules/find-merge/2 points
-
I don't know when I last found a tool for my daily work that had such a huge impact as Latte. The last one was ProcessWire I guess (which had an even greater impact of course, but still). Latte uses PHP syntax so you don't have to learn a new language! It plays extremely well with the PW API and it makes my markup files so much cleaner and easier to read and maintain I can't tell you. From time to time I stumble over situations where I'm once more impressed by latte, so this thread is intended to collect small latte snippets to show how one can use it. If you want use Latte for your ProcessWire projects you can either install it via composer or you wait some more days until I release RockFrontend - a frontend module that can render latte files and comes with several other nice frontend helpers ? ----- Breadcrumbs the Latte+PW way: <ul> <li n:foreach="$page->parents()->add($page) as $item"> <a href="{$item->url}" n:tag-if="$item->id != $page->id"> {$item->title} </a> </li> </ul> The cool thing here is that the last item (the page we are currently viewing) is NOT linked. The <a> tag is only rendered for all other items. Still the label of the link is rendered, because we are using n:tag-if and not n:if ----- Output an image, but only if it exists: <img n:if="{$page->yourimage}" src="{$page->yourimage->maxSize(400,300)->url}" alt="..."> Note that the ->maxSize() call will NOT throw an error even if you didn't upload an image! This is one of my favourite features of latte: Write the markup once and then add simple instructions directly within that tag. No more if/else/foreach chaos ?1 point
-
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!1 point
-
https://methodsanalytics.co.uk/ What makes this project cool: We integrated an impossible geometry concept using 3D, Three.JS - Shown in main concept and used for Error 404 concept. We allow the client to create impossible geomerty in the ProcessWire CMS. We created the 3D editor which loads in with a process module. Custom front-end design and UI and full content management across evey aspect of the site. Modular system for page content providing maximum flexibility on page construction. Health check feature looking for broken links and lorem ipsum, page scanning tool. SEO module with global editing section across all pages. Global shared content modules. Methods-Analytics.mp41 point
-
I'm going to play with Laragon and start using VSCode and see how I do. Thank you. Once I get comfortable, maybe I'll start working with ddev.1 point
-
Usually methods do something and properties hold a value. So $pages->count() would be a method whereas $page->id would access the id property of the page object...1 point
-
@wbmnfktr I thought about putting this post on processwire.dev, but it somehow feels like it doesn't belong there. Not sure why – maybe it doesn't provide as much a benefit as (I feel) the other tutorials there do. I've had this topic on my mind because I've been repeatedly reading that Twig doesn't do anything that you can't do in plain PHP. I obviously disagree and needed to get this out of my head and onto a page so I can link to it when it comes up ? But if you don't need convincing to use Twig, the post doesn't provide anything the other two tutorials don't already cover. Maybe I'll integrate this post in processwire.dev at some point …1 point
-
Thank you DaveP! // Otherwise get images of their book covers else: $source = new WireArray(); foreach($page->page_ref_for_their_books as $a_page): $source->add($a_page->images->first); endforeach; endif; echo $source->first->size(100,100)->url;1 point
-
Yep, but, PW gives you methods for working with WireArrays - https://processwire.com/api/ref/functions/wire-array/. This way you can use ->first() or any of the usual methods. You'll need to refactor your code a bit but it might well be worth it.1 point
-
@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.1 point
-
Actually this is a really great starting-point for Twig in ProcessWire. While I still stay with plain PHP in my projects I really like the Twig syntax due to Nunjucks/Liquid in 11ty. Maybe you could update your processwire.dev with this point - as this is a thing some people really look for. Maybe even @bernhard could write a guest-post there with something about latte in plain-vanilla-pw and when it's coming RockFrontend. For myself so far I decided to look more into this after recent projects are done. Especially twig (as I know already the syntax).1 point
-
I kbow it has not directly to do with the topic but thanks to your code examples I finally stumbled upon a solution to include php files with arguments! Like this: <? wireRenderFile('inc/project-data.php', ['data' => $page->project_data]); ?> I've been using Processwire for years and always looked for a method like that. Crazy... I used PHP functions in that case - until now. It's time to update my projects ?1 point
-
1 point
-
Latte is great indeed! I've been using it for all projects since discovering it. Everything is so much more concise and elegant, especially once your start building your own filters and macros. Quick note that you can also use Latte with Wanze's TemplateEngineFactory. I've found that the easiest way to get started; one composer install and you're ready to go ?1 point
-
Just a little rant: Please don't turn our textarea/RTF-fields into something like Gutenberg or similar. As a module, ok. But please... not as the default option. I'm not that deep into all the details about those RTF-Editor tools/scripts but what's so bad about staying with CKEditor 4.x for a while as it's stable and mature enough. Never had any issues with it.1 point
-
I know you can throw a 404 easily enough: throw new \ProcessWire\Wire404Exception(); However, is there an easy way to throw other HTTP status codes that sends the appropriate HTTP header and loads the error page rather than the current template? For example, I have some pages where I want to restrict the request method to GET or POST only and therefore want to sent a 405 Method Not Allowed if the wrong method is requested. I don't just want to manually send the headers and exit() though — I want the error page to show and for ProcessWire to finish the request. Is this easy to do? I looked but couldn't find anything. Thanks.1 point
-
It sounds like it might be a namespace issue. Check that your template files and included files start with: <?php namespace ProcessWire; FileCompiler tries to automatically fix files that don't declare the PW namespace but I think the most reliable approach is to always add it yourself.1 point