From 3a837ef6bb8445ba8e944741d6f00f763c12e714 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Fri, 7 Jun 2024 21:48:12 +0200 Subject: initial commit --- .gitignore | 1 + Readme.md | 77 ++++++++ composer.json | 15 ++ src/Columns.php | 234 ++++++++++++++++++++++++ src/PostType.php | 535 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Taxonomy.php | 376 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 1238 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 composer.json create mode 100644 src/Columns.php create mode 100644 src/PostType.php create mode 100644 src/Taxonomy.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61ead86 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..372edc1 --- /dev/null +++ b/Readme.md @@ -0,0 +1,77 @@ +# PostTypes + +> Simple WordPress custom post types. + +## Requirements + +* PHP >=7.2 +* [Composer](https://getcomposer.org/) +* [WordPress](https://wordpress.org) >=5.1 + +## Installation + +#### Install with composer + +Run the following in your terminal to install PostTypes with [Composer](https://getcomposer.org/). + +``` +$ composer require dweipert/posttypes +``` + +PostTypes uses [PSR-4](https://www.php-fig.org/psr/psr-4/) autoloading and can be used with the Composer's autoloader. Below is a basic example of getting started, though your setup may be different depending on how you are using Composer. + +```php +require __DIR__ . '/vendor/autoload.php'; + +use PostTypes\PostType; + +$books = new PostType( 'book' ); + +$books->register(); +``` + +See Composer's [basic usage](https://getcomposer.org/doc/01-basic-usage.md#autoloading) guide for details on working with Composer and autoloading. + +## Basic Usage + +Below is a basic example of setting up a simple book post type with a genre taxonomy. + +```php +// Require the Composer autoloader. +require __DIR__ . '/vendor/autoload.php'; + +// Import PostTypes. +use PostTypes\PostType; +use PostTypes\Taxonomy; + +// Create a book post type. +$books = new PostType( 'book' ); + +// Attach the genre taxonomy (which is created below). +$books->taxonomy( 'genre' ); + +// Hide the date and author columns. +$books->columns()->hide( [ 'date', 'author' ] ); + +// Set the Books menu icon. +$books->icon( 'dashicons-book-alt' ); + +// Register the post type to WordPress. +$books->register(); + +// Create a genre taxonomy. +$genres = new Taxonomy( 'genre' ); + +// Set options for the taxonomy. +$genres->options( [ + 'hierarchical' => false, +] ); + +// Register the taxonomy to WordPress. +$genres->register(); +``` + +## Notes + +* The class has no methods for making custom fields for post types, use [Advanced Custom Fields](https://advancedcustomfields.com) +* Maintained under the [Semantic Versioning Guide](https://semver.org) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a20a7ef --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "dweipert/posttypes", + "description": "Simple WordPress custom post types.", + "keywords": ["wordpress", "post-types"], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { + "PostTypes\\": "src/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/src/Columns.php b/src/Columns.php new file mode 100644 index 0000000..51f0cd7 --- /dev/null +++ b/src/Columns.php @@ -0,0 +1,234 @@ +items = $columns; + } + + /** + * Add a new column + * @param string $column the slug of the column + * @param string $label the label for the column + */ + public function add($columns, $label = null) + { + + if (!is_array($columns)) { + $columns = [$columns => $label]; + } + + foreach ($columns as $column => $label) { + if (is_null($label)) { + $label = str_replace(['_', '-'], ' ', ucfirst($column)); + } + + $this->add[$column] = $label; + } + + return $this; + } + + /** + * Add a column to hide + * @param string $column the slug of the column to hdie + */ + public function hide($columns) + { + if (!is_array($columns)) { + $columns = [$columns]; + } + + foreach ($columns as $column) { + $this->hide[] = $column; + } + + return $this; + } + + /** + * Set a custom callback to populate a column + * @param string $column the column slug + * @param mixed $callback callback function + */ + public function populate($column, $callback) + { + $this->populate[$column] = $callback; + + return $this; + } + + /** + * Define the postion for a columns + * @param string $columns an array of columns + */ + public function order($columns) + { + foreach ($columns as $column => $position) { + $this->positions[$column] = $position; + } + + return $this; + } + + /** + * Set columns that are sortable + * @param string $column the slug of the column + * @param string $meta_value the meta_value to orderby + * @param boolean $is_num whether to order by string/number + */ + public function sortable($sortable) + { + foreach ($sortable as $column => $options) { + $this->sortable[$column] = $options; + } + + return $this; + } + + /** + * Check if an orderby field is a custom sort option. + * @param string $orderby the orderby value from query params + */ + public function isSortable($orderby) + { + if (is_string($orderby) && array_key_exists($orderby, $this->sortable)) { + return true; + } + + foreach ($this->sortable as $column => $options) { + if (is_string($options) && $options === $orderby) { + return true; + } + if (is_array($options) && isset($options[0]) && $options[0] === $orderby) { + return true; + } + } + + return false; + } + + /** + * Get meta key for an orderby. + * @param string $orderby the orderby value from query params + */ + public function sortableMeta($orderby) + { + if (array_key_exists($orderby, $this->sortable)) { + return $this->sortable[$orderby]; + } + + foreach ($this->sortable as $column => $options) { + if (is_string($options) && $options === $orderby) { + return $options; + } + if (is_array($options) && isset($options[0]) && $options[0] === $orderby) { + return $options; + } + } + + return ''; + } + + /** + * Modify the columns for the object + * @param array $columns WordPress default columns + * @return array The modified columns + */ + public function modifyColumns($columns) + { + // if user defined set columns, return those + if (!empty($this->items)) { + return $this->items; + } + + // add additional columns + if (!empty($this->add)) { + foreach ($this->add as $key => $label) { + $columns[$key] = $label; + } + } + + // unset hidden columns + if (!empty($this->hide)) { + foreach ($this->hide as $key) { + unset($columns[$key]); + } + } + + // if user has made added custom columns + if (!empty($this->positions)) { + foreach ($this->positions as $key => $position) { + // find index of the element in the array + $index = array_search($key, array_keys($columns)); + // retrieve the element in the array of columns + $item = array_slice($columns, $index, 1); + // remove item from the array + unset($columns[$key]); + + // split columns array into two at the desired position + $start = array_slice($columns, 0, $position, true); + $end = array_slice($columns, $position, count($columns) - 1, true); + + // insert column into position + $columns = $start + $item + $end; + } + } + + return $columns; + } +} diff --git a/src/PostType.php b/src/PostType.php new file mode 100644 index 0000000..97edc02 --- /dev/null +++ b/src/PostType.php @@ -0,0 +1,535 @@ +names($names); + + // assign custom options to the PostType + $this->options($options); + + // assign labels to the PostType + $this->labels($labels); + } + + /** + * Set the names for the PostType + * @param mixed $names A string for the name, or an array of names + * @return $this + */ + public function names($names) + { + // only the post type name is passed + if (is_string($names)) { + $names = ['name' => $names]; + } + + // set the names array + $this->names = $names; + + // create names for the PostType + $this->createNames(); + + return $this; + } + + /** + * Set the options for the PostType + * @param array $options An array of options for the PostType + * @return $this + */ + public function options(array $options) + { + $this->options = $options; + + return $this; + } + + /** + * Set the labels for the PostType + * @param array $labels An array of labels for the PostType + * @return $this + */ + public function labels(array $labels) + { + $this->labels = $labels; + + return $this; + } + + /** + * Add a Taxonomy to the PostType + * @param mixed $taxonomies The Taxonomy name(s) to add + * @return $this + */ + public function taxonomy($taxonomies) + { + $taxonomies = is_string($taxonomies) ? [$taxonomies] : $taxonomies; + + foreach ($taxonomies as $taxonomy) { + $this->taxonomies[] = $taxonomy; + } + + return $this; + } + + /** + * Add filters to the PostType + * @param array $filters An array of Taxonomy filters + * @return $this + */ + public function filters(array $filters) + { + $this->filters = $filters; + + return $this; + } + + /** + * Set the menu icon for the PostType + * @param string $icon A dashicon class for the menu icon + * @return $this + */ + public function icon($icon) + { + $this->icon = $icon; + + return $this; + } + + /** + * Flush rewrite rules + * @link https://codex.wordpress.org/Function_Reference/flush_rewrite_rules + * @param boolean $hard + * @return void + */ + public function flush($hard = true) + { + flush_rewrite_rules($hard); + } + + /** + * Get the Column Manager for the PostType + * @return PostTypes\Columns + */ + public function columns() + { + if (!isset($this->columns)) { + $this->columns = new Columns; + } + + return $this->columns; + } + + /** + * Register the PostType to WordPress + * @return void + */ + public function register() + { + // register the PostType + if (!post_type_exists($this->name)) { + add_action('init', [$this, 'registerPostType']); + } else { + add_filter('register_post_type_args', [$this, 'modifyPostType'], 10, 2); + } + + // register Taxonomies to the PostType + add_action('init', [$this, 'registerTaxonomies']); + + // modify filters on the admin edit screen + add_action('restrict_manage_posts', [$this, 'modifyFilters']); + + if (isset($this->columns)) { + // modify the admin edit columns. + add_filter("manage_{$this->name}_posts_columns", [$this, 'modifyColumns'], 10, 1); + + // populate custom columns + add_filter("manage_{$this->name}_posts_custom_column", [$this, 'populateColumns'], 10, 2); + + // run filter to make columns sortable. + add_filter('manage_edit-'.$this->name.'_sortable_columns', [$this, 'setSortableColumns']); + + // run action that sorts columns on request. + add_action('pre_get_posts', [$this, 'sortSortableColumns']); + } + } + + /** + * Register the PostType + * @return void + */ + public function registerPostType() + { + // create options for the PostType + $options = $this->createOptions(); + + // check that the post type doesn't already exist + if (!post_type_exists($this->name)) { + // register the post type + register_post_type($this->name, $options); + } + } + + /** + * Modify the existing Post Type. + * + * @return array + */ + public function modifyPostType(array $args, string $posttype) + { + if ($posttype !== $this->name) { + return $args; + } + + // create options for the PostType + $options = $this->createOptions(); + + $args = array_replace_recursive($args, $options); + + return $args; + } + + /** + * Create the required names for the PostType + * @return void + */ + public function createNames() + { + // names required for the PostType + $required = [ + 'name', + 'singular', + 'plural', + 'slug', + ]; + + foreach ($required as $key) { + // if the name is set, assign it + if (isset($this->names[$key])) { + $this->$key = $this->names[$key]; + continue; + } + + // if the key is not set and is singular or plural + if (in_array($key, ['singular', 'plural'])) { + // create a human friendly name + $name = ucwords(strtolower(str_replace(['-', '_'], ' ', $this->names['name']))); + } + + if ($key === 'slug') { + // create a slug friendly name + $name = strtolower(str_replace([' ', '_'], '-', $this->names['name'])); + } + + // if is plural or slug, append an 's' + if (in_array($key, ['plural', 'slug'])) { + if (substr($name, strlen($name) - 1, 1) == "y") { + $name = substr($name, 0, strlen($name) - 1) . "ies"; + } else { + $name .= 's'; + } + } + + // asign the name to the PostType property + $this->$key = $name; + } + } + + /** + * Create options for PostType + * @return array Options to pass to register_post_type + */ + public function createOptions() + { + // default options + $options = [ + 'public' => true, + 'rewrite' => [ + 'slug' => $this->slug + ] + ]; + + // replace defaults with the options passed + $options = array_replace_recursive($options, $this->options); + + // create and set labels + if (!isset($options['labels'])) { + $options['labels'] = $this->createLabels(); + } + + // set the menu icon + if (!isset($options['menu_icon']) && isset($this->icon)) { + $options['menu_icon'] = $this->icon; + } + + return $options; + } + + /** + * Create the labels for the PostType + * @return array + */ + public function createLabels() + { + // default labels + $labels = [ + 'name' => $this->plural, + 'singular_name' => $this->singular, + 'menu_name' => $this->plural, + 'all_items' => $this->plural, + 'add_new' => "Erstellen", + 'add_new_item' => "{$this->singular} erstellen", + 'edit_item' => "{$this->singular} bearbeiten", + 'new_item' => "{$this->singular} erstellen", + 'view_item' => "{$this->singular} anschauen", + 'search_items' => "{$this->plural} suchen", + 'not_found' => "Kein {$this->plural} gefunden", + 'not_found_in_trash' => "Es wurden keine {$this->plural} im Papierkorb gefunden.", + 'parent_item_colon' => "Eltern-{$this->singular}:", + ]; + + return array_replace_recursive($labels, $this->labels); + } + + /** + * Register Taxonomies to the PostType + * @return void + */ + public function registerTaxonomies() + { + if (!empty($this->taxonomies)) { + foreach ($this->taxonomies as $taxonomy) { + register_taxonomy_for_object_type($taxonomy, $this->name); + } + } + } + + /** + * Modify and display filters on the admin edit screen + * @param string $posttype The current screen post type + * @return void + */ + public function modifyFilters($posttype) + { + // first check we are working with the this PostType + if ($posttype === $this->name) { + // calculate what filters to add + $filters = $this->getFilters(); + + foreach ($filters as $taxonomy) { + // if the taxonomy doesn't exist, ignore it + if (!taxonomy_exists($taxonomy)) { + continue; + } + + // If the taxonomy is not registered to the post type, continue. + if (!is_object_in_taxonomy($this->name, $taxonomy)) { + continue; + } + + // get the taxonomy object + $tax = get_taxonomy($taxonomy); + + // start the html for the filter dropdown + $selected = null; + + if (isset($_GET[$taxonomy])) { + $selected = sanitize_title($_GET[$taxonomy]); + } + + $dropdown_args = [ + 'name' => $taxonomy, + 'value_field' => 'slug', + 'taxonomy' => $tax->name, + 'show_option_all' => $tax->labels->all_items, + 'hierarchical' => $tax->hierarchical, + 'selected' => $selected, + 'orderby' => 'name', + 'hide_empty' => 0, + 'show_count' => 0, + ]; + + // Output screen reader label. + echo ''; + + // Output dropdown for taxonomy. + wp_dropdown_categories($dropdown_args); + } + } + } + + /** + * Calculate the filters for the PostType + * @return array + */ + public function getFilters() + { + // default filters are empty + $filters = []; + + // if custom filters have been set, use them + if (!is_null($this->filters)) { + return $this->filters; + } + + // if no custom filters have been set, and there are + // Taxonomies assigned to the PostType + if (is_null($this->filters) && !empty($this->taxonomies)) { + // create filters for each taxonomy assigned to the PostType + return $this->taxonomies; + } + + return $filters; + } + + /** + * Modify the columns for the PostType + * @param array $columns Default WordPress columns + * @return array The modified columns + */ + public function modifyColumns($columns) + { + $columns = $this->columns->modifyColumns($columns); + + return $columns; + } + + /** + * Populate custom columns for the PostType + * @param string $column The column slug + * @param int $post_id The post ID + */ + public function populateColumns($column, $post_id) + { + if (isset($this->columns->populate[$column])) { + call_user_func_array($this->columns()->populate[$column], [$column, $post_id]); + } + } + + /** + * Make custom columns sortable + * @param array $columns Default WordPress sortable columns + */ + public function setSortableColumns($columns) + { + if (!empty($this->columns()->sortable)) { + $columns = array_merge($columns, $this->columns()->sortable); + } + + return $columns; + } + + /** + * Set query to sort custom columns + * @param WP_Query $query + */ + public function sortSortableColumns($query) + { + // don't modify the query if we're not in the post type admin + if (!is_admin() || $query->get('post_type') !== $this->name) { + return; + } + + $orderby = $query->get('orderby'); + + // if the sorting a custom column + if ($this->columns()->isSortable($orderby)) { + // get the custom column options + $meta = $this->columns()->sortableMeta($orderby); + + // determine type of ordering + if (is_string($meta) or !$meta[1]) { + $meta_key = $meta; + $meta_value = 'meta_value'; + } else { + $meta_key = $meta[0]; + $meta_value = 'meta_value_num'; + } + + // set the custom order + $query->set('meta_key', $meta_key); + $query->set('orderby', $meta_value); + } + } +} diff --git a/src/Taxonomy.php b/src/Taxonomy.php new file mode 100644 index 0000000..a7aca5b --- /dev/null +++ b/src/Taxonomy.php @@ -0,0 +1,376 @@ +names($names); + + $this->options($options); + + $this->labels($labels); + } + + /** + * Set the names for the Taxonomy + * @param mixed $names The name(s) for the Taxonomy + * @return $this + */ + public function names($names) + { + if (is_string($names)) { + $names = ['name' => $names]; + } + + $this->names = $names; + + // create names for the Taxonomy + $this->createNames(); + + return $this; + } + + /** + * Set options for the Taxonomy + * @param array $options + * @return $this + */ + public function options(array $options = []) + { + $this->options = $options; + + return $this; + } + + /** + * Set the Taxonomy labels + * @param array $labels + * @return $this + */ + public function labels(array $labels = []) + { + $this->labels = $labels; + + return $this; + } + + /** + * Assign a PostType to register the Taxonomy to + * @param mixed $posttypes + * @return $this + */ + public function posttype($posttypes) + { + $posttypes = is_string($posttypes) ? [$posttypes] : $posttypes; + + foreach ($posttypes as $posttype) { + $this->posttypes[] = $posttype; + } + + return $this; + } + + /** + * Get the Column Manager for the Taxonomy + * @return Columns + */ + public function columns() + { + if (!isset($this->columns)) { + $this->columns = new Columns; + } + + return $this->columns; + } + + /** + * Register the Taxonomy to WordPress + * @return void + */ + public function register() + { + // register the taxonomy, set priority to 9 + // so taxonomies are registered before PostTypes + add_action('init', [$this, 'registerTaxonomy'], 9); + + // assign taxonomy to post type objects + add_action('init', [$this, 'registerTaxonomyToObjects']); + + if (isset($this->columns)) { + // modify the columns for the Taxonomy + add_filter("manage_edit-{$this->name}_columns", [$this, 'modifyColumns']); + + // populate the columns for the Taxonomy + add_filter("manage_{$this->name}_custom_column", [$this, 'populateColumns'], 10, 3); + + // set custom sortable columns + add_filter("manage_edit-{$this->name}_sortable_columns", [$this, 'setSortableColumns']); + + // run action that sorts columns on request + add_action('parse_term_query', [$this, 'sortSortableColumns']); + } + } + + /** + * Register the Taxonomy to WordPress + * @return void + */ + public function registerTaxonomy() + { + // Get the existing taxonomy options if it exists. + $options = (taxonomy_exists($this->name)) ? (array) get_taxonomy($this->name) : []; + + // create options for the Taxonomy. + $options = array_replace_recursive($options, $this->createOptions()); + + // register the Taxonomy with WordPress. + register_taxonomy($this->name, null, $options); + } + + /** + * Register the Taxonomy to PostTypes + * @return void + */ + public function registerTaxonomyToObjects() + { + // register Taxonomy to each of the PostTypes assigned + if (!empty($this->posttypes)) { + foreach ($this->posttypes as $posttype) { + register_taxonomy_for_object_type($this->name, $posttype); + } + } + } + + /** + * Create names for the Taxonomy + * @return void + */ + public function createNames() + { + $required = [ + 'name', + 'singular', + 'plural', + 'slug', + ]; + + foreach ($required as $key) { + // if the name is set, assign it + if (isset($this->names[$key])) { + $this->$key = $this->names[$key]; + continue; + } + + // if the key is not set and is singular or plural + if (in_array($key, ['singular', 'plural'])) { + // create a human friendly name + $name = ucwords(strtolower(str_replace(['-', '_'], ' ', $this->names['name']))); + } + + if ($key === 'slug') { + // create a slug friendly name + $name = strtolower(str_replace([' ', '_'], '-', $this->names['name'])); + } + + // if is plural or slug, append an 's' + if (in_array($key, ['plural', 'slug'])) { + $name .= 's'; + } + + // asign the name to the PostType property + $this->$key = $name; + } + } + + /** + * Create options for Taxonomy + * @return array Options to pass to register_taxonomy + */ + public function createOptions() + { + // default options + $options = [ + 'hierarchical' => true, + 'show_admin_column' => true, + 'rewrite' => [ + 'slug' => $this->slug, + ], + ]; + + // replace defaults with the options passed + $options = array_replace_recursive($options, $this->options); + + // create and set labels + if (!isset($options['labels'])) { + $options['labels'] = $this->createLabels(); + } + + return $options; + } + + /** + * Create labels for the Taxonomy + * @return array + */ + public function createLabels() + { + // default labels + $labels = [ + 'name' => $this->plural, + 'singular_name' => $this->singular, + 'menu_name' => $this->plural, + 'all_items' => "Alle {$this->plural}", + 'edit_item' => "{$this->singular} bearbeiten", + 'view_item' => "{$this->singular} anschauen", + 'update_item' => "{$this->singular} aktualisieren", + 'add_new_item' => "{$this->singular} hinzufügen", + 'new_item_name' => "Neuer {$this->singular} Name", + 'parent_item' => "Übergeordnete {$this->plural}", + 'parent_item_colon' => "Übergeordnete {$this->plural}:", + 'search_items' => "{$this->plural} suchen", + 'popular_items' => "Häufig genutzte {$this->plural}", + 'separate_items_with_commas' => "Trenne {$this->plural} mit Komma", + 'add_or_remove_items' => "{$this->plural} hinzufügen oder entfernen", + 'choose_from_most_used' => "Wählen aus den meistgenutzten {$this->plural}", + 'not_found' => "Keine {$this->plural} gefunden", + ]; + + return array_replace($labels, $this->labels); + } + + /** + * Modify the columns for the Taxonomy + * @param array $columns The WordPress default columns + * @return array + */ + public function modifyColumns($columns) + { + $columns = $this->columns->modifyColumns($columns); + + return $columns; + } + + /** + * Populate custom columns for the Taxonomy + * @param string $content + * @param string $column + * @param int $term_id + */ + public function populateColumns($content, $column, $term_id) + { + if (isset($this->columns->populate[$column])) { + $content = call_user_func_array($this->columns()->populate[$column], [$content, $column, $term_id]); + } + + return $content; + } + + /** + * Make custom columns sortable + * @param array $columns Default WordPress sortable columns + */ + public function setSortableColumns($columns) + { + if (!empty($this->columns()->sortable)) { + $columns = array_merge($columns, $this->columns()->sortable); + } + + return $columns; + } + + /** + * Set query to sort custom columns + * @param WP_Term_Query $query + */ + public function sortSortableColumns($query) + { + // don't modify the query if we're not in the post type admin + if (!is_admin() || !in_array($this->name, $query->query_vars['taxonomy'] ?? [])) { + return; + } + + // check the orderby is a custom ordering + if (isset($_GET['orderby']) && array_key_exists($_GET['orderby'], $this->columns()->sortable)) { + // get the custom sorting options + $meta = $this->columns()->sortable[$_GET['orderby']]; + + // check ordering is not numeric + if (is_string($meta)) { + $meta_key = $meta; + $orderby = 'meta_value'; + } else { + $meta_key = $meta[0]; + $orderby = 'meta_value_num'; + } + + // set the sort order + $query->query_vars['orderby'] = $orderby; + $query->query_vars['meta_key'] = $meta_key; + } + } +} -- cgit v1.2.3