Jump to content

Run code after page save


Pete
 Share

Recommended Posts

Hi folks

Is there any way to run some external PHP code after a page is saved (or even have this as part of a module if that's easier?).

The scenario is this - I'd like news articles for a site to be entered into PW. I'd like comments for the news articles to be posted in a forum. I've got PW for the news side of things, the forum software for the commenting, and I've also got a lovely PHP class available with lots of nifty functions including the ability to create a new topic in a news forum on the site.

This PHP class also gives me the ability to pull replies to a topic so that in my PW template for news articles I could list anything from the number of replies to a full-blown list of replies like you see in a blog.

What I need to do is somehow run some code after a page is saved (lets assume I've got a News page and all pages under it are news articles - makes sense ;)), that would then allow me to use this PHP class to check if a topic exists for this news article and, if not, create one for me.

Since I know the PHP class pretty well, the only thing I'm unsure of is how to run some code after a page save.

What would also be nice, but less essential right now, is the ability to run another bit of code after a page is deleted so that if a news article is removed then the related topic in the forums is also removed.

Link to comment
Share on other sites

I'm not much of a coder, so i could be wrong, but I think you can make a module that runs custom code after each page save. A lot of things in PW are hookable. Page save is one of them.

In the module i would check if the page i'm saving is of the news article type (i.e. using news_item template)

If true, run your code after page save.

See https://github.com/ryancramerdesign/P21/blob/master/site-default/modules/Helloworld.module for some inspiration.

If what i'm saying is bs i 'm sure Ryan or a power user will come in and correct me. :)

Link to comment
Share on other sites

See the code example on this page, which demonstrates running a hook after a page is saved:

http://processwire.com/api/modules/

You could also do the same thing when a page is deleted by hooking into the $pages->delete. method. There are a lot of these kind of hooks in PW. You can identify them by doing a grep for "___" (3 underscores) in PW's core or modules, as they all start with 3 underscores. Likewise, you can make any functions in your own modules hookable by preceding the function name with 3 underscores.

Link to comment
Share on other sites

Cheers guys - this is my first foray into modules and the API and it's proving to be very intuitive.

I'm happily testing if a hidden field that I'll store my forum topic ID in is empty - if it is then fire up the PHP class to create a topic in the forums, get that topic ID and save the ID to the hidden field.

It's nearly done, and I just want to do some other things like check if the page is published (on publish it should create the forum topic, then if it's unpublished for any reason it should hide the forum topic, and if the page is deleted then delete the topic).

The joy of working on something based off jQuery's way of doing things is that I also don't have to think too hard about how to do things either - the majority of the time tonight it's been pretty straightfoward :)

Link to comment
Share on other sites

Just a quick one for ryan - how well does PW scale? What's the most amount of pages you've had on a site and how do you cope in the admin when you get 200 sub-pages in a News section for example (is there pagination in the admin page tree? That would be awesome and I suspect I've seen it already this week and that's why it's popped into my head).

Link to comment
Share on other sites

Just running into a bit of trouble saving my page content via this module. I've set it to only run the module if the page's parent is the news page and with xDegug turned on on my XAMPP server it says it's stuck in a loop (without xDebug on it just crashes Apache).

Here's a very stripped-down version of my module with the issue:

<?php

/**
* ProcessWire 'Hello world' demonstration module
*
* Demonstrates the Module interface and how to add hooks.
* 
* ProcessWire 2.x 
* Copyright (C) 2010 by Ryan Cramer 
* Licensed under GNU/GPL v2, see LICENSE.TXT
* 
* http://www.processwire.com
* http://www.ryancramer.com
*
*/

class Newstopic extends WireData implements Module {

/**
 * getModuleInfo is a module required by all modules to tell ProcessWire about them
 *
 * @return array
 *
 */
public static function getModuleInfo() {

	return array(

		// The module's title, typically a little more descriptive than the class name
		'title' => 'News Topic Create', 

		// version: major, minor, revision, i.e. 100 = 1.0.0
		'version' => 100, 

		// summary is brief description of what this module is
		'summary' => 'Creates a topic for each new news article that is saved. Checks none exists currently.',

		// Optional URL to more information about the module
		'href' => 'http://www',

		// singular=true: indicates that only one instance of the module is allowed.
		// This is usually what you want for modules that attach hooks. 
		'singular' => true, 

		// autoload=true: indicates the module should be started with ProcessWire.
		// This is necessary for any modules that attach runtime hooks, otherwise those
		// hooks won't get attached unless some other code calls the module on it's own.
		// Note that autoload modules are almost always also 'singular' (seen above).
		'autoload' => true, 
		);
}

/**
 * Initialize the module
 *
 * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called
 * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. 
 *
 */
public function init() {

	// add a hook after the $pages->save, to issue a notice every time a page is saved
	$this->pages->addHookAfter('save', $this, 'createNewsTopic'); 

	// add a hook after each page is rendered and modify the output
	//$this->addHookAfter('Page::render', $this, 'example2'); 

	// add a 'hello' method to every page that returns "Hello World"
	//$this->addHook('Page::hello', $this, 'example3'); 

	// add a 'hello_world' property to every page that returns "Hello [user]"
	//$this->addHookProperty('Page::hello_world', $this, 'example4'); 

}

/**
 * Example1 hooks into the pages->save method and displays a notice every time a page is saved
 *
 */
public function createNewsTopic($event) {

	$page = $event->arguments[0];

	// Check it's a news article
	if ($page->parent_id == 5831) {

		// If the hidden news_topic field for this article is empty, then we'll create a topic
		if (empty($page->news_topic)) {
			echo "It's empty";
			$page->news_topic = 123;
		} else {
			echo "It's got a value";
			$page->news_topic = '';
			$page->save();
		}

		exit;
		//$this->message("Hello World! You saved {$page->path}."); 
	}
}

}

and here's the debug info from xDebug:

( ! ) Fatal error: Maximum function nesting level of '100' reached, aborting! in C:\xampp\htdocs\sc11\wire\core\Wire.php on line 229
Call Stack
#	Time	Memory	Function	Location
1	0.0035	392416	{main}( )	..\index.php:0
2	0.2985	7064064	ProcessPageView->execute( )	..\index.php:170
3	0.2985	7064216	Wire->__call( )	..\Wire.php:0
4	0.2985	7064216	Wire->runHooks( )	..\Wire.php:229
5	0.2985	7065264	call_user_func_array ( )	..\Wire.php:267
6	0.2985	7065400	ProcessPageView->___execute( )	..\Wire.php:0
7	0.3080	7109160	Page->render( )	..\ProcessPageView.module:73
8	0.3080	7109312	Wire->__call( )	..\Wire.php:0
9	0.3080	7109312	Wire->runHooks( )	..\Wire.php:229
10	0.3082	7112336	PageRender->renderPage( )	..\Wire.php:289
11	0.3082	7112536	Wire->__call( )	..\Wire.php:0
12	0.3082	7112536	Wire->runHooks( )	..\Wire.php:229
13	0.3082	7113584	call_user_func_array ( )	..\Wire.php:267
14	0.3082	7113768	PageRender->___renderPage( )	..\Wire.php:0
15	0.3102	7157328	TemplateFile->render( )	..\PageRender.module:194
16	0.3102	7157480	Wire->__call( )	..\Wire.php:0
17	0.3102	7157480	Wire->runHooks( )	..\Wire.php:229
18	0.3103	7158528	call_user_func_array ( )	..\Wire.php:267
19	0.3103	7158664	TemplateFile->___render( )	..\Wire.php:0
20	0.3117	7204976	require( 'C:\xampp\htdocs\sc11\site\templates\admin.php' )	..\TemplateFile.php:88
21	0.3122	7206648	require( 'C:\xampp\htdocs\sc11\wire\templates-admin\controller.php' )	..\admin.php:13
22	0.3130	7239504	require( 'C:\xampp\htdocs\sc11\wire\core\admin.php' )	..\controller.php:13
23	0.3264	7437216	ProcessController->execute( )	..\admin.php:42
24	0.3264	7437368	Wire->__call( )	..\Wire.php:0
25	0.3264	7437368	Wire->runHooks( )	..\Wire.php:229
26	0.3265	7438416	call_user_func_array ( )	..\Wire.php:267
27	0.3265	7438552	ProcessController->___execute( )	..\Wire.php:0
28	0.3436	8017272	ProcessPageEdit->execute( )	..\ProcessController.php:194
29	0.3436	8017424	Wire->__call( )	..\Wire.php:0
30	0.3437	8017424	Wire->runHooks( )	..\Wire.php:229
31	0.3437	8018472	call_user_func_array ( )	..\Wire.php:267
32	0.3437	8018608	ProcessPageEdit->___execute( )	..\Wire.php:0
33	0.4291	9419280	ProcessPageEdit->processSave( )	..\ProcessPageEdit.module:94
34	0.4475	9558616	Page->save( )	..\ProcessPageEdit.module:160
35	0.4475	9558784	Pages->save( )	..\Page.php:791
36	0.4475	9558984	Wire->__call( )	..\Wire.php:0
37	0.4475	9558984	Wire->runHooks( )	..\Wire.php:229
38	0.4509	9545040	Newstopic->createNewsTopic( )	..\Wire.php:289
39	0.6296	23635104	Page->save( )	..\Newstopic.module:103
40	0.6296	23635272	Pages->save( )	..\Page.php:791
41	0.6296	23635472	Wire->__call( )	..\Wire.php:0
42	0.6296	23635472	Wire->runHooks( )	..\Wire.php:229
43	0.6352	23644064	Newstopic->createNewsTopic( )	..\Wire.php:289
44	0.6370	23644512	Page->save( )	..\Newstopic.module:103
45	0.6370	23644680	Pages->save( )	..\Page.php:791
46	0.6370	23644880	Wire->__call( )	..\Wire.php:0
47	0.6370	23644880	Wire->runHooks( )	..\Wire.php:229
48	0.6392	23648016	Newstopic->createNewsTopic( )	..\Wire.php:289
49	0.6400	23648136	Page->save( )	..\Newstopic.module:103
50	0.6400	23648304	Pages->save( )	..\Page.php:791
51	0.6400	23648504	Wire->__call( )	..\Wire.php:0
52	0.6400	23648504	Wire->runHooks( )	..\Wire.php:229
53	0.6425	23651408	Newstopic->createNewsTopic( )	..\Wire.php:289
54	0.6441	23651856	Page->save( )	..\Newstopic.module:103
55	0.6441	23652024	Pages->save( )	..\Page.php:791
56	0.6441	23652224	Wire->__call( )	..\Wire.php:0
57	0.6441	23652224	Wire->runHooks( )	..\Wire.php:229
58	0.6461	23655296	Newstopic->createNewsTopic( )	..\Wire.php:289
59	0.6468	23655416	Page->save( )	..\Newstopic.module:103
60	0.6468	23655584	Pages->save( )	..\Page.php:791
61	0.6468	23655784	Wire->__call( )	..\Wire.php:0
62	0.6468	23655784	Wire->runHooks( )	..\Wire.php:229
63	0.6487	23658696	Newstopic->createNewsTopic( )	..\Wire.php:289
64	0.6502	23659144	Page->save( )	..\Newstopic.module:103
65	0.6503	23659312	Pages->save( )	..\Page.php:791
66	0.6503	23659512	Wire->__call( )	..\Wire.php:0
67	0.6503	23659512	Wire->runHooks( )	..\Wire.php:229
68	0.6522	23662584	Newstopic->createNewsTopic( )	..\Wire.php:289
69	0.6530	23662704	Page->save( )	..\Newstopic.module:103
70	0.6530	23662872	Pages->save( )	..\Page.php:791
71	0.6530	23663072	Wire->__call( )	..\Wire.php:0
72	0.6530	23663072	Wire->runHooks( )	..\Wire.php:229
73	0.6551	23665984	Newstopic->createNewsTopic( )	..\Wire.php:289
74	0.6567	23666432	Page->save( )	..\Newstopic.module:103
75	0.6567	23666600	Pages->save( )	..\Page.php:791
76	0.6567	23666800	Wire->__call( )	..\Wire.php:0
77	0.6567	23666800	Wire->runHooks( )	..\Wire.php:229
78	0.6586	23669872	Newstopic->createNewsTopic( )	..\Wire.php:289
79	0.6593	23669992	Page->save( )	..\Newstopic.module:103
80	0.6593	23670160	Pages->save( )	..\Page.php:791
81	0.6593	23670360	Wire->__call( )	..\Wire.php:0
82	0.6593	23670360	Wire->runHooks( )	..\Wire.php:229
83	0.6593	23671432	call_user_func_array ( )	..\Wire.php:267
84	0.6593	23671616	Pages->___save( )	..\Wire.php:0
85	0.6604	23673256	FieldtypeInteger->savePageField( )	..\Pages.php:421
86	0.6604	23673504	Wire->__call( )	..\Wire.php:0
87	0.6604	23673504	Wire->runHooks( )	..\Wire.php:229
88	0.6604	23674560	call_user_func_array ( )	..\Wire.php:267
89	0.6604	23674792	Fieldtype->___savePageField( )	..\Wire.php:0
90	0.6605	23674952	FieldtypeInteger->deletePageField( )	..\Fieldtype.php:504
91	0.6605	23675200	Wire->__call( )	..\Wire.php:0
92	0.6605	23675200	Wire->runHooks( )	..\Wire.php:229
93	0.6605	23676256	call_user_func_array ( )	..\Wire.php:267
94	0.6606	23676488	Fieldtype->___deletePageField( )	..\Wire.php:0
95	0.6606	23676520	WireData->__unset( )	..\Data.php:0
96	0.6606	23676520	WireData->remove( )	..\Data.php:169
97	0.6606	23676480	Wire->trackChange( )	..\Data.php:133
98	0.6606	23676632	Page->changed( )	..\Wire.php:504
99	0.6606	23676832	Wire->__call( )	..\Wire.php:0

This error message was shown because the site is in DEBUG mode.

Any ideas why it might be going crazy nesting functions like that?

Link to comment
Share on other sites

It may be obvious, but didn't you loop the function?

What you did: (as i suspect)

  • you hooked function to page::save[after]
  • you check whether page's parent is 5831
  • you edit it
  • you save it (means: go to point 2)

Some sort of one time block, or different 'run hook' condition is needed, otherwise this hook will go into this loop everytime you save() page with parent 5831 first time.

Link to comment
Share on other sites

I think Adam is right here. I would try this:

change addHookAfter -> addHookBefore

and remove $page->save (shouldn't be needed since you modify values before actual save.

Not sure though.

Link to comment
Share on other sites

Just a quick one for ryan - how well does PW scale? What's the most amount of pages you've had on a site and how do you cope in the admin when you get 200 sub-pages in a News section for example (is there pagination in the admin page tree? That would be awesome and I suspect I've seen it already this week and that's why it's popped into my head).

I think Ryan has build pretty big sites with PW, with lots of data, so i think it will scale well. Examples: http://www.tripsite.com/ and http://villarental.com/

There is pagination in the admin page tree. Log in to the skyscrapers demo site and take a look at the cities and architects pages. The pagination defaults at 50 items, plus there is a 'more' link that shows the next 50 items beneath the current 50 in one view.

http://processwire.com/demo/

For really big numbers of pages it could be a good idea to have another way of viewing and selecting. Maybe a grid-like view or something based on functionality of the new search functions.

Link to comment
Share on other sites

Thanks all - seems more obvious this morning than at 11.30pm last night ;)

Is there a way to just save that specific field for that page then instead of saving the entire page, as that's all I need to really do? That way I could still run the code on page save.

Thanks SINNUT - I thought I'd seen it somewhere but thought I might have imagined it. Very useful feature - in my MODx sites all I end up with is an ever-expanding list of pages in the admin with no pagination and it gets messy quickly so this is an awesome feature.

Link to comment
Share on other sites

Adam and Antti are right. When you are calling save() you are triggering an recursive/infinite loop. Why? Because every time you call $pages->save() your hook it getting executed once again. So in this case you'll want to do what Antti suggested which is to make your hook run before (rather than after). That way you can jump in before PW executes the save and set what you need, and let the original $pages->save() do it for you. Also, I'm assuming that "exit;" is there just for debugging...

In terms of scalability, this is exactly what PW is built for. I've used it on sites up to ~10k pages. Most sites I develop in it are 2k-8k pages and up to 10x that number of URLs. This is either a lot of pages, or not a lot, depending on your point of view. I think for most open source CMSs, this is considered large scale. But if you are working at Google or Facebook, then you would say this is small scale. :) PW hasn't been tested in the hundreds of thousands of pages scale yet. I imagine it would work just fine at that scale, but don't have the sites to prove it yet.

As you start to use PW at a larger scale, you do have to pay more attention to how you use it. For instance, calling $page->children() on a page that has 2,000 children is a bad idea because it'll be slow. Whereas calling $page->children('limit=25') is fast. Here's some more details on this: http://processwire.com/talk/index.php/topic,5.0.html

Another factor to consider as you go larger scale is caching. PW 2.1 comes with some strong caching features that you can define on a per-template basis (edit a template, set a cache time, and more options open up). This is for caching entire page renders. For caching smaller pieces, it's really good to use the MarkupCache module. For instance, on that VillaRental site, the top left "villa search" box is expensive to create since it has to to iterate through all the regions, destinations and vacation types. As a result, it is cached using the MarkupCache module, which re-creates it once per day, while the rest of the page may be uncached. More info on how to use the MarkupCache module is here: http://processwire.com/talk/index.php/topic,8.0.html

Link to comment
Share on other sites

Is there a way to just save that specific field for that page then instead of saving the entire page, as that's all I need to really do? That way I could still run the code on page save.

You could do $page->save('fieldname'), replacing fieldname with the name of the field you want to save. But unless we're talking about something different than above, you don't need to do any save() calls. Also, saving a single field isn't going to trigger your hook.

Link to comment
Share on other sites

Hi ryan

Thanks for that - I changed it to saving before and it worked. I think I was just suffering from late night coding tiredness and not seeing the rather obvious source of the recursion. 'Exit' was just there for debugging, though to be honest I could just have easily printed the message as per the code in the Hello World module - something I realised this morning.

Thanks for the info on the size of those sites. I knew they were big but it's hard at a glance to see exactly how big. I've not got any large (I'm with you in considering those large ;)) sites to develop yet - StrategyCore is only a few hundred pages at most at the moment - but I do have a few projects in mind that I'll move onto later this year that could potentially have a few thousand pages eventually so those figures are good to know.

Thanks as well for the info on that markup caching module. I was going to ask about that as some parts of a page will change very rarely. Does that list on the Villa rental site update itself after a new page is added that would affect it then, or is it literally only once a day? To my mind it would be useful if a page save could trigger rebuilding a list and re-caching that part of the template, but I'm not sure if it's possible now or easy to implement if it's not.

Link to comment
Share on other sites

Just for info I've got this all working now. The process goes something like this:

1) Before a page is saved, we check if it's published using if ($page->status >= Page::statusUnpublished) {

2) If not, then we don't need to create a news topic in the forums as the page is obviously brand new and not visible on the site yet, but we should update the forum topic if there is one (checks hidden news_topic_id field in the template, then updates the forum topic if there is an ID in that field)

2) If page is published, check there's a topic id in the news_topic_id field - if not, create one and if there is update the topic

3) If a page is deleted (to the trash) then remove the forum topic if there is one

Now, I ended up using the trash function instead of the delete function purely because I couldn't figure out how to run my module when the trash is emptied. Any suggestions on how I might convert this to run the module and execute the code below when the trash is emptied instead of a page being moved to the trash?

Here's my hook for when a page is saved and it's moved to the trash:

$this->pages->addHookBefore('trash', $this, 'deleteNewsTopic'); 

and here's my function that that calls:

public function deleteNewsTopic($event) {

	$page = $event->arguments[0];

	// Check it's a news article
	if ($page->parent_id == 5831) {
		require_once('IPBWI/ipbwi.inc.php');

		if (!empty($page->news_topic)) {
			$ipbwi->topic->delete($page->news_topic);
		}
	}
}

There's other code in there too, but this is just the few snippets for this particular part ;)

I tried changing 'trash' to 'delete' in the hook, but that doesn't work - probably because you're not viewing a page when you empty the trash... but I'm not sure how you would add a hook to the actual act of emptying the trash (and presumably then there would have to be code iterating through the pages being deleted, checking the parent ID for the relevant page parent (5831 above is a page called "news" and I bet there's an easier way to do that than remembering the page ID ;)), then checking the news_topic_id field and deleting topics from the forums as necessary.

Other than that the complete module I've got works perfectly and with so few lines of code too!

Link to comment
Share on other sites

Oops - it's an obvious issue in my code that I worked out this morning before I even switched my PC on. More late night tiredness getting in the way ;)

If it's in the trash, it's no longer relevant to check the page's parent - it's no longer under the news page :) instead I really should be checking for the page template ('news' in this case) so it doesn't matter where the page is on the site.

I'm getting there and learning the system as I go. Hopefully tonight I'll have a more comprehensive module because of this.

Link to comment
Share on other sites

Hehe, now I have the problem of when a page is moved to trash in the main Pages list rather than via the page editor itself my module won't run. Same problem if I empty the trash from that main page list.

I'll have a look for hooks for those later on :)

Link to comment
Share on other sites

Your solution here might be some Page::move hook -> detect whether new parent is Trash. Theoretically, you shouldn't change your pageTrash routine at all, just add another hook with this function to run.

Also, I'm not sure how exactly is the Page::move hook called or whether it exist... :D

Link to comment
Share on other sites

The functions in wire/core/Pages.php are:

  • find
  • save
  • saveField
  • trash
  • restore
  • delete

So I guess Restore is going to be the one used when moving a page out of the trash - that's one thing potentially solved at least.

Link to comment
Share on other sites

Hehe, now I have the problem of when a page is moved to trash in the main Pages list rather than via the page editor itself my module won't run.

Are you sure that your hook isn't being triggered when moved in PageList? I can't think of any reason why it shouldn't. But if you can confirm that's the case let me know because it would be a bug. Both the trash() and restore() functions are triggered by $pages->save() when it detects that the parent has changed to or from the trash.

There isn't currently a move() hook, though I'd be willing to add one to $pages if you all think it would be worthwhile. Currently, you can detect a move by hooking into $pages->save() and checking if the page's parent has changed:

<?php
if($page->parentPrevious) {
    // page has moved
    // $page->parentPrevious is the previous parent (Page object)
    // $page->parent is the new parent (Page object)
}

There also isn't currently a trash empty() hook either, though I'll also be happy to add one of these if you want it. However, when you empty the trash, $pages->delete() is called for every page in the trash, so an 'empty trash' hook might be irrelevant?

Link to comment
Share on other sites

Cheers ryan - I'll do some more tinkering, but it sounds like these should work if I can get my code working.

The code you just posted to detect previous parent - is the previous parent saved somewhere in the db then after a page is moved, or is this info available just before the page is saved? Just curious.

Link to comment
Share on other sites

The parentPrevious is only available until the page is saved, and it's a runtime thing, so it's not saved in the DB. Though a revisions module is underway that will be saving all previous states of pages, but we're a couple months away from that being ready. 

Of course, you can always save the previous parent info yourself if you need it. For runtime, you can set whatever you want to a page and it will act as a data container. It only saves data in the fields assigned to it, so there's no harm in setting some other random values to a page if you want to. So you could for instance set $page->parentPreviousPete = $page->parentPrevious and use it where you needed it. If you wanted it to save to the DB, then you would need to actually add a field to the page called parentPreviousPete.

Link to comment
Share on other sites

  • 1 year later...

OK, my first effort with modules and I've run into a similar infinite loop issue.

I'm writing a module which will allow me to automatically delete a number of existing pages and create a handful of new pages when a certain document is saved.

At present my code is very cut-down - it doesn't do anything as yet except create a new page when the one being edited is saved.

public function init() {
  $this->pages->addHookBefore('save', $this, 'pageSaved');
}
public function pageSaved($event) {
  $page = $event->arguments[0];
  $p = new Page();
  $p->template = 'basic-page';
  $p->parent = '/about/';
  $p->status = Page::statusUnpublished;
  $p->title = "This is the pagename";
  $p->save();
}

When I edit and save a page it loops endlessly on the save hook.

Apeisa mentioned you could get rid of the $p->save() and use the addHookBefore save hook- doing this results in no page save. I'm not following his logic there, can someone help me to understand what's happening here.

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