WordPress.org

Ready to get started?Download WordPress

Forums

[resolved] Using one function for two scheduled events and a form (15 posts)

  1. Piotr
    Member
    Posted 7 months ago #

    I think this is going to be difficult to explain, but I hope someone will understand. ;)

    I'm building a plugin that queries data from a non-Wordpress database (a forum) and inserts it into WordPress as custom posts. Thanks to the help of bcworkz in this topic (Thx!) I managed to create a function that does this and scheduled an event that uses this function once every hour to import any new posts created on the forum or update any edited posts. It's alsp assigning different categories to posts from different forum sections - so, I can say I'm quite happy with my first WordPress plugin, but obviously I want it to do more. ;)

    What I want to do is to use WordPress options to change what the plugin does. I want it to do three things and I need to control some parts of them. Here's how it should work:

    1. A scheduled event will import/update ALL posts from SELECTED forum sections once a day.
    2. A second scheduled event will import/update a SELECTED NUMBER of latest posts from SELECTED forum sections once every hour.
    3. A form will let me execute the import function to update ONE SELECTED post (let's say in case it's older than the posts which are updated every hour).

    Scheduling events isn't a problem for me, neither is setting options - I already created options panel and I'm able to save options into WordPress database. What I don't know is how to pass these options into my function depending on which event is fired. I still consider myself a newbie in PHP, so maybe the solution is simple and I just can't see it. What I think I have to do is to use variables in the WHERE clause of SQL SELECT statement of my import function. Right now it looks like this:

    WHERE forums.id IN (61, 62, 63) ORDER BY topic.id DESC LIMIT 5

    I think in case of my daily event it should look like this:
    WHERE forums.id IN ($forum_ids) ORDER BY topic.id DESC
    This will let me change or add forum sections without editing the code if my forum would have to be reorganized somehow in the future.

    And in my hourly event it should look like this:
    WHERE forums.id IN ($forum_ids) ORDER BY topic.id DESC $limit
    So I could change the number of updated posts.

    The third case is different and it's not the WHERE clause that is the biggest problem - it will be one topic.id. The problem is it appears I can't use the Setting API to execute a function. I would have to connect a form in my admin panel to my import function so that when I type in the ID and hit the submit button, the function is executed - but again, I don't know how to do it.

    So in short, I need to know how to pass different number of options (one in case of the daily event, two in case of the hourly event and again one in the third case) and how to do it. Unless the whole idea is wrong, and there is a better way. I also need to know how to create the form that executes my plugins function.

    I'll be grateful for any advice.

  2. bcworkz
    Member
    Posted 7 months ago #

    Hi Piotr! Sounds like you've been busy and have made good progress, well done!

    Getting values from the options table where settings are stored and placing the values in a query string is fairly straight forward. You use get_option() and assign it to a variable, like so: $forum_ids = get_option('forum_ids');

    Depending on exactly how this data is structured, you may be able to use the variable directly, or you my need to convert it into a format compatible with mySQL. If you data was stored as a comma delimited string such as: "12,23,32,34"you can use the string value in mySQL directly. If your data was in array form:array(12,23,32,34,)you need to useimplode()` to convert it into the comma delimited string.

    Once you've gotten the data in the correct form, you can use the variables in the query string just as you had done in your examples. Don't forget to use double quotes to delineate the query string so that the variables get expanded into their values. This will not happen if single quotes are used.

    Executing a command from the settings page is a fair bit more involved. The technique though is used quite often in programming, so it's a skill well worth learning. I'm referring to AJAX techniques. You can send commands from the settings page and the server can report the results on the same page without any need to reload the current page.

    You will need some content on the settings page unrelated to the Settings API. If this is solely your own creation, this is no problem. If you've added onto another existing page, you'll need to find an action to hook onto in order to output some HTML

    The submit button is not a real submit button, you are not POSTing the form data like a submit button would do. It can be any HTML element capable of registering a click event. It can even be an inactive submit button. The click event triggers some jQuery code that sends a request to the server. The server does its assigned tasks and sends a response back to the jQuery function. jQuery interprets the response and displays something on the page accordingly. Thats AJAX in summary.

    You probably will not be surprised that it's very much more involved than my brief description indicates. You can start understanding AJAX by reviewing AJAX in Plugins. This document is quite confusing to novices unfortunately. The WP Documentation Team is attempting to rectify that with Plugin Developer Handbook. This is still a work in progress, but the AJAX chapter is mostly complete.

    I'm actually involved with Documentation as well as these forums. If you have any comments or suggestions regarding the Handbook, we'd love some feedback. Especially if there're particular sections you found confusing and/or needs improvement. You may either respond here or directly on the article page if you register for the "Make" site.

  3. Piotr
    Member
    Posted 7 months ago #

    Thanks for the reply. :) I knew that sooner or later I will have to learn about AJAX. I Really hoped it would be later, but that won't stop me. ;) Anyways, first I need to figure out how to use options in those scheduled events.

    You were right about placing values in my query string, it was very simple, I managed to do it some time after I posted the question. My real problem is that I need two different queries for two different events. I may have not been to clear about this, so I'll try again.

    My daily event needs to fire the import function to insert/update all my posts, and my hourly event should fire the function to insert/update let's say just 5 latest posts (this will be set by an option). So in case of the hourly event the query should have a limit, and in the daily event there should be no limit. How do I achieve this?

  4. bcworkz
    Member
    Posted 7 months ago #

    Oops. I neglected to address that aspect in my response, my apologies. There's a few ways to do this. You could create completely separate callback functions for each scheduled task. One function specifies a limit, the other does not.

    You could use one function with two scheduled tasks. Each scheduled task has the opportunity to pass arguments to your callback function. This is in the form of an array, even if it only contained a single value. For example, the argument could be the actual limit in string form. In the no limit case the argument could be an empty string. When the variable containing the empty string argument is expanded into the query string, it would be as if there was no variable there.

    A variation would be to store the limit in options so you could alter the value easily through settings. In this case the argument passed from the schedule task could be a simple boolean true/false. There is logic code in your callback to get the option in one case and to assign the empty string in the other case.

    You don't even really need two scheduled tasks. You could just run the hourly task and your callback could keep track of the current time. On any call where it's been more than 24 hours since the last daily run, it will do both the hourly and daily queries automatically. The daily timer is then reset and the cycle repeats. Of course only the hourly version has a limit, in the daily the empty string is assigned.

    If you really want to postpone AJAX, it would be possible to execute the special query without it. The resulting page loads make for a clunky user experience, but the possibility does exist. I think you realize AJAX will need to be part of your skill set at some point. Would you rather put the time in now and have a nice interface? Or postpone the inevitable and have a clunky interface? There's always decisions to make :)

  5. Piotr
    Member
    Posted 7 months ago #

    I was thinking about using two functions, but as you probably remember, importing posts in my case was quite complicated - inserting posts, terms, term taxonomies, modifying the data etc., is a lot of code. Using two such functions for almost the same thing didn't seem very efficient. So I chose to pass an argument. I'm handling it this way:

    if ( $arg[0] = 'limit' )
                $limit = "LIMIT " . $options['limit'];
            else
                $limit = " LIMIT 2";

    (I'm using the 'else' for testing, I didn't want to import all 900 posts just to see if this works. ;) )

    And it does work, yay! ;) But there's a problem. I'm trying to set a second event and for some reason I can't do it. Here's how I do it:

    if ( ! wp_next_scheduled( 'hourly_import', array( 'limit' ) ) ) {
    
                wp_schedule_event( time(), 'two_min', 'hourly_import', array( 'limit' ) );
            }
            add_action( 'hourly_import', array( 'News', 'news_import' ) );
    
    if ( ! wp_next_scheduled( 'daily_import' ) ) {
    
                wp_schedule_event( time(), 'five_min', 'daily_import' );
            }
            add_action( 'daily_import', array( 'News', 'news_import' ) );

    This in my class constructor. Obviously I'm using custom intervals to test these events, but only the first one is working. The daily_import event isn't even added - I checked _get_cron_array, only the hourly_import event is set. What am I missing? Is there something I don't know about adding multiple events or am I doing some, well... stupid mistake? There are no errors when I use the code.

    As for AJAX, yes, you are right. There would be no sense in postponing it at this point. I just need to find out how to get the schedules up and running - I'll need two or three more of these later on, as I'm planning to import some more data. And then I'll move on to AJAX. Thanks for the links.

  6. bcworkz
    Member
    Posted 7 months ago #

    You've wandered into an area I have little experience in, so I'm pretty much guessing. My best guess is you need to pass an argument through the daily_import action even though you don't need it. Even though you don't need it, News::news_import() is expecting one. It can be just an empty array, but something needs to be passed.

    If that doesn't work, my other ideas get illogical very quickly. First be sure wp_schedule_event() is being called, the if() condition may not be returning as expected. If there's a problem adding the same callback to two events, you could create a wrapper function that merely calls the other function.

    I'm assuming you correctly created custom intervals and didn't just make them up. Please don't be offended at the inference. I've done some pretty stupid things though I'm not stupid. You may have done similarly?

    Beyond that you'd have to do a detailed debug into the core function to determine where things fall apart. It's OK to edit core files if it's just temporary and you restore things when you're done.

  7. Piotr
    Member
    Posted 7 months ago #

    I still can't tell you what the problem was, it looks like you can't set up more events from a constructor. I have no idea why. So I used actions to call functions that set up those events and now both are there.

    I also made a surprising discovery - the schedules don't pass an array, only the value (at least when there's one argument). Earlier when I used optional arguments, I think in add_settings_field function, the whole array was passed. Why is it different here?

    And one final question, not really connected to this topic, but again, I just need some directions. While testing those schedules, I executed the import function with no limit. After importing about 90 posts it timed out. Not that I'm surprised, but I need to find a way around that. What are the best ways to deal with a long foreach loop?

    Thanks for the help!

  8. bcworkz
    Member
    Posted 7 months ago #

    About your array question. I'm not sure I understand. Do you mean the optional arguments parameter that is passed back to your callback function? You're saying that though you passed a single value as an array, it came back into your function as a single value, not an array? That is surprising!

    I don't really know, I would guess it is because scheduled events need to persist so are stored in the options table. Thus arrays must be serialized. It may be that in the serialize/unserialize process that single element serial arrays do not become arrays again. In the case of settings fields, the form is constructed on the fly and the arguments do not need to persist so are never serialized.

    If you're having time out issues, use set_time_limit( $secs ); to get more time. The default is 30 secs. There may not be much point in a value more than around 300 because there is a HTTP time limit that kicks in around then. You can use a value of zero to have no time limit. This can be dangerous if a script should fall into an infinite loop condition.

    Calling the function actually resets the timer, so you could just repeatedly call set_time_limit( 30 ); in your loop and still finish in the default condition.

  9. Piotr
    Member
    Posted 7 months ago #

    Hmm, I did some reading - what if the server timeout value is lower than I need? I mean the set_time_limit() will work if execution time limit in php,ini is the only problem.

    About the array - that's exactly what I mean. More than that, when I try to pass an array ( key => value ), only the value is passed. I didn't know what the problem was until I used print_r on the variable in my function. I know that I'm a newbie, but I think this isn't normal. ;)

  10. bcworkz
    Member
    Posted 7 months ago #

    I believe the default Apache timeout value is 300 sec., at least it is on my test server and I never changed it. You could change the value in httpd.conf or one of it's included files, mine's in extra/httpd.default.conf. If you do not have access to this file, I suppose you'll need to find a way to chunk the data into manageable blocks.

    Other ideas: If it's a one time thing, maybe you could ask you host to up the value for a couple days. Or: Import to a locally installed clone, then export the DB to the live version. I'm unsure if this will be faster or not, mySQL may have different limits, no idea at all on what that may be.

    OK, I could sort of see how a single array value could become just a simple value, but an associative array with a key? That's not right at all! Loss of the key name is destroying possibly important data. Wrong enough that I would consider it a bug. I will look into this further, but it will take some time. I'll let you know what I find.

  11. bcworkz
    Member
    Posted 7 months ago #

    OK. I've looked into the array/not array thing. The whole thing is pretty convoluted, the root cause is that in PHP 5.4, call time pass by reference is no longer allowed. Normally, you could still pass by reference in 5.4 if you hide the reference in an array. Most situations do not demand a true pass by reference ability anyway.

    In the case of WP scheduled events, it's simply not possible to pass by reference because the reference is converted to a value when it is serialized to store as an option. In the end, it's not exactly a WP issue, it's a PHP 5.4 issue. There's also a simple work around, it's actually rather silly. Go ahead and define your arguments array. Before passing it to the schedule event function, simply place it in yet another array.

    Your callback will be passed the first element of the array, your original array in its entirety. Another curious thing, you were never supposed to pass associative arrays as arguments through user callback functions, only indexed arrays. By wrapping an associative array in another indexed array, everything works fine.

    It's unclear to me what a proper solution would be for dealing with the way 5.4 works for scheduling events. It probably entails doing the extra array wrapping in code so the plugin and theme developers need not be concerned with the issues. Until then, the extra array is an easy workaround.

  12. bcworkz
    Member
    Posted 7 months ago #

    Uh, never mind??

    We've been looking at this wrong! We were expecting the array we provide to be passed through to our scheduled function. Bad assumption! The array elements are sliced into individual parameters by do_action_ref_array() in wp-cron.php.

    When we add the action that calls our scheduled function we need to specify the priority and number of elements in the array. When we declare our function, we must receive each parameter into a separate variable. If we pass a two element array when we schedule an event, the add action and function declaration may look like this:

    add_action('my_schedule', 'do_my_event', 10, 2);
    function do_my_event( $parm1, $parm2 ) {
        //do stuff...

    It's actually always been this way. Before PHP 5.4, we got away with accepting an array through a single parameter because that parameter was actually a reference pointer to an array. In 5.4, references are no longer passed, so we need to provide variables to copy the array elements into.

  13. Piotr
    Member
    Posted 7 months ago #

    I think I understand. Or at least I get the idea - I'm not completely awake yet, so it's too early to tell. ;) The most important thing is that I managed to use that variable and it works the way I needed, so... another small success. But thank you for taking the time to see what the problem was and explaining it to me. In moments like that I see how much I don't know - it's kind of scary. ;) Then again, two month ago I started with "chapter 1: what is PHP".

    Now I'm moving on to AJAX. To be honest, after reading articles you gave me I have no idea how to create what I need - I mean the form that passes my topic ID that I want to import. I think I understand how it works, but I need to learn about Javascript and Jquery. I'll find some tutorials.

  14. Piotr
    Member
    Posted 7 months ago #

    Ups, I forgot to mark the topic as resolved, I'm sorry. But I'll share my progress while I'm here.

    So... I did it. By using a form and some jQuery/AJAX I'm able to import and/or update specific news wih one click. It's great. ;) And I'm amazed, how many possibilities I now have with the jQuery alone. Even the form validation is great this way. So thank you again for all the help.

    But as I'm planning something bigger now, another section of my site, you'll probably hear from me again in a couple of days. It's going to be articles section with each article formed from a set of forum posts. I still need to wrap my head around it, but it looks like I'll need to dynamically create a form and a set of options for each article that needs to be added. This is going to be complicated...

  15. bcworkz
    Member
    Posted 7 months ago #

    I sense much excitement about your new discoveries! I'm glad to be a part of it. Your next plan sounds intriguing, I'll be glad to help in any way I can. I may not be able to respond right away for the next week or so, but go ahead and ask anyway. Someone else may have an answer, and I will see your posts eventually.

Reply

You must log in to post.

About this Topic