Jump to content

MoritzLost

Members
  • Posts

    364
  • Joined

  • Last visited

  • Days Won

    18

Everything posted by MoritzLost

  1. Thanks adrian! Yeah, I kinda overlooked the author field the first time, my bad ^^ Curious issue ... anyway, thanks for your help!
  2. Not sure where to post this, but I have a problem with the modules directory. I wanted to update the information for my module, but my changes won't appear. I thought they were in a manual review queue, but it's been a couple of weeks and nothing happened. Namely, I wanted to change those things in my Textformatter module: The stability from beta to stable. Add the category SEO/Accessibility. Anyone know why I can't update the information? Who is responsible for managing the modules directory, anyway? Only ryan or someone else as well? Would be nice if someone with access to that could take a look ? (Also, every time I go to the edit page, the "Author's Forum Name" field is empty, what's up with that? ^^)
  3. It doesn't work because your wildcard SSL certificate in the format "*.example.com" only matches one "level" of subdomain, so it's not valid for www.subdomain.example.com, see https://serverfault.com/questions/104160/wildcard-ssl-certificate-for-second-level-subdomain The redirect directives look good to me, Apache doesn't care about how many subdomains you're matching in the RewriteCond. But the browser cares about the invalid SSL certificate. The redirection won't work in this case, because the browser won't accept the SSL certificate, as it's not valid for www.subdomain.example.com. Since the SSL handshake and certificate validation takes places before the redirect headers are followed, the browser will show the security warning before doing anything else. I'd wager that if you manually add a security exception, the redirection will work. If you want to redirect https traffic to another https URL, you need a valid certificate for both the domain originally requested by the client and the target of the redirection, or the browser will show the security warning before or after the redirect, so your solution with the additional SSL certificate is the only one that can work. Why do you even need to support the second www subdomain? Sounds like one of "those" client requests ?
  4. Thanks for this @LostKobrakai! I just came across this very problem, thankfully I found this thread. Took me a while to understand how your solution works, certainly a creative solution to just add a string to make the cached value invalid JSON ? I modified your code slightly: const RAW_PREFIX = '::RAW::'; $wire->addHook('WireCache::saveRaw', function (HookEvent $event) { $args = $event->arguments(); $args[1] = RAW_PREFIX . $args[1]; return $event->return = call_user_func_array([$event->object, 'save'], $args); }); $wire->addHook('WireCache::getRaw', function (HookEvent $event) { $args = $event->arguments(); $cached_val = call_user_func_array([$event->object, 'get'], $args); return $event->return = $cached_val === null ? null : substr($cached_val, strlen(RAW_PREFIX)); }); I made two notable changes: If the Cache API returns null (i.e. no cached value exists), getRaw will also return null instead of an empty string. Use substr instead of str_replace, since it will be faster for long strings and it won't break anything in case the RAW_PREFIX (::HOOK::) appears anywhere inside the cached value (I'm a bit paranoid ? )
  5. I just pushed version 2.1.0 to Github & the modules directory! Includes a minimum length settings for linkable titles, a preference to link the oldest or most recent page if multiple pages have the same title, and an important bugfix for older MySQL versions. Check out the full changelog above ?
  6. Hey mdp, regarding your questions: The code you pasted will be called whenever any select element (or other input element, for that matter) with the class sort-select. This part is the key: $('.sort-select') That's a jQuery selector, the dot symbolizes a class selector (it mimics CSS selectors). If you want to do the same thing for an element with an id, use a pound sign instead instead: // select the element with id 'sort-select' $('#sort-select') // select any element that has either the class or the id 'sort-select' $('.sort-select, #sort-select') A cleaner, native way to handle this are GET-parameters in the url. First, you need to include a submit button in the form: <form id="sort-select" method="get"> <select name="sort"> <option value="-likes">Sort by: # Recommendations</option> <option selected value="title">Sort by: Title (A-Z)</option> <option value="-title">Sort by: Title (Z-A)</option> <!-- ... --> </select> <input type="submit" value="Submit"> </form> Just a side-note, your HTML attribute values should be encased by quotes. Also, the options need closing tags to be valid HTML5. I also removed the onchange attribute, though you can keep it in if you want to automatically trigger the submit. This is actually all that is needed! Since the form doesn't have an action attribute, it will go to the same page the form is on. The selected sort-option will be included in the targeted URL as a GET-parameter, where it can be picked up by PHP to generate the results listing. The onchange-attribute does the same thing, it basically auto-submits the form whenever you change the value of the select input (the code in the skyscraper.js file does the same thing, by the way). However, by including the submit-button, you make the entire form more accessible and ensure it will even work if JavaScript is disabled.
  7. Changelog The changelog can now be found in the module directory itself, see CHANGELOG.md.
  8. @horst That looks interesting as well! Of course with pages it's easier to maintain, and clients can their own options. I usually only use pages for larger structures such as taxonomies, and Selectable Options for simpler display options, since it's a bit faster to set up. Maybe I'll try a "pure" setup with only pages for options next time ^^ Hm, this opens up some possibilities, something like creating setting groups or presets containing multiple other options .. I'll definitely play around with that one ?
  9. I don't think there's an inbuilt method for that, but you can quickly built your own! For the excerpt, you just need to find the first occurance of the search term inside the field you're searching, then display the text around that. A quick example: // get the search term from the GET-parameter and sanitize it $term = $sanitizer->text($input->get->q); // how many characters to include before and after the search term $include_around = 50; foreach ($pages->find('body~={$term}') as $result) { echo '<h2>' . $result->title . '</h2>'; // start and end positions of the search result in the body field $term_start = mb_strpos($result->body, $term); $term_end = $term_start + mb_strlen($term); // where to start and end the output, and additional // checks to make sure it's not out of bounds $offset_start = $term_start - $include_around; if ($offset_start < 0) $offset_start = 0; $offset_end = $term_end + $include_around; if ($offset_end > mb_strlen($result->body)) $offset_end = mb_strlen($result->body); $output = mb_substr($result->body, $offset_start, $term_start); $output .= '<mark>' . $result . '</mark>'; $output .= mb_substr($result->body, $term_end, $offset_end); echo '<p>&hellip; ' . $output . ' &hellip;</p>'; } I don't have a ProcessWire installation to test this right now, so might be some errors in there, but you get the idea ^^ Basically, find the term inside the field you're searching, then include some characters before and after it for context, and wrap the search term in <mark> tags for highlighting (you can also add a CSS class to target the search result for styling). You can of course refine this as much as you want, for example you can check for full stops to include complete sentences; you'll also want to make sure to find matches in the title as well, and in this case maybe only display the first X characters of the body ...
  10. Thanks ^^ I enjoy writing tutorials, I find it helps to consolidate the experience from my projects into organized knowledge. Most of the time I come across some areas where I'm not sure how something works, so I can look it up and further my own understanding (it's also great to improve my English as a non-native speaker). I think writing this stuff down helps bring out the core ideas or the important takeaways; before I started writing this one, I hadn't really considered the part about semantic/intuitive options, I mostly realized during writing that this was the crux of the matter. So yeah, I write for my own benefit as much as everyone else's ?
  11. This will be more of a quick tip, and maybe obvious to many of you, but it's a technique I found very useful when building display options. By display options I mean fields that control how parts of the page are displayed on the frontend, for example background colors, sizing, spacing and alignment of certain elements. I'll also touch on how to make those options intuitive and comfortable to use for clients. It basically involves setting up option values that can be used directly in CSS classes or as HTML elements and mapping those to CSS styling (which can be quickly generated in a couple of lines using a pre-processor such as SASS). Another important aspect is to keep the option values seperate from their corresponding labels; the former can be technical, the latter should be semantically meaningful. The field type that lends itself to this this seperation of concerns is the Selectable Options field, the following examples mostly use this field type. Note that this module is part of the ProcessWire core, but not installed by default. The following examples all come from real projects I built (though some are slightly modified to better demonstrate the principle). #1: Headline levels & semantics For a project that had many pages with long texts, I used a Repeater field to represent sections of text. Each section has a headline. Those sections may have a hierarchical order, so I used a Selectable Option field to allow setting the headline level for each section (you can guess where this is going). The definition of the options looks something like this (those are all in the format value|label, with each line representing one option, see the blogpost above for details): h2|Section headline h3|Sub-section headline Of course, the PHP code that generates the corresponding HTML can use those values : // "sections" is the repeater field foreach ($page->sections as $section) { // create an h2 / h3 tag depending on the selected option (called headline_level here) echo "<{$section->headline_level->value}>{$section->headline}</{$section->headline_level->value}>"; echo $section->body; } That's a pretty obvious example, but there are two important takeaways: I only used two options. Just because there are six levels of headlines in HTML, doesn't mean those are all relevant to the client. The less options there are, the easier it is to understand them, so only the options that are relevant should be provided. In this case, the client had provided detailed, structured documents containing his articles, so I could determine how many levels of hierarchy were actually needed. I also started at h2, since there should be only one h1 per page, so that became it's own field separate from the repeater. The two options have a label that is semantically relevant to the client. It's much easier for a client who doesn't know anything about HTML to understand the options "Section headline" and "Sub-section headline" than "h2" and "h3". Sure, it can be cleared up in the field description, but this way it goes from something that's quickly explained to something that needs no explanation at all. #2: Image width and SASS In the same project, there was also an image section; in our layout, some images spanned the entire width of the text body, others only half of it. So again, I created an options field: 50|Half width 100|Full width In this case, I expected the client to request different sizes at some point, so I wanted it to be extensible. Of course, the values could be used to generate inline styles, but that's not a very clean solution (since inline styled break the cascade, and it's not semantic as HTML should be). Instead, I used it to create a class (admittedly, this isn't strictly semantic as well): <img src="..." class="w-<?= $section->image_width->value ?>"> With pure CSS, the amount of code needed to write out those class definitions will increase linearly with the number of options. In SASS however, you only need a couple of lines: @each $width in (50, 100) { .w-#{$width}{ max-width: percentage($width/100); } } This way, if you ever need to add other options like 25% or 75%, you only need to add those numbers to the list in parenthesis and you're done. You can even put the definition of the list in a variable that's defined in a central variables.scss file. Something like this also exists in Bootstrap 4 as a utility, by the way. It also becomes easier to modifiy those all at once. For example, if you decide all images should be full-width on mobile, you only need to add that once, no need to throw around !important's or modify multiple CSS definitions (this is also where the inline styles approach would break down) : # _variables.scss $image-widths: (25, 50, 75, 100); $breakpoint-mobile: 576px; # _images.scss @import "variables"; @each $width in $image-widths { .w-#{$width}{ max-width: percentage($width/100); @media (max-width: $breakpoint-mobile) { max-width: 100%; } } } One important gotcha: It might be tempting to just use an integer field with allowed values between 10 - 100. In fact, the amount of SASS code would be identical with a @for-directive to loop throuh the numbers. But that's exactly what makes point-and-click page builders so terrible for clients: too many options. No client wants to manually set numerical values for size, position and margins for each and every element (looking at you, Visual Composer). In fact, having too many options makes it much harder to create a consistent layout. So in those cases, less is more. #3: Multiple options in one field Another example for repeatable page sections, this time for a two-column layout. The design included multiple variants regarding column-span and alignment. Using a 12-column grid, we needed a 6-6 split, a centered 5-5 split, a left-aligned 6-4 split and a right-aligned 4-6 split. I didn't want to litter the repeater items with options, so I decided to put both settings in one field (called something like Column layout) : center_6_6|6 / 6 (Centered) center_5_5|5 / 5 (Centered) left_6_4|6 / 4 (Left-aligned) right_4_6|4 / 6 (Right-aligned) As long as the value format is consistent, the individual options can be quickly extracted and applied in PHP: [$alignment, $width['left'], $width['right']] = explode('_', $section->column_layout->value); echo '<section class="row justify-content-' . $alignment . '">'; foreach (['left', 'right'] as $side) { echo '<div class="col-lg-' . $width[$side] . '">'; echo $section->get("body_{$side}"); echo '</div>'; } echo '</section>'; If you don't recognize the syntax in the first line, it's symmetric array destructuring, introduced in PHP 7.1. For older versions you can use list() instead. This example uses Bootstrap 4 grid classes and flexbox utility classes for alignment. The corresponding CSS can be quickly generated in SASS as well, check the Bootstrap source code for a pointer. Again, I'm limiting the options to what is actually needed, while keeping it extensible. With this format, I can easily add other column layouts without having to touch the code at all. #4: Sorting page elements A final example. In this case I was working on a template for reference projects that had three main content sections in the frontend: A project description, an image gallery and embedded videos (each using their own set of fields). The client requested an option to change the order in which those sections appeared on the page. Had I known this earlier, I maybe would have gone for a Repeater Matrix approach once again, but that would have required restructuring all the fields (and the corresponding code), so instead I used a Selectable Option field (labelled "Display order"). My approach is similar to the one from the last example: body_gallery_embeds|Description - Gallery - Videos body_embeds_gallery|Description - Videos - Gallery gallery_body_embeds|Gallery - Description - Videos gallery_embeds_body|Gallery - Videos - Description embeds_body_gallery|Videos - Description - Gallery embeds_gallery_body|Videos - Gallery - Description Since there are six possibilities to sort three items, this is the expected number of options. So I decided to include them all, even though some are probably never going to be used. I also tried to use a predictable order for the options (i.e. the options come in pairs, depending on what element is first). And here is the code used on the frontend: // render the template files for each section and store the result in an associative array $contents = [ 'body' => wireRenderFile('partials/_section-body.php', $page), 'gallery' => wireRenderFile('partials/_section-gallery.php', $page), 'embeds' => wireRenderFile('partials/_section-embeds.php', $page), ]; // e.g. 'gallery_body_embeds' => ['gallery', 'body', 'embeds']; $order = explode('_', $page->display_order->value); // echo the contents in the order defined by the option value foreach ($order as $item) { echo $contents[$item]; } You can see how it will be easy to add an additional section and integrate it into the existing solution. Though a fourth item would result in 4! = 24 possibilities to sort them, so at that point I'd talk to my client about which layouts they actually need ? Conclusion I always try to keep my code and the interfaces I create with ProcessWire extensible and intuitive. Those are a couple of solutions I came up with for projects at work. They are certainly not the only approach, and there is nothing super special about those examples, but I found that putting a little more effort into defining options with meaningful labels and using option values that I can use directly in my templates makes the result less verbose and more maintainable. Some or most of this tutorial may be immediately obvious to you, but if you made it this far, hopefully you got something out of it ? Feel free to share your own methods to create display options, or how you would've approached those problems differently. Thanks for reading!
  12. @ryan Congrats on the launch! One more thing I noticed, in the "ProcessWire Pro Shop" section on the homepage, the module tiles use some JavaScript magic to figure out if a tile was clicked and navigate to that page. That's a bit annoying, since I can't use CMD-click or middle-mouse-click to open the link in a new tab (I like to open multiple links in new tabs and then read through them in turn). This could easily be improved by just wrapping the entire card inside the anchor-tag, then the JavaScript isn't needed at all. This would also help accesibility.
  13. For the record, I also see heavy compression artifacts on those screenshots. Normal full HD monitor here. How about using some responsive images for those screenshots and the iMac frame? Then you can provide multiple PNGs and each browser/device only needs to download the size it actually requires. Also makes it easy to provide high-resolution variants for retina displays. Shouldn't be a problem to include a srcset in the function that loops over the individual screenshots and shows/hides them.
  14. Hey @ryan, looks great! But I agree with @gmclelland and @teppo, the site has major accessibility problems; the thing I noticed immediately was too little contrast in the areas with blue background. Only the pure white headlines pass AA level, AAA fails completely (see Screenshot). The ProcessWire blue is unfortunate as a background color, as there can't really be enough contrast to white copy on it, and black copy just looks bad and doesn't fit the theme. Obviously it would be some work to change that color at this stage, but the homepage of a CMS that aims to be inclusive really shouldn't have such accessibility issues itself. Maybe remove the background and use the blue as an accent color for buttons, outlines et c. instead? Or use a much darker shade for the background sections, something like @3fingers suggestion here. Another thing, the main content on the documentation pages is a bit too wide for my taste, longer texts are harder too read since the lines are too long. Currently the #content-body sits at about 70rem, I'd reduce that to maybe 45~50 rem and center it between the sidebars. See screenshots, feels much easier on the eyes to me.
  15. @bernhard @LostKobrakai Just adding my two cents here ... I think if we're talking about writing site-specific modules or code, both options are equally valid and it comes down to preference. I try to write my site-specific functionality in general classes (not extending the Module class) and wire them up in my templates; in this case, it's just less work to connect the namespace through composer than to set up a module and add the namespace manually through the classloader. But really, for code that won't be used anywhere else it doesn't really matter. For reusable code or shareable modules I guess it depends on how closely it integrates with ProcessWire; i.e. my Textformatter module is built as a Processwire module, since it can't be used outside of Processwire anyway. But I'm also working on a collection of utility classes for my personal workflows (based on my markup generation tutorials), and most of those could be used in another context, so in this case I'm building it as a normal composer package, not as a ProcessWire module. One usecase for the Composer autoloader for me is this module: It's not available on any public repository or the module directory, but I can still manually clone it into my project from my private repo and just connect the namespace through Composer. In this case it would definitely be more work to write a wrapper module around it, so for that it's a useful technique. Thanks @horst, no idea why that never crossed my mind ? It's much better indeed, I've edited the tutorial accordingly! I don't think that will work in this specific setup though, since the custom installer mentioned in that blogpost just returns "site/modules/..." as the install path, which isn't correct if your composer.json lives one directory above. Though it would be trivial to fork it and just add the public/ part there ...
  16. @LostKobrakai Thanks for the clarifications! As for the PHP read access, I'm more worried about remote code execution through undiscovered vulnerabilities in external packages (or an attacker gaining write access to a package that gets included through the dependency chain, like we've seen with the event-stream debacle recently). If a PHP file with such a vulnerability lives inside the webroot (especially if it's in a "standard" path like /vendor/vulnerable-package/...), this could give an attacker access to the entire system. Granted, direct access can be blocked in the .htaccess or something like that, but for me it's just one less thing to worry about. Though I'm admittedly a bit paranoid ? ProcessWire's classloader is certainly a good option as well, I just use the composer autoloader because I need to set up Composer anyway for most projects, I guess it comes down to preference. For a shared module or site profile I'd definitely go with the ProcessWire classloader!
  17. I've been working with ProcessWire for a while now, and I've noticed that using Composer to manage dependencies and autoload external libraries isn't as prevalent in ProcessWire development as in other areas of PHP programming. I started out by using the default setup recommend in this blogpost. However, one major problem I have with this approach is that all external dependencies live in the webroot (the directory the server points to), which is unfavourable from a security standpoint and, in my opinion, just feels a bit messy. In this tutorial, I want to go through a quick setup of Composer and ProcessWire that keeps the dependencies, all custom-written code and other source material outside of the webroot, and makes full usage of the Composer autoloader. This setup is pretty basic, so this tutorial is probably more useful to beginners (this is why I'll also include some general information on Composer), but hopefully everyone can take something away from this for their personal workflow. Site structure after setup This is what the directory structure can look like after the setup: . ├── composer.json ├── composer.lock ├── node_modules │ └── ... ├── public │ ├── index.php │ ├── site │ ├── wire │ └── ... ├── packacke-lock.json ├── package.json ├── sass │ ├── main.scss │ ├── _variables.scss │ └── ... ├── src │ ├── ContentBag.php │ └── ... └── vendor ├── autoload.php ├── composer ├── league ├── symfony └── ... As mentioned, the main point of this setup is to keep all external libraries, all other custom source code and resources out of the webroot. That includes Composer's vendor folder, your node_modules and JavaScript source folder if you are compiling JavaScript with webpack or something similar and including external scripts via NPM, or your CSS preprocessor files if you are using SASS or LESS. In this setup, the public directory acts as the webroot (the directory that is used as the entry point by the server, DocumentRoot in the Apache configuration). So all other files and directories in the mysite folder aren't accessible over the web, even if something goes wrong. One caveat of this setup is that it's not possible to install ProcessWire modules through Composer using the PW Module Installer (see Blogpost above), but that's just a minor inconvenience in my experience. Installation You'll need to have composer installed on your system for this. Installation guides can be found on getcomposer.org. First, open up your shell and navigate to the mysite folder. $ cd /path/to/mysite/ Now, we'll initialize a new Composer project: $ composer init The CLI will ask some questions about your projects. Some hints if you are unsure how to answer the prompts: Package names are in the format <vendor>/<project>, where vendor is your developer handle. I use my Github account, so I'll put moritzlost/mysite (all lowercase). Project type is project if you are creating a website. Author should be in the format Name <email>. Minimum Stability: I prefer stable, this way you only get stable versions of dependencies. License will be proprietary unless you plan on sharing your code under a FOSS license. Answer no to the interactive dependencies prompts. This creates the composer.json file, which will be used to keep track of your dependencies. For now, you only need to run the composer install command to initialize the vendor directory and the autoloader: $ composer install Now it's time to download and install ProcessWire into the public directory: $ git clone https://github.com/processwire/processwire public If you don't use git, you can also download ProcessWire manually. I like to clean up the directory after that: $ cd public $ rm -r .git .gitattributes .gitignore CONTRIBUTING.md LICENSE.TXT README.md Now, setup your development server to point to the /path/to/mysite/public/ directory (mind the public/ at the end!) and install ProcessWire normally. Including & using the autoloader With ProcessWire installed, we need to include the composer autoloader. If you check ProcessWire's index.php file, you'll see that it tries to include the autoloader if present. However, this assumes the vendor folder is inside the webroot, so it won't work in our case. One good place to include the autoloader is using a site hook file. We need the autoloader as early as possible, so we'll use init.php: EDIT: As @horst pointed out, it's much better to put this code inside the config.php file instead, as the autoloader will be included much earlier: // public/site/config.php <?php namespace Processwire; require '../../vendor/autoload.php'; The following also doesn't apply when including the autoloader in the config-file. This has one caveat: Since this file is executed by ProcessWire after all modules had their init methods called, the autoloader will not be available in those. I haven't come across a case where I needed it this early so far; however, if you really need to include the autoloader earlier than that, you could just edit the lines in the index.php file linked above to include the correct autoloader path. In this case, make sure not to overwrite this when you update the core! Now we can finally include external libraries and use them in our code without hassle! I'll give you an example. For one project, I needed to parse URLs and check some properties of the path, host et c. I could use parse_url, however that has a couple of downsides (specifically, it doesn't throw exceptions, but just fails silently). Since I didn't want to write a huge error-prone regex myself, I looked for a package that would help me out. I decided to use this URI parser, since it's included in the PHP League directory, which generally stands for high quality. First, install the dependency (from the project root, the folder your composer.json file lives in): $ composer require league/uri-parser This will download the package into your vendor directory and refresh the autoloader. Now you can just use the package in your own code, and composer will autoload the required class files: // public/site/templates/basic-page.php <?php namespace Processwire; use \League\Uri\Parser; // ... if ($url = $page->get('url')) { $parser = new Parser(); $parsed_url = $parser->parse($url); // do stuff with $parsed_url ... } Wiring up custom classes and code Another topic that I find really useful but often gets overlooked in Composer tutorials is the ability to wire up your own namespace to a folder. So if you want to write some object-oriented code outside of your template files, this gives you an easy way to autoload those using Composer as well. If you look at the tree above, you'll see there's a src/ directory inside the project root, and a ContentBag.php file inside. I want to connect classes in this directory with a custom namespace to be able to have them autoloaded when I use them in my templates. To do this, you need to edit your composer.json file: { "name": "moritzlost/mysite", "type": "project", "license": "proprietary", "authors": [ { "name": "Moritz L'Hoest", "email": "info@herebedragons.world" } ], "minimum-stability": "stable", "require": {}, "autoload": { "psr-4": { "MoritzLost\\MySite\\": "src/" } } } Most of this stuff was added during initialization, for now take note of the autoload information. The syntax is a bit tricky, since you have to escape the namespace seperator (backslash) with another backslash (see the documentation for more information). Also note the PSR-4 key, since that's the standard I use to namespace my classes. The line "MoritzLost\\MySite\\": "src/" tells Composer to look for classes under the namespace \MoritzLost\MySite\ in the src/ directory in my project root. After adding the autoload information, you have to tell composer to refresh the autoloader information: $ composer dump-autoload Now I'm ready to use my classes in my templates. So, if I have this file: // src/ContentBag.php <?php namespace MoritzLost\MySite; class ContentBag { // class stuff } I can now use the ContentBag class freely in my templates without having to include those files manually: // public/site/templates/home.php <?php namespace Processwire; use MoritzLost\MySite\ContentBag; $contentbag = new ContentBag(); // do stuff with contentbag ... Awesome! By the way, in PSR-4, sub-namespaces correspond to folders, so I can put the class MoritzLost\MySite\Stuff\SomeStuff in src/Stuff/SomeStuff.php and it will get autoloaded as well. If you have a lot of classes, you can group them this way. Conclusion With this setup, you are following secure practices and have much flexibility over what you want to include in your project. For example, you can just as well initialize a JavaScript project by typing npm init in the project root. You can also start tracking the source code of your project inside your src/ directory independently of the ProcessWire installation. All in all, you have good seperation of concerns between ProcessWire, external dependencies, your templates and your OOP-code, as well as another level of security should your Server or CGI-handler ever go AWOL. You can also build upon this approach. For example, it's good practice to keep credentials for your database outside the webroot. So you could modify the public/site/config.php file to include a config or .env file in your project root and read the database credentials from there. Anyway, that's the setup I came up with. I'm sure it's not perfect yet; also this tutorial is probably missing some information or isn't detailed enough in some areas depending on your level of experience. Feel free to ask for clarification, and to point out the things I got wrong. I like to learn as well ? Thanks for making it all the way to the bottom. Cheers!
  18. Just for the curious, your garbled text error was caused by using the wrong operator. A single pipe character (|) is the bitwise OR-operator in PHP. Bitwise operators perform comparisons on the bits of integer values. They aren't really useful for strings, as the results are pretty much random. From the PHP manual: What you were looking for is probably the logical OR operator (||). However, your original $key1 assignment still wouldn't work because of operator precedence in PHP: $key1 = $vars['key1'] || "Default1"; // executed as $key1 = ($vars['key'] || "Default1"); In this case the OR operation will be executed first, and the expression will always evaluate to true because non-empty strings are true-ish. Then the result of the expression is assigned to $key1. A cleaner way that works and will never throw errors: $var1 = !empty($vars['key']) ? $vars['key'] : 'Default1'; In PHP 7 you can use null coalescing: $var1 = $vars['key'] ?? 'Default1'; Keep in mind that this will only assign the default value if $vars['key'] is NULL, but not if it is (int) 0 or an empty string!
  19. This is a textformatter module that will automatically replace titles of other pages on your site with links to those pages. For example, if you have a template glossary-term, and mention the exact title of one page using that template in a textfield, the title will be automatically linked to that page (if the textfield uses that formatter). This is good for SEO, and saves you some manual labour. You can configure which templates should get automatically linked, and of course the formatter is only active for the fields you add this formatter to. Note that if you need more manual control over when and where titles are automatically linked, you're probably better of using Autolink from a Glossary by @mr-fan. Features Allows you to limit the automatic links by template. Only includes published & visible pages by default, with an option to include hidden pages. Automatically excludes the current page, with an option to change that behaviour. Allows you to configure the minimum title length for linked pages. Doesn't overwrite existing links, and detects most edge cases (titles inside other tag's attributes, titles inside existing links et c.). Supports multi-language sites. Titles will only be linked if a title in the current language is set. Can add configurable attributes to all automatically created links. This includes the ability to use page fields as replacement patterns for attributes. For example, you can create CSS classes that include the name of the template of the linked page. Extensive options and hooks to change the generated markup completely. Need <mark> elements with a title attribute based on a page field instead of a link? No problem. See the example project below. Prefer oldest or newest page in the case of duplicate titles. Queries the database directly for improved performance. Has options to switch between case sensitive and case insensitive modes, and force case sensitive behaviour even for case insensitive database collations. Allows you to overwrite the module configuration via the API to call the module with different settings for different requirements on the same site. Download & Documentation The module is now available in the modules directory: https://modules.processwire.com/modules/textformatter-page-title-links/ You can download & install it through the backend using the classname TextformatterPageTitleLinks. To install it manually, download or clone the module from the Github repository into your site/modules folder and install through the backend. The complete documentation can be found in the README in the repository. Make sure to check out the module configuration page after installing the module. Requirements PHP 7.1 or higher ProcessWire 3+ (it will probably work in older versions, I haven't tested those though). This is my first module, I hope it may become useful to some of you ? If you find any errors or have some other suggestions or feedback, let me know!
  20. Hi LAPS, based on the error message it appears the file your function lives in is not namespaced. So your type-hint Page $item is telling PHP that you expect an object of the class Page in the global namespace. All ProcessWire classes are in the Processwire namespace (check out the namespace declaration at the top of any given core PHP file). So you need to type-hint against the class in that namespace: function functionName(\Processwire\Page $item) { //... } Alternatively, you can alias the Page class for the file with your function, or put that file in the Processwire namespace itself. At the top of the PHP file, add one of those: // importing the Page class, this tells PHP that `Page` should be interpretated as `\Processwire\Page` <?php use \Processwire\Page; // namespacing the file itself, so ´Page´ will resolve to ´\Processwire\Page´ <?php namespace Processwire; Hope this works for you. Cheers!
  21. Hey @arjen, thanks for the reply! I don't want to use them myself, I just need to make sure I don't included unpublished pages or drafts in my module (see link above). But I've added some additional checks for that purpose now, and I guess the draft module will surely add the correct status for unpublished drafts, so it should be ok. Anyway, thanks for your help!
  22. @horst Or for anyone who owns the module and can tell me what's going on there ^^
  23. I have made some updates to the module, in case someone wants to check it out ? The module will now correctly exclude unpublished pages. There's also an additional check to make sure a page is viewable by the current user before it gets linked. Hidden pages are not included by default, but this can be changed through the module settings. I adjusted the option for including the current page; this option is now disabled by default. Also, this will now work inside Repeater or Repeater Matrix fields. The updated README file on Github reflects all changes. I still have a couple of things I want to add before I call it a stable release, but feel free to check out the current version and let me know if anything doesn't work! I'm not sure if the formatter will work with the ProDrafts module, as I don't own that module and I'm not sure how the draft and version statuses interact with the default statuses. If someone can test the module on a site using that module or shed some light on those questions (forum thread here), it'd be very much appreciated!
  24. Hi horst, thanks for the reply! I'll use that check to make sure no pages are linked that shouldn't be visible. However, I would still like to filter out pages with statuses that will never be viewable. I'm querying the database directly to get a list of pages for performance reasons. Since I'm performing one costly regex per title, I would like to only get the pages that are actually published & viewable, so I still need to understand all status IDs to determine whether they're relevant for the bitmask ... I looked into what checks $page->viewable perfoms, but haven't been able to find a reference to the IDs for "draft" and "versions", so maybe they are not relevant after all?
  25. I'm trying to build a bitwise filter for a database query for my textformatter module, and I stumbled over some page statuses I don't quite understand: /** * Page has pending draft changes (name: "draft"). * #pw-internal * */ const statusDraft = 64; /** * Page has version data available (name: "versions"). * #pw-internal * */ const statusVersions = 128; If I'm not mistaked, there's no way to create multiple draft of a page in the ProcessWire core without module, is that correct? I initially assumed was the status assigned to unpublished pages, but the unpublished status corresponds to id 2048. Is the only way to create drafts with the ProDrafts module? For my purposes, I just need to understand how I should treat those statuses. I'm trying to get all pages that are published and either hidden or not hidden (I want to make this configurable). Do I need to exclude pages that have the statusDraft, or statusVersions? Only one of them? Only in combination with other statuses? Or can I safely ignore those (so it's fine if the pages have the status or not)? I don't own the ProDrafts module, so I can't check the code to see how individual drafts are stored and how the draft ids correspond to the published id. Any insight into those statuses is appreciated. Thanks in advance!
×
×
  • Create New...