Jump to content
ryan

PW 3.0.152 – Core updates

Recommended Posts

This week we have some major improvements to our core date/time Inputfield, as well as a new ability to specify your own custom classes for Page objects. Read on for all the details and examples—

https://processwire.com/blog/posts/pw-3.0.152/

  • Like 18
  • Thanks 1

Share this post


Link to post
Share on other sites

Wow. Thank you Ryan. Great update.

  • Like 1

Share this post


Link to post
Share on other sites

Love the custom Page classes!

  • Like 8

Share this post


Link to post
Share on other sites

@ryan It looks like there are some issues with InputfieldDatetime. I have to fields 'publish_from' and 'publish_until'. None of them are configurated to prefill today's date if empty, but after the update, if one of the fields has value second one is prefilled with its value. The second issue is that after page save I cannot change the selected value, it shows that the date is changed but after page save it still show the previous date. The third issue is that Date Picker options are not applied to the field. For example if I set Date Picker option to "No date/time picker" on the first field second one will also have no date/time picker. 
Looks like there is something wrong with js initialization and first field configuration is applied to all other date fields on the page edit screen. Tested on two different sites. 

  • Like 3

Share this post


Link to post
Share on other sites
8 hours ago, adrian said:

Love the custom Page classes!

+1  🙂

@ryan Thanks for these great new features!

However, I am wondering if it is (or will be) possible to define where to store the files of custom classes. Some time ago, I started to organize files belonging to a given parent/children template relationship into their own dedicated directory. I explained it in a bit more detail here: https://processwire.com/talk/topic/23193-mvc-architecture-and-processwire/?do=findComment&comment=198246

For example:

articles-article-dir.png.3e9bc90d78087fd78d3f1f2b8ed1d629.png

I put this in /site/init.php for each template that needs code:

$templates->get('article')->filename = $config->paths->templates . 'pages/articles/article.php';

I even put the files of field rendering under /site/templates/pages/articles/fields/ and render them like this (note: it is not seen in the screenshot above...):

<?= $article->render('article_lead', '/articles/fields/lead')  ?>

I am asking for something similar so that I do not have to put class files into /site/classes/ , instead, I would like to store them alongside all the other related files, eg. in /site/templates/pages/articles/ or somewhere else when applicable (as it can bee seen in the screenshot, I also have a "global" folder for files not tied to a given parent/children template relation).

I find it particularly useful that I do not have to hunt for files when working on a given parent/children template relationship. By sticking to my simple naming conventions, files are are always just a click away and they are also easily "portable" form project to project.

  • Like 1

Share this post


Link to post
Share on other sites
1 hour ago, szabesz said:

However, I am wondering if it is (or will be) possible to define where to store the files of custom classes.

Technically it's already possible — you just need to tell WireClassLoader where it can find your custom Page classes. Just gave it a try, adding this to /site/init.php, and it seemed to work fine:

$classLoader->addSuffix('Page', __DIR__ . '/templates/page-classes/');

Now ProcessWire checks for Page classes from /site/templates/page-classes/. This is in addition to the default /site/classes/ directory, which still has precedence if a class is found from there.

  • Like 5

Share this post


Link to post
Share on other sites

Hi @szabesz

Interesting workflow. Just a sidenote from me: if you want to start right now, you may store the new page class files within your workflow structure and additionally maintain the given site/classes directory by just give it proxy files that include or require the files from your internal structure.

Just as a temporary workaround. And if Ryan cannot implement your request, it would be easy to use a script that scans and create / sync the proxy files directory for you in regard to portability.

@ryan Well done! 😉

  • Like 1

Share this post


Link to post
Share on other sites

Thank you @ryan this is an awesome update! Really looking forward to replacing lots of hook-chaos by well organized and easy to maintain PageClasses 🙂 👍

d8LypjF.png

TsK9ZJd.png

So far, so 🥰🥰🥰 BUT:

I've put together this quick testing module:

<?php namespace ProcessWire;
/**
 * Custom page classes for ProcessWire
 *
 * @author Bernhard Baumrock, 07.03.2020
 * @license Licensed under MIT
 * @link https://www.baumrock.com
 */
class RockPageClasses extends WireData implements Module {

  public static function getModuleInfo() {
    return [
      'title' => 'RockPageClasses',
      'version' => '0.0.1',
      'summary' => 'Custom page classes for ProcessWire',
      'autoload' => true,
      'singular' => true,
      'icon' => 'bolt',
      'requires' => [
        'ProcessWire>=3.0.152', // custom page classes update
      ],
      'installs' => [],
    ];
  }

  public function init() {
    $this->classLoader->addSuffix('Page', __DIR__ . '/PageClasses/');
    $file = $this->classLoader->findClassFile("HomePage");
    bd($file);
  }
}

Now I deleted the file in /site/classes/HomePage.php and added it to /site/modules/RockPageClasses/PageClasses/HomePage.php and this is the result:

UQoVmc1.png

Strangely, the file is found but not loaded for the HOME template!

PcgcqCN.png

Any ideas? I think 95% of my custom classes will live in a folder inside /site/modules 🙂 

PS: This is the HomePage class:

Spoiler

<?php namespace ProcessWire;
class HomePage extends Page {
  /**
   * Just for testing :)
   * @return string
   */
  public function whoami() {
    return 'I am a RockPageClasses HomePage!';
  }
}

 

 

  • Like 1

Share this post


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

$classLoader->addSuffix('Page', __DIR__ . '/templates/page-classes/');

Thanks for the tip! Do you maybe have a similar tip for how to specify a custom folder for field rendering files? Or a tip on how to shorten $article->render('article_lead', '/articles/fields/lead') ?

I dislike the idea of scattering related files in all these directories:

  • /site/classes/
  • /site/templates/
  • /site/templates/fields/

It is not just me who thinks that the old-school way of organizing files exclusively by type is not the smartest idea of all. I know that lots of developers have long been promoting it by using and recommending folders like "js", "css", "inc", "images", etc. in one parent folder to store them all, which is kinda ok for a small site, but storing logically related files is preferable right at beginning, because – when the need arises – refactoring for no other reason than to reorganize files is just a waste of time.

The worst example I've ever encountered was Magento 1 with its frontend-theme's files scattered all over the place, making it hard to remember where to look for.

Some related articles to show that I'm not alone:

Quote: "Unfortunately, as your project grows the traditional structure falls apart. Over time code becomes harder to find, the project is harder to maintain, and there is a lot of scrolling around the project to change code for a single feature. This is where the concept of feature folders come in handy."

  • Like 1

Share this post


Link to post
Share on other sites
Quote

It looks like there are some issues with InputfieldDatetime.

@Zeka Thanks, I think I tracked down the issue (caching issue that occurs only when multiple date fields) and have pushed a fix on the dev branch. 

Quote

I am wondering if it is (or will be) possible to define where to store the files of custom classes. Some time ago, I started to organize files belonging to a given parent/children template relationship into their own dedicated directory. 

@szabesz

From your /site/init.php you can set the location with $config->setLocation('classes', 'path/to/classes/'); for example $config->setLocation('classes', 'site/templates/classes/'); You can also use the method mentioned earlier by adding your own directories to the $classLoader. However, the more potential locations there are for something, the more overhead, so I would recommend sticking to the $config->setLocation() and keep all of your classes in one directory. This keeps everything fast and efficient. 

Quote

I am asking for something similar so that I do not have to put class files into /site/classes/ , instead, I would like to store them alongside all the other related files, eg. in /site/templates/pages/articles/ or somewhere else when applicable (as it can bee seen in the screenshot, I also have a "global" folder for files not tied to a given parent/children template relation).

If that's the case, then you would have to add locations with the $classLoader and accept the overhead of doing so (though it's probably negligible so long as things don't grow too much). But I also think with that kind of structure, things look isolated when it comes to reusability, limiting their usefulness. That may be fine for your use case, but as far as classes go, one of the primary purposes is reuse and extending one another where appropriate, so you ideally want to keep them together. 

Quote

Strangely, the file is found but not loaded for the HOME template!

@bernhard

ProcessWire preloads a few pages on every request, since they always get used. Preloaded pages are loaded as a group rather than individually, enabling it to boot faster. This happens before the init state for modules, so a module will not have the opportunity to specify a class for these preloaded pages, but a file in /site/classes/ will. See the $config->preloadPageIDs setting for a list of what those pages are. But essentially they are the homepage, admin root page, guest user+role, and superuser role. For your case, it sounds like only the homepage is a consideration here. I would suggest just asking PW to uncache the homepage at your module's init() state. This should force it to reload (when it's next requested) using your custom class. 

$homePage = $this->pages->cacher()->getCache(1); 
if($homePage) $this->pages->uncache($homePage); 
Quote

Do you maybe have a similar tip for how to specify a custom folder for field rendering files?

You can also use $config->setLocation() for this, i.e. $config->setLocation('fieldTemplates', 'site/templates/my-custom-directory/'); 

Quote

Or a tip on how to shorten $article->render('article_lead', '/articles/fields/lead')

I think this is fine if you are just doing it once or twice, but if you find you are repeating yourself, typing out "/articles/fields" or similar in multiple places, then you always want to abstract away stuff like that into a single location so that if you ever need to change it, you change it in just one place. PHP functions are ideal for this. This will be simple to do, but when it comes to the directory and file names that you are using, I recommend using the exact same names that you do with your templates and fields, which will make it a simple matter to abstract away repetitive code or locations into functions.

Quote

I dislike the idea of scattering related files in all these directories:

"All these directories" are 3 directories, 2 of which are for optional features that are disabled by default — the absolute minimum number of directories necessary to support these things. 

ProcessWire is focused in what is the most simple, efficient, familiar (as it relates to the admin), and maximizes reusability. It's not opinionated about it outside of those factors. 

For something like field-templates, a primary purpose of having them is so that you can reuse them across different templates (to avoid repeating yourself); otherwise they aren't that useful. For the structure that you appear to be using, it looks to me like your intention is to isolate them by template, so why have them at all? If the purpose is code/markup isolation (which is still worthwhile) then I think just using an include() or $files->render(); would be preferable, more efficient and flexible for your use case.  So that's what I'd recommend there. 

The same goes for the new page classes—if you are isolating these things in the way you've suggested, it'll be confusing for classes to extend one another or be shared across multiple templates, and it'll take more overhead for PW to find them. If you don't intend to use them like that, maybe they can still be useful, but not nearly as much so. So at that point I would reconsider whether you want/need custom Page classes. Basically, you are using a highly opinionated structure for your site files, and that can be a good thing because you've decided where everything goes and have a plan/system for how it will grow in terms of your file structure. ProcessWire is built to support a lot of different types of websites and applications, and at any scale. But it's not opinionated about this stuff beyond the bare minimum, precisely so that you can be as opinionated as much as you want with your own projects. That's why you are able to build a more complex structure for your files that suits your need, and also why it's also just as suited for others that might prefer a simpler structure in their projects.

There's also no need to feel obligated to use things like field templates or custom page classes just because they are there. The point of these features is to reduce redundancy and prevent you from having to repeat yourself. But you may have your own preferred ways of doing that—whether it's with functions, file/directory structure, or anything else, it's all perfectly fine in PW.  A template file receiving a $page is the only assumption needed in ProcessWire and everything else is optional. So use these things when they help you do that, and avoid using them when they don't. The reason these features are disabled by default is because not everyone needs them. Personally, the only time I use field templates is for multi-value fields with lots of properties, like repeaters, where I need to reuse the same output/markup across multiple template files. Though custom Page classes I'm already using quite a bit and likely will in most cases. 

  • Like 8

Share this post


Link to post
Share on other sites
Posted (edited)

@ryan Thanx for your help and explanation!

25 minutes ago, ryan said:

...one of the primary purposes is reuse and extending one another where appropriate, so you ideally want to keep them together. 

I understand that from the OOP point of view this is preferable, for example organizing the classes of ProcessWire as a system. However, we are dealing with frontend code in this case, which is not so often requires complex OOP implementation, at least in the case of my projects. Instead, I am seeking "portability", that is – for example – copy/pasting just one "articles" folder to another site in order to use it as the bases for the blog post section of that site. Features implemented in these files are somewhat different – but mostly similar – from project to project anyway, so I do not want to generalize anything in this regard. When generalization is needed, a custom helper module is a good choice, I think.

Sure, everyone has her/his own way of organizing, so having options to do things the way one prefers is great.

25 minutes ago, ryan said:

and accept the overhead of doing so (though it's probably negligible so long as things don't grow too much).

This can be solved by using cache – I guess – which might also be required for other reasons as well.

Edited by szabesz
typo
  • Like 1

Share this post


Link to post
Share on other sites
4 hours ago, szabesz said:

maybe have a similar tip for how to specify a custom folder for field rendering files? Or a tip on how to shorten $article->render('article_lead', '/articles/fields/lead') ?

Not without a hook, but this should work:

// Hook for site/ready.php that makes $page->render($fieldname) look for
// its template in site/templates/$templatename/fields/$fieldname.php.
// Specifying alternative files still works.
// Pass a path with a leading slash as $file to go directory from the templates dir.
$storedFieldTemplates = [];
$this->addHookBefore("Page::renderField", function(HookEvent $event) use($storedFieldTemplates) {
    $page = $event->object;
	$file = $event->arguments(1);
	$storedFieldTemplates[] = $this->config->paths->fieldTemplates;

	$this->config->paths->fieldTemplates =
		substr($file, 0, 1) == '/' ?
		$this->config->paths->templates :
		$this->config->paths->templates . $page->template->name . "/fields/";
});

$this->addHookAfter("Page::renderField", function(HookEvent $event) use($storedFieldTemplates) {
	$this->config->paths->fieldTemplates = array_pop($storedFieldTemplates);
});

Shortens it down to $article->render('article_lead', 'lead')

  • Like 4

Share this post


Link to post
Share on other sites
8 hours ago, szabesz said:

It is not just me who thinks that the old-school way of organizing files exclusively by type is not the smartest idea of all. I know that lots of developers have long been promoting it by using and recommending folders like "js", "css", "inc", "images", etc. in one parent folder to store them all, which is kinda ok for a small site, but storing logically related files is preferable right at beginning, because – when the need arises – refactoring for no other reason than to reorganize files is just a waste of time.

The replies above by Ryan and BitPoet pretty much covered the questions you raised, so just dropping a quick comment regarding this 🙂

I tend to fuss a lot over things like organising the project, so I'd say that I'm familiar with your concerns. In my opinion a sensible structure is a key design decision, and even more so when you're working with others. So yeah, I can't disagree with anything you've said here. Regardless, after giving this a lot of thought I ended up organising files by type — at least I think that's what you'd call it — in Wireframe. By this I mean that I've indeed bundled controllers in one directory, components in another, views in a third one, etc.

For me this just makes more sense:

  • When I add a new template and a view for it (assuming that it isn't already covered by some shared code, which is quite often the case), I'll probably start from the controller. I may copy-paste some parts from other controllers, or perhaps I'll extend another one if I have a very similar need at hand. Some controllers may use other controllers as sort of "subcontrollers", and often there are shared libraries as well.
  • After the "backend" is mostly done, I'll move on to the view in order to actually put those methods to use. In other words I tend to work in layers, going from backend to the frontend. (This is just the way I like to work, and I definitely don't assume everyone to prefer the same approach.)
  • Things like components or partials are made to be shared. If I only need a specific piece of code once — or in one template — then it probably doesn't need to be abstracted away. That'll just make things harder to grasp and add a tiny bit of overhead without actually providing much extra value.
  • Copying files from project to project has been a relatively rare need for me, so when it does come up, it doesn't matter much if I have to copy one directory, or perhaps a couple of files from a couple of directories. That being said: in an old environment this need did come up often (and the structure was pretty complex), so I ended up writing a script to automate it 🙂

It's true that the process of building a new site often involves moving back and forth between backend and frontend, but I still prefer this sort of structure over the alternatives. Also: I find it a tad easier to maintain after the site is already live, since often a specific change will only affect one "layer" of the site.

Anyway, this is largely a matter of preference 👌

3 hours ago, ryan said:

There's also no need to feel obligated to use things like field templates or custom page classes just because they are there. The point of these features is to reduce redundancy and prevent you from having to repeat yourself. But you may have your own preferred ways of doing that—whether it's with functions, file/directory structure, or anything else, it's all perfectly fine in PW. 

Exactly. When I first read about custom Page classes, my first thought was that this stuff is frigging cool... but I probably won't be using it anytime soon. I fully agree that it's a great feature, but in my case I've already solved it in a way that — in my opinion — fits my needs better. I may end up using this for some stuff eventually, but right now I don't have a need for it, and that's fine 🙂

  • Like 5

Share this post


Link to post
Share on other sites

Thanks for the update which seems to be a great addition ... Following the blog tutorial I have a some problems.
First of all, I'm not sure (I only know the basics of php OOP), but the summary() method should look like this:

return wire()->sanitizer->truncate($this->body, 300);

instead

return $this->sanitizer->truncate($this->body, 300);

In general, I mean the byline() method which does not return the created user correctly, Tracy Debugger show me:

PHP Notice: Trying to get property 'name' of non-object in ...\regular\site\classes\BlogPostPage.php:7

This is my code site/classes/BlogPostPage.php:

<?php namespace ProcessWire;

// classes/BlogPostPage.php

class BlogPostPage extends Page {

	public function byline() {
	  $date = wireDate('Y/m/d', $this->published);
	  $name = $this->createdUser->name;
	  return "Posted on $date by $name";
	}

	public function summary() {
	  return wire()->sanitizer->truncate($this->body, 300);
	}
}

This is home.php

<?php foreach($pages->find('template=blog-post, sort=-published') as $post): ?>
  <div class='blog-post'>
    <h2><?=$post->title?></h2>
    <p class='byline'>
      <?=$post->byline()?>
    </p>
    <p>
      <?=$post->summary()?>
      <a href='<?=$post->url?>'>Read more</a>
    </p>
  </div>
<?php endforeach; ?>

All with the latest ProcessWire 3.0.152 dev and Regular UIKIT profile
Can anyone confirm the issue or what I am doing wrong?

  • Like 1

Share this post


Link to post
Share on other sites

@ryan, I just fiddled with custom Page classes and hit the same issue as @rafaoski. I can access every Page property from methods in my custom class but not createdUser and modifiedUser.

<?php namespace ProcessWire;

class BasicPagePage extends Page {

	public function myMethod() {
		$creator = $this->createdUser;
		var_dump($creator);
		// Outputs:
		// NULL

		$creator = $this->get('createdUser');
		var_dump($creator);
		// Outputs:
		// object(ProcessWire\User)#214 (7) {
		//   ["id"]=>
		//   int(41)
		//   ["name"]=>
		//   string(5) "admin"
		//   ["parent"]=>
		//   string(26) "/processwire/access/users/"
		//   ["template"]=>
		//   string(4) "user"
		//   ["email"]=>
		//   string(15) "nobody@invalid"
		//   ["roles"]=>
		//   string(17) "(PageArray) 37|38"
		//   ["data"]=>
		//   array(2) {
		//     ["email"]=>
		//     string(15) "nobody@invalid"
		//     ["roles"]=>
		//     object(ProcessWire\PageArray)#189 (3) {
		//   ...
		// }
	}
}

Calling $page->createdUser->name from outside works, though. Inside the class, $this->createdUser never hits Page::__get().

  • Like 2

Share this post


Link to post
Share on other sites

@ryan There is one more issue that I faced after the update. 

I'm using "Multiple templates or parents for users" and after update when I try to add new user with my custom template I get

Can’t save page 0: /ua/authors/test/: Page using template “author” is not moveable. (Template::noMove) [/admin/access/users/ => /ua/authors/]

 

Share this post


Link to post
Share on other sites
On 3/10/2020 at 2:44 PM, Zeka said:

@ryan There is one more issue that I faced after the update. 

I'm using "Multiple templates or parents for users" and after update when I try to add new user with my custom template I get


Can’t save page 0: /ua/authors/test/: Page using template “author” is not moveable. (Template::noMove) [/admin/access/users/ => /ua/authors/]

 

If you switch advanced mode back on in config, go to the template Advanced tab and uncheck this right at the bottom you should be golden.

Share this post


Link to post
Share on other sites
4 minutes ago, cb2004 said:

If you switch advanced mode back on in config, go to the template Advanced tab and uncheck this right at the bottom you should be golden.

Thanks, I know it. The issue is that somehow it worked before update with this option checked. 

Share this post


Link to post
Share on other sites

I now have played with Custom Page Classes too. I successfully have created a cascading hierarchy for a sort of mvc system of mine:

I make massive use of templates, where most of them are simple clones. I do this to have bullet-proofed identification of specific pages, but give the clients the opportunity to change title, name, and also the ID, if he once deleted a specific page and created a new one. (Template family is set to onlyOne!)

All the public viewable pages are set to the same template filename (basic-page), which previously was my controller file with all the rendering stuff. Now it's content is reduced to 4 lines only, and all render stuff is included into custom page classes, whereas it now is much cleaner to read and follow compared to before, because now I can overwrite the basic render method in custom page classes for specific pages.

I created a BasicPagePage as base:

<?php namespace ProcessWire;

class BasicPagePage extends Page {

    public function renderSections() {
      // ... 
    }
}

This one is used from all other public pages and extended only where needed:

<?php namespace ProcessWire;

if(!class_exists('ProcessWire\BasicPagePage')) require_once(__DIR__ . '/BasicPagePage.php');
class LegalpagePage extends BasicPagePage {

    public function renderSections() {
      // override parent method here, if needed
    }
}

Here is a debug list of a few pages

Spoiler

image.thumb.png.8bc1fae9ae17542f007c0504104e7b26.png

The basic-page.php controller now:

<?php namespace ProcessWire;

// load header with navbar and footer with links, hamburger, scroll-to-top item, etc.
include(__DIR__ . '/includes/_header.php');
include(__DIR__ . '/includes/_footer.php');
include(__DIR__ . '/includes/_absolute-positioned-elements.php');

// call method in CustomPageClasses to render all public viewable sections of the current page
$page->renderSections();

 

  • Like 4

Share this post


Link to post
Share on other sites

Hey @ryan! The new custom classes are amazing, basically my dream feature for how I build sites. Thank you!

Just wanted to ask about an issue I ran into. Like others I started playing with this as soon as I could. I ran into some issues that I wanted to check if they were actual "issues", intended behavior or maybe just "not ideal, but currently not solvable"? Currently two things I ran into:

1)  Repeater and Repeater Matrix pages can't use DefaultPage methods. This one makes sense to me as I imagine it's as simple as, the don't extend DefaultPage, but if they can maybe they should? Not sure. Basically I have some simple helper methods that were previous set as hooks, now when using them in a loop through a Matrix field I get Exception: Method RepeaterMatrixPage::summarize does not exist or is not callable in this context . I can get around this by calling $page->parent->summarize() so it's not critical, but it's certainly not intuitive when looking at the code why I'm calling on the parent instead.

2) I have a fairly complex Recurring Schedule system in the site I'm working on. I moved a "getFormattedSchedule" method to the EventPage class, in most cases it works brilliantly, and certainly allowed for cleaner code in my opinion. I just discovered though if I want to use the method in a Page::saveReady hook in order to cache the formatted results I get basically the same error as the above "not callable in this context". In this situation I find it a bit more confusing since I would assume the classes would be loaded before hooks?

Scrap #2, that one was on me. Still would love to see the below life-cycle doc, so I'll leave it for some context.

Somewhat per #2, do you have a detailed overview of the ProcessWire lifecycle anywhere. If not I'd love to see one eventually, often when developing more complex systems using PW I forget the exact order everything is loaded, accessed, etc. Would be nice to have something as clean and easy to refer to as the PW API docs. (Bravo on those too by the way, best in class for sure).

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.

×
×
  • Create New...