6
\$\begingroup\$

Note: This plugin is / was never intended to be submitted to repo just for personal use but the questions still apply to general Plugin Development.

This plugin listens for $_POSTs from some of my other sites and servers (whose IPs I've replaced with one, two, three). I've listed an example at the bottom but the core of this question is: how can I properly set up a PHP class in the context of a WordPress plugin?

Here is what the class looks like:

<?php

defined( 'ABSPATH' ) or die( 'There was a HOLE here. It\'s gone now.' );

/**
 * This is our The Watcher!
 * S/he monitors all website data and puts it into a nice format for us to view
 * FIELDS
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 * `site_title`         ( @string -         "Some Title"                 )
 * `site_url`           ( @string           "http://www.someurl.com"     )
 * `site_domain`        ( @string           "someurl.com"                )
 * `server_ip`          ( @IP Address       127.0.0.0                    )
 * `site_log`           ( @bool             1 / 0                        )
 * `site_log_size`      ( @int              123                          )
 * `site_mu_flag`       ( @bool             1 / 0                        )
 * `site_robots`        ( @bool             1 / 0                        )
 * `site_version`       ( @version          4.3.2                        )
 * `site_log_contents`  ( @Serialized Array                              )
 * `theme_data`         ( @Serialized Array                              )
 * `site_plugins`       ( @Serialized Array                              )
 * `woo_templates`      ( @Serialized Array                              )
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */
if( ! class_exists( 'The_Watcher' ) ) {
    register_deactivation_hook( __FILE__, array( 'The_Watcher', 'deactivate_watcher' ) );
    add_action( 'plugins_loaded', array( 'The_Watcher', 'init' ) );
    class The_Watcher {


        /**
         * Delete Data / Kill Jobs / Cleanup
         *
         * @return void
         */
        public function deactivate_watcher() {
            wp_clear_scheduled_hook( 'plugin_term_cleanup' );
            // You need to come back to this and do some proper cleanup man...
        }


        /**
         * Initialize Our Watcher Class
         *
         * @return void
         */
        public static function init() {
            global $watcher;
            $watcher = __CLASS__;
            $watcher = new $watcher;
        }


        /**
         * Watcher Construct
         * Initialize all our functions
         * 
         * @return void
         */
        public function __construct() {
            add_action( 'admin_post_tw_listener',       array( $this, 'tw_listener' )                   );
            add_action( 'admin_post_nopriv_tw_listener',array( $this, 'tw_listener' )                   );
            add_action( 'init',                         array( $this, 'tw_plugin_setup' )               );
            add_action( 'template_include',             array( $this, 'tw_templates' )                  );
            add_action( 'pre_get_posts',                array( $this, 'tw_modify_pgp' )                 );
            add_action( 'before_delete_post',           array( $this, 'tw_website_delete_cleanup' )     );
            add_filter( 'intermediate_image_sizes',     array( $this, 'tw_website_image_sizes' ),   998 );
            add_filter( 'cron_schedules',               array( $this, 'tw_add_monthly_schedule' )       ); 
        }


        /**
         * Listen To $_POSTs
         *
         * @return void ( exit() )
         */
        public function tw_listener() {

            // Ensure we at least have the basics
            if( isset( $_POST, $_POST['site_title'], $_POST['site_domain'], $_POST['site_url'] ) && ! empty( $_POST ) && ! empty( $_POST['site_domain'] ) ) {

                // These would be my servers IPs - if the $_POST doesn't come from there BAIL!
                if( ! in_array( $_SERVER['REMOTE_ADDR'], array( 'one', 'two', 'three' ) ) ) {
                    exit();
                }

                $today      = date( 'Y-m-d' );
                $time       = date( 'h:i:sa' );

                // Get our Website IF it Exists
                $website    = get_posts( array(
                    'name'          => sanitize_title( $_POST['site_domain'] ),
                    'post_type'     => 'tw_websites',
                    'post_status'   => 'publish',
                    'posts_per_page'=> 1
                ) );

                // If our website isn't found, we'll create one!
                if( empty( $website ) ) {
                    $site_id = wp_insert_post( array(
                        'post_title'    => sanitize_text_field( $_POST['site_title'] ),
                        'post_name'     => sanitize_title( $_POST['site_domain'] ),
                        'post_status'   => 'publish',
                        'post_type'     => 'tw_websites'
                    ) );
                    $slug    = sanitize_title( $_POST['site_domain'] );
                } else {
                    $site_id = $website[0]->ID;
                    $slug    = $website[0]->post_name;
                }

                // Site Check Conditional
                if( ! is_int( $site_id ) ) {
                    error_log( "::ERROR:: Website {$slug} did not give us a integer for `site_id` which is a major problem - " . print_r( $site_id, 1 ) );
                    exit();
                }

                // If our delete_site boolean is set and true, delete all the websites data
                if( isset( $_POST['delete_site'] ) && 1 == $_POST['delete_site'] ) {
                    $this->delete_website_data( $site_id );
                    exit();
                }

                // Create our Update Post Type
                $update_id  = wp_insert_post( array(
                    'post_title'    => sanitize_text_field( $_POST['site_title'] ) . ' - ' . date( 'F d', strtotime( $today ) ) . " at {$time}",
                    'post_name'     => "webdata-{$slug}-" . date( 'F d', strtotime( $today ) ),
                    'post_status'   => 'publish',
                    'post_type'     => 'tw_updates',
                    'post_parent'   => $site_id
                ) );

                // Update Post Check Conditional
                if( ! is_int( $update_id ) ) {
                    error_log( "::ERROR:: Update Post Creation For {$slug} did not give us a integer for `update_id` which is a major problem - " . print_r( $update_id, 1 ) );
                    exit();
                }

                /**
                 * This part is a huge bit about updating post meta
                 * And looping through arrays
                 * Validating data
                 * Tons of non-fun stuff
                 * So let's just skip it
                 * Maybe replace it with a nice uplifting comment like:
                 * "You're awesome, keep being exactly that!"
                 */

                // Lets Run Cleanup...
                $this->clean_updates( $site_id, $today, $time );
            }

            exit();
        }


        /**
         * Template Includes, Mostly For Server Meta
         * Make sure our server endpoints use a
         * Template just like websites archive
         *
         * @param string $template
         *
         * @return $template 
         */
        public function tw_templates( $template ) {
            global $wp_query;

            $server = $wp_query->get( 'server' );

            if( ! empty( $server ) ) {
                $new_template = locate_template( array( 'archive-tw_websites.php' ) );

                if( ! empty( $new_template ) ) {
                    $template = $new_template;
                }
            }

            return $template;
        }


        /**
         * Modify the main query
         * Usually to account for server meta
         *
         * @param WP_Query Object $query
         *
         * @return void
         */
        public function tw_modify_pgp( $query ) {
            if( is_admin() ) {
                return;
            }

            $queried_server     = $query->get( 'server' );

            if( ! empty( $queried_server ) ) {
                $servers        = array( 'one', 'two', 'three' );   // These are made up - not my real servers :)
                $queried_server = strtolower( sanitize_text_field( $queried_server ) );

                if( in_array( $queried_server, $servers ) ) {
                    $query->set( 'post_type', 'tw_websites' );
                    $query->set( 'meta_key', '_site_server' );
                    $query->set( 'meta_value', "{$queried_server}.domain.net" );
                }
            }
        }


        /**
         * Setup Theme Options like Image Sizes and Such
         * Register All Post Types & Taxonomies
         *
         * @return void
         */
        public function tw_plugin_setup() {

          /** Global Settings **/
            add_image_size( 'screenshot_thumb', 365, 275, true );

          /** Rewrites **/
            add_rewrite_tag( '%server%', '([^/]+)' );
            add_rewrite_rule( 'websites/server/([^/]+)/?', 'index.php?server=$matches[1]', 'top' );

          /** Cron Jobs **/
            if( ! wp_next_scheduled( 'tw_term_cleanup' ) ) {
                wp_schedule_event( time(), 'monthly', 'tw_term_cleanup' );
            }
            add_action( 'tw_term_cleanup', array( $this, 'tw_term_cleanup_callback' ) );

          /** Post Types & Taxonomies **/
            // Websites Post Type
            register_post_type( 'tw_websites', array(
                'labels'            =>  array(
                    'name'          =>      __( 'Websites' ),
                    'singular_name' =>      __( 'Website' ),
                    'all_items'     =>      __( 'View Websites' ),
                    'add_new'       =>      __( 'New Website' ),
                    'add_new_item'  =>      __( 'New Website' ),
                    'edit_item'     =>      __( 'Edit Website' ),
                    'view_item'     =>      __( 'View Website' ),
                    'search_items'  =>      __( 'Search Websites' ),
                    'no_found'      =>      __( 'No Websites Found' ),
                    'not_found_in_trash' => __( 'No Websites in Trash' )
                ),
                'public'            =>  true, 
                'publicly_queryable'=>  true,
                'show_ui'           =>  true, 
                'query_var'         =>  true,
                'show_in_nav_menus' =>  false,
                'hierarchical'      =>  false,
                'capability_type'   =>  'post',
                'has_archive'       =>  'websites',
                'menu_icon'         =>  'dashicons-admin-site',
                'rewrite'           =>  array( 'slug' => 'websites/site', 'with_front' => false, 'ep_mask' => 'EP_SITES' ),
                'supports'          =>  array( 'title', 'editor', 'thumbnail' ),
                'menu_position'     =>  21
            ) );

            // Private Updates Post Type
            register_post_type( 'tw_updates', array(
                'label'             =>  'Website Last Updated',
                'public'            =>  true, 
                'publicly_queryable'=>  true,
                'exclude_from_search'=> true,
                'show_ui'           =>  false, 
                'query_var'         =>  true,
                'show_in_nav_menus' =>  false,
                'hierarchical'      =>  false,
                'rewrite'           =>  array( 'slug' => 'websites/past-data', 'with_front' => false ),
                'capability_type'   =>  'page'
            ) );

            // Private Plugins Taxonomy
            register_taxonomy( 'tw_plugins', 'tw_websites', array(
                'labels'            =>  array(
                    'name'              => __( 'Plugins' ),
                    'singular_name'     => __( 'Plugin' ),
                    'search_items'      => __( 'Search Plugins' ),
                    'all_items'         => __( 'All Plugins' ),
                    'parent_item'       => __( 'Parent Plugin' ),
                    'parent_item_colon' => __( 'Parent Plugin:' ),
                    'edit_item'         => __( 'Edit Plugin' ), 
                    'update_item'       => __( 'Update Plugin' ),
                    'add_new_item'      => __( 'Add New Plugin' ),
                    'new_item_name'     => __( 'New Plugin' ),
                    'menu_name'         => __( 'Plugins' )
                ),
                'public'            =>  true,
                'hierarchical'      =>  true,
                'show_in_nav_menus' =>  false,
                'show_ui'           =>  true,
                'show_admin_column' =>  false,
                'rewrite'           =>  array( 'slug' => 'websites/plugin', 'with_front' => false, 'hierarchical' => true )
            ) );
        }


        /**
         * Used when reading Theme Screenshot to set
         * Screenshot as Website Featured Image.
         * Action removed immediately after use.
         *
         * @param int $attachment_id
         *
         * @return void
         */
        public function tw_ss_as_featured( $attachment_id ) {
            $attachment = get_post( $attachment_id );
            update_post_meta( $attachment->post_parent, '_thumbnail_id', $attachment_id );
        }


        /**
         * Stop WordPress From Making Additional Sizes, Only Make Sizes For What We Need
         * 'medium' Is ALWAYS Needed For Media Library, don't forget that YOU!
         *
         * @param array $image_sizes
         *
         * @return array $image_sizes
         */
        function tw_website_image_sizes( $image_sizes ) {
            $request = array_change_key_case( $_REQUEST, CASE_LOWER );  // WordPress uses post_id and post_ID
            $type    = null;

            if( isset( $request['post_type'] ) ) {
                $type = $request['post_type'];
            } else if( isset( $request['post_id'] ) ) {
                $type = get_post_type( $request['post_id'] );
            } else if ( isset( $request['taxonomy'] ) ) {
                $type = $request['taxonomy'];
            }

            if( ! empty( $type ) ) {
                switch( $type ) {
                    case 'tw_websites':         // Website Screenshots
                        $image_sizes = array( 'medium', 'full', 'screenshot_thumb' );
                      break;
                }
            }

            return $image_sizes;
        }


        /**
         * Update Cron Schedules to add a Monthly Cron Interval
         *
         * @param array $schedules
         *
         * @return array $schedules
         */
        public function tw_add_monthly_schedule( $schedules ) {
            if( ! array_key_exists ( 'monthly', $schedules ) ) {
                $schedules['monthly'] = array(
                    'interval'  => 2635200,
                    'display'   => __('Once A Month')
                );
            }

            return $schedules;
        }


        /**
         * Cleanup Whenever a Website ( `tw_websites` ) is Deleted
         * This specifically removed all Updates
         *
         * @param int $post_id
         *
         * @return void
         */
        public function tw_website_delete_cleanup( $post_id ) {
            global $post_type;
            $type = $post_type;

            // If global $post_type isn't our post type, try to get it by ID
            if( 'tw_websites' != $type ) {
                $type = get_post_field( 'post_type', $post_id );
            }

            // On final check - break out if not the right post type
            if( 'tw_websites' != $type ) {
                return;
            }

            $updates    = new WP_Query( array( 
                'fields'            => 'ids',
                'post_type'         => 'tw_updates',
                'posts_per_page'    => -1,
                'post_parent'       => $post_id,
                'orderby'           => 'post_date',
                'order'             => 'DESC'
            ) );
            $media      = get_children( array(
                'post_parent'   => $post_id,
                'post_type'     => 'attachment'
            ) );

            if ( ! empty( $media ) ) {
                foreach( $media as $file ) {
                    wp_delete_attachment( $file->ID );
                }
            }

            if( ! empty( $updates->posts ) ) {
                foreach( $updates->posts as $update ) {
                    wp_delete_post( $update, true );
                }
            }
        }


        /**
         * Every Month, Remove Terms ( Plugins ) That Do Not Have Posts
         * This Keeps Plugin Versions At A Minimum
         *
         * @return void
         */
        public function tw_term_cleanup_callback() {
            $plugins = get_terms( 'tw_plugins', array(
                'hide_empty'    => false
            ) );

            if( ! empty( $plugins ) && ! is_wp_error( $plugins ) ) {
                foreach( $plugins as $plugin ) {
                    if( 0 == $plugin->count ) {
                        wp_delete_term( $plugin->term_id, 'tw_plugins' );
                    }
                }
            } else {
                error_log( 'Deactivate Watcher - Plugins Empty or Error...' );
            }
        }


        /**
         * Format Bytes to Human Readable Data
         *
         * @param int $bytes
         * @param int $precision = 2
         *
         * @return float
         */
        public function format_bytes( $bytes = 0, $precision = 2 ) { 
            if( $bytes >= 1073741824 ) {
                $bytes = number_format( $bytes / 1073741824, $precision ) . ' GB';
            } elseif( $bytes >= 1048576 ) {
                $bytes = number_format( $bytes / 1048576, $precision ) . ' MB';
            } elseif ( $bytes >= 1024 ) {
                $bytes = number_format( $bytes / 1024, $precision ) . ' KB';
            } else {
                $bytes = $bytes . ' B';
            }

            return $bytes;
        }


        /**
         * Return if Given Post Has Errors
         *
         * @param Post Object $post
         *
         * @return bool || string
         */
        public function has_error( $post, $plugin_slug = false ) { 
            if( is_object( $post ) && property_exists( $post, 'ID' ) ) {
                $post_id = $post->ID;
            } elseif( is_numeric( $post ) ) {
                $post_id = absint( $post );
            } else {
                return false;
            }

            $screenshot = get_post_meta( $post_id, '_thumbnail_id',             true );
            $log        = get_post_meta( $post_id, '_site_log',                 true );
            $mu_flag    = get_post_meta( $post_id, '_site_mu_flag',             true );
            $robots     = get_post_meta( $post_id, '_site_robots',              true );
            $wooOutdated= get_post_meta( $post_id, '_woo_templates_outdated',   true );

            if(     empty( $screenshot )    ) { return 'Screenshot'; }
            if(     $log                    ) { return 'Debug Log'; }
            if(     $mu_flag                ) { return 'MU Error'; }
            if(     ! $robots               ) { return 'Robots Disabled'; }
            if(     $wooOutdated            ) { return 'Woo Templates'; }

            return false; 
        }


        /**
         * Get Plugin Info From Repository - Cache Data in Base
         *
         * @param Term Object $term
         *
         * @return bool / Object $plugin_info
        */
        public function get_plugin_data( $term = '', $force = false ) {
            if( ! is_object( $term ) ) {
                return false;
            } elseif( is_object( $term ) && ( ! isset( $term->taxonomy ) || 'tw_plugins' != $term->taxonomy ) ) {
                return false;
            }

            $plugin_info = get_transient( "watcher_{$term->slug}" );

            if( false === $plugin_info ) {
                $api_url        = 'http://api.wordpress.org/plugins/info/1.0/';
                $api_request    = array(
                    'action'    => 'plugin_information',
                    'timeout'   => 30,
                    'request'   => serialize( (object) array( 
                            'slug'  => $term->slug
                        )
                    )
                );
                $plugin_info    = wp_remote_post( $api_url, array( 'body' => $api_request ) );

                if( 'N;' == $plugin_info['body'] ) {
                    return false;
                } elseif( ! is_wp_error( $plugin_info ) ) {
                    set_transient( "watcher_{$term->slug}", maybe_unserialize( wp_remote_retrieve_body( $plugin_info ) ), HOUR_IN_SECONDS );
                } else {
                    error_log( 'WP Error occurred in `get_plugin_data()` function - Error:' . $plugin_info->get_error_message() );
                    error_log( 'WP Error occurred in `get_plugin_data()` function - Passed Term: ' . print_r( $term, 1 ) );
                    return false;
                }
            }

            return ( is_array( $plugin_info ) ) ? maybe_unserialize( $plugin_info['body'] ) : $plugin_info;
        }


        /**
         * Make sure we keep our limit or 10 posts
         *
         * @param int $parent_id
         * @param string $today
         * @param string $time
         *
         * @return void
         */
        private function clean_updates( $parent_id, $today, $time ) {
            if( is_int( $parent_id ) ) {
                $updates = new WP_Query( array(
                    'fields'            => 'ids',
                    'posts_per_page'    => -1,
                    'post_type'         => 'tw_updates',
                    'post_status'       => 'any',
                    'orderby'           => 'post_date',
                    'order'             => 'ASC',
                    'post_parent'       => $parent_id,
                ) );

                if( ! empty( $updates->posts ) && count( $updates->posts ) > 10 ) {
                    $diff = count( $updates->posts ) - 10;

                    while( $diff-- ) {
                        $update = array_shift( $updates->posts );

                        $images = new WP_Query( array(
                            'fields'            => 'ids',
                            'posts_per_page'    => -1,
                            'post_type'         => 'attachment',
                            'post_status'       => 'any',
                            'post_parent'       => $update,
                        ) );

                        foreach( $images->posts as $img ) {
                            wp_delete_attachment( $img, true );
                        }

                        wp_delete_post( $update, true );
                    }

                    wp_reset_postdata();
                }
            } else {
                error_log( "Cannot perform Update Post Type clean up. Given Parent ID `{$parent_id}` is not an integer. Ran on {$today} at {$time}." );
            }
        }


        /**
         * The Watch Supplier Function ( Feeder ) was uninstalled
         * On a specific site. Delete all data associated with it
         *
         * @param int $post_id
         *
         * @return void
         */
        private function delete_website_data( $post_id ) {
            wp_delete_post( $post_id, true );
        }

    } // END Watcher Class
} // END Watcher Class Check Conditional

There are a ton of tutorials out there as to how to properly set up a Plugin for WordPress and OOP best practices but I just wanted to confirm I was setting things up correctly.

Here's a list of questions I was looking to get addressed:

  • Since functions have to be public so WordPress can access them via hooks, how can I keep my actual object from access these hooked functions as methods? I don't want users to accidentally $watcher->tw_website_delete_cleanup() for example but I want my method $watcher->has_error() to be accessible.
  • Is it a better idea to have my object accessible via global or make the user initialize it when needed?

This is a snippet example to post to the plugin with the necessary data:

$post['site_title'] = get_bloginfo( 'name' );
$post['site_url']       = get_bloginfo( 'wpurl' );
$post['site_domain']    = stripslashes(
                            preg_replace( 
                                '/^www\./i',
                                '', 
                                preg_replace(
                                    '(^https?://)',
                                    '',
                                    $post['site_url']
                                )
                            )
                        );

$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, 'http://domain.com/wp-admin/admin-post.php?action=tw_listener' );
curl_setopt( $ch, CURLOPT_POST, 1 );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $post );
curl_exec( $ch );
curl_close( $ch );
\$\endgroup\$
  • \$\begingroup\$ This is ok, Probably what I would do is break it down into several classes. Have this one (which is like a router or the Main class). This shouldn't handle any business logic. It should be the part that sets up the other parts so they can handle it. Then group your functionality in logical ways. So an example is pre_get_posts and before_delete_post etc. These deal with posts, so make a class to take care of all things "post". Then do one for "cron_schedules" etc. That way your working with SOLID principals en.wikipedia.org/wiki/SOLID \$\endgroup\$ – ArtisticPhoenix Mar 15 at 19:05
  • \$\begingroup\$ When you go to fix a bug or something, if something in a post doesn't work as expected, you'll know right where to look. Instead of doing ctrl+f and trying to remember what you named something. This also lets you do things like Abstract classes Interfaces and Inheritance (if this was more complex) \$\endgroup\$ – ArtisticPhoenix Mar 15 at 19:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.