Jump to content

Recommended Posts

Page Query Boss

Build complex nested queries containing multiple fields and pages and return an array or JSON. This is useful to fetch data for SPA and PWA.

You can use the Module to transform a ProcessWire Page or PageArray – even RepeaterMatrixPageArrays – into an array or JSON. Queries can be nested and contain closures as callback functions. Some field-types are transformed automatically, like Pageimages or MapMarker.

Installation

Via ProcessWire Backend

It is recommended to install the Module via the ProcessWire admin "Modules" > "Site" > "Add New" > "Add Module from Directory" using the PageQueryBoss class name.

Manually

Download the files from Github or the ProcessWire repository: https://modules.processwire.com/modules/page-query-builder/

  1. Copy all of the files for this module into /site/modules/PageQueryBoss/
  2. Go to “Modules > Refresh” in your admin, and then click “install” for the this module.

Module Methods

There are two main methods:

Return query as JSON

$page->pageQueryJson($query);

Return query as Array

$page->pageQueryArray($query);

Building the query

The query can contain key and value pairs, or only keys. It can be nested and 
contain closures for dynamic values. To illustrate a short example:

// simple query:
$query = [
	'height',
	'floors',
];
$pages->find('template=skyscraper')->pageQueryJson($query);

Queries can be nested, contain page names, template names or contain functions and ProcessWire selectors:

// simple query:
$query = [
	'height',
	'floors',
	'images', // < some fileds contain default sub-queries to return data
	'files' => [ // but you can also overrdide these defaults:
		'filename'
		'ext',
		'url',
	],
	// Assuming there are child pages with the architec template, or a
	// field name with a page relation to architects
	'architect' => [ // sub-query
		'name',
		'email'
	],
	// queries can contain closure functions that return dynamic content
	'querytime' => function($parent){
		return "Query for $parent->title was built ".time();
	}
];
$pages->find('template=skyscraper')->pageQueryJson($query);

Keys:

  1. A single fieldname; height or floors or architects 
    The Module can handle the following fields:

    • Strings, Dates, Integer… any default one-dimensional value
    • Page references
    • Pageimages
    • Pagefiles
    • PageArray
    • MapMarker
    • FieldtypeFunctional
  2. A template name; skyscraper or city

  3. Name of a child page (page.child.name=pagename); my-page-name
  4. A ProcessWire selector; template=building, floors>=25

  5. A new name for the returned index passed by a # delimiter:

     // the field skyscraper will be renamed to "building":
     $query = ["skyscraper`#building`"]

     

Key value pars:

  1. Any of the keys above (1-5) with an new nested sub-query array:

    $query = [
    	'skyscraper' => [
    		'height',
    		'floors'
    	],
    	'architect' => [
    		'title',
    		'email'
    	],
    ]

     

  2. A named key and a closure function to process and return a query. The closure gets the parent object as argument:

    $query = [
    	'architecs' => function($parent)
    		{
    			$architects = $parent->find('template=architect');
    			return $architects->arrayQuery(['name', 'email']);
    			// or return $architects->explode('name, email');
    		}
    ]

Real life example:

$query = [
	'title',
	'subtitle',
	// naming the key invitation
	'template=Invitation, limit=1#invitation' => [
		'title',
		'subtitle',
		'body',
	],
	// returns global speakers and local ones...
	'speakers' => function($page){
		$speakers = $page->speaker_relation;
		$speakers = $speakers->prepend(wire('pages')->find('template=Speaker, global=1, sort=-id'));

		// build a query of the speakers with
		return $speakers->arrayQuery([
			'title#name', // rename title field to name
			'subtitle#ministry', // rename subtitle field to ministry
			'links' => [
				'linklabel#label', // rename linklabel field to minlabelistry
				'link'
			],
		]);
	},
	'Program' => [ // Child Pages with template=Program
		'title',
		'summary',
		'start' => function($parent){ // calculate the startdate from timetables
			return $parent->children->first->date;
		},
		'end' => function($parent){ // calculate the endate from timetables
			return $parent->children->last->date;
		},
		'Timetable' => [
			'date', // date
			'timetable#entry'=> [
				'time#start', // time
				'time_until#end', // time
				'subtitle#description', // entry title
			],
		],
	],
	// ProcessWire selector, selecting children > name result "location"
	'template=Location, limit=1#location' => [
		'title#city', // summary title field to city
		'body',
		'country',
		'venue',
		'summary#address', // rename summary field to address
		'link#tickets', // rename ticket link
		'map', // Mapmarker field, automatically transformed
		'images',
		'infos#categories' => [ // repeater matrix! > rename to categories
			'title#name', // rename title field to name
			'entries' => [ // nested repeater matrix!
				'title',
				'body'
			]
		],
	],
];

if ($input->urlSegment1 === 'json') {
	header('Content-type: application/json');
	echo $page->pageQueryJson($query);
	exit();
}

Module default settings

The modules settings are public. They can be directly modified, for example:

$modules->get('PageQueryBoss')->debug = true;
$modules->get('PageQueryBoss')->defaults = []; // reset all defaults

Default queries for fields:

Some field-types or templates come with default selectors, like Pageimages etc. These are the default queries:

// Access and modify default queries: $modules->get('PageQueryBoss')->defaults['queries'] …
public $defaults = [
	'queries' => [
		'Pageimages' => [
			'basename',
			'url',
			'httpUrl',
			'description',
			'ext',
			'focus',
		],
		'Pagefiles' => [
			'basename',
			'url',
			'httpUrl',
			'description',
			'ext',
			'filesize',
			'filesizeStr',
			'hash',
		],
		'MapMarker' => [
			'lat',
			'lng',
			'zoom',
			'address',
		],
		'User' => [
			'name',
			'email',
		],
	],
];

These defaults will only be used if there is no nested sub-query for the respective type. If you query a field with complex data and do not provide a sub-query, it will be transformed accordingly:

$page->pageQueryArry(['images']);

// returns something like this
'images' => [
	'basename',
	'url',
	'httpUrl',
	'description',
	'ext',
	'focus'=> [
		'top',
		'left',
		'zoom',
		'default',
		'str',
	]
];

You can always provide your own sub-query, so the defaults will not be used:

$page->pageQueryArry([
	'images' => [
		'filename',
		'description'
	],
]);

Overriding default queries:

You can also override the defaults, for example

$modules->get('PageQueryBoss')->defaults['queries']['Pageimages'] = [
	'basename',
	'url',
	'description',
];

Index of nested elements

The index for nested elements can be adjusted. This is also done with defaults. There are 3 possibilities:

  1. Nested by name (default)
  2. Nested by ID
  3. Nested by numerical index

Named index (default):

This is the default setting. If you have a field that contains sub-items, the name will be the key in the results:

// example
$pagesByName = [
	'page-1-name' => [
		'title' => "Page one title",
		'name' => 'page-1-name',
	],
	'page-2-name' => [
		'title' => "Page two title",
		'name' => 'page-2-name',
	]
]

ID based index:

If an object is listed in $defaults['index-id'] the id will be the key in the results. Currently, no items are listed as defaults for id-based index:

// Set pages to get ID based index:
$modules->get('PageQueryBoss')->defaults['index-id']['Page'];
// Example return array:
$pagesById = [
	123 => [
		'title' => "Page one title",
		'name' => 123,
	],
	124 => [
		'title' => "Page two title",
		'name' => 124,
	]
]

Number based index

By default, a couple of fields are transformed automatically to contain numbered indexes:

// objects or template names that should use numerical indexes for children instead of names
$defaults['index-n'] => [
	'Pageimage',
	'Pagefile',
	'RepeaterMatrixPage',
];

// example
$images = [
	0 => [
		'filename' => "image1.jpg",
	],
	1 => [
		'filename' => "image2.jpg",
	]
]

Tipp: When you remove the key 'Pageimage' from $defaults['index-n'], the index will again be name-based.

 

Help-fill closures & tipps:

These are few helpfill closure functions you might want to use or could help as a
starting point for your own (let me know if you have your own):


Get an overview of languages:

    $query = ['languages' => function($page){
        $ar = [];
        $l=0;
        foreach (wire('languages') as $language) {
            // build the json url with segment 1
            $ar[$l]['url']= $page->localHttpUrl($language).wire('input')->urlSegment1;
            $ar[$l]['name'] = $language->name == 'default' ? 'en' : $language->name;
            $ar[$l]['title'] = $language->getLanguageValue($language, 'title');
            $ar[$l]['active'] = $language->id == wire('user')->language->id;
            $l++;
        }
        return $ar;
    }];

Get county info from ContinentsAndCountries Module

Using the [ContinentsAndCountries Module](https://modules.processwire.com/modules/continents-and-countries/) you can extract iso
code and names for countries:

    $query = ['country' => function($page){
        $c = wire('modules')->get('ContinentsAndCountries')->findBy('countries', array('name', 'iso', 'code'),['code' =>$page->country]);
        return count($c) ? (array) $c[count($c)-1] : null;
    }];

Custom strings from a RepeaterTable for interface

Using a RepeaterMatrix you can create template string for your frontend. This is
usefull for buttons, labels etc. The following code uses a repeater with the
name `strings` has a `key` and a `body` field, the returned array contains the `key` field as,
you guess, keys and the `body` field as values:

    // build custom translations
    $query = ['strings' => function($page){
        return array_column($page->get('strings')->each(['key', 'body']), 'body', 'key');
    }];

Multilanguage with default language fallback

Using the following setup you can handle multilanguage and return your default
language if the requested language does not exist. The url is composed like so:
`page/path/{language}/{content-type}` for example: `api/icf/zurich/conference/2019/de/json`
 

    // get contenttype and language (or default language if not exists)
    $lang = wire('languages')->get($input->urlSegment1);
    if(!$lang instanceof Nullpage){
        $user->language = $lang;
    } else {
        $lang = $user->language;
    }

    // contenttype segment 2 or 1 if language not present
    $contenttype = $input->urlSegment2 ? $input->urlSegment2 : $input->urlSegment1;

    if ($contenttype === 'json') {
        header('Content-type: application/json');
        echo $page->pageQueryJson($query);
        exit();
    }

Debug

The module respects wire('config')->debug. It integrates with TracyDebug. You can override it like so:

// turns on debug output no mather what:
$modules->get('PageQueryBoss')->debug = true;

Todos

Make defaults configurable via Backend. How could that be done in style with the default queries?

Module in alpha Stage: Subject to change

This module is in alpha stage … Query behaviour (especially selecting child-templates, renaming, naming etc)  could change ;)

  • Like 26
  • Thanks 1

Share this post


Link to post
Share on other sites

@noelboss, this is BEAUTIFUL!!

Very easy to understand. I'm eager to play with it and create a front-end using Vuejs and another with Gatsbyjs.

Many thanks for this module!!

  • Like 3

Share this post


Link to post
Share on other sites
2 minutes ago, Sergio said:

create a front-end using Vuejs

Thats what I built it for :)

  • Like 2

Share this post


Link to post
Share on other sites

Wow, da hätt glaubs öpper gschaffed... trotz de Früehligs-Polle :-)

From just a very quick first glance, this looks really nice. I'm sure I'll install this when I have some spare time and play around with it.

I don't actually have a real use-case myself atm, but since we're experimenting with conversational interface stuff lately (chatbots etc.), Dialogflow Webhooks etc., this could come in very handy for REST/API-like scenarios.

  • Like 2

Share this post


Link to post
Share on other sites
18 hours ago, kongondo said:

Looks great, thanks!

I've just had a quick look. Any reason why the module is autoload?

Thank you @kongondo :D No, no reason – I'm new to PW and Module development and had issues with Modules without autoload before – and the module I was taking clues from had autoload true as well. I probably have to look into it a bit more. Any good resources about that? Should I not use it?

Share this post


Link to post
Share on other sites
3 hours ago, Noel Boss said:

Thank you @kongondo :D No, no reason – I'm new to PW and Module development and had issues with Modules without autoload before – and the module I was taking clues from had autoload true as well. I probably have to look into it a bit more. Any good resources about that? Should I not use it?

You only use autoload if the module needs to load automatically when ProcessWire boots so that you can listen to every single request and typically attach hooks to some event of interest. Don't let this confuse you though. Modules can hook into events without being autoload modules. An example autoload module is Blog module's Publish Date. It listens to when Blog Posts are published and sets a publish date. As such, it needs to be auto-loaded.

Does your module need to boot with ProcessWire or does one call it when one needs it? If the latter, then it means presently, it is just loading and using up resources when it doesn't need to. Can your module work like this?

$pageQuery = $modules->get('PageQueryBoss');
$query = $pages->find('template=skyscraper');
$json = $pageQuery->pageQueryJson($query);

It doesn't seem to me like it needs to autoload, but I've only had a brief look, so I could be wrong.

You can read a bit more about autoload modules in the module class docs here.

  • Like 3

Share this post


Link to post
Share on other sites

Thank you for the detailed response @kongondo ! The above code does not work, the plugin extends $page so you can directly call ‚pageQueryJson‘ on a Page or a PageArray you call it - altough you need to specify what results you‘d like to receive;

$json = $pages->find('template=skyscraper')->pageQueryJson(['title']);

from what you describe, it probably does not need to autoload :) I’ll change that with the next release. Thanks again!

  • Like 1

Share this post


Link to post
Share on other sites
On 4/28/2018 at 5:22 PM, kongondo said:

@Noel Boss,

Looks great, thanks!

I've just had a quick look. Any reason why the module is autoload?

I have just released an updated version without autoload. I also added a few helpfull closures to the readme, as well as hook-able methods:

https://github.com/noelboss/PageQueryBoss#helpfull-closures--tipps

  • Like 3

Share this post


Link to post
Share on other sites
On 4/29/2018 at 5:47 PM, Noel Boss said:

The above code does not work, the plugin extends $page so you can directly call ‚pageQueryJson‘ on a Page or a PageArray you call it - altough you need to specify what results you‘d like to receive;

Hi @Noel Boss,

In your case, if the module extends (not in a PHP technical sense) $page, then it will have to be an autoload module. Your module adds methods to the  Page and WireArray classes. Hence, if it is not an autoload module, I don't see how it will work just by doing this:

$json = $pages->find('template=skyscraper')->pageQueryJson(['title']);

You will definitely get a PHP error about Method PageArray::pageQueryJson does not exist etc.

Maybe I'm missing something. Did it work for you with the new changes? 

Sorry if I am taking your round in circles 😊.

 

  • Like 1

Share this post


Link to post
Share on other sites

Hi @Noel Boss,

now I get exactly the problem 

Exception: Method PageArray::pageQueryJson does not exist or is not callable in this context

as @kongondo mentioned. I've tried a minimal version of the pageQueryBoss:

<?php namespace ProcessWire; 
  $query = [
      "title"
  ];
echo $pages->find('template=portal-entry')->pageQueryJson($query);

But still with this error. Setting autoload to true does not change anything...
Do you have any idea to solve this?

 

Many thanks in advance - for your module but also for your medium article!

 

Share this post


Link to post
Share on other sites

Oh, now I got it!
Setting module autoload and singular to true again and clearing all caches helped... 

Thanks for this great module!

  • Like 1

Share this post


Link to post
Share on other sites

Perhaps I am missing something here, but where is the "singular" and "isAutoload" at for the module? I am running into the same issue that kongondo mentioned, but can not find anything to change.

Share this post


Link to post
Share on other sites

Hey Noel,

I just had a quick question for you regarding pageQueryBoss. I was just trying to get a simple example up and running (using the most up to date version),  I got it working by adding in autoload true to to the config. However, I am a bit stumped. I really need a query set up to return something like:
 

Parent Page
  - Grandchild Page
    - field
    - field
  - Grandchild Page
    - field
    - field
  - Grandchild Page
    - field
    - field

Though no matter what I try, I can not seem to wrap my ahead around how to return the desired result. 

Share this post


Link to post
Share on other sites

This looks very nice @Noel Boss,

is there a way to support repeater fields?

 

EDIT: Sorry for my silly question, found it in your docs.

This works for me:

$query = ['repeater' => function($page){
        return array_column($page->get('repeater')->each(['key', 'body']), 'body', 'key');

 

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites

Hi @Noel Boss

Thank you so much for the plugin! I'm still trying to figure out how to make it do precisely what I need it to do, so I just have a quick question about formatting. 

Basically I'm trying to get this kind of json output: 

[
  {
    "title": "Paul Rand",
    "year": [
      "1993"
    ],
    "authors": [
      "Abrams, Janet"
    ],
    "categories": [
      "Criticism",
      "Design",
      "Design Histories"
    ]
  }
]

But I'm getting this instead:

{
  paul-rand: {
    title: "Paul Rand",
    year: [
    "1993"
    ],
    authors: [
    "Abrams, Janet"
    ],
    categories: [
    "Criticism",
    "Design",
    "Design Histories"
    ]
  }
}

This is the query I'm currently using: 

header("Content-type: application/json");
$modules->get('PageQueryBoss')->debug = true;
$query = [
  'title',
  'year.title#year',
  'authors.title#authors',
  'categories.title#categories'
];
echo $pages->find('template=book, sort=authors.title')->pageQueryJson($query);

Is there some setting I'm missing, or is the problem with how I'm structuring the query?

  • Thanks 1

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
      This is a new module that provides a simple solution to clearing all your cache layers at once, and an extensible interface to perform various cache-related actions.
      The simple motivation behind this module was that I was tired of manually clearing caches in several places after deploying a change on a live site. The basic purpose of this module is a simple Clear all caches link in the Setup menu which clears out all caches, no matter where they hide. You can customize what exactly the module does through it's configuration menu:
      Expire or delete all cache entries in the database, or selectively clear caches by namespace ($cache API) Clear the the template render cache. Clear out specific folders inside your site's cache directory (/site/assets/cache) Refresh version strings for static assets to bust client-side browser caches (this requires some setup, see the full documentation for details). This is the basic function of the module. However, you can also add different cache management action through the API and execute them through the module's interface. For this advanced usage, the module provides:
      An interface to see all available cache actions and execute them. A system log and logging output on the module page to see verify what the module is doing. A CacheControlTools class with utility functions to clear out different caches. An API to add cache actions, execute them programmatically and even modify the default action. Permission management, allowing you granular control over which user roles can execute which actions. The complete documentation can be found in the module's README.
      Beta release
      Note that I consider this a Beta release. Since the module is relatively aggressive in deleting some caches, I would advise you to install in on a test environment before using it on a live site.
      Let me know if you're getting any errors, have trouble using the module or if you have suggestions for improvement!
      In particular, can someone let me know if this module causes any problems with the ProCache module? I don't own or use it, so I can't check. As far as I can tell, ProCache uses a folder inside the cache directory to cache static pages, so my module should be able to clear the ProCache site cache as well, I'd appreciate it if someone can test that for me.
      Future plans
      If there is some interest in this, I plan to expand this to a more general cache management solution. I particular, I would like to add additional cache actions. Some ideas that came to mind:
      Warming up the template render cache for publicly accessible pages. Removing all active user sessions. Let me know if you have more suggestions!
      Links
      https://github.com/MoritzLost/ProcessCacheControl ProcessCacheControl in the Module directory

    • By joshua
      This module is (yet another) way for implementing a cookie management solution.
      Of course there are several other possibilities:
      - https://processwire.com/talk/topic/22920-klaro-cookie-consent-manager/
      - https://github.com/webmanufaktur/CookieManagementBanner
      - https://github.com/johannesdachsel/cookiemonster
      - https://www.oiljs.org/
      - ... and so on ...
      In this module you can configure which kind of cookie categories you want to manage:

      You can also enable the support for respecting the Do-Not-Track (DNT) header to don't annoy users, who already decided for all their browsing experience.
      Currently there are four possible cookie groups:
      - Necessary (always enabled)
      - Statistics
      - Marketing
      - External Media
      All groups can be renamed, so feel free to use other cookie group names. I just haven't found a way to implement a "repeater like" field as configurable module field ...
      When you want to load specific scripts ( like Google Analytics, Google Maps, ...) only after the user's content to this specific category of cookies, just use the following script syntax:
      <script type="optin" data-type="text/javascript" data-category="statistics" data-src="/path/to/your/statistic/script.js"></script> <script type="optin" data-type="text/javascript" data-category="marketing" data-src="/path/to/your/mareketing/script.js"></script> <script type="optin" data-type="text/javascript" data-category="external_media" data-src="/path/to/your/external-media/script.js"></script> <script type="optin" data-type="text/javascript" data-category="marketing">console.log("Inline scripts are also working!");</script> The type has to be "optin" to get recognized by PrivacyWire, the data-attributes are giving hints, how the script shall be loaded, if the data-category is within the cookie consents of the user. These scripts are loaded asynchronously after the user made the decision.
      If you want to give the users the possibility to change their consent, you can use the following Textformatter:
      [[privacywire-choose-cookies]] It's planned to add also other Textformatters to opt-out of specific cookie groups or delete the whole consent cookie.
      You can also add a custom link to output the banner again with a link / button with following class:
      <a href="#" class="privacywire-show-options">Show Cookie Options</a> <button class="privacywire-show-options">Show Cookie Options</button> This module is still in development, but we already use it on several production websites.
      You find it here: https://github.com/blaueQuelle/privacywire/tree/master
      Download: https://github.com/blaueQuelle/privacywire/archive/master.zip
      I would love to hear your feedback 🙂
      Edit: Updated URLs to master tree of git repo
       
    • By David Karich
      Admin Page Tree Multiple Sorting
      ClassName: ProcessPageListMultipleSorting
      Extend the ordinary sort of children of a template in the admin page tree with multiple properties. For each template, you can define your own rule. Write each template (template-name) in a row, followed by a colon and then the additional field names for sorting.
      Example: All children of the template "blog" to be sorted in descending order according to the date of creation, then descending by modification date, and then by title. Type:
      blog: -created, -modified, title  Installation
      Copy the files for this module to /site/modules/ProcessPageListMultipleSorting/ In admin: Modules > Check for new modules. Install Module "Admin Page Tree Multible Sorting". Alternative in ProcessWire 2.4+
      Login to ProcessWire backend and go to Modules Click tab "New" and enter Module Class Name: "ProcessPageListMultipleSorting" Click "Download and Install"   Compatibility   I have currently tested the module only under PW 2.6+, but think that it works on older versions too. Maybe someone can give a feedback.     Download   PW-Repo: http://modules.processwire.com/modules/process-page-list-multiple-sorting/ GitHub: https://github.com/FlipZoomMedia/Processwire-ProcessPageListMultipleSorting     I hope someone can use the module. Have fun and best regards, David
    • By dimitrios
      Hello,
      this module can publish content of a Processwire page on a Facebook page, triggered by saving the Processwire page.
      To set it up, configure the module with a Facebook app ID, secret and a Page ID. Following is additional configuration on Facebook for developers:
      Minimum Required Facebook App configuration:
      on Settings -> Basics, provide the App Domains, provide the Site URL, on Settings -> Advanced, set the API version (has been tested up to v3.3), add Product: Facebook Login, on Facebook Login -> Settings, set Client OAuth Login: Yes, set Web OAuth Login: Yes, set Enforce HTTPS: Yes, add "https://www.example.com/processwire/page/" to field Valid OAuth Redirect URIs. This module is configurable as follows:
      Templates: posts can take place only for pages with the defined templates. On/Off switch: specify a checkbox field that will not allow the post if checked. Specify a message and/or an image for the post.
      Usage
      edit the desired PW page and save; it will post right after the initial Facebook log in and permission granting. After that, an access token is kept.
       
      Download
      PW module directory: http://modules.processwire.com/modules/auto-fb-post/ Github: https://github.com/kastrind/AutoFbPost   Note: Facebook SDK for PHP is utilized.


×
×
  • Create New...