Jump to content

New blog: All about custom page classes in ProcessWire


Recommended Posts

Posted

Awesome! Want more of those!

‐--

Custom page classes really aren't a "view" layer in any form, and I suggest keeping all markup generation code within /site/templates/ and subdirectories within it.

Would love to know more about how you implement the view layer. Components with several views.. Controllers for url segments and stuff like that... 

  • Like 2
Posted

In line with this topic, I recently expanded AutoTemplateStubs to include ProFields Stubs. There should be stubs for FieldtypeTable, FieldtypeTextareas, FieldtypeCustom and RepeaterMatrix (including stubs for all types).  I haven't created a PR yet because I wanted to test it myself first, but since it fits so well with the blog post, if anyone would like to help with testing: https://github.com/phlppschrr/AutoTemplateStubs

  • Like 5
Posted
Quote

"Custom page classes really aren't a "view" layer in any form, and I suggest keeping all markup generation code within /site/templates/ and subdirectories within it."
 Would love to know more about how you implement the view layer. Components with several views.. Controllers for url segments and stuff like that... 

@Ivan Gretsky I'm always a little reluctant to make a blanket statement like "avoid markup in page classes", but I'm referring to what I think works best with the projects I work on. The files in /site/templates/ are the view layer, as nearly all code in there is aimed at generating markup/output. Even if something isn't directly generating markup, it's still finding and preparing things for output. Most markup comes from "partials", which are files that I put in /site/templates/parts/, or if exclusive to a particular template, then /site/templates/[template]/. And then I either include() them, or files()->render() them from the site's template files.

I primarily use Markup Regions. The _main.php establishes the base markup:

<?php namespace ProcessWire; 
/** @var Page $page **/
?><!DOCTYPE html>
<html>
<head id="html-head">
  <?php include('./parts/html-head.php');?>
</head>
<body id="html-body">
  <header id="header">
     <?php include('./parts/header.php');?>
  </header>
  <h1 id="headline"><?=$page->title?></h1>
  <main id="content">
    <?=$page->body?>
  </main>
  <aside id="sidebar" pw-optional>
  </aside>
  <footer id="footer">
    <?php include('./parts/footer.php');?>
  </footer>
</body>
</html>

Below is a template file for the /products/ page which lists product pages, supports pagination, and uses URL segments for sorting:

<?php namespace ProcessWire; // products.php

/** @var ProductsPage|CategoryPage $page */

$products = findProducts($page); 
$body = input()->pageNum === 1 ? $page->body : '';
$headline = $page->get('headline|title'); 

?>
<h1 id="headline"><?=$headline?></h1>

<main id="content">
  <?=$body?>
  <?php include('./parts/sorts.php'); // no expects ?> 
  <?php include('./parts/products-list.php'); // expects $products ?> 
</main>

<aside id="sidebar">
  <?php include('./parts/categories-list.php'); // no expects ?> 
</aside>

The category template file works exactly the same way, except that it lists products for the category rather than listing all products.  The same code works either way, so "category.php" just includes "products.php":

<?php namespace ProcessWire; // category.php
include('./products.php'); 

There's that findProducts() function above in the products.php template file -- I usually have helper functions in a /site/templates/_func.php, /site/templates/_products.php, or /site/templates/products/func.php (assuming exclusive for "products"). Another place would be for the ProductsPage and CategoryPage to have findProducts() methods, but usually I don't want the page classes getting involved with detecting stuff about the current request (sort, pageNum, etc.) so like these in a simple function library file: 

<?php namespace ProcessWire;
// file site/templates/_func.php included by _init.php

function getSorts(): array {
  return [ 
    'name' => 'A-Z', 
    '-name' => 'Z-A', 
    'price' => 'Price (low-high)', 
    '-price' => 'Price (high-low)', 
    'created' => 'Date added (oldest)', 
    '-created' => 'Date added (newest)'
  ];
}

function getSort(): string {
  $sorts = getSorts();
  $sort = input()->urlSegment('sort-(*)'); 
  if(empty($sort)) $sort = 'name';
  if(!isset($sorts[$sort])) wire404('Invalid sort'); 
  return $sort;
}

function findProducts($page, $limit = 20): PageArray {
  $sort = getSort();
  $find = "template=product, sort=$sort, limit=$limit";
  if($page instanceof CategoryPage) $find .= ", categories=$page";
  return pages()->find($find); 
}

Here's an example of a ./parts/products-list.php file:

<?php namespace ProcessWire; 

// file: parts/products-list.php
/** @var PageArray|ProductPage[] $products */ 

$subhead = $products->getPaginationStr('Products'); 
$pagination = files()->render('parts/pagination.php', [ 'items' => $products ]); 

?>
<h3><?=$subhead?></h3>
<?=$pagination?>
<ul class="products-list">
  <?php foreach($products as $product): ?
    <?php include('./parts/products-item.php'); // expects $product ?>
  <?php endforeach; ?>
</ul>

And the parts/products-item.php, though in reality there would likely be more to it: 

<?php namespace ProcessWire; 

// file: parts/products-item.php
/** @var ProductPage $product */

?>
<li class="products-item">
  <h3><?=$product->title?></h3>
  <p><?=$product->summary?></p>
  <p><a href="<?=$product->url?>">View Details</a></p>
</li>

To complete it, here's the parts/sorts.php file:

<?php namespace ProcessWire;
// file: parts/sorts.php

$currentSort = getSort();
$url = page()->url;
$sorts = [];

foreach(getSorts() as $sort => $label) {
  if($sort != $currentSort) $label = "<a href='{$url}sort-$sort/'>$label</a>";
  $sorts[] = $label;
}

echo "<p class='sorts'>Sort by: " . implode(' / ', $sorts) . "</p>";

If I start needing to output products in more places in the site, then I'll usually do fewer include()'s and move the rendering to dedicated functions. That way these things render in their own variable namespace and don't bleed variables or overwrite variables in the main rendering. So this would also go in that _func.php (or _products.php or ./products/func.php) mentioned above, and the include() calls in template fiels would be replaced with render...() calls:

function renderProducts(PageArray $products): string {
  return files()->render('parts/products-list.php', [ 'products' => $products ]); 
}

function renderCategories(): string {
  return files()->render('parts/categories-list.php'); 
}

function renderPagination(PageArray $items) {
  return files()->render('parts/pagination.php', [ 'items' => $items ]); 
}

So if using render() functions then the <main> with the include('./parts/products-list.php'); would get replaced with this: 

<main id="content">
  <?=$body?>
  <?=renderProducts($products)?>
</main>

 

Quote

And about Repeater Matrix page classes. There were discussions to have custom classes for RM types... Is this possible?

Ah yes, I hadn't thought about that in a long time. I can't remember if that was implemented yet or not. I'll find out. If not yet implemented I'll have to implement it, it should be fairly simple. 

  • Like 3
Posted

@gebeer That's an excellent summary, thanks! 

On the core-patterns one, the 'getExcerpt' example probably isn't ideal because it's only returning an excerpt if output formatting is on, otherwise it's returning the entire 'body' field with tags stripped out, which isn't an 'excerpt'. Usually a 'body' field is HTML (TinyMCE, CKE, etc.), so the formatted version would be the same as the unformatted version, unless there's some other formatters being applied on top of it, like TextformatterVideoEmbed, etc. There's an example of a getExcerpt() in the blog post that I think might work better, though I'm sure there are even better examples possible. 

In the same file, under API wire access, it says that pages() would not work in a Page class. But actually it would work just fine, so long as functions API is enabled. But what's preferable is $this->wire()->pages because it would be guaranteed to be tied to the correct instance (just in case multi-instance, even if rare). 

For calls like $this->wire('sanitizer') (where API var is in quotes) I'd suggest $this->wire()->sanitizer instead, just because the IDE will know that it's referring to the Sanitizer class, whereas if 'sanitizer' is in quotes then the IDE won't know, or at least it will have to do a lot more work to know. The same goes for any API variable. 

Lastly, do you think it could link to the blog post also, since that's the source for some of it -- It might help for folks looking for additional info? Thanks! 

  • Like 1

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...