Jump to content

Routing feature / custom endpoints without hooking pageNotFound/404


Recommended Posts

This is what I wrote ryan lately:


I'm developing a module that I want to ship with some example files. The example files should be simple HTML/PHP files that render example implementations of my module. Now I want that examples to be displayed at a custom url:

mysite.com/mymodule/examples/foo --> render foo example
mysite.com/mymodule/examples/bar --> render bar example

Unfortunately this is really not as easy as it could be in ProcessWire. I see these options at the moment:

  • Enable url segments on the home page and match the urlsegmentstring against an expression
    This is not ideal, because I can't do that from within my modules code!
  • Create a page in the page tree with a custom template
    Thats a lot of work for such a simple task. It's also messing the pagetree with a page that nobody needs to see or edit. And it's also not possible to do that from within my module. Ok, I could use RockMigrations, but then one had to install another module just for rendering an example file?! Also not ideal 🙂 
  • Hook ProcessPageView::pageNotFound
    That is somewhat common practise and I've used this technique quite often (for ajax endpoints etc). But it is also not ideal in my opinion and I faced two problems today:
    • Using addHookAfter did not work, because I redirect all 404 requests to the admin login
    • Using addHookBefore did somehow work, but Tracy's debug bar is not available then, which is also not really an option

I know that this is a quite common need and I wonder if we could get a better option for such scenarios:

WireRoute::get("/mymodule/examples/{example}", function(String $example) {
  return $this->wire->files->render(__DIR__."/examples/$example.php");

That's all I want to do and it could be as simple as that 🙂 There's a lot of wisdom and inspiration on the Laravel Routing docs: https://laravel.com/docs/8.x/routing

It just does not feel right to me to abuse a 404 Exception for the purpose of rendering custom output that has no actual Page representation in the system...

PS: I've tried to have a look myself. First I looked at ProcessPageView and thought it might be possible to just inject a checkRoute() check before the pageNotFound is thrown, but I realized it's not as simple as that. Then I had a look at PageRender::renderPage but that's also quite... complicated 😄 So I'd like to hear if that request would be something more complicated than I think or maybe it is something quite simple for you (as you know exactly how a PageView process executes and what it takes (like triggering all necessary pieces like ready() etc)).

And that was his response:


Hi Bernhard,

Sounds potentially useful for autoload modules. If ProcessWire were to have such a feature, it would be something for it to check if a URL fails to match a page. The 404 hook is at least a technically accurate description of this, even if the wording isn't as pretty as something we could add. But given that there would be no matching $page, the API ready event could never occur, which is likely why Tracy isn't loading. So I don't think it would solve the Tracy issue you are seeing simply because a $page is required for the API ready event to trigger in ProcessWire. The $page could be the 404 page, but maybe Tracy is excluding that case (?), I'm not sure. 

Though as for solving your immediate need, if hooking to ProcessWire's pageNotFound event isn't workable for your case, I think it's maybe better to handle the example files in some other way, but would probably want to see more specifically what we're talking about before I could provide a more useful suggestion.

Now the topic came up here: https://processwire.com/talk/topic/20668-page-hit-counter-–-simple-page-view-tracking/?do=findComment&comment=211318


Now that you mention it, the first thing that strikes me is how often I have to build endpoints like this over and over again on every project. 😵😄 I think that would be a really useful feature request, to have a fixed endpoint, which then each module can extend and have access to it.

As this is getting offtopic in the pagehitcounter thread I suggest to continue discussion here @adrian @David Karich

What do you think? Have you ever had the need for a custom endpoint in your projects? How did you solve it? Which issues did you have? Let's collect some examples so that ryan can see "more specifically what we are talking about" 🙂 

  • Like 8
Link to comment
Share on other sites

The last 5 projects all needed some kind of API endpoint. Over time, I've settled for a custom "api" page under home. In two projects, this is a very simple piece of code with a switch statement, in others it looks for individual templates under templates/api. I'm not completely happy with any of them, though, and I've often wished for something akin to hooks with enough boilerplate to avoid repetitive code.

I do concur that Laravel has some very nice and expressive features. I could picture a WireApi singleton class with which modules could register, and a fixed "api" page that is by default not much more than

<?php namespace ProcessWire;


WireApi may or may not route to matching PHP files under templates/api by default.

Autload modules could register their routes in their init() method. Just a rough example with one or two Laravel niceties stirred into the usual PW awesomeness to get a nice declarative approach to api route definitions:

<?php namespace ProcessWire;

class ApiExampleHandler extends ApiRoute {
  public static function getModuleInfo() {
    return [...];
  public function init() {
    # hand GET requests to /api/users/USERID/profiledata off to our getUsrProfileData method
    # and pass the value in place of USERID as the first parameter $user to the method.
    # Use $sanitizer->intUnsigned on $user to make sure it is a valid page id.
    $this->api->get('/users/{user}/profiledata', $this, 'getUserProfileData')->whereSanitized('user', 'intUnsigned');
  public function getUserProfileData($user) {
    $u = $this->users->get($user) ?: $this->api->notFound();
    $p = $u->profilepage ?: $this->api->notFound();
    # Set Content-Type header, set json_encode return value and output it
      'nickname'		=>	$p->nick,
      'hobbies'			=>	$p->hobbies,
      'membersince'		=>	$p->joindate

Something along those lines could go a long way to making endpoint implementation easier. I don't know how often I've checked for isAjax et al, called json_decode and json_encode, checked the request method, sanitized and compared urlSegement1 and urlSegment2 and urlSegment3 (and sometimes even urlSegement4) in nested conditionals...

For quick and dirty solutions, api route registration could go into api.php before handleRequest() is called, or even into site/ready.php.

With a bit of creative magic, a nice PWish equivalent to Laravel's model binding should also be in the realm of possible things.

  • Like 7
Link to comment
Share on other sites

Having a sanctioned way for creating "dynamic" routes would definitely be a worthwhile addition to the core.

I've settled on creating JSON endpoints as separate templates and implementing the routing inside the template file. Sometimes a switch statement is enough, sometimes FastRoute makes more sense. It tends to be a lot of boilerplate even for simple tasks.

I agree with Ryan that piggybacking on 404s is technically correct even though somewhat of a misnomer. It ensures that regular pages always have priority when routing a request. In the case of conflicts with a module, the site has a way of overwriting and customising the routes.

Link to comment
Share on other sites

Perhaps FatFree can be another source of inspiration (or it could even be added to our projects easily? The whole library is 483 KB and 45 files...!)

FatFree has good a history of being maintained: https://github.com/bcosca/fatfree/releases

  • Like 1
Link to comment
Share on other sites

Did a little tinkering since I'm going to need another API endpoint for a new project soon and came up with this PoC:


It's very crude and all as its just meant as a playground for ideas for now (wouldn't dare to clutter up the Wire namespace), and it does still use pages behind the scene (though not the implicit 404 redirect). Perhaps hiding the endpoints from the tree for everybody but superusers might be an option, but I haven't fully thought that out.

Here are a few very simple examples of an API template (in fact, very close to what I thought up above):

<?php namespace ProcessWire;

$api->routeGET('/api/hello/{name}/', null, 'helloWorldApi', true);

$api->routeGET('/api/pagelist/', 'apitemplates/pagelist')

$api->route(['GET'], '/api/custom/{value}/', function($url, $values) {
      'success'	=>	true,
      'youare' => wire('user')->name,
      'data' => $values
})->check(function($url, $route, $check, $values) { return ctype_digit($values['value']); });

$api->handleRequest(['debug' => true]);

// ________________Only function declarations below_____________________

function helloWorldApi($url, $values) {

	wire('api')->jsonResponse(['Hello', $values['name']]);

Oh, and it ships with a near-one-click endpoint installer 😉


Let me know what you think, and if you have any ideas about improvements or doing things differently, bring it on.

  • Like 3
Link to comment
Share on other sites

I have not thought about the technical view, but I liked how your example really felt more "PW-ish" (like hooks) than my first approach. But I didn't like that it worked different to normal autoload modules... I came up with this idea that at least for my taste merges best of both worlds:

<?php namespace ProcessWire;
class FooModule extends WireData implements Module, ConfigurableModule {

  public static function getModuleInfo() {
    return [
      'title' => 'FooModule',
      'version' => '0.0.1',
      'summary' => 'Your module description',
      'autoload' => true,
      'singular' => true,
      'icon' => 'smile-o',
      'requires' => [],
      'installs' => [],

  public function init() {
    $this->addRoute("/my/custom/{user}/endpoint/{action}", $this, "userEndpoint");

  public function userEndpoint($event) {
    // get parameters from route and sanitize them
    // not sure about the syntax? maybe $event->route->get('user', 'int');
    $user = $event->api->get('user', 'int');
    $action = $event->api->get('action', 'string');
    $method = $event->api->method;

    if($method == 'GET') {
      // return found data as json
      $result = $this->wire->pages->findRaw([
        'template' => 'foo',
        'user' => $user,
      ], ["title", "foo", "bar"]);
      return $this->api->json($result);
    elseif($method == 'POST') {
      // update database
      // some kind of helper (https status codes etc)
      // would be nice, but I don't have much experience
      // with APIs so there might be others to talk to :)
      $user = $this->wire->users->get($user);
      $user->setAndSave('action', $action);
      return $this->api->success("Updated $user's action to $action");
    else {
      throw new Wire404Exception('Not allowed');

or in init.php:

$wire->addRoute("/my/custom/{user}/endpoint/{action}", function($event) {
    // get parameters from route and sanitize them
    // not sure about the syntax? maybe $event->route->get('user', 'int');
    $user = $event->api->get('user', 'int');
    $action = $event->api->get('action', 'string');
    $method = $event->api->method;


  • Thanks 1
Link to comment
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.
  • Create New...