Jump to content

dynamic function name call


fruid
 Share

Recommended Posts

I have different types of content throughout my site that I want to display on one page. For each type of content I have different markup.

I'm trying to create a very generic template so that it is easily expandable, maybe someone can help me with ideas how to approach this.

Right now I loop through the items and have one function with dozens of if/elseif/else-conditions. Depending on each item's template it renders different markup. Not very pretty. 

Another idea would be to create individual functions for each different markup and, depending on the template, call the according function.

I was thinking I could given the function the same name as the template and in case I add new content with a new template I would only need to add a function with that template's name. So in order to make this work I would need to store each function in a variable with the template's name and then do something like… 

$sometemplate = $item->template;
$somefunction = $sometemplate;
call_user_func($somefunction, $item);

right?

I wonder if there's a even smarter way, what's ProcessWire's approach to that? 

Link to comment
Share on other sites

It's a premium module but I believe Repeater Matrix will do exactly what you need. It's one of my 'must haves' on every site I create. It's part of the ProFields pack and I use other features on every site too, like Functional Fields . I'll be using the new Combo field as well. The time it saves me is well worth the investment not to mention the great support in the forum from other ProField users & Ryan himself. https://processwire.com/store/pro-fields/

 

  • Like 1
Link to comment
Share on other sites

Without needing to buy a Pro module (although they are excellent!), there is indeed a ProcessWire way for this.

You can use $files->render()/wireRenderFile() to achieve what you want:
https://processwire.com/api/ref/wire-file-tools/render/
https://processwire.com/api/ref/functions/wire-render-file/

The advantage of using this over your own functions is that it will automatically keep access to all of PW's API variables (no need to pass them in).

Here is a simple example based on your use-case description:

/**
 * In your generic template file.
 * Note: it assumes there is a directory under 'templates' called 'layouts' (it could be called anything).
 * Just create your layouts in the 'layouts' dir and name them the same as the '$item->template' names you are using.
 */

// Get the content of the specified layout into a variable.
// This example looks for '/site/templates/layouts/name-of-template.php' (the .php extension is assumed)
$pageContent = $file->render('layouts/' . $item->template);

// Just echo it out wherever you need it.
echo $pageContent;

Read the docs on it, there is more you can do with this method/function, including passing custom variables, and some other options to make it more flexible. There is also a $files->include() method which is the same except it directly echos out the content instead of returning it to a variable.

 

  • Like 5
Link to comment
Share on other sites

I'll just leave this here: How to set up Twig as a flexible view layer for ProcessWire and/or Create flexible content modules using Repeater Matrix fields

https://processwire.dev/processwire-flexible-content-sections/

The feature you're looking for is template inheritance – the ability to have a base template which is extended by child templates. The child templates can then override any block they want to. If you go down the path of wireRenderFile or MarkupRegions you'll always notice you're fundamentally lacking the ability to overwrite blocks in a parent template. With pure PHP templates, you'll always end up with a system that either needs to import a bunch of partials in every new template, just for the ability to leave out or change some of them for one particular template. Or your base template will need to be aware of every possible permutation and arrangement of partials and mixins in all your templates, resulting in messy and unmaintainable code which grows linearly with every template you add.

Using a template system with template inheritance solves that for you and makes your template system infinitely scalable (besides other benefits like autoescaping and better readability).

  • Like 2
Link to comment
Share on other sites

  • 2 weeks later...

hm… maybe I didn't make myself too clear.

So let's say I have an PageArray $items that should get a specific markup aka layout.

Let's say the markup is something like…

<li>
<h3><?=$item->title?></h3>
<p><strong>price: </strong><span><?=$item->price?></span>
<p><strong>category: </strong><span><?=$item->category?></span>
<p><?=$item->body?></p>
</li>

would I loop through the $items?

$filename = "somelayout.php";

foreach ($items as $item) :
$out .= $item->render($filename);
endforeach;
echo $out;

or just do:

$out = $items->render($filename);
echo $out;

Don't know why I ask this because both doesn't work but maybe this gives you an idea of my thinking.

Link to comment
Share on other sites

In somelayout.php the current Page is in the $page variable! It doesn’t know $item. Otherwise your loop should work.

PageArray doesn’t have a render method, although I guess you could do

$items->each(fn($item) => echo $item->render('somelayout.php'));

(or something like that)

  • Like 1
Link to comment
Share on other sites

Actually, I was wrong, PageArray does have a render method, but it comes through the default module MarkupPageArray.

Not sure what you mean about the arrays?

My code was supposed to use an arrow function. Maybe this works for you:

$out .= $items->each(function($item) => return $item->render('somelayout.php'));

But really, just do this

foreach ($items as $item)
    $out .= $item->render('somelayout.php')

and make sure you use $page instead of $item in somelayout.php.

Link to comment
Share on other sites

// somelayout.php
echo $page->title;

// somewhere else
$items = $pages("template=posts, limit=10");
$filename = 'somelayout.php'; // inside /site/template/
$out = '';
$out .= $items->renderPageArray($filename); // module is installed
echo $out;

// result: nothing whatsoever.

 

Link to comment
Share on other sites

1 hour ago, fruid said:

$out .= $items->renderPageArray($filename); // module is installed

Dude, at what point did I tell you to try that 😅

If you want to use that module, you need to familiarize yourself with it. Unfortunately there doesn’t seem to be any documentation apart from what’s in the code itself. What it does is add a method called render() to the PageArray class. Internally that calls MarkupPageArray::renderPageArray(), but you still need to write $items->render(). Now you need to figure out what arguments to pass. The information is in the code I linked. It takes an associative array of options, none of which are an actual template file like what you want to do.

You could do something like this if you really wanted to:

$items->render(array(
	'listMarkup' => '<ul>{out}</ul>',
	'itemMarkup' =>
  	"<li>
  	<h3>{title}</h3>
  	<p><strong>price: </strong><span>{price}</span>
  	<p><strong>category: </strong><span>{category}</span>
  	<p>{body}</p>
  	</li>"
));

But really, just use the foreach. Sorry for posting more confusing alternatives than anything. Do the foreach, it’s exactly what you want. You can write it in a single line if it feels cleaner.

I haven’t tested any of this, I’m just writing it into the comment box.

  • Like 1
Link to comment
Share on other sites

I tried with 

foreach ($items as $item)
    $out .= $item->render('somelayout.php')

as well, to no effect. Correct me if I'm wrong. This the one that uses a simple array. So I turned to MarkupPageArray, which is the name of the module, ->renderPageArray() is its method that works for PageArrays (as the module name claims). 

None of the above work for me but completely mess up my layout.

Just use foreach like you propose is not as simple as it sounds and the workaround that is needed to make the method that is build around normal php arrays work for wire arrays as well defeats the purpose of why I'm even here. I have a working solution with dozens of ifs and elses, just thought I could find a more elegant, scalable and straightforward solution instead.

Link to comment
Share on other sites

Right, so coming back to your original post, the items you want to render are ProcessWire Pages and they’re all in one PageArray, right? Now you want to render all of them but dynamically pick a markup file based on each item’s template. You can do this similar to the way LMD suggested, by creating a file for each template and naming it accordingly.

foreach ($items as $item) {
    $out .= $item->render($item->template->name . '_listitem.php');
}
echo $out;

I mean, this is basically the same as what you originally proposed, only using template files instead of functions, but the call_user_func way should work, too.

If your items are only ever viewed this way, you could even just use render() without any arguments.

Can you tell us more about the specific logic that goes into rendering each item?

Link to comment
Share on other sites

I know it looks and sounds simple but it doesn't work. As soon as I echo what I ->render(); the entire page breaks. It returns the entire page inside the <content> tag and inside that one's <content> tag it renders the entire page and so on. 

I'm using markup regions, I guess that's an issue…

Link to comment
Share on other sites

11 hours ago, fruid said:

I'm using markup regions, I guess that's an issue…

Good point. The render method will render the page according to all the usual settings, so for this use case you’ll need to disable any prepend/append files. If you have template caching enabled, you need to turn that off, too. You can call render with options like so:

foreach ($items as $item) {
    $out .= $item->render($item->template->name . '_listitem.php', ['prependFile' => null, 'appendFile' => null, 'allowCache' => false]);
}
echo $out;

 

  • Thanks 1
Link to comment
Share on other sites

Excellent question, because the documentation on it is a little stupid. The information is here: https://processwire.com/api/ref/page-render/render-page/. Although if you really want to see what’s going on, you can look at the code directly: https://github.com/processwire/processwire/blob/master/wire/modules/PageRender.module#L399.

One would expect this to show up in the documentation for the Page class, but there isn’t even a link…

The reason it’s a little convoluted is that the render method is added by a Module as a Hook, so I guess it doesn’t technically belong to the Page class.

Maybe it’s not really supposed to be used that way. You wouldn’t have all that trouble with append files and cache using LMD’s method. However, you would need to pass your $item to it somehow (for example $files->render('yourfile.php', ['item' => $item]);).

  • Like 1
Link to comment
Share on other sites

You’re in ProcessWire’s namespace, so if you paste the code you linked, those functions will actually be ProcessWire\exclaim() and ProcessWire\ask(). You can call them like this:

printFormatted("Hello world", 'Processwire\exclaim');
printFormatted("Hello world", __NAMESPACE__.'\ask');

Or alternatively, you can add the namespace in printFormatted:

function printFormatted($str, $format) {
    // Calling the $format callback function
    echo (__NAMESPACE__ . '\\' . $format)($str);
}

Or you could declare exclaim() and ask() differently. For example by putting them in a class or in an include file with a different/global namespace.

  • Like 1
Link to comment
Share on other sites

thanks @Jan Romero works like a charm.

I wonder if that is best practice to jump in and out of namespaces, but I guess it doesn't matter.

Also curious why that namespace declaration is necessary, cause I have created my own functions before, no problem. And, again, wondering how you know this and how I could know something like this without having to ask. 

Link to comment
Share on other sites

You’re not jumping in an out of namespaces in that sense (you could do that if you wanted to get really freaky). As long as you’re in the ProcessWire namespace, any function you declare belongs to that namespace. Since you’re inside the namespace anyway, you can call them without specifying ProcessWire\myFunction(), but apparently PHP’s dynamic features like call_user_func() need you to be more specific.

I don’t know this per se, it’s a combination of googling and talking out of my bum bum, lol, so I’m probably getting a good amount of technicalities wrong. Here’s a link to the PHP docs about it https://www.php.net/manual/en/language.namespaces.dynamic.php.

  • Like 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
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...