MoritzLost

Writing reusable markup generation functions

Recommended Posts

Writing reusable markup generation functions

Hello there, I've been working with ProcessWire for a while now, and I've been writing some helper functions to generate markup and reduce the amount of repetitive code in my templates. In this tutorial I want to explain how to write small, reusable functions and combine them to accomplish bigger tasks. Note that this is the follow-up to my last post, Building a reusable function to generate responsive images. In that tutorial, I demonstrated a pretty large function that generates multiple image variations for responsive images, as well as the corresponding markup. In this post, I'll split this function into multiple smaller functions that can be utilized for other purposes as well. This will be more beginner-orientated than the last one, I hope there's some interest in this anyway 🙂

Note that for my purposes, I prefer to have those functions as static methods on a namespaced object, so the following code examples will be placed in a simple Html class. However, you can use those as normal functions just as well.

class Html {
	// code goes here
}

Edit: Those functions use some syntax exclusive to PHP 7.1 and above, they won't work in PHP 7.0 and lower. Thanks for @Robin S for pointing that out.

Seperation of concerns

To split up the original function, we need to analyze all the individual tasks it performs:

  • Generate several image variations in different sizes.
  • Generate the corresponding srcset attribute markup according to the specification.
  • Generate the sizes attribute markup based on the passed queries.
  • Automatically include the description as the alt attribute.
  • Generate the markup for all attributes (including the ones passed to the function).
  • Generate the markup for the complete img tag.

The first three of those tasks are very specifically concerned with generating responsive images. Generating the alt attribute is relevant to any img tag, not just responsive images. Finally, generating the attributes and HTML markup is relevant to all HTML markup that one wants to generate. Therefore, this is how a hierarchy between those functions could look like.

  • Generate responsive image
    • Generate image markup
      • Generate any HTML tag markup
        • Generate an HTML start tag
          • Generate HTML attributes markup
        • Generate an HTML end tag

Those bullet points are the tasks I want to turn into individual functions, each accepting arguments as general as they can be, facilitating code reuse. I'll start writing those out from the ground up.

Generating attributes markup

HTML attributes are a list of property-value pairs, where the value is wrapped in quotation marks (") and assigned to the property with an equals-sign (=). Each pair is separated by a space. There are also standalone/empty attributes that don't have a value, for example:

<input id="name" class="form-control" disabled>

Since the input format consists of key-value pairs, it makes sense to use an associative array as the argument to the attributes functions.

public static function attributes(
    array $attributes
): string {
    $attr_string = '';
    foreach ($attributes as $attr => $val) {
        $attr_string .= ' ' . $attr . '="' . $val . '"';
    }
	return $attr_string;
}

However, this still needs to support standalone attributes. Those attributes are also known as boolean attributes, since their presence indicates a true value, their absence the opposite. Since all other values in the markup are strings or integers, we can differentiate between those based on the type of the value in the associative array. If it's a boolean, we'll treat it as a standalone attribute and only include it if the value is also true.

public static function attributes(
    array $attributes,
    bool $leading_space = false
): string {
    $attr_string = '';
    foreach ($attributes as $attr => $val) {
        if (is_bool($val)) {
            if ($val) {
                $attr_string .= " $attr";
            }
        } else {
            $attr_string .= ' ' . $attr . '="' . $val . '"';
        }
    }
    if (!$leading_space) {
        $attr_string = ltrim($attr_string, ' ');
    }
    return $attr_string;
}

Of course, this means that if a value in the array is boolean false, this array item will be left out. This is by design, as it allows the caller to use expressions in the array declaration. For example:

echo Html::attributes([
	'id' => 'name',
	'class' => 'form-control',
	'disabled' => $this->isDisabled()
]);

This way, if isDisabled returns true, the disabled attribute will be included, and left out if it doesn't.

Note that I also included a $leading_space argument for convenience.

Generating start tags, end tags and complete HTML elements

The start tag is identified by the element name and the attributes it contains. The end tag only needs a name. Those functions are trivial:

public static function startTag(
    string $element,
    ?array $attributes = []
): string {
    $attribute_string = self::attributes($attributes, true);
    return "<{$element}{$attribute_string}>";
}

public static function endTag(string $element): string
{
    return "</{$element}>";
}

Of course, the startTag function builds on the existing function to generate the attributes. Note that a start tag is identical with a standalone tag (i.e. a void HTML element such as the img tag).

At this point, it's also trivial to write a function that builds a complete element, including start and end tag as well as the enclosed content.

public static function element(
    string $element,
    ?string $content = null,
    array $attributes = [],
    $self_closing = false
): string {
    if ($self_closing) {
        return self::startTag($element, $attributes);
    } else {
        return self::startTag($element, $attributes) . $content . self::endTag($element);
    }
}

Note that while this function does take several arguments, all except the first have reasonable default values, so usually the caller will only have to pass two or three of them. Some examples:

echo Html::startTag('hr'); // <hr>
echo Html::element('a', 'My website', ['href' => 'http://herebedragons.world']); // <a href="http://herebedragons.world">My website</a>

Image tags

Those functions make for a solid foundation to build any type of HTML element markup. Based on the type, the functions can accept more specific arguments to be easier to use. For example, the previous link example could be simplified by writing a link function that accepts a link text and an href value, since those are needed for any link:

public static function link(
    string $url,
    ?string $text = null,
    array $attributes = []
): string {
    // use url as text if no text was passed
    $text = $text ?? $url;
    $attributes['href'] = $url;
    return self::element('a', $text, $attributes);
}

Anyway, for our image markup function, we'll take a Pageimage object as an argument instead, since most images we will use in a ProcessWire template will come from the ProcessWire API. Since all ProcessWire image fields have a description field by default, we can use that description as the alt attribute, which is good practice for accessibility.

public static function image(Pageimage $img, array $attributes = []): string
{
    $attributes['src'] = $img->url();
    // use image description as alt text, unless specified in $attributes
    if (empty($attributes['alt']) && !empty($img->description())) {
        $attributes['alt'] = $img->description();
    }
    return self::selfClosingElement('img', $attributes);
}

Pretty simple. Note that the alt attribute can still be manually overridden by the caller by including it in the $attributes array.

Responsive images

Now, the responsive image function can be shortened by building on this function in turn. Optimally, the three distinct tasks this performs (see above) should be separated into their own functions as well, however in practice I haven't seen much need for this. Also, this post is plenty long already, so ...

public static function imageResponsive(
    Pageimage $img,
    ?int $standard_width = 0,
    ?int $standard_height = 0,
    ?array $attributes = [],
    ?array $sizes_queries = [],
    array $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]
): string {
    // use inherit dimensions of the passed image if standard width/height is empty
    if (empty($standard_width)) {
        $standard_width = $img->width();
    }
    if (empty($standard_height)) {
        $standard_height = $img->height();
    }
    $suffix = 'auto_srcset';

    // if $attributes is null, default to an empty array
    $attributes = $attributes ?? [];

    // get original image for resizing
    $original_image = $img->getOriginal() ?? $img;

    // the default image for the src attribute
    $default_image = $original_image->size(
        $standard_width,
        $standard_height,
        ['upscaling' => false, 'suffix' => $suffix]
    );

    // build the srcset attribute string, and generate the corresponding widths
    $srcset = [];
    foreach ($variant_factors as $factor) {
        // round up, srcset doesn't allow fractions
        $width = ceil($standard_width * $factor);
        $height = ceil($standard_height * $factor);
        // we won't upscale images
        if ($width <= $original_image->width() && $height <= $original_image->height()) {
            $current_image = $original_image->size($width, $height, ['upscaling' => false, 'suffix' => $suffix]);
            $srcset[] = $current_image->url() . " {$width}w";
        }
    }
    $attributes['srcset'] = implode(', ', $srcset);

    // build the sizes attribute string
    if ($sizes_queries) {
        $attributes['sizes'] = implode(', ', $sizes_queries);
    }

    return self::image($default_image, $attributes);
}

See my last post for details. Since then, I made some changed to the function I outlined here (thanks to @horst for pointing out some pitfalls with my approach). Most notably:

  • The generated images now include a prefix so they can be removed by a cleanup script more easily.
  • The function now accepts a width and a height parameter so that the aspect ratio of the generated images is fixed (reasons for this change are explained here). To get the original functionality back, I also wrote two helper functions that takes only a width/height and fill in the missing parameter based on the aspect ratio of the passed image.

The helper functions look like this:

public static function imageResponsiveByWidth(
    Pageimage $img,
    ?int $standard_width = 0,
    ?array $attributes = [],
    ?array $sizes_queries = [],
    array $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]
): string {
    // automatically fill the height parameter based
    // on the aspect ratio of the passed image
    if (empty($standard_width)) {
        $standard_width = $img->width();
    }
    $factor = $img->height() / $img->width();
    $standard_height = ceil($factor * $standard_width);

    return self::imageResponsive(
        $img,
        $standard_width,
        $standard_height,
        $attributes,
        $sizes_queries,
        $variant_factors
    );
}

Conclusion

This approach was born out of necessity, since pure PHP templating makes for some messy code. Of course, another approach would be to use a template engine in the first place. However, I didn't want the overhead of installing Twig or Blades for my smaller projects, so for those small to medium-sized projects, I found some helper functions to generate markup and clean up my code to be a helpful addition.

A small disclaimer, I update those functions pretty frequently while developing with ProcessWire, so it's possible some errors made their way into the versions I posted here that I haven't discovered yet. If you want to use some of the included code in your own projects, make sure to properly test it.

I'm also working on a small library including those and some other helpers I wrote, I'll post a Github link once it's in a usable stage.

So this post got way longer than I intended, I hope that some of you still made your way through it and enjoyed it a bit 🙂 If you see some problems or possible improvements to those methods and the general approach, I'd be happy to hear them!

Complete code for reference


<?php

use \Processwire\Pageimage;

class Html
{

    /**
     * Build a simple element tag with the passed element.
     *
     * @param string $element The element/tag name as a string.
     * @param ?string $content The content of the element (what goes between the tags).
     * @param ?array $attributes Optional attributes for the element.
     * @param boolean $self_closing Whether the element is self-closing (i.e. no end tag). $content is ignored if true.
     * @return string The HTML element markup.
     */
    public static function element(
        string $element,
        ?string $content = null,
        array $attributes = [],
        $self_closing = false
    ): string {
        if ($self_closing) {
            return self::startTag($element, $attributes);
        } else {
            return self::startTag($element, $attributes) . $content . self::endTag($element);
        }
    }

    /**
     * Builds a start tag for an element (or a self-closing/void element).
     *
     * @param string $element
     * @param array $attributes
     * @return string The HTML start tag markup.
     */
    public static function startTag(
        string $element,
        ?array $attributes = []
    ): string {
        $attribute_string = self::attributes($attributes, true);
        return "<{$element}{$attribute_string}>";
    }

    /**
     * Build an end tag for an element.
     *
     * @param string $element The HTML end tag markup.
     * @return void
     */
    public static function endTag(string $element): string
    {
        return "</{$element}>";
    }
	
    /**
     * Build an HTML attribute string from an array of attributes. Attributes set
     * to (bool) true will be included as standalone (no attribute value) and left
     * out if set to (bool) false.
     *
     * @param array $attributes Attributes in attribute => value form.
     * @param bool $leading_space Whether to include a leading space in the attribute string.
     * @return string The attributes as html markup.
     */
    public static function attributes(
        array $attributes,
        bool $leading_space = false
    ): string {
        $attr_string = '';
        foreach ($attributes as $attr => $val) {
            if (is_bool($val)) {
                if ($val) {
                    $attr_string .= " $attr";
                }
            } else {
                $attr_string .= ' ' . $attr . '="' . $val . '"';
            }
        }
        if (!$leading_space) {
            $attr_string = ltrim($attr_string, ' ');
        }
        return $attr_string;
    }

    /**
     * Image Functions.
     */

    /**
     * Build a simple image tag from a Processwire Pageimage object.
     *
     * @param Pageimage $img The image to use.
     * @param array $attributes Optional attributes for the element.
     * @return string
     */
    public static function image(Pageimage $img, array $attributes = []): string
    {
        $attributes['src'] = $img->url();
        // use image description as alt text, unless specified in $attributes
        if (empty($attributes['alt']) && !empty($img->description())) {
            $attributes['alt'] = $img->description();
        }
        return self::selfClosingElement('img', $attributes);
    }

    /**
    * Builds a responsive image element including different resolutions
    * of the passed image and optionally a sizes attribute build from
    * the passed queries.
    *
    * @param Pageimage $img The base image. Must be passed in the largest size available.
    * @param int|null $standard_width The standard width for the generated image. Use NULL to use the inherent size of the passed image.
    * @param int|null $standard_height The standard height for the generated image. Use NULL to use the inherent size of the passed image.
    * @param array|null $attributes Optional array of html attributes.
    * @param array|null $sizes_queries The full queries and sizes for the sizes attribute.
    * @param array|null $variant_factors The multiplication factors for the alternate resolutions.
    * @return string
    */
    public static function imageResponsive(
        Pageimage $img,
        ?int $standard_width = 0,
        ?int $standard_height = 0,
        ?array $attributes = [],
        ?array $sizes_queries = [],
        array $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]
    ): string {
        // use inherit dimensions of the passed image if standard width/height is empty
        if (empty($standard_width)) {
            $standard_width = $img->width();
        }
        if (empty($standard_height)) {
            $standard_height = $img->height();
        }
        $suffix = 'auto_srcset';

        // if $attributes is null, default to an empty array
        $attributes = $attributes ?? [];

        // get original image for resizing
        $original_image = $img->getOriginal() ?? $img;

        // the default image for the src attribute
        $default_image = $original_image->size(
            $standard_width,
            $standard_height,
            ['upscaling' => false, 'suffix' => $suffix]
        );

        // build the srcset attribute string, and generate the corresponding widths
        $srcset = [];
        foreach ($variant_factors as $factor) {
            // round up, srcset doesn't allow fractions
            $width = ceil($standard_width * $factor);
            $height = ceil($standard_height * $factor);
            // we won't upscale images
            if ($width <= $original_image->width() && $height <= $original_image->height()) {
                $current_image = $original_image->size($width, $height, ['upscaling' => false, 'suffix' => $suffix]);
                $srcset[] = $current_image->url() . " {$width}w";
            }
        }
        $attributes['srcset'] = implode(', ', $srcset);

        // build the sizes attribute string
        if ($sizes_queries) {
            $attributes['sizes'] = implode(', ', $sizes_queries);
        }

        return self::image($default_image, $attributes);
    }

    /**
    * Shortcut for the responsiveImage function that only takes a width parameter.
    * Height is automatically generated based on the aspect ratio of the passed image.
    *
    * @param Pageimage $img The base image. Must be passed in the largest size available.
    * @param int|null $standard_width The standard width for this image. Use NULL to use the inherent size of the passed image.
    * @param array|null $attributes Optional array of html attributes.
    * @param array|null $sizes_queries The full queries and sizes for the sizes attribute.
    * @param array|null $variant_factors The multiplication factors for the alternate resolutions.
    * @return string
    */
    public static function imageResponsiveByWidth(
        Pageimage $img,
        ?int $standard_width = 0,
        ?array $attributes = [],
        ?array $sizes_queries = [],
        array $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]
    ): string {
        // automatically fill the height parameter based
        // on the aspect ratio of the passed image
        if (empty($standard_width)) {
            $standard_width = $img->width();
        }
        $factor = $img->height() / $img->width();
        $standard_height = ceil($factor * $standard_width);

        return self::imageResponsive(
            $img,
            $standard_width,
            $standard_height,
            $attributes,
            $sizes_queries,
            $variant_factors
        );
    }

    /**
    * Shortcut for the responsiveImage function that only takes a height parameter.
    * Width is automatically generated based on the aspect ratio of the passed image.
    *
    * @param Pageimage $img The base image. Must be passed in the largest size available.
    * @param int|null $standard_height The standard height for this image. Use NULL to use the inherent size of the passed image.
    * @param array|null $attributes Optional array of html attributes.
    * @param array|null $sizes_queries The full queries and sizes for the sizes attribute.
    * @param array|null $variant_factors The multiplication factors for the alternate resolutions.
    * @return string
    */
    public static function imageResponsiveByHeight(
        Pageimage $img,
        ?int $standard_height = 0,
        ?array $attributes = [],
        ?array $sizes_queries = [],
        array $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]
    ): string {
        // automatically fill the width parameter based
        // on the aspect ratio of the passed image
        if (empty($standard_height)) {
            $standard_height = $img->height();
        }
        $factor = $img->width() / $img->height();
        $standard_width = ceil($factor * $standard_height);

        return self::imageResponsive(
            $img,
            $standard_width,
            $standard_height,
            $attributes,
            $sizes_queries,
            $variant_factors
        );
    }

}

 

Edited by MoritzLost
PHP compatibility notice
  • Like 11

Share this post


Link to post
Share on other sites

Really great tutorial, thanks!

Might be worth noting that a minimum of PHP 7.1 is required for the code syntax used.

  • Like 3

Share this post


Link to post
Share on other sites
22 hours ago, Robin S said:

Really great tutorial, thanks!

Might be worth noting that a minimum of PHP 7.1 is required for the code syntax used.

That's correct, I barely think about that anymore since I'm able to write PHP 7.2 at work ^^' Thanks!

11 hours ago, bernhard said:

Very nice tutorial, thx!

I wonder if that would be nice to have as a module. Installable, reusable, one-click-updateable....

But maybe it would be easier to extend available libraries, such as https://doc.nette.org/en/2.4/html-elements

I'm working on that, though at the moment I'm just building it as a generic composer package. Just seems easy since you get to use the composer autoloader ... I haven't found a simple way to autoload classes in a Processwire module, and performance-wise I don't like the idea of including everything on every request.

That nette module is interesting, I actually started building something like that at first, but it turned out to be more overhead than I needed most of the time. Though the nette module seems much simpler than my first approach, so maybe I gave up too quickly ...

How would you include another library such as nette in a Processwire module? Manually or is there a more Processwire-y way to include dependencies?

  • Like 3

Share this post


Link to post
Share on other sites
42 minutes ago, MoritzLost said:

How would you include another library such as nette in a Processwire module? Manually or is there a more Processwire-y way to include dependencies?

That's two approaches I did in my modules:

https://gitlab.com/baumrock/RockPdf/blob/master/RockPdf.module.php#L37-38

https://gitlab.com/baumrock/RockForms/blob/master/RockForms.module.php#L63-64

But I have to admit that I don't use all those composer, gulp and whatsoever tools, so there might be better solutions and I'd be happy to see how others do 🙂 

  • Like 1

Share this post


Link to post
Share on other sites
On 10/25/2018 at 9:59 PM, bernhard said:

That's two approaches I did in my modules:

https://gitlab.com/baumrock/RockPdf/blob/master/RockPdf.module.php#L37-38

https://gitlab.com/baumrock/RockForms/blob/master/RockForms.module.php#L63-64

But I have to admit that I don't use all those composer, gulp and whatsoever tools, so there might be better solutions and I'd be happy to see how others do 🙂 

I see, so you're packing the dependencies alongside the composer autoloader and including the autoloader on a per-module basis. That's certainly self-contained, very good solution for sites that don't use composer. Though it does mean dependencies can't be reused across modules. I guess there's something to be said for both approaches. Maybe I can put out a little module that provides a wrapper through the composer library (once it's published) so it can be installed through the Processwire GUI and called through the Processwire API. First I gotta get around to finishing it though ^^

I have started to use Composer for my latest two Processwire projects; it's a pretty simple setup, I just put the composer.json and vendor folder one directory above the webroot and then include the autoloader in the init.php file. This way it's no overhead at all, I can just use any libraries I want to and don't need to worry about anything else. Of course I have to update the composer dependencies independently from the modules, but that's just a few CLI commands away. Maybe I'll write a short tutorial for that too :)

  • Like 3

Share this post


Link to post
Share on other sites
45 minutes ago, MoritzLost said:

Maybe I'll write a short tutorial for that too :)

Would love to see such a tutorial! 🙂 

  • Like 2

Share this post


Link to post
Share on other sites
23 hours ago, gmclelland said:

@bernhard I thought /vendor/autoload.php was already loaded by Processwire automatically if it exist?  https://processwire.com/blog/posts/composer-google-calendars-and-processwire/

Maybe you don't need to require it in your modules?

ProcessWire automatically includes the autoloader if it's located at /vendor/autoload.php. If the module is installed manually / through the backend, ProcessWire has no way of knowing about the included autoloader. Likewise, even if you have composer initialized in the root directory, the autoloader has no way of knowing that the dependencies of the module are located in the module's directory. So bernhard just includes the dependencies and the composer autoloader. The included autoload.php generated by Composer just calls spl_autoload_register, which adds the included composer autoloader instance to the autoload queue. This way, the module contains it's own dependencies and autoloader, so it can be installed through the GUI and still benefit from autoloading. Quite a neat solution actually.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By MoritzLost
      Hello there,
      I've started using ProcessWire at work a while ago and I have been really enjoying building modular, clean and fast sites based on the CMS (at work, I usually post as @schwarzdesign). While building my first couple of websites with ProcessWire, I have written some useful helper functions for repetitive tasks. In this post I want to showcase and explain a particular function that generates a responsive image tag based on an image field, in the hope that some of you will find it useful :)
      I'll give a short explanation of responsive images and then walk through the different steps involved in generating the necessary markup & image variations. I want to keep this beginner-friendly, so most of you can probably skip over some parts.
      What are responsive images
      I want to keep this part short, there's a really good in-depth article about responsive images on MDN if you are interested in the details. The short version is that a responsive image tag is simply an <img>-tag that includes a couple of alternative image sources with different resolutions for the browser to choose from. This way, smaller screens can download the small image variant and save data, whereas high-resolution retina displays can download the extra-large variants for a crisp display experience. This information is contained in two special attributes:
      srcset - This attribute contains a list of source URLs for this image. For each source, the width of the image in pixels is specified. sizes - This attribute tells the browser how wide a space is available for the image, based on media queries (usually the width of the viewport). This is what a complete responsive image tag may look like:
      <img srcset="/site/assets/files/1015/happy_sheep_07.300x0.jpg 300w, /site/assets/files/1015/happy_sheep_07.600x0.jpg 600w, /site/assets/files/1015/happy_sheep_07.900x0.jpg 900w, /site/assets/files/1015/happy_sheep_07.1200x0.jpg 1200w, /site/assets/files/1015/happy_sheep_07.1800x0.jpg 1800w, /site/assets/files/1015/happy_sheep_07.2400x0.jpg 2400w" sizes="(min-width: 1140px) 350px, (min-width: 992px) 480px, (min-width: 576px) 540px, 100vw" src="/site/assets/files/1015/happy_sheep_07.1200x0.jpg" alt="One sheep"> This tells the browser that there are six different sources for this image available, ranging from 300px to 2400px wide variants (those are all the same image, just in different resolutions). It also tells the browser how wide the space for the image will be:
      350px for viewports >= 1140px 480px for viewports >= 992px 540px for viewports >= 576px 100vw (full viewport width) for smaller viewports The sizes queries are checked in order of appearance and the browser uses the first one that matches. So now, the browser can calculate how large the image needs to be and then select the best fit from the srcset list to download. For browsers that don't support responsive images, a medium-sized variant is included as the normal src-Attribute.
      This is quite a lot of markup which I don't want to write by hand every time I want to place an image in a ProcessWire template. The helper function will need to generate both the markup and the variations of the original image.
      Building a reusable responsive image function
      Let's start with a function that takes two parameters: a Pageimage object and a standard width. Every time you access an image field through the API in a template (e.g. $page->my_image_field), you get a Pageimage object. Let's start with a skeleton for our function:
      function buildResponsiveImage( Pageimage $img, int $standard_width ): string { $default_img = $img->maxWidth($standard_width); return '<img src="' . $default_img->url() . '" alt="' . $img->description() . '">'; } // usage example echo buildResponsiveImage($page->my_image_field, 1200); This is already enough for a normal img tag (and it will serve as a fallback for older browsers). Now let's start adding to this, trying to keep the function as flexible and reusable as possible.
      Generating alternate resolutions
      We want to add a parameter that will allow the caller to specify in what sizes the alternatives should be generated. We could just accept an array parameter that contains the desired sizes as integers. But that is not very extendible, as we'll need to specify those sizes in each function call and change them all if the normal size of the image in the layout changes. Instead, we can use an array of factors; that will allow us to set a reasonable default, and still enable us to manually overwrite it. In the following, the function gets an optional parameter $variant_factor.
      // get the original image in full size $original_img = $img->getOriginal() ?? $img; // the default image for the src attribute, it wont be upscaled $default_image = $original_img->width($standard_width, ['upscaling' => false]); // the maximum size for our generated images $full_image_width = $original_img->width(); // fill the variant factors with defaults if not set if (empty($variant_factors)) { $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]; } // build the srcset attribute string, and generate the corresponding widths $srcset = []; foreach ($variant_factors as $factor) { // round up, srcset doesn't allow fractions $width = ceil($standard_width * $factor); // we won't upscale images if ($width <= $full_image_width) { $srcset[] = $original_img->width($width)->url() . " {$width}w"; } } $srcset = implode(', ', $srcset); // example usage echo buildResponsiveImage($page->my_image_field, 1200, [0.4, 0.5, 0.6, 0.8, 1, 1.25, 1.5, 2]); Note that for resizing purposes, we want to get the original image through the API first, as we will generate some larger alternatives of the images for retina displays. We also don't want to generate upscaled versions of the image if the original image isn't wide enough, so I added a constraint for that.
      The great thing about the foreach-loop is that it generates the markup and the images on the server at the same time. When we call $original_img->width($width), ProcessWire automatically generates a variant of the image in that size if it doesn't exist already. So we need to do little work in terms of image manipulation.
      Generating the sizes attribute markup
      For this, we could build elaborate abstractions of the normal media queries, but for now, I've kept it very simple. The sizes attribute is defined through another array parameter that contains the media queries as strings in order of appearance.
      $sizes_attribute = implode(', ', $sizes_queries); The media queries are always separated by commas followed by a space character, so that part can be handled by the function. We'll still need to manually write the media queries when calling the function though, so that is something that can be improved upon.
      Finetuning & improvements
      This is what the function looks like now:
      function buildResponsiveImage( Pageimage $img, int $standard_width, array $sizes_queries, ?array $variant_factors = [] ): string { // get the original image in full size $original_img = $img->getOriginal() ?? $img; // the default image for the src attribute, it wont be upscaled $default_image = $original_img->width($standard_width, ['upscaling' => false]); // the maximum size for our generated images $full_image_width = $original_img->width(); // fill the variant factors with defaults if not set if (empty($variant_factors)) { $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]; } // build the srcset attribute string, and generate the corresponding widths $srcset = []; foreach ($variant_factors as $factor) { // round up, srcset doesn't allow fractions $width = ceil($standard_width * $factor); // we won't upscale images if ($width <= $full_image_width) { $srcset[] = $original_img->width($width)->url() . " {$width}w"; } } $srcset = implode(', ', $srcset); return '<img src="' . $default_img->url() . '" alt="' . $img->description() . '" sizes="' . $sizes_attribute . '" srcset="' . $srcset . '">'; } It contains all the part we need, but there are some optimizations to make.
      First, we can make the $sizes_queries parameters optional. The sizes attribute default to 100vw (so the browser will always download an image large enough to fill the entire viewport width). This isn't optimal as it wastes bandwidth if the image doesn't fill the viewport, but it's good enough as a fallback.
      We can also make the width optional. When I have used this function in a project, the image I passed in was oftentimes already resized to the correct size. So we can make $standard_width an optional parameter that defaults to the width of the passed image.
      if (empty($standard_width)) { $standard_width = $img->width(); } Finally, we want to be able to pass in arbitrary attributes that will be added to the element. For now, we can just add a parameter $attributes that will be an associative array of attribute => value pairs. Then we need to collapse those into html markup.
      $attr_string = implode( ' ', array_map( function($attr, $value) { return $attr . '="' . $value . '"'; }, array_keys($attributes), $attributes ) ); This will also allow for some cleanup in the way the other attributes are generated, as we can simply add those to the $attributes array along the way.
      Here's the final version of this function with typehints and PHPDoc. Feel free to use this is your own projects.
      /** * Builds a responsive image element including different resolutions * of the passed image and optionally a sizes attribute build from * the passed queries. * * @param \Processwire\Pageimage $img The base image. * @param int|null $standard_width The standard width for this image. Use 0 or NULL to use the inherent size of the passed image. * @param array|null $attributes Optional array of html attributes. * @param array|null $sizes_queries The full queries and sizes for the sizes attribute. * @param array|null $variant_factors The multiplication factors for the alternate resolutions. * @return string */ function buildResponsiveImage( \Processwire\Pageimage $img, ?int $standard_width = 0, ?array $attributes = [], ?array $sizes_queries = [], ?array $variant_factors = [] ): string { // if $attributes is null, default to an empty array $attributes = $attributes ?? []; // if the standard width is empty, use the inherent width of the image if (empty($standard_width)) { $standard_width = $img->width(); } // get the original image in full size $original_img = $img->getOriginal() ?? $img; // the default image for the src attribute, it wont be // upscaled if the desired width is larger than the original $default_image = $original_img->width($standard_width, ['upscaling' => false]); // we won't create images larger than the original $full_image_width = $original_img->width(); // fill the variant factors with defaults if (empty($variant_factors)) { $variant_factors = [0.25, 0.5, 0.75, 1, 1.5, 2]; } // build the srcset attribute string, and generate the corresponding widths $srcset = []; foreach ($variant_factors as $factor) { // round up, srcset doesn't allow fractions $width = ceil($standard_width * $factor); // we won't upscale images if ($width <= $full_image_width) { $srcset[] = $original_img->width($width)->url() . " {$width}w"; } } $attributes['srcset'] = implode(', ', $srcset); // build the sizes attribute string if ($sizes_queries) { $attributes['sizes'] = implode(', ', $sizes_queries); } // add src fallback and alt attribute $attributes['src'] = $default_image->url(); if ($img->description()) { $attriutes['alt'] = $img->description(); } // implode the attributes array to html markup $attr_string = implode(' ', array_map(function($attr, $value) { return $attr . '="' . $value . '"'; }, array_keys($attributes), $attributes)); return "<img ${attr_string}>"; } Example usage with all arguments:
      echo buildResponsiveImage( $page->testimage, 1200, ['class' => 'img-fluid', 'id' => 'photo'], [ '(min-width: 1140px) 350px', '(min-width: 992px) 480px', '(min-width: 576px) 540px', '100vw' ], [0.4, 0.5, 0.6, 0.8, 1, 1.25, 1.5, 2] ); Result:
      <img class="img-fluid" id="photo" srcset="/site/assets/files/1/sean-pierce-1053024-unsplash.480x0.jpg 480w, /site/assets/files/1/sean-pierce-1053024-unsplash.600x0.jpg 600w, /site/assets/files/1/sean-pierce-1053024-unsplash.720x0.jpg 720w, /site/assets/files/1/sean-pierce-1053024-unsplash.960x0.jpg 960w, /site/assets/files/1/sean-pierce-1053024-unsplash.1200x0.jpg 1200w, /site/assets/files/1/sean-pierce-1053024-unsplash.1500x0.jpg 1500w, /site/assets/files/1/sean-pierce-1053024-unsplash.1800x0.jpg 1800w, /site/assets/files/1/sean-pierce-1053024-unsplash.2400x0.jpg 2400w" sizes="(min-width: 1140px) 350px, (min-width: 992px) 480px, (min-width: 576px) 540px, 100vw" src="/site/assets/files/1/sean-pierce-1053024-unsplash.1200x0.jpg" alt="by Sean Pierce"> Now this is actually too much functionality for one function; also, some of the code will be exactly the same for other, similar helper functions. If some of you are interested, I'll write a second part on how to split this into multiple smaller helper functions with some ideas on how to build upon it. But this has gotten long enough, so yeah, I hope this will be helpful or interesting to some of you :)
      Also, if you recognized any problems with this approach, or can point out some possible improvements, let me know. Thanks for reading!
    • By Guy Verville
      First of all, I'm not an expert on PHP. I recently read about generators and I understand their usefulness in avoiding loading a set of objects into an array to the point of saturating the memory.
      The $pages->find() call is known to be greedy (and slow) when it comes to processing large amounts of pages, because it loads all objects into memory.
      Is there a way to use a generator to avoid this problem? Is there a workaround? I know that $pages->findMany() exists, but it is also called greedy.
      See https://secure.php.net/manual/en/language.generators.php
      Translated with www.DeepL.com/Translator
    • By John W.
      SYNOPSIS
      A little guide to generating an sitemap.xml using (I believe) a script Ryan originally wrote with the addition of being able to optionally exclude child pages from being output in the sitemap.xml file.
      I was looking back on a small project today where I was using a php script to generate an xml file, I believe the original was written by Ryan. Anyway, I needed a quick fix for the script to allow me to optionally exclude children of pages from being included in the sitemap.xml output.
      OVERVIEW
      A good example of this is a site where if you visit /minutes/ a page displays a list of board meetings which includes a title,  date, description and link to download the .pdf file.
      I have a template called minutes and a template called minutes-document. The first page, minutes, when loaded via /minutes/ simply grabs all of its child pages and outputs the name, description and actual path of an uploaded .pdf file for a visitor to download.
      In my back-end I have the template MINUTES and MINUTES-DOCUMENT. Thus:


      So, basically, their employee can login, hover over minutes, click new, then create a new (child) record and name it the date of the meeting e.g. June 3rd, 2016 :

       
      ---------------------------
      OPTIONALLY EXCLUDING CHILDREN - SETUP
      Outputting the sitemap.xml and optionally excluding children that belong to a template.
      The setup of the original script is as follows:
      1. Save the file to the templates folder as sitemap.xml.php
      2. Create a template called sitemap-xml and use the sitemap.xml.php file.
      3. Create a page called sitemap.xml using the sitemap-xml template
       
      Now, with that done you will need to make only a couple of slight modifications that will allow the script to exclude children of a template from output to the sitemap.xml
      1. Create a new checkbox field and name it:   sitemap_exclude_children
      2. Add the field to a template that you want to control whether the children are included/excluded from the sitemap. In my example I added it to my "minutes" template.
      3. Next, go to a page that uses a template with the field you added above. In my case, "MINUTES"
      4. Enable the checkbox to exclude children, leave it unchecked to include children.
      For example, in my MINUTES page I enabled the checkbox and now when /sitemap.xml is loaded the children for the MINUTES do not appear in the file.

       
      A SIMPLE CONDITIONAL TO CHECK THE "sitemap_exclude_children" VALUE
      This was a pretty easy modification to an existing script, adding only one line. I just figure there may be others out there using this script with the same needs.
      I simply inserted the if condition as the first line in the function:
      function renderSitemapChildren(Page $page) { if($page->sitemap_exclude_children) return ""; ... ... ...  
      THE FULL SCRIPT WITH MODIFICATION
      <?php /** * ProcessWire Template to power a sitemap.xml * * 1. Copy this file to /site/templates/sitemap-xml.php * 2. Add the new template from the admin. * Under the "URLs" section, set it to NOT use trailing slashes. * 3. Create a new page at the root level, use your sitemap-xml template * and name the page "sitemap.xml". * * Note: hidden pages (and their children) are excluded from the sitemap. * If you have hidden pages that you want to be included, you can do so * by specifying the ID or path to them in an array sent to the * renderSiteMapXML() method at the bottom of this file. For instance: * * echo renderSiteMapXML(array('/hidden/page/', '/another/hidden/page/')); * * patch to prevent pages from including children in the sitemap when a field is checked / johnwarrenllc.com * 1. create a checkbox field named sitemap_exclude_children * 2. add the field to the parent template(s) you plan to use * 3. when a new page is create with this template, checking the field will prevent its children from being included in the sitemap.xml output */ function renderSitemapPage(Page $page) { return "\n<url>" . "\n\t<loc>" . $page->httpUrl . "</loc>" . "\n\t<lastmod>" . date("Y-m-d", $page->modified) . "</lastmod>" . "\n</url>"; } function renderSitemapChildren(Page $page) { if($page->sitemap_exclude_children) return ""; /* Aded to exclude CHILDREN if field is checked */ $out = ''; $newParents = new PageArray(); $children = $page->children; foreach($children as $child) { $out .= renderSitemapPage($child); if($child->numChildren) $newParents->add($child); else wire('pages')->uncache($child); } foreach($newParents as $newParent) { $out .= renderSitemapChildren($newParent); wire('pages')->uncache($newParent); } return $out; } function renderSitemapXML(array $paths = array()) { $out = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'; array_unshift($paths, '/'); // prepend homepage foreach($paths as $path) { $page = wire('pages')->get($path); if(!$page->id) continue; $out .= renderSitemapPage($page); if($page->numChildren) { $out .= renderSitemapChildren($page); } } $out .= "\n</urlset>"; return $out; } header("Content-Type: text/xml"); echo renderSitemapXML(); // Example: echo renderSitemapXML(array('/hidden/page/'));  
      In conclusion, I have used a couple different processwire sitemap generating modules. But for my needs, the above script is fast and easy to setup/modify.
      - Thanks
       
    • By Robin S
      Password Generator
      Adds a password generator to InputfieldPassword.

       
      Usage
      Install the Password Generator module.
      Now any InputfieldPassword has a password generation feature. The settings for the generator are taken automatically from the settings* of the password field.
      *Settings not supported by the generator:
      Complexify: but generated passwords should still satisfy complexify settings in the recommended range. Banned words: but the generated passwords are random strings so actual words are unlikely to occur.  
      https://modules.processwire.com/modules/password-generator/
      https://github.com/Toutouwai/PasswordGenerator