Jump to content

How do you implement custom page classes?


gebeer
 Share

Recommended Posts

Hello all,

Since https://processwire.com/docs/tutorials/using-custom-page-types-in-processwire/ came out, I used to implement custom page classes as modules, following the principles described in that tutorial.

Now only a few weeks ago I stumbled across https://processwire.com/blog/posts/pw-3.0.152/#new-ability-to-specify-custom-page-classes. This seems to me a much cleaner and easier way of implementation. Though it restricts the naming of custom classes to the naming conventions for the class loader. Other than that I can't really see any more disadvantages.

Which way do you prefer and why?

On a side note, useful features like described in the second link often can only be found in @ryans core update blog posts. If you don't read them on a regular basis, those new features are easy to miss. I'd love to see those hidden gems find their way into the API reference in more detail. Although $config->usePageClasses is documented at https://processwire.com/api/ref/config/, I think it would deserve its own page with all the explanations from the blog post.

  • Like 4
Link to comment
Share on other sites

Hi @gebeer,

Great point. I read those two articles back when they were published, but hadn't delved further because I haven't seen any forum posts regarding its use. I basically forgot about these two articles, so thank you for bring up the subject again.

I may have a couple of cases where page class would be beneficial to my projects. That being said, I would  be interested in seeing an example of your implementation of page classes as modules since I am more of a visual learner, if that is at all possible.

1 hour ago, gebeer said:

Which way do you prefer and why?

I've stuck with using modules simply because what I believe is ease of installation. I can distribute a module without manually adding to, or changing a user's directory structure as it is implied in Ryan's article. I'm curious how page classes could be included in a process and/or module distribution and installation.

I hope I made some sense as I am still a bit confused.

Link to comment
Share on other sites

/site/modules/MyModule

/site/modules/MyModule/classes
/site/modules/MyModule/classes/MyPageClassOne.php
/site/modules/MyModule/classes/MyPageClassTwo.php

/site/modules/MyModule/MyModule.module.php

The module:

<?php namespace ProcessWire;
class MyModule extends WireData implements Module {

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

  public function init() {
    $this->rm()->initClasses(__DIR__."/classes");
  }

  /**
   * @return RockMigrations
   */
  public function rm() {
    return $this->wire->modules->get('RockMigrations');
  }
  
}

The class:

<?php namespace ProcessWire;
class MyPageClassOne extends Page {
  
  public function init() {
    $this->wire->addHookAfter("Pages::saveReady", $this, "onCreate");
  }
  
  public function onCreate(HookEvent $event) {
    $page = $event->arguments(0);
    if(!$page instanceof self) return;
    if($page->id) return;
    $page->status = 1; // auto-publish this page
    $this->message('Auto-published page '.$page->path);
  }

}

initClasses() will not only load your custom page classes but also trigger the init() method once. That makes it possible to attach all kinds of hooks that belong to the custom page class where they belong: Into the page classe's php file. You avoid hook-hell in ready.php like this. It will load one instance of your page class on every request though.

You can also use custom namespaces easily:

// in the module:
$this->rm()->initClasses(__DIR__."/classes", "MyNameSpace");

// your page class:
<?php namespace MyNameSpace;
use ProcessWire\Page;
class MyPageClassOne extends Page {
  ...
}

You don't need to use RockMigrations for that. You can have a look what initClasses() does. You could also just set the page class on the template. I don't know if that can be done manually but using RockMigrations it is just setting one property of the template:

'my_template' => [
  'fields' => [...],
  'pageClass' => ...,
]

 

  • Like 5
Link to comment
Share on other sites

On 1/8/2022 at 2:57 PM, rick said:

I may have a couple of cases where page class would be beneficial to my projects. That being said, I would  be interested in seeing an example of your implementation of page classes as modules since I am more of a visual learner, if that is at all possible.

I published a generic module with some examples at https://github.com/gebeer/CustomPageTypes

Happy visual learning ?

  • Like 3
Link to comment
Share on other sites

On 1/8/2022 at 5:11 PM, bernhard said:

The module:

....
3 hours ago, gebeer said:

Happy visual learning ?

A picture (meaning a few lines of example code) is worth a thousands words :) Thanks for caring and sharing guys, btw.

  • Like 1
Link to comment
Share on other sites

  • 8 months later...
On 1/8/2022 at 4:11 PM, bernhard said:

That makes it possible to attach all kinds of hooks that belong to the custom page class where they belong: Into the page classe's php file

This looks really useful. For some reason I missed it at the time, but I thought I saw a tutorial covering it within the context of custom page classes generally - which I now can't find. Is there one @bernhard?

Link to comment
Share on other sites

That’s great @bernhard. I really think custom page hooks should be in the page class, not ready.php, which can get really messy. I have a couple of questions:

  1. why do you extend page rather than introduce an intermediate DefaultPage class as recommended by @ryan?
  2. how do you deal with pages that are repeaters (or repeaterMatrix)? Per a comment in the RepeaterMatrix forum, custom pages classes don’t work with these as they have their own classes which descend directly from Page and extending those classes is not advised. @ryansuggests using addHookMethod on RepeaterMatrixPage (or, presumably on RepeaterPage for plain repeaters), but that would place all the code in ready.php. The approach I have adopted is to put the methods in the page class of the getForPage with an argument for the repeater page object. Any suggestions?
Link to comment
Share on other sites

1 hour ago, MarkE said:

why do you extend page rather than introduce an intermediate DefaultPage class as recommended by @ryan?

Where's that recommendation?

1 hour ago, MarkE said:

how do you deal with pages that are repeaters (or repeaterMatrix)? Per a comment in the RepeaterMatrix forum, custom pages classes don’t work with these as they have their own classes which descend directly from Page and extending those classes is not advised. @ryansuggests using addHookMethod on RepeaterMatrixPage (or, presumably on RepeaterPage for plain repeaters), but that would place all the code in ready.php. The approach I have adopted is to put the methods in the page class of the getForPage with an argument for the repeater page object. Any suggestions?

I'm not sure I understand the question. Do you have an example or use case?

  • Like 1
Link to comment
Share on other sites

42 minutes ago, bernhard said:

Where's that recommendation?

Well, more of a suggestion than a recommendation 

Quote

Your custom classes can use PHP object inheritance, enabling you to maintain Page objects that build upon one another. For instance, your BlogPostPage class could extend your DefaultPage class rather than the Page class, and so on.

in https://processwire.com/blog/posts/pw-3.0.152/#custom-page-classes-vs-hooks

However, it struck me as a good idea since any custom methods for all pages can go in DefaultPage and will be available to any other custom page classes. If, for some reason, you don't want to include the method in a custom page class, you can either declare it in the subclass or (to exclude them all) extend Page. Much better than adding methods to Page via hooks, no? So my practice is always to include a DefaultPage class, even if I end up putting nothing in it.

48 minutes ago, bernhard said:

I'm not sure I understand the question. Do you have an example or use case?

Absolutely, but it is a rather large app, so difficult to encapsulate. The context is the same as the worked example here (which I will repeat for ease of reading):

Quote

The context is my new web-based app for cider production and sales management "CiderMaster". Among other things, this has a template - 'Cider' - for recording all stages of production and use. The stages are recorded in a repeaterMatrix field called 'stage'. Two such stages are 'blend_from' and 'blend_to' to record the blending of (part of) one cider to another. 

There is CiderPage.php containing: class CiderPage extends DefaultPage. This contains a lot of methods, but one simple one is:

	/**
	 * Get the stage sequence as it is actually stored in the page array - this may be different from the 'sort' property
	 *
	 * @param $stage
	 * @return false|int|string
	 */
	public function getStageSequence($stage): bool|int|string {
		$sort = array_search($stage->id, $this->stage->explode('id'));
		return $sort;
	}

Ideally, I would have something like RepeaterStagePage class with method:

	/**
	 * Get the stage sequence as it is actually stored in the page array - this may be different from the 'sort' property
	 *
	 * @return false|int|string
	 */
	public function getStageSequence(): bool|int|string {
		$sort = array_search($this->id, $this->getForPage()->stage->explode('id'));
		return $sort;
	}

Admittedly, this is not any less code, but at least it would sit in the right place. For example, the first method would be less good if the stage field was used in a two different templates (which it is in fact, but the class of one -JuicePage - extends CiderPage, so it is not a problem in my case).

Link to comment
Share on other sites

39 minutes ago, MarkE said:
1 hour ago, bernhard said:

Where's that recommendation?

Well, more of a suggestion than a recommendation 

Quote

Your custom classes can use PHP object inheritance, enabling you to maintain Page objects that build upon one another. For instance, your BlogPostPage class could extend your DefaultPage class rather than the Page class, and so on.

in https://processwire.com/blog/posts/pw-3.0.152/#custom-page-classes-vs-hooks

Thx. He is just showing what you CAN do, if you want to. But there's no need for doing that. You could for example create a specific page class for your project that all custom page classes extend on. That could be helpful to share some common business logic. Let's say we had a project FOO and we had custom page classes EVENT and NEWS. Maybe both EVENT and NEWS share a common method that returns the slogan of our FOO project, which is "FOO is a great project!".

You could create a FooPage:

class FooPage extends Page {
  public function slogan() {
    return "FOO is a great project!";
  }
}

and then if you extend that FooPage rather than the PW Page from your EVENT and NEWS pageclasses you can access the slogan from both:

class EventPage extends FooPage {...}
class NewsPage extends FooPage {...}

$event = new EventPage();
echo $event->slogan();

$news = new NewsPage();
echo $news->slogan();

Now if you wanted to change the slogan you'd just need to change the slogan in FooPage and all other classes would reflect that change.

You can also use traits for a similar thing which is even more flexible since you can let your classes use multiple traits. That's what I'm doing with my RockMigrations MagicPage to make those pages have (and also trigger) init/ready/migrate methods. But still they are just extending the "Page" class.

39 minutes ago, MarkE said:
1 hour ago, bernhard said:

I'm not sure I understand the question. Do you have an example or use case?

Absolutely, but it is a rather large app, so difficult to encapsulate. The context is the same as the worked example here (which I will repeat for ease of reading):

I'm still not sure I fully understand but I'm not using Repeaters or RepeaterMatrix at all nowadays, so I can't say much about them. The problems you are facing might just be another reason why I'm avoiding them ? But I might be wrong. I don't know ? I've built several apps based on Repeaters and I always got into trouble later. So I stick to the fundamentals, which are Pages rather than repeater items.

Another example would be RockMatrix, where all items are just Pages and all have a common base class that they extend. So that could also be an example of the first question I answered above.

  • Like 2
Link to comment
Share on other sites

2 hours ago, bernhard said:

I've built several apps based on Repeaters and I always got into trouble later. So I stick to the fundamentals, which are Pages rather than repeater items.

That explains a lot! I have LOTS of repeaters in this new app as it seemed a really neat idea from the point of view of the admin gui. Then I ran into LOTS of problems with dependent page selectors, which I think I’ve now fixed, either by contributions to the core or by the new CustomDependSelects module. I have wondered whether it would have been wiser to use plain pages. RepeaterMatrix does seem to be very popular (and I can see why), particularly for a UI for web page blocks. Other contributions on the “repeaters or no” discussion would be helpful. 

  • Like 1
Link to comment
Share on other sites

3 hours ago, MarkE said:

Other contributions on the “repeaters or no” discussion would be helpful. 

Just my two cents: used to avoid Repeaters as well, because in the beginning they were quite unstable, but from my point of view they have matured nicely. For years I/we have used them a lot — in fact nearly all sites we've built last couple of years have used a Repeater Matrix based page builder. Can't remember the last time I had an issue directly related to repeaters ?‍♂️

That being said, I do try to keep things simple, just in case. Field dependencies is an example of a feature I've not tried with repeaters ?

  • Like 1
Link to comment
Share on other sites

Just to clarify... I didn't want to make repeaters bad. RockMatrix is based on repeaters and it is absolutely great (for flexible page building). But what I found is that Repeaters are not my first choice when it comes to storing data. For example I've built a survey tool where I saved all the votes in a repeater field of the survey template. That sounded good because every vote belongs to the survey and in the admin gui you can easily inspect all the votes and you can easily loop through all of them via foreach($page->votes as $vote)...

So far so good, but then I needed to do complicated calculations and it turned out that would have been a lot easier and more efficient if all the votes had been pages under one parent. Selectors would have been easier, calculations could have easily done in SQL rather than looping all the results in PHP (because you need the parent/child relationship which is easily available in PHP/in memory but not so easy to get from the DB/SQL).

What I said might not be 100% accurate and there might have been good solutions even with repeaters, but I had troubles back then and I now have to tools to display all kinds of page data easily and in a great way using RockGrid. Because that's the other side of the coin: If you store everything in pages under one parent it might need a little more thinking where and how you present things. That's obviously easier with repeaters...

  • Like 1
Link to comment
Share on other sites

Short question: Is it possible to add custom page classes to repeaters?

I'm trying to do this with method but without any luck:
 

Repeater Field Name: my-repeater
File: /site/classes/RepaterMyRepeaterfield.php
<?php namespace ProcessWire;

class OwnRepeaterPage extends RepeaterPage {
    public function test() {
        return 'Test';
    }

};

Any ideas or hints?

Link to comment
Share on other sites

6 hours ago, DV-JF said:

Short question: Is it possible to add custom page classes to repeaters?

I'm trying to do this with method but without any luck:
 

Repeater Field Name: my-repeater
File: /site/classes/RepaterMyRepeaterfield.php
<?php namespace ProcessWire;

class OwnRepeaterPage extends RepeaterPage {
    public function test() {
        return 'Test';
    }

};

Any ideas or hints?

The file name and class name are important here. Otherwise PW doesn't know which pages to apply the custom class to. The file name is derived from the template name. See here https://processwire.com/blog/posts/pw-3.0.152/#a-more-practical-example

Now you need to find out what the name of the template is, that your repeater pages use behind the scenes. Usually the template name for repeater pages is "repeater_" + fieldname. So if your repeater field is named "my_repeater", the template name would be "repeater_my_repeater". You can look up the template name under Sytem->templates Filters->Show System Templates.

Following all that logic in my example, the file name should be Repeater_my_repeaterPage.php and the class name  Repeater_my_repeaterPage.
I haven't tried this out and I'm not 100% sure about the naming because of the underscores. But in the documentation it isn't mentioned that underscores are converted to CamelCase. So theoretically it should work.

Also make sure that custom page classes are enabled in config.php with $config->usePageClasses = true;

EDIT: I just saw in https://github.com/processwire/processwire/blob/3acd7709c1cfc1817579db00c2f608235bdfb1e7/wire/core/Templates.php#L853 that underscores are converted to CamelCase, too.

So in my example class name would be RepeaterMyRepeaterPage and file name RepeaterMyRepeaterPage.php

Link to comment
Share on other sites

Hey @gebeer

thx for your hints. I corrected my code and tried it again but it doesn't seam to work - only "normal" pages are working:

ProcessWire 3.0.200 © 2022 

782535642_2022-09-1408_34_45-HeroSectionPage.php-elements.vieregg.design-VisualStudioCode.png.e696a3beef461099386a5515a3b7fef8.png1506100205_2022-09-1408_21_41-RepeaterHeroRepeater.php-elements.vieregg.design-VisualStudioCode.png.32ceb92d9db07dbdb4e86633231d3f5c.png


1986473975_2022-09-1408_26_45-BearbeiteSeite_Heroelements.vieregg.designMozillaFirefox.thumb.png.e011d16d42948872d6fb0634bd1a734c.png

Can you @gebeer or someone else ( perhaps @ryan) confirm that you can implement custom page classes for repeater pages in this way?

Link to comment
Share on other sites

42 minutes ago, DV-JF said:

Can you @gebeer or someone else ( perhaps @ryan) confirm that you can implement custom page classes for repeater pages in this way?

Can't confirm that. But one more thing you could try is to extend Page and not RepeaterPage. Just a wild guess but maybe it helps.

Link to comment
Share on other sites

1506100205_2022-09-1408_21_41-RepeaterHe

Just a guess, never tried that, but if that worked than only if you call it RepeaterHeroRepeaterPage (both the class and the file)

Template "home" --> "HomePage"

1986473975_2022-09-1408_26_45-BearbeiteS

Template "repeater_hero_repeater" --> "RepeaterHeroRepeaterPage"

You should definitely extend "RepeaterPage" as this might have methods that repeater pages have and need!

Link to comment
Share on other sites

39 minutes ago, bernhard said:

Template "repeater_hero_repeater" --> "RepeaterHeroRepeaterPage"

Thx @bernhard for your suggestion, I've changed my code to

File: site/classes/RepeaterHeroRepeaterPage.php
<?php namespace ProcessWire;

class RepeaterHeroRepeaterPage extends RepeaterPage {
    public function test() {
        return 'Test';
    }

};

but no success ?

Are there any other ways to set a custom page class to a repeater page?

Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

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