diff --git a/README.md b/README.md new file mode 100644 index 0000000..2cb101e --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Gamajo_Registerable + +Register WordPress post types and taxonomies using object-orientated design. + +## Description + +Most implementation of registering post types and taxonomies in WordPress might use classes, but these are little more than poor namespaces. Multiple post types and taxonomies are either all registered via methods in the same class, or contain duplicate code across multiple classes. I wanted to dive into how registration could be structured using something that is closer to real OOP. + +The code seen here is in working use on a live project. + +## Structure + +The main code is under the `Gamajo\Registerable` namespace. + + * `Registerable` - interface for different things that are registerable. Methods include `register()`, `unregister()`, `set_args()` and `get_args()`. + * `Post_Type` - abstract class for registering post types. Implements `Registerable`, so required methods say how a post type should be registered, unregistered and how arguments should be handled. Includes abstract methods for `default_args()` and `messages()` which will contain implementation for specific post types. + * `Taxonomy` - abstract class for registering taxonomies. Implements `Registerable`, so required methods say how a taxonomy should be registered, unregistered and how arguments should be handled. Includes abstract methods for `default_args()` which will contain implementation for specific taxonomies. There is a little duplication here between this and `Post_type` - either the `Registerable` could be changed from an interface to an abstract class to accommodate the common method implementations, or traits could be used if the minimum PHP version was increased from 5.2. + +In the `examples` directory are some example implementations, under a `Gamajo\Meal_Planner` namespace. + * `Post_Type_{post type}` e.g. `Post_Type_Recipe` - specific implementation of a post type, which extends `Gamajo\Registerable\Post_Type`. Only defines the `$post_type` property, and the `default_args()` and `messages()` methods. + * `Taxonomy_{taxonomy}` e.g. `Taxonomy_Recipe_Type` - specific implementation of a taxonomy, which extends `Gamajo\Registerable\Taxonomy`. Only defines the `$taxonomy` property, and the `default_args()` method. + +If you register multiple post types and taxonomies, you can see that only multiple classes that extend the abstract classes are needed, with specific details, creating immutable objects. The boilerplate of registering, unregistering and handling arguments don't need to be repeated. + +## Requirements + * PHP 5.2+ + +## Installation + +This isn't a WordPress plugin on its own, so the usual instructions don't apply. Instead: + +1. Copy the `gamajo` files into your plugin, either manually, or via Composer. +2. Ensure the classes and interfaces are available. You can `require_once` each file, if the structural element doesn't exist, or just use an autoloader (renaming anything as needed if following PSR-4). + +## Usage + +The `examples` files are examples - concrete objects of a post type and taxonomy. The only thing you need to populate for each new post type or taxonomy are the default args, and the messages. At this point, we've only created a specific post type or taxonomy class - since the class isn't instantiated, WordPress won't actually register anything. + +Once the classes exist, instantiating can be done with something like: + +```php +add_action( 'init', 'prefix_mealplanner' ); +/** + * Kickstart the Meal Planner plugin. + */ +function dt_contracts() { + // Register Recipe Type taxonomy. + require plugin_dir_path( __FILE__ ) . 'src/Taxonomy_Recipe_Type.php'; // Or use an autoloader. + global $prefix_taxonomy_recipe_type; + $prefix_taxonomy_recipe_type = new Gamajo\MealPlanner\Taxonomy_Recipe_Type; + $prefix_taxonomy_recipe_type->register(); + + // Register Recipe post type. + require plugin_dir_path( __FILE__ ) . 'src/Post_Type_Recipe.php'; // Or use an autoloader. + global $prefix_post_type_recipe; + $prefix_post_type_recipe = new Gamajo\MealPlanner\Post_Type_Recipe; + $prefix_post_type_recipe->register(); + + register_taxonomy_for_object_type( $prefix_taxonomy_recipe_type->get_taxonomy(), $prefix_post_type_recipe->get_post_type() ); +} +``` + +## Contribute + +Issues and Pull Requests welcomed against the 'develop' branch. All code should follow the WordPress coding standards and be fully documented. + +## License + +MIT, so feel free to amend and use in any personal or commercial projects. + +## Credits + +Built by [Gary Jones](https://twitter.com/GaryJ) +Copyright 2015 [Gamajo Tech](http://gamajo.com/) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9f73ea0 --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name" : "gamajo/registerable", + "description": "A package for your WordPress core plugin, to allow registering post types and taxonomies", + "keywords" : ["wordpress", "register", "custom post type", "custom taxonomy"], + "homepage" : "http://github.com/gamajo/registerable", + "license" : "GPL-2.0+", + "authors" : [ + { + "name" : "Gary Jones", + "email" : "gamajo@gamajo.com", + "homepage": "http://gamajo.com", + "role" : "Developer" + } + ], + "support" : { + "issues": "https://github.com/gamajo/registerable/issues" + }, + "require" : { + "php": ">=5.4", + "ext-filter": "*" + }, + "autoload" : { + "psr-4": { + "Gamajo\\Registerable\\": "src" + } + } +} diff --git a/examples/Post_Type_Recipe.php b/examples/Post_Type_Recipe.php new file mode 100644 index 0000000..b55a685 --- /dev/null +++ b/examples/Post_Type_Recipe.php @@ -0,0 +1,114 @@ + _x( 'Recipes', 'post type general name', 'meal-planner' ), + 'singular_name' => _x( 'Recipe', 'post type singular name', 'meal-planner' ), + 'menu_name' => _x( 'Recipes', 'admin menu', 'meal-planner' ), + 'name_admin_bar' => _x( 'Recipe', 'add new on admin bar', 'meal-planner' ), + 'add_new' => _x( 'Add New', 'mp_recipe', 'meal-planner' ), + 'add_new_item' => __( 'Add New Recipe', 'meal-planner' ), + 'new_item' => __( 'New Recipe', 'meal-planner' ), + 'edit_item' => __( 'Edit Recipe', 'meal-planner' ), + 'view_item' => __( 'View Recipe', 'meal-planner' ), + 'all_items' => __( 'All Recipes', 'meal-planner' ), + 'search_items' => __( 'Search Recipes', 'meal-planner' ), + 'parent_item_colon' => __( 'Parent Recipe:', 'meal-planner' ), + 'not_found' => __( 'No recipes found.', 'meal-planner' ), + 'not_found_in_trash' => __( 'No recipes found in Trash.', 'meal-planner' ), + ]; + + $supports = [ + 'title', + 'thumbnail', + 'revisions', + 'author', + ]; + + $args = [ + 'labels' => $labels, + 'supports' => $supports, + 'public' => true, + 'show_in_nav_menus' => false, + 'rewrite' => [ 'slug' => 'recipe' ], + 'menu_position' => 7, + 'has_archive' => 'recipes', + ]; + + return $args; + } + + /** + * Return post type updated messages. + * + * @since 1.0.0 + * + * @return array Post type updated messages. + */ + public function messages() { + $post = get_post(); + + $revision = $this->get_revision_input(); + + $messages = [ + 0 => '', // Unused. Messages start at index 1. + 1 => __( 'Recipe updated.', 'meal-planner' ), + 2 => __( 'Custom field updated.', 'meal-planner' ), + 3 => __( 'Custom field deleted.', 'meal-planner' ), + 4 => __( 'Recipe updated.', 'meal-planner' ), + /* translators: %s: date and time of the revision */ + 5 => $revision ? sprintf( __( 'Recipe restored to revision from %s', 'meal-planner' ), wp_post_revision_title( $revision, false ) ) : false, + 6 => __( 'Recipe published.', 'meal-planner' ), + 7 => __( 'Recipe saved.', 'meal-planner' ), + 8 => __( 'Recipe submitted.', 'meal-planner' ), + 9 => sprintf( + __( 'Recipe scheduled for: %1$s.', 'meal-planner' ), + /* translators: Publish box date format, see http://php.net/date */ + date_i18n( __( 'M j, Y @ G:i', 'meal-planner' ), strtotime( $post->post_date ) ) + ), + 10 => __( 'Recipe draft updated.', 'meal-planner' ), + 'view' => __( 'View recipe', 'meal-planner' ), + 'preview' => __( 'Preview recipe', 'meal-planner' ), + ]; + + $messages = $this->maybe_add_message_links( $messages, $post ); + + return $messages; + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6499c46 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Gamajo Registerable Examples + +The files in this directory are example implementations of the library classes. Your implementations would go into your `plugins/{your-plugin}/includes` directory, rather than an `examples` directory. diff --git a/examples/Taxonomy_Recipe_Type.php b/examples/Taxonomy_Recipe_Type.php new file mode 100644 index 0000000..5573fb8 --- /dev/null +++ b/examples/Taxonomy_Recipe_Type.php @@ -0,0 +1,71 @@ + __( 'Recipe Types', 'meal-planner' ), + 'singular_name' => __( 'Recipe Type', 'meal-planner' ), + 'menu_name' => __( 'Recipe Types', 'meal-planner' ), + 'edit_item' => __( 'Edit Recipe Type', 'meal-planner' ), + 'update_item' => __( 'Update Recipe Type', 'meal-planner' ), + 'add_new_item' => __( 'Add New Recipe Type', 'meal-planner' ), + 'new_item_name' => __( 'New Recipe Type Name', 'meal-planner' ), + 'parent_item' => __( 'Parent Recipe Type', 'meal-planner' ), + 'parent_item_colon' => __( 'Parent Recipe Type:', 'meal-planner' ), + 'all_items' => __( 'All Recipe Types', 'meal-planner' ), + 'search_items' => __( 'Search Recipe Types', 'meal-planner' ), + 'popular_items' => __( 'Popular Recipe Types', 'meal-planner' ), + 'separate_items_with_commas' => __( 'Separate recipe types with commas', 'meal-planner' ), + 'add_or_remove_items' => __( 'Add or remove recipe types', 'meal-planner' ), + 'choose_from_most_used' => __( 'Choose from the most used recipe types', 'meal-planner' ), + 'not_found' => __( 'No recipe types found.', 'meal-planner' ), + ]; + + $args = [ + 'labels' => $labels, + 'public' => true, + 'show_tagcloud' => true, + 'hierarchical' => false, + 'rewrite' => [ 'slug' => 'recipe_type' ], + 'show_admin_column' => true, + 'query_var' => true, + ]; + + return $args; + } +} diff --git a/src/Post_Type.php b/src/Post_Type.php new file mode 100644 index 0000000..6ff6ad9 --- /dev/null +++ b/src/Post_Type.php @@ -0,0 +1,173 @@ +args ) { + $this->set_args(); + } + + register_post_type( $this->post_type, $this->args ); + } + + /** + * Unregister the post type. + * + * Since there is no unregister_post_type() function, the value is unset from the global instead. + * + * @since 1.0.0 + * + * @global array $wp_post_types + */ + public function unregister() { + global $wp_post_types; + + if ( isset( $wp_post_types[ $this->post_type ] ) ) { + unset( $wp_post_types[ $this->post_type ] ); + } + } + + /** + * Merge any provided arguments with the default ones for a post type. + * + * @since 1.0.0 + * + * @param array $args Post type arguments. + */ + public function set_args( $args = null ) { + $this->args = wp_parse_args( $args, $this->default_args() ); + } + + /** + * Return post type arguments. + * + * @since 1.0.0 + * + * @return array Post type arguments. + */ + public function get_args() { + return $this->args; + } + + /** + * Return post type ID. + * + * @since 1.0.0 + * + * @return string Post type ID. + */ + public function get_post_type() { + return $this->post_type; + } + + /** + * Return post type updated messages. + * + * @since 1.0.0 + * + * @return array Post type updated messages. + */ + abstract public function messages(); + + /** + * Return post type default arguments. + * + * @since 1.0.0 + * + * @return array Post type default arguments. + */ + abstract protected function default_args(); + + /** + * Get the revision ID from the querystring. + * + * Validates as a positive integer. Used for message 5, when restoring + * from a previous revision. + * + * @since 1.0.0 + * + * @return int|bool Positive integer if valid, false otherwise. + */ + protected function get_revision_input() { + return filter_input( INPUT_GET, 'revision', FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); + } + + /** + * Add view or preview links to the end of specific messages. + * + * Only applies if post type is publicly queryable. + * + * @since 1.0.0 + * + * @param array $messages Existing plain text post type messages. + * @param \WP_Post $post Post object. + * @return array Post type messages, maybe with appended links. + */ + protected function maybe_add_message_links( array $messages, $post ) { + $post_type = get_post_type( $post ); + $post_type_object = get_post_type_object( $post_type ); + + if ( ! $post_type_object->publicly_queryable || ! isset( $messages['view'], $messages['preview'] ) ) { + return $messages; + } + + $permalink = get_permalink( $post->ID ); + // get_permalink() can return false. + if ( ! $permalink ) { + return $messages; + } + + $preview_permalink = add_query_arg( 'preview', 'true', $permalink ); + + $view_link = sprintf( ' %s', esc_url( $permalink ), $messages['view'] ); + $preview_link = sprintf( ' %s', esc_url( $preview_permalink ), $messages['preview'] ); + + $messages[1] .= $view_link; + $messages[6] .= $view_link; + $messages[9] .= $view_link; + $messages[8] .= $preview_link; + $messages[10] .= $preview_link; + + return $messages; + } +} diff --git a/src/Registerable.php b/src/Registerable.php new file mode 100644 index 0000000..d47473c --- /dev/null +++ b/src/Registerable.php @@ -0,0 +1,26 @@ +args ) { + $this->set_args(); + } + register_taxonomy( $this->taxonomy, $object_type, $this->args ); + } + + /** + * Unregister the post type. + * + * Since there is no unregister_taxonomy() function, the value is unset from the global instead. + * + * @since 1.0.0 + * + * @global array $wp_taxonomies + */ + public function unregister() { + global $wp_taxonomies; + if ( taxonomy_exists( $this->taxonomy ) ) { + unset( $wp_taxonomies[ $this->taxonomy ] ); + } + // Alternatively, to leave it not registered to any post type: + // register_taxonomy( $this->taxonomy, null ); + } + + /** + * Merge any provided arguments with the default ones for the taxonomy. + * + * @since 1.0.0 + * + * @param array $args Taxonomy arguments. + */ + public function set_args( $args = null ) { + $this->args = wp_parse_args( $args, $this->default_args() ); + } + + /** + * Return taxonomy arguments. + * + * @since 1.0.0 + * + * @return array Post type arguments. + */ + public function get_args() { + return $this->args; + } + + /** + * Return taxonomy ID. + * + * @since 1.0.0 + * + * @return string Taxonomy ID. + */ + public function get_taxonomy() { + return $this->taxonomy; + } + + /** + * Return taxonomy default arguments. + * + * @since 1.0.0 + * + * @return array Taxonomy default arguments. + */ + abstract protected function default_args(); +}