Noel Boss Posted April 25, 2018 Share Posted April 25, 2018 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/ Copy all of the files for this module into /site/modules/PageQueryBoss/ 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: 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 A template name; skyscraper or city Name of a child page (page.child.name=pagename); my-page-name A ProcessWire selector; template=building, floors>=25 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: Any of the keys above (1-5) with an new nested sub-query array: $query = [ 'skyscraper' => [ 'height', 'floors' ], 'architect' => [ 'title', 'email' ], ] 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: Nested by name (default) Nested by ID 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 28 1 Link to comment Share on other sites More sharing options...
Sergio Posted April 25, 2018 Share Posted April 25, 2018 @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!! 3 Link to comment Share on other sites More sharing options...
Noel Boss Posted April 25, 2018 Author Share Posted April 25, 2018 2 minutes ago, Sergio said: create a front-end using Vuejs Thats what I built it for 2 Link to comment Share on other sites More sharing options...
dragan Posted April 25, 2018 Share Posted April 25, 2018 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. 2 Link to comment Share on other sites More sharing options...
flydev Posted April 26, 2018 Share Posted April 26, 2018 Totally useful ? Thanks you ! 3 Link to comment Share on other sites More sharing options...
kongondo Posted April 28, 2018 Share Posted April 28, 2018 @Noel Boss, Looks great, thanks! I've just had a quick look. Any reason why the module is autoload? 1 Link to comment Share on other sites More sharing options...
Noel Boss Posted April 29, 2018 Author Share Posted April 29, 2018 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 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? Link to comment Share on other sites More sharing options...
kongondo Posted April 29, 2018 Share Posted April 29, 2018 3 hours ago, Noel Boss said: Thank you @kongondo 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. 3 Link to comment Share on other sites More sharing options...
Noel Boss Posted April 29, 2018 Author Share Posted April 29, 2018 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! 1 Link to comment Share on other sites More sharing options...
Noel Boss Posted May 1, 2018 Author Share Posted May 1, 2018 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 3 Link to comment Share on other sites More sharing options...
kongondo Posted May 2, 2018 Share Posted May 2, 2018 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 ?. 1 Link to comment Share on other sites More sharing options...
joshua Posted July 6, 2018 Share Posted July 6, 2018 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! Link to comment Share on other sites More sharing options...
joshua Posted July 6, 2018 Share Posted July 6, 2018 Oh, now I got it! Setting module autoload and singular to true again and clearing all caches helped... Thanks for this great module! 1 Link to comment Share on other sites More sharing options...
louisstephens Posted August 7, 2018 Share Posted August 7, 2018 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. Link to comment Share on other sites More sharing options...
louisstephens Posted November 19, 2018 Share Posted November 19, 2018 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. Link to comment Share on other sites More sharing options...
lecrackffm Posted February 18, 2019 Share Posted February 18, 2019 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'); 1 1 Link to comment Share on other sites More sharing options...
Lars Høie Posted June 30, 2019 Share Posted June 30, 2019 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? 1 Link to comment Share on other sites More sharing options...
3fingers Posted December 16, 2020 Share Posted December 16, 2020 @Noel Boss hi! Just here to check if you are still around in the forum, since I'm looking at your module for future projects. Does anybody using it without gotchas with the latest PW releases? :) Link to comment Share on other sites More sharing options...
Noel Boss Posted January 6, 2021 Author Share Posted January 6, 2021 I’m around… You can contact me on noelboss.com if I’m not here… I would also try the latest API Modules: https://processwire.com/modules/app-api/ 1 Link to comment Share on other sites More sharing options...
wbmnfktr Posted February 22, 2022 Share Posted February 22, 2022 Is anyone using this module and can help with a few questions? I'm looking for a solution to retrieve an almost flat JSON result without unnecessary nesting of some kind. My query for now looks like this: // myTestingApi.php $json = $pages->find('template=restaurant')->pageQueryJson(['id', 'title', 'dateLastMod']); header('Content-Type: application/json'); echo $json; The result looks like this, which is fine, but...: { "restaurant-abc": { "id": 1234, "title": "Restaurant ABC", "dateLastMod": "dd.mm.yyyy" }, "xyz-bistro": { "id": 2345, "title": "XYZ Bistro", "dateLastMod": "dd.mm.yyyy" }, "restaurant-citycenter": { "id": 3456, "title": "Restaurant Citycenter", "dateLastMod": "dd.mm.yyyy" }, "bar-cafe-123": { "id": 4567, "title": "Bar Cafe 123", "dateLastMod": "dd.mm.yyyy" }, "paradise-food": { "id": 5678, "title": "Paradise Food", "dateLastMod": "dd.mm.yyyy" } } I'm looking for something more like this. { "restaurants": [ { "id": 1234, "title": "Restaurant ABC", "dateLastMod": "dd.mm.yyyy" }, { "id": 2345, "title": "XYZ Bistro", "dateLastMod": "dd.mm.yyyy" }, { "id": 3456, "title": "Restaurant Citycenter", "dateLastMod": "dd.mm.yyyy" }, { "id": 4567, "title": "Bar Cafe 123", "dateLastMod": "dd.mm.yyyy" }, { "id": 5678, "title": "Paradise Food", "dateLastMod": "dd.mm.yyyy" } ] } How would I get this? I guess I tried whatever I could based on what's written in this thread but maybe I'm missing details here. Anyone an idea? Link to comment Share on other sites More sharing options...
bernhard Posted February 23, 2022 Share Posted February 23, 2022 Not sure if I'm missing something... No module, 2 lines of code: $restaurants = $pages->findRaw("template=restaurant", ['id', 'title', 'modified']); $json = json_encode(['restaurants' => array_values($restaurants)]); 3 Link to comment Share on other sites More sharing options...
wbmnfktr Posted February 23, 2022 Share Posted February 23, 2022 This little bit of PHP works pretty well. I'm quite surprised actually. There are still questions, but this would be a bit too off-topic here. 1 Link to comment Share on other sites More sharing options...
kongondo Posted February 23, 2022 Share Posted February 23, 2022 1 hour ago, wbmnfktr said: I'm quite surprised actually. Why? Were you not aware of findRaw()? For me it has been one of the biggest game changers in ProcessWire in recent years ?. Sorry, OT. Link to comment Share on other sites More sharing options...
bernhard Posted February 23, 2022 Share Posted February 23, 2022 13 minutes ago, kongondo said: For me it has been one of the biggest game changers in ProcessWire in recent years ?. Why? Were you not aware of RockFinder? Sorry, OT ? 2 Link to comment Share on other sites More sharing options...
kongondo Posted February 23, 2022 Share Posted February 23, 2022 2 minutes ago, bernhard said: Why? Were you not aware of RockFinder? Sorry, OT ? Hehe. In fact I had started adding the below but thought to save it for another day. But I'll add it here and then stop this OT business! Quote I don't know if Ryan had findRaw() plans before but my guess is that it was influenced by RockFinder. RockFinder was my daily companion when I was developing Padloper before findRaw(). It was a great; no a massive help for me - helping me understand ProcessWire PageFinder class as well help shape some of my decisions. When findRaw() came along, I had to switch. Padloper makes extensive use of findRaw() and I know how it works, thanks to @bernhard's work on RockFinder ?. I was saving above for a Blog post when Padloper stable is released...but oh well. And now, I shut up ?. 1 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now